| // Copyright 2013 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:file/file.dart'; |
| import 'package:file/local.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| |
| import 'command_line.dart'; |
| import 'operation_result.dart'; |
| import 'ssh_key_manager.dart'; |
| |
| /// A wrapper for running Fuchsia images on the Android Emulator (AEMU). |
| class Emulator { |
| /// Creates a new wrapper for the `emu` tool. |
| Emulator({ |
| @required this.aemuPath, |
| @required this.fuchsiaImagePath, |
| @required this.fuchsiaSdkPath, |
| this.fs = const LocalFileSystem(), |
| this.cli = const CommandLine(), |
| @required this.qemuKernelPath, |
| @required this.sshKeyManager, |
| @required this.zbiPath, |
| }) : assert(cli != null), |
| assert(fs != null); |
| |
| /// The path to the AEMU executable on disk. |
| final String aemuPath; |
| |
| /// Fuchsia image to load into the emulator. |
| final String fuchsiaImagePath; |
| |
| /// The path to the Fuchsia SDK that contains the tools `fvm` and `zbi`. |
| final String fuchsiaSdkPath; |
| |
| /// The QEMU kernel image to use. This is only bundled in Fuchsia QEMU images. |
| final String qemuKernelPath; |
| |
| /// The Fuchsia bootloader image. |
| final String zbiPath; |
| |
| /// Location of `fvm` in [fuchsiaSdkPath]. |
| @visibleForTesting |
| final String fvmToolPath = 'sdk/tools/fvm'; |
| |
| /// Location of `zbi` in [fuchsiaSdkPath]. |
| @visibleForTesting |
| final String zbiToolPath = 'sdk/tools/zbi'; |
| |
| /// Default AEMU window size to be launched. |
| @visibleForTesting |
| final String defaultWindowSize = '1280x800'; |
| |
| /// Flag to pass to AEMU to run in headless mode. |
| @visibleForTesting |
| final String aemuHeadlessFlag = '-no-window'; |
| |
| /// [SshKeyManager] for creating `authorized_keys` to access emulator. |
| final SshKeyManager sshKeyManager; |
| |
| /// The [FileSystem] to use when running the `emu` tool. |
| final FileSystem fs; |
| |
| /// The [CommandLine] wrapper for interacting with the current shell. |
| final CommandLine cli; |
| |
| /// FVM extended image of [fuchsiaImagePath] for running on FEMU. |
| @visibleForTesting |
| String fvmImagePath; |
| |
| /// [zbiPath] that is accessible with SSH using [sshPath] keys. |
| @visibleForTesting |
| String signedZbiPath; |
| |
| /// Update given Fuchsia assets to make them compatible with FEMU. |
| /// |
| /// 1. Ensure required assets exist. |
| /// 2. Create FVM image for running with FEMU. |
| /// 3. Sign boot image for host access to the guest FEMU instance. |
| Future<void> prepareEnvironment() async { |
| assert(fs.isFileSync(fuchsiaImagePath)); |
| assert(fs.isFileSync(zbiPath)); |
| assert(fs.isFileSync(qemuKernelPath)); |
| |
| final String tmpPath = fs.systemTempDirectory.createTempSync().path; |
| fvmImagePath = '$tmpPath/fvm.blk'; |
| signedZbiPath = '$tmpPath/fuchsia-ssh.zbi'; |
| |
| await _prepareFvmImage(fuchsiaImagePath, fvmImagePath); |
| await _signBootImage(zbiPath, signedZbiPath); |
| } |
| |
| /// Double the size of [fuchsiaImagePath] to make space for the emulator |
| /// to write back to it. |
| Future<void> _prepareFvmImage(String fuchsiaImagePath, String fvmPath, |
| {String fvmExecutable}) async { |
| fvmExecutable ??= path.join(fuchsiaSdkPath, fvmToolPath); |
| |
| await cli.run(<String>['cp', fuchsiaImagePath, fvmPath]); |
| |
| /// [fvmTool] and FEMU need write access to [fvmPath]. |
| await cli.run(<String>['chmod', 'u+w', fvmPath]); |
| |
| // Calculate new size by doubling the current size |
| final File fvmFile = fs.file(fvmPath)..createSync(); |
| final int newSize = fvmFile.lengthSync() * 2; |
| |
| await cli.run( |
| <String>[fvmExecutable, fvmPath, 'extend', '--length', '$newSize']); |
| } |
| |
| /// Signed [zbiPath] using [zbiExecutable] with [publicKeyPath] to |
| /// create a bootloader image that is accessible from the host. |
| Future<void> _signBootImage(String zbiPath, String signedZbiPath, |
| {String zbiExecutable}) async { |
| zbiExecutable ??= path.join(fuchsiaSdkPath, zbiToolPath); |
| |
| await sshKeyManager.createKeys(); |
| |
| /// Ensure `zbi` is able to find the ssh keys by giving the full path. |
| final File authorizedKeysAbsolute = |
| fs.file('.ssh/authorized_keys').absolute; |
| |
| final List<String> zbiCommand = <String>[ |
| zbiExecutable, |
| '--compressed=zstd', |
| '-o', |
| signedZbiPath, |
| zbiPath, |
| '-e', |
| 'data/ssh/authorized_keys=${authorizedKeysAbsolute.path}' |
| ]; |
| await cli.run(zbiCommand); |
| } |
| |
| /// Launch AEMU with [fvmImagePath], [signedZbiPath], and [qemuKernelPath]. |
| /// |
| /// [prepareEnvironment] must have been called before starting the emulator. |
| /// |
| /// If [headless] is true, AEMU will run without a graphical window. Infra |
| /// will run AEMU in headless mode. |
| /// |
| /// [windowSize] is what AEMU will set its window size to. Defaults to |
| /// [defaultWindowSize]. Expected to be in the format of "WIDTHxHEIGHT". |
| Future<OperationResult> start( |
| {bool headless = false, String windowSize}) async { |
| assert(fvmImagePath != null && fs.isFileSync(fvmImagePath)); |
| assert(signedZbiPath != null && fs.isFileSync(signedZbiPath)); |
| |
| final List<String> aemuCommand = <String>[ |
| aemuPath, |
| '-feature', |
| 'VirtioInput,RefCountPipe,KVM,GLDirectMem,Vulkan', |
| '-window-size', |
| windowSize ?? defaultWindowSize, |
| '-gpu', |
| 'swiftshader_indirect', |
| if (headless) aemuHeadlessFlag, |
| |
| /// Anything after -fuchsia flag will be passed to QEMU |
| '-fuchsia', |
| '-kernel', qemuKernelPath, |
| '-initrd', signedZbiPath, |
| '-m', '2048', |
| '-serial', 'stdio', |
| '-vga', 'none', |
| '-device', 'virtio-keyboard-pci', |
| '-device', 'virtio_input_multi_touch_pci_1', |
| '-smp', '4,threads=2', |
| '-machine', 'q35', |
| '-device', 'isa-debug-exit,iobase=0xf4,iosize=0x04', |
| // TODO(chillers): Add hardware acceleration option to configure this. |
| '-enable-kvm', |
| '-cpu', 'host,migratable=no,+invtsc', |
| '-netdev', 'type=tap,ifname=qemu,script=no,downscript=no,id=net0', |
| '-device', 'e1000,netdev=net0,mac=52:54:00:63:5e:7a', |
| '-drive', 'file=$fvmImagePath,format=raw,if=none,id=vdisk', |
| '-device', 'virtio-blk-pci,drive=vdisk', |
| '-append', |
| // TODO(chillers): Generate entropy mixin. |
| "'TERM=xterm-256color kernel.serial=legacy kernel.entropy-mixin=660486b6b20b4ace3fb5c81b0002abf5271289185c6a5620707606c55b377562 kernel.halt-on-panic=true'", |
| ]; |
| |
| await cli.start(aemuCommand); |
| |
| return OperationResult.success(); |
| } |
| } |