Unverified Commit 82830fa1 authored by Hannes Winkler's avatar Hannes Winkler Committed by GitHub

[custom-devices] add screenshotting support (#80675)

parent 7bff366b
...@@ -11,6 +11,7 @@ import 'package:process/process.dart'; ...@@ -11,6 +11,7 @@ import 'package:process/process.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/process.dart'; import '../base/process.dart';
...@@ -594,6 +595,24 @@ class CustomDevice extends Device { ...@@ -594,6 +595,24 @@ class CustomDevice extends Device {
@override @override
Future<bool> get isLocalEmulator async => false; Future<bool> get isLocalEmulator async => false;
@override
bool get supportsScreenshot => _config.supportsScreenshotting;
@override
Future<void> takeScreenshot(File outputFile) async {
if (supportsScreenshot == false) {
throw UnsupportedError('Screenshotting is not supported for this device.');
}
final List<String> interpolated = interpolateCommand(
_config.screenshotCommand,
<String, String>{},
);
final RunResult result = await _processUtils.run(interpolated, throwOnError: true);
await outputFile.writeAsBytes(base64Decode(result.stdout));
}
@override @override
bool isSupported() { bool isSupported() {
return true; return true;
......
...@@ -22,7 +22,8 @@ class CustomDeviceConfig { ...@@ -22,7 +22,8 @@ class CustomDeviceConfig {
required this.uninstallCommand, required this.uninstallCommand,
required this.runDebugCommand, required this.runDebugCommand,
this.forwardPortCommand, this.forwardPortCommand,
this.forwardPortSuccessRegex this.forwardPortSuccessRegex,
this.screenshotCommand
}) : assert(forwardPortCommand == null || forwardPortSuccessRegex != null); }) : assert(forwardPortCommand == null || forwardPortSuccessRegex != null);
factory CustomDeviceConfig.fromJson(dynamic json) { factory CustomDeviceConfig.fromJson(dynamic json) {
...@@ -40,7 +41,8 @@ class CustomDeviceConfig { ...@@ -40,7 +41,8 @@ class CustomDeviceConfig {
uninstallCommand: _castStringList(typedMap[_kUninstallCommand]!), uninstallCommand: _castStringList(typedMap[_kUninstallCommand]!),
runDebugCommand: _castStringList(typedMap[_kRunDebugCommand]!), runDebugCommand: _castStringList(typedMap[_kRunDebugCommand]!),
forwardPortCommand: _castStringListOrNull(typedMap[_kForwardPortCommand]), forwardPortCommand: _castStringListOrNull(typedMap[_kForwardPortCommand]),
forwardPortSuccessRegex: _convertToRegexOrNull(typedMap[_kForwardPortSuccessRegex]) forwardPortSuccessRegex: _convertToRegexOrNull(typedMap[_kForwardPortSuccessRegex]),
screenshotCommand: _castStringListOrNull(typedMap[_kScreenshotCommand])
); );
} }
...@@ -56,6 +58,7 @@ class CustomDeviceConfig { ...@@ -56,6 +58,7 @@ class CustomDeviceConfig {
static const String _kRunDebugCommand = 'runDebug'; static const String _kRunDebugCommand = 'runDebug';
static const String _kForwardPortCommand = 'forwardPort'; static const String _kForwardPortCommand = 'forwardPort';
static const String _kForwardPortSuccessRegex = 'forwardPortSuccessRegex'; static const String _kForwardPortSuccessRegex = 'forwardPortSuccessRegex';
static const String _kScreenshotCommand = 'screenshot';
/// An example device config used for creating the default config file. /// An example device config used for creating the default config file.
static final CustomDeviceConfig example = CustomDeviceConfig( static final CustomDeviceConfig example = CustomDeviceConfig(
...@@ -70,7 +73,8 @@ class CustomDeviceConfig { ...@@ -70,7 +73,8 @@ class CustomDeviceConfig {
uninstallCommand: const <String>['ssh', 'pi@raspberrypi', r'rm -rf "/tmp/${appName}"'], uninstallCommand: const <String>['ssh', 'pi@raspberrypi', r'rm -rf "/tmp/${appName}"'],
runDebugCommand: const <String>['ssh', 'pi@raspberrypi', r'flutter-pi "/tmp/${appName}"'], runDebugCommand: const <String>['ssh', 'pi@raspberrypi', r'flutter-pi "/tmp/${appName}"'],
forwardPortCommand: const <String>['ssh', '-o', 'ExitOnForwardFailure=yes', '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}', 'pi@raspberrypi'], forwardPortCommand: const <String>['ssh', '-o', 'ExitOnForwardFailure=yes', '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}', 'pi@raspberrypi'],
forwardPortSuccessRegex: RegExp('Linux') forwardPortSuccessRegex: RegExp('Linux'),
screenshotCommand: const <String>['ssh', 'pi@raspberrypi', r"fbgrab /tmp/screenshot.png && cat /tmp/screenshot.png | base64 | tr -d ' \n\t'"]
); );
final String id; final String id;
...@@ -85,9 +89,12 @@ class CustomDeviceConfig { ...@@ -85,9 +89,12 @@ class CustomDeviceConfig {
final List<String> runDebugCommand; final List<String> runDebugCommand;
final List<String>? forwardPortCommand; final List<String>? forwardPortCommand;
final RegExp? forwardPortSuccessRegex; final RegExp? forwardPortSuccessRegex;
final List<String>? screenshotCommand;
bool get usesPortForwarding => forwardPortCommand != null; bool get usesPortForwarding => forwardPortCommand != null;
bool get supportsScreenshotting => screenshotCommand != null;
static List<String> _castStringList(Object object) { static List<String> _castStringList(Object object) {
return (object as List<dynamic>).cast<String>(); return (object as List<dynamic>).cast<String>();
} }
...@@ -113,7 +120,8 @@ class CustomDeviceConfig { ...@@ -113,7 +120,8 @@ class CustomDeviceConfig {
_kUninstallCommand: uninstallCommand, _kUninstallCommand: uninstallCommand,
_kRunDebugCommand: runDebugCommand, _kRunDebugCommand: runDebugCommand,
_kForwardPortCommand: forwardPortCommand, _kForwardPortCommand: forwardPortCommand,
_kForwardPortSuccessRegex: forwardPortSuccessRegex?.pattern _kForwardPortSuccessRegex: forwardPortSuccessRegex?.pattern,
_kScreenshotCommand: screenshotCommand,
}; };
} }
...@@ -133,7 +141,9 @@ class CustomDeviceConfig { ...@@ -133,7 +141,9 @@ class CustomDeviceConfig {
bool explicitForwardPortCommand = false, bool explicitForwardPortCommand = false,
List<String>? forwardPortCommand, List<String>? forwardPortCommand,
bool explicitForwardPortSuccessRegex = false, bool explicitForwardPortSuccessRegex = false,
RegExp? forwardPortSuccessRegex RegExp? forwardPortSuccessRegex,
bool explicitScreenshotCommand = false,
List<String>? screenshotCommand
}) { }) {
return CustomDeviceConfig( return CustomDeviceConfig(
id: id ?? this.id, id: id ?? this.id,
...@@ -147,7 +157,8 @@ class CustomDeviceConfig { ...@@ -147,7 +157,8 @@ class CustomDeviceConfig {
uninstallCommand: uninstallCommand ?? this.uninstallCommand, uninstallCommand: uninstallCommand ?? this.uninstallCommand,
runDebugCommand: runDebugCommand ?? this.runDebugCommand, runDebugCommand: runDebugCommand ?? this.runDebugCommand,
forwardPortCommand: explicitForwardPortCommand ? forwardPortCommand : (forwardPortCommand ?? this.forwardPortCommand), forwardPortCommand: explicitForwardPortCommand ? forwardPortCommand : (forwardPortCommand ?? this.forwardPortCommand),
forwardPortSuccessRegex: explicitForwardPortSuccessRegex ? forwardPortSuccessRegex : (forwardPortSuccessRegex ?? this.forwardPortSuccessRegex) forwardPortSuccessRegex: explicitForwardPortSuccessRegex ? forwardPortSuccessRegex : (forwardPortSuccessRegex ?? this.forwardPortSuccessRegex),
screenshotCommand: explicitScreenshotCommand ? screenshotCommand : (screenshotCommand ?? this.screenshotCommand),
); );
} }
} }
...@@ -107,6 +107,18 @@ ...@@ -107,6 +107,18 @@
"format": "regex", "format": "regex",
"default": "Linux", "default": "Linux",
"required": false "required": false
},
"screenshot": {
"description": "Take a screenshot of the app as a png image. This command should take the screenshot, convert it to png, then base64 encode it and echo to stdout. Any stderr output will be ignored. If this command is not given, screenshotting will be disabled for this device.",
"type": ["array", "null"],
"items": {
"type": "string"
},
"minItems": 1,
"default": [
"ssh", "pi@raspberrypi", "fbgrab /tmp/screenshot.png && cat /tmp/screenshot.png | base64 | tr -d ' \\n\\t'"
],
"required": false
} }
} }
} }
......
...@@ -10,6 +10,7 @@ import 'package:file/file.dart'; ...@@ -10,6 +10,7 @@ import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:file/src/interface/directory.dart'; import 'package:file/src/interface/directory.dart';
import 'package:file/src/interface/file.dart'; import 'package:file/src/interface/file.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/build_system.dart';
...@@ -108,7 +109,8 @@ void main() { ...@@ -108,7 +109,8 @@ void main() {
uninstallCommand: const <String>['testuninstall'], uninstallCommand: const <String>['testuninstall'],
runDebugCommand: const <String>['testrundebug'], runDebugCommand: const <String>['testrundebug'],
forwardPortCommand: const <String>['testforwardport'], forwardPortCommand: const <String>['testforwardport'],
forwardPortSuccessRegex: RegExp('testforwardportsuccess') forwardPortSuccessRegex: RegExp('testforwardportsuccess'),
screenshotCommand: const <String>['testscreenshot']
); );
const String testConfigPingSuccessOutput = 'testpingsuccess\n'; const String testConfigPingSuccessOutput = 'testpingsuccess\n';
...@@ -520,6 +522,63 @@ void main() { ...@@ -520,6 +522,63 @@ void main() {
ProcessManager: () => FakeProcessManager.any() ProcessManager: () => FakeProcessManager.any()
} }
); );
testWithoutContext('CustomDevice screenshotting', () async {
bool screenshotCommandWasExecuted = false;
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: testConfig.screenshotCommand,
onRun: () => screenshotCommandWasExecuted = true,
)
]);
final MemoryFileSystem fs = MemoryFileSystem.test();
final File screenshotFile = fs.file('screenshot.png');
final CustomDevice device = CustomDevice(
config: testConfig,
logger: BufferLogger.test(),
processManager: processManager
);
expect(device.supportsScreenshot, true);
await device.takeScreenshot(screenshotFile);
expect(screenshotCommandWasExecuted, true);
expect(screenshotFile, exists);
});
testWithoutContext('CustomDevice without screenshotting support', () async {
bool screenshotCommandWasExecuted = false;
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: testConfig.screenshotCommand,
onRun: () => screenshotCommandWasExecuted = true,
)
]);
final MemoryFileSystem fs = MemoryFileSystem.test();
final File screenshotFile = fs.file('screenshot.png');
final CustomDevice device = CustomDevice(
config: testConfig.copyWith(
explicitScreenshotCommand: true,
screenshotCommand: null
),
logger: BufferLogger.test(),
processManager: processManager
);
expect(device.supportsScreenshot, false);
expect(
() => device.takeScreenshot(screenshotFile),
throwsA(const TypeMatcher<UnsupportedError>()),
);
expect(screenshotCommandWasExecuted, false);
expect(screenshotFile.existsSync(), false);
});
} }
class FakeBundleBuilder extends Fake implements BundleBuilder { class FakeBundleBuilder extends Fake implements BundleBuilder {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment