diff --git a/dev/devicelab/bin/tasks/flutter_attach_test.dart b/dev/devicelab/bin/tasks/flutter_attach_test.dart index a3defc5895..64c89902f8 100644 --- a/dev/devicelab/bin/tasks/flutter_attach_test.dart +++ b/dev/devicelab/bin/tasks/flutter_attach_test.dart @@ -33,7 +33,7 @@ Future testReload(Process process, { Future Function() onListening } stdout.add(line); if (line.contains('Waiting') && onListening != null) listening.complete(onListening()); - if (line.contains('To quit, press "q".')) + if (line.contains('To detach, press "d"; to quit, press "q".')) ready.complete(); if (line.contains('Reloaded ')) reloaded.complete(); diff --git a/packages/flutter_tools/doc/daemon.md b/packages/flutter_tools/doc/daemon.md index a407c8afb6..5e2c70ad54 100644 --- a/packages/flutter_tools/doc/daemon.md +++ b/packages/flutter_tools/doc/daemon.md @@ -92,6 +92,12 @@ The `callServiceExtension()` allows clients to make arbitrary calls to service p - `methodName`: the name of the service protocol extension to invoke; this is required. - `params`: an optional Map of parameters to pass to the service protocol extension. +#### app.detach + +The `detach()` command takes one parameter, `appId`. It returns a `bool` to indicate success or failure in detaching from an app without stopping it. + +- `appId`: the id of a previously started app; this is required. + #### app.stop The `stop()` command takes one parameter, `appId`. It returns a `bool` to indicate success or failure in stopping an app. @@ -110,7 +116,7 @@ This is sent when an observatory port is available for a started app. The `param #### app.started -This is sent once the application launch process is complete and the app is either paused before main() (if `startPaused` is true) or main() has begun running. The `params` field will be a map containing the field `appId`. +This is sent once the application launch process is complete and the app is either paused before main() (if `startPaused` is true) or main() has begun running. When attaching, this even will be fired once attached. The `params` field will be a map containing the field `appId`. #### app.log @@ -122,7 +128,7 @@ This is sent when an operation starts and again when it stops. When an operation #### app.stop -This is sent when an app is stopped. The `params` field will be a map with the field `appId`. +This is sent when an app is stopped or detached from. The `params` field will be a map with the field `appId`. ### device domain @@ -204,6 +210,7 @@ The following subset of the app domain is available in `flutter run --machine`. - Commands - [`restart`](#apprestart) - [`callServiceExtension`](#appcallserviceextension) + - [`detach`](#appdetach) - [`stop`](#appstop) - Events - [`start`](#appstart) @@ -219,6 +226,7 @@ See the [source](https://github.com/flutter/flutter/blob/master/packages/flutter ## Changelog +- 0.4.2: Added `app.detach` command - 0.4.1: Added `flutter attach --machine` - 0.4.0: Added `emulator.create` command - 0.3.0: Added `daemon.connected` event at startup diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 274f45b713..9fd00a4c96 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -28,7 +28,7 @@ import '../runner/flutter_command.dart'; import '../tester/flutter_tester.dart'; import '../vmservice.dart'; -const String protocolVersion = '0.4.1'; +const String protocolVersion = '0.4.2'; /// A server process command. This command will start up a long-lived server. /// It reads JSON-RPC based commands from stdin, executes them, and returns @@ -316,6 +316,7 @@ class AppDomain extends Domain { registerHandler('restart', restart); registerHandler('callServiceExtension', callServiceExtension); registerHandler('stop', stop); + registerHandler('detach', detach); } static final Uuid _uuidGenerator = new Uuid(); @@ -516,6 +517,23 @@ class AppDomain extends Domain { }); } + Future detach(Map args) async { + final String appId = _getStringArg(args, 'appId', required: true); + + final AppInstance app = _getApp(appId); + if (app == null) + throw "app '$appId' not found"; + + return app.detach().timeout(const Duration(seconds: 5)).then((_) { + return true; + }).catchError((dynamic error) { + _sendAppEvent(app, 'log', { 'log': '$error', 'error': true }); + app.closeLogger(); + _apps.remove(app); + return false; + }); + } + AppInstance _getApp(String id) { return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null); } @@ -769,6 +787,7 @@ class AppInstance { } Future stop() => runner.stop(); + Future detach() => runner.detach(); void closeLogger() { _logger.close(); diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 395d26c679..40a3340285 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -66,6 +66,7 @@ class HotRunner extends ResidentRunner { final bool benchmarkMode; final File applicationBinary; final bool hostIsIde; + bool _didAttach = false; Set _dartDependencies; final String dillOutputPath; @@ -152,6 +153,7 @@ class HotRunner extends ResidentRunner { Completer appStartedCompleter, String viewFilter, }) async { + _didAttach = true; try { await connectToServiceProtocol(viewFilter: viewFilter, reloadSources: _reloadSourcesService, @@ -751,18 +753,25 @@ class HotRunner extends ResidentRunner { for (Uri uri in device.observatoryUris) printStatus('An Observatory debugger and profiler on $dname is available at: $uri'); } + final String quitMessage = _didAttach + ? 'To detach, press "d"; to quit, press "q".' + : 'To quit, press "q".'; if (details) { printHelpDetails(); - printStatus('To repeat this help message, press "h". To quit, press "q".'); + printStatus('To repeat this help message, press "h". $quitMessage'); } else { - printStatus('For a more detailed help message, press "h". To quit, press "q".'); + printStatus('For a more detailed help message, press "h". $quitMessage'); } } @override Future cleanupAfterSignal() async { await stopEchoingDeviceLog(); - await stopApp(); + if (_didAttach) { + appFinished(); + } else { + await stopApp(); + } } @override diff --git a/packages/flutter_tools/test/integration/flutter_attach_test.dart b/packages/flutter_tools/test/integration/flutter_attach_test.dart index 2a6e104cb5..b31d5f896f 100644 --- a/packages/flutter_tools/test/integration/flutter_attach_test.dart +++ b/packages/flutter_tools/test/integration/flutter_attach_test.dart @@ -6,7 +6,6 @@ import 'package:file/file.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import '../src/common.dart'; -import '../src/context.dart'; import 'test_data/basic_project.dart'; import 'test_driver.dart'; @@ -18,24 +17,36 @@ void main() { setUp(() async { tempDir = fs.systemTempDirectory.createTempSync('flutter_attach_test.'); await _project.setUpIn(tempDir); - _flutterRun = new FlutterTestDriver(tempDir); - _flutterAttach = new FlutterTestDriver(tempDir); + _flutterRun = new FlutterTestDriver(tempDir, logPrefix: 'RUN'); + _flutterAttach = new FlutterTestDriver(tempDir, logPrefix: 'ATTACH'); }); tearDown(() async { - // We can't call stop() on both of these because they'll both try to stop the - // same app. Just quit the attach process and then send a stop to the original - // process. + await _flutterAttach.detach(); await _flutterRun.stop(); - await _flutterAttach.quit(); tryToDelete(tempDir); }); group('attached process', () { - testUsingContext('can hot reload', () async { + test('can hot reload', () async { await _flutterRun.run(withDebugger: true); await _flutterAttach.attach(_flutterRun.vmServicePort); await _flutterAttach.hotReload(); }); + test('can detach, reattach, hot reload', () async { + await _flutterRun.run(withDebugger: true); + await _flutterAttach.attach(_flutterRun.vmServicePort); + await _flutterAttach.detach(); + await _flutterAttach.attach(_flutterRun.vmServicePort); + await _flutterAttach.hotReload(); + }); + test('killing process behaves the same as detach ', () async { + await _flutterRun.run(withDebugger: true); + await _flutterAttach.attach(_flutterRun.vmServicePort); + await _flutterAttach.quit(); + _flutterAttach = new FlutterTestDriver(tempDir, logPrefix: 'ATTACH-2'); + await _flutterAttach.attach(_flutterRun.vmServicePort); + await _flutterAttach.hotReload(); + }); }, timeout: const Timeout.factor(6)); } diff --git a/packages/flutter_tools/test/integration/test_driver.dart b/packages/flutter_tools/test/integration/test_driver.dart index 892c275b45..fb350f4348 100644 --- a/packages/flutter_tools/test/integration/test_driver.dart +++ b/packages/flutter_tools/test/integration/test_driver.dart @@ -23,9 +23,11 @@ const Duration appStartTimeout = Duration(seconds: 120); const Duration quitTimeout = Duration(seconds: 10); class FlutterTestDriver { - FlutterTestDriver(this._projectFolder); + FlutterTestDriver(this._projectFolder, {String logPrefix}): + this._logPrefix = logPrefix != null ? '$logPrefix: ' : ''; final Directory _projectFolder; + final String _logPrefix; Process _proc; int _procPid; final StreamController _stdout = new StreamController.broadcast(); @@ -49,7 +51,7 @@ class FlutterTestDriver { msg.length > maxLength ? msg.substring(0, maxLength) + '...' : msg; _allMessages.add(truncatedMsg); if (_printJsonAndStderr) { - print(truncatedMsg); + print('$_logPrefix$truncatedMsg'); } return msg; } @@ -162,6 +164,31 @@ class FlutterTestDriver { _throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed'); } + Future detach() async { + if (vmService != null) { + _debugPrint('Closing VM service'); + await vmService.close() + .timeout(quitTimeout, + onTimeout: () { _debugPrint('VM Service did not quit within $quitTimeout'); }); + } + if (_currentRunningAppId != null) { + _debugPrint('Detaching from app'); + await Future.any(>[ + _proc.exitCode, + _sendRequest( + 'app.detach', + {'appId': _currentRunningAppId} + ), + ]).timeout( + quitTimeout, + onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); } + ); + _currentRunningAppId = null; + } + _debugPrint('Waiting for process to end'); + return _proc.exitCode.timeout(quitTimeout, onTimeout: _killGracefully); + } + Future stop() async { if (vmService != null) { _debugPrint('Closing VM service');