| // 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 'package:meta/meta.dart'; |
| |
| import '../base/platform.dart'; |
| import '../build_info.dart'; |
| |
| /// Quiver has this, but unfortunately we can't depend on it bc flutter_tools |
| /// uses non-nullsafe quiver by default (because of dwds). |
| bool _listsEqual(List<dynamic>? a, List<dynamic>? b) { |
| if (a == b) { |
| return true; |
| } |
| if (a == null || b == null) { |
| return false; |
| } |
| if (a.length != b.length) { |
| return false; |
| } |
| |
| return a.asMap().entries.every((MapEntry<int, dynamic> e) => e.value == b[e.key]); |
| } |
| |
| /// The normal [RegExp.==] operator is inherited from [Object], so only |
| /// returns true when the regexes are the same instance. |
| /// |
| /// This function instead _should_ return true when the regexes are |
| /// functionally the same, i.e. when they have the same matches & captures for |
| /// any given input. At least that's the goal, in reality this has lots of false |
| /// negatives (for example when the flags differ). Still better than [RegExp.==]. |
| bool _regexesEqual(RegExp? a, RegExp? b) { |
| if (a == b) { |
| return true; |
| } |
| if (a == null || b == null) { |
| return false; |
| } |
| |
| return a.pattern == b.pattern |
| && a.isMultiLine == b.isMultiLine |
| && a.isCaseSensitive == b.isCaseSensitive |
| && a.isUnicode == b.isUnicode |
| && a.isDotAll == b.isDotAll; |
| } |
| |
| /// Something went wrong while trying to load the custom devices config from the |
| /// JSON representation. Maybe some value is missing, maybe something has the |
| /// wrong type, etc. |
| @immutable |
| class CustomDeviceRevivalException implements Exception { |
| const CustomDeviceRevivalException(this.message); |
| |
| const CustomDeviceRevivalException.fromDescriptions( |
| String fieldDescription, |
| String expectedValueDescription |
| ) : message = 'Expected $fieldDescription to be $expectedValueDescription.'; |
| |
| final String message; |
| |
| @override |
| String toString() { |
| return message; |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| return (other is CustomDeviceRevivalException) && |
| (other.message == message); |
| } |
| |
| @override |
| int get hashCode => message.hashCode; |
| } |
| |
| /// A single configured custom device. |
| /// |
| /// In the custom devices config file on disk, there may be multiple custom |
| /// devices configured. |
| @immutable |
| class CustomDeviceConfig { |
| const CustomDeviceConfig({ |
| required this.id, |
| required this.label, |
| required this.sdkNameAndVersion, |
| this.platform, |
| required this.enabled, |
| required this.pingCommand, |
| this.pingSuccessRegex, |
| required this.postBuildCommand, |
| required this.installCommand, |
| required this.uninstallCommand, |
| required this.runDebugCommand, |
| this.forwardPortCommand, |
| this.forwardPortSuccessRegex, |
| this.screenshotCommand |
| }) : assert(forwardPortCommand == null || forwardPortSuccessRegex != null), |
| assert( |
| platform == null |
| || platform == TargetPlatform.linux_x64 |
| || platform == TargetPlatform.linux_arm64 |
| ); |
| |
| /// Create a CustomDeviceConfig from some JSON value. |
| /// If anything fails internally (some value doesn't have the right type, |
| /// some value is missing, etc) a [CustomDeviceRevivalException] with the description |
| /// of the error is thrown. (No exceptions/errors other than JsonRevivalException |
| /// should ever be thrown by this factory.) |
| factory CustomDeviceConfig.fromJson(dynamic json) { |
| final Map<String, dynamic> typedMap = _castJsonObject( |
| json, |
| 'device configuration', |
| 'a JSON object' |
| ); |
| |
| final List<String>? forwardPortCommand = _castStringListOrNull( |
| typedMap[_kForwardPortCommand], |
| _kForwardPortCommand, |
| 'null or array of strings with at least one element', |
| minLength: 1 |
| ); |
| |
| final RegExp? forwardPortSuccessRegex = _convertToRegexOrNull( |
| typedMap[_kForwardPortSuccessRegex], |
| _kForwardPortSuccessRegex, |
| 'null or string-ified regex' |
| ); |
| |
| final String? archString = _castStringOrNull( |
| typedMap[_kPlatform], |
| _kPlatform, |
| 'null or one of linux-arm64, linux-x64' |
| ); |
| |
| late TargetPlatform? platform; |
| try { |
| platform = archString == null |
| ? null |
| : getTargetPlatformForName(archString); |
| } on UnsupportedError { |
| throw const CustomDeviceRevivalException.fromDescriptions( |
| _kPlatform, |
| 'null or one of linux-arm64, linux-x64' |
| ); |
| } |
| |
| if (platform != null |
| && platform != TargetPlatform.linux_arm64 |
| && platform != TargetPlatform.linux_x64 |
| ) { |
| throw const CustomDeviceRevivalException.fromDescriptions( |
| _kPlatform, |
| 'null or one of linux-arm64, linux-x64' |
| ); |
| } |
| |
| if (forwardPortCommand != null && forwardPortSuccessRegex == null) { |
| throw const CustomDeviceRevivalException('When forwardPort is given, forwardPortSuccessRegex must be specified too.'); |
| } |
| |
| return CustomDeviceConfig( |
| id: _castString(typedMap[_kId], _kId, 'a string'), |
| label: _castString(typedMap[_kLabel], _kLabel, 'a string'), |
| sdkNameAndVersion: _castString(typedMap[_kSdkNameAndVersion], _kSdkNameAndVersion, 'a string'), |
| platform: platform, |
| enabled: _castBool(typedMap[_kEnabled], _kEnabled, 'a boolean'), |
| pingCommand: _castStringList( |
| typedMap[_kPingCommand], |
| _kPingCommand, |
| 'array of strings with at least one element', |
| minLength: 1 |
| ), |
| pingSuccessRegex: _convertToRegexOrNull(typedMap[_kPingSuccessRegex], _kPingSuccessRegex, 'null or string-ified regex'), |
| postBuildCommand: _castStringListOrNull( |
| typedMap[_kPostBuildCommand], |
| _kPostBuildCommand, |
| 'null or array of strings with at least one element', |
| minLength: 1, |
| ), |
| installCommand: _castStringList( |
| typedMap[_kInstallCommand], |
| _kInstallCommand, |
| 'array of strings with at least one element', |
| minLength: 1 |
| ), |
| uninstallCommand: _castStringList( |
| typedMap[_kUninstallCommand], |
| _kUninstallCommand, |
| 'array of strings with at least one element', |
| minLength: 1 |
| ), |
| runDebugCommand: _castStringList( |
| typedMap[_kRunDebugCommand], |
| _kRunDebugCommand, |
| 'array of strings with at least one element', |
| minLength: 1 |
| ), |
| forwardPortCommand: forwardPortCommand, |
| forwardPortSuccessRegex: forwardPortSuccessRegex, |
| screenshotCommand: _castStringListOrNull( |
| typedMap[_kScreenshotCommand], |
| _kScreenshotCommand, |
| 'array of strings with at least one element', |
| minLength: 1 |
| ) |
| ); |
| } |
| |
| static const String _kId = 'id'; |
| static const String _kLabel = 'label'; |
| static const String _kSdkNameAndVersion = 'sdkNameAndVersion'; |
| static const String _kPlatform = 'platform'; |
| static const String _kEnabled = 'enabled'; |
| static const String _kPingCommand = 'ping'; |
| static const String _kPingSuccessRegex = 'pingSuccessRegex'; |
| static const String _kPostBuildCommand = 'postBuild'; |
| static const String _kInstallCommand = 'install'; |
| static const String _kUninstallCommand = 'uninstall'; |
| static const String _kRunDebugCommand = 'runDebug'; |
| static const String _kForwardPortCommand = 'forwardPort'; |
| static const String _kForwardPortSuccessRegex = 'forwardPortSuccessRegex'; |
| static const String _kScreenshotCommand = 'screenshot'; |
| |
| /// An example device config used for creating the default config file. |
| /// Uses windows-specific ping and pingSuccessRegex. For the linux and macOs |
| /// example config, see [exampleUnix]. |
| static final CustomDeviceConfig exampleWindows = CustomDeviceConfig( |
| id: 'pi', |
| label: 'Raspberry Pi', |
| sdkNameAndVersion: 'Raspberry Pi 4 Model B+', |
| platform: TargetPlatform.linux_arm64, |
| enabled: false, |
| pingCommand: const <String>[ |
| 'ping', |
| '-w', '500', |
| '-n', '1', |
| 'raspberrypi', |
| ], |
| pingSuccessRegex: RegExp(r'[<=]\d+ms'), |
| postBuildCommand: null, |
| installCommand: const <String>[ |
| 'scp', |
| '-r', |
| '-o', 'BatchMode=yes', |
| r'${localPath}', |
| r'pi@raspberrypi:/tmp/${appName}', |
| ], |
| uninstallCommand: const <String>[ |
| 'ssh', |
| '-o', 'BatchMode=yes', |
| 'pi@raspberrypi', |
| r'rm -rf "/tmp/${appName}"', |
| ], |
| runDebugCommand: const <String>[ |
| 'ssh', |
| '-o', 'BatchMode=yes', |
| 'pi@raspberrypi', |
| r'flutter-pi "/tmp/${appName}"', |
| ], |
| forwardPortCommand: const <String>[ |
| 'ssh', |
| '-o', 'BatchMode=yes', |
| '-o', 'ExitOnForwardFailure=yes', |
| '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}', |
| 'pi@raspberrypi', |
| "echo 'Port forwarding success'; read", |
| ], |
| forwardPortSuccessRegex: RegExp('Port forwarding success'), |
| screenshotCommand: const <String>[ |
| 'ssh', |
| '-o', 'BatchMode=yes', |
| 'pi@raspberrypi', |
| r"fbgrab /tmp/screenshot.png && cat /tmp/screenshot.png | base64 | tr -d ' \n\t'", |
| ], |
| ); |
| |
| /// An example device config used for creating the default config file. |
| /// Uses ping and pingSuccessRegex values that only work on linux or macOs. |
| /// For the Windows example config, see [exampleWindows]. |
| static final CustomDeviceConfig exampleUnix = exampleWindows.copyWith( |
| pingCommand: const <String>[ |
| 'ping', |
| '-w', '1', |
| '-c', '1', |
| 'raspberrypi', |
| ], |
| explicitPingSuccessRegex: true |
| ); |
| |
| /// Returns an example custom device config that works on the given host platform. |
| /// |
| /// This is not the platform of the target device, it's the platform of the |
| /// development machine. Examples for different platforms may be different |
| /// because for example the ping command is different on Windows or Linux/macOS. |
| static CustomDeviceConfig getExampleForPlatform(Platform platform) { |
| if (platform.isWindows) { |
| return exampleWindows; |
| } |
| if (platform.isLinux || platform.isMacOS) { |
| return exampleUnix; |
| } |
| throw UnsupportedError('Unsupported operating system'); |
| } |
| |
| final String id; |
| final String label; |
| final String sdkNameAndVersion; |
| final TargetPlatform? platform; |
| final bool enabled; |
| final List<String> pingCommand; |
| final RegExp? pingSuccessRegex; |
| final List<String>? postBuildCommand; |
| final List<String> installCommand; |
| final List<String> uninstallCommand; |
| final List<String> runDebugCommand; |
| final List<String>? forwardPortCommand; |
| final RegExp? forwardPortSuccessRegex; |
| final List<String>? screenshotCommand; |
| |
| /// Returns true when this custom device config uses port forwarding, |
| /// which is the case when [forwardPortCommand] is not null. |
| bool get usesPortForwarding => forwardPortCommand != null; |
| |
| /// Returns true when this custom device config supports screenshotting, |
| /// which is the case when the [screenshotCommand] is not null. |
| bool get supportsScreenshotting => screenshotCommand != null; |
| |
| /// Invokes and returns the result of [closure]. |
| /// |
| /// If anything at all is thrown when executing the closure, a |
| /// [CustomDeviceRevivalException] is thrown with the given [fieldDescription] and |
| /// [expectedValueDescription]. |
| static T _maybeRethrowAsRevivalException<T>(T Function() closure, String fieldDescription, String expectedValueDescription) { |
| try { |
| return closure(); |
| } on Object { |
| throw CustomDeviceRevivalException.fromDescriptions(fieldDescription, expectedValueDescription); |
| } |
| } |
| |
| /// Tries to make a string-keyed, non-null map from [value]. |
| /// |
| /// If the value is null or not a valid string-keyed map, a [CustomDeviceRevivalException] |
| /// with the given [fieldDescription] and [expectedValueDescription] is thrown. |
| static Map<String, dynamic> _castJsonObject(dynamic value, String fieldDescription, String expectedValueDescription) { |
| if (value == null) { |
| throw CustomDeviceRevivalException.fromDescriptions(fieldDescription, expectedValueDescription); |
| } |
| |
| return _maybeRethrowAsRevivalException( |
| () => Map<String, dynamic>.from(value as Map<dynamic, dynamic>), |
| fieldDescription, |
| expectedValueDescription, |
| ); |
| } |
| |
| /// Tries to cast [value] to a bool. |
| /// |
| /// If the value is null or not a bool, a [CustomDeviceRevivalException] with the given |
| /// [fieldDescription] and [expectedValueDescription] is thrown. |
| static bool _castBool(dynamic value, String fieldDescription, String expectedValueDescription) { |
| if (value == null) { |
| throw CustomDeviceRevivalException.fromDescriptions(fieldDescription, expectedValueDescription); |
| } |
| |
| return _maybeRethrowAsRevivalException( |
| () => value as bool, |
| fieldDescription, |
| expectedValueDescription, |
| ); |
| } |
| |
| /// Tries to cast [value] to a String. |
| /// |
| /// If the value is null or not a String, a [CustomDeviceRevivalException] with the given |
| /// [fieldDescription] and [expectedValueDescription] is thrown. |
| static String _castString(dynamic value, String fieldDescription, String expectedValueDescription) { |
| if (value == null) { |
| throw CustomDeviceRevivalException.fromDescriptions(fieldDescription, expectedValueDescription); |
| } |
| |
| return _maybeRethrowAsRevivalException( |
| () => value as String, |
| fieldDescription, |
| expectedValueDescription, |
| ); |
| } |
| |
| /// Tries to cast [value] to a nullable String. |
| /// |
| /// If the value not null and not a String, a [CustomDeviceRevivalException] with the given |
| /// [fieldDescription] and [expectedValueDescription] is thrown. |
| static String? _castStringOrNull(dynamic value, String fieldDescription, String expectedValueDescription) { |
| if (value == null) { |
| return null; |
| } |
| |
| return _castString(value, fieldDescription, expectedValueDescription); |
| } |
| |
| /// Tries to make a list of strings from [value]. |
| /// |
| /// If the value is null or not a list containing only string values, |
| /// a [CustomDeviceRevivalException] with the given [fieldDescription] and |
| /// [expectedValueDescription] is thrown. |
| static List<String> _castStringList( |
| dynamic value, |
| String fieldDescription, |
| String expectedValueDescription, { |
| int minLength = 0, |
| }) { |
| if (value == null) { |
| throw CustomDeviceRevivalException.fromDescriptions(fieldDescription, expectedValueDescription); |
| } |
| |
| final List<String> list = _maybeRethrowAsRevivalException( |
| () => List<String>.from(value as Iterable<dynamic>), |
| fieldDescription, |
| expectedValueDescription, |
| ); |
| |
| if (list.length < minLength) { |
| throw CustomDeviceRevivalException.fromDescriptions(fieldDescription, expectedValueDescription); |
| } |
| |
| return list; |
| } |
| |
| /// Tries to make a list of strings from [value], or returns null if [value] |
| /// is null. |
| /// |
| /// If the value is not null and not a list containing only string values, |
| /// a [CustomDeviceRevivalException] with the given [fieldDescription] and |
| /// [expectedValueDescription] is thrown. |
| static List<String>? _castStringListOrNull( |
| dynamic value, |
| String fieldDescription, |
| String expectedValueDescription, { |
| int minLength = 0, |
| }) { |
| if (value == null) { |
| return null; |
| } |
| |
| return _castStringList(value, fieldDescription, expectedValueDescription, minLength: minLength); |
| } |
| |
| /// Tries to construct a RegExp from [value], or returns null if [value] |
| /// is null. |
| /// |
| /// If the value is not null and not a valid string-ified regex, |
| /// a [CustomDeviceRevivalException] with the given [fieldDescription] and |
| /// [expectedValueDescription] is thrown. |
| static RegExp? _convertToRegexOrNull(dynamic value, String fieldDescription, String expectedValueDescription) { |
| if (value == null) { |
| return null; |
| } |
| |
| return _maybeRethrowAsRevivalException( |
| () => RegExp(value as String), |
| fieldDescription, |
| expectedValueDescription, |
| ); |
| } |
| |
| Object toJson() { |
| return <String, Object?>{ |
| _kId: id, |
| _kLabel: label, |
| _kSdkNameAndVersion: sdkNameAndVersion, |
| _kPlatform: platform == null ? null : getNameForTargetPlatform(platform!), |
| _kEnabled: enabled, |
| _kPingCommand: pingCommand, |
| _kPingSuccessRegex: pingSuccessRegex?.pattern, |
| _kPostBuildCommand: (postBuildCommand?.length ?? 0) > 0 ? postBuildCommand : null, |
| _kInstallCommand: installCommand, |
| _kUninstallCommand: uninstallCommand, |
| _kRunDebugCommand: runDebugCommand, |
| _kForwardPortCommand: forwardPortCommand, |
| _kForwardPortSuccessRegex: forwardPortSuccessRegex?.pattern, |
| _kScreenshotCommand: screenshotCommand, |
| }; |
| } |
| |
| CustomDeviceConfig copyWith({ |
| String? id, |
| String? label, |
| String? sdkNameAndVersion, |
| bool explicitPlatform = false, |
| TargetPlatform? platform, |
| bool? enabled, |
| List<String>? pingCommand, |
| bool explicitPingSuccessRegex = false, |
| RegExp? pingSuccessRegex, |
| bool explicitPostBuildCommand = false, |
| List<String>? postBuildCommand, |
| List<String>? installCommand, |
| List<String>? uninstallCommand, |
| List<String>? runDebugCommand, |
| bool explicitForwardPortCommand = false, |
| List<String>? forwardPortCommand, |
| bool explicitForwardPortSuccessRegex = false, |
| RegExp? forwardPortSuccessRegex, |
| bool explicitScreenshotCommand = false, |
| List<String>? screenshotCommand |
| }) { |
| return CustomDeviceConfig( |
| id: id ?? this.id, |
| label: label ?? this.label, |
| sdkNameAndVersion: sdkNameAndVersion ?? this.sdkNameAndVersion, |
| platform: explicitPlatform ? platform : (platform ?? this.platform), |
| enabled: enabled ?? this.enabled, |
| pingCommand: pingCommand ?? this.pingCommand, |
| pingSuccessRegex: explicitPingSuccessRegex ? pingSuccessRegex : (pingSuccessRegex ?? this.pingSuccessRegex), |
| postBuildCommand: explicitPostBuildCommand ? postBuildCommand : (postBuildCommand ?? this.postBuildCommand), |
| installCommand: installCommand ?? this.installCommand, |
| uninstallCommand: uninstallCommand ?? this.uninstallCommand, |
| runDebugCommand: runDebugCommand ?? this.runDebugCommand, |
| forwardPortCommand: explicitForwardPortCommand ? forwardPortCommand : (forwardPortCommand ?? this.forwardPortCommand), |
| forwardPortSuccessRegex: explicitForwardPortSuccessRegex ? forwardPortSuccessRegex : (forwardPortSuccessRegex ?? this.forwardPortSuccessRegex), |
| screenshotCommand: explicitScreenshotCommand ? screenshotCommand : (screenshotCommand ?? this.screenshotCommand), |
| ); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| return other is CustomDeviceConfig |
| && other.id == id |
| && other.label == label |
| && other.sdkNameAndVersion == sdkNameAndVersion |
| && other.platform == platform |
| && other.enabled == enabled |
| && _listsEqual(other.pingCommand, pingCommand) |
| && _regexesEqual(other.pingSuccessRegex, pingSuccessRegex) |
| && _listsEqual(other.postBuildCommand, postBuildCommand) |
| && _listsEqual(other.installCommand, installCommand) |
| && _listsEqual(other.uninstallCommand, uninstallCommand) |
| && _listsEqual(other.runDebugCommand, runDebugCommand) |
| && _listsEqual(other.forwardPortCommand, forwardPortCommand) |
| && _regexesEqual(other.forwardPortSuccessRegex, forwardPortSuccessRegex) |
| && _listsEqual(other.screenshotCommand, screenshotCommand); |
| } |
| |
| @override |
| int get hashCode { |
| return id.hashCode |
| ^ label.hashCode |
| ^ sdkNameAndVersion.hashCode |
| ^ platform.hashCode |
| ^ enabled.hashCode |
| ^ pingCommand.hashCode |
| ^ (pingSuccessRegex?.pattern).hashCode |
| ^ postBuildCommand.hashCode |
| ^ installCommand.hashCode |
| ^ uninstallCommand.hashCode |
| ^ runDebugCommand.hashCode |
| ^ forwardPortCommand.hashCode |
| ^ (forwardPortSuccessRegex?.pattern).hashCode |
| ^ screenshotCommand.hashCode; |
| } |
| |
| @override |
| String toString() { |
| return 'CustomDeviceConfig(' |
| 'id: $id, ' |
| 'label: $label, ' |
| 'sdkNameAndVersion: $sdkNameAndVersion, ' |
| 'platform: $platform, ' |
| 'enabled: $enabled, ' |
| 'pingCommand: $pingCommand, ' |
| 'pingSuccessRegex: $pingSuccessRegex, ' |
| 'postBuildCommand: $postBuildCommand, ' |
| 'installCommand: $installCommand, ' |
| 'uninstallCommand: $uninstallCommand, ' |
| 'runDebugCommand: $runDebugCommand, ' |
| 'forwardPortCommand: $forwardPortCommand, ' |
| 'forwardPortSuccessRegex: $forwardPortSuccessRegex, ' |
| 'screenshotCommand: $screenshotCommand)'; |
| } |
| } |