blob: 31729504e3fc3135233175102a7ebba37ec463c8 [file]
// 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:async/async.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'package:uuid/uuid.dart';
import '../android/android_sdk.dart';
import '../android/android_workflow.dart';
import '../android/java.dart';
import '../application_package.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/signals.dart';
import '../base/terminal.dart';
import '../base/time.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../context/android_context.dart';
import '../context/tool_context.dart';
import '../convert.dart';
import '../daemon.dart';
import '../device.dart';
import '../device_port_forwarder.dart';
import '../device_vm_service_discovery_for_attach.dart';
import '../emulator.dart';
import '../features.dart';
import '../project.dart';
import '../proxied_devices/debounce_data_stream.dart';
import '../proxied_devices/file_transfer.dart';
import '../resident_runner.dart';
import '../run_cold.dart';
import '../run_hot.dart';
import '../runner/flutter_command.dart';
import '../vmservice.dart';
import '../web/web_runner.dart';
const protocolVersion = '0.6.1';
/// 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({
required super.toolContext,
required this._androidContext,
this._androidWorkflow,
this._deviceManager,
this.hidden = false,
}) {
argParser.addOption(
'listen-on-tcp-port',
help: 'If specified, the daemon will be listening for commands on the specified port instead of stdio.',
valueHelp: 'port',
);
}
final AndroidContext _androidContext;
final AndroidWorkflow? _androidWorkflow;
final DeviceManager? _deviceManager;
AndroidSdk? get _androidSdk => _androidContext.androidSdk;
Java? get _java => _androidContext.java;
@override
ToolContext get toolContext => super.toolContext!;
@override
final name = 'daemon';
@override
final description = 'Run a persistent, JSON-RPC based server to communicate with devices.';
@override
final String category = FlutterCommandCategory.tools;
@override
final bool hidden;
@override
Future<FlutterCommandResult> runCommand() async {
final Logger logger = toolContext.logger;
final Stdio stdio = toolContext.stdio;
final AnsiTerminal terminal = toolContext.terminal;
final OutputPreferences outputPreferences = toolContext.outputPreferences;
final FileSystem fs = toolContext.fs;
final Platform platform = toolContext.platform;
final ProcessManager processManager = toolContext.processManager;
final SystemClock systemClock = toolContext.systemClock;
if (argResults!['listen-on-tcp-port'] != null) {
int? port;
try {
port = int.parse(stringArg('listen-on-tcp-port')!);
} on FormatException catch (error) {
throwToolExit('Invalid port for `--listen-on-tcp-port`: $error');
}
await DaemonServer(
toolContext: toolContext,
logger: StdoutLogger(
terminal: terminal,
stdio: stdio,
outputPreferences: outputPreferences,
),
analytics: analytics,
androidSdk: _androidSdk,
androidWorkflow: _androidWorkflow,
deviceManager: _deviceManager,
featureFlags: featureFlags,
fileSystem: fs,
java: _java,
notifyingLogger: asLogger<NotifyingLogger>(logger),
outputPreferences: outputPreferences,
platform: platform,
port: port,
processManager: processManager,
systemClock: systemClock,
terminal: terminal,
).run();
return FlutterCommandResult.success();
}
final daemon = Daemon(
DaemonConnection(
daemonStreams: DaemonStreams.fromStdio(stdio, logger: logger),
logger: logger,
),
toolContext: toolContext,
analytics: analytics,
androidSdk: _androidSdk,
androidWorkflow: _androidWorkflow,
deviceManager: _deviceManager,
featureFlags: featureFlags,
fileSystem: fs,
java: _java,
logger: logger,
notifyingLogger: asLogger<NotifyingLogger>(logger),
outputPreferences: outputPreferences,
platform: platform,
processManager: processManager,
stdio: stdio,
systemClock: systemClock,
terminal: terminal,
);
logger.printStatus('Device daemon started.');
final int code = await daemon.onExit;
if (code != 0) {
throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
}
return FlutterCommandResult.success();
}
}
@visibleForTesting
class DaemonServer {
DaemonServer({
required this.toolContext,
required this.logger,
this.analytics,
this.androidSdk,
this.androidWorkflow,
@visibleForTesting this._bind = ServerSocket.bind,
this.deviceManager,
required this.featureFlags,
this.fileSystem,
this.java,
this.notifyingLogger,
this.outputPreferences,
this.platform,
this.port,
this.processManager,
this.stdio,
this.systemClock,
this.terminal,
});
final int? port;
final ToolContext toolContext;
/// Stdout logger used to print general server-related errors.
final Logger logger;
// Logger that sends the message to the other end of daemon connection.
final NotifyingLogger? notifyingLogger;
final FileSystem? fileSystem;
final Platform? platform;
final ProcessManager? processManager;
final Analytics? analytics;
final SystemClock? systemClock;
final AnsiTerminal? terminal;
final OutputPreferences? outputPreferences;
final DeviceManager? deviceManager;
final Java? java;
final AndroidSdk? androidSdk;
final FeatureFlags featureFlags;
final AndroidWorkflow? androidWorkflow;
final Stdio? stdio;
final Future<ServerSocket> Function(InternetAddress address, int port) _bind;
Future<void> run() async {
ServerSocket? serverSocket;
try {
serverSocket = await _bind(InternetAddress.loopbackIPv4, port!);
} on SocketException {
logger.printTrace('Bind on $port failed with IPv4, retrying on IPv6');
}
// If binding on IPv4 failed, try binding on IPv6.
// Omit try catch here, let the failure fallthrough.
serverSocket ??= await _bind(InternetAddress.loopbackIPv6, port!);
logger.printStatus('Daemon server listening on ${serverSocket.port}');
final StreamSubscription<Socket> subscription = serverSocket.listen((Socket socket) async {
// We have to listen to socket.done. Otherwise when the connection is
// reset, we will receive an uncatchable exception.
// https://github.com/dart-lang/sdk/issues/25518
final Future<void> socketDone = socket.done.handleError((
Object error,
StackTrace stackTrace,
) {
logger.printError('Socket error: $error');
logger.printTrace('$stackTrace');
});
final daemon = Daemon(
DaemonConnection(
daemonStreams: DaemonStreams.fromSocket(socket, logger: logger),
logger: logger,
),
toolContext: toolContext,
notifyingLogger: notifyingLogger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
logger: logger,
analytics: analytics,
systemClock: systemClock,
terminal: terminal,
outputPreferences: outputPreferences,
deviceManager: deviceManager,
java: java,
androidSdk: androidSdk,
featureFlags: featureFlags,
androidWorkflow: androidWorkflow,
stdio: stdio,
);
await daemon.onExit;
await socketDone;
});
// Wait indefinitely until the server closes.
await subscription.cancel();
}
}
typedef CommandHandler = Future<Object?>? Function(Map<String, Object?> args);
typedef CommandHandlerWithBinary = Future<Object?> Function(
Map<String, Object?> args,
Stream<List<int>>? binary,
);
class Daemon {
Daemon(
this.connection, {
required ToolContext toolContext,
Analytics? analytics,
AndroidSdk? androidSdk,
AndroidWorkflow? androidWorkflow,
DeviceManager? deviceManager,
required FeatureFlags featureFlags,
FileSystem? fileSystem,
FileTransfer fileTransfer = const FileTransfer(),
Java? java,
this.logToStdout = false,
Logger? logger,
this.notifyingLogger,
OutputPreferences? outputPreferences,
Platform? platform,
ProcessManager? processManager,
Stdio? stdio,
SystemClock? systemClock,
AnsiTerminal? terminal,
}) : _stdio = stdio,
_logger = logger ?? BufferLogger.test(),
_fs =
fileSystem ??
LocalFileSystem(LocalSignals.instance, Signals.defaultExitSignals, ShutdownHooks()) {
final Platform p = platform ?? const LocalPlatform();
final ProcessManager pm = processManager ?? const LocalProcessManager();
final AnsiTerminal term = terminal ?? AnsiTerminal(stdio: stdio ?? Stdio(), platform: p);
final OutputPreferences prefs = outputPreferences ?? OutputPreferences.test();
final Analytics an = analytics ?? const NoOpAnalytics();
final SystemClock clock = systemClock ?? const SystemClock();
final AndroidWorkflow workflow =
androidWorkflow ?? AndroidWorkflow(androidSdk: androidSdk, featureFlags: featureFlags);
// Set up domains.
registerDomain(
daemonDomain = DaemonDomain(
this,
featureFlags: featureFlags,
fileSystem: _fs,
logger: _logger,
stdio: _stdio,
),
);
registerDomain(
appDomain = AppDomain(
this,
toolContext: toolContext,
analytics: an,
fileSystem: _fs,
logger: _logger,
outputPreferences: prefs,
platform: p,
systemClock: clock,
terminal: term,
),
);
registerDomain(
deviceDomain = DeviceDomain(this, logger: _logger, deviceManager: deviceManager),
);
registerDomain(
emulatorDomain = EmulatorDomain(
this,
androidWorkflow: workflow,
fileSystem: _fs,
logger: _logger,
processManager: pm,
androidSdk: androidSdk,
java: java,
),
);
registerDomain(devToolsDomain = DevToolsDomain(this));
registerDomain(
proxyDomain = ProxyDomain(this, fileSystem: _fs, fileTransfer: fileTransfer, logger: _logger),
);
// Start listening.
_commandSubscription = connection.incomingCommands.listen(
_handleRequest,
onDone: () {
shutdown();
if (!_onExitCompleter.isCompleted) {
_onExitCompleter.complete(0);
}
},
);
}
factory Daemon.createMachineDaemon({
required ToolContext toolContext,
required FeatureFlags featureFlags,
required Logger logger,
required Stdio stdio,
Analytics? analytics,
AndroidSdk? androidSdk,
AndroidWorkflow? androidWorkflow,
DeviceManager? deviceManager,
FileSystem? fileSystem,
Java? java,
OutputPreferences? outputPreferences,
Platform? platform,
ProcessManager? processManager,
SystemClock? systemClock,
AnsiTerminal? terminal,
}) {
final daemon = Daemon(
DaemonConnection(
daemonStreams: DaemonStreams.fromStdio(stdio, logger: logger),
logger: logger,
),
toolContext: toolContext,
notifyingLogger: (logger is NotifyingLogger)
? logger
: NotifyingLogger(verbose: logger.isVerbose, parent: logger),
logToStdout: true,
stdio: stdio,
logger: logger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
analytics: analytics,
systemClock: systemClock,
terminal: terminal,
outputPreferences: outputPreferences,
deviceManager: deviceManager,
java: java,
androidSdk: androidSdk,
featureFlags: featureFlags,
androidWorkflow: androidWorkflow,
);
return daemon;
}
final DaemonConnection connection;
final Stdio? _stdio;
final Logger _logger;
final FileSystem _fs;
late DaemonDomain daemonDomain;
late AppDomain appDomain;
late DeviceDomain deviceDomain;
EmulatorDomain? emulatorDomain;
DevToolsDomain? devToolsDomain;
late ProxyDomain proxyDomain;
StreamSubscription<DaemonMessage>? _commandSubscription;
final NotifyingLogger? notifyingLogger;
final bool logToStdout;
final _onExitCompleter = Completer<int>();
final _domainMap = <String, Domain>{};
@visibleForTesting
void registerDomain(Domain domain) {
_domainMap[domain.name] = domain;
}
Future<int> get onExit => _onExitCompleter.future;
void _handleRequest(DaemonMessage request) {
// {id, method, params}
// [id] is an opaque type to us.
final Object? id = request.data['id'];
if (id == null) {
if (_stdio != null) {
_stdio.stderrWrite('no id for request: $request\n');
} else {
_logger.printError('no id for request: $request');
}
return;
}
try {
final method = request.data['method']! as String;
if (!method.contains('.')) {
throw DaemonException('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 DaemonException('no domain for method: $method');
}
_domainMap[prefix]!.handleCommand(
name,
id,
castStringKeyedMap(request.data['params']) ?? const <String, Object?>{},
request.binary,
);
} on Exception catch (error, trace) {
connection.sendErrorResponse(id, _toJsonable(error), trace);
}
}
Future<void> shutdown({Object? error}) async {
await devToolsDomain?.dispose();
await _commandSubscription?.cancel();
await connection.dispose();
for (final Domain domain in _domainMap.values) {
await 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 _handlers = <String, CommandHandler>{};
final _handlersWithBinary = <String, CommandHandlerWithBinary>{};
void registerHandler(String name, CommandHandler handler) {
assert(!_handlers.containsKey(name));
assert(!_handlersWithBinary.containsKey(name));
_handlers[name] = handler;
}
void registerHandlerWithBinary(String name, CommandHandlerWithBinary handler) {
assert(!_handlers.containsKey(name));
assert(!_handlersWithBinary.containsKey(name));
_handlersWithBinary[name] = handler;
}
@override
String toString() => name;
void handleCommand(
String command,
Object id,
Map<String, Object?> args,
Stream<List<int>>? binary,
) {
Future<Object?>.sync(() {
if (_handlers.containsKey(command)) {
return _handlers[command]!(args);
} else if (_handlersWithBinary.containsKey(command)) {
return _handlersWithBinary[command]!(args, binary);
}
throw DaemonException('command not understood: $name.$command');
}).then<Object?>(
(Object? result) {
daemon.connection.sendResponse(id, _toJsonable(result));
return null;
},
onError: (Object error, StackTrace stackTrace) {
daemon.connection.sendErrorResponse(id, _toJsonable(error), stackTrace);
return null;
},
);
}
void sendEvent(String name, [Object? args, List<int>? binary]) {
daemon.connection.sendEvent(name, _toJsonable(args), binary);
}
String? _getStringArg(Map<String, Object?> args, String name, {bool required = false}) {
if (required && !args.containsKey(name)) {
throw DaemonException('$name is required');
}
final Object? val = args[name];
if (val != null && val is! String) {
throw DaemonException('$name is not a String');
}
return val as String?;
}
bool? _getBoolArg(Map<String, Object?> args, String name, {bool required = false}) {
if (required && !args.containsKey(name)) {
throw DaemonException('$name is required');
}
final Object? val = args[name];
if (val != null && val is! bool) {
throw DaemonException('$name is not a bool');
}
return val as bool?;
}
int? _getIntArg(Map<String, Object?> args, String name, {bool required = false}) {
if (required && !args.containsKey(name)) {
throw DaemonException('$name is required');
}
final Object? val = args[name];
if (val != null && val is! int) {
throw DaemonException('$name is not an int');
}
return val as int?;
}
Future<void> dispose() async {}
}
/// This domain responds to methods like [version] and [shutdown].
///
/// This domain fires the `daemon.logMessage` event.
class DaemonDomain extends Domain {
DaemonDomain(
Daemon daemon, {
required this._featureFlags,
required FileSystem fileSystem,
required this._logger,
this._stdio,
}) : _fs = fileSystem,
super(daemon, 'daemon') {
registerHandler('version', version);
registerHandler('shutdown', shutdown);
registerHandler('getSupportedPlatforms', getSupportedPlatforms);
registerHandler('setNotifyVerbose', setNotifyVerbose);
sendEvent('daemon.connected', <String, Object?>{'version': protocolVersion, 'pid': pid});
_subscription = daemon.notifyingLogger!.onMessage.listen((LogMessage message) {
if (daemon.logToStdout) {
if (message.level == 'status' || message.level == 'trace') {
// We use `print()` here instead of `stdout.writeln()` in order to
// capture the print output for testing.
// ignore: avoid_print
print(message.message);
} else if (message.level == 'error' || message.level == 'warning') {
if (_stdio != null) {
_stdio.stderrWrite('${message.message}\n');
if (message.stackTrace != null) {
_stdio.stderrWrite('${message.stackTrace.toString().trimRight()}\n');
}
} else {
_logger.printError(message.message, stackTrace: message.stackTrace);
}
}
} else {
if (message.stackTrace != null) {
sendEvent('daemon.logMessage', <String, Object?>{
'level': message.level,
'message': message.message,
'stackTrace': message.stackTrace.toString(),
});
} else {
sendEvent('daemon.logMessage', <String, Object?>{
'level': message.level,
'message': message.message,
});
}
}
});
}
final FileSystem _fs;
final FeatureFlags _featureFlags;
final Logger _logger;
final Stdio? _stdio;
StreamSubscription<LogMessage>? _subscription;
Future<String> version(Map<String, Object?> 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 Object? res = await daemon.connection.sendRequest('app.exposeUrl', <String, String>{
'url': url,
});
if (res is Map<String, Object?> && res['url'] is String) {
return res['url']! as String;
} else {
_logger.printError(
'Invalid response to exposeUrl - params should include a String url field',
);
return url;
}
}
Future<void> shutdown(Map<String, Object?> args) {
Timer.run(daemon.shutdown);
return Future<void>.value();
}
@override
Future<void> dispose() async {
await _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, Object?> args) async {
final String? projectRoot = _getStringArg(args, 'projectRoot', required: true);
final platformTypes = <String>[];
final platformTypesMap = <String, Object>{};
try {
final FlutterProject flutterProject = FlutterProject.fromDirectory(
_fs.directory(projectRoot),
);
final Set<SupportedPlatform> supportedPlatforms = flutterProject
.getSupportedPlatforms()
.toSet();
void handlePlatformType(PlatformType platform) {
final reasons = <Map<String, Object>>[];
switch (platform) {
case PlatformType.linux:
if (!_featureFlags.isLinuxEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Linux feature is not enabled',
'fixText': 'Run "flutter config --enable-linux-desktop"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.linux)) {
reasons.add(<String, Object>{
'reasonText': 'the Linux platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=linux ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.macos:
if (!_featureFlags.isMacOSEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the macOS feature is not enabled',
'fixText': 'Run "flutter config --enable-macos-desktop"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.macos)) {
reasons.add(<String, Object>{
'reasonText': 'the macOS platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=macos ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.windows:
if (!_featureFlags.isWindowsEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Windows feature is not enabled',
'fixText': 'Run "flutter config --enable-windows-desktop"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.windows)) {
reasons.add(<String, Object>{
'reasonText': 'the Windows platform is not enabled for this project',
'fixText':
'Run "flutter create --platforms=windows ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.ios:
if (!_featureFlags.isIOSEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the iOS feature is not enabled',
'fixText': 'Run "flutter config --enable-ios"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.ios)) {
reasons.add(<String, Object>{
'reasonText': 'the iOS platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=ios ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.android:
if (!_featureFlags.isAndroidEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Android feature is not enabled',
'fixText': 'Run "flutter config --enable-android"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.android)) {
reasons.add(<String, Object>{
'reasonText': 'the Android platform is not enabled for this project',
'fixText':
'Run "flutter create --platforms=android ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.web:
if (!_featureFlags.isWebEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Web feature is not enabled',
'fixText': 'Run "flutter config --enable-web"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.web)) {
reasons.add(<String, Object>{
'reasonText': 'the Web platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=web ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.fuchsia:
if (!_featureFlags.isFuchsiaEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Fuchsia feature is not enabled',
'fixText': 'Run "flutter config --enable-fuchsia"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.fuchsia)) {
reasons.add(<String, Object>{
'reasonText': 'the Fuchsia platform is not enabled for this project',
'fixText':
'Run "flutter create --platforms=fuchsia ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.custom:
if (!_featureFlags.areCustomDevicesEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the custom devices feature is not enabled',
'fixText': 'Run "flutter config --enable-custom-devices"',
'fixCode': _ReasonCode.config.name,
});
}
}
if (reasons.isEmpty) {
platformTypes.add(platform.name);
platformTypesMap[platform.name] = const <String, Object>{'isSupported': true};
} else {
platformTypesMap[platform.name] = <String, Object>{
'isSupported': false,
'reasons': reasons,
};
}
}
PlatformType.values.forEach(handlePlatformType);
return <String, Object>{
// TODO(bkonyi): remove 'platformTypes' once clients have migrated to 'platforms'. See https://github.com/flutter/flutter/issues/140473
'platforms': platformTypes,
'platformTypes': platformTypesMap,
};
} on Exception catch (err, stackTrace) {
sendEvent('log', <String, Object?>{
'log': 'Failed to parse project metadata',
'stackTrace': stackTrace.toString(),
'error': true,
});
// On any sort of failure, fall back to Android and iOS for backwards
// compatibility.
return const <String, Object>{
'platforms': <String>['android', 'ios'],
'platformTypes': <String, Object>{
'android': <String, Object>{'isSupported': true},
'ios': <String, Object>{'isSupported': true},
},
};
}
}
/// If notifyVerbose is set, the daemon will forward all verbose logs.
Future<void> setNotifyVerbose(Map<String, Object?> args) async {
daemon.notifyingLogger?.notifyVerbose = _getBoolArg(args, 'verbose') ?? true;
}
}
/// The reason a [PlatformType] is not currently supported.
///
/// The [name] of this value will be sent as a response to daemon client.
enum _ReasonCode { create, config }
typedef RunOrAttach = Future<void> Function({
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
});
/// This domain responds to methods like [startApp] and [stop].
///
/// It fires events for application start, stop, and stdout and stderr.
class AppDomain extends Domain {
AppDomain(
Daemon daemon, {
required this._toolContext,
Analytics? analytics,
FileSystem? fileSystem,
Logger? logger,
OutputPreferences? outputPreferences,
Platform? platform,
SystemClock? systemClock,
AnsiTerminal? terminal,
}) : _fs = fileSystem ?? daemon._fs,
_platform = platform ?? const LocalPlatform(),
_analytics = analytics ?? const NoOpAnalytics(),
_systemClock = systemClock ?? const SystemClock(),
_logger = logger ?? daemon._logger,
_terminal =
terminal ??
AnsiTerminal(
stdio: daemon._stdio ?? Stdio(),
platform: platform ?? const LocalPlatform(),
),
_outputPreferences = outputPreferences ?? OutputPreferences.test(),
super(daemon, 'app') {
registerHandler('restart', restart);
registerHandler('callServiceExtension', callServiceExtension);
registerHandler('stop', stop);
registerHandler('detach', detach);
}
final FileSystem _fs;
final Platform _platform;
final Analytics _analytics;
final SystemClock _systemClock;
final Logger _logger;
final AnsiTerminal _terminal;
final ToolContext _toolContext;
final OutputPreferences _outputPreferences;
static const _uuidGenerator = Uuid();
static String _getNewAppId() => _uuidGenerator.v4();
final _apps = <AppInstance>[];
final operationQueue = DebounceOperationQueue<OperationResult, OperationType>();
Future<AppInstance> startApp(
Device device,
String projectDirectory,
String target,
String? route,
DebuggingOptions options,
bool enableHotReload, {
Map<String, String> webDefines = const <String, String>{},
File? applicationBinary,
required bool trackWidgetCreation,
String? projectRootPath,
String? packagesFilePath,
String? dillOutputPath,
String? isolateFilter,
bool machine = true,
String? userIdentifier,
}) async {
if (!await device.supportsRuntimeMode(options.buildInfo.mode)) {
throw Exception(
'${options.buildInfo.mode.uppercaseFriendlyName} '
'mode is not supported for ${device.displayName}.',
);
}
// We change the current working directory for the duration of the `start` command.
final Directory cwd = _fs.currentDirectory;
_fs.currentDirectory = _fs.directory(projectDirectory);
final FlutterProject flutterProject = FlutterProject.fromDirectory(_fs.currentDirectory);
final FlutterDevice flutterDevice = await FlutterDevice.create(
device,
toolContext: _toolContext,
buildInfo: options.buildInfo,
target: target,
userIdentifier: userIdentifier,
);
ResidentRunner runner;
if (await device.targetPlatform == TargetPlatform.web_javascript) {
runner = webRunnerFactory!.createWebRunner(
flutterDevice,
flutterProject: flutterProject,
target: target,
debuggingOptions: options,
stayResident: true,
urlTunneller: options.webEnableExposeUrl! ? daemon.daemonDomain.exposeUrl : null,
machine: machine,
analytics: _analytics,
systemClock: _systemClock,
logger: _logger,
terminal: _terminal,
platform: _platform,
outputPreferences: _outputPreferences,
fileSystem: _fs,
webDefines: webDefines,
);
} else if (enableHotReload) {
runner = HotRunner(
<FlutterDevice>[flutterDevice],
target: target,
debuggingOptions: options,
applicationBinary: applicationBinary,
projectRootPath: projectRootPath,
dillOutputPath: dillOutputPath,
hostIsIde: true,
machine: machine,
analytics: _analytics,
logger: _logger,
);
} else {
runner = ColdRunner(
<FlutterDevice>[flutterDevice],
target: target,
debuggingOptions: options,
applicationBinary: applicationBinary,
machine: machine,
);
}
return launch(
runner,
({
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
}) {
return runner.run(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter,
route: route,
);
},
device,
projectDirectory,
enableHotReload,
cwd,
LaunchMode.run,
asLogger<MachineOutputLogger>(_logger),
);
}
Future<AppInstance> launch(
ResidentRunner runner,
RunOrAttach runOrAttach,
Device device,
String? projectDirectory,
bool enableHotReload,
Directory cwd,
LaunchMode launchMode,
MachineOutputLogger logger,
) async {
final app = AppInstance(
_getNewAppId(),
runner: runner,
logToStdout: daemon.logToStdout,
logger: logger,
);
_apps.add(app);
// Set the domain and app for the given AppRunLogger. This allows the logger
// to log messages containing the app ID to the host.
logger._domain = this;
logger._app = app;
_sendAppEvent(app, 'start', <String, Object?>{
'deviceId': device.id,
'directory': projectDirectory,
'supportsRestart': isRestartSupported(enableHotReload, device),
'launchMode': launchMode.toString(),
'mode': runner.debuggingOptions.buildInfo.modeName,
});
Completer<DebugConnectionInfo>? connectionInfoCompleter;
if (runner.supportsServiceProtocol && 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) {
_sendAppEvent(app, 'debugPort', {
// The web vmservice proxy does not have an http address.
'port': info.httpUri?.port ?? info.wsUri!.port,
'wsUri': info.wsUri.toString(),
'baseUri': ?info.baseUri,
});
if (info.devToolsUri != null) {
_sendAppEvent(app, 'devTools', {'uri': info.devToolsUri!.toString()});
}
if (info.dtdUri != null) {
_sendAppEvent(app, 'dtd', {'uri': info.dtdUri!.toString()});
}
}),
);
}
// Kept separate from [AppInstance.started]. Runners attach listeners to this
// completer that have no error handler, so it must only ever be completed.
final appStartedCompleter = Completer<void>();
// This future won't complete until the application has shutdown, so we don't want to
// await it. However, we do need to listen to the future in order to handle possible
// tool exits
final Future<void> appRunFuture = app._runInZone<void>(this, () async {
try {
await runOrAttach(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter,
);
_sendAppEvent(app, 'stop');
} on Exception catch (error, trace) {
_sendAppEvent(app, 'stop', <String, Object?>{
'error': _toJsonable(error),
'trace': '$trace',
});
} finally {
// If the full directory is used instead of the path then this causes
// a TypeError with the ErrorHandlingFileSystem.
_fs.currentDirectory = cwd.path;
_apps.remove(app);
}
});
try {
await Future.any(<Future<void>>[
appStartedCompleter.future.then<void>((void value) {
app._markStarted();
_sendAppEvent(app, 'started');
}),
appRunFuture,
]);
} on Object catch (error, stackTrace) {
// `appRunFuture` only converts an [Exception] into a `stop` event, so an
// [Error] thrown by the runner or by the `finally` above surfaces here.
// Settle anything waiting on [AppInstance.started] before it propagates,
// otherwise a deferred `app.restart` waits forever.
app._failedToStart(error, stackTrace);
rethrow;
}
// If appRunFuture completes early due to a fatal initialization error
// without actually starting the app, we must explicitly fail both this
// request and anything waiting on [AppInstance.started], to prevent the
// IDE/client from hanging indefinitely.
if (!appStartedCompleter.isCompleted) {
final failure = DaemonException('App failed to start');
app._failedToStart(failure);
throw failure;
}
return app;
}
bool isRestartSupported(bool enableHotReload, Device device) =>
enableHotReload && device.supportsHotRestart;
final _hotReloadDebounceDurationMs = 50;
Future<OperationResult>? restart(Map<String, Object?> 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 bool debounce = _getBoolArg(args, 'debounce') ?? false;
// This is an undocumented parameter used for integration tests.
final int? debounceDurationOverrideMs = _getIntArg(args, 'debounceDurationOverrideMs');
final AppInstance? app = _getApp(appId);
if (app == null) {
throw DaemonException("app '$appId' not found");
}
// The `app.start` event carries the app ID and is sent before the runner
// has finished starting up, so a client can ask for a restart while the
// initial compile is still in flight. Servicing it now would issue a
// recompile against a compiler that has not accepted its first compile
// yet. Defer instead of dropping it: the client may have edited a file
// that the initial compile did not pick up.
await app.started;
return _queueAndDebounceReloadAction(
app,
fullRestart ? OperationType.restart : OperationType.reload,
debounce,
debounceDurationOverrideMs,
() {
return app.restart(
fullRestart: fullRestart,
pause: pauseAfterRestart,
reason: restartReason,
);
},
)!;
}
/// Debounce and queue reload actions.
///
/// Only one reload action will run at a time. Actions requested in quick
/// succession (within [_hotReloadDebounceDurationMs]) will be merged together
/// and all return the same result. If an action is requested after an identical
/// action has already started, it will be queued and run again once the first
/// action completes.
Future<OperationResult>? _queueAndDebounceReloadAction(
AppInstance app,
OperationType operationType,
bool debounce,
int? debounceDurationOverrideMs,
Future<OperationResult> Function() action,
) {
final Duration debounceDuration = debounce
? Duration(milliseconds: debounceDurationOverrideMs ?? _hotReloadDebounceDurationMs)
: Duration.zero;
return operationQueue.queueAndDebounce(
operationType,
debounceDuration,
() => app._runInZone<OperationResult>(this, action),
);
}
/// Returns an error, or the service extension result. For example:
///
/// {
/// "value":"android"
/// }
Future<Map<String, Object?>> callServiceExtension(Map<String, Object?> args) async {
final String? appId = _getStringArg(args, 'appId', required: true);
final String methodName = _getStringArg(args, 'methodName')!;
final Map<String, Object?>? params = args['params'] == null
? <String, Object?>{}
: castStringKeyedMap(args['params']);
final AppInstance? app = _getApp(appId);
if (app == null) {
throw DaemonException("app '$appId' not found");
}
final FlutterDevice device = app.runner.flutterDevices.first;
final List<FlutterView> views = await device.vmService!.getFlutterViews();
final Map<String, Object?>? result = await device.vmService!.invokeFlutterExtensionRpcRaw(
methodName,
args: params,
isolateId: views.first.uiIsolate!.id,
);
if (result == null) {
throw DaemonException('method not available: $methodName');
}
if (result.containsKey('error')) {
// ignore: only_throw_errors
throw result['error']!;
}
return result;
}
Future<bool> stop(Map<String, Object?> args) async {
final String? appId = _getStringArg(args, 'appId', required: true);
final AppInstance? app = _getApp(appId);
if (app == null) {
throw DaemonException("app '$appId' not found");
}
return app.stop().then<bool>(
(void value) => true,
onError: (Object? error, StackTrace stack) {
_sendAppEvent(app, 'log', <String, Object?>{'log': '$error', 'error': true});
app.closeLogger();
_apps.remove(app);
return false;
},
);
}
Future<bool> detach(Map<String, Object?> args) async {
final String? appId = _getStringArg(args, 'appId', required: true);
final AppInstance? app = _getApp(appId);
if (app == null) {
throw DaemonException("app '$appId' not found");
}
return app.detach().then<bool>(
(void value) => true,
onError: (Object? error, StackTrace stack) {
_sendAppEvent(app, 'log', <String, Object?>{'log': '$error', 'error': true});
app.closeLogger();
_apps.remove(app);
return false;
},
);
}
AppInstance? _getApp(String? id) {
for (final AppInstance app in _apps) {
if (app.id == id) {
return app;
}
}
return null;
}
void _sendAppEvent(AppInstance app, String name, [Map<String, Object?>? args]) {
sendEvent('app.$name', <String, Object?>{'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, {required this._logger, this._deviceManager})
: super(daemon, 'device') {
registerHandler('getDevices', getDevices);
registerHandler('discoverDevices', discoverDevices);
registerHandler('enable', enable);
registerHandler('disable', disable);
registerHandler('forward', forward);
registerHandler('unforward', unforward);
registerHandler('supportsRuntimeMode', supportsRuntimeMode);
registerHandler('uploadApplicationPackage', uploadApplicationPackage);
registerHandler('logReader.start', startLogReader);
registerHandler('logReader.stop', stopLogReader);
registerHandler('startApp', startApp);
registerHandler('stopApp', stopApp);
registerHandler('takeScreenshot', takeScreenshot);
registerHandler('startDartDevelopmentService', startDartDevelopmentService);
registerHandler('shutdownDartDevelopmentService', shutdownDartDevelopmentService);
registerHandler('getDiagnostics', getDiagnostics);
registerHandler('startVMServiceDiscoveryForAttach', startVMServiceDiscoveryForAttach);
registerHandler('stopVMServiceDiscoveryForAttach', stopVMServiceDiscoveryForAttach);
// Use the device manager discovery so that client provided device types
// are usable via the daemon protocol.
_deviceManager?.deviceDiscoverers.forEach(addDeviceDiscoverer);
}
final DeviceManager? _deviceManager;
final Logger _logger;
/// An incrementing number used to generate unique ids.
var _id = 0;
final _applicationPackages = <String, ApplicationPackage?>{};
final _logReaders = <String, DeviceLogReader>{};
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'));
}
}
var _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) {
_logger.printError('$err');
}
});
};
}
final _discoverers = <PollingDeviceDiscovery>[];
/// Return a list of the currently connected devices, with each device
/// represented as a map of properties (id, name, platform, ...).
Future<List<Map<String, Object?>>> getDevices([Map<String, Object?>? args]) async {
return <Map<String, Object?>>[
for (final PollingDeviceDiscovery discoverer in _discoverers)
for (final Device device in await discoverer.devices(filter: DeviceDiscoveryFilter()))
await _deviceToMap(device),
];
}
/// Return a list of the current devices, discarding existing cache of devices.
Future<List<Map<String, Object?>>> discoverDevices(Map<String, Object?> args) async {
final int? timeoutInMilliseconds = _getIntArg(args, 'timeoutInMilliseconds');
final Duration? timeout = timeoutInMilliseconds != null
? Duration(milliseconds: timeoutInMilliseconds)
: null;
// Calling `discoverDevices()` and `_deviceToMap()` in parallel for better performance.
final List<List<Device>> devicesListList = await Future.wait(<Future<List<Device>>>[
for (final PollingDeviceDiscovery discoverer in _discoverers)
discoverer.discoverDevices(timeout: timeout),
]);
final devices = <Device>[
for (final List<Device> devicesList in devicesListList) ...devicesList,
];
return Future.wait(<Future<Map<String, Object?>>>[
for (final Device device in devices) _deviceToMap(device),
]);
}
/// Enable device events.
Future<void> enable(Map<String, Object?> args) async {
for (final PollingDeviceDiscovery discoverer in _discoverers) {
discoverer.startPolling();
}
}
/// Disable device events.
Future<void> disable(Map<String, Object?> args) async {
for (final PollingDeviceDiscovery discoverer in _discoverers) {
discoverer.stopPolling();
}
}
/// Forward a host port to a device port.
Future<Map<String, Object?>> forward(Map<String, Object?> 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 DaemonException("device '$deviceId' not found");
}
hostPort = await device.portForwarder!.forward(devicePort, hostPort: hostPort);
return <String, Object?>{'hostPort': hostPort};
}
/// Removes a forwarded port.
Future<void> unforward(Map<String, Object?> 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 DaemonException("device '$deviceId' not found");
}
return device.portForwarder!.unforward(ForwardedPort(hostPort, devicePort));
}
/// Returns whether a device supports runtime mode.
Future<bool> supportsRuntimeMode(Map<String, Object?> args) async {
final String? deviceId = _getStringArg(args, 'deviceId', required: true);
final Device? device = await daemon.deviceDomain._getDevice(deviceId);
if (device == null) {
throw DaemonException("device '$deviceId' not found");
}
final String buildMode = _getStringArg(args, 'buildMode', required: true)!;
return await device.supportsRuntimeMode(BuildMode.fromCliName(buildMode));
}
/// Creates an application package from a file in the temp directory.
Future<String> uploadApplicationPackage(Map<String, Object?> args) async {
final targetPlatform = TargetPlatform.fromName(
_getStringArg(args, 'targetPlatform', required: true)!,
);
final File applicationBinary = daemon.proxyDomain.tempDirectory.childFile(
_getStringArg(args, 'applicationBinary', required: true)!,
);
final ApplicationPackage? applicationPackage = await ApplicationPackageFactory.instance!
.getPackageForPlatform(targetPlatform, applicationBinary: applicationBinary);
final id = 'application_package_${_id++}';
_applicationPackages[id] = applicationPackage;
return id;
}
/// Starts the log reader on the device.
Future<String> startLogReader(Map<String, Object?> args) async {
final String? deviceId = _getStringArg(args, 'deviceId', required: true);
final Device? device = await daemon.deviceDomain._getDevice(deviceId);
if (device == null) {
throw DaemonException("device '$deviceId' not found");
}
final String? applicationPackageId = _getStringArg(args, 'applicationPackageId');
final ApplicationPackage? applicationPackage = applicationPackageId != null
? _applicationPackages[applicationPackageId]
: null;
final id = '${deviceId}_${_id++}';
final DeviceLogReader logReader = await device.getLogReader(app: applicationPackage);
logReader.logLines.listen((String log) => sendEvent('device.logReader.logLines.$id', log));
_logReaders[id] = logReader;
return id;
}
/// Stops a log reader that was previously started.
Future<void> stopLogReader(Map<String, Object?> args) async {
final String? id = _getStringArg(args, 'id', required: true);
_logReaders.remove(id)?.dispose();
}
/// Starts an app on a device.
Future<Map<String, Object?>> startApp(Map<String, Object?> args) async {
final String? deviceId = _getStringArg(args, 'deviceId', required: true);
final Device? device = await daemon.deviceDomain._getDevice(deviceId);
if (device == null) {
throw DaemonException("device '$deviceId' not found");
}
final String? applicationPackageId = _getStringArg(
args,
'applicationPackageId',
required: true,
);
final ApplicationPackage applicationPackage = _applicationPackages[applicationPackageId!]!;
final LaunchResult result = await device.startApp(
applicationPackage,
debuggingOptions: DebuggingOptions.fromJson(
castStringKeyedMap(args['debuggingOptions'])!,
// We are using prebuilts, build info does not matter here.
BuildInfo.dummy,
),
mainPath: _getStringArg(args, 'mainPath'),
route: _getStringArg(args, 'route'),
platformArgs: castStringKeyedMap(args['platformArgs']) ?? const <String, Object>{},
prebuiltApplication: _getBoolArg(args, 'prebuiltApplication') ?? false,
userIdentifier: _getStringArg(args, 'userIdentifier'),
);
return <String, Object?>{
'started': result.started,
'vmServiceUri': result.vmServiceUri?.toString(),
};
}
/// Stops an app.
Future<bool> stopApp(Map<String, Object?> args) async {
final String? deviceId = _getStringArg(args, 'deviceId', required: true);
final Device? device = await daemon.deviceDomain._getDevice(deviceId);
if (device == null) {
throw DaemonException("device '$deviceId' not found");
}
final String? applicationPackageId = _getStringArg(args, 'applicationPackageId');
ApplicationPackage? applicationPackage;
if (applicationPackageId != null) {
applicationPackage = _applicationPackages[applicationPackageId];
}
return device.stopApp(
applicationPackage,
userIdentifier: _getStringArg(args, 'userIdentifier'),
);
}
/// Takes a screenshot.
Future<String?> takeScreenshot(Map<String, Object?> args) async {
final String? deviceId = _getStringArg(args, 'deviceId', required: true);
final Device? device = await daemon.deviceDomain._getDevice(deviceId);
if (device == null) {
throw DaemonException("device '$deviceId' not found");
}
final tempFileName = 'screenshot_${_id++}';
final File tempFile = daemon.proxyDomain.tempDirectory.childFile(tempFileName);
await device.takeScreenshot(tempFile);
if (tempFile.existsSync()) {
final String imageBase64 = base64.encode(await tempFile.readAsBytes());
return imageBase64;
} else {
return null;
}
}
/// Starts DDS for the device.
Future<Map<String, Object?>> startDartDevelopmentService(Map<String, Object?> args) async {
final String? deviceId = _getStringArg(args, 'deviceId', required: true);
final bool? disableServiceAuthCodes = _getBoolArg(args, 'disableServiceAuthCodes');
final String vmServiceUriStr = _getStringArg(args, 'vmServiceUri', required: true)!;
final bool enableDevTools = _getBoolArg(args, 'enableDevTools') ?? false;
final String? devToolsServerAddressStr = _getStringArg(args, 'devToolsServerAddress');
final Device? device = await daemon.deviceDomain._getDevice(deviceId);
if (device == null) {
throw DaemonException("device '$deviceId' not found");
}
Uri? devToolsServerAddress;
if (devToolsServerAddressStr != null) {
devToolsServerAddress = Uri.parse(devToolsServerAddressStr);
}
FlutterProject? project;
try {
project = FlutterProject.current();
} on ToolExit catch (_) {
// In daemon mode the cwd may not be a Flutter project, so we just ignore
// these errors and use 'Unknown' as the package name below.
}
await device.dds.startDartDevelopmentService(
Uri.parse(vmServiceUriStr),
appName:
'Kind: Flutter - Device: ${device.displayName} - '
'Package: ${project?.manifest.appName ?? 'Unknown'}',
disableServiceAuthCodes: disableServiceAuthCodes,
enableDevTools: enableDevTools,
devToolsServerAddress: devToolsServerAddress,
);
unawaited(device.dds.done.whenComplete(() => sendEvent('device.dds.done.$deviceId')));
return <String, Object?>{
'ddsUri': device.dds.uri?.toString(),
'devToolsUri': device.dds.devToolsUri?.toString(),
'dtdUri': device.dds.dtdUri?.toString(),
};
}
/// Starts DDS for the device.
Future<void> shutdownDartDevelopmentService(Map<String, Object?> args) async {
final String? deviceId = _getStringArg(args, 'deviceId', required: true);
final Device? device = await daemon.deviceDomain._getDevice(deviceId);
if (device == null) {
throw DaemonException("device '$deviceId' not found");
}
await device.dds.shutdown();
}
@override
Future<void> dispose() {
for (final PollingDeviceDiscovery discoverer in _discoverers) {
discoverer.dispose();
}
return Future<void>.value();
}
/// Return the connected device matching the deviceId field in the args.
Future<Device?> _getDevice(String? deviceId) async {
for (final PollingDeviceDiscovery discoverer in _discoverers) {
final List<Device> devices = await discoverer.devices(filter: DeviceDiscoveryFilter());
Device? device;
for (final localDevice in devices) {
if (localDevice.id == deviceId) {
device = localDevice;
}
}
if (device != null) {
return device;
}
}
return null;
}
/// Gets a list of diagnostic messages pertaining to issues with any connected
/// devices.
Future<List<String>> getDiagnostics(Map<String, Object?> args) async {
// Call `getDiagnostics()` in parallel to improve performance.
final List<List<String>> diagnosticsLists = await Future.wait(<Future<List<String>>>[
for (final PollingDeviceDiscovery discoverer in _discoverers) discoverer.getDiagnostics(),
]);
return <String>[for (final List<String> diagnostics in diagnosticsLists) ...diagnostics];
}
final _vmServiceDiscoverySubscriptions = <String, StreamSubscription<Uri>>{};
Future<String> startVMServiceDiscoveryForAttach(Map<String, Object?> args) async {
final String? deviceId = _getStringArg(args, 'deviceId', required: true);
final String? appId = _getStringArg(args, 'appId');
final String? fuchsiaModule = _getStringArg(args, 'fuchsiaModule');
final int? filterDevicePort = _getIntArg(args, 'filterDevicePort');
final bool? ipv6 = _getBoolArg(args, 'ipv6');
final Device? device = await daemon.deviceDomain._getDevice(deviceId);
if (device == null) {
throw DaemonException("device '$deviceId' not found");
}
final id = '${_id++}';
final VMServiceDiscoveryForAttach discovery = device.getVMServiceDiscoveryForAttach(
appId: appId,
fuchsiaModule: fuchsiaModule,
filterDevicePort: filterDevicePort,
ipv6: ipv6 ?? false,
logger: _logger,
);
_vmServiceDiscoverySubscriptions[id] = discovery.uris.listen(
(Uri uri) => sendEvent('device.VMServiceDiscoveryForAttach.$id', uri.toString()),
);
return id;
}
Future<void> stopVMServiceDiscoveryForAttach(Map<String, Object?> args) async {
final String? id = _getStringArg(args, 'id', required: true);
await _vmServiceDiscoverySubscriptions.remove(id)?.cancel();
}
}
class DevToolsDomain extends Domain {
DevToolsDomain(Daemon daemon) : super(daemon, 'devtools') {
registerHandler('serve', serve);
}
DevtoolsLauncher? _devtoolsLauncher;
Future<Map<String, Object?>> serve([Map<String, Object?>? args]) async {
_devtoolsLauncher ??= DevtoolsLauncher.instance;
final DevToolsServerAddress? server = await _devtoolsLauncher?.serve();
return <String, Object?>{'host': server?.host, 'port': server?.port};
}
@override
Future<void> dispose() async {
await _devtoolsLauncher?.close();
}
}
Future<Map<String, Object?>> _deviceToMap(Device device) async {
return <String, Object?>{
'id': device.id,
'name': device.displayName,
'platform': (await device.targetPlatform).getName(),
'emulator': await device.isLocalEmulator,
'category': device.category?.toString(),
'platformType': device.platformType?.toString(),
'cpuArch': (await device.cpuArch).name,
'ephemeral': device.ephemeral,
'emulatorId': await device.emulatorId,
'sdk': await device.sdkNameAndVersion,
'isConnected': device.isConnected,
'connectionInterface': getNameForDeviceConnectionInterface(device.connectionInterface),
'capabilities': <String, Object>{
'hotReload': device.supportsHotReload,
'hotRestart': device.supportsHotRestart,
'screenshot': device.supportsScreenshot,
// TODO(bkonyi): remove once fg3 is updated.
'fastStart': false,
'flutterExit': device.supportsFlutterExit,
'hardwareRendering': await device.supportsHardwareRendering,
'startPaused': device.supportsStartPaused,
},
};
}
Map<String, Object?> _emulatorToMap(Emulator emulator) {
return <String, Object?>{
'id': emulator.id,
'name': emulator.name,
'category': emulator.category.toString(),
'platformType': emulator.platformType.toString(),
};
}
Map<String, Object?> _operationResultToMap(OperationResult result) {
return <String, Object?>{'code': result.code, 'message': result.message};
}
Object? _toJsonable(Object? obj) {
return switch (obj) {
String() || int() || bool() || Map<Object?, Object?>() || List<Object?>() || null => obj,
OperationResult() => _operationResultToMap(obj),
ToolExit() => obj.message,
_ => obj.toString(),
};
}
class NotifyingLogger extends DelegatingLogger {
NotifyingLogger({required this.verbose, required Logger parent, this.notifyVerbose = false})
: super(parent) {
_messageController = StreamController<LogMessage>.broadcast(onListen: _onListen);
}
final bool verbose;
final messageBuffer = <LogMessage>[];
late StreamController<LogMessage> _messageController;
bool notifyVerbose = false;
void _onListen() {
if (messageBuffer.isNotEmpty) {
messageBuffer.forEach(_messageController.add);
messageBuffer.clear();
}
}
Stream<LogMessage> get onMessage => _messageController.stream;
@override
void printError(
String message, {
StackTrace? stackTrace,
bool? emphasis = false,
TerminalColor? color,
int? indent,
int? hangingIndent,
bool? wrap,
}) {
_sendMessage(LogMessage('error', message, stackTrace));
}
@override
void printWarning(
String message, {
bool? emphasis = false,
TerminalColor? color,
int? indent,
int? hangingIndent,
bool? wrap,
bool fatal = true,
}) {
_sendMessage(LogMessage('warning', message));
}
@override
void printStatus(
String message, {
bool? emphasis = false,
TerminalColor? color,
bool? newline = true,
int? indent,
int? hangingIndent,
bool? wrap,
}) {
_sendMessage(LogMessage('status', message));
}
@override
void printBox(String message, {String? title}) {
_sendMessage(LogMessage('status', title == null ? message : '$title: $message'));
}
@override
void printTrace(String message) {
if (notifyVerbose) {
_sendMessage(LogMessage('trace', message));
return;
}
if (!verbose) {
return;
}
super.printError(message);
}
@override
Status startProgress(
String message, {
Duration? timeout,
String? progressId,
bool multilineOutput = false,
bool includeTiming = true,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
assert(timeout != null);
printStatus(message);
return SilentStatus(stopwatch: Stopwatch());
}
void _sendMessage(LogMessage logMessage) {
if (_messageController.hasListener) {
return _messageController.add(logMessage);
}
messageBuffer.add(logMessage);
}
void dispose() {
_messageController.close();
}
@override
void sendEvent(String name, [Map<String, Object?>? args]) {}
@override
bool get supportsColor => false;
@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, {required this.runner, this.logToStdout = false, required this._logger}) {
// Nothing is obliged to await [started], so make sure a failure never
// surfaces as an unhandled async error.
_startedCompleter.future.ignore();
}
final String id;
final ResidentRunner runner;
final bool logToStdout;
final MachineOutputLogger _logger;
final _startedCompleter = Completer<void>();
/// Completes once [runner] reports the app has started, which is the same
/// signal the `app.started` event is sent from, or with an error if the app
/// exited before it got there.
///
/// For a runner with an incremental compiler this is after the initial
/// compile was accepted, which is what makes it safe to ask for a reload. It
/// does not mean the VM service is connected: the web runner attaches that
/// asynchronously afterwards, so the VM service may still be unattached once
/// this completes.
Future<void> get started => _startedCompleter.future;
void _markStarted() {
_startedCompleter.complete();
}
void _failedToStart(Object error, [StackTrace? stackTrace]) {
// Reachable after [_markStarted] when sending the `app.started` event is
// what threw, so this cannot assume the completer is still pending.
if (!_startedCompleter.isCompleted) {
_startedCompleter.completeError(error, stackTrace);
}
}
Future<OperationResult> restart({bool fullRestart = false, bool pause = false, String? reason}) {
return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason);
}
Future<void> stop() => runner.exit();
Future<void> detach() => runner.detach();
void closeLogger() {
_logger.close();
}
Future<T> _runInZone<T>(AppDomain domain, FutureOr<T> Function() method) async {
return method();
}
}
/// This domain responds to methods like [getEmulators] and [launch].
class EmulatorDomain extends Domain {
EmulatorDomain(
Daemon daemon, {
required AndroidWorkflow androidWorkflow,
required FileSystem fileSystem,
required Logger logger,
required ProcessManager processManager,
AndroidSdk? androidSdk,
EmulatorManager? emulatorManager,
Java? java,
}) : emulators =
emulatorManager ??
EmulatorManager(
fileSystem: fileSystem,
logger: logger,
java: java,
androidSdk: androidSdk,
processManager: processManager,
androidWorkflow: androidWorkflow,
),
super(daemon, 'emulator') {
registerHandler('getEmulators', getEmulators);
registerHandler('launch', launch);
registerHandler('create', create);
}
final EmulatorManager emulators;
Future<List<Map<String, Object?>>> getEmulators([Map<String, Object?>? args]) async {
final List<Emulator> list = await emulators.getAllAvailableEmulators();
return list.map<Map<String, Object?>>(_emulatorToMap).toList();
}
Future<void> launch(Map<String, Object?> args) async {
final String emulatorId = _getStringArg(args, 'emulatorId', required: true)!;
final bool coldBoot = _getBoolArg(args, 'coldBoot') ?? false;
final List<Emulator> matches = await emulators.getEmulatorsMatching(emulatorId);
if (matches.isEmpty) {
throw DaemonException("emulator '$emulatorId' not found");
} else if (matches.length > 1) {
throw DaemonException("multiple emulators match '$emulatorId'");
} else {
await matches.first.launch(coldBoot: coldBoot);
}
}
Future<Map<String, Object?>> create(Map<String, Object?> args) async {
final String? name = _getStringArg(args, 'name');
final CreateEmulatorResult res = await emulators.createEmulator(name: name);
return <String, Object?>{
'success': res.success,
'emulatorName': res.emulatorName,
'error': res.error,
};
}
}
class ProxyDomain extends Domain {
ProxyDomain(
Daemon daemon, {
required FileSystem fileSystem,
required this._fileTransfer,
required this._logger,
}) : _fs = fileSystem,
super(daemon, 'proxy') {
registerHandlerWithBinary('writeTempFile', writeTempFile);
registerHandler('calculateFileHashes', calculateFileHashes);
registerHandlerWithBinary('updateFile', updateFile);
registerHandler('connect', connect);
registerHandler('disconnect', disconnect);
registerHandlerWithBinary('write', write);
}
final FileTransfer _fileTransfer;
final FileSystem _fs;
final Logger _logger;
final _forwardedConnections = <String, Socket>{};
var _id = 0;
/// Writes to a file in a local temporary directory.
Future<void> writeTempFile(Map<String, Object?> args, Stream<List<int>>? binary) async {
final String path = _getStringArg(args, 'path', required: true)!;
final File file = tempDirectory.childFile(path);
await file.parent.create(recursive: true);
await file.openWrite().addStream(binary!);
}
/// Calculate rolling hashes for a file in the local temporary directory.
Future<Map<String, Object?>?> calculateFileHashes(Map<String, Object?> args) async {
final String path = _getStringArg(args, 'path', required: true)!;
final bool cacheResult = _getBoolArg(args, 'cacheResult') ?? false;
final File file = tempDirectory.childFile(path);
if (!file.existsSync()) {
return null;
}
final File hashFile = file.parent.childFile('${file.basename}.hashes');
if (hashFile.existsSync() && hashFile.statSync().modified.isAfter(file.statSync().modified)) {
// If the cached hash file is newer than the file, assume that the cached
// is up to date. Return the cached result directly.
final String cachedJson = await hashFile.readAsString();
return json.decode(cachedJson) as Map<String, Object?>;
}
final BlockHashes result = await _fileTransfer.calculateBlockHashesOfFile(file);
final Map<String, Object?> resultObject = result.toJson();
if (cacheResult) {
await hashFile.writeAsString(json.encode(resultObject));
}
return resultObject;
}
Future<bool?> updateFile(Map<String, Object?> args, Stream<List<int>>? binary) async {
final String path = _getStringArg(args, 'path', required: true)!;
final File file = tempDirectory.childFile(path);
if (!file.existsSync()) {
return null;
}
final List<Map<String, Object?>> deltaJson = (args['delta']! as List<Object?>)
.cast<Map<String, Object?>>();
final List<FileDeltaBlock> delta = FileDeltaBlock.fromJsonList(deltaJson);
final bool result = await _fileTransfer.rebuildFile(file, delta, binary!);
return result;
}
/// Opens a connection to a local port, and returns the connection id.
Future<String> connect(Map<String, Object?> args) async {
final int targetPort = _getIntArg(args, 'port', required: true)!;
final id = 'portForwarder_${targetPort}_${_id++}';
Socket? socket;
try {
socket = await Socket.connect(InternetAddress.loopbackIPv4, targetPort);
} on SocketException {
_logger.printTrace('Connecting to localhost:$targetPort failed with IPv4');
}
try {
// If connecting to IPv4 loopback interface fails, try IPv6.
socket ??= await Socket.connect(InternetAddress.loopbackIPv6, targetPort);
} on SocketException {
_logger.printError('Connecting to localhost:$targetPort failed');
}
if (socket == null) {
throw Exception('Failed to connect to the port');
}
_forwardedConnections[id] = socket;
debounceDataStream(socket).listen(
(List<int> data) {
sendEvent('proxy.data.$id', null, data);
},
onError: (Object error, StackTrace stackTrace) {
// Socket error, probably disconnected.
_logger.printTrace('Socket error: $error, $stackTrace');
},
);
unawaited(
socket.done
.then<Object?>(
(Object? obj) => obj,
onError: (Object error, StackTrace stackTrace) {
// Socket error, probably disconnected.
_logger.printTrace('Socket error: $error, $stackTrace');
},
)
.then((Object? _) {
sendEvent('proxy.disconnected.$id');
}),
);
return id;
}
/// Disconnects from a previously established connection.
Future<bool> disconnect(Map<String, Object?> args) async {
final String? id = _getStringArg(args, 'id', required: true);
if (_forwardedConnections.containsKey(id)) {
await _forwardedConnections.remove(id)?.close();
return true;
}
return false;
}
/// Writes to a previously established connection.
Future<bool> write(Map<String, Object?> args, Stream<List<int>>? binary) async {
final String? id = _getStringArg(args, 'id', required: true);
if (_forwardedConnections.containsKey(id)) {
final StreamSubscription<List<int>> subscription = binary!.listen(
_forwardedConnections[id!]!.add,
);
await subscription.asFuture<void>();
await subscription.cancel();
return true;
}
return false;
}
@override
Future<void> dispose() async {
for (final Socket connection in _forwardedConnections.values) {
connection.destroy();
}
// We deliberately not clean up the tempDirectory here. The application package files that
// are transferred into this directory through ProxiedDevices are left in the directory
// to be reused on any subsequent runs.
}
Directory? _tempDirectory;
Directory get tempDirectory =>
_tempDirectory ??= _fs.systemTempDirectory.childDirectory('flutter_tool_daemon')
..createSync();
}
/// A [Logger] which omits log messages to avoid breaking `--machine` formatting.
final class MachineOutputLogger extends DelegatingLogger {
MachineOutputLogger({required Logger parent}) : super(parent);
@override
bool get isMachine => true;
AppDomain? _domain;
late final AppInstance _app;
var _nextProgressId = 0;
Status? _status;
@override
Status startProgress(
String message, {
Duration? timeout,
String? progressId,
bool multilineOutput = false,
bool includeTiming = true,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
final int id = _nextProgressId++;
_sendProgressEvent(eventId: id.toString(), eventType: progressId, message: message);
_status = SilentStatus(
onFinish: () {
_status = null;
_sendProgressEvent(eventId: id.toString(), eventType: progressId, finished: true);
},
stopwatch: Stopwatch(),
)..start();
return _status!;
}
void close() {
_domain = null;
}
void _sendProgressEvent({
required String eventId,
required String? eventType,
bool finished = false,
String? message,
}) {
if (_domain case final domain?) {
final event = <String, Object?>{
'id': eventId,
'progressId': eventType,
'message': ?message,
'finished': finished,
};
domain._sendAppEvent(_app, 'progress', event);
}
}
@override
void sendEvent(String name, [Map<String, Object?>? args, List<int>? binary]) {
if (_domain case final domain?) {
domain.sendEvent(name, args, binary);
}
}
@override
bool get supportsColor => false;
@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.
enum LaunchMode {
run._('run'),
attach._('attach');
const LaunchMode._(this._value);
final String _value;
@override
String toString() => _value;
}
enum OperationType { reload, restart }
/// A queue that debounces operations for a period and merges operations of the same type.
/// Only one action (or any type) will run at a time. Actions of the same type requested
/// in quick succession will be merged together and all return the same result. If an action
/// is requested after an identical action has already started, it will be queued
/// and run again once the first action completes.
class DebounceOperationQueue<T, K> {
final _debounceTimers = <K, RestartableTimer>{};
final _operationQueue = <K, Future<T>>{};
Future<void>? _inProgressAction;
Future<T> queueAndDebounce(
K operationType,
Duration debounceDuration,
Future<T> Function() action,
) {
// If there is already an operation of this type waiting to run, reset its
// debounce timer and return its future.
if (_operationQueue[operationType] != null) {
_debounceTimers[operationType]?.reset();
return _operationQueue[operationType]!;
}
// Otherwise, put one in the queue with a timer.
final completer = Completer<T>();
_operationQueue[operationType] = completer.future;
_debounceTimers[operationType] = RestartableTimer(debounceDuration, () async {
// Remove us from the queue so we can't be reset now we've started.
unawaited(_operationQueue.remove(operationType));
_debounceTimers.remove(operationType);
// No operations should be allowed to run concurrently even if they're
// different types.
while (_inProgressAction != null) {
await _inProgressAction;
}
_inProgressAction = action()
.then(completer.complete, onError: completer.completeError)
.whenComplete(() => _inProgressAction = null);
});
return completer.future;
}
}
/// Specialized exception for returning errors to the daemon client.
class DaemonException implements Exception {
DaemonException(this.message);
final String message;
@override
String toString() => message;
}