Skip to main content

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