From 804ea1a0c13892ac188b2c57709df07991619a2e Mon Sep 17 00:00:00 2001 From: Hidenori Matsubayashi Date: Wed, 21 Jul 2021 15:42:13 +0900 Subject: [PATCH] Add device discovery and custom-devices (#14) Fixed Discovery remote devices #12 --- lib/elinux_device.dart | 8 +- lib/elinux_device_discovery.dart | 65 ++++- lib/elinux_remote_device_config.dart | 359 ++++++++++++++++++++++++++ lib/elinux_remote_devices_config.dart | 140 ++++++++++ 4 files changed, 564 insertions(+), 8 deletions(-) create mode 100644 lib/elinux_remote_device_config.dart create mode 100644 lib/elinux_remote_devices_config.dart diff --git a/lib/elinux_device.dart b/lib/elinux_device.dart index 41ec210..426d07f 100644 --- a/lib/elinux_device.dart +++ b/lib/elinux_device.dart @@ -35,23 +35,26 @@ class ELinuxDevice extends Device { @required bool desktop, @required String backendType, @required String targetArch, + String sdkNameAndVersion = '', @required Logger logger, @required ProcessManager processManager, @required OperatingSystemUtils operatingSystemUtils, }) : _desktop = desktop, _backendType = backendType, _targetArch = targetArch, + _sdkNameAndVersion = sdkNameAndVersion, _logger = logger, _processManager = processManager, _operatingSystemUtils = operatingSystemUtils, super(id, - category: Category.desktop, + category: desktop ? Category.desktop : Category.mobile, platformType: PlatformType.custom, ephemeral: true); final bool _desktop; final String _backendType; final String _targetArch; + final String _sdkNameAndVersion; final Logger _logger; final ProcessManager _processManager; final OperatingSystemUtils _operatingSystemUtils; @@ -77,7 +80,8 @@ class ELinuxDevice extends Device { buildMode != BuildMode.jitRelease; @override - Future get sdkNameAndVersion async => _operatingSystemUtils.name; + Future get sdkNameAndVersion async => + _desktop ? _operatingSystemUtils.name : _sdkNameAndVersion; @override String get name => 'eLinux'; diff --git a/lib/elinux_device_discovery.dart b/lib/elinux_device_discovery.dart index 5ea68e7..bfade8b 100644 --- a/lib/elinux_device_discovery.dart +++ b/lib/elinux_device_discovery.dart @@ -6,11 +6,14 @@ // @dart = 2.8 -import 'package:flutter_tools/src/android/android_device_discovery.dart'; +import 'dart:io'; + import 'package:flutter_tools/src/android/android_workflow.dart'; +import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/context_runner.dart'; import 'package:flutter_tools/src/custom_devices/custom_devices_config.dart'; @@ -27,6 +30,8 @@ import 'package:process/process.dart'; import 'elinux_device.dart'; import 'elinux_doctor.dart'; +import 'elinux_remote_device_config.dart'; +import 'elinux_remote_devices_config.dart'; /// An extended [FlutterDeviceManager] for managing eLinux devices. class ELinuxDeviceManager extends FlutterDeviceManager { @@ -78,8 +83,6 @@ class ELinuxDeviceManager extends FlutterDeviceManager { } /// Device discovery for eLinux devices. -/// -/// Source: [AndroidDevices] in `android_device_discovery.dart` class ELinuxDeviceDiscovery extends PollingDeviceDiscovery { ELinuxDeviceDiscovery({ @required ELinuxWorkflow eLinuxWorkflow, @@ -88,11 +91,20 @@ class ELinuxDeviceDiscovery extends PollingDeviceDiscovery { }) : _eLinuxWorkflow = eLinuxWorkflow, _logger = logger, _processManager = processManager, + _processUtils = + ProcessUtils(logger: logger, processManager: processManager), + _eLinuxRemoteDevicesConfig = ELinuxRemoteDevicesConfig( + platform: globals.platform, + fileSystem: globals.fs, + logger: logger, + ), super('eLinux devices'); final ELinuxWorkflow _eLinuxWorkflow; final Logger _logger; final ProcessManager _processManager; + final ProcessUtils _processUtils; + final ELinuxRemoteDevicesConfig _eLinuxRemoteDevicesConfig; @override bool get supportsPlatform => _eLinuxWorkflow.appliesToHostPlatform; @@ -106,8 +118,47 @@ class ELinuxDeviceDiscovery extends PollingDeviceDiscovery { return const []; } - return [ - ELinuxDevice('eLinux', + final List devices = []; + + // Adds remote devices. + for (final ELinuxRemoteDeviceConfig remoteDevice + in _eLinuxRemoteDevicesConfig.devices) { + if (!remoteDevice.enabled) { + continue; + } + + String stdout; + RunResult result; + try { + result = await _processUtils.run(remoteDevice.pingCommand, + throwOnError: true); + stdout = result.stdout.trim(); + } on ProcessException catch (ex) { + throwToolExit('ping failed to list attached devices:\n$ex'); + } + + if (result.exitCode == 0 && + stdout.contains(remoteDevice.pingSuccessRegex)) { + final ELinuxDevice device = ELinuxDevice(remoteDevice.id, + desktop: false, + targetArch: remoteDevice.platform, + backendType: remoteDevice.backend, + sdkNameAndVersion: remoteDevice.sdkNameAndVersion, + logger: _logger ?? globals.logger, + processManager: _processManager ?? globals.processManager, + operatingSystemUtils: OperatingSystemUtils( + fileSystem: globals.fs, + logger: _logger ?? globals.logger, + platform: globals.platform, + processManager: const LocalProcessManager(), + )); + devices.add(device); + } + } + + // Adds current desktop host. + devices.add( + ELinuxDevice('elinux', desktop: true, targetArch: _getCurrentHostPlatformArchName(), backendType: 'wayland', @@ -119,7 +170,9 @@ class ELinuxDeviceDiscovery extends PollingDeviceDiscovery { platform: globals.platform, processManager: const LocalProcessManager(), )), - ]; + ); + + return devices; } @override diff --git a/lib/elinux_remote_device_config.dart b/lib/elinux_remote_device_config.dart new file mode 100644 index 0000000..446ed66 --- /dev/null +++ b/lib/elinux_remote_device_config.dart @@ -0,0 +1,359 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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'; + +/// See: [_listsEqual] in `custom_device_config.dart` (exact copy) +bool _listsEqual(List? a, List? 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 e) => e.value == b[e.key]); +} + +/// See: [_regexesEqual] in `custom_device_config.dart` (exact copy) +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; +} + +/// See: [CustomDeviceRevivalException] in `custom_device_config.dart` +@immutable +class ELinuxRemoteDeviceRevivalException implements Exception { + const ELinuxRemoteDeviceRevivalException(this.message); + + const ELinuxRemoteDeviceRevivalException.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 ELinuxRemoteDeviceRevivalException) && + (other.message == message); + } + + @override + int get hashCode => message.hashCode; +} + +/// See: [CustomDeviceConfig] in `custom_device_config.dart` +@immutable +class ELinuxRemoteDeviceConfig { + const ELinuxRemoteDeviceConfig( + {required this.id, + required this.label, + required this.sdkNameAndVersion, + this.platform = 'arm64', + this.backend = 'wayland', + 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); + + factory ELinuxRemoteDeviceConfig.fromJson(dynamic json) { + final Map typedMap = + _castJsonObject(json, 'device configuration', 'a JSON object'); + + final List? 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'); + + if (forwardPortCommand != null && forwardPortSuccessRegex == null) { + throw const ELinuxRemoteDeviceRevivalException( + 'When forwardPort is given, forwardPortSuccessRegex must be specified too.'); + } + + return ELinuxRemoteDeviceConfig( + id: _castString(typedMap[_kId], _kId, 'a string'), + label: _castString(typedMap[_kLabel], _kLabel, 'a string'), + sdkNameAndVersion: _castString( + typedMap[_kSdkNameAndVersion], _kSdkNameAndVersion, 'a string'), + enabled: _castBool(typedMap[_kEnabled], _kEnabled, 'a boolean'), + platform: _castString(typedMap[_kPlatform], _kPlatform, 'arm64'), + backend: _castString(typedMap[_kBackend], _kBackend, 'wayland'), + 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 _kBackend = 'backend'; + 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'; + + final String id; + final String label; + final String sdkNameAndVersion; + final String? platform; + final String? backend; + final bool enabled; + final List pingCommand; + final RegExp? pingSuccessRegex; + final List? postBuildCommand; + final List installCommand; + final List uninstallCommand; + final List runDebugCommand; + final List? forwardPortCommand; + final RegExp? forwardPortSuccessRegex; + final List? screenshotCommand; + + bool get usesPortForwarding => forwardPortCommand != null; + + bool get supportsScreenshotting => screenshotCommand != null; + + static T _maybeRethrowAsRevivalException(T Function() closure, + String fieldDescription, String expectedValueDescription) { + try { + return closure(); + } on Object { + throw ELinuxRemoteDeviceRevivalException.fromDescriptions( + fieldDescription, expectedValueDescription); + } + } + + static Map _castJsonObject( + dynamic value, String fieldDescription, String expectedValueDescription) { + if (value == null) { + throw ELinuxRemoteDeviceRevivalException.fromDescriptions( + fieldDescription, expectedValueDescription); + } + + return _maybeRethrowAsRevivalException( + () => Map.from(value as Map), + fieldDescription, + expectedValueDescription, + ); + } + + static bool _castBool( + dynamic value, String fieldDescription, String expectedValueDescription) { + if (value == null) { + throw ELinuxRemoteDeviceRevivalException.fromDescriptions( + fieldDescription, expectedValueDescription); + } + + return _maybeRethrowAsRevivalException( + () => value as bool, + fieldDescription, + expectedValueDescription, + ); + } + + static String _castString( + dynamic value, String fieldDescription, String expectedValueDescription) { + if (value == null) { + throw ELinuxRemoteDeviceRevivalException.fromDescriptions( + fieldDescription, expectedValueDescription); + } + + return _maybeRethrowAsRevivalException( + () => value as String, + fieldDescription, + expectedValueDescription, + ); + } + + static List _castStringList( + dynamic value, + String fieldDescription, + String expectedValueDescription, { + int minLength = 0, + }) { + if (value == null) { + throw ELinuxRemoteDeviceRevivalException.fromDescriptions( + fieldDescription, expectedValueDescription); + } + + final List list = _maybeRethrowAsRevivalException( + () => List.from(value as Iterable), + fieldDescription, + expectedValueDescription, + ); + + if (list.length < minLength) { + throw ELinuxRemoteDeviceRevivalException.fromDescriptions( + fieldDescription, expectedValueDescription); + } + + return list; + } + + static List? _castStringListOrNull( + dynamic value, + String fieldDescription, + String expectedValueDescription, { + int minLength = 0, + }) { + if (value == null) { + return null; + } + + return _castStringList(value, fieldDescription, expectedValueDescription, + minLength: minLength); + } + + 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 { + _kId: id, + _kLabel: label, + _kSdkNameAndVersion: sdkNameAndVersion, + _kPlatform: platform, + _kEnabled: enabled, + _kBackend: backend, + _kPingCommand: pingCommand, + _kPingSuccessRegex: pingSuccessRegex?.pattern, + _kPostBuildCommand: postBuildCommand, + _kInstallCommand: installCommand, + _kUninstallCommand: uninstallCommand, + _kRunDebugCommand: runDebugCommand, + _kForwardPortCommand: forwardPortCommand, + _kForwardPortSuccessRegex: forwardPortSuccessRegex?.pattern, + _kScreenshotCommand: screenshotCommand, + }; + } + + @override + bool operator ==(Object other) { + return other is ELinuxRemoteDeviceConfig && + other.id == id && + other.label == label && + other.sdkNameAndVersion == sdkNameAndVersion && + other.platform == platform && + other.enabled == enabled && + other.backend == backend && + _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 ^ + backend.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 'ELinuxDeviceConfig(' + 'id: $id, ' + 'label: $label, ' + 'sdkNameAndVersion: $sdkNameAndVersion, ' + 'platform: $platform, ' + 'enabled: $enabled, ' + 'backend: $backend, ' + 'pingCommand: $pingCommand, ' + 'pingSuccessRegex: $pingSuccessRegex, ' + 'postBuildCommand: $postBuildCommand, ' + 'installCommand: $installCommand, ' + 'uninstallCommand: $uninstallCommand, ' + 'runDebugCommand: $runDebugCommand, ' + 'forwardPortCommand: $forwardPortCommand, ' + 'forwardPortSuccessRegex: $forwardPortSuccessRegex, ' + 'screenshotCommand: $screenshotCommand)'; + } +} diff --git a/lib/elinux_remote_devices_config.dart b/lib/elinux_remote_devices_config.dart new file mode 100644 index 0000000..610d88e --- /dev/null +++ b/lib/elinux_remote_devices_config.dart @@ -0,0 +1,140 @@ +// Copyright 2021 Sony Group Corporation. All rights reserved. +// 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:flutter_tools/src/base/config.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/cache.dart'; + +import 'elinux_remote_device_config.dart'; + +/// See: [CustomDevicesConfig] in `custom_devices_config.dart` +class ELinuxRemoteDevicesConfig { + ELinuxRemoteDevicesConfig({ + required Platform platform, + required FileSystem fileSystem, + required Logger logger, + }) : _fileSystem = fileSystem, + _logger = logger, + _config = Config(_kCustomDevicesConfigName, + fileSystem: fileSystem, + logger: logger, + platform: platform, + deleteFileOnFormatException: false) { + ensureFileExists(); + } + + static const String _kCustomDevicesConfigName = 'custom_devices.json'; + static const String _kCustomDevicesConfigKey = 'custom-devices'; + static const String _kSchema = r'$schema'; + static const String _kCustomDevices = 'custom-devices'; + + final FileSystem _fileSystem; + final Logger _logger; + final Config _config; + + String get _defaultSchema { + final Uri uri = _fileSystem + .directory(Cache.flutterRoot) + .childDirectory('packages') + .childDirectory('flutter_tools') + .childDirectory('static') + .childFile('custom-devices.schema.json') + .uri; + assert(uri.isAbsolute); + return uri.toString(); + } + + void ensureFileExists() { + if (!_fileSystem.file(_config.configPath).existsSync()) { + _config.setValue(_kSchema, _defaultSchema); + _config.setValue(_kCustomDevices, [ + ]); + } + } + + List? _getDevicesJsonValue() { + final dynamic json = _config.getValue(_kCustomDevicesConfigKey); + if (json == null) { + return null; + } else if (json is! List) { + const String msg = + "Could not load custom devices config. config['$_kCustomDevicesConfigKey'] is not a JSON array."; + _logger.printError(msg); + throw const ELinuxRemoteDeviceRevivalException(msg); + } + + return json; + } + + List get devices { + final List? typedListNullable = _getDevicesJsonValue(); + if (typedListNullable == null) { + return []; + } + + final List typedList = typedListNullable; + final List revived = []; + for (final MapEntry entry in typedList.asMap().entries) { + try { + revived.add(ELinuxRemoteDeviceConfig.fromJson(entry.value)); + } on ELinuxRemoteDeviceRevivalException catch (e) { + final String msg = + 'Could not load custom device from config index ${entry.key}: $e'; + _logger.printError(msg); + throw ELinuxRemoteDeviceRevivalException(msg); + } + } + + return revived; + } + + List tryGetDevices() { + try { + return devices; + } on Exception { + return []; + } + } + + set devices(List configs) { + _config.setValue( + _kCustomDevicesConfigKey, + configs + .map((ELinuxRemoteDeviceConfig c) => c.toJson()) + .toList()); + } + + void add(ELinuxRemoteDeviceConfig config) { + _config.setValue(_kCustomDevicesConfigKey, + [...?_getDevicesJsonValue(), config.toJson()]); + } + + bool contains(String deviceId) { + return devices + .any((ELinuxRemoteDeviceConfig device) => device.id == deviceId); + } + + bool remove(String deviceId) { + final List modifiedDevices = devices; + final ELinuxRemoteDeviceConfig? device = modifiedDevices + .cast() + .firstWhere((ELinuxRemoteDeviceConfig? d) => d!.id == deviceId, + orElse: () => null); + + if (device == null) { + return false; + } + + modifiedDevices.remove(device); + devices = modifiedDevices; + return true; + } + + String get configPath => _config.configPath; + + void forEach(Null Function(int value) param0, void add) {} +}