mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
825 lines
26 KiB
Dart
825 lines
26 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package: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';
|
|
|
|
/// 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 void Function(Object) _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 = stringArgDeprecated(_kJson)!;
|
|
final bool shouldCheck = boolArgDeprecated(_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 == true ? '[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 = boolArgDeprecated(_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 (stringArgDeprecated(_kJson) != null) {
|
|
return runNonInteractively();
|
|
}
|
|
if (boolArgDeprecated(_kSsh) == true) {
|
|
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!['device-id'] 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();
|
|
}
|
|
}
|