1use crate::platform::PlatformServices;
6use anyhow::{anyhow, Error};
7use fidl::endpoints::{create_proxy, 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(services, 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 => "dm 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 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 send_stop_rpc<P: PlatformServices>(
112 services: &P,
113 guest: arguments::GuestType,
114) -> Result<(), Error> {
115 assert!(guest == arguments::GuestType::Termina);
116 let linux_manager = services.connect_to_linux_manager().await?;
117 linux_manager
118 .graceful_shutdown()
119 .map_err(|err| anyhow!("failed to send shutdown to termina manager: {}", err))
120}
121
122async fn graceful_stop_guest<P: PlatformServices>(
123 services: &P,
124 guest: arguments::GuestType,
125 manager: GuestManagerProxy,
126) -> Result<StopResult, Error> {
127 let (guest_endpoint, guest_server_end) = create_proxy::<GuestMarker>();
128 manager
129 .connect(guest_server_end)
130 .await
131 .map_err(|err| anyhow!("failed to get a connect response: {}", err))?
132 .map_err(|err| anyhow!("connect failed with: {:?}", err))?;
133
134 match guest {
135 arguments::GuestType::Zircon => {
136 send_stop_shell_command(ShutdownCommand::ZirconShutdownCommand, guest_endpoint.clone())
137 .await
138 }
139 arguments::GuestType::Debian => {
140 send_stop_shell_command(ShutdownCommand::DebianShutdownCommand, guest_endpoint.clone())
141 .await
142 }
143 arguments::GuestType::Termina => send_stop_rpc(services, guest).await,
144 }?;
145
146 let start = fasync::MonotonicInstant::now();
147 println!("Waiting for guest to stop");
148
149 let unresponsive_help_delay =
150 fasync::MonotonicInstant::now() + std::time::Duration::from_secs(10).into();
151 let guest_closed =
152 guest_endpoint.on_closed().on_timeout(unresponsive_help_delay, || Err(Status::TIMED_OUT));
153
154 match guest_closed.await {
155 Ok(_) => Ok(()),
156 Err(Status::TIMED_OUT) => {
157 println!("If the guest is unresponsive, you may force stop it by passing -f");
158 guest_endpoint.on_closed().await.map(|_| ())
159 }
160 Err(err) => Err(err),
161 }
162 .map_err(|err| anyhow!("failed to wait on guest stop signal: {}", err))?;
163
164 let stop_time_nanos = get_time_nanos(fasync::MonotonicInstant::now() - start);
165 Ok(StopResult { status: StopStatus::Graceful, stop_time_nanos })
166}
167
168async fn force_stop_guest(
169 guest: arguments::GuestType,
170 manager: GuestManagerProxy,
171) -> Result<StopResult, Error> {
172 println!("Forcing {} to stop", guest);
173 let start = fasync::MonotonicInstant::now();
174 manager.force_shutdown().await?;
175
176 let stop_time_nanos = get_time_nanos(fasync::MonotonicInstant::now() - start);
177 Ok(StopResult { status: StopStatus::Forced, stop_time_nanos })
178}
179
180fn get_time_nanos(duration: fasync::MonotonicDuration) -> i64 {
181 #[cfg(target_os = "fuchsia")]
182 let nanos = duration.into_nanos();
183
184 #[cfg(not(target_os = "fuchsia"))]
185 let nanos = duration.as_nanos().try_into().unwrap();
186
187 nanos
188}
189
190#[cfg(test)]
191mod test {
192 use super::*;
193 use crate::platform::FuchsiaPlatformServices;
194 use async_utils::PollExt;
195 use fidl::endpoints::create_proxy_and_stream;
196 use fidl::Socket;
197 use fidl_fuchsia_virtualization::GuestManagerMarker;
198 use futures::TryStreamExt;
199
200 #[test]
201 fn graceful_stop_waits_for_shutdown() {
202 let mut executor = fasync::TestExecutor::new_with_fake_time();
203 executor.set_fake_time(fuchsia_async::MonotonicInstant::now());
204
205 let (manager_proxy, mut manager_stream) = create_proxy_and_stream::<GuestManagerMarker>();
206
207 let service = FuchsiaPlatformServices::new();
208 let fut = graceful_stop_guest(&service, arguments::GuestType::Debian, manager_proxy);
209 futures::pin_mut!(fut);
210
211 assert!(executor.run_until_stalled(&mut fut).is_pending());
212
213 let (guest_server_end, responder) = executor
214 .run_until_stalled(&mut manager_stream.try_next())
215 .expect("future should be ready")
216 .unwrap()
217 .unwrap()
218 .into_connect()
219 .expect("received unexpected request on stream");
220
221 responder.send(Ok(())).expect("failed to send response");
222 let mut guest_stream = guest_server_end.into_stream();
223
224 assert!(executor.run_until_stalled(&mut fut).is_pending());
225
226 let responder = executor
227 .run_until_stalled(&mut guest_stream.try_next())
228 .expect("future should be ready")
229 .unwrap()
230 .unwrap()
231 .into_get_console()
232 .expect("received unexpected request on stream");
233
234 let (client, device) = Socket::create_stream();
235 responder.send(Ok(client)).expect("failed to send response");
236
237 assert!(executor.run_until_stalled(&mut fut).is_pending());
238
239 let expected_command = get_graceful_stop_command(ShutdownCommand::DebianShutdownCommand);
240 let mut actual_command = vec![0u8; expected_command.len()];
241 assert_eq!(device.read(actual_command.as_mut_slice()).unwrap(), expected_command.len());
242
243 let duration = std::time::Duration::from_secs(10) + std::time::Duration::from_nanos(1);
245 executor.set_fake_time(fasync::MonotonicInstant::after((duration).into()));
246
247 assert!(executor.run_until_stalled(&mut fut).is_pending());
250
251 drop(guest_stream);
253
254 let result = executor.run_until_stalled(&mut fut).expect("future should be ready").unwrap();
255 assert_eq!(result.status, StopStatus::Graceful);
256 assert_eq!(result.stop_time_nanos, duration.as_nanos() as i64);
257 }
258
259 #[test]
260 fn force_stop_guest_calls_stop_endpoint() {
261 let mut executor = fasync::TestExecutor::new();
262 let (proxy, mut stream) = create_proxy_and_stream::<GuestManagerMarker>();
263
264 let fut = force_stop_guest(arguments::GuestType::Debian, proxy);
265 futures::pin_mut!(fut);
266
267 assert!(executor.run_until_stalled(&mut fut).is_pending());
268
269 let responder = executor
270 .run_until_stalled(&mut stream.try_next())
271 .expect("future should be ready")
272 .unwrap()
273 .unwrap()
274 .into_force_shutdown()
275 .expect("received unexpected request on stream");
276 responder.send().expect("failed to send response");
277
278 let result = executor.run_until_stalled(&mut fut).expect("future should be ready").unwrap();
279 assert_eq!(result.status, StopStatus::Forced);
280 }
281}