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