| // 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 '../base/common.dart'; |
| import '../base/error_handling_io.dart'; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/logger.dart'; |
| import '../base/os.dart'; |
| import '../base/platform.dart'; |
| import '../base/terminal.dart'; |
| import '../convert.dart'; |
| import '../custom_devices/custom_device.dart'; |
| import '../custom_devices/custom_device_config.dart'; |
| import '../custom_devices/custom_devices_config.dart'; |
| import '../device_port_forwarder.dart'; |
| import '../features.dart'; |
| import '../runner/flutter_command.dart'; |
| import '../runner/flutter_command_runner.dart'; |
| |
| /// just the function signature of the [print] function. |
| /// The Object arg may be null. |
| typedef PrintFn = void Function(Object); |
| |
| class CustomDevicesCommand extends FlutterCommand { |
| factory CustomDevicesCommand({ |
| required CustomDevicesConfig customDevicesConfig, |
| required OperatingSystemUtils operatingSystemUtils, |
| required Terminal terminal, |
| required Platform platform, |
| required ProcessManager processManager, |
| required FileSystem fileSystem, |
| required Logger logger, |
| required FeatureFlags featureFlags, |
| }) { |
| return CustomDevicesCommand._common( |
| customDevicesConfig: customDevicesConfig, |
| operatingSystemUtils: operatingSystemUtils, |
| terminal: terminal, |
| platform: platform, |
| processManager: processManager, |
| fileSystem: fileSystem, |
| logger: logger, |
| featureFlags: featureFlags |
| ); |
| } |
| |
| @visibleForTesting |
| factory CustomDevicesCommand.test({ |
| required CustomDevicesConfig customDevicesConfig, |
| required OperatingSystemUtils operatingSystemUtils, |
| required Terminal terminal, |
| required Platform platform, |
| required ProcessManager processManager, |
| required FileSystem fileSystem, |
| required Logger logger, |
| required FeatureFlags featureFlags, |
| PrintFn usagePrintFn = print |
| }) { |
| return CustomDevicesCommand._common( |
| customDevicesConfig: customDevicesConfig, |
| operatingSystemUtils: operatingSystemUtils, |
| terminal: terminal, |
| platform: platform, |
| processManager: processManager, |
| fileSystem: fileSystem, |
| logger: logger, |
| featureFlags: featureFlags, |
| usagePrintFn: usagePrintFn |
| ); |
| } |
| |
| CustomDevicesCommand._common({ |
| required CustomDevicesConfig customDevicesConfig, |
| required OperatingSystemUtils operatingSystemUtils, |
| required Terminal terminal, |
| required Platform platform, |
| required ProcessManager processManager, |
| required FileSystem fileSystem, |
| required Logger logger, |
| required FeatureFlags featureFlags, |
| PrintFn usagePrintFn = print, |
| }) : _customDevicesConfig = customDevicesConfig, |
| _featureFlags = featureFlags, |
| _usagePrintFn = usagePrintFn |
| { |
| addSubcommand(CustomDevicesListCommand( |
| customDevicesConfig: customDevicesConfig, |
| featureFlags: featureFlags, |
| logger: logger, |
| )); |
| addSubcommand(CustomDevicesResetCommand( |
| customDevicesConfig: customDevicesConfig, |
| featureFlags: featureFlags, |
| fileSystem: fileSystem, |
| logger: logger, |
| )); |
| addSubcommand(CustomDevicesAddCommand( |
| customDevicesConfig: customDevicesConfig, |
| operatingSystemUtils: operatingSystemUtils, |
| terminal: terminal, |
| platform: platform, |
| featureFlags: featureFlags, |
| processManager: processManager, |
| fileSystem: fileSystem, |
| logger: logger, |
| )); |
| addSubcommand(CustomDevicesDeleteCommand( |
| customDevicesConfig: customDevicesConfig, |
| featureFlags: featureFlags, |
| fileSystem: fileSystem, |
| logger: logger, |
| )); |
| } |
| |
| final CustomDevicesConfig _customDevicesConfig; |
| final FeatureFlags _featureFlags; |
| final PrintFn _usagePrintFn; |
| |
| @override |
| String get description { |
| String configFileLine; |
| if (_featureFlags.areCustomDevicesEnabled) { |
| configFileLine = '\nMakes changes to the config file at "${_customDevicesConfig.configPath}".\n'; |
| } else { |
| configFileLine = ''; |
| } |
| |
| return ''' |
| List, reset, add and delete custom devices. |
| $configFileLine |
| This is just a collection of commonly used shorthands for things like adding |
| ssh devices, resetting (with backup) and checking the config file. For advanced |
| configuration or more complete documentation, edit the config file with an |
| editor that supports JSON schemas like VS Code. |
| |
| Requires the custom devices feature to be enabled. You can enable it using "flutter config --enable-custom-devices". |
| '''; |
| } |
| |
| @override |
| String get name => 'custom-devices'; |
| |
| @override |
| String get category => FlutterCommandCategory.tools; |
| |
| @override |
| Future<FlutterCommandResult> runCommand() async { |
| return FlutterCommandResult.success(); |
| } |
| |
| @override |
| void printUsage() { |
| _usagePrintFn(usage); |
| } |
| } |
| |
| /// This class is meant to provide some commonly used utility functions |
| /// to the subcommands, like backing up the config file & checking if the |
| /// feature is enabled. |
| abstract class CustomDevicesCommandBase extends FlutterCommand { |
| CustomDevicesCommandBase({ |
| required this.customDevicesConfig, |
| required this.featureFlags, |
| required this.fileSystem, |
| required this.logger, |
| }); |
| |
| @protected final CustomDevicesConfig customDevicesConfig; |
| @protected final FeatureFlags featureFlags; |
| @protected final FileSystem? fileSystem; |
| @protected final Logger logger; |
| |
| /// The path to the (potentially non-existing) backup of the config file. |
| @protected |
| String get configBackupPath => '${customDevicesConfig.configPath}.bak'; |
| |
| /// Copies the current config file to [configBackupPath], overwriting it |
| /// if necessary. Returns false and does nothing if the current config file |
| /// doesn't exist. (True otherwise) |
| @protected |
| bool backup() { |
| final File configFile = fileSystem!.file(customDevicesConfig.configPath); |
| if (configFile.existsSync()) { |
| configFile.copySync(configBackupPath); |
| return true; |
| } |
| return false; |
| } |
| |
| /// Checks if the custom devices feature is enabled and returns true/false |
| /// accordingly. Additionally, logs an error if it's not enabled with a hint |
| /// on how to enable it. |
| @protected |
| void checkFeatureEnabled() { |
| if (!featureFlags.areCustomDevicesEnabled) { |
| throwToolExit( |
| 'Custom devices feature must be enabled. ' |
| 'Enable using `flutter config --enable-custom-devices`.' |
| ); |
| } |
| } |
| } |
| |
| class CustomDevicesListCommand extends CustomDevicesCommandBase { |
| CustomDevicesListCommand({ |
| required super.customDevicesConfig, |
| required super.featureFlags, |
| required super.logger, |
| }) : super( |
| fileSystem: null |
| ); |
| |
| @override |
| String get description => ''' |
| List the currently configured custom devices, both enabled and disabled, reachable or not. |
| '''; |
| |
| @override |
| String get name => 'list'; |
| |
| @override |
| Future<FlutterCommandResult> runCommand() async { |
| checkFeatureEnabled(); |
| |
| late List<CustomDeviceConfig> devices; |
| try { |
| devices = customDevicesConfig.devices; |
| } on Exception { |
| throwToolExit('Could not list custom devices.'); |
| } |
| |
| if (devices.isEmpty) { |
| logger.printStatus('No custom devices found in "${customDevicesConfig.configPath}"'); |
| } else { |
| logger.printStatus('List of custom devices in "${customDevicesConfig.configPath}":'); |
| for (final CustomDeviceConfig device in devices) { |
| logger.printStatus('id: ${device.id}, label: ${device.label}, enabled: ${device.enabled}', indent: 2, hangingIndent: 2); |
| } |
| } |
| |
| return FlutterCommandResult.success(); |
| } |
| } |
| |
| class CustomDevicesResetCommand extends CustomDevicesCommandBase { |
| CustomDevicesResetCommand({ |
| required super.customDevicesConfig, |
| required super.featureFlags, |
| required FileSystem super.fileSystem, |
| required super.logger, |
| }); |
| |
| @override |
| String get description => ''' |
| Reset the config file to the default. |
| |
| The current config file will be backed up to the same path, but with a `.bak` appended. |
| If a file already exists at the backup location, it will be overwritten. |
| '''; |
| |
| @override |
| String get name => 'reset'; |
| |
| @override |
| Future<FlutterCommandResult> runCommand() async { |
| checkFeatureEnabled(); |
| |
| final bool wasBackedUp = backup(); |
| |
| ErrorHandlingFileSystem.deleteIfExists(fileSystem!.file(customDevicesConfig.configPath)); |
| customDevicesConfig.ensureFileExists(); |
| |
| logger.printStatus( |
| wasBackedUp |
| ? 'Successfully reset the custom devices config file and created a ' |
| 'backup at "$configBackupPath".' |
| : 'Successfully reset the custom devices config file.' |
| ); |
| return FlutterCommandResult.success(); |
| } |
| } |
| |
| class CustomDevicesAddCommand extends CustomDevicesCommandBase { |
| CustomDevicesAddCommand({ |
| required super.customDevicesConfig, |
| required OperatingSystemUtils operatingSystemUtils, |
| required Terminal terminal, |
| required Platform platform, |
| required super.featureFlags, |
| required ProcessManager processManager, |
| required FileSystem super.fileSystem, |
| required super.logger, |
| }) : _operatingSystemUtils = operatingSystemUtils, |
| _terminal = terminal, |
| _platform = platform, |
| _processManager = processManager |
| { |
| argParser.addFlag( |
| _kCheck, |
| help: |
| 'Make sure the config actually works. This will execute some of the ' |
| 'commands in the config (if necessary with dummy arguments). This ' |
| 'flag is enabled by default when "--json" is not specified. If ' |
| '"--json" is given, it is disabled by default.\n' |
| 'For example, a config with "null" as the "runDebug" command is ' |
| 'invalid. If the "runDebug" command is valid (so it is an array of ' |
| 'strings) but the command is not found (because you have a typo, for ' |
| 'example), the config won\'t work and "--check" will spot that.' |
| ); |
| |
| argParser.addOption( |
| _kJson, |
| help: |
| 'Add the custom device described by this JSON-encoded string to the ' |
| 'list of custom-devices instead of using the normal, interactive way ' |
| 'of configuring. Useful if you want to use the "flutter custom-devices ' |
| 'add" command from a script, or use it non-interactively for some ' |
| 'other reason.\n' |
| "By default, this won't check whether the passed in config actually " |
| 'works. For more info see the "--check" option.', |
| valueHelp: '{"id": "pi", ...}', |
| aliases: _kJsonAliases |
| ); |
| |
| argParser.addFlag( |
| _kSsh, |
| help: |
| 'Add a ssh-device. This will automatically fill out some of the config ' |
| 'options for you with good defaults, and in other cases save you some ' |
| "typing. So you'll only need to enter some things like hostname and " |
| 'username of the remote device instead of entering each individual ' |
| 'command.', |
| defaultsTo: true, |
| negatable: false |
| ); |
| } |
| |
| static const String _kJson = 'json'; |
| static const List<String> _kJsonAliases = <String>['js']; |
| static const String _kCheck = 'check'; |
| static const String _kSsh = 'ssh'; |
| |
| // A hostname consists of one or more "names", separated by a dot. |
| // A name may consist of alpha-numeric characters. Hyphens are also allowed, |
| // but not as the first or last character of the name. |
| static final RegExp _hostnameRegex = RegExp(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'); |
| |
| final OperatingSystemUtils _operatingSystemUtils; |
| final Terminal _terminal; |
| final Platform _platform; |
| final ProcessManager _processManager; |
| late StreamQueue<String> inputs; |
| |
| @override |
| String get description => 'Add a new device the custom devices config file.'; |
| |
| @override |
| String get name => 'add'; |
| |
| void _printConfigCheckingError(String err) { |
| logger.printError(err); |
| } |
| |
| /// Check this config by executing some of the commands, see if they run |
| /// fine. |
| Future<bool> _checkConfigWithLogging(final CustomDeviceConfig config) async { |
| final CustomDevice device = CustomDevice( |
| config: config, |
| logger: logger, |
| processManager: _processManager |
| ); |
| |
| bool result = true; |
| |
| try { |
| final bool reachable = await device.tryPing(); |
| if (!reachable) { |
| _printConfigCheckingError("Couldn't ping device."); |
| result = false; |
| } |
| } on Exception catch (e) { |
| _printConfigCheckingError('While executing ping command: $e'); |
| result = false; |
| } |
| |
| final Directory temp = await fileSystem!.systemTempDirectory.createTemp(); |
| |
| try { |
| final bool ok = await device.tryInstall( |
| localPath: temp.path, |
| appName: temp.basename |
| ); |
| if (!ok) { |
| _printConfigCheckingError("Couldn't install test app on device."); |
| result = false; |
| } |
| } on Exception catch (e) { |
| _printConfigCheckingError('While executing install command: $e'); |
| result = false; |
| } |
| |
| await temp.delete(); |
| |
| try { |
| final bool ok = await device.tryUninstall(appName: temp.basename); |
| if (!ok) { |
| _printConfigCheckingError("Couldn't uninstall test app from device."); |
| result = false; |
| } |
| } on Exception catch (e) { |
| _printConfigCheckingError('While executing uninstall command: $e'); |
| result = false; |
| } |
| |
| if (config.usesPortForwarding) { |
| final CustomDevicePortForwarder portForwarder = CustomDevicePortForwarder( |
| deviceName: device.name, |
| forwardPortCommand: config.forwardPortCommand!, |
| forwardPortSuccessRegex: config.forwardPortSuccessRegex!, |
| processManager: _processManager, |
| logger: logger, |
| ); |
| |
| try { |
| // find a random port we can forward |
| final int port = await _operatingSystemUtils.findFreePort(); |
| |
| final ForwardedPort? forwardedPort = await portForwarder.tryForward(port, port); |
| if (forwardedPort == null) { |
| _printConfigCheckingError("Couldn't forward test port $port from device.",); |
| result = false; |
| } else { |
| await portForwarder.unforward(forwardedPort); |
| } |
| } on Exception catch (e) { |
| _printConfigCheckingError( |
| 'While forwarding/unforwarding device port: $e', |
| ); |
| result = false; |
| } |
| } |
| |
| if (result) { |
| logger.printStatus('Passed all checks successfully.'); |
| } |
| |
| return result; |
| } |
| |
| /// Run non-interactively (useful if running from scripts or bots), |
| /// add value of the `--json` arg to the config. |
| /// |
| /// Only check if `--check` is explicitly specified. (Don't check by default) |
| Future<FlutterCommandResult> runNonInteractively() async { |
| final String jsonStr = stringArg(_kJson)!; |
| final bool shouldCheck = boolArg(_kCheck); |
| |
| dynamic json; |
| try { |
| json = jsonDecode(jsonStr); |
| } on FormatException catch (e) { |
| throwToolExit('Could not decode json: $e'); |
| } |
| |
| late CustomDeviceConfig config; |
| try { |
| config = CustomDeviceConfig.fromJson(json); |
| } on CustomDeviceRevivalException catch (e) { |
| throwToolExit('Invalid custom device config: $e'); |
| } |
| |
| if (shouldCheck && !await _checkConfigWithLogging(config)) { |
| throwToolExit("Custom device didn't pass all checks."); |
| } |
| |
| customDevicesConfig.add(config); |
| printSuccessfullyAdded(); |
| |
| return FlutterCommandResult.success(); |
| } |
| |
| void printSuccessfullyAdded() { |
| logger.printStatus('Successfully added custom device to config file at "${customDevicesConfig.configPath}".'); |
| } |
| |
| bool _isValidHostname(String s) => _hostnameRegex.hasMatch(s); |
| |
| bool _isValidIpAddr(String s) => InternetAddress.tryParse(s) != null; |
| |
| /// Ask the user to input a string. |
| Future<String?> askForString( |
| String name, { |
| String? description, |
| String? example, |
| String? defaultsTo, |
| Future<bool> Function(String)? validator, |
| }) async { |
| String msg = description ?? name; |
| |
| final String exampleOrDefault = <String>[ |
| if (example != null) 'example: $example', |
| if (defaultsTo != null) 'empty for $defaultsTo', |
| ].join(', '); |
| |
| if (exampleOrDefault.isNotEmpty) { |
| msg += ' ($exampleOrDefault)'; |
| } |
| |
| logger.printStatus(msg); |
| while (true) { |
| if (!await inputs.hasNext) { |
| return null; |
| } |
| |
| final String input = await inputs.next; |
| |
| if (validator != null && !await validator(input)) { |
| logger.printStatus('Invalid input. Please enter $name:'); |
| } else { |
| return input; |
| } |
| } |
| } |
| |
| /// Ask the user for a y(es) / n(o) or empty input. |
| Future<bool> askForBool( |
| String name, { |
| String? description, |
| bool defaultsTo = true, |
| }) async { |
| final String defaultsToStr = defaultsTo ? '[Y/n]' : '[y/N]'; |
| logger.printStatus('$description $defaultsToStr (empty for default)'); |
| while (true) { |
| final String input = await inputs.next; |
| |
| if (input.isEmpty) { |
| return defaultsTo; |
| } else if (input.toLowerCase() == 'y') { |
| return true; |
| } else if (input.toLowerCase() == 'n') { |
| return false; |
| } else { |
| logger.printStatus('Invalid input. Expected is either y, n or empty for default. $name? $defaultsToStr'); |
| } |
| } |
| } |
| |
| /// Ask the user if he wants to apply the config. |
| /// Shows a different prompt if errors or warnings exist in the config. |
| Future<bool> askApplyConfig({bool hasErrorsOrWarnings = false}) { |
| return askForBool( |
| 'apply', |
| description: hasErrorsOrWarnings |
| ? 'Warnings or errors exist in custom device. ' |
| 'Would you like to add the custom device to the config anyway?' |
| : 'Would you like to add the custom device to the config now?', |
| defaultsTo: !hasErrorsOrWarnings |
| ); |
| } |
| |
| /// Run interactively (with user prompts), the target device should be |
| /// connected to via ssh. |
| Future<FlutterCommandResult> runInteractivelySsh() async { |
| final bool shouldCheck = boolArg(_kCheck); |
| |
| // Listen to the keystrokes stream as late as possible, since it's a |
| // single-subscription stream apparently. |
| // Also, _terminal.keystrokes can be closed unexpectedly, which will result |
| // in StreamQueue.next throwing a StateError when make the StreamQueue listen |
| // to that directly. |
| // This caused errors when using Ctrl+C to terminate while the |
| // custom-devices add command is waiting for user input. |
| // So instead, we add the keystrokes stream events to a new single-subscription |
| // stream and listen to that instead. |
| final StreamController<String> nonClosingKeystrokes = StreamController<String>(); |
| final StreamSubscription<String> keystrokesSubscription = _terminal.keystrokes.listen( |
| (String s) => nonClosingKeystrokes.add(s.trim()), |
| cancelOnError: true |
| ); |
| |
| inputs = StreamQueue<String>(nonClosingKeystrokes.stream); |
| |
| final String id = (await askForString( |
| 'id', |
| description: |
| 'Please enter the id you want to device to have. Must contain only ' |
| 'alphanumeric or underscore characters.', |
| example: 'pi', |
| validator: (String s) async => RegExp(r'^\w+$').hasMatch(s), |
| ))!; |
| |
| final String label = (await askForString( |
| 'label', |
| description: |
| 'Please enter the label of the device, which is a slightly more verbose ' |
| 'name for the device.', |
| example: 'Raspberry Pi', |
| ))!; |
| |
| final String sdkNameAndVersion = (await askForString( |
| 'SDK name and version', |
| example: 'Raspberry Pi 4 Model B+', |
| ))!; |
| |
| final bool enabled = await askForBool( |
| 'enabled', |
| description: 'Should the device be enabled?', |
| ); |
| |
| final String targetStr = (await askForString( |
| 'target', |
| description: 'Please enter the hostname or IPv4/v6 address of the device.', |
| example: 'raspberrypi', |
| validator: (String s) async => _isValidHostname(s) || _isValidIpAddr(s) |
| ))!; |
| |
| final InternetAddress? targetIp = InternetAddress.tryParse(targetStr); |
| final bool useIp = targetIp != null; |
| final bool ipv6 = useIp && targetIp.type == InternetAddressType.IPv6; |
| final InternetAddress loopbackIp = ipv6 |
| ? InternetAddress.loopbackIPv6 |
| : InternetAddress.loopbackIPv4; |
| |
| final String username = (await askForString( |
| 'username', |
| description: 'Please enter the username used for ssh-ing into the remote device.', |
| example: 'pi', |
| defaultsTo: 'no username', |
| ))!; |
| |
| final String remoteRunDebugCommand = (await askForString( |
| 'run command', |
| description: |
| 'Please enter the command executed on the remote device for starting ' |
| r'the app. "/tmp/${appName}" is the path to the asset bundle.', |
| example: r'flutter-pi /tmp/${appName}' |
| ))!; |
| |
| final bool usePortForwarding = await askForBool( |
| 'use port forwarding', |
| description: 'Should the device use port forwarding? ' |
| 'Using port forwarding is the default because it works in all cases, however if your ' |
| 'remote device has a static IP address and you have a way of ' |
| 'specifying the "--vm-service-host=<ip>" engine option, you might prefer ' |
| 'not using port forwarding.', |
| ); |
| |
| final String screenshotCommand = (await askForString( |
| 'screenshot command', |
| description: 'Enter the command executed on the remote device for taking a screenshot.', |
| example: r"fbgrab /tmp/screenshot.png && cat /tmp/screenshot.png | base64 | tr -d ' \n\t'", |
| defaultsTo: 'no screenshotting support', |
| ))!; |
| |
| // SSH expects IPv6 addresses to use the bracket syntax like URIs do too, |
| // but the IPv6 the user enters is a raw IPv6 address, so we need to wrap it. |
| final String sshTarget = |
| (username.isNotEmpty ? '$username@' : '') |
| + (ipv6 ? '[${targetIp.address}]' : targetStr); |
| |
| final String formattedLoopbackIp = ipv6 |
| ? '[${loopbackIp.address}]' |
| : loopbackIp.address; |
| |
| CustomDeviceConfig config = CustomDeviceConfig( |
| id: id, |
| label: label, |
| sdkNameAndVersion: sdkNameAndVersion, |
| enabled: enabled, |
| |
| // host-platform specific, filled out later |
| pingCommand: const <String>[], |
| |
| postBuildCommand: const <String>[], |
| |
| // just install to /tmp/${appName} by default |
| installCommand: <String>[ |
| 'scp', |
| '-r', |
| '-o', 'BatchMode=yes', |
| if (ipv6) '-6', |
| r'${localPath}', |
| '$sshTarget:/tmp/\${appName}', |
| ], |
| |
| uninstallCommand: <String>[ |
| 'ssh', |
| '-o', 'BatchMode=yes', |
| if (ipv6) '-6', |
| sshTarget, |
| r'rm -rf "/tmp/${appName}"', |
| ], |
| |
| runDebugCommand: <String>[ |
| 'ssh', |
| '-o', 'BatchMode=yes', |
| if (ipv6) '-6', |
| sshTarget, |
| remoteRunDebugCommand, |
| ], |
| |
| forwardPortCommand: usePortForwarding |
| ? <String>[ |
| 'ssh', |
| '-o', 'BatchMode=yes', |
| '-o', 'ExitOnForwardFailure=yes', |
| if (ipv6) '-6', |
| '-L', '$formattedLoopbackIp:\${hostPort}:$formattedLoopbackIp:\${devicePort}', |
| sshTarget, |
| "echo 'Port forwarding success'; read", |
| ] |
| : null, |
| forwardPortSuccessRegex: usePortForwarding |
| ? RegExp('Port forwarding success') |
| : null, |
| |
| screenshotCommand: screenshotCommand.isNotEmpty |
| ? <String>[ |
| 'ssh', |
| '-o', 'BatchMode=yes', |
| if (ipv6) '-6', |
| sshTarget, |
| screenshotCommand, |
| ] |
| : null |
| ); |
| |
| if (_platform.isWindows) { |
| config = config.copyWith( |
| pingCommand: <String>[ |
| 'ping', |
| if (ipv6) '-6', |
| '-n', '1', |
| '-w', '500', |
| targetStr, |
| ], |
| explicitPingSuccessRegex: true, |
| pingSuccessRegex: RegExp(r'[<=]\d+ms') |
| ); |
| } else if (_platform.isLinux || _platform.isMacOS) { |
| config = config.copyWith( |
| pingCommand: <String>[ |
| 'ping', |
| if (ipv6) '-6', |
| '-c', '1', |
| '-w', '1', |
| targetStr, |
| ], |
| explicitPingSuccessRegex: true, |
| ); |
| } else { |
| throw UnsupportedError('Unsupported operating system'); |
| } |
| |
| final bool apply = await askApplyConfig( |
| hasErrorsOrWarnings: |
| shouldCheck && !(await _checkConfigWithLogging(config)) |
| ); |
| |
| unawaited(keystrokesSubscription.cancel()); |
| unawaited(nonClosingKeystrokes.close()); |
| |
| if (apply) { |
| customDevicesConfig.add(config); |
| printSuccessfullyAdded(); |
| } |
| |
| return FlutterCommandResult.success(); |
| } |
| |
| @override |
| Future<FlutterCommandResult> runCommand() async { |
| checkFeatureEnabled(); |
| |
| if (stringArg(_kJson) != null) { |
| return runNonInteractively(); |
| } |
| if (boolArg(_kSsh)) { |
| return runInteractivelySsh(); |
| } |
| throw UnsupportedError('Unknown run mode'); |
| } |
| } |
| |
| class CustomDevicesDeleteCommand extends CustomDevicesCommandBase { |
| CustomDevicesDeleteCommand({ |
| required super.customDevicesConfig, |
| required super.featureFlags, |
| required FileSystem super.fileSystem, |
| required super.logger, |
| }); |
| |
| @override |
| String get description => ''' |
| Delete a device from the config file. |
| '''; |
| |
| @override |
| String get name => 'delete'; |
| |
| @override |
| Future<FlutterCommandResult> runCommand() async { |
| checkFeatureEnabled(); |
| |
| final String? id = globalResults![FlutterGlobalOptions.kDeviceIdOption] as String?; |
| if (id == null || !customDevicesConfig.contains(id)) { |
| throwToolExit('Couldn\'t find device with id "$id" in config at "${customDevicesConfig.configPath}"'); |
| } |
| |
| backup(); |
| customDevicesConfig.remove(id); |
| logger.printStatus('Successfully removed device with id "$id" from config at "${customDevicesConfig.configPath}"'); |
| return FlutterCommandResult.success(); |
| } |
| } |