Skip to main content

guest_cli/
stop.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 crate::platform::PlatformServices;
6use anyhow::{Error, anyhow};
7use fidl::endpoints::{Proxy, create_proxy};
8use fidl_fuchsia_virtualization::{GuestManagerProxy, GuestMarker, GuestProxy, GuestStatus};
9use fuchsia_async::{self as fasync, TimeoutExt};
10use guest_cli_args as arguments;
11use std::fmt;
12use zx_status::Status;
13
14#[derive(Default, serde::Serialize, serde::Deserialize, PartialEq, Debug)]
15pub enum StopStatus {
16    #[default]
17    NotStopped,
18    NotRunning,
19    Forced,
20    Graceful,
21}
22
23#[derive(Default, serde::Serialize, serde::Deserialize)]
24pub struct StopResult {
25    pub status: StopStatus,
26    pub stop_time_nanos: i64,
27}
28
29impl fmt::Display for StopResult {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        let time_to_str = |nanos: i64| -> String {
32            let duration = std::time::Duration::from_nanos(nanos as u64);
33            if duration.as_millis() > 1 {
34                format!("{}ms", duration.as_millis())
35            } else {
36                format!("{}μs", duration.as_micros())
37            }
38        };
39
40        match self.status {
41            StopStatus::NotStopped => write!(f, "Failed to stop guest"),
42            StopStatus::NotRunning => write!(f, "Nothing to do - the guest is not running"),
43            StopStatus::Forced => {
44                write!(f, "Guest forced to stop in {}", time_to_str(self.stop_time_nanos))
45            }
46            StopStatus::Graceful => {
47                write!(f, "Guest finished stopping in {}", time_to_str(self.stop_time_nanos))
48            }
49        }
50    }
51}
52
53enum ShutdownCommand {
54    DebianShutdownCommand,
55    ZirconShutdownCommand,
56}
57
58pub async fn handle_stop<P: PlatformServices>(
59    services: &P,
60    args: &arguments::stop_args::StopArgs,
61) -> Result<StopResult, Error> {
62    let manager = services.connect_to_manager(args.guest_type).await?;
63    let status = manager.get_info().await?.guest_status.expect("guest status should always be set");
64    if status != GuestStatus::Starting && status != GuestStatus::Running {
65        return Ok(StopResult { status: StopStatus::NotRunning, ..StopResult::default() });
66    }
67
68    if args.force {
69        force_stop_guest(args.guest_type, manager).await
70    } else {
71        graceful_stop_guest(args.guest_type, manager).await
72    }
73}
74
75fn get_graceful_stop_command(guest_cmd: ShutdownCommand) -> Vec<u8> {
76    let arg_string = match guest_cmd {
77        ShutdownCommand::ZirconShutdownCommand => "power shutdown\n".to_string(),
78        ShutdownCommand::DebianShutdownCommand => "shutdown now\n".to_string(),
79    };
80
81    arg_string.into_bytes()
82}
83
84async fn send_stop_shell_command(
85    guest_cmd: ShutdownCommand,
86    guest_endpoint: GuestProxy,
87) -> Result<(), Error> {
88    // TODO(https://fxbug.dev/42062425): Use a different console for sending the stop command.
89    let socket = guest_endpoint
90        .get_console()
91        .await
92        .map_err(|err| anyhow!("failed to get a get_console response: {}", err))?
93        .map_err(|err| anyhow!("get_console failed with: {:?}", err))?;
94
95    println!("Sending stop command to guest");
96    let command = get_graceful_stop_command(guest_cmd);
97    let bytes_written = socket
98        .write(&command)
99        .map_err(|err| anyhow!("failed to write command to socket: {}", err))?;
100    if bytes_written != command.len() {
101        return Err(anyhow!(
102            "attempted to send command '{}', but only managed to write '{}'",
103            std::str::from_utf8(&command).expect("failed to parse as utf-8"),
104            std::str::from_utf8(&command[0..bytes_written]).expect("failed to parse as utf-8")
105        ));
106    }
107
108    Ok(())
109}
110
111async fn graceful_stop_guest(
112    guest: arguments::GuestType,
113    manager: GuestManagerProxy,
114) -> Result<StopResult, Error> {
115    let (guest_endpoint, guest_server_end) = create_proxy::<GuestMarker>();
116    manager
117        .connect(guest_server_end)
118        .await
119        .map_err(|err| anyhow!("failed to get a connect response: {}", err))?
120        .map_err(|err| anyhow!("connect failed with: {:?}", err))?;
121
122    match guest {
123        arguments::GuestType::Zircon => {
124            send_stop_shell_command(ShutdownCommand::ZirconShutdownCommand, guest_endpoint.clone())
125                .await
126        }
127        arguments::GuestType::Debian => {
128            send_stop_shell_command(ShutdownCommand::DebianShutdownCommand, guest_endpoint.clone())
129                .await
130        }
131    }?;
132
133    let start = fasync::MonotonicInstant::now();
134    println!("Waiting for guest to stop");
135
136    let unresponsive_help_delay =
137        fasync::MonotonicInstant::now() + std::time::Duration::from_secs(10).into();
138    let guest_closed =
139        guest_endpoint.on_closed().on_timeout(unresponsive_help_delay, || Err(Status::TIMED_OUT));
140
141    match guest_closed.await {
142        Ok(_) => Ok(()),
143        Err(Status::TIMED_OUT) => {
144            println!("If the guest is unresponsive, you may force stop it by passing -f");
145            guest_endpoint.on_closed().await.map(|_| ())
146        }
147        Err(err) => Err(err),
148    }
149    .map_err(|err| anyhow!("failed to wait on guest stop signal: {}", err))?;
150
151    let stop_time_nanos = get_time_nanos(fasync::MonotonicInstant::now() - start);
152    Ok(StopResult { status: StopStatus::Graceful, stop_time_nanos })
153}
154
155async fn force_stop_guest(
156    guest: arguments::GuestType,
157    manager: GuestManagerProxy,
158) -> Result<StopResult, Error> {
159    println!("Forcing {} to stop", guest);
160    let start = fasync::MonotonicInstant::now();
161    manager.force_shutdown().await?;
162
163    let stop_time_nanos = get_time_nanos(fasync::MonotonicInstant::now() - start);
164    Ok(StopResult { status: StopStatus::Forced, stop_time_nanos })
165}
166
167fn get_time_nanos(duration: fasync::MonotonicDuration) -> i64 {
168    #[cfg(target_os = "fuchsia")]
169    let nanos = duration.into_nanos();
170
171    #[cfg(not(target_os = "fuchsia"))]
172    let nanos = duration.as_nanos().try_into().unwrap();
173
174    nanos
175}
176
177#[cfg(test)]
178mod test {
179    use super::*;
180    use async_utils::PollExt;
181    use fidl::Socket;
182    use fidl::endpoints::create_proxy_and_stream;
183    use fidl_fuchsia_virtualization::GuestManagerMarker;
184    use futures::TryStreamExt;
185
186    #[test]
187    fn graceful_stop_waits_for_shutdown() {
188        let mut executor = fasync::TestExecutor::new_with_fake_time();
189        executor.set_fake_time(fuchsia_async::MonotonicInstant::now());
190
191        let (manager_proxy, mut manager_stream) = create_proxy_and_stream::<GuestManagerMarker>();
192
193        let fut = graceful_stop_guest(arguments::GuestType::Debian, manager_proxy);
194        futures::pin_mut!(fut);
195
196        assert!(executor.run_until_stalled(&mut fut).is_pending());
197
198        let (guest_server_end, responder) = executor
199            .run_until_stalled(&mut manager_stream.try_next())
200            .expect("future should be ready")
201            .unwrap()
202            .unwrap()
203            .into_connect()
204            .expect("received unexpected request on stream");
205
206        responder.send(Ok(())).expect("failed to send response");
207        let mut guest_stream = guest_server_end.into_stream();
208
209        assert!(executor.run_until_stalled(&mut fut).is_pending());
210
211        let responder = executor
212            .run_until_stalled(&mut guest_stream.try_next())
213            .expect("future should be ready")
214            .unwrap()
215            .unwrap()
216            .into_get_console()
217            .expect("received unexpected request on stream");
218
219        let (client, device) = Socket::create_stream();
220        responder.send(Ok(client)).expect("failed to send response");
221
222        assert!(executor.run_until_stalled(&mut fut).is_pending());
223
224        let expected_command = get_graceful_stop_command(ShutdownCommand::DebianShutdownCommand);
225        let mut actual_command = vec![0u8; expected_command.len()];
226        assert_eq!(device.read(actual_command.as_mut_slice()).unwrap(), expected_command.len());
227
228        // One nano past the helpful message timeout.
229        let duration = std::time::Duration::from_secs(10) + std::time::Duration::from_nanos(1);
230        executor.set_fake_time(fasync::MonotonicInstant::after((duration).into()));
231
232        // Waiting for CHANNEL_PEER_CLOSED timed out (printing the helpful message), but then
233        // a new indefinite wait began as the channel is still not closed.
234        assert!(executor.run_until_stalled(&mut fut).is_pending());
235
236        // Send a CHANNEL_PEER_CLOSED to the guest proxy.
237        drop(guest_stream);
238
239        let result = executor.run_until_stalled(&mut fut).expect("future should be ready").unwrap();
240        assert_eq!(result.status, StopStatus::Graceful);
241        assert_eq!(result.stop_time_nanos, duration.as_nanos() as i64);
242    }
243
244    #[test]
245    fn force_stop_guest_calls_stop_endpoint() {
246        let mut executor = fasync::TestExecutor::new();
247        let (proxy, mut stream) = create_proxy_and_stream::<GuestManagerMarker>();
248
249        let fut = force_stop_guest(arguments::GuestType::Debian, proxy);
250        futures::pin_mut!(fut);
251
252        assert!(executor.run_until_stalled(&mut fut).is_pending());
253
254        let responder = executor
255            .run_until_stalled(&mut stream.try_next())
256            .expect("future should be ready")
257            .unwrap()
258            .unwrap()
259            .into_force_shutdown()
260            .expect("received unexpected request on stream");
261        responder.send().expect("failed to send response");
262
263        let result = executor.run_until_stalled(&mut fut).expect("future should be ready").unwrap();
264        assert_eq!(result.status, StopStatus::Forced);
265    }
266}