blob: 36a14e42a359a9dd768e44cf25c0fb45f16e7750 [file] [log] [blame]
// 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.
//! Helpers for launching components.
use {
crate::logs::{create_log_stream, create_std_combined_log_stream, LoggerError, LoggerStream},
anyhow::Error,
fidl_fuchsia_process as fproc,
fuchsia_component::client::connect_to_protocol,
fuchsia_runtime as runtime, fuchsia_zircon as zx,
namespace::Namespace,
runtime::{HandleInfo, HandleType},
thiserror::Error,
zx::{AsHandleRef, HandleBased, Process, Rights, Task},
};
/// Error encountered while launching a component.
#[derive(Debug, Error)]
pub enum LaunchError {
#[error("{:?}", _0)]
Logger(#[from] LoggerError),
#[error("Error connecting to launcher: {:?}", _0)]
Launcher(Error),
#[error("{:?}", _0)]
LoadInfo(runner::component::LaunchError),
#[error("Error launching process: {:?}", _0)]
LaunchCall(fidl::Error),
#[error("Error launching process: {:?}", _0)]
ProcessLaunch(zx::Status),
#[error("Error duplicating vDSO: {:?}", _0)]
DuplicateVdso(zx::Status),
#[error("Error launching process: {:?}", _0)]
Fidl(#[from] fidl::Error),
#[error("Error launching process, cannot create socket {:?}", _0)]
CreateSocket(zx::Status),
#[error("Error cloning UTC clock: {:?}", _0)]
UtcClock(zx::Status),
#[error("unexpected error")]
UnExpectedError,
}
/// Arguments to launch_process.
pub struct LaunchProcessArgs<'a> {
/// Relative binary path to /pkg.
pub bin_path: &'a str,
/// Name of the binary to add to process. This will be truncated to
/// `zx::sys::ZX_MAX_NAME_LEN` bytes.
pub process_name: &'a str,
/// Job used launch process, if None, a new child of default_job() is used.
pub job: Option<zx::Job>,
/// Namespace for binary process to be launched.
pub ns: Namespace,
/// Arguments to binary. Binary name is automatically appended as first argument.
pub args: Option<Vec<String>>,
/// Extra names to add to namespace. by default only names from `ns` are added.
pub name_infos: Option<Vec<fproc::NameInfo>>,
/// Process environment variables.
pub environs: Option<Vec<String>>,
/// Extra handle infos to add. Handles for stdout, stderr, and utc_clock are added.
/// The UTC clock handle is cloned from the current process.
pub handle_infos: Option<Vec<fproc::HandleInfo>>,
/// Handle to lib loader protocol client.
pub loader_proxy_chan: Option<zx::Channel>,
/// VMO containing mapping to executable binary.
pub executable_vmo: Option<zx::Vmo>,
/// Options to create process with.
pub options: zx::ProcessOptions,
// The structured config vmo.
pub config_vmo: Option<zx::Vmo>,
}
/// Launches process, assigns a combined logger stream as stdout/stderr to launched process.
pub async fn launch_process(
args: LaunchProcessArgs<'_>,
) -> Result<(Process, ScopedJob, LoggerStream), LaunchError> {
let launcher = connect_to_protocol::<fproc::LauncherMarker>().map_err(LaunchError::Launcher)?;
let (logger, stdout_handle, stderr_handle) =
create_std_combined_log_stream().map_err(LaunchError::Logger)?;
let (process, job) = launch_process_impl(args, launcher, stdout_handle, stderr_handle).await?;
Ok((process, job, logger))
}
/// Launches process, assigns two separate stdout and stderr streams to launched process.
/// Returns (process, job, stdout_logger, stderr_logger)
pub async fn launch_process_with_separate_std_handles(
args: LaunchProcessArgs<'_>,
) -> Result<(Process, ScopedJob, LoggerStream, LoggerStream), LaunchError> {
let launcher = connect_to_protocol::<fproc::LauncherMarker>().map_err(LaunchError::Launcher)?;
let (stdout_logger, stdout_handle) = create_log_stream().map_err(LaunchError::Logger)?;
let (stderr_logger, stderr_handle) = create_log_stream().map_err(LaunchError::Logger)?;
let (process, job) = launch_process_impl(args, launcher, stdout_handle, stderr_handle).await?;
Ok((process, job, stdout_logger, stderr_logger))
}
async fn launch_process_impl(
args: LaunchProcessArgs<'_>,
launcher: fproc::LauncherProxy,
stdout_handle: zx::Handle,
stderr_handle: zx::Handle,
) -> Result<(Process, ScopedJob), LaunchError> {
const STDOUT: u16 = 1;
const STDERR: u16 = 2;
let mut handle_infos = args.handle_infos.unwrap_or(vec![]);
handle_infos.push(fproc::HandleInfo {
handle: stdout_handle,
id: HandleInfo::new(HandleType::FileDescriptor, STDOUT).as_raw(),
});
handle_infos.push(fproc::HandleInfo {
handle: stderr_handle,
id: HandleInfo::new(HandleType::FileDescriptor, STDERR).as_raw(),
});
handle_infos.push(fproc::HandleInfo {
handle: runtime::duplicate_utc_clock_handle(
Rights::DUPLICATE | Rights::READ | Rights::WAIT | Rights::TRANSFER,
)
.map_err(LaunchError::UtcClock)?
.into_handle(),
id: HandleInfo::new(HandleType::ClockUtc, 0).as_raw(),
});
if let Some(config_vmo) = args.config_vmo {
handle_infos.push(fproc::HandleInfo {
handle: config_vmo.into_handle(),
id: HandleInfo::new(HandleType::ComponentConfigVmo, 0).as_raw(),
});
}
// Load the component
let launch_info =
runner::component::configure_launcher(runner::component::LauncherConfigArgs {
bin_path: args.bin_path,
name: args.process_name,
args: args.args,
options: args.options,
ns: args.ns,
job: args.job,
handle_infos: Some(handle_infos),
name_infos: args.name_infos,
environs: args.environs,
launcher: &launcher,
loader_proxy_chan: args.loader_proxy_chan,
executable_vmo: args.executable_vmo,
})
.await
.map_err(LaunchError::LoadInfo)?;
let component_job = launch_info
.job
.as_handle_ref()
.duplicate(zx::Rights::SAME_RIGHTS)
.expect("handle duplication failed!");
let (status, process) = launcher.launch(launch_info).await.map_err(LaunchError::LaunchCall)?;
let status = zx::Status::from_raw(status);
if status != zx::Status::OK {
return Err(LaunchError::ProcessLaunch(status));
}
let process = process.ok_or_else(|| LaunchError::UnExpectedError)?;
Ok((process, ScopedJob::new(zx::Job::from_handle(component_job))))
}
// Structure to guard job and kill it when going out of scope.
pub struct ScopedJob {
pub object: Option<zx::Job>,
}
impl ScopedJob {
pub fn new(job: zx::Job) -> Self {
Self { object: Some(job) }
}
/// Return the job back from this scoped object
pub fn take(mut self) -> zx::Job {
self.object.take().unwrap()
}
}
impl Drop for ScopedJob {
fn drop(&mut self) {
if let Some(job) = self.object.take() {
job.kill().ok();
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl::endpoints::{create_proxy_and_stream, ClientEnd, Proxy},
fidl_fuchsia_component_runner as fcrunner, fidl_fuchsia_io as fio, fuchsia_async as fasync,
fuchsia_runtime::{job_default, process_self, swap_utc_clock_handle},
fuchsia_zircon as zx,
futures::prelude::*,
};
#[test]
fn scoped_job_works() {
let new_job = job_default().create_child_job().unwrap();
let job_dup = new_job.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
// create new child job, else killing a job has no effect.
let _child_job = new_job.create_child_job().unwrap();
// check that job is alive
let info = job_dup.info().unwrap();
assert!(!info.exited);
{
let _job_about_to_die = ScopedJob::new(new_job);
}
// check that job was killed
let info = job_dup.info().unwrap();
assert!(info.exited);
}
#[test]
fn scoped_job_take_works() {
let new_job = job_default().create_child_job().unwrap();
let raw_handle = new_job.raw_handle();
let scoped = ScopedJob::new(new_job);
let ret_job = scoped.take();
// make sure we got back same job handle.
assert_eq!(ret_job.raw_handle(), raw_handle);
}
#[fasync::run_singlethreaded(test)]
async fn utc_clock_is_cloned() {
let clock =
zx::Clock::create(zx::ClockOpts::MONOTONIC, None).expect("failed to create clock");
let expected_clock_koid =
clock.as_handle_ref().get_koid().expect("failed to get clock koid");
// We are affecting the process-wide clock here, but since Rust test cases are run in their
// own process, this won't interact with other running tests.
let _ = swap_utc_clock_handle(clock).expect("failed to swap clocks");
// We can't fake all the arguments, as there is actual IO happening. Pass in the bare
// minimum that a process needs, and use this test's process handle for real values.
let pkg = fuchsia_fs::directory::open_in_namespace(
"/pkg",
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
)
.expect("failed to open pkg");
let args = LaunchProcessArgs {
bin_path: "bin/test_runners_lib_lib_test", // path to this binary
environs: None,
args: None,
job: None,
process_name: "foo",
name_infos: None,
handle_infos: None,
ns: vec![fcrunner::ComponentNamespaceEntry {
path: Some("/pkg".into()),
directory: Some(ClientEnd::new(pkg.into_channel().unwrap().into_zx_channel())),
..Default::default()
}]
.try_into()
.unwrap(),
loader_proxy_chan: None,
executable_vmo: None,
options: zx::ProcessOptions::empty(),
config_vmo: None,
};
let (mock_proxy, mut mock_stream) = create_proxy_and_stream::<fproc::LauncherMarker>()
.expect("failed to create mock handles");
let mock_fut = async move {
let mut all_handles = vec![];
while let Some(request) =
mock_stream.try_next().await.expect("failed to get next message")
{
match request {
fproc::LauncherRequest::AddHandles { handles, .. } => {
all_handles.extend(handles);
}
fproc::LauncherRequest::Launch { responder, .. } => {
responder
.send(
zx::Status::OK.into_raw(),
Some(
process_self()
.duplicate(Rights::SAME_RIGHTS)
.expect("failed to duplicate process handle"),
),
)
.expect("failed to send reply");
}
_ => {}
}
}
return all_handles;
};
let (_logger, stdout_handle, stderr_handle) =
create_std_combined_log_stream().map_err(LaunchError::Logger).unwrap();
let client_fut = async move {
let _ = launch_process_impl(args, mock_proxy, stdout_handle, stderr_handle)
.await
.expect("failed to launch process");
};
let (all_handles, ()) = futures::future::join(mock_fut, client_fut).await;
let clock_id = HandleInfo::new(HandleType::ClockUtc, 0).as_raw();
let utc_clock_handle = all_handles
.into_iter()
.find_map(
|hi: fproc::HandleInfo| if hi.id == clock_id { Some(hi.handle) } else { None },
)
.expect("UTC clock handle");
let clock_koid = utc_clock_handle.get_koid().expect("failed to get koid");
assert_eq!(expected_clock_koid, clock_koid);
}
}