blob: ee3577f4f7ebda9b2a52b9fdc24ba0e240f7e02e [file] [log] [blame]
// Copyright 2022 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::socket;
use fidl::{
endpoints::{ClientEnd, Proxy},
HandleBased,
};
use fidl_fuchsia_component as fcomp;
use fidl_fuchsia_dash::LauncherError;
use fidl_fuchsia_hardware_pty as pty;
use fidl_fuchsia_io as fio;
use fidl_fuchsia_process::{HandleInfo, LaunchInfo, LauncherMarker, NameInfo};
use fidl_fuchsia_sys2 as fsys;
use fuchsia_async as fasync;
use fuchsia_component::client::connect_to_protocol;
use fuchsia_component::server::ServiceFs;
use fuchsia_runtime::{HandleInfo as HandleId, HandleType};
use fuchsia_zircon as zx;
use futures::prelude::*;
use moniker::{RelativeMoniker, RelativeMonikerBase};
use std::sync::Arc;
use tracing::*;
pub async fn launch_with_socket(
moniker: &str,
socket: zx::Socket,
) -> Result<zx::Process, LauncherError> {
let pty = socket::spawn_pty_forwarder(socket).await?;
launch_with_pty(moniker, pty).await
}
pub async fn launch_with_pty(
moniker: &str,
pty: ClientEnd<pty::DeviceMarker>,
) -> Result<zx::Process, LauncherError> {
let (stdin, stdout, stderr) = split_pty_into_handles(pty)?;
launch_with_handles(moniker, stdin, stdout, stderr).await
}
pub async fn launch_with_handles(
moniker: &str,
stdin: zx::Handle,
stdout: zx::Handle,
stderr: zx::Handle,
) -> Result<zx::Process, LauncherError> {
// Process moniker
let moniker = RelativeMoniker::parse(&moniker).map_err(|_| LauncherError::BadMoniker)?;
if !moniker.up_path().is_empty() {
return Err(LauncherError::BadMoniker);
}
let moniker = moniker.to_string();
let launcher =
connect_to_protocol::<LauncherMarker>().map_err(|_| LauncherError::ProcessLauncher)?;
let query =
connect_to_protocol::<fsys::RealmQueryMarker>().map_err(|_| LauncherError::RealmQuery)?;
let instance_scope = InstanceScope::new(&query, &moniker).await?;
// The dash-launcher can be asked to launch multiple dash processes, each of which can
// make their own process hierarchies. This will look better topologically if we make a
// child job for each dash process.
let job =
fuchsia_runtime::job_default().create_child_job().map_err(|_| LauncherError::Internal)?;
let mut ns = create_dash_namespace(instance_scope.ns)?;
let mut handles = create_dash_handles(&job, stdin, stdout, stderr, instance_scope.lib_dir)?;
// -s: force input from stdin
// -i: force interactive
let args = vec!["-i".as_bytes(), "-s".as_bytes()];
// Set PATH to generic command-line tools and package-specific command-line tools
let env = vec!["PATH=/bin:/ns/pkg/bin".as_bytes()];
let mut info = create_launch_info(&moniker, &job).await?;
// Spawn the dash process
launcher.add_names(&mut ns.iter_mut()).map_err(|_| LauncherError::ProcessLauncher)?;
launcher.add_handles(&mut handles.iter_mut()).map_err(|_| LauncherError::ProcessLauncher)?;
launcher.add_args(&mut args.into_iter()).map_err(|_| LauncherError::ProcessLauncher)?;
launcher.add_environs(&mut env.into_iter()).map_err(|_| LauncherError::ProcessLauncher)?;
let (status, process) =
launcher.launch(&mut info).await.map_err(|_| LauncherError::ProcessLauncher)?;
zx::Status::ok(status).map_err(|_| LauncherError::ProcessLauncher)?;
let process = process.ok_or(LauncherError::ProcessLauncher)?;
// The job should be terminated when the dash process dies
job.set_critical(zx::JobCriticalOptions::empty(), &process)
.map_err(|_| LauncherError::Internal)?;
Ok(process)
}
fn get_bin_from_launcher_namespace() -> Result<ClientEnd<fio::DirectoryMarker>, LauncherError> {
let (bin_dir, server) = fidl::endpoints::create_endpoints::<fio::DirectoryMarker>().unwrap();
fuchsia_fs::node::connect_in_namespace(
"/pkg/bin",
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
server.into_channel(),
)
.map_err(|_| LauncherError::Internal)?;
Ok(bin_dir)
}
async fn create_launch_info(moniker: &str, job: &zx::Job) -> Result<LaunchInfo, LauncherError> {
// Load `/pkg/bin/sh` as an executable VMO and pass it to the Launcher
let dash_file = fuchsia_fs::file::open_in_namespace(
"/pkg/bin/sh",
fio::OpenFlags::RIGHT_EXECUTABLE | fio::OpenFlags::RIGHT_READABLE,
)
.map_err(|_| LauncherError::DashBinary)?;
let executable = dash_file
.get_backing_memory(
fio::VmoFlags::READ | fio::VmoFlags::EXECUTE | fio::VmoFlags::PRIVATE_CLONE,
)
.await
.map_err(|_| LauncherError::DashBinary)?
.map_err(|_| LauncherError::DashBinary)?;
let job_dup =
job.duplicate_handle(zx::Rights::SAME_RIGHTS).map_err(|_| LauncherError::Internal)?;
// Set a name that's easy to find
// if moniker is `./core/foo`, process name is `sh-core-foo`
let mut process_name = moniker.replace('/', "-");
process_name.remove(0);
let process_name = format!("sh{}", process_name);
Ok(LaunchInfo { name: process_name, job: job_dup, executable })
}
fn serve_dash_svc_dir() -> Result<ClientEnd<fio::DirectoryMarker>, LauncherError> {
// Serve a directory that only provides fuchsia.process.Launcher to dash
let (svc_dir, server_end) =
fidl::endpoints::create_endpoints().map_err(|_| LauncherError::Internal)?;
let mut fs = ServiceFs::new();
fs.add_proxy_service::<LauncherMarker, ()>();
fs.serve_connection(server_end.into_channel()).map_err(|_| LauncherError::Internal)?;
fasync::Task::spawn(async move {
fs.collect::<()>().await;
})
.detach();
Ok(svc_dir)
}
fn create_dash_namespace(instance_ns: Vec<NameInfo>) -> Result<Vec<NameInfo>, LauncherError> {
let mut ns = instance_ns;
// Add the dash-launcher `/pkg/bin` to dash as `/bin`
let bin_dir = get_bin_from_launcher_namespace()?;
ns.push(NameInfo { path: "/bin".to_string(), directory: bin_dir });
// Add a custom `/svc` directory to dash
let svc_dir = serve_dash_svc_dir()?;
ns.push(NameInfo { path: "/svc".to_string(), directory: svc_dir });
Ok(ns)
}
fn split_pty_into_handles(
pty: ClientEnd<pty::DeviceMarker>,
) -> Result<(zx::Handle, zx::Handle, zx::Handle), LauncherError> {
let pty = pty.into_proxy().map_err(|_| LauncherError::Pty)?;
// Split the PTY into 3 channels (stdin, stdout, stderr)
let (stdout, to_pty_stdout) =
fidl::endpoints::create_endpoints::<pty::DeviceMarker>().map_err(|_| LauncherError::Pty)?;
let (stderr, to_pty_stderr) =
fidl::endpoints::create_endpoints::<pty::DeviceMarker>().map_err(|_| LauncherError::Pty)?;
let to_pty_stdout = to_pty_stdout.into_channel().into();
let to_pty_stderr = to_pty_stderr.into_channel().into();
// Clone the PTY to also be used for stdout and stderr
pty.clone(fio::OpenFlags::CLONE_SAME_RIGHTS, to_pty_stdout).map_err(|_| LauncherError::Pty)?;
pty.clone(fio::OpenFlags::CLONE_SAME_RIGHTS, to_pty_stderr).map_err(|_| LauncherError::Pty)?;
let stdin = pty.into_channel().map_err(|_| LauncherError::Pty)?.into_zx_channel().into_handle();
let stdout = stdout.into_handle();
let stderr = stderr.into_handle();
Ok((stdin, stdout, stderr))
}
fn create_dash_handles(
job: &zx::Job,
stdin: zx::Handle,
stdout: zx::Handle,
stderr: zx::Handle,
lib_dir: Option<fio::DirectoryProxy>,
) -> Result<Vec<HandleInfo>, LauncherError> {
let stdin_handle = HandleInfo {
handle: stdin.into_handle(),
id: HandleId::new(HandleType::FileDescriptor, 0).as_raw(),
};
let stdout_handle = HandleInfo {
handle: stdout.into_handle(),
id: HandleId::new(HandleType::FileDescriptor, 1).as_raw(),
};
let stderr_handle = HandleInfo {
handle: stderr.into_handle(),
id: HandleId::new(HandleType::FileDescriptor, 2).as_raw(),
};
let job_dup =
job.duplicate_handle(zx::Rights::SAME_RIGHTS).map_err(|_| LauncherError::Internal)?;
let job_handle = HandleInfo {
handle: zx::Handle::from(job_dup),
id: HandleId::new(HandleType::DefaultJob, 0).as_raw(),
};
let ldsvc = if let Some(lib_dir) = lib_dir {
let (ldsvc, server_end) = zx::Channel::create().map_err(|_| LauncherError::Internal)?;
library_loader::start(Arc::new(lib_dir), server_end);
ldsvc.into_handle()
} else {
// Use the default loader in the dash-launcher process
fuchsia_runtime::loader_svc().map_err(|_| LauncherError::Internal)?
};
let ldsvc_handle =
HandleInfo { handle: ldsvc, id: HandleId::new(HandleType::LdsvcLoader, 0).as_raw() };
let utc_clock = {
let utc_clock = fuchsia_runtime::duplicate_utc_clock_handle(zx::Rights::SAME_RIGHTS)
.map_err(|_| LauncherError::Internal)?;
utc_clock.into_handle()
};
let utc_clock_handle =
HandleInfo { handle: utc_clock, id: HandleId::new(HandleType::ClockUtc, 0).as_raw() };
Ok(vec![stdin_handle, stdout_handle, stderr_handle, job_handle, ldsvc_handle, utc_clock_handle])
}
struct InstanceScope {
lib_dir: Option<fio::DirectoryProxy>,
ns: Vec<NameInfo>,
}
impl InstanceScope {
async fn new(query: &fsys::RealmQueryProxy, moniker: &str) -> Result<Self, LauncherError> {
let (_, resolved) = query
.get_instance_info(moniker)
.await
.map_err(|_| LauncherError::RealmQuery)?
.map_err(|e| {
if e == fcomp::Error::InstanceNotFound {
LauncherError::InstanceNotFound
} else {
LauncherError::RealmQuery
}
})?;
let mut ns = vec![];
let mut lib_dir = None;
if let Some(resolved) = resolved {
if let Some(started) = resolved.started {
if let Some(out_dir) = started.out_dir {
ns.push(NameInfo { path: "/out".to_string(), directory: out_dir });
}
if let Some(runtime_dir) = started.runtime_dir {
ns.push(NameInfo { path: "/runtime".to_string(), directory: runtime_dir });
}
}
ns.push(NameInfo { path: "/ns".to_string(), directory: resolved.ns_dir });
ns.push(NameInfo { path: "/exposed".to_string(), directory: resolved.exposed_dir });
// If available, use the component's /pkg/lib dir to load libraries
if let Some(pkg_dir) = resolved.pkg_dir {
let pkg_dir = pkg_dir.into_proxy().map_err(|_| LauncherError::Internal)?;
if let Ok(dir) = fuchsia_fs::directory::open_directory(
&pkg_dir,
"lib",
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
)
.await
{
info!("Using /pkg/lib dir of {} for loading libraries", moniker);
lib_dir.replace(dir);
}
}
}
Ok(Self { ns, lib_dir })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn serve_realm_query(
instance_info: fsys::InstanceInfo,
resolved: Option<Box<fsys::ResolvedState>>,
) -> fsys::RealmQueryProxy {
let (proxy, server_end) =
fidl::endpoints::create_proxy::<fsys::RealmQueryMarker>().unwrap();
fasync::Task::spawn(async move {
let mut stream = server_end.into_stream().unwrap();
let fsys::RealmQueryRequest::GetInstanceInfo { moniker, responder } =
stream.next().await.unwrap().unwrap();
assert_eq!(moniker, ".");
responder.send(&mut Ok((instance_info, resolved))).unwrap();
})
.detach();
proxy
}
#[fuchsia::test]
async fn check_namespace_started() {
let instance_info = fsys::InstanceInfo {
moniker: ".".to_string(),
url: "test://foo.com#meta/bar.cm".to_string(),
instance_id: None,
state: fsys::InstanceState::Started,
};
let (exposed_dir, _exposed_dir_server) =
fidl::endpoints::create_endpoints::<fio::DirectoryMarker>().unwrap();
let (ns_dir, _ns_dir_server) =
fidl::endpoints::create_endpoints::<fio::DirectoryMarker>().unwrap();
let (out_dir, _ns_dir_server) =
fidl::endpoints::create_endpoints::<fio::DirectoryMarker>().unwrap();
let (runtime_dir, _ns_dir_server) =
fidl::endpoints::create_endpoints::<fio::DirectoryMarker>().unwrap();
let started = Some(Box::new(fsys::StartedState {
out_dir: Some(out_dir),
runtime_dir: Some(runtime_dir),
start_reason: "Debug".to_string(),
}));
let resolved = Some(Box::new(fsys::ResolvedState {
uses: vec![],
exposes: vec![],
config: None,
pkg_dir: None,
started,
exposed_dir,
ns_dir,
}));
let query = serve_realm_query(instance_info, resolved);
let instance_scope = InstanceScope::new(&query, ".").await.unwrap();
let ns = create_dash_namespace(instance_scope.ns).unwrap();
assert_eq!(ns.len(), 6);
let mut paths: Vec<String> = ns.into_iter().map(|e| e.path).collect();
paths.sort();
assert_eq!(paths, vec!["/bin", "/exposed", "/ns", "/out", "/runtime", "/svc"]);
}
#[fuchsia::test]
async fn check_namespace_resolved() {
let instance_info = fsys::InstanceInfo {
moniker: ".".to_string(),
url: "test://foo.com#meta/bar.cm".to_string(),
instance_id: None,
state: fsys::InstanceState::Resolved,
};
let (exposed_dir, _exposed_dir_server) =
fidl::endpoints::create_endpoints::<fio::DirectoryMarker>().unwrap();
let (ns_dir, _ns_dir_server) =
fidl::endpoints::create_endpoints::<fio::DirectoryMarker>().unwrap();
let resolved = Some(Box::new(fsys::ResolvedState {
uses: vec![],
exposes: vec![],
config: None,
pkg_dir: None,
started: None,
exposed_dir,
ns_dir,
}));
let query = serve_realm_query(instance_info, resolved);
let instance_scope = InstanceScope::new(&query, ".").await.unwrap();
let ns = create_dash_namespace(instance_scope.ns).unwrap();
assert_eq!(ns.len(), 4);
let mut paths: Vec<String> = ns.into_iter().map(|e| e.path).collect();
paths.sort();
assert_eq!(paths, vec!["/bin", "/exposed", "/ns", "/svc"]);
}
#[fuchsia::test]
async fn check_namespace_unresolved() {
let instance_info = fsys::InstanceInfo {
moniker: ".".to_string(),
url: "test://foo.com#meta/bar.cm".to_string(),
instance_id: None,
state: fsys::InstanceState::Unresolved,
};
let resolved = None;
let query = serve_realm_query(instance_info, resolved);
let instance_scope = InstanceScope::new(&query, ".").await.unwrap();
let ns = create_dash_namespace(instance_scope.ns).unwrap();
assert_eq!(ns.len(), 2);
let mut paths: Vec<String> = ns.into_iter().map(|e| e.path).collect();
paths.sort();
assert_eq!(paths, vec!["/bin", "/svc"]);
}
}