blob: ff24831960ac24e71b34eae9941859433baf1599 [file]
// Copyright 2026 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 anyhow::{Error, anyhow};
use bootfs::{ZbiItemIterator, get_bootfs_vmo};
use cmdline::{ProgramInfo, parse_cmdline};
use fidl_next::fuchsia::create_channel;
use fidl_next_fuchsia_boot as fuchsia_boot;
use fidl_next_fuchsia_io as fuchsia_io;
use fuchsia_async::OnSignals;
use fuchsia_runtime::{HandleInfo, HandleType};
use process_builder::StartupHandle;
use program::{SystemHandles, launch_program};
use std::fmt::Write as _;
use userboot::{Log, take_system_handles};
use zbi::ZbiType;
use zx::sys::{ZX_SYSTEM_POWERCTL_SHUTDOWN, zx_system_powerctl};
use zx::{Rights, Signals, Vmo};
const BOOT_TEST_SUCCESS_STRING: &str = env!("BOOT_TEST_SUCCESS_STRING");
/// Test configuration options parsed from the command line.
#[derive(Default)]
struct Options {
/// Information about the test program to launch.
test: ProgramInfo,
/// Information about the primary boot program to launch.
boot: ProgramInfo,
/// Whether the next program to execute is a test.
next_is_test: bool,
}
fn parse_option(key: &str, value: &str, opts: &mut Options) -> bool {
match key {
"userboot.test.next" => {
opts.test.next = value.to_string();
true
}
"userboot.test.root" => {
opts.test.root = value.strip_suffix('/').unwrap_or(value).to_string();
true
}
"userboot.next" => {
opts.boot.next = value.to_string();
true
}
"userboot.root" => {
opts.boot.root = value.strip_suffix('/').unwrap_or(value).to_string();
true
}
"userboot.next-is-test" => {
opts.next_is_test = true;
true
}
_ => false,
}
}
fn get_options(zbi_vmo: &Vmo, log: &mut Log) -> Result<Options, Error> {
let mut options = Options::default();
for item in ZbiItemIterator::new(zbi_vmo)? {
let item = item?;
if item.header.type_ == ZbiType::CmdLine as u32 {
let payload =
zbi_vmo.read_to_vec::<u8>(item.payload_offset, item.header.length.into())?;
if let Ok(cmdline) = str::from_utf8(&payload) {
writeln!(log, "CMDLINE {}", cmdline)?;
parse_cmdline(cmdline, log, &mut options, parse_option)?;
}
}
}
if !options.test.root.is_empty() && options.test.root.starts_with('/') {
anyhow::bail!("`userboot.test.root` (\"{}\") must not begin with a '/'", options.test.root);
}
if !options.boot.root.is_empty() && options.boot.root.starts_with('/') {
anyhow::bail!("`userboot.root` (\"{}\") must not begin with a '/'", options.boot.root);
}
Ok(options)
}
async fn wait_for_process(
process: &zx::Process,
program_name: &str,
log: &mut Log,
) -> Result<(), Error> {
writeln!(log, "Waiting for {program_name} to exit...")?;
let _ = OnSignals::new(process, Signals::PROCESS_TERMINATED).await?;
let return_code = process.info()?.return_code;
writeln!(log, "*** Exit status {return_code} ***")?;
if return_code == 0 {
writeln!(log, "{BOOT_TEST_SUCCESS_STRING}")?;
}
Ok(())
}
async fn run(log: &mut Log) -> Result<(), Error> {
let mut handles = SystemHandles::from_handles(take_system_handles()?)?;
let power_resource = handles
.power_resource
.as_ref()
.map(|p| p.duplicate_handle(Rights::SAME_RIGHTS))
.transpose()?;
let options = get_options(&handles.zbi_vmo, log)?;
let bootfs_vmo = get_bootfs_vmo(&handles.zbi_vmo, false, log)?;
let (userboot_client, userboot_server) = create_channel::<fuchsia_boot::Userboot>();
let userboot_client = userboot_client.spawn();
let (svc_stash_client, svc_stash_server) = create_channel::<fuchsia_boot::SvcStash>();
let svc_stash_client = svc_stash_client.spawn();
let _ = userboot_client.post_stash_svc(svc_stash_server).await;
if !options.test.next.is_empty() {
let (program_name, target_path) = options.test.filename();
let (svc_client, svc_server) = create_channel::<fuchsia_io::Directory>();
let _ = svc_stash_client.store(svc_server).await;
let mut test_handles = handles.duplicate()?;
test_handles.startup_handles.push(StartupHandle {
handle: test_handles
.zbi_vmo
.duplicate_handle(Rights::SAME_RIGHTS)?
.into_handle()
.into(),
info: HandleInfo::new(HandleType::BootdataVmo, 0),
});
test_handles.startup_handles.push(StartupHandle {
handle: bootfs_vmo.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
info: HandleInfo::new(HandleType::BootfsVmo, 0),
});
test_handles.startup_handles.push(StartupHandle {
handle: svc_client.into_untyped().into_handle().into(),
info: HandleInfo::new(HandleType::NamespaceDirectory, 0),
});
let bootfs_vmo_dup = bootfs_vmo.duplicate_handle(Rights::SAME_RIGHTS)?;
let process = launch_program(
program_name,
&target_path,
options.test.next.split('+'),
bootfs_vmo_dup,
test_handles,
None,
log,
)
.await
.map_err(|e| anyhow!("launch_program {program_name} failed: {e:?}"))?;
wait_for_process(&process, program_name, log).await?;
}
if !options.boot.next.is_empty() {
let (program_name, target_path) = options.boot.filename();
let (svc_client, svc_server) = create_channel::<fuchsia_io::Directory>();
let _ = svc_stash_client.store(svc_server).await;
handles.startup_handles.push(StartupHandle {
handle: svc_client.into_untyped().into_handle().into(),
info: HandleInfo::new(HandleType::NamespaceDirectory, 0),
});
handles.startup_handles.push(StartupHandle {
handle: userboot_server.into_untyped().into_handle().into(),
info: HandleInfo::new(HandleType::User0, 0),
});
userboot_client.post_boot_handles_with(handles.take_boot_handles()?).await?;
let mut bootfs_entries = Vec::new();
let process = launch_program(
program_name,
&target_path,
options.boot.next.split('+'),
bootfs_vmo.duplicate_handle(Rights::SAME_RIGHTS)?,
handles,
Some(&mut bootfs_entries),
log,
)
.await
.map_err(|e| anyhow!("launch_program {program_name} failed: {e:?}"))?;
let _ = userboot_client.post_bootfs_vmo(bootfs_vmo).await;
if !bootfs_entries.is_empty() {
let files = bootfs_entries
.into_iter()
.map(|(offset, contents)| fuchsia_boot::natural::BootfsFileVmo { offset, contents })
.collect::<Vec<_>>();
let _ = userboot_client.post_bootfs_files(files).await;
}
drop(userboot_client);
if options.next_is_test {
wait_for_process(&process, program_name, log).await?;
}
}
if options.boot.next.is_empty() || options.next_is_test {
if let Some(power_resource) = power_resource {
writeln!(log, "Process exited, executing poweroff")?;
// SAFETY: `power_resource` holds a valid resource handle required for poweroff.
unsafe {
zx_system_powerctl(power_resource.raw_handle(), ZX_SYSTEM_POWERCTL_SHUTDOWN);
}
writeln!(log, "Still here after poweroff")?;
}
}
Ok(())
}
#[fuchsia_async::run_singlethreaded]
async fn main() {
let mut log = Log::new();
if let Err(e) = run(&mut log).await {
let _ = writeln!(log, "userboot error: {e:?}");
}
}