1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#![deny(missing_docs)]

//! Some functions to help run command line processes for tests.
//!
//! ## Usage:
//! ```
//! let mut fs = ServiceFs::new();
//! let (svc_client_end, svc_server_end) = zx::Channel::create().expect("create channel");
//! let svc_proxy = fidl_fuchsia_io::DirectoryProxy::from_channel(
//!     fuchsia_async::Channel::from_channel(svc_client_end),
//! );
//! let env = fs.serve_connection(svc_server_end);
//! ...
//! let output = shell_process::run_process(
//!     "someprocess",
//!     ["--arg", "foo"],
//!     [("/svc", &svc_proxy)],
//! );
//! assert!(output.is_ok());
//! ```

use {
    fdio::{SpawnAction, SpawnOptions},
    fuchsia_async::{self as fasync},
    fuchsia_runtime::{job_default, HandleInfo, HandleType},
    fuchsia_zircon::{self as zx, HandleBased, ProcessInfo},
    futures::prelude::*,
    libc::{STDERR_FILENO, STDOUT_FILENO},
    std::ffi::{CStr, CString},
};

/// A struct to contain the results of a shell process.
pub struct ProcessOutput {
    /// The return code from a process.
    pub return_code: i64,
    /// The stderr text output of a process.
    pub stderr: Vec<u8>,
    /// The stdout text output of a process.
    pub stdout: Vec<u8>,
}

impl ProcessOutput {
    /// Whether or not a process ended with return_code zero.
    pub fn is_ok(&self) -> bool {
        self.return_code == 0
    }
    /// The return code with which a process ended.
    pub fn return_code(&self) -> i64 {
        self.return_code
    }
    /// The stdout text output of a process, as a &str.
    pub fn stdout_str(&self) -> &str {
        std::str::from_utf8(&self.stdout).unwrap()
    }
    /// The stderr text output of a process, as a &str.
    pub fn stderr_str(&self) -> &str {
        std::str::from_utf8(&self.stderr).unwrap()
    }
}

/// Runs a binary with some arguments asynchronously; returns a delayed exit
/// code and two sockets with stdout and stderr. For a simpler API, use
/// run_process().
pub async fn run_process_async<'a>(
    binary_path: &'a str,
    args: impl IntoIterator<Item = &'a str>,
    proxies: impl IntoIterator<Item = (&'a str, &fidl_fuchsia_io::DirectoryProxy)>,
) -> (fasync::Task<i64>, fasync::Socket, fasync::Socket) {
    //
    let (stdout_reader, stdout_writer) = zx::Socket::create_stream();
    let (stderr_reader, stderr_writer) = zx::Socket::create_stream();
    // The reader-ends should not write.
    let () = stdout_writer.half_close().expect("stdout_reader.half_close");
    let () = stderr_writer.half_close().expect("stderr_reader.half_close");

    let args: Vec<CString> = std::iter::once(binary_path)
        .chain(args)
        .map(|a| CString::new(a).unwrap_or_else(|e| panic!("failed to parse {a} to CString: {e}")))
        .collect();
    let args: Vec<&CStr> = args.iter().map(|s| s.as_c_str()).collect();

    let mut spawn_actions = vec![];

    let proxy_by_path_cstring: Vec<(CString, &fidl_fuchsia_io::DirectoryProxy)> =
        proxies.into_iter().map(|(path, dir)| (CString::new(path).unwrap(), dir)).collect();
    let proxy_by_path_cstr: Vec<(&CStr, &fidl_fuchsia_io::DirectoryProxy)> =
        proxy_by_path_cstring.iter().map(|(path, dir)| (path.as_c_str(), *dir)).collect();

    for (path, proxy) in proxy_by_path_cstr {
        let (proxy_client_end, proxy_server_end) = fidl::endpoints::create_endpoints();
        fuchsia_fs::directory::clone_onto_no_describe(proxy, None, proxy_server_end).unwrap();
        let proxy_client_channel = proxy_client_end.into_channel();
        spawn_actions
            .push(SpawnAction::add_namespace_entry(path, proxy_client_channel.into_handle()));
    }

    spawn_actions.push(SpawnAction::add_handle(
        HandleInfo::new(
            HandleType::FileDescriptor,
            STDOUT_FILENO.try_into().expect("STDOUT_FILENO.try_into"),
        ),
        stdout_writer.into(),
    ));

    spawn_actions.push(SpawnAction::add_handle(
        HandleInfo::new(
            HandleType::FileDescriptor,
            STDERR_FILENO.try_into().expect("STDERR_FILENO.try_into"),
        ),
        stderr_writer.into(),
    ));

    let process = fdio::spawn_etc(
        /*job=*/ &job_default(),
        /*options=*/ SpawnOptions::DEFAULT_LOADER,
        /*path=*/ CString::new(binary_path).expect("cstring path").as_c_str(),
        /*argv=*/ &args[..],
        /*environ=*/ None,
        /*actions=*/ &mut spawn_actions,
    )
    .expect("spawn process");

    (
        fasync::Task::spawn(async move {
            assert_eq!(
                fasync::OnSignals::new(&process, zx::Signals::PROCESS_TERMINATED)
                    .await
                    .expect("wait for process termination"),
                zx::Signals::PROCESS_TERMINATED
            );
            let ProcessInfo { return_code, start_time: _, flags: _ } =
                process.info().expect("process info");
            return_code
        }),
        fasync::Socket::from_socket(stdout_reader),
        fasync::Socket::from_socket(stderr_reader),
    )
}

/// Runs a binary with some arguments synchronously; returns a struct with exit
/// code, stdout, and stderr.
pub async fn run_process<'a>(
    binary_path: &'a str,
    args: impl IntoIterator<Item = &'a str>,
    proxies: impl IntoIterator<Item = (&'a str, &fidl_fuchsia_io::DirectoryProxy)>,
) -> ProcessOutput {
    let (update, stdout_reader, stderr_reader) =
        run_process_async(binary_path, args, proxies).await;

    let drain = |pipe: fasync::Socket| pipe.into_datagram_stream().try_concat().err_into();

    future::try_join3(
        update.map(Result::<_, anyhow::Error>::Ok),
        drain(stdout_reader),
        drain(stderr_reader),
    )
    .map_ok(|(return_code, stdout, stderr)| ProcessOutput { return_code, stdout, stderr })
    .await
    .unwrap()
}