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