test_runners_lib/
launch.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
5//! Helpers for launching components.
6
7use crate::logs::{LoggerError, LoggerStream, create_log_stream, create_std_combined_log_stream};
8use anyhow::Error;
9use fidl_fuchsia_component::IntrospectorMarker;
10use fuchsia_component::client::connect_to_protocol;
11use namespace::Namespace;
12use runtime::{HandleInfo, HandleType};
13use thiserror::Error;
14use zx::{AsHandleRef, HandleBased, Process, Rights, Task};
15use {fidl_fuchsia_process as fproc, fuchsia_runtime as runtime};
16
17/// The basic rights to use when creating or duplicating a UTC clock. Restrict these
18/// on a case-by-case basis only.
19///
20/// Rights:
21///
22/// - `Rights::DUPLICATE`, `Rights::TRANSFER`: used to forward the UTC clock in runners.
23/// - `Rights::READ`: used to read the clock indication.
24/// - `Rights::WAIT`: used to wait on signals such as "clock is updated" or "clock is started".
25/// - `Rights::MAP`, `Rights::INSPECT`: used to memory-map the UTC clock.
26///
27/// The `Rights::WRITE` is notably absent, since on Fuchsia this right is given to particular
28/// components only and a writable clock can not be obtained via procargs.
29pub static UTC_CLOCK_BASIC_RIGHTS: std::sync::LazyLock<zx::Rights> =
30    std::sync::LazyLock::new(|| {
31        Rights::DUPLICATE
32            | Rights::READ
33            | Rights::WAIT
34            | Rights::TRANSFER
35            | Rights::MAP
36            | Rights::INSPECT
37    });
38
39/// Error encountered while launching a component.
40#[derive(Debug, Error)]
41pub enum LaunchError {
42    #[error("{:?}", _0)]
43    Logger(#[from] LoggerError),
44
45    #[error("Error connecting to launcher: {:?}", _0)]
46    Launcher(Error),
47
48    #[error("{:?}", _0)]
49    LoadInfo(runner::component::LaunchError),
50
51    #[error("Error launching process: {:?}", _0)]
52    LaunchCall(fidl::Error),
53
54    #[error("Error launching process: {:?}", _0)]
55    ProcessLaunch(zx::Status),
56
57    #[error("Error duplicating vDSO: {:?}", _0)]
58    DuplicateVdso(zx::Status),
59
60    #[error("Error launching process: {:?}", _0)]
61    Fidl(#[from] fidl::Error),
62
63    #[error("Error launching process, cannot create socket {:?}", _0)]
64    CreateSocket(zx::Status),
65
66    #[error("Error cloning UTC clock: {:?}", _0)]
67    UtcClock(zx::Status),
68
69    #[error("unexpected error")]
70    UnExpectedError,
71}
72
73/// Arguments to launch_process.
74pub struct LaunchProcessArgs<'a> {
75    /// Relative binary path to /pkg.
76    pub bin_path: &'a str,
77    /// Name of the binary to add to process. This will be truncated to
78    /// `zx::sys::ZX_MAX_NAME_LEN` bytes.
79    pub process_name: &'a str,
80    /// Job used launch process, if None, a new child of default_job() is used.
81    pub job: Option<zx::Job>,
82    /// Namespace for binary process to be launched.
83    pub ns: Namespace,
84    /// Arguments to binary. Binary name is automatically appended as first argument.
85    pub args: Option<Vec<String>>,
86    /// Extra names to add to namespace. by default only names from `ns` are added.
87    pub name_infos: Option<Vec<fproc::NameInfo>>,
88    /// Process environment variables.
89    pub environs: Option<Vec<String>>,
90    /// Extra handle infos to add. Handles for stdout, stderr, and utc_clock are added.
91    /// The UTC clock handle is cloned from the current process.
92    pub handle_infos: Option<Vec<fproc::HandleInfo>>,
93    /// Handle to lib loader protocol client.
94    pub loader_proxy_chan: Option<zx::Channel>,
95    /// VMO containing mapping to executable binary.
96    pub executable_vmo: Option<zx::Vmo>,
97    /// Options to create process with.
98    pub options: zx::ProcessOptions,
99    // The structured config vmo.
100    pub config_vmo: Option<zx::Vmo>,
101    // The component instance, used only in tracing
102    pub component_instance: Option<fidl::Event>,
103    // The component URL, used only in tracing
104    pub url: Option<String>,
105}
106
107/// Launches process, assigns a combined logger stream as stdout/stderr to launched process.
108pub async fn launch_process(
109    args: LaunchProcessArgs<'_>,
110) -> Result<(Process, ScopedJob, LoggerStream), LaunchError> {
111    let launcher = connect_to_protocol::<fproc::LauncherMarker>().map_err(LaunchError::Launcher)?;
112    let (logger, stdout_handle, stderr_handle) =
113        create_std_combined_log_stream().map_err(LaunchError::Logger)?;
114    let (process, job) = launch_process_impl(args, launcher, stdout_handle, stderr_handle).await?;
115    Ok((process, job, logger))
116}
117
118/// Launches process, assigns two separate stdout and stderr streams to launched process.
119/// Returns (process, job, stdout_logger, stderr_logger)
120pub async fn launch_process_with_separate_std_handles(
121    args: LaunchProcessArgs<'_>,
122) -> Result<(Process, ScopedJob, LoggerStream, LoggerStream), LaunchError> {
123    let launcher = connect_to_protocol::<fproc::LauncherMarker>().map_err(LaunchError::Launcher)?;
124    let (stdout_logger, stdout_handle) = create_log_stream().map_err(LaunchError::Logger)?;
125    let (stderr_logger, stderr_handle) = create_log_stream().map_err(LaunchError::Logger)?;
126    let (process, job) = launch_process_impl(args, launcher, stdout_handle, stderr_handle).await?;
127    Ok((process, job, stdout_logger, stderr_logger))
128}
129
130async fn launch_process_impl(
131    args: LaunchProcessArgs<'_>,
132    launcher: fproc::LauncherProxy,
133    stdout_handle: zx::Handle,
134    stderr_handle: zx::Handle,
135) -> Result<(Process, ScopedJob), LaunchError> {
136    const STDOUT: u16 = 1;
137    const STDERR: u16 = 2;
138
139    let mut handle_infos = args.handle_infos.unwrap_or(vec![]);
140
141    handle_infos.push(fproc::HandleInfo {
142        handle: stdout_handle,
143        id: HandleInfo::new(HandleType::FileDescriptor, STDOUT).as_raw(),
144    });
145
146    handle_infos.push(fproc::HandleInfo {
147        handle: stderr_handle,
148        id: HandleInfo::new(HandleType::FileDescriptor, STDERR).as_raw(),
149    });
150
151    handle_infos.push(fproc::HandleInfo {
152        handle: runtime::duplicate_utc_clock_handle(*UTC_CLOCK_BASIC_RIGHTS)
153            .map_err(LaunchError::UtcClock)?
154            .into_handle(),
155        id: HandleInfo::new(HandleType::ClockUtc, 0).as_raw(),
156    });
157
158    if let Some(config_vmo) = args.config_vmo {
159        handle_infos.push(fproc::HandleInfo {
160            handle: config_vmo.into_handle(),
161            id: HandleInfo::new(HandleType::ComponentConfigVmo, 0).as_raw(),
162        });
163    }
164
165    let LaunchProcessArgs {
166        bin_path,
167        process_name,
168        args,
169        options,
170        ns,
171        job,
172        name_infos,
173        environs,
174        loader_proxy_chan,
175        executable_vmo,
176        component_instance,
177        url,
178        ..
179    } = args;
180    // Load the component
181    let launch_info =
182        runner::component::configure_launcher(runner::component::LauncherConfigArgs {
183            bin_path,
184            name: process_name,
185            args,
186            options,
187            ns,
188            job,
189            handle_infos: Some(handle_infos),
190            name_infos,
191            environs,
192            launcher: &launcher,
193            loader_proxy_chan,
194            executable_vmo,
195        })
196        .await
197        .map_err(LaunchError::LoadInfo)?;
198
199    let component_job = launch_info
200        .job
201        .as_handle_ref()
202        .duplicate(zx::Rights::SAME_RIGHTS)
203        .expect("handle duplication failed!");
204
205    let (status, process) = launcher.launch(launch_info).await.map_err(LaunchError::LaunchCall)?;
206
207    let status = zx::Status::from_raw(status);
208    if status != zx::Status::OK {
209        return Err(LaunchError::ProcessLaunch(status));
210    }
211
212    let process = process.ok_or_else(|| LaunchError::UnExpectedError)?;
213
214    trace_component_start(&process, component_instance, url).await;
215
216    Ok((process, ScopedJob::new(zx::Job::from_handle(component_job))))
217}
218
219/// Reports the component starting to the trace system, if tracing is enabled.
220/// Uses the Introspector protocol, which must be routed to the component to
221/// report the moniker correctly.
222async fn trace_component_start(
223    process: &Process,
224    component_instance: Option<fidl::Event>,
225    url: Option<String>,
226) {
227    if fuchsia_trace::category_enabled(c"component:start") {
228        let pid = process.get_koid().unwrap().raw_koid();
229        let moniker = match component_instance {
230            None => "Missing component instance".to_string(),
231            Some(component_instance) => match connect_to_protocol::<IntrospectorMarker>() {
232                Ok(introspector) => {
233                    let component_instance =
234                        component_instance.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
235                    match introspector.get_moniker(component_instance).await {
236                        Ok(Ok(moniker)) => moniker,
237                        Ok(Err(e)) => {
238                            format!("Couldn't get moniker: {e:?}")
239                        }
240                        Err(e) => {
241                            format!("Couldn't get the moniker: {e:?}")
242                        }
243                    }
244                }
245                Err(e) => {
246                    format!("Couldn't get introspector: {e:?}")
247                }
248            },
249        };
250        let url = url.unwrap_or_else(|| "Missing URL".to_string());
251        fuchsia_trace::instant!(
252            c"component:start",
253            // If you change this name, include the string "-test-".
254            // Scripts will match that to detect processes started by a test runner.
255            c"-test-",
256            fuchsia_trace::Scope::Thread,
257            "moniker" => format!("{}", moniker).as_str(),
258            "url" => url.as_str(),
259            "pid" => pid
260        );
261    }
262}
263
264// Structure to guard job and kill it when going out of scope.
265pub struct ScopedJob {
266    pub object: Option<zx::Job>,
267}
268
269impl ScopedJob {
270    pub fn new(job: zx::Job) -> Self {
271        Self { object: Some(job) }
272    }
273
274    /// Return the job back from this scoped object
275    pub fn take(mut self) -> zx::Job {
276        self.object.take().unwrap()
277    }
278}
279
280impl Drop for ScopedJob {
281    fn drop(&mut self) {
282        if let Some(job) = self.object.take() {
283            job.kill().ok();
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use fidl::endpoints::{ClientEnd, Proxy, create_proxy_and_stream};
292    use fuchsia_runtime::{job_default, process_self, swap_utc_clock_handle};
293    use futures::prelude::*;
294    use {
295        fidl_fuchsia_component_runner as fcrunner, fidl_fuchsia_io as fio, fuchsia_async as fasync,
296        zx,
297    };
298
299    #[test]
300    fn scoped_job_works() {
301        let new_job = job_default().create_child_job().unwrap();
302        let job_dup = new_job.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
303
304        // create new child job, else killing a job has no effect.
305        let _child_job = new_job.create_child_job().unwrap();
306
307        // check that job is alive
308        let info = job_dup.info().unwrap();
309        assert!(!info.exited);
310        {
311            let _job_about_to_die = ScopedJob::new(new_job);
312        }
313
314        // check that job was killed
315        let info = job_dup.info().unwrap();
316        assert!(info.exited);
317    }
318
319    #[test]
320    fn scoped_job_take_works() {
321        let new_job = job_default().create_child_job().unwrap();
322        let raw_handle = new_job.raw_handle();
323
324        let scoped = ScopedJob::new(new_job);
325
326        let ret_job = scoped.take();
327
328        // make sure we got back same job handle.
329        assert_eq!(ret_job.raw_handle(), raw_handle);
330    }
331
332    #[fasync::run_singlethreaded(test)]
333    #[ignore] // TODO: b/422533641 - remove
334    async fn utc_clock_is_cloned() {
335        let clock = fuchsia_runtime::UtcClock::create(zx::ClockOpts::MONOTONIC, None)
336            .expect("failed to create clock");
337        let expected_clock_koid =
338            clock.as_handle_ref().get_koid().expect("failed to get clock koid");
339
340        // We are affecting the process-wide clock here, but since Rust test cases are run in their
341        // own process, this won't interact with other running tests.
342        let _ = swap_utc_clock_handle(clock).expect("failed to swap clocks");
343
344        // We can't fake all the arguments, as there is actual IO happening. Pass in the bare
345        // minimum that a process needs, and use this test's process handle for real values.
346        let pkg = fuchsia_fs::directory::open_in_namespace(
347            "/pkg",
348            fio::PERM_READABLE | fio::PERM_EXECUTABLE,
349        )
350        .expect("failed to open pkg");
351        let args = LaunchProcessArgs {
352            bin_path: "bin/test_runners_lib_lib_test", // path to this binary
353            environs: None,
354            args: None,
355            job: None,
356            process_name: "foo",
357            name_infos: None,
358            handle_infos: None,
359            ns: vec![fcrunner::ComponentNamespaceEntry {
360                path: Some("/pkg".into()),
361                directory: Some(ClientEnd::new(pkg.into_channel().unwrap().into_zx_channel())),
362                ..Default::default()
363            }]
364            .try_into()
365            .unwrap(),
366            loader_proxy_chan: None,
367            executable_vmo: None,
368            options: zx::ProcessOptions::empty(),
369            config_vmo: None,
370            url: None,
371            component_instance: None,
372        };
373        let (mock_proxy, mut mock_stream) = create_proxy_and_stream::<fproc::LauncherMarker>();
374        let mock_fut = async move {
375            let mut all_handles = vec![];
376            while let Some(request) =
377                mock_stream.try_next().await.expect("failed to get next message")
378            {
379                match request {
380                    fproc::LauncherRequest::AddHandles { handles, .. } => {
381                        all_handles.extend(handles);
382                    }
383                    fproc::LauncherRequest::Launch { responder, .. } => {
384                        responder
385                            .send(
386                                zx::Status::OK.into_raw(),
387                                Some(
388                                    process_self()
389                                        .duplicate(zx::Rights::SAME_RIGHTS)
390                                        .expect("failed to duplicate process handle"),
391                                ),
392                            )
393                            .expect("failed to send reply");
394                    }
395                    _ => {}
396                }
397            }
398            return all_handles;
399        };
400        let (_logger, stdout_handle, stderr_handle) =
401            create_std_combined_log_stream().map_err(LaunchError::Logger).unwrap();
402        let client_fut = async move {
403            let _ = launch_process_impl(args, mock_proxy, stdout_handle, stderr_handle)
404                .await
405                .expect("failed to launch process");
406        };
407
408        let (all_handles, ()) = futures::future::join(mock_fut, client_fut).await;
409        let clock_id = HandleInfo::new(HandleType::ClockUtc, 0).as_raw();
410
411        let utc_clock_handle = all_handles
412            .into_iter()
413            .find_map(
414                |hi: fproc::HandleInfo| if hi.id == clock_id { Some(hi.handle) } else { None },
415            )
416            .expect("UTC clock handle");
417        let clock_koid = utc_clock_handle.get_koid().expect("failed to get koid");
418        assert_eq!(expected_clock_koid, clock_koid);
419    }
420}