Visitar URL original
[fuchsia_ctl] initial emulator support (#147) · flutter/packages@c19aef9 · GitHub
Skip to content

Commit c19aef9

Browse files
authored
[fuchsia_ctl] initial emulator support (#147)
1 parent caa67e0 commit c19aef9

13 files changed

Lines changed: 539 additions & 14 deletions

‎packages/fuchsia_ctl/CHANGELOG.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
# CHANGELOG
22

3+
## 0.0.23
34

5+
- Added `emu` tool for spawning an emulator instance given a Fuchsia QEMU build,
6+
the Fuchsia SDK, and an Android emulator executable.
7+
- Fixed homepage link.
48

5-
## 0.0.18 - 0.0.21
9+
## 0.0.18 - 0.0.22
610

711
- Add retries to paving operations.
812
- Add timeouts to paving and ssh operations.
913
- Add arguments parameter to test command.
1014

11-
12-
1315
## 0.0.17
1416

1517
- Add "push-packages" option.

‎packages/fuchsia_ctl/bin/main.dart‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,18 @@ import 'dart:io';
77
import 'package:args/args.dart';
88
import 'package:file/file.dart';
99
import 'package:file/local.dart';
10-
import 'package:fuchsia_ctl/fuchsia_ctl.dart';
11-
import 'package:fuchsia_ctl/src/amber_ctl.dart';
12-
import 'package:fuchsia_ctl/src/operation_result.dart';
13-
import 'package:fuchsia_ctl/src/tar.dart';
1410
import 'package:meta/meta.dart';
1511
import 'package:path/path.dart' as path;
1612
import 'package:retry/retry.dart';
1713
import 'package:uuid/uuid.dart';
1814

15+
import 'package:fuchsia_ctl/fuchsia_ctl.dart';
16+
1917
typedef AsyncResult = Future<OperationResult> Function(
2018
String, DevFinder, ArgResults);
2119

2220
const Map<String, AsyncResult> commands = <String, AsyncResult>{
21+
'emu': emulator,
2322
'pave': pave,
2423
'pm': pm,
2524
'ssh': ssh,
@@ -42,6 +41,21 @@ Future<void> main(List<String> args) async {
4241
defaultsTo: './dev_finder',
4342
help: 'The path to the dev_finder executable.')
4443
..addFlag('help', defaultsTo: false, help: 'Prints help.');
44+
45+
/// This is a blocking command and will run until exited.
46+
parser.addCommand('emu')
47+
..addOption('image', help: 'Fuchsia image to run')
48+
..addOption('zbi', help: 'Bootloader image to sign and run')
49+
..addOption('qemu-kernel', help: 'QEMU kernel to run')
50+
..addOption('window-size', help: 'Emulator window size formatted "WxH"')
51+
..addOption('aemu', help: 'AEMU executable path')
52+
..addOption('sdk',
53+
help: 'Location to Fuchsia SDK containing tools and images')
54+
..addOption('public-key',
55+
defaultsTo: '.fuchsia/authorized_keys',
56+
help: 'Path to the authorized_keys to sign zbi image with')
57+
..addFlag('headless', help: 'Run FEMU without graphical window');
58+
4559
parser.addCommand('ssh')
4660
..addFlag('interactive',
4761
abbr: 'i',
@@ -129,6 +143,30 @@ Future<void> main(List<String> args) async {
129143
}
130144
}
131145

146+
@visibleForTesting
147+
Future<OperationResult> emulator(
148+
String deviceName,
149+
DevFinder devFinder,
150+
ArgResults args,
151+
) async {
152+
final Emulator emulator = Emulator(
153+
aemuPath: args['aemu'],
154+
fuchsiaImagePath: args['image'],
155+
fuchsiaSdkPath: args['sdk'],
156+
qemuKernelPath: args['qemu-kernel'],
157+
sshKeyManager: SystemSshKeyManager.defaultProvider(
158+
publicKeyPath: args['public-key'],
159+
),
160+
zbiPath: args['zbi'],
161+
);
162+
await emulator.prepareEnvironment();
163+
164+
return emulator.start(
165+
headless: args['headless'],
166+
windowSize: args['window-size'],
167+
);
168+
}
169+
132170
@visibleForTesting
133171
Future<OperationResult> ssh(
134172
String deviceName,

‎packages/fuchsia_ctl/lib/fuchsia_ctl.dart‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
export 'src/amber_ctl.dart';
56
export 'src/dev_finder.dart';
7+
export 'src/emulator.dart';
68
export 'src/image_paver.dart';
79
export 'src/operation_result.dart';
810
export 'src/package_server.dart';
911
export 'src/ssh_client.dart';
12+
export 'src/ssh_key_manager.dart';
13+
export 'src/tar.dart';

‎packages/fuchsia_ctl/lib/src/amber_ctl.dart‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
import 'dart:io';
66

77
import 'package:meta/meta.dart';
8-
import 'package:fuchsia_ctl/fuchsia_ctl.dart';
9-
import 'package:fuchsia_ctl/src/ssh_client.dart';
108
import 'package:uuid/uuid.dart';
119

12-
import 'operation_result.dart';
10+
import 'package:fuchsia_ctl/fuchsia_ctl.dart';
1311

1412
const SshClient _kSsh = SshClient();
1513

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2020 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:meta/meta.dart';
8+
import 'package:process/process.dart';
9+
10+
/// Class for common actions with processes on the command line.
11+
@immutable
12+
class CommandLine {
13+
/// Create a new instance of [CommandLine].
14+
const CommandLine(
15+
{this.processManager = const LocalProcessManager(),
16+
@visibleForTesting this.stdoutValue,
17+
@visibleForTesting this.stderrValue});
18+
19+
/// The underlying [ProcessManager] to use for running on the current shell.
20+
final ProcessManager processManager;
21+
22+
/// Mock value for use in tests.
23+
final Stdout stdoutValue;
24+
25+
/// Mock value for use in tests.
26+
final Stdout stderrValue;
27+
28+
/// Current shells stdout to use.
29+
Stdout get shellStdout => stdoutValue ?? stdout;
30+
31+
/// Current shells stderr to use.
32+
Stdout get shellStderr => stderrValue ?? stderr;
33+
34+
/// Run [command] and handle its stdio. Once [command] is complete, it will
35+
/// output its stdio to the console.
36+
///
37+
/// Use this for tasks where stdio does not need to be monitored.
38+
///
39+
/// Throw [CommandLineException] if [command] returns a non-0 exit code.
40+
Future<void> run(List<String> command) async {
41+
shellStdout.writeln(command.join(' '));
42+
final ProcessResult process = await processManager.run(command);
43+
shellStdout.writeln(process.stdout);
44+
shellStderr.writeln(process.stderr);
45+
46+
if (process.exitCode != 0) {
47+
throw CommandLineException('${command.first} did not return exit code 0');
48+
}
49+
}
50+
51+
/// Start [command] and handle its stdio by streaming it to the existing
52+
/// stdio. While [command] is running, its stdio is streamed to the shell.
53+
///
54+
/// Use this for long running tasks where stdio should be monitored.
55+
///
56+
/// Throw [CommandLineException] if [command] returns a non-0 exit code.
57+
Future<Process> start(List<String> command) async {
58+
shellStdout.writeln(command.join(' '));
59+
final Process process = await processManager.start(command);
60+
shellStdout.addStream(process.stdout);
61+
shellStderr.addStream(process.stderr);
62+
63+
if (await process.exitCode != 0) {
64+
throw CommandLineException('${command.first} did not return exit code 0');
65+
}
66+
67+
return process;
68+
}
69+
}
70+
71+
/// Wraps exceptions thrown by [CommandLine].
72+
class CommandLineException implements Exception {
73+
/// Creates a new [CommandLineException].
74+
const CommandLineException(this.message);
75+
76+
/// The user-facing message to display.
77+
final String message;
78+
79+
@override
80+
String toString() => message;
81+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2019 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:file/file.dart';
8+
import 'package:file/local.dart';
9+
import 'package:meta/meta.dart';
10+
import 'package:path/path.dart' as path;
11+
12+
import 'command_line.dart';
13+
import 'operation_result.dart';
14+
import 'ssh_key_manager.dart';
15+
16+
/// A wrapper for running Fuchsia images on the Android Emulator (AEMU).
17+
class Emulator {
18+
/// Creates a new wrapper for the `emu` tool.
19+
Emulator({
20+
@required this.aemuPath,
21+
@required this.fuchsiaImagePath,
22+
@required this.fuchsiaSdkPath,
23+
this.fs = const LocalFileSystem(),
24+
this.cli = const CommandLine(),
25+
@required this.qemuKernelPath,
26+
@required this.sshKeyManager,
27+
@required this.zbiPath,
28+
}) : assert(cli != null),
29+
assert(fs != null);
30+
31+
/// The path to the AEMU executable on disk.
32+
final String aemuPath;
33+
34+
/// Fuchsia image to load into the emulator.
35+
final String fuchsiaImagePath;
36+
37+
/// The path to the Fuchsia SDK that contains the tools `fvm` and `zbi`.
38+
final String fuchsiaSdkPath;
39+
40+
/// The QEMU kernel image to use. This is only bundled in Fuchsia QEMU images.
41+
final String qemuKernelPath;
42+
43+
/// The Fuchsia bootloader image.
44+
final String zbiPath;
45+
46+
/// Location of `fvm` in [fuchsiaSdkPath].
47+
@visibleForTesting
48+
final String fvmToolPath = 'sdk/tools/fvm';
49+
50+
/// Location of `zbi` in [fuchsiaSdkPath].
51+
@visibleForTesting
52+
final String zbiToolPath = 'sdk/tools/zbi';
53+
54+
/// Default AEMU window size to be launched.
55+
@visibleForTesting
56+
final String defaultWindowSize = '1280x800';
57+
58+
/// Flag to pass to AEMU to run in headless mode.
59+
@visibleForTesting
60+
final String aemuHeadlessFlag = '-no-window';
61+
62+
/// [SshKeyManager] for creating `authorized_keys` to access emulator.
63+
final SshKeyManager sshKeyManager;
64+
65+
/// The [FileSystem] to use when running the `emu` tool.
66+
final FileSystem fs;
67+
68+
/// The [CommandLine] wrapper for interacting with the current shell.
69+
final CommandLine cli;
70+
71+
/// FVM extended image of [fuchsiaImagePath] for running on FEMU.
72+
@visibleForTesting
73+
String fvmImagePath;
74+
75+
/// [zbiPath] that is accessible with SSH using [sshPath] keys.
76+
@visibleForTesting
77+
String signedZbiPath;
78+
79+
/// Update given Fuchsia assets to make them compatible with FEMU.
80+
///
81+
/// 1. Ensure required assets exist.
82+
/// 2. Create FVM image for running with FEMU.
83+
/// 3. Sign boot image for host access to the guest FEMU instance.
84+
Future<void> prepareEnvironment() async {
85+
assert(fs.isFileSync(fuchsiaImagePath));
86+
assert(fs.isFileSync(zbiPath));
87+
assert(fs.isFileSync(qemuKernelPath));
88+
89+
final String tmpPath = fs.systemTempDirectory.createTempSync().path;
90+
fvmImagePath = '$tmpPath/fvm.blk';
91+
signedZbiPath = '$tmpPath/fuchsia-ssh.zbi';
92+
93+
await _prepareFvmImage(fuchsiaImagePath, fvmImagePath);
94+
await _signBootImage(zbiPath, signedZbiPath);
95+
}
96+
97+
/// Double the size of [fuchsiaImagePath] to make space for the emulator
98+
/// to write back to it.
99+
Future<void> _prepareFvmImage(String fuchsiaImagePath, String fvmPath,
100+
{String fvmExecutable}) async {
101+
fvmExecutable ??= path.join(fuchsiaSdkPath, fvmToolPath);
102+
103+
await cli.run(<String>['cp', fuchsiaImagePath, fvmPath]);
104+
105+
/// [fvmTool] and FEMU need write access to [fvmPath].
106+
await cli.run(<String>['chmod', 'u+w', fvmPath]);
107+
108+
// Calculate new size by doubling the current size
109+
final File fvmFile = fs.file(fvmPath)..createSync();
110+
final int newSize = fvmFile.lengthSync() * 2;
111+
112+
await cli.run(
113+
<String>[fvmExecutable, fvmPath, 'extend', '--length', '$newSize']);
114+
}
115+
116+
/// Signed [zbiPath] using [zbiExecutable] with [publicKeyPath] to
117+
/// create a bootloader image that is accessible from the host.
118+
Future<void> _signBootImage(String zbiPath, String signedZbiPath,
119+
{String zbiExecutable}) async {
120+
zbiExecutable ??= path.join(fuchsiaSdkPath, zbiToolPath);
121+
122+
await sshKeyManager.createKeys();
123+
124+
/// Ensure `zbi` is able to find the ssh keys by giving the full path.
125+
final File authorizedKeysAbsolute =
126+
fs.file('.ssh/authorized_keys').absolute;
127+
128+
final List<String> zbiCommand = <String>[
129+
zbiExecutable,
130+
'--compressed=zstd',
131+
'-o',
132+
signedZbiPath,
133+
zbiPath,
134+
'-e',
135+
'data/ssh/authorized_keys=${authorizedKeysAbsolute.path}'
136+
];
137+
await cli.run(zbiCommand);
138+
}
139+
140+
/// Launch AEMU with [fvmImagePath], [signedZbiPath], and [qemuKernelPath].
141+
///
142+
/// [prepareEnvironment] must have been called before starting the emulator.
143+
///
144+
/// If [headless] is true, AEMU will run without a graphical window. Infra
145+
/// will run AEMU in headless mode.
146+
///
147+
/// [windowSize] is what AEMU will set its window size to. Defaults to
148+
/// [defaultWindowSize]. Expected to be in the format of "WIDTHxHEIGHT".
149+
Future<OperationResult> start(
150+
{bool headless = false, String windowSize}) async {
151+
assert(fvmImagePath != null && fs.isFileSync(fvmImagePath));
152+
assert(signedZbiPath != null && fs.isFileSync(signedZbiPath));
153+
154+
final List<String> aemuCommand = <String>[
155+
aemuPath,
156+
'-feature',
157+
'VirtioInput,RefCountPipe,KVM,GLDirectMem,Vulkan',
158+
'-window-size',
159+
windowSize ?? defaultWindowSize,
160+
'-gpu',
161+
'swiftshader_indirect',
162+
if (headless) aemuHeadlessFlag,
163+
164+
/// Anything after -fuchsia flag will be passed to QEMU
165+
'-fuchsia',
166+
'-kernel', qemuKernelPath,
167+
'-initrd', signedZbiPath,
168+
'-m', '2048',
169+
'-serial', 'stdio',
170+
'-vga', 'none',
171+
'-device', 'virtio-keyboard-pci',
172+
'-device', 'virtio_input_multi_touch_pci_1',
173+
'-smp', '4,threads=2',
174+
'-machine', 'q35',
175+
'-device', 'isa-debug-exit,iobase=0xf4,iosize=0x04',
176+
// TODO(chillers): Add hardware acceleration option to configure this.
177+
'-enable-kvm',
178+
'-cpu', 'host,migratable=no,+invtsc',
179+
'-netdev', 'type=tap,ifname=qemu,script=no,downscript=no,id=net0',
180+
'-device', 'e1000,netdev=net0,mac=52:54:00:63:5e:7a',
181+
'-drive', 'file=$fvmImagePath,format=raw,if=none,id=vdisk',
182+
'-device', 'virtio-blk-pci,drive=vdisk',
183+
'-append',
184+
// TODO(chillers): Generate entropy mixin.
185+
'\'TERM=xterm-256color kernel.serial=legacy kernel.entropy-mixin=660486b6b20b4ace3fb5c81b0002abf5271289185c6a5620707606c55b377562 kernel.halt-on-panic=true\'',
186+
];
187+
188+
await cli.start(aemuCommand);
189+
190+
return OperationResult.success();
191+
}
192+
}

0 commit comments

Comments
 (0)