Unverified Commit 1fb94fb8 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Noninteractive iOS debugger session (#68145)

parent 72798aaf
...@@ -468,12 +468,10 @@ class IOSDevice extends Device { ...@@ -468,12 +468,10 @@ class IOSDevice extends Device {
packageName: FlutterProject.current().manifest.appName, packageName: FlutterProject.current().manifest.appName,
); );
if (localUri == null) { if (localUri == null) {
iosDeployDebugger?.detach();
return LaunchResult.failed(); return LaunchResult.failed();
} }
return LaunchResult.succeeded(observatoryUri: localUri); return LaunchResult.succeeded(observatoryUri: localUri);
} on ProcessException catch (e) { } on ProcessException catch (e) {
iosDeployDebugger?.detach();
_logger.printError(e.message); _logger.printError(e.message);
return LaunchResult.failed(); return LaunchResult.failed();
} finally { } finally {
...@@ -486,12 +484,7 @@ class IOSDevice extends Device { ...@@ -486,12 +484,7 @@ class IOSDevice extends Device {
IOSApp app, { IOSApp app, {
String userIdentifier, String userIdentifier,
}) async { }) async {
// If the debugger is not attached, killing the ios-deploy process won't stop the app. return iosDeployDebugger?.exit();
if (iosDeployDebugger!= null && iosDeployDebugger.debuggerAttached) {
// Avoid null.
return iosDeployDebugger?.exit() == true;
}
return false;
} }
@override @override
...@@ -723,8 +716,8 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -723,8 +716,8 @@ class IOSDeviceLogReader extends DeviceLogReader {
} }
void logMessage(vm_service.Event event) { void logMessage(vm_service.Event event) {
if (_iosDeployDebugger != null && _iosDeployDebugger.debuggerAttached) { if (_iosDeployDebugger != null) {
// Prefer the more complete logs from the attached debugger. // Prefer the more complete logs from the debugger.
return; return;
} }
final String message = processVmServiceMessage(event); final String message = processVmServiceMessage(event);
...@@ -739,7 +732,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -739,7 +732,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
]); ]);
} }
/// Log reader will listen to [debugger.logLines] and will detach debugger on dispose. /// Log reader will listen to [debugger.logLines].
set debuggerStream(IOSDeployDebugger debugger) { set debuggerStream(IOSDeployDebugger debugger) {
// Logging is gathered from syslog on iOS 13 and earlier. // Logging is gathered from syslog on iOS 13 and earlier.
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) { if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
...@@ -818,7 +811,6 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -818,7 +811,6 @@ class IOSDeviceLogReader extends DeviceLogReader {
loggingSubscription.cancel(); loggingSubscription.cancel();
} }
_idevicesyslogProcess?.kill(); _idevicesyslogProcess?.kill();
_iosDeployDebugger?.detach();
} }
} }
......
...@@ -118,14 +118,13 @@ class IOSDeploy { ...@@ -118,14 +118,13 @@ class IOSDeploy {
/// Returns [IOSDeployDebugger] wrapping attached debugger logic. /// Returns [IOSDeployDebugger] wrapping attached debugger logic.
/// ///
/// This method does not install the app. Call [IOSDeployDebugger.launchAndAttach()] /// This method does not install the app. Call [IOSDeployDebugger.launchAndAttach()]
/// to install and attach the debugger to the specified app bundle. /// to install the specified app bundle.
IOSDeployDebugger prepareDebuggerForLaunch({ IOSDeployDebugger prepareDebuggerForLaunch({
@required String deviceId, @required String deviceId,
@required String bundlePath, @required String bundlePath,
@required List<String> launchArguments, @required List<String> launchArguments,
@required IOSDeviceInterface interfaceType, @required IOSDeviceInterface interfaceType,
}) { }) {
// Interactive debug session to support sending the lldb detach command.
final List<String> launchCommand = <String>[ final List<String> launchCommand = <String>[
'script', 'script',
'-t', '-t',
...@@ -137,6 +136,7 @@ class IOSDeploy { ...@@ -137,6 +136,7 @@ class IOSDeploy {
'--bundle', '--bundle',
bundlePath, bundlePath,
'--debug', '--debug',
'--noninteractive',
if (interfaceType != IOSDeviceInterface.network) if (interfaceType != IOSDeviceInterface.network)
'--no-wifi', '--no-wifi',
if (launchArguments.isNotEmpty) ...<String>[ if (launchArguments.isNotEmpty) ...<String>[
...@@ -224,7 +224,7 @@ enum _IOSDeployDebuggerState { ...@@ -224,7 +224,7 @@ enum _IOSDeployDebuggerState {
attached, attached,
} }
/// Wrapper to launch app and attach the debugger with ios-deploy. /// Wrapper to launch app with the debugger with ios-deploy.
class IOSDeployDebugger { class IOSDeployDebugger {
IOSDeployDebugger({ IOSDeployDebugger({
@required Logger logger, @required Logger logger,
...@@ -264,15 +264,16 @@ class IOSDeployDebugger { ...@@ -264,15 +264,16 @@ class IOSDeployDebugger {
Stream<String> get logLines => _debuggerOutput.stream; Stream<String> get logLines => _debuggerOutput.stream;
final StreamController<String> _debuggerOutput = StreamController<String>.broadcast(); final StreamController<String> _debuggerOutput = StreamController<String>.broadcast();
bool get debuggerAttached => _debuggerState == _IOSDeployDebuggerState.attached;
_IOSDeployDebuggerState _debuggerState; _IOSDeployDebuggerState _debuggerState;
// (lldb) run // (lldb) run
// https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51 // https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run'); static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run');
// (lldb) run // (lldb) autoexit
// https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51 // hhttps://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L61
static final RegExp _lldbAutoexit = RegExp(r'\(lldb\)\s*autoexit');
// From lldb on exit.
static final RegExp _lldbProcessExit = RegExp(r'Process \d* exited with status ='); static final RegExp _lldbProcessExit = RegExp(r'Process \d* exited with status =');
/// Launch the app on the device, and attach the debugger. /// Launch the app on the device, and attach the debugger.
...@@ -295,6 +296,7 @@ class IOSDeployDebugger { ...@@ -295,6 +296,7 @@ class IOSDeployDebugger {
// (lldb) run // (lldb) run
// success // success
// (lldb) autoexit
// 2020-09-15 13:42:25.185474-0700 Runner[477:181141] flutter: Observatory listening on http://127.0.0.1:57782/ // 2020-09-15 13:42:25.185474-0700 Runner[477:181141] flutter: Observatory listening on http://127.0.0.1:57782/
if (_lldbRun.hasMatch(line)) { if (_lldbRun.hasMatch(line)) {
_logger.printTrace(line); _logger.printTrace(line);
...@@ -324,7 +326,7 @@ class IOSDeployDebugger { ...@@ -324,7 +326,7 @@ class IOSDeployDebugger {
} }
return; return;
} }
if (_debuggerState != _IOSDeployDebuggerState.attached) { if (_debuggerState != _IOSDeployDebuggerState.attached || _lldbAutoexit.hasMatch(line)) {
_logger.printTrace(line); _logger.printTrace(line);
return; return;
} }
...@@ -378,21 +380,6 @@ class IOSDeployDebugger { ...@@ -378,21 +380,6 @@ class IOSDeployDebugger {
_iosDeployProcess = null; _iosDeployProcess = null;
return success; return success;
} }
void detach() {
if (!debuggerAttached) {
return;
}
try {
// Detach lldb from the app process.
_iosDeployProcess?.stdin?.writeln('process detach');
_debuggerState = _IOSDeployDebuggerState.detached;
} on SocketException catch (error) {
// Best effort, try to detach, but maybe the app already exited or already detached.
_logger.printTrace('Could not detach from debugger: $error');
}
}
} }
// Maps stdout line stream. Must return original line. // Maps stdout line stream. Must return original line.
......
...@@ -3,10 +3,8 @@ ...@@ -3,10 +3,8 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
...@@ -50,6 +48,7 @@ void main () { ...@@ -50,6 +48,7 @@ void main () {
'--bundle', '--bundle',
'/', '/',
'--debug', '--debug',
'--noninteractive',
'--args', '--args',
<String>[ <String>[
'--enable-dart-profiling', '--enable-dart-profiling',
...@@ -87,7 +86,7 @@ void main () { ...@@ -87,7 +86,7 @@ void main () {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>['ios-deploy'], command: <String>['ios-deploy'],
stdout: '(lldb) run\r\nsuccess\r\nsuccess\r\nLog on attach1\r\n\r\nLog on attach2\r\n\r\n\r\n\r\nPROCESS_STOPPED\r\nLog after process exit', stdout: '(lldb) run\r\nsuccess\r\n(lldb) autoexit\r\nsuccess\r\nLog on attach1\r\n\r\nLog on attach2\r\n\r\n\r\n\r\nPROCESS_STOPPED\r\nLog after process exit',
), ),
]); ]);
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
...@@ -111,7 +110,7 @@ void main () { ...@@ -111,7 +110,7 @@ void main () {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>['ios-deploy'], command: <String>['ios-deploy'],
stdout: '(lldb) run\r\nsuccess\r\nLog on attach\r\nProcess 100 exited with status = 0\r\nLog after process exit', stdout: '(lldb) run\r\nsuccess\r\n(lldb) autoexit\r\nLog on attach\r\nProcess 100 exited with status = 0\r\nLog after process exit',
), ),
]); ]);
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
...@@ -226,26 +225,6 @@ void main () { ...@@ -226,26 +225,6 @@ void main () {
expect(logger.errorText, contains('Could not attach the debugger')); expect(logger.errorText, contains('Could not attach the debugger'));
}); });
}); });
testWithoutContext('detach', () async {
final StreamController<List<int>> stdin = StreamController<List<int>>();
final Stream<String> stdinStream = stdin.stream.transform<String>(const Utf8Decoder());
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>[
'ios-deploy',
],
stdout: '(lldb) run\nsuccess',
stdin: IOSink(stdin.sink),
),
]);
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
processManager: processManager,
);
await iosDeployDebugger.launchAndAttach();
iosDeployDebugger.detach();
expect(await stdinStream.first, 'process detach');
});
}); });
group('IOSDeploy.uninstallApp', () { group('IOSDeploy.uninstallApp', () {
......
...@@ -239,7 +239,6 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt ...@@ -239,7 +239,6 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
)); ));
final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger(); final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
when(iosDeployDebugger.debuggerAttached).thenReturn(true);
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[ final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'Message from debugger' 'Message from debugger'
...@@ -304,24 +303,6 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt ...@@ -304,24 +303,6 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
await streamComplete.future; await streamComplete.future;
}); });
testWithoutContext('detaches debugger', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
useSyslog: false,
);
final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => const Stream<String>.empty());
logReader.debuggerStream = iosDeployDebugger;
logReader.dispose();
verify(iosDeployDebugger.detach());
});
}); });
} }
......
...@@ -92,6 +92,7 @@ const FakeCommand kAttachDebuggerCommand = FakeCommand(command: <String>[ ...@@ -92,6 +92,7 @@ const FakeCommand kAttachDebuggerCommand = FakeCommand(command: <String>[
'--bundle', '--bundle',
'/', '/',
'--debug', '--debug',
'--noninteractive',
'--no-wifi', '--no-wifi',
'--args', '--args',
'--enable-dart-profiling --enable-service-port-fallback --disable-service-auth-codes --observatory-port=60700 --enable-checked-mode --verify-entry-points' '--enable-dart-profiling --enable-service-port-fallback --disable-service-auth-codes --observatory-port=60700 --enable-checked-mode --verify-entry-points'
...@@ -99,7 +100,7 @@ const FakeCommand kAttachDebuggerCommand = FakeCommand(command: <String>[ ...@@ -99,7 +100,7 @@ const FakeCommand kAttachDebuggerCommand = FakeCommand(command: <String>[
'PATH': '/usr/bin:null', 'PATH': '/usr/bin:null',
'DYLD_LIBRARY_PATH': '/path/to/libraries', 'DYLD_LIBRARY_PATH': '/path/to/libraries',
}, },
stdout: '(lldb) run\nsuccess', stdout: '(lldb) run\nsuccess\n(lldb) autoexit',
); );
void main() { void main() {
...@@ -166,7 +167,6 @@ void main() { ...@@ -166,7 +167,6 @@ void main() {
expect(launchResult.started, true); expect(launchResult.started, true);
expect(launchResult.hasObservatory, true); expect(launchResult.hasObservatory, true);
verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1); verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1);
expect(await device.stopApp(iosApp), false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => MockUsage(), Usage: () => MockUsage(),
}); });
...@@ -216,7 +216,6 @@ void main() { ...@@ -216,7 +216,6 @@ void main() {
expect(launchResult.started, true); expect(launchResult.started, true);
expect(launchResult.hasObservatory, true); expect(launchResult.hasObservatory, true);
verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1); verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1);
expect(await device.stopApp(iosApp), false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => MockUsage(), Usage: () => MockUsage(),
}); });
...@@ -302,7 +301,6 @@ void main() { ...@@ -302,7 +301,6 @@ void main() {
expect(launchResult.started, true); expect(launchResult.started, true);
expect(launchResult.hasObservatory, false); expect(launchResult.hasObservatory, false);
expect(await device.stopApp(iosApp), false);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => MockUsage(), Usage: () => MockUsage(),
...@@ -325,6 +323,7 @@ void main() { ...@@ -325,6 +323,7 @@ void main() {
'--bundle', '--bundle',
'/', '/',
'--debug', '--debug',
'--noninteractive',
'--no-wifi', '--no-wifi',
// The arguments below are determined by what is passed into // The arguments below are determined by what is passed into
// the debugging options argument to startApp. // the debugging options argument to startApp.
...@@ -406,67 +405,10 @@ void main() { ...@@ -406,67 +405,10 @@ void main() {
); );
expect(launchResult.started, true); expect(launchResult.started, true);
expect(await device.stopApp(iosApp), false);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Usage: () => MockUsage(), Usage: () => MockUsage(),
}); });
// Still uses context for analytics.
testUsingContext(
'IOSDevice.startApp detaches lldb when VM service connection fails',
() async {
final FileSystem fileSystem = MemoryFileSystem.test();
final MockIOSDeploy mockIOSDeploy = MockIOSDeploy();
final MockIOSDeployDebugger mockIOSDeployDebugger = MockIOSDeployDebugger();
when(mockIOSDeploy.prepareDebuggerForLaunch(
deviceId: anyNamed('deviceId'),
bundlePath: anyNamed('bundlePath'),
launchArguments: anyNamed('launchArguments'),
interfaceType: anyNamed('interfaceType')))
.thenReturn(mockIOSDeployDebugger);
when(mockIOSDeploy.installApp(
deviceId: anyNamed('deviceId'),
bundlePath: anyNamed('bundlePath'),
launchArguments: anyNamed('launchArguments'),
interfaceType: anyNamed('interfaceType')))
.thenAnswer((_) async => 0);
when(mockIOSDeployDebugger.launchAndAttach()).thenAnswer((_) async => true);
final IOSDevice device = setUpIOSDevice(
fileSystem: fileSystem,
iosDeploy: mockIOSDeploy,
vmServiceConnector: (String string, {Log log}) async {
throw const io.SocketException(
'OS Error: Connection refused, errno = 61, address = localhost, port '
'= 58943',
);
},
);
final IOSApp iosApp = PrebuiltIOSApp(
projectBundleId: 'app',
bundleName: 'Runner',
bundleDir: fileSystem.currentDirectory,
);
device.portForwarder = const NoOpDevicePortForwarder();
device.setLogReader(iosApp, FakeDeviceLogReader());
final LaunchResult launchResult = await device.startApp(
iosApp,
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
platformArgs: <String, dynamic>{},
fallbackPollingDelay: Duration.zero,
fallbackThrottleTimeout: const Duration(milliseconds: 10),
);
expect(launchResult.started, false);
verify(mockIOSDeployDebugger.detach()).called(1);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
});
} }
IOSDevice setUpIOSDevice({ IOSDevice setUpIOSDevice({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment