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