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}