scoped_task/
zircon.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fuchsia_runtime as runtime;
6use log::error;
7use std::borrow::Borrow;
8use std::ffi::CStr;
9use std::ops::Deref;
10use std::panic;
11use std::sync::LazyLock;
12
13static SCOPED_JOB: LazyLock<Scoped<zx::Job>> = LazyLock::new(initialize);
14
15fn initialize() -> Scoped<zx::Job> {
16    let job = runtime::job_default().create_child_job().expect("couldn't create job");
17
18    unsafe {
19        libc::atexit(exit_hook);
20    }
21    install_panic_hook();
22    // We cannot panic past this point.
23
24    Scoped::new(job)
25}
26
27#[cfg(panic = "abort")]
28fn install_panic_hook() {
29    let old_hook = panic::take_hook();
30    let new_hook = Box::new(
31        // TODO(https://fxbug.dev/357638987): Remove #[allow(deprecated)]
32        // and change PanicInfo to PanicHookInfo once we roll
33        // https://github.com/rust-lang/rust/commit/64e56db72aa2b1f112504504a7dceba63881cdef.
34        #[allow(deprecated)]
35        move |panic_info: &panic::PanicInfo<'_>| {
36            exit_hook();
37            old_hook(panic_info);
38        },
39    );
40    panic::set_hook(new_hook);
41}
42
43#[cfg(panic = "unwind")]
44fn install_panic_hook() {
45    // When panic=unwind we can rely on the destructor of the individual
46    // processes.
47}
48
49extern "C" fn exit_hook() {
50    kill(SCOPED_JOB.0.as_ref().unwrap(), "scoped job root");
51}
52
53/// Installs the hooks that ensure scoped tasks are cleaned up on panic and
54/// process exit.
55///
56/// Programs and tests that are multithreaded should call this before starting
57/// any threads. Single-threaded programs do not have to call this, and can rely
58/// on hooks being installed lazily.
59///
60/// This function can be called multiple times. It will not have any effect
61/// after the first call.
62pub fn install_hooks() {
63    let _ = &*SCOPED_JOB;
64}
65
66/// Returns the default job for spawning scoped processes.
67///
68/// This job is a child of the job returned by [`fuchsia_runtime::job_default`].
69pub fn job_default() -> &'static Scoped<zx::Job> {
70    &*SCOPED_JOB
71}
72
73/// Creates and returns a child job of the job returned by [`job_default`].
74pub fn create_child_job() -> Result<Scoped<zx::Job>, zx::Status> {
75    Ok(Scoped::new(SCOPED_JOB.create_child_job()?))
76}
77
78/// A convenience wrapper around [`fdio::spawn_etc`] that returns a
79/// [`Scoped`].
80///
81/// Note that you must assign the return value to a local. You can use a leading
82/// underscore in the name to avoid the unused variable lint, but don't use just
83/// `_` or the process will be killed immediately. For example,
84///
85/// ```
86/// let _process = scoped_task::spawn_etc(
87///     &scoped_task::job_default(),
88///     SpawnOptions::CLONE_ALL,
89///     c"/pkg/bin/echo",
90///     &[c"hello world"],
91///     None,
92///     &mut [],
93/// ).expect("could not spawn process");
94/// ```
95pub fn spawn_etc<'a>(
96    job: impl Borrow<&'a Scoped<zx::Job>>,
97    options: fdio::SpawnOptions,
98    path: &CStr,
99    argv: &[&CStr],
100    environ: Option<&[&CStr]>,
101    actions: &mut [fdio::SpawnAction<'_>],
102) -> Result<Scoped<zx::Process>, (zx::Status, String)> {
103    fdio::spawn_etc(job.borrow().0.as_ref().unwrap(), options, path, argv, environ, actions)
104        .map(Scoped::new)
105}
106
107/// A convenience wrapper around [`fdio::spawn`] that returns a
108/// [`Scoped`].
109///
110/// Note that you must assign the return value to a local. You can use a leading
111/// underscore in the name to avoid the unused variable lint, but don't use just
112/// `_` or the process will be killed immediately. For example,
113///
114/// ```
115/// let _process = scoped_task::spawn(
116///     &scoped_task::job_default(),
117///     SpawnOptions::CLONE_ALL,
118///     c"/pkg/bin/echo",
119///     &[c"hello world"],
120/// ).expect("could not spawn process");
121/// ```
122pub fn spawn<'a>(
123    job: impl Borrow<&'a Scoped<zx::Job>>,
124    options: fdio::SpawnOptions,
125    path: &CStr,
126    argv: &[&CStr],
127) -> Result<Scoped<zx::Process>, zx::Status> {
128    fdio::spawn(job.borrow().0.as_ref().unwrap(), options, path, argv).map(Scoped::new)
129}
130
131/// Scoped wrapper for a process or job backed by a Zircon handle.
132///
133/// The process or job is killed when the wrapper goes out of scope or the
134/// current process panics. See the module-level documentation for more details.
135#[must_use]
136pub struct Scoped<T: zx::Task = zx::Process>(Option<T>);
137
138impl<T: zx::Task> Scoped<T> {
139    fn new(process: T) -> Self {
140        Scoped(Some(process))
141    }
142
143    /// Kills the process, consuming the Scoped wrapper and returning
144    /// the inner task object.
145    pub fn kill(mut self) -> Result<T, zx::Status> {
146        let inner = self.0.take().unwrap();
147        let result = inner.kill();
148        result.map(|()| inner)
149    }
150
151    /// Consumes the Scoped wrapper and returns the inner task object. The task
152    /// will no longer be killed on Drop, but may still be killed on exit.
153    #[must_use]
154    pub fn into_inner(mut self) -> T {
155        self.0.take().unwrap()
156    }
157}
158
159impl<T: zx::Task> Drop for Scoped<T> {
160    fn drop(&mut self) {
161        if let Some(process) = self.0.take() {
162            kill(&process, "scoped task");
163        }
164    }
165}
166
167impl<T: zx::Task> Deref for Scoped<T> {
168    type Target = T;
169    fn deref(&self) -> &T {
170        self.0.as_ref().unwrap()
171    }
172}
173
174fn kill<T: zx::Task>(process: &T, what: &'static str) {
175    let result: Result<(), zx::Status> = (|| {
176        process.kill()?;
177        process
178            .wait_handle(zx::Signals::TASK_TERMINATED, zx::MonotonicInstant::INFINITE)
179            .to_result()?;
180        Ok(())
181    })();
182    match result {
183        Ok(()) => {}
184        Err(status) => {
185            eprintln!("error: could not kill {what}: {status}");
186            error!(status:%; "error: could not kill {what}");
187        }
188    }
189}