| // 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:process/process.dart'; |
| |
| import '../application_package.dart'; |
| import '../base/common.dart'; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/logger.dart'; |
| import '../base/process.dart'; |
| import '../base/utils.dart'; |
| import '../build_info.dart'; |
| import '../bundle.dart'; |
| import '../bundle_builder.dart'; |
| import '../convert.dart'; |
| import '../device.dart'; |
| import '../device_port_forwarder.dart'; |
| import '../features.dart'; |
| import '../project.dart'; |
| import '../protocol_discovery.dart'; |
| import 'custom_device_config.dart'; |
| import 'custom_device_workflow.dart'; |
| import 'custom_devices_config.dart'; |
| |
| /// Replace all occurrences of `${someName}` with the value found for that |
| /// name inside replacementValues or additionalReplacementValues. |
| /// |
| /// The replacement value is first looked for in [replacementValues] and then |
| /// [additionalReplacementValues]. If no value is found, an empty string will be |
| /// substituted instead. |
| List<String> interpolateCommand( |
| List<String> command, |
| Map<String, String> replacementValues, { |
| Map<String, String> additionalReplacementValues = const <String, String>{} |
| }) { |
| return interpolateStringList( |
| command, |
| Map<String, String>.of(additionalReplacementValues) |
| ..addAll(replacementValues) |
| ); |
| } |
| |
| /// A log reader that can listen to a process' stdout / stderr or another log line |
| /// Stream. |
| class CustomDeviceLogReader extends DeviceLogReader { |
| CustomDeviceLogReader(this.name); |
| |
| /// The name of the device this log reader is associated with. |
| @override |
| final String name; |
| |
| @visibleForTesting |
| final StreamController<String> logLinesController = StreamController<String>.broadcast(); |
| |
| @visibleForTesting |
| final List<StreamSubscription<String>> subscriptions = <StreamSubscription<String>>[]; |
| |
| /// Listen to [process]' stdout and stderr, decode them using [SystemEncoding] |
| /// and add each decoded line to [logLines]. |
| /// |
| /// However, [logLines] will not be done when the [process]' stdout and stderr |
| /// streams are done. So [logLines] will still be alive after the process has |
| /// finished. |
| /// |
| /// See [CustomDeviceLogReader.dispose] to end the [logLines] stream. |
| void listenToProcessOutput(Process process, {Encoding encoding = systemEncoding}) { |
| final Converter<List<int>, String> decoder = encoding.decoder; |
| |
| subscriptions.add( |
| process.stdout.transform<String>(decoder) |
| .transform<String>(const LineSplitter()) |
| .listen(logLinesController.add), |
| ); |
| |
| subscriptions.add( |
| process.stderr.transform<String>(decoder) |
| .transform<String>(const LineSplitter()) |
| .listen(logLinesController.add) |
| ); |
| } |
| |
| /// Add all lines emitted by [lines] to this [CustomDeviceLogReader]s [logLines] |
| /// stream. |
| /// |
| /// Similar to [listenToProcessOutput], [logLines] will not be marked as done |
| /// when the argument stream is done. |
| /// |
| /// Useful when you want to combine the contents of multiple log readers. |
| void listenToLinesStream(Stream<String> lines) { |
| subscriptions.add( |
| lines.listen(logLinesController.add) |
| ); |
| } |
| |
| /// Dispose this log reader, freeing all associated resources and marking |
| /// [logLines] as done. |
| @override |
| Future<void> dispose() async { |
| final List<Future<void>> futures = <Future<void>>[]; |
| |
| for (final StreamSubscription<String> subscription in subscriptions) { |
| futures.add(subscription.cancel()); |
| } |
| |
| futures.add(logLinesController.close()); |
| |
| await Future.wait(futures); |
| } |
| |
| @override |
| Stream<String> get logLines => logLinesController.stream; |
| } |
| |
| /// A [DevicePortForwarder] that uses commands to forward / unforward a port. |
| class CustomDevicePortForwarder extends DevicePortForwarder { |
| CustomDevicePortForwarder({ |
| required String deviceName, |
| required List<String> forwardPortCommand, |
| required RegExp forwardPortSuccessRegex, |
| this.numTries, |
| required ProcessManager processManager, |
| required Logger logger, |
| Map<String, String> additionalReplacementValues = const <String, String>{} |
| }) : _deviceName = deviceName, |
| _forwardPortCommand = forwardPortCommand, |
| _forwardPortSuccessRegex = forwardPortSuccessRegex, |
| _processManager = processManager, |
| _processUtils = ProcessUtils( |
| processManager: processManager, |
| logger: logger |
| ), |
| _additionalReplacementValues = additionalReplacementValues; |
| |
| final String _deviceName; |
| final List<String> _forwardPortCommand; |
| final RegExp _forwardPortSuccessRegex; |
| final ProcessManager _processManager; |
| final ProcessUtils _processUtils; |
| final int? numTries; |
| final Map<String, String> _additionalReplacementValues; |
| final List<ForwardedPort> _forwardedPorts = <ForwardedPort>[]; |
| |
| @override |
| Future<void> dispose() async { |
| // copy the list so we don't modify it concurrently |
| await Future.wait(List<ForwardedPort>.of(_forwardedPorts).map(unforward)); |
| } |
| |
| Future<ForwardedPort?> tryForward(int devicePort, int hostPort) async { |
| final List<String> interpolated = interpolateCommand( |
| _forwardPortCommand, |
| <String, String>{ |
| 'devicePort': '$devicePort', |
| 'hostPort': '$hostPort' |
| }, |
| additionalReplacementValues: _additionalReplacementValues |
| ); |
| |
| // launch the forwarding command |
| final Process process = await _processUtils.start(interpolated); |
| |
| final Completer<ForwardedPort?> completer = Completer<ForwardedPort?>(); |
| |
| // read the outputs of the process, if we find a line that matches |
| // the configs forwardPortSuccessRegex, we complete with a successfully |
| // forwarded port |
| // Note that if that regex never matches, this will potentially run forever |
| // and the forwarding will never complete |
| final CustomDeviceLogReader reader = CustomDeviceLogReader(_deviceName)..listenToProcessOutput(process); |
| final StreamSubscription<String> logLinesSubscription = reader.logLines.listen((String line) { |
| if (_forwardPortSuccessRegex.hasMatch(line) && !completer.isCompleted) { |
| completer.complete( |
| ForwardedPort.withContext(hostPort, devicePort, process) |
| ); |
| } |
| }); |
| |
| // if the process exits (even with exitCode == 0), that is considered |
| // a port forwarding failure and we complete with a null value. |
| unawaited(process.exitCode.whenComplete(() { |
| if (!completer.isCompleted) { |
| completer.complete(null); |
| } |
| })); |
| |
| unawaited(completer.future.whenComplete(() { |
| unawaited(logLinesSubscription.cancel()); |
| unawaited(reader.dispose()); |
| })); |
| |
| return completer.future; |
| } |
| |
| @override |
| Future<int> forward(int devicePort, {int? hostPort}) async { |
| int actualHostPort = (hostPort == 0 || hostPort == null) ? devicePort : hostPort; |
| int tries = 0; |
| |
| while ((numTries == null) || (tries < numTries!)) { |
| // when the desired host port is already forwarded by this Forwarder, |
| // choose another one |
| while (_forwardedPorts.any((ForwardedPort port) => port.hostPort == actualHostPort)) { |
| actualHostPort += 1; |
| } |
| |
| final ForwardedPort? port = await tryForward(devicePort, actualHostPort); |
| |
| if (port != null) { |
| _forwardedPorts.add(port); |
| return actualHostPort; |
| } else { |
| // null value means the forwarding failed (for whatever reason) |
| // increase port by one and try again |
| actualHostPort += 1; |
| tries += 1; |
| } |
| } |
| |
| throw ToolExit('Forwarding port for custom device $_deviceName failed after $tries tries.'); |
| } |
| |
| @override |
| List<ForwardedPort> get forwardedPorts => List<ForwardedPort>.unmodifiable(_forwardedPorts); |
| |
| @override |
| Future<void> unforward(ForwardedPort forwardedPort) async { |
| assert(_forwardedPorts.contains(forwardedPort)); |
| |
| // since a forwarded port represents a running process launched with |
| // the forwardPortCommand, unforwarding is as easy as killing the process |
| final int? pid = forwardedPort.context?.pid; |
| if (pid != null) { |
| _processManager.killPid(pid); |
| } |
| _forwardedPorts.remove(forwardedPort); |
| } |
| } |
| |
| /// A combination of [ApplicationPackage] and a [CustomDevice]. Can only start, |
| /// stop this specific app package with this specific device. Useful because we |
| /// often need to store additional context to an app that is running on a device, |
| /// like any forwarded ports we need to unforward later, the process we need to |
| /// kill to stop the app, maybe other things in the future. |
| class CustomDeviceAppSession { |
| CustomDeviceAppSession({ |
| required this.name, |
| required CustomDevice device, |
| required ApplicationPackage appPackage, |
| required Logger logger, |
| required ProcessManager processManager |
| }) : _appPackage = appPackage, |
| _device = device, |
| _logger = logger, |
| _processManager = processManager, |
| _processUtils = ProcessUtils( |
| processManager: processManager, |
| logger: logger |
| ), |
| logReader = CustomDeviceLogReader(name); |
| |
| final String name; |
| final CustomDevice _device; |
| final ApplicationPackage _appPackage; |
| final Logger _logger; |
| final ProcessManager _processManager; |
| final ProcessUtils _processUtils; |
| final CustomDeviceLogReader logReader; |
| |
| Process? _process; |
| int? _forwardedHostPort; |
| |
| /// Get the engine options for the given [debuggingOptions], |
| /// [traceStartup] and [route]. |
| /// |
| /// [debuggingOptions] and [route] can be null. |
| /// |
| /// For example, `_getEngineOptions(null, false, null)` will return |
| /// `['enable-dart-profiling=true']` |
| List<String> _getEngineOptions(DebuggingOptions debuggingOptions, bool traceStartup, String? route) { |
| final List<String> options = <String>[]; |
| |
| void addFlag(String value) { |
| options.add(value); |
| } |
| |
| addFlag('enable-dart-profiling=true'); |
| |
| if (traceStartup) { |
| addFlag('trace-startup=true'); |
| } |
| if (route != null) { |
| addFlag('route=$route'); |
| } |
| if (debuggingOptions != null) { |
| if (debuggingOptions.enableSoftwareRendering) { |
| addFlag('enable-software-rendering=true'); |
| } |
| if (debuggingOptions.skiaDeterministicRendering) { |
| addFlag('skia-deterministic-rendering=true'); |
| } |
| if (debuggingOptions.traceSkia) { |
| addFlag('trace-skia=true'); |
| } |
| if (debuggingOptions.traceAllowlist != null) { |
| addFlag('trace-allowlist=${debuggingOptions.traceAllowlist}'); |
| } |
| if (debuggingOptions.traceSystrace) { |
| addFlag('trace-systrace=true'); |
| } |
| if (debuggingOptions.endlessTraceBuffer) { |
| addFlag('endless-trace-buffer=true'); |
| } |
| if (debuggingOptions.dumpSkpOnShaderCompilation) { |
| addFlag('dump-skp-on-shader-compilation=true'); |
| } |
| if (debuggingOptions.cacheSkSL) { |
| addFlag('cache-sksl=true'); |
| } |
| if (debuggingOptions.purgePersistentCache) { |
| addFlag('purge-persistent-cache=true'); |
| } |
| // Options only supported when there is a VM Service connection between the |
| // tool and the device, usually in debug or profile mode. |
| if (debuggingOptions.debuggingEnabled) { |
| if (debuggingOptions.deviceVmServicePort != null) { |
| addFlag('observatory-port=${debuggingOptions.deviceVmServicePort}'); |
| } |
| if (debuggingOptions.buildInfo.isDebug) { |
| addFlag('enable-checked-mode=true'); |
| addFlag('verify-entry-points=true'); |
| } |
| if (debuggingOptions.startPaused) { |
| addFlag('start-paused=true'); |
| } |
| if (debuggingOptions.disableServiceAuthCodes) { |
| addFlag('disable-service-auth-codes=true'); |
| } |
| final String dartVmFlags = computeDartVmFlags(debuggingOptions); |
| if (dartVmFlags.isNotEmpty) { |
| addFlag('dart-flags=$dartVmFlags'); |
| } |
| if (debuggingOptions.useTestFonts) { |
| addFlag('use-test-fonts=true'); |
| } |
| if (debuggingOptions.verboseSystemLogs) { |
| addFlag('verbose-logging=true'); |
| } |
| } |
| } |
| |
| return options; |
| } |
| |
| /// Get the engine options for the given [debuggingOptions], |
| /// [traceStartup] and [route]. |
| /// |
| /// [debuggingOptions] and [route] can be null. |
| /// |
| /// For example, `_getEngineOptionsForCmdline(null, false, null)` will return |
| /// `--enable-dart-profiling=true` |
| String _getEngineOptionsForCmdline(DebuggingOptions debuggingOptions, bool traceStartup, String? route) { |
| return _getEngineOptions(debuggingOptions, traceStartup, route).map((String e) => '--$e').join(' '); |
| } |
| |
| /// Start the app on the device. |
| /// Needs the app to be installed on the device and not running already. |
| /// |
| /// [mainPath], [route], [debuggingOptions], [platformArgs] and |
| /// [userIdentifier] may be null. |
| /// |
| /// [ipv6] may not be respected since it depends on the device config whether |
| /// it uses ipv6 or ipv4 |
| Future<LaunchResult> start({ |
| String? mainPath, |
| String? route, |
| required DebuggingOptions debuggingOptions, |
| Map<String, Object?> platformArgs = const <String, Object>{}, |
| bool prebuiltApplication = false, |
| bool ipv6 = false, |
| String? userIdentifier |
| }) async { |
| final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false; |
| final String? packageName = _appPackage.name; |
| if (packageName == null) { |
| throw ToolExit('Could not start app, name for $_appPackage is unknown.'); |
| } |
| final List<String> interpolated = interpolateCommand( |
| _device._config.runDebugCommand, |
| <String, String>{ |
| 'remotePath': '/tmp/', |
| 'appName': packageName, |
| 'engineOptions': _getEngineOptionsForCmdline(debuggingOptions, traceStartup, route) |
| } |
| ); |
| |
| final Process process = await _processUtils.start(interpolated); |
| assert(_process == null); |
| _process = process; |
| |
| final ProtocolDiscovery discovery = ProtocolDiscovery.observatory( |
| logReader, |
| portForwarder: _device._config.usesPortForwarding ? _device.portForwarder : null, |
| logger: _logger, |
| ipv6: ipv6, |
| ); |
| |
| // We need to make the discovery listen to the logReader before the logReader |
| // listens to the process output since logReader.lines is a broadcast stream |
| // and events may be discarded. |
| // Whether that actually happens is another thing since this is all executed |
| // in the same microtask AFAICT but this way we're on the safe side. |
| logReader.listenToProcessOutput(process); |
| |
| final Uri? observatoryUri = await discovery.uri; |
| await discovery.cancel(); |
| |
| if (_device._config.usesPortForwarding) { |
| _forwardedHostPort = observatoryUri?.port; |
| } |
| |
| return LaunchResult.succeeded(observatoryUri: observatoryUri); |
| } |
| |
| void _maybeUnforwardPort() { |
| if (_forwardedHostPort != null) { |
| final ForwardedPort forwardedPort = _device.portForwarder.forwardedPorts.singleWhere((ForwardedPort forwardedPort) { |
| return forwardedPort.hostPort == _forwardedHostPort; |
| }); |
| |
| _forwardedHostPort = null; |
| _device.portForwarder.unforward(forwardedPort); |
| } |
| } |
| |
| /// Stop the app on the device. |
| /// Returns false if the app is not yet running. Also unforwards any |
| /// forwarded ports. |
| Future<bool> stop() async { |
| if (_process == null) { |
| return false; |
| } |
| |
| _maybeUnforwardPort(); |
| final bool result = _processManager.killPid(_process!.pid); |
| _process = null; |
| return result; |
| } |
| |
| void dispose() { |
| if (_process != null) { |
| _maybeUnforwardPort(); |
| _processManager.killPid(_process!.pid); |
| _process = null; |
| } |
| |
| unawaited(logReader.dispose()); |
| } |
| } |
| |
| /// A device that uses user-configured actions for the common device methods. |
| /// The exact actions are defined by the contents of the [CustomDeviceConfig] |
| /// used to construct it. |
| class CustomDevice extends Device { |
| CustomDevice({ |
| required CustomDeviceConfig config, |
| required Logger logger, |
| required ProcessManager processManager, |
| }) : _config = config, |
| _logger = logger, |
| _processManager = processManager, |
| _processUtils = ProcessUtils( |
| processManager: processManager, |
| logger: logger |
| ), |
| _globalLogReader = CustomDeviceLogReader(config.label), |
| portForwarder = config.usesPortForwarding ? |
| CustomDevicePortForwarder( |
| deviceName: config.label, |
| forwardPortCommand: config.forwardPortCommand!, |
| forwardPortSuccessRegex: config.forwardPortSuccessRegex!, |
| processManager: processManager, |
| logger: logger, |
| ) : const NoOpDevicePortForwarder(), |
| super( |
| config.id, |
| category: Category.mobile, |
| ephemeral: true, |
| platformType: PlatformType.custom |
| ); |
| |
| final CustomDeviceConfig _config; |
| final Logger _logger; |
| final ProcessManager _processManager; |
| final ProcessUtils _processUtils; |
| final Map<ApplicationPackage, CustomDeviceAppSession> _sessions = <ApplicationPackage, CustomDeviceAppSession>{}; |
| final CustomDeviceLogReader _globalLogReader; |
| |
| @override |
| final DevicePortForwarder portForwarder; |
| |
| CustomDeviceAppSession _getOrCreateAppSession(covariant ApplicationPackage app) { |
| return _sessions.putIfAbsent( |
| app, |
| () { |
| /// create a new session and add its logging to the global log reader. |
| /// (needed bc it's possible the infra requests a global log in [getLogReader] |
| final CustomDeviceAppSession session = CustomDeviceAppSession( |
| name: name, |
| device: this, |
| appPackage: app, |
| logger: _logger, |
| processManager: _processManager |
| ); |
| |
| _globalLogReader.listenToLinesStream(session.logReader.logLines); |
| |
| return session; |
| } |
| ); |
| } |
| |
| /// Tries to ping the device using the ping command given in the config. |
| /// All string interpolation occurrences inside the ping command will be replaced |
| /// using the entries in [replacementValues]. |
| /// |
| /// If the process finishes with an exit code != 0, false will be returned and |
| /// the error (with the process' stdout and stderr) will be logged using |
| /// [_logger.printError]. |
| /// |
| /// If [timeout] is not null and the process doesn't finish in time, |
| /// it will be killed with a SIGTERM, false will be returned and the timeout |
| /// will be reported in the log using [_logger.printError]. If [timeout] |
| /// is null, it's treated as if it's an infinite timeout. |
| Future<bool> tryPing({ |
| Duration? timeout, |
| Map<String, String> replacementValues = const <String, String>{} |
| }) async { |
| final List<String> interpolated = interpolateCommand( |
| _config.pingCommand, |
| replacementValues |
| ); |
| |
| final RunResult result = await _processUtils.run( |
| interpolated, |
| timeout: timeout |
| ); |
| |
| if (result.exitCode != 0) { |
| return false; |
| } |
| |
| // If the user doesn't configure a ping success regex, any ping with exitCode zero |
| // is good enough. Otherwise we check if either stdout or stderr have a match of |
| // the pingSuccessRegex. |
| final RegExp? pingSuccessRegex = _config.pingSuccessRegex; |
| return pingSuccessRegex == null |
| || pingSuccessRegex.hasMatch(result.stdout) |
| || pingSuccessRegex.hasMatch(result.stderr); |
| } |
| |
| /// Tries to execute the configs postBuild command using [appName] for the |
| /// `${appName}` and [localPath] for the `${localPath}` interpolations, |
| /// any additional string interpolation occurrences will be replaced using the |
| /// entries in [additionalReplacementValues]. |
| /// |
| /// Calling this when the config doesn't have a configured postBuild command |
| /// is an error. |
| /// |
| /// If [timeout] is not null and the process doesn't finish in time, it |
| /// will be killed with a SIGTERM, false will be returned and the timeout |
| /// will be reported in the log using [_logger.printError]. If [timeout] |
| /// is null, it's treated as if it's an infinite timeout. |
| Future<bool> _tryPostBuild({ |
| required String appName, |
| required String localPath, |
| Duration? timeout, |
| Map<String, String> additionalReplacementValues = const <String, String>{} |
| }) async { |
| assert(_config.postBuildCommand != null); |
| |
| final List<String> interpolated = interpolateCommand( |
| _config.postBuildCommand!, |
| <String, String>{ |
| 'appName': appName, |
| 'localPath': localPath |
| }, |
| additionalReplacementValues: additionalReplacementValues |
| ); |
| |
| try { |
| await _processUtils.run( |
| interpolated, |
| throwOnError: true, |
| timeout: timeout |
| ); |
| return true; |
| } on ProcessException catch (e) { |
| _logger.printError('Error executing postBuild command for custom device $id: $e'); |
| return false; |
| } |
| } |
| |
| /// Tries to execute the configs uninstall command. |
| /// |
| /// [appName] is the name of the app to be installed. |
| /// |
| /// If [timeout] is not null and the process doesn't finish in time, it |
| /// will be killed with a SIGTERM, false will be returned and the timeout |
| /// will be reported in the log using [_logger.printError]. If [timeout] |
| /// is null, it's treated as if it's an infinite timeout. |
| Future<bool> tryUninstall({ |
| required String appName, |
| Duration? timeout, |
| Map<String, String> additionalReplacementValues = const <String, String>{} |
| }) async { |
| final List<String> interpolated = interpolateCommand( |
| _config.uninstallCommand, |
| <String, String>{ |
| 'appName': appName |
| }, |
| additionalReplacementValues: additionalReplacementValues |
| ); |
| |
| try { |
| await _processUtils.run( |
| interpolated, |
| throwOnError: true, |
| timeout: timeout |
| ); |
| return true; |
| } on ProcessException catch (e) { |
| _logger.printError('Error executing uninstall command for custom device $id: $e'); |
| return false; |
| } |
| } |
| |
| /// Tries to install the app to the custom device. |
| /// |
| /// [localPath] is the file / directory on the local device that will be |
| /// copied over to the target custom device. This is substituted for any occurrence |
| /// of `${localPath}` in the custom device configs `install` command. |
| /// |
| /// [appName] is the name of the app to be installed. Substituted for any occurrence |
| /// of `${appName}` in the custom device configs `install` command. |
| Future<bool> tryInstall({ |
| required String localPath, |
| required String appName, |
| Duration? timeout, |
| Map<String, String> additionalReplacementValues = const <String, String>{} |
| }) async { |
| final List<String> interpolated = interpolateCommand( |
| _config.installCommand, |
| <String, String>{ |
| 'localPath': localPath, |
| 'appName': appName |
| }, |
| additionalReplacementValues: additionalReplacementValues |
| ); |
| |
| try { |
| await _processUtils.run( |
| interpolated, |
| throwOnError: true, |
| timeout: timeout |
| ); |
| |
| return true; |
| } on ProcessException catch (e) { |
| _logger.printError('Error executing install command for custom device $id: $e'); |
| return false; |
| } |
| } |
| |
| @override |
| void clearLogs() {} |
| |
| @override |
| Future<void> dispose() async { |
| _sessions |
| ..forEach((_, CustomDeviceAppSession session) => session.dispose()) |
| ..clear(); |
| } |
| |
| @override |
| Future<String?> get emulatorId async => null; |
| |
| @override |
| FutureOr<DeviceLogReader> getLogReader({ |
| covariant ApplicationPackage? app, |
| bool includePastLogs = false |
| }) { |
| if (app != null) { |
| return _getOrCreateAppSession(app).logReader; |
| } |
| |
| return _globalLogReader; |
| } |
| |
| @override |
| Future<bool> installApp(covariant ApplicationPackage app, {String? userIdentifier}) async { |
| final String? appName = app.name; |
| if (appName == null || !await tryUninstall(appName: appName)) { |
| return false; |
| } |
| |
| final bool result = await tryInstall( |
| localPath: getAssetBuildDirectory(), |
| appName: appName, |
| ); |
| |
| return result; |
| } |
| |
| @override |
| Future<bool> isAppInstalled(covariant ApplicationPackage app, {String? userIdentifier}) async { |
| return false; |
| } |
| |
| @override |
| Future<bool> isLatestBuildInstalled(covariant ApplicationPackage app) async { |
| return false; |
| } |
| |
| @override |
| Future<bool> get isLocalEmulator async => false; |
| |
| @override |
| bool get supportsScreenshot => _config.supportsScreenshotting; |
| |
| @override |
| Future<void> takeScreenshot(File outputFile) async { |
| if (supportsScreenshot == false) { |
| throw UnsupportedError('Screenshotting is not supported for this device.'); |
| } |
| |
| final List<String> interpolated = interpolateCommand( |
| _config.screenshotCommand!, |
| <String, String>{}, |
| ); |
| |
| final RunResult result = await _processUtils.run(interpolated, throwOnError: true); |
| await outputFile.writeAsBytes(base64Decode(result.stdout)); |
| } |
| |
| @override |
| bool isSupported() { |
| return true; |
| } |
| |
| @override |
| bool isSupportedForProject(FlutterProject flutterProject) { |
| return true; |
| } |
| |
| @override |
| FutureOr<bool> supportsRuntimeMode(BuildMode buildMode) { |
| return buildMode == BuildMode.debug; |
| } |
| |
| @override |
| String get name => _config.label; |
| |
| @override |
| Future<String> get sdkNameAndVersion => Future<String>.value(_config.sdkNameAndVersion); |
| |
| @override |
| Future<LaunchResult> startApp( |
| covariant ApplicationPackage package, { |
| String? mainPath, |
| String? route, |
| required DebuggingOptions debuggingOptions, |
| Map<String, Object?> platformArgs = const <String, Object>{}, |
| bool prebuiltApplication = false, |
| bool ipv6 = false, |
| String? userIdentifier, |
| BundleBuilder? bundleBuilder, |
| }) async { |
| if (!prebuiltApplication) { |
| final String assetBundleDir = getAssetBuildDirectory(); |
| |
| bundleBuilder ??= BundleBuilder(); |
| |
| // this just builds the asset bundle, it's the same as `flutter build bundle` |
| await bundleBuilder.build( |
| platform: await targetPlatform, |
| buildInfo: debuggingOptions.buildInfo, |
| mainPath: mainPath, |
| depfilePath: defaultDepfilePath, |
| assetDirPath: assetBundleDir, |
| ); |
| |
| // if we have a post build step (needed for some embedders), execute it |
| if (_config.postBuildCommand != null) { |
| final String? packageName = package.name; |
| if (packageName == null) { |
| throw ToolExit('Could not start app, name for $package is unknown.'); |
| } |
| await _tryPostBuild( |
| appName: packageName, |
| localPath: assetBundleDir, |
| ); |
| } |
| } |
| |
| // install the app on the device |
| // (will invoke the uninstall and then the install command internally) |
| await installApp(package, userIdentifier: userIdentifier); |
| |
| // finally launch the app |
| return _getOrCreateAppSession(package).start( |
| mainPath: mainPath, |
| route: route, |
| debuggingOptions: debuggingOptions, |
| platformArgs: platformArgs, |
| prebuiltApplication: prebuiltApplication, |
| ipv6: ipv6, |
| userIdentifier: userIdentifier, |
| ); |
| } |
| |
| @override |
| Future<bool> stopApp(covariant ApplicationPackage app, {String? userIdentifier}) { |
| return _getOrCreateAppSession(app).stop(); |
| } |
| |
| @override |
| Future<TargetPlatform> get targetPlatform async => _config.platform ?? TargetPlatform.linux_arm64; |
| |
| @override |
| Future<bool> uninstallApp(covariant ApplicationPackage app, {String? userIdentifier}) async { |
| final String? appName = app.name; |
| if (appName == null) { |
| return false; |
| } |
| return tryUninstall(appName: appName); |
| } |
| } |
| |
| /// A [PollingDeviceDiscovery] that'll try to ping all enabled devices in the argument |
| /// [CustomDevicesConfig] and report the ones that were actually reachable. |
| class CustomDevices extends PollingDeviceDiscovery { |
| /// Create a custom device discovery that pings all enabled devices in the |
| /// given [CustomDevicesConfig]. |
| CustomDevices({ |
| required FeatureFlags featureFlags, |
| required ProcessManager processManager, |
| required Logger logger, |
| required CustomDevicesConfig config |
| }) : _customDeviceWorkflow = CustomDeviceWorkflow( |
| featureFlags: featureFlags, |
| ), |
| _logger = logger, |
| _processManager = processManager, |
| _config = config, |
| super('custom devices'); |
| |
| final CustomDeviceWorkflow _customDeviceWorkflow; |
| final ProcessManager _processManager; |
| final Logger _logger; |
| final CustomDevicesConfig _config; |
| |
| @override |
| bool get supportsPlatform => true; |
| |
| @override |
| bool get canListAnything => _customDeviceWorkflow.canListDevices; |
| |
| CustomDevicesConfig get _customDevicesConfig => _config; |
| |
| List<CustomDevice> get _enabledCustomDevices { |
| return _customDevicesConfig.tryGetDevices() |
| .where((CustomDeviceConfig element) => element.enabled) |
| .map( |
| (CustomDeviceConfig config) => CustomDevice( |
| config: config, |
| logger: _logger, |
| processManager: _processManager |
| ) |
| ).toList(); |
| } |
| |
| @override |
| Future<List<Device>> pollingGetDevices({Duration? timeout}) async { |
| if (!canListAnything) { |
| return const <Device>[]; |
| } |
| |
| final List<CustomDevice> devices = _enabledCustomDevices; |
| |
| // maps any custom device to whether its reachable or not. |
| final Map<CustomDevice, bool> pingedDevices = Map<CustomDevice, bool>.fromIterables( |
| devices, |
| await Future.wait(devices.map((CustomDevice e) => e.tryPing(timeout: timeout))) |
| ); |
| |
| // remove all the devices we couldn't reach. |
| pingedDevices.removeWhere((_, bool value) => value == false); |
| |
| // return only the devices. |
| return pingedDevices.keys.toList(); |
| } |
| |
| @override |
| Future<List<String>> getDiagnostics() async => const <String>[]; |
| |
| @override |
| List<String> get wellKnownIds => const <String>[]; |
| } |