| // 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::device::DeviceSpec; |
| use crate::portpicker::{is_free_tcp_port, pick_unused_port, Port}; |
| use crate::tools::HostTools; |
| use crate::types::{get_sdk_data_dir, read_env_path, ImageFiles, InTreePaths, SSHKeys, VDLArgs}; |
| use crate::vdl_proto_parser::get_emu_pid; |
| |
| use ansi_term::Colour::*; |
| use anyhow::Result; |
| use errors::ffx_bail; |
| use regex::Regex; |
| use shared_child::SharedChild; |
| use signal_hook; |
| use std::env; |
| use std::fs::{copy, File}; |
| use std::io::Write; |
| use std::path::PathBuf; |
| use std::process::{Command, Output}; |
| use std::str; |
| use std::sync::atomic::{AtomicBool, Ordering}; |
| use std::sync::Arc; |
| use std::thread; |
| use std::time; |
| use tempfile::{Builder, TempDir}; |
| |
| static ANALYTICS_ENV_VAR: &str = "FVDL_INVOKER"; |
| static DEFAULT_SSH_PORT: u16 = 8022; |
| |
| /// Monitors a shared process for the interrupt signal. Only used for --monitor or --emu-only modes. |
| /// |
| /// If user runs with --montior or --emu-only, Fuchsia Emulator will be running in the foreground, |
| /// here we listen for the interrupt signal (ctrl+c), once detected, we'll wait for the emulator |
| /// process to finish. |
| fn monitored_child_process(child_arc: &Arc<SharedChild>) -> Result<()> { |
| let child_arc_clone = child_arc.clone(); |
| let term = Arc::new(AtomicBool::new(false)); |
| signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&term))?; |
| let thread = std::thread::spawn(move || { |
| while !term.load(Ordering::Relaxed) { |
| thread::sleep(time::Duration::from_secs(1)); |
| } |
| child_arc_clone.wait().unwrap() |
| }); |
| thread.join().expect("cannot join monitor thread"); |
| Ok(()) |
| } |
| 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, |
| |
| /// True if user invoked fvdl with the arg --sdk. |
| is_sdk: bool, |
| |
| /// True to enable extra logging. |
| verbose: bool, |
| } |
| |
| /// Note that a new TempDir will be created when we clone VDLFiles. |
| impl Clone for VDLFiles { |
| fn clone(&self) -> Self { |
| Self { |
| image_files: self.image_files.clone(), |
| host_tools: self.host_tools.clone(), |
| ssh_files: self.ssh_files.clone(), |
| output_proto: self.output_proto.clone(), |
| emulator_log: self.emulator_log.clone(), |
| staging_dir: TempDir::new().unwrap(), |
| is_sdk: self.is_sdk, |
| verbose: self.verbose, |
| } |
| } |
| } |
| |
| impl VDLFiles { |
| pub fn new(is_sdk: bool, verbose: bool) -> Result<VDLFiles> { |
| let staging_dir = Builder::new().prefix("vdl_staging_").tempdir()?; |
| let staging_dir_path = staging_dir.path().to_owned(); |
| let vdl_files; |
| if is_sdk { |
| 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, |
| is_sdk: is_sdk, |
| verbose: verbose, |
| }; |
| } else { |
| let mut in_tree = InTreePaths { root_dir: None, build_dir: None }; |
| vdl_files = VDLFiles { |
| image_files: ImageFiles::from_tree_env(&mut in_tree)?, |
| host_tools: HostTools::from_tree_env(&mut in_tree)?, |
| ssh_files: SSHKeys::from_tree_env(&mut in_tree)?, |
| output_proto: staging_dir_path.join("vdl_proto"), |
| emulator_log: staging_dir_path.join("emu_log"), |
| staging_dir: staging_dir, |
| is_sdk: is_sdk, |
| verbose: verbose, |
| }; |
| } |
| if verbose { |
| println!("{:#?}", vdl_files.image_files); |
| println!("{:#?}", vdl_files.ssh_files); |
| println!("{:#?}", vdl_files.host_tools); |
| } |
| 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.authorized_keys.display())) |
| .status()?; |
| if status.success() { |
| if self.verbose { |
| println!("[fvdl] provisioned zbi {:?}", zbi_out); |
| } |
| Ok(zbi_out) |
| } else { |
| ffx_bail!("Cannot provision zbi. Exit status was {}", status.code().unwrap_or_default()) |
| } |
| } |
| |
| fn assemble_system_images(&self, sdk_version: &String) -> Result<String> { |
| if sdk_version.is_empty() { |
| // When SDK version is not specified, then all required files already exists locally (i.e in-tree) |
| Ok(format!( |
| "{},{},{},{},{},{},{}", |
| self.ssh_files.private_key.display(), |
| self.ssh_files.authorized_keys.display(), |
| self.provision_zbi()?.display(), |
| self.image_files.kernel.display(), |
| self.image_files.amber_files.as_ref().unwrap_or(&PathBuf::new()).display(), |
| self.image_files.build_args.as_ref().unwrap_or(&PathBuf::new()).display(), |
| self.image_files.fvm.as_ref().unwrap_or(&PathBuf::new()).display(), |
| )) |
| } else { |
| // Not specifying any image files will allow device_launcher to download from GCS. |
| Ok(format!( |
| "{},{}", |
| self.ssh_files.private_key.display(), |
| self.ssh_files.authorized_keys.display(), |
| )) |
| } |
| } |
| |
| fn generate_fvd(&self, device: &DeviceSpec) -> Result<PathBuf> { |
| let data = format!( |
| "device_spec {{ |
| horizontal_resolution: {} |
| vertical_resolution: {} |
| vm_heap: 192 |
| ram: {} |
| cache: 32 |
| screen_density: 240 |
| }} |
| ", |
| &device.window_width, &device.window_height, &device.ram_mb |
| ); |
| let fvd_proto = self.staging_dir.path().join("virtual_device.textproto"); |
| File::create(&fvd_proto)?.write_all(data.as_bytes())?; |
| Ok(fvd_proto) |
| } |
| |
| // 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. |
| pub fn resolve_aemu_path(&self, start_command: &StartCommand) -> Result<PathBuf> { |
| match &start_command.aemu_path { |
| Some(aemu_path) => Ok(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 { |
| return Ok(self.host_tools.aemu.clone()); |
| } |
| } |
| }; |
| Ok(self |
| .host_tools |
| .download_and_extract( |
| aemu_cipd_version.to_string(), |
| "third_party/android/aemu/release".to_string(), |
| )? |
| .join("emulator")) |
| } |
| } |
| } |
| |
| pub fn resolve_grpcwebproxy_path(&self, start_command: &StartCommand) -> Result<PathBuf> { |
| match &start_command.grpcwebproxy_path { |
| Some(grpcwebproxy_path) => Ok(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 { |
| return Ok(self.host_tools.grpcwebproxy.clone()); |
| } |
| } |
| }; |
| Ok(self |
| .host_tools |
| .download_and_extract( |
| grpcwebproxy_cipd_version.to_string(), |
| "third_party/grpcwebproxy".to_string(), |
| )? |
| .join("grpcwebproxy")) |
| } |
| } |
| } |
| |
| pub fn resolve_vdl_path(&self, start_command: &StartCommand) -> Result<PathBuf> { |
| match &start_command.vdl_path { |
| Some(vdl_path) => Ok(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 { |
| return Ok(self.host_tools.vdl.clone()); |
| } |
| } |
| }; |
| Ok(self |
| .host_tools |
| .download_and_extract(vdl_cipd_version.to_string(), "vdl".to_string())? |
| .join("device_launcher")) |
| } |
| } |
| } |
| |
| // Checks if user has specified a portmap. If portmap is specified, we'll check if ssh port is included. |
| // If ssh port is not included, we'll pick a port and forward that together with the rest of portmap. |
| pub fn resolve_portmap(&self, start_command: &StartCommand) -> (String, u16) { |
| let ssh_port = match is_free_tcp_port(DEFAULT_SSH_PORT) { |
| Some(port) => port, |
| None => pick_unused_port().unwrap(), |
| }; |
| match &start_command.port_map { |
| Some(port_map) => { |
| let mut mapped_port = 0; |
| let re = Regex::new(r":+(?P<ssh>\d+)-?:22(,|$)").unwrap(); |
| re.captures(port_map).and_then(|cap| { |
| cap.name("ssh").map(|ssh| mapped_port = ssh.as_str().parse::<u16>().unwrap()) |
| }); |
| if mapped_port == 0 { |
| (format!("{},hostfwd=tcp::{}-:22", port_map.clone(), ssh_port), ssh_port) |
| } else { |
| (port_map.clone(), mapped_port) |
| } |
| } |
| None => (format!("hostfwd=tcp::{}-:22", ssh_port), ssh_port), |
| } |
| } |
| |
| // Sets the label used in analytics. If running in automated testing environment, user can create |
| // a custom label to track usage by setting the environment variable FVDL_INVOKER. |
| pub fn resolve_invoker(&self) -> String { |
| match env::var_os(ANALYTICS_ENV_VAR) { |
| Some(v) => return String::from(v.to_str().unwrap_or_default()), |
| None => { |
| if self.is_sdk { |
| return String::from("fvdl-sdk"); |
| } |
| return String::from("fvdl-intree"); |
| } |
| } |
| } |
| |
| pub fn check_start_command(&self, command: &StartCommand) -> Result<()> { |
| // TODO(fxb/82804) Remove after a month. |
| if command.nopackageserver { |
| println!( |
| "{}", |
| Yellow |
| .paint("WARNING: --nopackageserver will be removed soon, the default behavior no longer starts package server.") |
| ); |
| } |
| if command.nointeractive && command.vdl_output.is_none() { |
| ffx_bail!( |
| "--vdl-output must be specified for --nointeractive mode.\n\ |
| example: fx vdl start --nointeractive --vdl-output /path/to/saved/output.log\n\ |
| example: ./fvdl --sdk start --nointeractive --vdl-output /path/to/saved/output.log\n" |
| ) |
| } |
| // Check that build architecture is specified when overriding image files |
| if (command.amber_files.is_some() |
| || command.fvm_image.is_some() |
| || command.kernel_image.is_some() |
| || command.zbi_image.is_some()) |
| && command.image_architecture.is_none() |
| { |
| ffx_bail!( |
| "--image-architecture must be specified in order to override image files.\n\ |
| accepted values are 'arm64' and 'x64'. |
| example: fx vdl start --image-architecture x64 --kernel-image /path/to/kernel \n\ |
| example: ./fvdl --sdk start --image-architecture x64 --kernel-image /path/to/kernel \n" |
| ) |
| } |
| // At a minimum, zbi and kernel images must be both specified in order to boot up the emulator. |
| if (command.kernel_image.is_some() && command.zbi_image.is_none()) |
| || (command.kernel_image.is_none() && command.zbi_image.is_some()) |
| { |
| ffx_bail!("--kernel-image and --zbi-image must both be specified in order to override fuchsia image used for emulator.\n\ |
| You can optionally specify --amber-files and --fvm-image locations. \n |
| ") |
| } |
| if command.device_spec.is_some() && command.device_proto.is_some() { |
| ffx_bail!("--device-spec and --device-proto are mutually exclusive options.") |
| } |
| Ok(()) |
| } |
| |
| /// Launches FEMU, opens an SSH session, and waits for the FEMU instance or SSH session to exit. |
| pub async fn start_emulator(&mut self, start_command: &StartCommand) -> Result<i32> { |
| self.check_start_command(&start_command)?; |
| #[allow(clippy::clone_double_ref)] // TODO(fxbug.dev/95079) |
| let vdl_args: VDLArgs = start_command.clone().into(); |
| |
| let mut gcs_image = vdl_args.gcs_image_archive; |
| let mut gcs_bucket = vdl_args.gcs_bucket; |
| let mut sdk_version = vdl_args.sdk_version; |
| |
| if vdl_args.cache_root.to_str().unwrap_or("") != "" { |
| println!("[fvdl] using cached image files"); |
| self.image_files.update_paths_from_cache(&vdl_args.cache_root); |
| } |
| |
| // overriding image files via args will make cache a no-op |
| self.image_files.update_paths_from_args(&start_command); |
| self.ssh_files.update_paths_from_args(&start_command); |
| |
| // If minimum required image files are specified & exist, skip download by clearing out gcs related flags even |
| // if user has specified --sdk-version etc. |
| if self.image_files.images_exist() { |
| gcs_image = String::from(""); |
| gcs_bucket = String::from(""); |
| sdk_version = String::from(""); |
| self.image_files.stage_files(&self.staging_dir.path().to_owned())?; |
| } |
| self.ssh_files.stage_files(&self.staging_dir.path().to_owned())?; |
| |
| if self.verbose { |
| println!("[fvdl] using the following image files to launch emulator:"); |
| println!("{:?}", self.image_files); |
| } |
| |
| if !self.is_sdk { |
| self.image_files.check()?; |
| self.ssh_files.check()?; |
| } |
| |
| let device_spec = DeviceSpec::from_manifest(&start_command)?; |
| |
| let fvd = match &start_command.device_proto { |
| Some(proto) => PathBuf::from(proto), |
| None => self.generate_fvd(&device_spec)?, |
| }; |
| |
| let aemu = self.resolve_aemu_path(start_command)?; |
| if !aemu.exists() || !aemu.is_file() { |
| ffx_bail!("Invalid 'emulator' binary at path {}", aemu.display()) |
| } |
| |
| let vdl = self.resolve_vdl_path(start_command)?; |
| if !vdl.exists() || !vdl.is_file() { |
| ffx_bail!("device_launcher binary cannot be found at {}", vdl.display()) |
| } |
| |
| let mut grpcwebproxy = self.host_tools.grpcwebproxy.clone(); |
| if vdl_args.enable_grpcwebproxy { |
| grpcwebproxy = self.resolve_grpcwebproxy_path(start_command)?; |
| if !grpcwebproxy.exists() || !grpcwebproxy.is_file() { |
| ffx_bail!("grpcwebproxy binary cannot be found at {}", grpcwebproxy.display()) |
| } |
| } |
| |
| let emu_log = start_command |
| .emulator_log |
| .as_ref() |
| .map_or(self.emulator_log.clone(), |v| PathBuf::from(v)); |
| |
| let package_server_log = |
| start_command.package_server_log.as_ref().map_or(PathBuf::new(), |v| PathBuf::from(v)); |
| |
| if let Some(location) = &start_command.vdl_output { |
| self.output_proto = PathBuf::from(location); |
| } |
| |
| let (port_map, ssh_port) = self.resolve_portmap(&start_command); |
| |
| // 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 invoker = self.resolve_invoker(); |
| let mut cmd = Command::new(&vdl); |
| cmd.arg("--action=start") |
| .arg("--emulator_binary_path") |
| .arg(&aemu) |
| .arg("--pm_tool") |
| .arg(&self.host_tools.pm.as_ref().unwrap_or(&PathBuf::new())) |
| .arg("--far_tool") |
| .arg(&self.host_tools.far.as_ref().unwrap_or(&PathBuf::new())) |
| .arg("--fvm_tool") |
| .arg(&self.host_tools.fvm.as_ref().unwrap_or(&PathBuf::new())) |
| .arg("--ffx_tool") |
| .arg(&self.host_tools.ffx.as_ref().unwrap_or(&PathBuf::new())) |
| .arg("--zbi_tool") |
| .arg(&self.host_tools.zbi) |
| .arg("--grpcwebproxy_tool") |
| .arg(&grpcwebproxy) |
| .arg(format!("--system_images={}", self.assemble_system_images(&sdk_version)?)) |
| .arg("--host_port_map") |
| .arg(&port_map) |
| .arg("--output_launched_device_proto") |
| .arg(&self.output_proto) |
| .arg("--emu_log") |
| .arg(&emu_log) |
| .arg("--package_server_log") |
| .arg(&package_server_log) |
| .arg("--proto_file_path") |
| .arg(&fvd) |
| .arg(format!("--audio={}", &device_spec.audio)) |
| .arg(format!("--event_action={}", &invoker)) |
| .arg(format!("--debugger={}", &start_command.debugger)) |
| .arg(format!("--monitor={}", &start_command.monitor)) |
| .arg(format!("--emu_only={}", &start_command.emu_only)) |
| .arg(format!("--resize_fvm={}", device_spec.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!("--start_package_server={}", vdl_args.start_package_server)) |
| .arg(format!("--serve_packages={}", vdl_args.packages_to_serve)) |
| .arg(format!("--package_server_port={}", vdl_args.package_server_port)) |
| .arg(format!("--unpack_repo_root={}", vdl_args.amber_unpack_root)) |
| .arg(format!("--pointing_device={}", device_spec.pointing_device)) |
| .arg(format!("--enable_webrtc={}", vdl_args.enable_grpcwebproxy)) |
| .arg(format!("--grpcwebproxy_port={}", vdl_args.grpcwebproxy_port)) |
| .arg(format!("--gcs_bucket={}", gcs_bucket)) |
| .arg(format!("--image_archive={}", gcs_image)) |
| .arg(format!("--build_id={}", sdk_version)) |
| .arg(format!("--enable_emu_controller={}", enable_emu_controller)) |
| .arg(format!("--hidpi_scaling={}", vdl_args.enable_hidpi_scaling)) |
| .arg(format!("--image_cache_path={}", vdl_args.cache_root.display())) |
| .arg(format!("--kernel_args={}", vdl_args.extra_kerel_args)) |
| .arg(format!("--accel={}", vdl_args.acceleration)) |
| .arg(format!("--image_architecture={}", vdl_args.image_architecture)) |
| .arg(format!("--isolated_ffx_config_path={}", vdl_args.isolated_ffx_config_path)); |
| |
| for i in 0..start_command.envs.len() { |
| cmd.arg("--env").arg(&start_command.envs[i]); |
| } |
| if start_command.dry_run { |
| cmd.arg("--dry-run"); |
| } |
| if let Some(core_count) = start_command.cpu_count { |
| cmd.arg(format!("--cpu_count={}", core_count)); |
| } |
| if self.verbose || start_command.dry_run { |
| println!("[fvdl] Running device_launcher cmd: {:?}", cmd); |
| } |
| |
| let shared_process = SharedChild::spawn(&mut cmd)?; |
| let child_arc = Arc::new(shared_process); |
| if start_command.emu_only || start_command.monitor { |
| // When running with '--emu-only' or '--monitor' mode, the user is directly interacting |
| // with the emulator console, the execution ends when either QEMU or AEMU terminates. |
| match fuchsia_async::unblock(move || monitored_child_process(&child_arc)).await { |
| Ok(_) => { |
| self.stop_vdl(&KillCommand { |
| launched_proto: Some(self.output_proto.display().to_string()), |
| vdl_path: Some(vdl.display().to_string()), |
| sdk: self.is_sdk, |
| }) |
| .await?; |
| return Ok(0); |
| } |
| Err(e) => { |
| self.stop_vdl(&KillCommand { |
| launched_proto: Some(self.output_proto.display().to_string()), |
| vdl_path: Some(vdl.display().to_string()), |
| sdk: self.is_sdk, |
| }) |
| .await?; |
| ffx_bail!("emulator launcher did not terminate properly, error: {}", e) |
| } |
| } |
| } |
| |
| let status = child_arc.wait()?; |
| if !status.success() { |
| if self.emulator_log.exists() { |
| 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)?; |
| println!("Emulator log is copied to {}", persistent_emu_log.display()); |
| } |
| // device_launcher will return exit code: |
| // Launcher = 1 <- default |
| // AEMUCrash = 2 |
| // UserActionRequired = 3 |
| // SSHConnection = 4 |
| // DryRunMode = 5 |
| // |
| // We only bail! if device_launcher failed with a launcher related error. |
| let exit_code = status.code().unwrap_or_default(); |
| if exit_code == 1 { |
| ffx_bail!("Cannot start Fuchsia Emulator.") |
| } |
| return Ok(exit_code); |
| } |
| |
| if !self.is_sdk { |
| let command; |
| if vdl_args.tuntap { |
| command = String::from("fx set-device fuchsia-5254-0063-5e7a"); |
| } else { |
| command = format!("fx set-device 127.0.0.1:{}", ssh_port); |
| } |
| println!( |
| "{}", |
| Yellow |
| .paint(format!("To support fx tools on emulator, please run \"{}\"", command)) |
| ); |
| } |
| if start_command.nointeractive { |
| println!( |
| "{}", |
| Yellow.paint( |
| "\nNOTE: For --nointeractive, launcher artifacts need to be manually cleaned using the `kill` subcommand:")); |
| if !self.is_sdk { |
| println!( |
| "{}", |
| Yellow.paint(format!( |
| " fvdl kill --launched-proto {}", |
| self.output_proto.display() |
| )) |
| ); |
| } else { |
| println!( |
| "{}", |
| Yellow.paint(format!( |
| " fvdl --sdk kill --launched-proto {}", |
| self.output_proto.display() |
| )) |
| ); |
| } |
| } else { |
| // TODO(fxbug.dev/72190) Ensure we have a way for user to interact with emulator |
| // once SSH support goes away. |
| let pid = get_emu_pid(&self.output_proto).unwrap(); |
| let self_clone = self.clone(); |
| let is_tuntap = vdl_args.tuntap.clone(); |
| fuchsia_async::unblock(move || { |
| 'keep_ssh: loop { |
| match Command::new("pgrep").arg("qemu").output() { |
| Ok(out) => { |
| // pgrep can no longer find any qemu pid process we think the emulator |
| // has terminated, stop trying to ssh. |
| if out.stdout.is_empty() { |
| break 'keep_ssh; |
| } |
| if !str::from_utf8(&out.stdout) |
| .unwrap() |
| .lines() |
| .any(|p| p == pid.to_string()) |
| { |
| break 'keep_ssh; |
| } |
| let ssh_out = self_clone.ssh_and_wait(is_tuntap, ssh_port).unwrap(); |
| // If SSH process terminated successfully, we think user intend to end |
| // SSH session as well as shutting down emulator, stop trying to ssh. |
| // If SSH process terminated with a non-zero exit status, we think the |
| // user has issued "dm reboot", which reboots fuchsia and disconnects |
| // ssh, but emulator should still be running. |
| if ssh_out.status.success() { |
| break 'keep_ssh; |
| } |
| } |
| Err(_) => break 'keep_ssh, |
| } |
| } |
| }) |
| .await; |
| self.stop_vdl(&KillCommand { |
| launched_proto: Some(self.output_proto.display().to_string()), |
| vdl_path: Some(vdl.display().to_string()), |
| sdk: self.is_sdk, |
| }) |
| .await?; |
| } |
| Ok(0) |
| } |
| |
| /// SSH into the emulator and wait for exit signal. |
| fn ssh_and_wait(&self, tuntap: bool, ssh_port: Port) -> Result<Output, std::io::Error> { |
| // Ref to SSH flags: http://man.openbsd.org/ssh_config |
| let mut ssh_options = vec![ |
| "-o", |
| "StrictHostKeyChecking=no", |
| "-o", |
| "CheckHostIP=no", |
| "-o", |
| "UserKnownHostsFile=/dev/null", |
| "-o", |
| "ConnectTimeout=10", |
| "-o", |
| "ServerAliveInterval=1", |
| "-o", |
| "ServerAliveCountMax=5", |
| "-o", |
| "LogLevel=ERROR", |
| ]; |
| if tuntap { |
| let device_addr = |
| Command::new(&self.host_tools.ffx.as_ref().unwrap_or(&PathBuf::new())) |
| .args(&["target", "list", "--format", "a", "fuchsia-5254-0063-5e7a"]) |
| .output()?; |
| ssh_options.append(&mut vec![ |
| "-i", |
| &self.ssh_files.private_key.to_str().unwrap(), |
| str::from_utf8(&device_addr.stdout).unwrap().trim_end_matches('\n'), |
| ]); |
| Command::new("ssh").args(&ssh_options).spawn()?.wait_with_output() |
| } else { |
| let port = &ssh_port.to_string(); |
| ssh_options.append(&mut vec![ |
| "-i", |
| &self.ssh_files.private_key.to_str().unwrap(), |
| "fuchsia@localhost", |
| "-p", |
| port, |
| ]); |
| Command::new("ssh").args(&ssh_options).spawn()?.wait_with_output() |
| } |
| } |
| |
| // Shuts down emulator and local services. |
| pub async fn stop_vdl(&self, kill_command: &KillCommand) -> Result<()> { |
| let invoker = self.resolve_invoker(); |
| // If user specified vdl_path in arg, use that. If not, check if environment variable |
| // PREBUILD_VDL_DIR is set, if set use that. If not, check if self.host_tools has found |
| // default path for device_launcher, when running in-tree this will be read from |
| // tool_paths.json, when running in sdk, this will be empty; if found, use that. |
| // If empty (i.e from sdk), look for the tool in sdk_data_dir. |
| let vdl: PathBuf = match &kill_command.vdl_path { |
| Some(vdl_path) => PathBuf::from(vdl_path), |
| None => match read_env_path("PREBUILT_VDL_DIR") { |
| Ok(default_path) => default_path.join("device_launcher"), |
| _ => { |
| if self.host_tools.vdl.as_os_str().is_empty() { |
| let label = self |
| .host_tools |
| .read_prebuild_version("device_launcher.version") |
| .unwrap_or(String::from("latest")); |
| get_sdk_data_dir()? |
| .join("femu") |
| .join(format!("vdl-{}", label.replace(":", "-"))) |
| .join("device_launcher") |
| } else { |
| self.host_tools.vdl.clone() |
| } |
| } |
| }, |
| }; |
| if !vdl.exists() || !vdl.is_file() { |
| ffx_bail!("device_launcher binary cannot be found at {}", vdl.display()) |
| } |
| match &kill_command.launched_proto { |
| None => { |
| ffx_bail!( |
| "--launched-proto must be specified for `kill` subcommand.\n\ |
| example: \"./fvdl --sdk kill --launched-proto /path/to/saved/output.log\"\n" |
| ) |
| } |
| Some(proto_location) => { |
| Command::new(&vdl) |
| .arg("--action=kill") |
| .arg(format!("--launched_virtual_device_proto={}", &proto_location)) |
| .arg(format!("--event_action={}", &invoker)) |
| .status()?; |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use serial_test::serial; |
| |
| pub fn setup() { |
| env::set_var("HOST_OUT_DIR", "/host/out"); |
| env::set_var("FUCHSIA_BUILD_DIR", "/build/out"); |
| } |
| |
| pub fn create_start_command() -> StartCommand { |
| StartCommand { |
| tuntap: true, |
| upscript: Some("/path/to/upscript".to_string()), |
| packages_to_serve: Some("pkg1.far,pkg2.far".to_string()), |
| aemu_path: Some("/path/to/aemu".to_string()), |
| vdl_path: Some("/path/to/device_launcher".to_string()), |
| host_gpu: true, |
| grpcwebproxy_path: Some("/path/to/grpcwebproxy".to_string()), |
| pointing_device: Some("mouse".to_string()), |
| aemu_version: Some("git_revision:da1cc2ee512714a176f08b8b5fec035994ca305d".to_string()), |
| grpcwebproxy_version: Some("git_revision:1".to_string()), |
| sdk_version: Some("0.20201130.3.1".to_string()), |
| image_name: Some("qemu-x64".to_string()), |
| vdl_version: Some("git_revision:2".to_string()), |
| envs: vec!["A=1".to_string(), "B=2".to_string(), "C=3".to_string()], |
| fvm_image: Some("fvm".to_string()), |
| zbi_image: Some("zircona".to_string()), |
| kernel_image: Some("kernel".to_string()), |
| ..Default::default() |
| } |
| } |
| #[test] |
| #[serial] |
| fn test_choosing_prebuild_with_path_specified() -> Result<()> { |
| setup(); |
| let start_command = &create_start_command(); |
| |
| // --sdk |
| let aemu = VDLFiles::new(true, false)?.resolve_aemu_path(start_command)?; |
| assert_eq!(PathBuf::from("/path/to/aemu"), aemu); |
| let vdl = VDLFiles::new(true, false)?.resolve_vdl_path(start_command)?; |
| assert_eq!(PathBuf::from("/path/to/device_launcher"), vdl); |
| let grpcwebproxy = VDLFiles::new(true, false)?.resolve_grpcwebproxy_path(start_command)?; |
| assert_eq!(PathBuf::from("/path/to/grpcwebproxy"), grpcwebproxy); |
| Ok(()) |
| } |
| |
| // TODO(fxb/73555) Mock download instead of downloading from cipd in this test. |
| #[ignore] |
| #[test] |
| #[serial] |
| fn test_choosing_prebuild_with_cipd_label_specified() -> Result<()> { |
| setup(); |
| |
| let tmp_dir = Builder::new().prefix("fvdl_test_cipd_label_").tempdir()?; |
| env::set_var("FEMU_DOWNLOAD_DIR", tmp_dir.path()); |
| |
| let mut start_command = &mut create_start_command(); |
| start_command.vdl_path = None; |
| start_command.vdl_version = Some("g3-revision:vdl_fuchsia_20210113_RC00".to_string()); |
| |
| // --sdk |
| let vdl = VDLFiles::new(true, false)?.resolve_vdl_path(start_command)?; |
| assert_eq!( |
| tmp_dir.path().join("vdl-g3-revision-vdl_fuchsia_20210113_RC00/device_launcher"), |
| vdl |
| ); |
| Ok(()) |
| } |
| |
| // TODO(fxb/73555) Mock download instead of downloading from cipd in this test. |
| #[ignore] |
| #[test] |
| #[serial] |
| fn test_choosing_prebuild_default() -> Result<()> { |
| setup(); |
| |
| let tmp_dir = Builder::new().prefix("fvdl_test_default_").tempdir()?; |
| env::set_var("FEMU_DOWNLOAD_DIR", tmp_dir.path()); |
| |
| let mut start_command = &mut create_start_command(); |
| start_command.aemu_path = None; |
| start_command.aemu_version = None; |
| start_command.vdl_path = None; |
| start_command.vdl_version = None; |
| start_command.grpcwebproxy_path = None; |
| start_command.grpcwebproxy_version = None; |
| |
| // --sdk |
| let vdl = VDLFiles::new(true, false)?.resolve_vdl_path(start_command)?; |
| assert_eq!(tmp_dir.path().join("vdl-latest/device_launcher"), vdl); |
| let aemu = VDLFiles::new(true, false)?.resolve_aemu_path(start_command)?; |
| assert_eq!(tmp_dir.path().join("aemu-integration/emulator"), aemu); |
| let grpcwebproxy = VDLFiles::new(true, false)?.resolve_grpcwebproxy_path(start_command)?; |
| assert_eq!(tmp_dir.path().join("grpcwebproxy-latest/grpcwebproxy"), grpcwebproxy); |
| Ok(()) |
| } |
| |
| #[test] |
| #[serial] |
| fn test_resolve_portmap() -> Result<()> { |
| setup(); |
| |
| let mut start_command = &mut create_start_command(); |
| start_command.port_map = None; |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert!(ssh > 0); |
| let re = Regex::new(r"hostfwd=tcp::\d+-:22").unwrap(); |
| assert!(re.is_match(&port_map)); |
| |
| start_command.port_map = Some("".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert!(ssh > 0); |
| let re = Regex::new(r"hostfwd=tcp::\d+-:22").unwrap(); |
| assert!(re.is_match(&port_map)); |
| |
| start_command.port_map = Some("hostfwd=tcp::123-:222,hostfwd=tcp::80-:223".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert!(ssh > 0); |
| let re = |
| Regex::new(r"hostfwd=tcp::123-:222,hostfwd=tcp::80-:223,hostfwd=tcp::\d+-:22").unwrap(); |
| assert!(re.is_match(&port_map)); |
| |
| start_command.port_map = |
| Some("hostfwd=tcp::123-:223,hostfwd=tcp::80-:322,hostfwd=tcp::456-:22".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert_eq!(456, ssh); |
| assert_eq!("hostfwd=tcp::123-:223,hostfwd=tcp::80-:322,hostfwd=tcp::456-:22", port_map); |
| |
| start_command.port_map = Some("hostfwd=tcp::789-:22".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert_eq!(789, ssh); |
| assert_eq!("hostfwd=tcp::789-:22", port_map); |
| |
| start_command.port_map = |
| Some("hostfwd=tcp::123-:22,hostfwd=tcp::80-:8022,hostfwd=tcp::456-:222".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert_eq!(123, ssh); |
| assert_eq!("hostfwd=tcp::123-:22,hostfwd=tcp::80-:8022,hostfwd=tcp::456-:222", port_map); |
| |
| start_command.port_map = Some("tcp:123:22".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert_eq!(123, ssh); |
| assert_eq!("tcp:123:22", port_map); |
| |
| start_command.port_map = Some("tcp:123:222".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert!(ssh > 0); |
| let re = Regex::new(r"tcp:123:222,hostfwd=tcp::\d+-:22").unwrap(); |
| assert!(re.is_match(&port_map)); |
| |
| start_command.port_map = Some("tcp:234:80,tcp:123:22".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert_eq!(123, ssh); |
| assert_eq!("tcp:234:80,tcp:123:22", port_map); |
| |
| start_command.port_map = Some("tcp:234:22,tcp:123:80".to_string()); |
| let (port_map, ssh) = VDLFiles::new(true, false)?.resolve_portmap(start_command); |
| assert_eq!(234, ssh); |
| assert_eq!("tcp:234:22,tcp:123:80", port_map); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| #[serial] |
| fn test_pid_check() -> Result<()> { |
| let out = vec![51, 49, 51, 49, 54, 51, 55, 10, 51, 49, 51, 50, 50, 57, 51, 10]; |
| let pid_match = 3132293; |
| let pid_no_match = 123; |
| assert!(str::from_utf8(&out).unwrap().lines().any(|p| p == pid_match.to_string())); |
| assert!(!str::from_utf8(&out).unwrap().lines().any(|p| p == pid_no_match.to_string())); |
| Ok(()) |
| } |
| |
| #[test] |
| #[serial] |
| fn test_resolve_analytics_label() -> Result<()> { |
| let mut label = VDLFiles::new(true /* is_sdk */, false /* verbose */)?.resolve_invoker(); |
| assert_eq!(label, "fvdl-sdk"); |
| env::set_var(ANALYTICS_ENV_VAR, "apple-pie"); |
| label = VDLFiles::new(true /* is_sdk */, false /* verbose */)?.resolve_invoker(); |
| assert_eq!(label, "apple-pie"); |
| Ok(()) |
| } |
| } |