mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
1254 lines
36 KiB
Dart
1254 lines
36 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:meta/meta.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import '../base/common.dart';
|
|
import '../base/context.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/terminal.dart';
|
|
import '../base/utils.dart';
|
|
import '../build_info.dart';
|
|
import '../cache.dart';
|
|
import '../convert.dart';
|
|
import '../device.dart';
|
|
import '../emulator.dart';
|
|
import '../globals.dart' as globals;
|
|
import '../project.dart';
|
|
import '../resident_runner.dart';
|
|
import '../run_cold.dart';
|
|
import '../run_hot.dart';
|
|
import '../runner/flutter_command.dart';
|
|
import '../web/web_runner.dart';
|
|
|
|
const String protocolVersion = '0.5.3';
|
|
|
|
/// 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
|
|
/// JSON-RPC based responses and events to stdout.
|
|
///
|
|
/// It can be shutdown with a `daemon.shutdown` command (or by killing the
|
|
/// process).
|
|
class DaemonCommand extends FlutterCommand {
|
|
DaemonCommand({ this.hidden = false });
|
|
|
|
@override
|
|
final String name = 'daemon';
|
|
|
|
@override
|
|
final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.';
|
|
|
|
@override
|
|
final bool hidden;
|
|
|
|
@override
|
|
Future<FlutterCommandResult> runCommand() async {
|
|
globals.printStatus('Starting device daemon...');
|
|
isRunningFromDaemon = true;
|
|
|
|
final NotifyingLogger notifyingLogger = NotifyingLogger();
|
|
|
|
Cache.releaseLockEarly();
|
|
|
|
await context.run<void>(
|
|
body: () async {
|
|
final Daemon daemon = Daemon(
|
|
stdinCommandStream, stdoutCommandResponse,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
final int code = await daemon.onExit;
|
|
if (code != 0) {
|
|
throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
|
|
}
|
|
},
|
|
overrides: <Type, Generator>{
|
|
Logger: () => notifyingLogger,
|
|
},
|
|
);
|
|
return FlutterCommandResult.success();
|
|
}
|
|
}
|
|
|
|
typedef DispatchCommand = void Function(Map<String, dynamic> command);
|
|
|
|
typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args);
|
|
|
|
class Daemon {
|
|
Daemon(
|
|
Stream<Map<String, dynamic>> commandStream,
|
|
this.sendCommand, {
|
|
this.notifyingLogger,
|
|
this.logToStdout = false,
|
|
}) {
|
|
// Set up domains.
|
|
_registerDomain(daemonDomain = DaemonDomain(this));
|
|
_registerDomain(appDomain = AppDomain(this));
|
|
_registerDomain(deviceDomain = DeviceDomain(this));
|
|
_registerDomain(emulatorDomain = EmulatorDomain(this));
|
|
|
|
// Start listening.
|
|
_commandSubscription = commandStream.listen(
|
|
_handleRequest,
|
|
onDone: () {
|
|
if (!_onExitCompleter.isCompleted) {
|
|
_onExitCompleter.complete(0);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
DaemonDomain daemonDomain;
|
|
AppDomain appDomain;
|
|
DeviceDomain deviceDomain;
|
|
EmulatorDomain emulatorDomain;
|
|
StreamSubscription<Map<String, dynamic>> _commandSubscription;
|
|
int _outgoingRequestId = 1;
|
|
final Map<String, Completer<dynamic>> _outgoingRequestCompleters = <String, Completer<dynamic>>{};
|
|
|
|
final DispatchCommand sendCommand;
|
|
final NotifyingLogger notifyingLogger;
|
|
final bool logToStdout;
|
|
|
|
final Completer<int> _onExitCompleter = Completer<int>();
|
|
final Map<String, Domain> _domainMap = <String, Domain>{};
|
|
|
|
void _registerDomain(Domain domain) {
|
|
_domainMap[domain.name] = domain;
|
|
}
|
|
|
|
Future<int> get onExit => _onExitCompleter.future;
|
|
|
|
void _handleRequest(Map<String, dynamic> request) {
|
|
// {id, method, params}
|
|
|
|
// [id] is an opaque type to us.
|
|
final dynamic id = request['id'];
|
|
|
|
if (id == null) {
|
|
globals.stdio.stderrWrite('no id for request: $request\n');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final String method = request['method'] as String;
|
|
if (method != null) {
|
|
if (!method.contains('.')) {
|
|
throw 'method not understood: $method';
|
|
}
|
|
|
|
final String prefix = method.substring(0, method.indexOf('.'));
|
|
final String name = method.substring(method.indexOf('.') + 1);
|
|
if (_domainMap[prefix] == null) {
|
|
throw 'no domain for method: $method';
|
|
}
|
|
|
|
_domainMap[prefix].handleCommand(name, id, castStringKeyedMap(request['params']) ?? const <String, dynamic>{});
|
|
} else {
|
|
// If there was no 'method' field then it's a response to a daemon-to-editor request.
|
|
final Completer<dynamic> completer = _outgoingRequestCompleters[id.toString()];
|
|
if (completer == null) {
|
|
throw 'unexpected response with id: $id';
|
|
}
|
|
_outgoingRequestCompleters.remove(id.toString());
|
|
|
|
if (request['error'] != null) {
|
|
completer.completeError(request['error']);
|
|
} else {
|
|
completer.complete(request['result']);
|
|
}
|
|
}
|
|
} on Exception catch (error, trace) {
|
|
_send(<String, dynamic>{
|
|
'id': id,
|
|
'error': _toJsonable(error),
|
|
'trace': '$trace',
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<dynamic> sendRequest(String method, [ dynamic args ]) {
|
|
final Map<String, dynamic> map = <String, dynamic>{'method': method};
|
|
if (args != null) {
|
|
map['params'] = _toJsonable(args);
|
|
}
|
|
|
|
final int id = _outgoingRequestId++;
|
|
final Completer<dynamic> completer = Completer<dynamic>();
|
|
|
|
map['id'] = id.toString();
|
|
_outgoingRequestCompleters[id.toString()] = completer;
|
|
|
|
_send(map);
|
|
return completer.future;
|
|
}
|
|
|
|
void _send(Map<String, dynamic> map) => sendCommand(map);
|
|
|
|
void shutdown({ dynamic error }) {
|
|
_commandSubscription?.cancel();
|
|
for (final Domain domain in _domainMap.values) {
|
|
domain.dispose();
|
|
}
|
|
if (!_onExitCompleter.isCompleted) {
|
|
if (error == null) {
|
|
_onExitCompleter.complete(0);
|
|
} else {
|
|
_onExitCompleter.completeError(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
abstract class Domain {
|
|
Domain(this.daemon, this.name);
|
|
|
|
|
|
final Daemon daemon;
|
|
final String name;
|
|
final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
|
|
|
|
void registerHandler(String name, CommandHandler handler) {
|
|
_handlers[name] = handler;
|
|
}
|
|
|
|
@override
|
|
String toString() => name;
|
|
|
|
void handleCommand(String command, dynamic id, Map<String, dynamic> args) {
|
|
Future<dynamic>.sync(() {
|
|
if (_handlers.containsKey(command)) {
|
|
return _handlers[command](args);
|
|
}
|
|
throw 'command not understood: $name.$command';
|
|
}).then<dynamic>((dynamic result) {
|
|
if (result == null) {
|
|
_send(<String, dynamic>{'id': id});
|
|
} else {
|
|
_send(<String, dynamic>{'id': id, 'result': _toJsonable(result)});
|
|
}
|
|
}).catchError((dynamic error, dynamic trace) {
|
|
_send(<String, dynamic>{
|
|
'id': id,
|
|
'error': _toJsonable(error),
|
|
'trace': '$trace',
|
|
});
|
|
});
|
|
}
|
|
|
|
void sendEvent(String name, [ dynamic args ]) {
|
|
final Map<String, dynamic> map = <String, dynamic>{'event': name};
|
|
if (args != null) {
|
|
map['params'] = _toJsonable(args);
|
|
}
|
|
_send(map);
|
|
}
|
|
|
|
void _send(Map<String, dynamic> map) => daemon._send(map);
|
|
|
|
String _getStringArg(Map<String, dynamic> args, String name, { bool required = false }) {
|
|
if (required && !args.containsKey(name)) {
|
|
throw '$name is required';
|
|
}
|
|
final dynamic val = args[name];
|
|
if (val != null && val is! String) {
|
|
throw '$name is not a String';
|
|
}
|
|
return val as String;
|
|
}
|
|
|
|
bool _getBoolArg(Map<String, dynamic> args, String name, { bool required = false }) {
|
|
if (required && !args.containsKey(name)) {
|
|
throw '$name is required';
|
|
}
|
|
final dynamic val = args[name];
|
|
if (val != null && val is! bool) {
|
|
throw '$name is not a bool';
|
|
}
|
|
return val as bool;
|
|
}
|
|
|
|
int _getIntArg(Map<String, dynamic> args, String name, { bool required = false }) {
|
|
if (required && !args.containsKey(name)) {
|
|
throw '$name is required';
|
|
}
|
|
final dynamic val = args[name];
|
|
if (val != null && val is! int) {
|
|
throw '$name is not an int';
|
|
}
|
|
return val as int;
|
|
}
|
|
|
|
void dispose() { }
|
|
}
|
|
|
|
/// This domain responds to methods like [version] and [shutdown].
|
|
///
|
|
/// This domain fires the `daemon.logMessage` event.
|
|
class DaemonDomain extends Domain {
|
|
DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
|
|
registerHandler('version', version);
|
|
registerHandler('shutdown', shutdown);
|
|
registerHandler('getSupportedPlatforms', getSupportedPlatforms);
|
|
|
|
sendEvent(
|
|
'daemon.connected',
|
|
<String, dynamic>{
|
|
'version': protocolVersion,
|
|
'pid': pid,
|
|
},
|
|
);
|
|
|
|
_subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
|
|
if (daemon.logToStdout) {
|
|
if (message.level == 'status') {
|
|
// We use `print()` here instead of `stdout.writeln()` in order to
|
|
// capture the print output for testing.
|
|
print(message.message);
|
|
} else if (message.level == 'error') {
|
|
globals.stdio.stderrWrite('${message.message}\n');
|
|
if (message.stackTrace != null) {
|
|
globals.stdio.stderrWrite(
|
|
'${message.stackTrace.toString().trimRight()}\n',
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
if (message.stackTrace != null) {
|
|
sendEvent('daemon.logMessage', <String, dynamic>{
|
|
'level': message.level,
|
|
'message': message.message,
|
|
'stackTrace': message.stackTrace.toString(),
|
|
});
|
|
} else {
|
|
sendEvent('daemon.logMessage', <String, dynamic>{
|
|
'level': message.level,
|
|
'message': message.message,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
StreamSubscription<LogMessage> _subscription;
|
|
|
|
Future<String> version(Map<String, dynamic> args) {
|
|
return Future<String>.value(protocolVersion);
|
|
}
|
|
|
|
/// Sends a request back to the client asking it to expose/tunnel a URL.
|
|
///
|
|
/// This method should only be called if the client opted-in with the
|
|
/// --web-allow-expose-url switch. The client may return the same URL back if
|
|
/// tunnelling is not required for a given URL.
|
|
Future<String> exposeUrl(String url) async {
|
|
final dynamic res = await daemon.sendRequest('app.exposeUrl', <String, String>{'url': url});
|
|
if (res is Map<String, dynamic> && res['url'] is String) {
|
|
return res['url'] as String;
|
|
} else {
|
|
globals.printError('Invalid response to exposeUrl - params should include a String url field');
|
|
return url;
|
|
}
|
|
}
|
|
|
|
Future<void> shutdown(Map<String, dynamic> args) {
|
|
Timer.run(daemon.shutdown);
|
|
return Future<void>.value();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_subscription?.cancel();
|
|
}
|
|
|
|
/// Enumerates the platforms supported by the provided project.
|
|
///
|
|
/// This does not filter based on the current workflow restrictions, such
|
|
/// as whether command line tools are installed or whether the host platform
|
|
/// is correct.
|
|
Future<Map<String, Object>> getSupportedPlatforms(Map<String, dynamic> args) async {
|
|
final String projectRoot = _getStringArg(args, 'projectRoot', required: true);
|
|
final List<String> result = <String>[];
|
|
try {
|
|
// TODO(jonahwilliams): replace this with a project metadata check once
|
|
// that has been implemented.
|
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot));
|
|
if (flutterProject.linux.existsSync()) {
|
|
result.add('linux');
|
|
}
|
|
if (flutterProject.macos.existsSync()) {
|
|
result.add('macos');
|
|
}
|
|
if (flutterProject.windows.existsSync()) {
|
|
result.add('windows');
|
|
}
|
|
if (flutterProject.ios.existsSync()) {
|
|
result.add('ios');
|
|
}
|
|
if (flutterProject.android.existsSync()) {
|
|
result.add('android');
|
|
}
|
|
if (flutterProject.web.existsSync()) {
|
|
result.add('web');
|
|
}
|
|
if (flutterProject.fuchsia.existsSync()) {
|
|
result.add('fuchsia');
|
|
}
|
|
return <String, Object>{
|
|
'platforms': result,
|
|
};
|
|
} on Exception catch (err, stackTrace) {
|
|
sendEvent('log', <String, dynamic>{
|
|
'log': 'Failed to parse project metadata',
|
|
'stackTrace': stackTrace.toString(),
|
|
'error': true,
|
|
});
|
|
// On any sort of failure, fall back to Android and iOS for backwards
|
|
// comparability.
|
|
return <String, Object>{
|
|
'platforms': <String>[
|
|
'android',
|
|
'ios',
|
|
],
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
typedef _RunOrAttach = Future<void> Function({
|
|
Completer<DebugConnectionInfo> connectionInfoCompleter,
|
|
Completer<void> appStartedCompleter,
|
|
});
|
|
|
|
/// This domain responds to methods like [start] and [stop].
|
|
///
|
|
/// It fires events for application start, stop, and stdout and stderr.
|
|
class AppDomain extends Domain {
|
|
AppDomain(Daemon daemon) : super(daemon, 'app') {
|
|
registerHandler('restart', restart);
|
|
registerHandler('reloadMethod', reloadMethod);
|
|
registerHandler('callServiceExtension', callServiceExtension);
|
|
registerHandler('stop', stop);
|
|
registerHandler('detach', detach);
|
|
}
|
|
|
|
static final Uuid _uuidGenerator = Uuid();
|
|
|
|
static String _getNewAppId() => _uuidGenerator.v4();
|
|
|
|
final List<AppInstance> _apps = <AppInstance>[];
|
|
|
|
Future<AppInstance> startApp(
|
|
Device device,
|
|
String projectDirectory,
|
|
String target,
|
|
String route,
|
|
DebuggingOptions options,
|
|
bool enableHotReload, {
|
|
File applicationBinary,
|
|
@required bool trackWidgetCreation,
|
|
String projectRootPath,
|
|
String packagesFilePath,
|
|
String dillOutputPath,
|
|
bool ipv6 = false,
|
|
String isolateFilter,
|
|
}) async {
|
|
if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator) {
|
|
throw Exception('${toTitleCase(options.buildInfo.friendlyModeName)} mode is not supported for emulators.');
|
|
}
|
|
// We change the current working directory for the duration of the `start` command.
|
|
final Directory cwd = globals.fs.currentDirectory;
|
|
globals.fs.currentDirectory = globals.fs.directory(projectDirectory);
|
|
final FlutterProject flutterProject = FlutterProject.current();
|
|
|
|
final FlutterDevice flutterDevice = await FlutterDevice.create(
|
|
device,
|
|
flutterProject: flutterProject,
|
|
viewFilter: isolateFilter,
|
|
target: target,
|
|
buildInfo: options.buildInfo,
|
|
);
|
|
|
|
ResidentRunner runner;
|
|
|
|
if (await device.targetPlatform == TargetPlatform.web_javascript) {
|
|
runner = webRunnerFactory.createWebRunner(
|
|
flutterDevice,
|
|
flutterProject: flutterProject,
|
|
target: target,
|
|
debuggingOptions: options,
|
|
ipv6: ipv6,
|
|
stayResident: true,
|
|
urlTunneller: options.webEnableExposeUrl ? daemon.daemonDomain.exposeUrl : null,
|
|
);
|
|
} else if (enableHotReload) {
|
|
runner = HotRunner(
|
|
<FlutterDevice>[flutterDevice],
|
|
target: target,
|
|
debuggingOptions: options,
|
|
applicationBinary: applicationBinary,
|
|
projectRootPath: projectRootPath,
|
|
packagesFilePath: packagesFilePath,
|
|
dillOutputPath: dillOutputPath,
|
|
ipv6: ipv6,
|
|
hostIsIde: true,
|
|
);
|
|
} else {
|
|
runner = ColdRunner(
|
|
<FlutterDevice>[flutterDevice],
|
|
target: target,
|
|
debuggingOptions: options,
|
|
applicationBinary: applicationBinary,
|
|
ipv6: ipv6,
|
|
);
|
|
}
|
|
|
|
return launch(
|
|
runner,
|
|
({
|
|
Completer<DebugConnectionInfo> connectionInfoCompleter,
|
|
Completer<void> appStartedCompleter,
|
|
}) {
|
|
return runner.run(
|
|
connectionInfoCompleter: connectionInfoCompleter,
|
|
appStartedCompleter: appStartedCompleter,
|
|
route: route,
|
|
);
|
|
},
|
|
device,
|
|
projectDirectory,
|
|
enableHotReload,
|
|
cwd,
|
|
LaunchMode.run,
|
|
);
|
|
}
|
|
|
|
Future<AppInstance> launch(
|
|
ResidentRunner runner,
|
|
_RunOrAttach runOrAttach,
|
|
Device device,
|
|
String projectDirectory,
|
|
bool enableHotReload,
|
|
Directory cwd,
|
|
LaunchMode launchMode,
|
|
) async {
|
|
final AppInstance app = AppInstance(_getNewAppId(),
|
|
runner: runner, logToStdout: daemon.logToStdout);
|
|
_apps.add(app);
|
|
_sendAppEvent(app, 'start', <String, dynamic>{
|
|
'deviceId': device.id,
|
|
'directory': projectDirectory,
|
|
'supportsRestart': isRestartSupported(enableHotReload, device),
|
|
'launchMode': launchMode.toString(),
|
|
});
|
|
|
|
Completer<DebugConnectionInfo> connectionInfoCompleter;
|
|
|
|
if (runner.debuggingEnabled) {
|
|
connectionInfoCompleter = Completer<DebugConnectionInfo>();
|
|
// We don't want to wait for this future to complete and callbacks won't fail.
|
|
// As it just writes to stdout.
|
|
unawaited(connectionInfoCompleter.future.then<void>(
|
|
(DebugConnectionInfo info) {
|
|
final Map<String, dynamic> params = <String, dynamic>{
|
|
// The web vmservice proxy does not have an http address.
|
|
'port': info.httpUri?.port ?? info.wsUri.port,
|
|
'wsUri': info.wsUri.toString(),
|
|
};
|
|
if (info.baseUri != null) {
|
|
params['baseUri'] = info.baseUri;
|
|
}
|
|
_sendAppEvent(app, 'debugPort', params);
|
|
},
|
|
));
|
|
}
|
|
final Completer<void> appStartedCompleter = Completer<void>();
|
|
// We don't want to wait for this future to complete, and callbacks won't fail,
|
|
// as it just writes to stdout.
|
|
unawaited(appStartedCompleter.future.then<void>((void value) {
|
|
_sendAppEvent(app, 'started');
|
|
}));
|
|
|
|
await app._runInZone<void>(this, () async {
|
|
try {
|
|
await runOrAttach(
|
|
connectionInfoCompleter: connectionInfoCompleter,
|
|
appStartedCompleter: appStartedCompleter,
|
|
);
|
|
_sendAppEvent(app, 'stop');
|
|
} on Exception catch (error, trace) {
|
|
_sendAppEvent(app, 'stop', <String, dynamic>{
|
|
'error': _toJsonable(error),
|
|
'trace': '$trace',
|
|
});
|
|
} finally {
|
|
// If the full directory is used instead of the path then this causes
|
|
// a TypeError with the ErrorHandlingFileSystem.
|
|
globals.fs.currentDirectory = cwd.path;
|
|
_apps.remove(app);
|
|
}
|
|
});
|
|
return app;
|
|
}
|
|
|
|
bool isRestartSupported(bool enableHotReload, Device device) =>
|
|
enableHotReload && device.supportsHotRestart;
|
|
|
|
Future<OperationResult> _inProgressHotReload;
|
|
|
|
Future<OperationResult> restart(Map<String, dynamic> args) async {
|
|
final String appId = _getStringArg(args, 'appId', required: true);
|
|
final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
|
|
final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
|
|
final String restartReason = _getStringArg(args, 'reason');
|
|
|
|
final AppInstance app = _getApp(appId);
|
|
if (app == null) {
|
|
throw "app '$appId' not found";
|
|
}
|
|
|
|
if (_inProgressHotReload != null) {
|
|
throw 'hot restart already in progress';
|
|
}
|
|
|
|
_inProgressHotReload = app._runInZone<OperationResult>(this, () {
|
|
return app.restart(fullRestart: fullRestart, pause: pauseAfterRestart, reason: restartReason);
|
|
});
|
|
return _inProgressHotReload.whenComplete(() {
|
|
_inProgressHotReload = null;
|
|
});
|
|
}
|
|
|
|
Future<OperationResult> reloadMethod(Map<String, dynamic> args) async {
|
|
final String appId = _getStringArg(args, 'appId', required: true);
|
|
final String classId = _getStringArg(args, 'class', required: true);
|
|
final String libraryId = _getStringArg(args, 'library', required: true);
|
|
|
|
final AppInstance app = _getApp(appId);
|
|
if (app == null) {
|
|
throw "app '$appId' not found";
|
|
}
|
|
|
|
if (_inProgressHotReload != null) {
|
|
throw 'hot restart already in progress';
|
|
}
|
|
|
|
_inProgressHotReload = app._runInZone<OperationResult>(this, () {
|
|
return app.reloadMethod(classId: classId, libraryId: libraryId);
|
|
});
|
|
return _inProgressHotReload.whenComplete(() {
|
|
_inProgressHotReload = null;
|
|
});
|
|
}
|
|
|
|
/// Returns an error, or the service extension result (a map with two fixed
|
|
/// keys, `type` and `method`). The result may have one or more additional keys,
|
|
/// depending on the specific service extension end-point. For example:
|
|
///
|
|
/// {
|
|
/// "value":"android",
|
|
/// "type":"_extensionType",
|
|
/// "method":"ext.flutter.platformOverride"
|
|
/// }
|
|
Future<Map<String, dynamic>> callServiceExtension(Map<String, dynamic> args) async {
|
|
final String appId = _getStringArg(args, 'appId', required: true);
|
|
final String methodName = _getStringArg(args, 'methodName');
|
|
final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']);
|
|
|
|
final AppInstance app = _getApp(appId);
|
|
if (app == null) {
|
|
throw "app '$appId' not found";
|
|
}
|
|
|
|
final Map<String, dynamic> result = await app.runner
|
|
.invokeFlutterExtensionRpcRawOnFirstIsolate(methodName, params: params);
|
|
if (result == null) {
|
|
throw 'method not available: $methodName';
|
|
}
|
|
|
|
if (result.containsKey('error')) {
|
|
throw result['error'];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Future<bool> stop(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.stop().then<bool>(
|
|
(void value) => true,
|
|
onError: (dynamic error, StackTrace stack) {
|
|
_sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
|
|
app.closeLogger();
|
|
_apps.remove(app);
|
|
return false;
|
|
},
|
|
);
|
|
}
|
|
|
|
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().then<bool>(
|
|
(void value) => true,
|
|
onError: (dynamic error, StackTrace stack) {
|
|
_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);
|
|
}
|
|
|
|
void _sendAppEvent(AppInstance app, String name, [ Map<String, dynamic> args ]) {
|
|
sendEvent('app.$name', <String, dynamic>{
|
|
'appId': app.id,
|
|
...?args,
|
|
});
|
|
}
|
|
}
|
|
|
|
typedef _DeviceEventHandler = void Function(Device device);
|
|
|
|
/// This domain lets callers list and monitor connected devices.
|
|
///
|
|
/// It exports a `getDevices()` call, as well as firing `device.added` and
|
|
/// `device.removed` events.
|
|
class DeviceDomain extends Domain {
|
|
DeviceDomain(Daemon daemon) : super(daemon, 'device') {
|
|
registerHandler('getDevices', getDevices);
|
|
registerHandler('enable', enable);
|
|
registerHandler('disable', disable);
|
|
registerHandler('forward', forward);
|
|
registerHandler('unforward', unforward);
|
|
|
|
// Use the device manager discovery so that client provided device types
|
|
// are usable via the daemon protocol.
|
|
deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer);
|
|
}
|
|
|
|
void addDeviceDiscoverer(DeviceDiscovery discoverer) {
|
|
if (!discoverer.supportsPlatform) {
|
|
return;
|
|
}
|
|
|
|
if (discoverer is PollingDeviceDiscovery) {
|
|
_discoverers.add(discoverer);
|
|
discoverer.onAdded.listen(_onDeviceEvent('device.added'));
|
|
discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
|
|
}
|
|
}
|
|
|
|
Future<void> _serializeDeviceEvents = Future<void>.value();
|
|
|
|
_DeviceEventHandler _onDeviceEvent(String eventName) {
|
|
return (Device device) {
|
|
_serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async {
|
|
try {
|
|
final Map<String, Object> response = await _deviceToMap(device);
|
|
sendEvent(eventName, response);
|
|
} on Exception catch (err) {
|
|
globals.printError('$err');
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
|
|
|
|
/// Return a list of the current devices, with each device represented as a map
|
|
/// of properties (id, name, platform, ...).
|
|
Future<List<Map<String, dynamic>>> getDevices([ Map<String, dynamic> args ]) async {
|
|
return <Map<String, dynamic>>[
|
|
for (final PollingDeviceDiscovery discoverer in _discoverers)
|
|
for (final Device device in await discoverer.devices)
|
|
await _deviceToMap(device),
|
|
];
|
|
}
|
|
|
|
/// Enable device events.
|
|
Future<void> enable(Map<String, dynamic> args) {
|
|
for (final PollingDeviceDiscovery discoverer in _discoverers) {
|
|
discoverer.startPolling();
|
|
}
|
|
return Future<void>.value();
|
|
}
|
|
|
|
/// Disable device events.
|
|
Future<void> disable(Map<String, dynamic> args) {
|
|
for (final PollingDeviceDiscovery discoverer in _discoverers) {
|
|
discoverer.stopPolling();
|
|
}
|
|
return Future<void>.value();
|
|
}
|
|
|
|
/// Forward a host port to a device port.
|
|
Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
|
|
final String deviceId = _getStringArg(args, 'deviceId', required: true);
|
|
final int devicePort = _getIntArg(args, 'devicePort', required: true);
|
|
int hostPort = _getIntArg(args, 'hostPort');
|
|
|
|
final Device device = await daemon.deviceDomain._getDevice(deviceId);
|
|
if (device == null) {
|
|
throw "device '$deviceId' not found";
|
|
}
|
|
|
|
hostPort = await device.portForwarder.forward(devicePort, hostPort: hostPort);
|
|
|
|
return <String, dynamic>{'hostPort': hostPort};
|
|
}
|
|
|
|
/// Removes a forwarded port.
|
|
Future<void> unforward(Map<String, dynamic> args) async {
|
|
final String deviceId = _getStringArg(args, 'deviceId', required: true);
|
|
final int devicePort = _getIntArg(args, 'devicePort', required: true);
|
|
final int hostPort = _getIntArg(args, 'hostPort', required: true);
|
|
|
|
final Device device = await daemon.deviceDomain._getDevice(deviceId);
|
|
if (device == null) {
|
|
throw "device '$deviceId' not found";
|
|
}
|
|
|
|
return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
for (final PollingDeviceDiscovery discoverer in _discoverers) {
|
|
discoverer.dispose();
|
|
}
|
|
}
|
|
|
|
/// Return the device matching the deviceId field in the args.
|
|
Future<Device> _getDevice(String deviceId) async {
|
|
for (final PollingDeviceDiscovery discoverer in _discoverers) {
|
|
final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null);
|
|
if (device != null) {
|
|
return device;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Stream<Map<String, dynamic>> get stdinCommandStream => globals.stdio.stdin
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.where((String line) => line.startsWith('[{') && line.endsWith('}]'))
|
|
.map<Map<String, dynamic>>((String line) {
|
|
line = line.substring(1, line.length - 1);
|
|
return castStringKeyedMap(json.decode(line));
|
|
});
|
|
|
|
void stdoutCommandResponse(Map<String, dynamic> command) {
|
|
globals.stdio.stdoutWrite(
|
|
'[${jsonEncodeObject(command)}]\n',
|
|
fallback: (String message, dynamic error, StackTrace stack) {
|
|
throwToolExit('Failed to write daemon command response to stdout: $error');
|
|
},
|
|
);
|
|
}
|
|
|
|
String jsonEncodeObject(dynamic object) {
|
|
return json.encode(object, toEncodable: _toEncodable);
|
|
}
|
|
|
|
dynamic _toEncodable(dynamic object) {
|
|
if (object is OperationResult) {
|
|
return _operationResultToMap(object);
|
|
}
|
|
return object;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _deviceToMap(Device device) async {
|
|
return <String, dynamic>{
|
|
'id': device.id,
|
|
'name': device.name,
|
|
'platform': getNameForTargetPlatform(await device.targetPlatform),
|
|
'emulator': await device.isLocalEmulator,
|
|
'category': device.category?.toString(),
|
|
'platformType': device.platformType?.toString(),
|
|
'ephemeral': device.ephemeral,
|
|
'emulatorId': await device.emulatorId,
|
|
};
|
|
}
|
|
|
|
Map<String, dynamic> _emulatorToMap(Emulator emulator) {
|
|
return <String, dynamic>{
|
|
'id': emulator.id,
|
|
'name': emulator.name,
|
|
'category': emulator.category?.toString(),
|
|
'platformType': emulator.platformType?.toString(),
|
|
};
|
|
}
|
|
|
|
Map<String, dynamic> _operationResultToMap(OperationResult result) {
|
|
return <String, dynamic>{
|
|
'code': result.code,
|
|
'message': result.message,
|
|
};
|
|
}
|
|
|
|
dynamic _toJsonable(dynamic obj) {
|
|
if (obj is String || obj is int || obj is bool || obj is Map<dynamic, dynamic> || obj is List<dynamic> || obj == null) {
|
|
return obj;
|
|
}
|
|
if (obj is OperationResult) {
|
|
return obj;
|
|
}
|
|
if (obj is ToolExit) {
|
|
return obj.message;
|
|
}
|
|
return '$obj';
|
|
}
|
|
|
|
class NotifyingLogger extends Logger {
|
|
final StreamController<LogMessage> _messageController = StreamController<LogMessage>.broadcast();
|
|
|
|
Stream<LogMessage> get onMessage => _messageController.stream;
|
|
|
|
@override
|
|
void printError(
|
|
String message, {
|
|
StackTrace stackTrace,
|
|
bool emphasis = false,
|
|
TerminalColor color,
|
|
int indent,
|
|
int hangingIndent,
|
|
bool wrap,
|
|
}) {
|
|
_messageController.add(LogMessage('error', message, stackTrace));
|
|
}
|
|
|
|
@override
|
|
void printStatus(
|
|
String message, {
|
|
bool emphasis = false,
|
|
TerminalColor color,
|
|
bool newline = true,
|
|
int indent,
|
|
int hangingIndent,
|
|
bool wrap,
|
|
}) {
|
|
_messageController.add(LogMessage('status', message));
|
|
}
|
|
|
|
@override
|
|
void printTrace(String message) {
|
|
// This is a lot of traffic to send over the wire.
|
|
}
|
|
|
|
@override
|
|
Status startProgress(
|
|
String message, {
|
|
@required Duration timeout,
|
|
String progressId,
|
|
bool multilineOutput = false,
|
|
int progressIndicatorPadding = kDefaultStatusPadding,
|
|
}) {
|
|
assert(timeout != null);
|
|
printStatus(message);
|
|
return SilentStatus(
|
|
timeout: timeout,
|
|
timeoutConfiguration: timeoutConfiguration,
|
|
stopwatch: Stopwatch(),
|
|
);
|
|
}
|
|
|
|
void dispose() {
|
|
_messageController.close();
|
|
}
|
|
|
|
@override
|
|
void sendEvent(String name, [Map<String, dynamic> args]) { }
|
|
|
|
@override
|
|
bool get supportsColor => throw UnimplementedError();
|
|
|
|
@override
|
|
bool get hasTerminal => false;
|
|
|
|
// This method is only relevant for terminals.
|
|
@override
|
|
void clear() { }
|
|
}
|
|
|
|
/// A running application, started by this daemon.
|
|
class AppInstance {
|
|
AppInstance(this.id, { this.runner, this.logToStdout = false });
|
|
|
|
final String id;
|
|
final ResidentRunner runner;
|
|
final bool logToStdout;
|
|
|
|
_AppRunLogger _logger;
|
|
|
|
Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) {
|
|
return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason);
|
|
}
|
|
|
|
Future<OperationResult> reloadMethod({ String classId, String libraryId }) {
|
|
return runner.reloadMethod(classId: classId, libraryId: libraryId);
|
|
}
|
|
|
|
Future<void> stop() => runner.exit();
|
|
Future<void> detach() => runner.detach();
|
|
|
|
void closeLogger() {
|
|
_logger.close();
|
|
}
|
|
|
|
Future<T> _runInZone<T>(AppDomain domain, FutureOr<T> method()) {
|
|
_logger ??= _AppRunLogger(domain, this, parent: logToStdout ? globals.logger : null);
|
|
|
|
return context.run<T>(
|
|
body: method,
|
|
overrides: <Type, Generator>{
|
|
Logger: () => _logger,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// This domain responds to methods like [getEmulators] and [launch].
|
|
class EmulatorDomain extends Domain {
|
|
EmulatorDomain(Daemon daemon) : super(daemon, 'emulator') {
|
|
registerHandler('getEmulators', getEmulators);
|
|
registerHandler('launch', launch);
|
|
registerHandler('create', create);
|
|
}
|
|
|
|
EmulatorManager emulators = EmulatorManager();
|
|
|
|
Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async {
|
|
final List<Emulator> list = await emulators.getAllAvailableEmulators();
|
|
return list.map<Map<String, dynamic>>(_emulatorToMap).toList();
|
|
}
|
|
|
|
Future<void> launch(Map<String, dynamic> args) async {
|
|
final String emulatorId = _getStringArg(args, 'emulatorId', required: true);
|
|
final List<Emulator> matches =
|
|
await emulators.getEmulatorsMatching(emulatorId);
|
|
if (matches.isEmpty) {
|
|
throw "emulator '$emulatorId' not found";
|
|
} else if (matches.length > 1) {
|
|
throw "multiple emulators match '$emulatorId'";
|
|
} else {
|
|
await matches.first.launch();
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>> create(Map<String, dynamic> args) async {
|
|
final String name = _getStringArg(args, 'name', required: false);
|
|
final CreateEmulatorResult res = await emulators.createEmulator(name: name);
|
|
return <String, dynamic>{
|
|
'success': res.success,
|
|
'emulatorName': res.emulatorName,
|
|
'error': res.error,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// A [Logger] which sends log messages to a listening daemon client.
|
|
///
|
|
/// This class can either:
|
|
/// 1) Send stdout messages and progress events to the client IDE
|
|
/// 1) Log messages to stdout and send progress events to the client IDE
|
|
//
|
|
// TODO(devoncarew): To simplify this code a bit, we could choose to specialize
|
|
// this class into two, one for each of the above use cases.
|
|
class _AppRunLogger extends Logger {
|
|
_AppRunLogger(this.domain, this.app, { this.parent });
|
|
|
|
AppDomain domain;
|
|
final AppInstance app;
|
|
final Logger parent;
|
|
int _nextProgressId = 0;
|
|
|
|
@override
|
|
void printError(
|
|
String message, {
|
|
StackTrace stackTrace,
|
|
bool emphasis,
|
|
TerminalColor color,
|
|
int indent,
|
|
int hangingIndent,
|
|
bool wrap,
|
|
}) {
|
|
if (parent != null) {
|
|
parent.printError(
|
|
message,
|
|
stackTrace: stackTrace,
|
|
emphasis: emphasis,
|
|
indent: indent,
|
|
hangingIndent: hangingIndent,
|
|
wrap: wrap,
|
|
);
|
|
} else {
|
|
if (stackTrace != null) {
|
|
_sendLogEvent(<String, dynamic>{
|
|
'log': message,
|
|
'stackTrace': stackTrace.toString(),
|
|
'error': true,
|
|
});
|
|
} else {
|
|
_sendLogEvent(<String, dynamic>{
|
|
'log': message,
|
|
'error': true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void printStatus(
|
|
String message, {
|
|
bool emphasis = false,
|
|
TerminalColor color,
|
|
bool newline = true,
|
|
int indent,
|
|
int hangingIndent,
|
|
bool wrap,
|
|
}) {
|
|
if (parent != null) {
|
|
parent.printStatus(
|
|
message,
|
|
emphasis: emphasis,
|
|
color: color,
|
|
newline: newline,
|
|
indent: indent,
|
|
hangingIndent: hangingIndent,
|
|
wrap: wrap,
|
|
);
|
|
} else {
|
|
_sendLogEvent(<String, dynamic>{'log': message});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void printTrace(String message) {
|
|
if (parent != null) {
|
|
parent.printTrace(message);
|
|
} else {
|
|
_sendLogEvent(<String, dynamic>{'log': message, 'trace': true});
|
|
}
|
|
}
|
|
|
|
Status _status;
|
|
|
|
@override
|
|
Status startProgress(
|
|
String message, {
|
|
@required Duration timeout,
|
|
String progressId,
|
|
bool multilineOutput = false,
|
|
int progressIndicatorPadding = kDefaultStatusPadding,
|
|
}) {
|
|
assert(timeout != null);
|
|
final int id = _nextProgressId++;
|
|
|
|
_sendProgressEvent(<String, dynamic>{
|
|
'id': id.toString(),
|
|
'progressId': progressId,
|
|
'message': message,
|
|
});
|
|
|
|
_status = SilentStatus(
|
|
timeout: timeout,
|
|
timeoutConfiguration: timeoutConfiguration,
|
|
onFinish: () {
|
|
_status = null;
|
|
_sendProgressEvent(<String, dynamic>{
|
|
'id': id.toString(),
|
|
'progressId': progressId,
|
|
'finished': true,
|
|
});
|
|
}, stopwatch: Stopwatch())..start();
|
|
return _status;
|
|
}
|
|
|
|
void close() {
|
|
domain = null;
|
|
}
|
|
|
|
void _sendLogEvent(Map<String, dynamic> event) {
|
|
if (domain == null) {
|
|
printStatus('event sent after app closed: $event');
|
|
} else {
|
|
domain._sendAppEvent(app, 'log', event);
|
|
}
|
|
}
|
|
|
|
void _sendProgressEvent(Map<String, dynamic> event) {
|
|
if (domain == null) {
|
|
printStatus('event sent after app closed: $event');
|
|
} else {
|
|
domain._sendAppEvent(app, 'progress', event);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void sendEvent(String name, [Map<String, dynamic> args]) {
|
|
if (domain == null) {
|
|
printStatus('event sent after app closed: $name');
|
|
} else {
|
|
domain.sendEvent(name, args);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool get supportsColor => throw UnimplementedError();
|
|
|
|
@override
|
|
bool get hasTerminal => false;
|
|
|
|
// This method is only relevant for terminals.
|
|
@override
|
|
void clear() { }
|
|
}
|
|
|
|
class LogMessage {
|
|
LogMessage(this.level, this.message, [this.stackTrace]);
|
|
|
|
final String level;
|
|
final String message;
|
|
final StackTrace stackTrace;
|
|
}
|
|
|
|
/// The method by which the flutter app was launched.
|
|
class LaunchMode {
|
|
const LaunchMode._(this._value);
|
|
|
|
/// The app was launched via `flutter run`.
|
|
static const LaunchMode run = LaunchMode._('run');
|
|
|
|
/// The app was launched via `flutter attach`.
|
|
static const LaunchMode attach = LaunchMode._('attach');
|
|
|
|
final String _value;
|
|
|
|
@override
|
|
String toString() => _value;
|
|
}
|