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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// 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.

use super::*;
use fidl::endpoints::Proxy;
use fidl_fuchsia_io as fio;
use fidl_fuchsia_netemul_guest as fnetemul_guest;
use fidl_fuchsia_virtualization_guest_interaction as fguest_interaction;
use futures_util::io::AsyncReadExt as _;

/// A controller for managing a single virtualized guest.
///
/// `Controller` instantiates a guest on creation and exposes
/// methods for communicating with the guest. The guest lifetime
/// is tied to the controller's; dropping the controller will shutdown
/// the guest.
pub struct Controller {
    // Option lets us simplify the implementation of `Drop` by taking
    // the GuestProxy and converting to a SynchronousGuestProxy.
    guest: Option<fnetemul_guest::GuestProxy>,
    name: String,
}

impl<'a> std::fmt::Debug for Controller {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
        let Self { guest: _, name } = self;
        f.debug_struct("Controller").field("name", name).finish_non_exhaustive()
    }
}

impl Controller {
    /// Instantiates a guest and installs it on the provided `network`. If `mac` is provided,
    /// the guest will be given the mac address; otherwise one will be picked by virtio.
    /// Returns an error if the sandbox already contains a guest.
    pub async fn new(
        name: impl Into<String>,
        network: &TestNetwork<'_>,
        mac: Option<fnet::MacAddress>,
    ) -> Result<Controller> {
        let name = name.into();
        let controller_proxy =
            fuchsia_component::client::connect_to_protocol::<fnetemul_guest::ControllerMarker>()
                .with_context(|| {
                    format!("failed to connect to guest controller protocol for guest {}", name)
                })?;

        let network_client =
            network.get_client_end_clone().await.context("failed to get network client end")?;
        let guest = controller_proxy
            .create_guest(&name, network_client, mac.as_ref())
            .await
            .with_context(|| format!("create_guest FIDL error for guest {}", name))?
            .map_err(|err| {
                anyhow::anyhow!(format!("create guest error for guest {}: {:?}", name, err))
            })?;
        Ok(Controller {
            guest: Some(guest.into_proxy().context("failed to convert guest to proxy")?),
            name,
        })
    }

    fn proxy(&self) -> &fnetemul_guest::GuestProxy {
        self.guest.as_ref().expect("guest_proxy was empty")
    }

    /// Copies the file located at `local_path` within the namespace of the executing process
    /// to `remote_path` on the guest.
    pub async fn put_file(&self, local_path: &str, remote_path: &str) -> Result {
        let (file_client_end, file_server_end) =
            fidl::endpoints::create_endpoints::<fio::FileMarker>();
        fdio::open(&local_path, fio::OpenFlags::RIGHT_READABLE, file_server_end.into_channel())
            .with_context(|| format!("failed to open file '{}'", local_path))?;
        let status = self
            .proxy()
            .put_file(file_client_end, remote_path)
            .await
            .with_context(|| format!("put_file FIDL error for guest {}", self.name))?;
        zx::Status::ok(status).with_context(|| {
            format!(
                "put_file for guest {} failed for file at local path {} and remote path {}",
                self.name, local_path, remote_path
            )
        })
    }

    /// Copies the file located at `remote_path` on the guest to `local_path` within the
    /// namespace of the current process.
    pub async fn get_file(&self, local_path: &str, remote_path: &str) -> Result {
        let (file_client_end, file_server_end) =
            fidl::endpoints::create_endpoints::<fio::FileMarker>();
        fdio::open(
            &local_path,
            fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE,
            file_server_end.into_channel(),
        )
        .with_context(|| format!("failed to open file '{}'", local_path))?;
        let status = self
            .proxy()
            .get_file(remote_path, file_client_end)
            .await
            .with_context(|| format!("get_file FIDL error for guest {}", self.name))?;
        zx::Status::ok(status).with_context(|| {
            format!(
                "get_file for guest {} failed for file at local path {} and remote path {}",
                self.name, local_path, remote_path
            )
        })
    }

    /// Executes `command` on the guest with environment variables held in
    /// `env`, writing `input` into the remote process's `stdin` and logs
    /// the remote process's stdout and stderr.
    ///
    /// Returns an error if the executed command's exit code is non-zero.
    pub async fn exec_with_output_logged(
        &self,
        command: &str,
        env: Vec<fguest_interaction::EnvironmentVariable>,
        input: Option<&str>,
    ) -> Result<()> {
        let (return_code, stdout, stderr) = self.exec(command, env, input).await?;
        tracing::info!(
            "command `{}` for guest {} output\nstdout: {}\nstderr: {}",
            command,
            self.name,
            stdout,
            stderr
        );
        if return_code != 0 {
            return Err(anyhow!(
                "command `{}` for guest {} failed with return code: {}",
                command,
                self.name,
                return_code,
            ));
        }
        Ok(())
    }

    /// Executes `command` on the guest with environment variables held in `env`, writing
    /// `input` into the remote process's `stdin` and returning the remote process's
    /// (stdout, stderr).
    pub async fn exec(
        &self,
        command: &str,
        env: Vec<fguest_interaction::EnvironmentVariable>,
        input: Option<&str>,
    ) -> Result<(i32, String, String)> {
        let (stdout_local, stdout_remote) = zx::Socket::create_stream();
        let (stderr_local, stderr_remote) = zx::Socket::create_stream();

        let (command_listener_client, command_listener_server) =
            fidl::endpoints::create_proxy::<fguest_interaction::CommandListenerMarker>()
                .context("failed to create CommandListener proxy")?;
        let (stdin_local, stdin_remote) = match input {
            Some(input) => {
                let (stdin_local, stdin_remote) = zx::Socket::create_stream();
                (Some((stdin_local, input)), Some(stdin_remote))
            }
            None => (None, None),
        };
        let () = self
            .proxy()
            .execute_command(
                command,
                &env,
                stdin_remote,
                Some(stdout_remote),
                Some(stderr_remote),
                command_listener_server,
            )
            .with_context(|| format!("execute_command FIDL error for guest {}", self.name))?;

        let mut async_stdout = fuchsia_async::Socket::from_socket(stdout_local);
        let mut async_stderr = fuchsia_async::Socket::from_socket(stderr_local);

        let mut stdout_buf = Vec::new();
        let mut stderr_buf = Vec::new();

        let stdout_fut = pin!(async_stdout
            .read_to_end(&mut stdout_buf)
            .map(|res| res.context("failed to read from stdout"))
            .fuse());
        let stderr_fut = pin!(async {
            async_stderr.read_to_end(&mut stderr_buf).await.context("failed to read from socket")
        }
        .fuse());

        let mut command_listener_stream = command_listener_client.take_event_stream();
        let listener_fut = pin!(async {
            loop {
                let event = command_listener_stream
                    .try_next()
                    .await
                    .with_context(|| {
                        format!("failed to get next CommandListenerEvent for guest {}", self.name)
                    })?
                    .with_context(|| {
                        format!("empty CommandListenerEvent for guest {}", self.name)
                    })?;
                match event {
                    fguest_interaction::CommandListenerEvent::OnStarted { status } => {
                        let () = zx::Status::ok(status).with_context(|| {
                            format!(
                                "error starting exec for guest {} and command {}",
                                self.name, command
                            )
                        })?;

                        if let Some((stdin_local, to_write)) = stdin_local.as_ref() {
                            assert_eq!(
                                stdin_local.write(to_write.as_bytes())?,
                                to_write.as_bytes().len()
                            );
                        }
                    }
                    fguest_interaction::CommandListenerEvent::OnTerminated {
                        status,
                        return_code,
                    } => {
                        let () = zx::Status::ok(status).with_context(|| {
                            format!(
                                "error returning from exec for guest {} and command {}",
                                self.name, command
                            )
                        })?;

                        return Ok(return_code);
                    }
                }
            }
        }
        .fuse());

        // Scope required to limit the lifetime of pinned futures.
        let return_code = {
            // Poll the stdout and stderr sockets in parallel while waiting for the remote
            // process to terminate. This avoids deadlock in case the remote process blocks
            // on writing to stdout/stderr.
            let (_, return_code, _): (usize, _, usize) =
                futures::try_join!(stderr_fut, listener_fut, stdout_fut)?;
            return_code
        };

        let stdout = String::from_utf8(stdout_buf).context("failed to convert stdout to string")?;
        let stderr = String::from_utf8(stderr_buf).context("failed to convert stderr to string")?;

        Ok((return_code, stdout, stderr))
    }
}

impl Drop for Controller {
    fn drop(&mut self) {
        let guest = fnetemul_guest::GuestSynchronousProxy::new(
            self.guest
                .take()
                .expect("guest proxy was empty")
                .into_channel()
                .expect("failed to convert to FIDL channel")
                .into_zx_channel(),
        );

        let () = guest.shutdown(zx::Time::INFINITE).expect("shutdown FIDL error");
    }
}