Skip to main content

netemul/
guest.rs

1// Copyright 2022 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
5use super::*;
6use fidl::endpoints::Proxy;
7use fidl_fuchsia_io as fio;
8use fidl_fuchsia_netemul_guest as fnetemul_guest;
9use fidl_fuchsia_virtualization_guest_interaction as fguest_interaction;
10use futures_util::io::AsyncReadExt as _;
11
12/// A controller for managing a single virtualized guest.
13///
14/// `Controller` instantiates a guest on creation and exposes
15/// methods for communicating with the guest. The guest lifetime
16/// is tied to the controller's; dropping the controller will shutdown
17/// the guest.
18pub struct Controller {
19    // Option lets us simplify the implementation of `Drop` by taking
20    // the GuestProxy and converting to a SynchronousGuestProxy.
21    guest: Option<fnetemul_guest::GuestProxy>,
22    name: String,
23}
24
25impl<'a> std::fmt::Debug for Controller {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
27        let Self { guest: _, name } = self;
28        f.debug_struct("Controller").field("name", name).finish_non_exhaustive()
29    }
30}
31
32impl Controller {
33    /// Instantiates a guest and installs it on the provided `network`. If `mac` is provided,
34    /// the guest will be given the mac address; otherwise one will be picked by virtio.
35    /// Returns an error if the sandbox already contains a guest.
36    pub async fn new(
37        name: impl Into<String>,
38        network: &TestNetwork<'_>,
39        mac: Option<fnet::MacAddress>,
40    ) -> Result<Controller> {
41        let name = name.into();
42        let controller_proxy =
43            fuchsia_component::client::connect_to_protocol::<fnetemul_guest::ControllerMarker>()
44                .with_context(|| {
45                    format!("failed to connect to guest controller protocol for guest {}", name)
46                })?;
47
48        let network_client =
49            network.get_client_end_clone().await.context("failed to get network client end")?;
50        let guest = controller_proxy
51            .create_guest(&name, network_client, mac.as_ref())
52            .await
53            .with_context(|| format!("create_guest FIDL error for guest {}", name))?
54            .map_err(|err| {
55                anyhow::anyhow!(format!("create guest error for guest {}: {:?}", name, err))
56            })?;
57        Ok(Controller { guest: Some(guest.into_proxy()), name })
58    }
59
60    fn proxy(&self) -> &fnetemul_guest::GuestProxy {
61        self.guest.as_ref().expect("guest_proxy was empty")
62    }
63
64    /// Copies the file located at `local_path` within the namespace of the executing process
65    /// to `remote_path` on the guest.
66    pub async fn put_file(&self, local_path: &str, remote_path: &str) -> Result {
67        let (file_client_end, file_server_end) =
68            fidl::endpoints::create_endpoints::<fio::FileMarker>();
69        fdio::open(&local_path, fio::PERM_READABLE, file_server_end.into_channel())
70            .with_context(|| format!("failed to open file '{}'", local_path))?;
71        let status = self
72            .proxy()
73            .put_file(file_client_end, remote_path)
74            .await
75            .with_context(|| format!("put_file FIDL error for guest {}", self.name))?;
76        zx::Status::ok(status).with_context(|| {
77            format!(
78                "put_file for guest {} failed for file at local path {} and remote path {}",
79                self.name, local_path, remote_path
80            )
81        })
82    }
83
84    /// Copies the file located at `remote_path` on the guest to `local_path` within the
85    /// namespace of the current process.
86    pub async fn get_file(&self, local_path: &str, remote_path: &str) -> Result {
87        let (file_client_end, file_server_end) =
88            fidl::endpoints::create_endpoints::<fio::FileMarker>();
89        fdio::open(
90            &local_path,
91            fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE,
92            file_server_end.into_channel(),
93        )
94        .with_context(|| format!("failed to open file '{}'", local_path))?;
95        let status = self
96            .proxy()
97            .get_file(remote_path, file_client_end)
98            .await
99            .with_context(|| format!("get_file FIDL error for guest {}", self.name))?;
100        zx::Status::ok(status).with_context(|| {
101            format!(
102                "get_file for guest {} failed for file at local path {} and remote path {}",
103                self.name, local_path, remote_path
104            )
105        })
106    }
107
108    /// Executes `command` on the guest with environment variables held in
109    /// `env`, writing `input` into the remote process's `stdin` and logs
110    /// the remote process's stdout and stderr.
111    ///
112    /// Returns an error if the executed command's exit code is non-zero.
113    pub async fn exec_with_output_logged(
114        &self,
115        command: &str,
116        env: Vec<fguest_interaction::EnvironmentVariable>,
117        input: Option<&str>,
118    ) -> Result<()> {
119        let (return_code, stdout, stderr) = self.exec(command, env, input).await?;
120        log::info!(
121            "command `{}` for guest {} output\nstdout: {}\nstderr: {}",
122            command,
123            self.name,
124            stdout,
125            stderr
126        );
127        if return_code != 0 {
128            return Err(anyhow!(
129                "command `{}` for guest {} failed with return code: {}",
130                command,
131                self.name,
132                return_code,
133            ));
134        }
135        Ok(())
136    }
137
138    /// Executes `command` on the guest with environment variables held in `env`, writing
139    /// `input` into the remote process's `stdin` and returning the remote process's
140    /// (stdout, stderr).
141    pub async fn exec(
142        &self,
143        command: &str,
144        env: Vec<fguest_interaction::EnvironmentVariable>,
145        input: Option<&str>,
146    ) -> Result<(i32, String, String)> {
147        let (stdout_local, stdout_remote) = zx::Socket::create_stream();
148        let (stderr_local, stderr_remote) = zx::Socket::create_stream();
149
150        let (command_listener_client, command_listener_server) =
151            fidl::endpoints::create_proxy::<fguest_interaction::CommandListenerMarker>();
152        let (stdin_local, stdin_remote) = match input {
153            Some(input) => {
154                let (stdin_local, stdin_remote) = zx::Socket::create_stream();
155                (Some((stdin_local, input)), Some(stdin_remote))
156            }
157            None => (None, None),
158        };
159        self.proxy()
160            .execute_command(
161                command,
162                &env,
163                stdin_remote,
164                Some(stdout_remote),
165                Some(stderr_remote),
166                command_listener_server,
167            )
168            .with_context(|| format!("execute_command FIDL error for guest {}", self.name))?;
169
170        let mut async_stdout = fuchsia_async::Socket::from_socket(stdout_local);
171        let mut async_stderr = fuchsia_async::Socket::from_socket(stderr_local);
172
173        let mut stdout_buf = Vec::new();
174        let mut stderr_buf = Vec::new();
175
176        let stdout_fut = pin!(
177            async_stdout
178                .read_to_end(&mut stdout_buf)
179                .map(|res| res.context("failed to read from stdout"))
180                .fuse()
181        );
182        let stderr_fut = pin!(
183            async {
184                async_stderr
185                    .read_to_end(&mut stderr_buf)
186                    .await
187                    .context("failed to read from socket")
188            }
189            .fuse()
190        );
191
192        let mut command_listener_stream = command_listener_client.take_event_stream();
193        let listener_fut = pin!(
194            async {
195                loop {
196                    let event = command_listener_stream
197                        .try_next()
198                        .await
199                        .with_context(|| {
200                            format!(
201                                "failed to get next CommandListenerEvent for guest {}",
202                                self.name
203                            )
204                        })?
205                        .with_context(|| {
206                            format!("empty CommandListenerEvent for guest {}", self.name)
207                        })?;
208                    match event {
209                        fguest_interaction::CommandListenerEvent::OnStarted { status } => {
210                            zx::Status::ok(status).with_context(|| {
211                                format!(
212                                    "error starting exec for guest {} and command {}",
213                                    self.name, command
214                                )
215                            })?;
216
217                            if let Some((stdin_local, to_write)) = stdin_local.as_ref() {
218                                assert_eq!(
219                                    stdin_local.write(to_write.as_bytes())?,
220                                    to_write.as_bytes().len()
221                                );
222                            }
223                        }
224                        fguest_interaction::CommandListenerEvent::OnTerminated {
225                            status,
226                            return_code,
227                        } => {
228                            zx::Status::ok(status).with_context(|| {
229                                format!(
230                                    "error returning from exec for guest {} and command {}",
231                                    self.name, command
232                                )
233                            })?;
234
235                            return Ok(return_code);
236                        }
237                    }
238                }
239            }
240            .fuse()
241        );
242
243        // Scope required to limit the lifetime of pinned futures.
244        let return_code = {
245            // Poll the stdout and stderr sockets in parallel while waiting for the remote
246            // process to terminate. This avoids deadlock in case the remote process blocks
247            // on writing to stdout/stderr.
248            let (_, return_code, _): (usize, _, usize) =
249                futures::try_join!(stderr_fut, listener_fut, stdout_fut)?;
250            return_code
251        };
252
253        let stdout = String::from_utf8(stdout_buf).context("failed to convert stdout to string")?;
254        let stderr = String::from_utf8(stderr_buf).context("failed to convert stderr to string")?;
255
256        Ok((return_code, stdout, stderr))
257    }
258}
259
260impl Drop for Controller {
261    fn drop(&mut self) {
262        let guest = fnetemul_guest::GuestSynchronousProxy::new(
263            self.guest
264                .take()
265                .expect("guest proxy was empty")
266                .into_channel()
267                .expect("failed to convert to FIDL channel")
268                .into_zx_channel(),
269        );
270
271        guest.shutdown(zx::MonotonicInstant::INFINITE).expect("shutdown FIDL error");
272    }
273}