blob: 2fcd044e37be505148b28d63546968b0c7e4ed37 [file] [log] [blame] [edit]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::args::{KillCommand, StartCommand};
use crate::portpicker::{pick_unused_port, Port};
use crate::types::{read_env_path, HostTools, ImageFiles, SSHKeys, VDLArgs};
use ansi_term::Colour::*;
use anyhow::{format_err, Result};
use std::env;
use std::fs::{copy, File};
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::str;
use tempfile::{Builder, TempDir};
pub struct VDLFiles {
image_files: ImageFiles,
host_tools: HostTools,
ssh_files: SSHKeys,
/// A temp directory to stage required files used to start emulator.
staging_dir: TempDir,
/// A file created under staging_dir that stores vdl proto output.
output_proto: PathBuf,
/// A file created under staging_dir to store emulator output.
emulator_log: PathBuf,
}
impl VDLFiles {
pub fn new(is_sdk: bool) -> Result<VDLFiles> {
let staging_dir = Builder::new().prefix("vdl_staging_").tempdir()?;
let staging_dir_path = staging_dir.path().to_owned();
if is_sdk {
let mut vdl_files = VDLFiles {
image_files: ImageFiles::from_sdk_env()?,
host_tools: HostTools::from_sdk_env()?,
ssh_files: SSHKeys::from_sdk_env()?,
output_proto: staging_dir_path.join("vdl_proto"),
emulator_log: staging_dir_path.join("emu_log"),
staging_dir: staging_dir,
};
vdl_files.image_files.stage_files(&staging_dir_path)?;
vdl_files.ssh_files.stage_files(&staging_dir_path)?;
Ok(vdl_files)
} else {
let mut vdl_files = VDLFiles {
image_files: ImageFiles::from_tree_env()?,
host_tools: HostTools::from_tree_env()?,
ssh_files: SSHKeys::from_tree_env()?,
output_proto: staging_dir_path.join("vdl_proto"),
emulator_log: staging_dir_path.join("emu_log"),
staging_dir: staging_dir,
};
vdl_files.image_files.stage_files(&staging_dir_path)?;
vdl_files.ssh_files.stage_files(&staging_dir_path)?;
Ok(vdl_files)
}
}
fn provision_zbi(&self) -> Result<PathBuf> {
let zbi_out = self.staging_dir.path().join("femu_zircona-ed25519");
let status = Command::new(&self.host_tools.zbi)
.arg(format!(
"--compressed={}",
env::var("FUCHSIA_ZBI_COMPRESSION").unwrap_or("zstd".to_string())
))
.arg("-o")
.arg(&zbi_out)
.arg(&self.image_files.zbi)
.arg("--entry")
.arg(format!("data/ssh/authorized_keys={}", self.ssh_files.auth_key.display()))
.status()?;
if status.success() {
return Ok(zbi_out);
} else {
return Err(format_err!(
"Cannot provision zbi. Exit status was {}",
status.code().unwrap_or_default()
));
}
}
fn assemble_system_images(&self, gcs_build_id: &String) -> Result<String> {
if gcs_build_id.is_empty() {
return Ok(format!(
"{},{},{},{},{},{},{}",
self.ssh_files.private_key.display(),
self.ssh_files.auth_key.display(),
self.provision_zbi()?.display(),
self.image_files.kernel.display(),
self.image_files.fvm.display(),
self.image_files.build_args.display(),
self.image_files.amber_files.display(),
));
} else {
return Ok(format!(
"{},{}",
self.ssh_files.private_key.display(),
self.ssh_files.auth_key.display(),
));
}
}
fn generate_fvd(&self, window_width: &usize, window_height: &usize) -> Result<PathBuf> {
let data = format!(
"device_spec {{
horizontal_resolution: {}
vertical_resolution: {}
vm_heap: 192
ram: 4096
cache: 32
screen_density: 240
}}
",
window_width, window_height
);
let fvd_proto = self.staging_dir.path().join("virtual_device.textproto");
File::create(&fvd_proto)?.write_all(data.as_bytes())?;
Ok(fvd_proto)
}
/// Launches FEMU, opens an SSH session, and waits for the FEMU instance or SSH session to exit.
pub fn start_emulator(&self, start_command: &StartCommand) -> Result<()> {
let vdl_args: VDLArgs = start_command.clone().into();
let fvd = match &start_command.device_proto {
Some(proto) => PathBuf::from(proto),
None => self.generate_fvd(&start_command.window_width, &start_command.window_height)?,
};
// Notes for usage in SDK (ex: `fvdl --sdk ...`)
// If `--aemu-path` is specified, use that.
// Else If `--aemu-version` is specified, download aemu with that version label from cipd.
// Else If `<sdk_root>/bin/aemu.version` is present, download aemu using that version from cipd.
// Else download aemu using version `integration` from cipd.
//
// Notes for usage in-tree (ex: `fx vdl start ...`)
// If `--aemu-path` is specified, use that.
// Else If `--aemu-version` is specified, download aemu with that version label from cipd.
// Else If env_var ${PREBUILT_AEMU_DIR} is set (in-tree default), use that.
// Else download aemu using version `integration` from cipd.
//
// Same logic applies to grpcwebproxy and device_launcher
let aemu = match &start_command.aemu_path {
Some(aemu_path) => PathBuf::from(aemu_path),
None => {
let aemu_cipd_version = match &start_command.aemu_version {
Some(version) => version.clone(),
None => {
if self.host_tools.aemu.as_os_str().is_empty() {
self.host_tools
.read_prebuild_version("aemu.version")
.unwrap_or(String::from("integration"))
} else {
String::from("")
}
}
};
if aemu_cipd_version != "" {
self.host_tools
.download_and_extract(
aemu_cipd_version.to_string(),
"third_party/aemu".to_string(),
)?
.join("emulator")
} else {
self.host_tools.aemu.clone()
}
}
};
if !aemu.exists() || !aemu.is_file() {
return Err(format_err!("Invalid 'emulator' binary at path {}", aemu.display()));
}
let grpcwebproxy = match &start_command.grpcwebproxy_path {
Some(grpcwebproxy_path) => PathBuf::from(grpcwebproxy_path),
None => {
let grpcwebproxy_cipd_version = match &start_command.grpcwebproxy_version {
Some(version) => version.clone(),
None => {
if self.host_tools.grpcwebproxy.as_os_str().is_empty() {
self.host_tools
.read_prebuild_version("grpcwebproxy.version")
.unwrap_or(String::from("latest"))
} else {
String::from("")
}
}
};
if vdl_args.enable_grpcwebproxy && grpcwebproxy_cipd_version != "" {
self.host_tools
.download_and_extract(
grpcwebproxy_cipd_version.to_string(),
"third_party/grpcwebproxy".to_string(),
)?
.join("grpcwebproxy")
} else {
self.host_tools.grpcwebproxy.clone()
}
}
};
if vdl_args.enable_grpcwebproxy && (!grpcwebproxy.exists() || !grpcwebproxy.is_file()) {
return Err(format_err!(
"grpcwebproxy binary cannot be found at {}",
grpcwebproxy.display()
));
}
let vdl = match &start_command.vdl_path {
Some(vdl_path) => PathBuf::from(vdl_path),
None => {
let vdl_cipd_version = match &start_command.vdl_version {
Some(version) => version.clone(),
None => {
if self.host_tools.vdl.as_os_str().is_empty() {
self.host_tools
.read_prebuild_version("device_launcher.version")
.unwrap_or(String::from("latest"))
} else {
String::from("")
}
}
};
if vdl_cipd_version != "" {
self.host_tools
.download_and_extract(vdl_cipd_version.to_string(), "vdl".to_string())?
.join("device_launcher")
} else {
self.host_tools.vdl.clone()
}
}
};
if !vdl.exists() || !vdl.is_file() {
return Err(format_err!("device_launcher binary cannot be found at {}", vdl.display()));
}
let emu_log = match &start_command.emulator_log {
Some(log_location) => PathBuf::from(log_location),
None => self.emulator_log.clone(),
};
let ssh_port = pick_unused_port().unwrap();
// Enable emulator grpc server if running on linux
// doc: https://android.googlesource.com/platform/external/qemu/+/refs/heads/emu-master-dev/android/android-grpc/docs
let enable_emu_controller = match env::consts::OS {
"linux" => true,
_ => false,
};
let status = Command::new(&vdl)
.arg("--action=start")
.arg("--emulator_binary_path")
.arg(&aemu)
.arg("--pm_tool")
.arg(&self.host_tools.pm)
.arg("--far_tool")
.arg(&self.host_tools.far)
.arg("--fvm_tool")
.arg(&self.host_tools.fvm)
.arg("--device_finder_tool")
.arg(&self.host_tools.device_finder)
.arg("--zbi_tool")
.arg(&self.host_tools.zbi)
.arg("--grpcwebproxy_tool")
.arg(&grpcwebproxy)
.arg(format!(
"--system_images={}",
self.assemble_system_images(&vdl_args.gcs_build_id)?
))
.arg(format!("--host_port_map=hostfwd=tcp::{}-:22", ssh_port))
.arg("--output_launched_device_proto")
.arg(&self.output_proto)
.arg("--emu_log")
.arg(&emu_log)
.arg("--proto_file_path")
.arg(&fvd)
.arg("--audio=true")
.arg(format!("--resize_fvm={}", vdl_args.image_size))
.arg(format!("--gpu={}", vdl_args.gpu))
.arg(format!("--headless_mode={}", vdl_args.headless))
.arg(format!("--tuntap={}", vdl_args.tuntap))
.arg(format!("--upscript={}", vdl_args.upscript))
.arg(format!("--serve_packages={}", vdl_args.packages_to_serve))
.arg(format!("--pointing_device={}", vdl_args.pointing_device))
.arg(format!("--enable_webrtc={}", vdl_args.enable_grpcwebproxy))
.arg(format!("--grpcwebproxy_port={}", vdl_args.grpcwebproxy_port))
.arg(format!("--gcs_bucket={}", vdl_args.gcs_bucket))
.arg(format!("--build_id={}", vdl_args.gcs_build_id))
.arg(format!("--image_archive={}", vdl_args.gcs_image_archive))
.arg(format!("--enable_emu_controller={}", enable_emu_controller))
.arg(format!("--hidpi_scaling={}", vdl_args.enable_hidpi_scaling))
.status()?;
if !status.success() {
let persistent_emu_log = read_env_path("FUCHSIA_OUT_DIR")
.unwrap_or(env::current_dir()?)
.join("emu_crash.log");
copy(&self.emulator_log, &persistent_emu_log)?;
return Err(format_err!(
"Cannot start Fuchsia Emulator. Exit status is {}\nEmulator log is copied to {}",
status.code().unwrap_or_default(),
persistent_emu_log.display()
));
}
if vdl_args.tuntap {
println!("{}", Yellow.paint("To support fx tools on emulator, please run \"fx set-device step-atom-yard-juicy\""));
} else {
println!(
"{}",
Yellow.paint(format!(
"To support fx tools on emulator, please run \"fx set-device 127.0.0.1:{}\"",
ssh_port
))
);
}
self.ssh_and_wait(vdl_args.tuntap, ssh_port)?;
self.stop_vdl(&KillCommand {
launched_proto: Some(self.output_proto.display().to_string()),
vdl_path: Some(vdl.clone().to_str().unwrap().to_string()),
})?;
Ok(())
}
/// SSH into the emulator and wait for exit signal.
fn ssh_and_wait(&self, tuntap: bool, ssh_port: Port) -> Result<()> {
if tuntap {
let device_addr = Command::new(&self.host_tools.device_finder)
.args(&["resolve", "-ipv4=false", "step-atom-yard-juicy"])
.output()?;
Command::new("ssh")
.args(&[
"-o",
"StrictHostKeyChecking=no",
"-o",
"CheckHostIP=no",
"-o",
"UserKnownHostsFile=/dev/null",
"-i",
&self.ssh_files.private_key.to_str().unwrap(),
str::from_utf8(&device_addr.stdout).unwrap().trim_end_matches('\n'),
])
.spawn()?
.wait_with_output()?;
} else {
Command::new("ssh")
.args(&[
"-o",
"StrictHostKeyChecking=no",
"-o",
"CheckHostIP=no",
"-o",
"UserKnownHostsFile=/dev/null",
"-i",
&self.ssh_files.private_key.to_str().unwrap(),
"fuchsia@localhost",
"-p",
&ssh_port.to_string(),
])
.spawn()?
.wait_with_output()?;
}
Ok(())
}
pub fn stop_vdl(&self, kill_command: &KillCommand) -> Result<()> {
let vdl_path = match &kill_command.vdl_path {
None => String::from(
read_env_path("PREBUILT_VDL_DIR")?.join("device_launcher").to_str().unwrap(),
),
Some(vdl_path) => vdl_path.to_string(),
};
match &kill_command.launched_proto {
None => {
Command::new(vdl_path)
.arg("--action=kill")
.arg("--launched_virtual_device_proto")
.arg(&self.output_proto)
.status()?;
}
Some(proto_location) => {
Command::new(vdl_path)
.arg("--action=kill")
.arg("--launched_virtual_device_proto")
.arg(proto_location)
.status()?;
}
}
Ok(())
}
}