Add a detach command to detach without terminating (#21490)
* Add a detach command to detach without terminating (#21376)
* Add a detach command to detach without terminating
Fixes #21154.
* Bump protocol version for app.detach
* Tweak to detach/quit text
* Change logPrefix to named param
* Fix the text that the devicelab attach test looks for
diff --git a/dev/devicelab/bin/tasks/flutter_attach_test.dart b/dev/devicelab/bin/tasks/flutter_attach_test.dart
index a3defc5..64c8990 100644
--- a/dev/devicelab/bin/tasks/flutter_attach_test.dart
+++ b/dev/devicelab/bin/tasks/flutter_attach_test.dart
@@ -33,7 +33,7 @@
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 a407c8a..5e2c70a 100644
--- a/packages/flutter_tools/doc/daemon.md
+++ b/packages/flutter_tools/doc/daemon.md
@@ -92,6 +92,12 @@
- `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 @@
#### 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 @@
#### 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 @@
- Commands
- [`restart`](#apprestart)
- [`callServiceExtension`](#appcallserviceextension)
+ - [`detach`](#appdetach)
- [`stop`](#appstop)
- Events
- [`start`](#appstart)
@@ -219,6 +226,7 @@
## 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 274f45b..9fd00a4 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -28,7 +28,7 @@
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 @@
registerHandler('restart', restart);
registerHandler('callServiceExtension', callServiceExtension);
registerHandler('stop', stop);
+ registerHandler('detach', detach);
}
static final Uuid _uuidGenerator = new Uuid();
@@ -516,6 +517,23 @@
});
}
+ Future<bool> detach(Map<String, dynamic> 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<bool>((_) {
+ return true;
+ }).catchError((dynamic error) {
+ _sendAppEvent(app, 'log', <String, dynamic>{ '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 @@
}
Future<Null> stop() => runner.stop();
+ Future<Null> 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 395d26c..40a3340 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -66,6 +66,7 @@
final bool benchmarkMode;
final File applicationBinary;
final bool hostIsIde;
+ bool _didAttach = false;
Set<String> _dartDependencies;
final String dillOutputPath;
@@ -152,6 +153,7 @@
Completer<void> appStartedCompleter,
String viewFilter,
}) async {
+ _didAttach = true;
try {
await connectToServiceProtocol(viewFilter: viewFilter,
reloadSources: _reloadSourcesService,
@@ -751,18 +753,25 @@
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<Null> 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 2a6e104..b31d5f8 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: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 @@
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 892c275..fb350f4 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 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<String> _stdout = new StreamController<String>.broadcast();
@@ -49,7 +51,7 @@
msg.length > maxLength ? msg.substring(0, maxLength) + '...' : msg;
_allMessages.add(truncatedMsg);
if (_printJsonAndStderr) {
- print(truncatedMsg);
+ print('$_logPrefix$truncatedMsg');
}
return msg;
}
@@ -162,6 +164,31 @@
_throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
}
+ Future<int> 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<void>(<Future<void>>[
+ _proc.exitCode,
+ _sendRequest(
+ 'app.detach',
+ <String, dynamic>{'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<int> stop() async {
if (vmService != null) {
_debugPrint('Closing VM service');