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 {
packageName: FlutterProject.current().manifest.appName,
);
if (localUri == null) {
iosDeployDebugger?.detach();
return LaunchResult.failed();
}
return LaunchResult.succeeded(observatoryUri: localUri);
} on ProcessException catch (e) {
iosDeployDebugger?.detach();
_logger.printError(e.message);
return LaunchResult.failed();
} finally {
......@@ -486,12 +484,7 @@ class IOSDevice extends Device {
IOSApp app, {
String userIdentifier,
}) async {
// If the debugger is not attached, killing the ios-deploy process won't stop the app.
if (iosDeployDebugger!= null && iosDeployDebugger.debuggerAttached) {
// Avoid null.
return iosDeployDebugger?.exit() == true;
}
return false;
return iosDeployDebugger?.exit();
}
@override
......@@ -723,8 +716,8 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
void logMessage(vm_service.Event event) {
if (_iosDeployDebugger != null && _iosDeployDebugger.debuggerAttached) {
// Prefer the more complete logs from the attached debugger.
if (_iosDeployDebugger != null) {
// Prefer the more complete logs from the debugger.
return;
}
final String message = processVmServiceMessage(event);
......@@ -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) {
// Logging is gathered from syslog on iOS 13 and earlier.
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
......@@ -818,7 +811,6 @@ class IOSDeviceLogReader extends DeviceLogReader {
loggingSubscription.cancel();
}
_idevicesyslogProcess?.kill();
_iosDeployDebugger?.detach();
}
}
......
......@@ -118,14 +118,13 @@ class IOSDeploy {
/// Returns [IOSDeployDebugger] wrapping attached debugger logic.
///
/// 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({
@required String deviceId,
@required String bundlePath,
@required List<String> launchArguments,
@required IOSDeviceInterface interfaceType,
}) {
// Interactive debug session to support sending the lldb detach command.
final List<String> launchCommand = <String>[
'script',
'-t',
......@@ -137,6 +136,7 @@ class IOSDeploy {
'--bundle',
bundlePath,
'--debug',
'--noninteractive',
if (interfaceType != IOSDeviceInterface.network)
'--no-wifi',
if (launchArguments.isNotEmpty) ...<String>[
......@@ -224,7 +224,7 @@ enum _IOSDeployDebuggerState {
attached,
}
/// Wrapper to launch app and attach the debugger with ios-deploy.
/// Wrapper to launch app with the debugger with ios-deploy.
class IOSDeployDebugger {
IOSDeployDebugger({
@required Logger logger,
......@@ -264,15 +264,16 @@ class IOSDeployDebugger {
Stream<String> get logLines => _debuggerOutput.stream;
final StreamController<String> _debuggerOutput = StreamController<String>.broadcast();
bool get debuggerAttached => _debuggerState == _IOSDeployDebuggerState.attached;
_IOSDeployDebuggerState _debuggerState;
// (lldb) run
// 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');
// (lldb) run
// https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51
// (lldb) autoexit
// 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 =');
/// Launch the app on the device, and attach the debugger.
......@@ -295,6 +296,7 @@ class IOSDeployDebugger {
// (lldb) run
// success
// (lldb) autoexit
// 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)) {
_logger.printTrace(line);
......@@ -324,7 +326,7 @@ class IOSDeployDebugger {
}
return;
}
if (_debuggerState != _IOSDeployDebuggerState.attached) {
if (_debuggerState != _IOSDeployDebuggerState.attached || _lldbAutoexit.hasMatch(line)) {
_logger.printTrace(line);
return;
}
......@@ -378,21 +380,6 @@ class IOSDeployDebugger {
_iosDeployProcess = null;
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.
......
......@@ -3,10 +3,8 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
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/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
......@@ -50,6 +48,7 @@ void main () {
'--bundle',
'/',
'--debug',
'--noninteractive',
'--args',
<String>[
'--enable-dart-profiling',
......@@ -87,7 +86,7 @@ void main () {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
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(
......@@ -111,7 +110,7 @@ void main () {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
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(
......@@ -226,26 +225,6 @@ void main () {
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', () {
......
......@@ -239,7 +239,6 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
));
final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger();
when(iosDeployDebugger.debuggerAttached).thenReturn(true);
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'Message from debugger'
......@@ -304,24 +303,6 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
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>[
'--bundle',
'/',
'--debug',
'--noninteractive',
'--no-wifi',
'--args',
'--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>[
'PATH': '/usr/bin:null',
'DYLD_LIBRARY_PATH': '/path/to/libraries',
},
stdout: '(lldb) run\nsuccess',
stdout: '(lldb) run\nsuccess\n(lldb) autoexit',
);
void main() {
......@@ -166,7 +167,6 @@ void main() {
expect(launchResult.started, true);
expect(launchResult.hasObservatory, true);
verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1);
expect(await device.stopApp(iosApp), false);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
});
......@@ -216,7 +216,6 @@ void main() {
expect(launchResult.started, true);
expect(launchResult.hasObservatory, true);
verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1);
expect(await device.stopApp(iosApp), false);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
});
......@@ -302,7 +301,6 @@ void main() {
expect(launchResult.started, true);
expect(launchResult.hasObservatory, false);
expect(await device.stopApp(iosApp), false);
expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
......@@ -325,6 +323,7 @@ void main() {
'--bundle',
'/',
'--debug',
'--noninteractive',
'--no-wifi',
// The arguments below are determined by what is passed into
// the debugging options argument to startApp.
......@@ -406,67 +405,10 @@ void main() {
);
expect(launchResult.started, true);
expect(await device.stopApp(iosApp), false);
expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
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({
......
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