blob: a3880c560dfec421a956e0123dc92046f569f22a [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'repository.dart';
import 'utils.dart';
class FuchsiaEmulator {
static final Uri toolsDir =
Repository.uri.resolve('third_party/fuchsia/sdk/linux/bin/');
static final String femuTool = toolsDir.resolve('femu.sh').toFilePath();
static final String fserveTool = toolsDir.resolve('fserve.sh').toFilePath();
static final String fpubTool = toolsDir.resolve('fpublish.sh').toFilePath();
static final String fsshTool = toolsDir.resolve('fssh.sh').toFilePath();
static final RegExp emulatorReadyPattern =
RegExp(r'Using unique host name (.+)\.local\.');
static final RegExp emulatorPidPattern =
RegExp(r'([0-9]+) .* qemu-system-x86');
static final String serverReadyPattern = '[pm serve] serving';
static FuchsiaEmulator _inst;
Process _emu;
Process _server;
String _deviceName;
static Future<void> publishPackage(String buildDir, String mode) async {
if (_inst == null) {
_inst = FuchsiaEmulator();
await _inst._start();
}
await _inst._publishPackage(buildDir, mode);
}
static void stop() {
_inst?._stop();
}
static List<String> getTestArgs(String mode, List<String> arguments) {
return _inst._getSshArgs(
mode,
arguments.map((arg) =>
arg.replaceAll(Repository.uri.toFilePath(), '/pkg/data/')));
}
Future<void> _start() async {
// Start the emulator.
DebugLogger.info('Starting Fuchsia emulator');
_emu = await Process.start(
'xvfb-run', [femuTool, '--image', 'qemu-x64', '-N', '--headless']);
// Wait until the emulator is ready and has a valid device name.
var deviceNameFuture = Completer<String>();
var emuStdout = StringBuffer();
var emuStderr = StringBuffer();
_emu.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(
(String line) {
if (!deviceNameFuture.isCompleted) {
emuStdout.write(line);
emuStdout.write('\n');
var match = emulatorReadyPattern.firstMatch(line);
if (match != null) {
deviceNameFuture.complete(match.group(1));
}
}
}, onDone: () {
if (!deviceNameFuture.isCompleted) {
deviceNameFuture.completeError(
'Fuchsia emulator terminated unexpectedly.\n\n' +
_formatOutputs(emuStdout.toString(), emuStderr.toString()));
}
_stop();
});
_emu.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String line) {
if (!deviceNameFuture.isCompleted) {
emuStderr.write(line);
emuStderr.write('\n');
}
});
_deviceName = await deviceNameFuture.future;
DebugLogger.info('Fuchsia emulator ready: $_deviceName');
// Start the server.
DebugLogger.info('Starting Fuchsia package server');
_server = await Process.start(fserveTool, [
'--bucket',
'fuchsia-sdk',
'--image',
'qemu-x64',
'--device-name',
_deviceName
]);
// Wait until the server is ready to serve packages.
var serverReadyFuture = Completer<String>();
var serverStdout = StringBuffer();
var serverStderr = StringBuffer();
_server.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String line) {
if (!serverReadyFuture.isCompleted) {
serverStdout.write(line);
serverStdout.write('\n');
if (line.contains(serverReadyPattern)) {
serverReadyFuture.complete();
}
}
}, onDone: () {
if (!serverReadyFuture.isCompleted) {
serverReadyFuture.completeError(
'Fuchsia package server terminated unexpectedly.\n\n' +
_formatOutputs(
serverStdout.toString(), serverStderr.toString()));
}
_stop();
});
_server.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String line) {
if (!serverReadyFuture.isCompleted) {
serverStderr.write(line);
serverStderr.write('\n');
}
});
await serverReadyFuture.future;
DebugLogger.info('Fuchsia package server ready');
}
List<String> _getSshArgs(String mode, Iterable<String> args) {
var sshArgs = [
'--device-name',
_deviceName,
'run',
'fuchsia-pkg://fuchsia.com/dart_test_$mode#meta/dart.cmx'
];
return sshArgs..addAll(args);
}
Future<void> _publishPackage(String buildDir, String mode) async {
var packageFile = '$buildDir/gen/dart_test_$mode/dart_test_$mode.far';
if (!File(packageFile).existsSync()) {
throw 'File $packageFile does not exist. Please build fuchsia_test_package.';
}
DebugLogger.info('Publishing package: $packageFile');
var result = await Process.run(fpubTool, [packageFile]);
if (result.exitCode != 0) {
_stop();
throw _formatFailedResult('Publishing package', result);
}
// Verify that the publication was successful by running hello_test.dart.
// This also forces the emulator to download the published package from the
// server, rather than waiting until the first tests are run. It can take a
// minute or two to transfer, and we don't want to eat into the timeout
// timer of the first tests.
DebugLogger.info('Verifying publication');
result = await Process.run(fsshTool,
_getSshArgs(mode, ['/pkg/data/pkg/testing/test/hello_test.dart']));
if (result.exitCode != 0 || result.stdout != 'Hello, World!\n') {
_stop();
throw _formatFailedResult('Verifying publication', result);
}
DebugLogger.info('Publication successful');
}
void _stop() {
if (_emu != null) {
DebugLogger.info('Stopping Fuchsia emulator');
_emu.kill(ProcessSignal.sigint);
_emu = null;
// Killing femu.sh seems to leave the underlying emulator running. So
// manually find the process and terminate it by PID.
var result = Process.runSync('ps', []);
var emuPid = int.tryParse(
emulatorPidPattern.firstMatch(result.stdout as String)?.group(1) ??
"");
if (result.exitCode != 0 || emuPid == null) {
DebugLogger.info(
_formatFailedResult('Searching for emulator process', result));
} else {
Process.killPid(emuPid);
DebugLogger.info('Fuchsia emulator stopped');
}
}
if (_server != null) {
DebugLogger.info('Stopping Fuchsia package server');
_server.kill();
_server = null;
// fserve.sh starts a package manager process in the background. We need
// to manually kill this process, using fserve.sh again.
var result = Process.runSync(fserveTool, ['--kill']);
if (result.exitCode != 0) {
DebugLogger.info(
_formatFailedResult('Killing package manager', result));
} else {
DebugLogger.info('Fuchsia package server stopped');
}
}
}
String _formatOutputs(String stdout, String stderr) {
var output = "";
if (stdout.isNotEmpty) output += "=== STDOUT ===\n$stdout\n";
if (stderr.isNotEmpty) output += "=== STDERR ===\n$stderr\n";
return output;
}
String _formatFailedResult(String name, ProcessResult result) {
return '$name failed with exit code: ${result.exitCode}\n\n' +
_formatOutputs(result.stdout as String, result.stderr as String);
}
}