blob: c5375f4b31fbeb8ad262a8d3d8860daa4e239507 [file] [log] [blame]
// 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)';
}
}