1use crate::platform::PlatformServices;
6use anyhow::{anyhow, Error};
7use fidl_fuchsia_virtualization::{GuestManagerProxy, GuestStatus};
8use guest_cli_args as arguments;
9use prettytable::format::consts::FORMAT_CLEAN;
10use prettytable::{cell, row, Table};
11use std::fmt;
12
13fn guest_status_to_string(status: GuestStatus) -> &'static str {
14 match status {
15 GuestStatus::NotStarted => "Not started",
16 GuestStatus::Starting => "Starting",
17 GuestStatus::Running => "Running",
18 GuestStatus::Stopping => "Stopping",
19 GuestStatus::Stopped => "Stopped",
20 GuestStatus::VmmUnexpectedTermination => "VMM Unexpectedly Terminated",
21 }
22}
23
24fn uptime_to_string(uptime_nanos: Option<i64>) -> String {
25 match uptime_nanos {
26 Some(uptime) => {
27 if uptime < 0 {
28 "Invalid negative uptime!".to_string()
29 } else {
30 let uptime = std::time::Duration::from_nanos(uptime as u64);
31 let seconds = uptime.as_secs() % 60;
32 let minutes = (uptime.as_secs() / 60) % 60;
33 let hours = uptime.as_secs() / 3600;
34 format!("{:0>2}:{:0>2}:{:0>2} HH:MM:SS", hours, minutes, seconds)
35 }
36 }
37 None => "--:--:-- HH:MM:SS".to_string(),
38 }
39}
40
41#[derive(Default, serde::Serialize, serde::Deserialize)]
42pub struct GuestDetails {
43 pub package_url: String,
44 pub status: String,
45 pub uptime_nanos: i64,
46 pub stop_reason: Option<String>,
47 pub cpu_count: Option<u8>,
48 pub memory_bytes: Option<u64>,
49 pub device_counts: Vec<(String, u32)>,
50 pub problems: Vec<String>,
51}
52
53impl fmt::Display for GuestDetails {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 let mut table = Table::new();
56 table.set_format(*FORMAT_CLEAN);
57 table.add_row(row!["Guest package:", self.package_url]);
58 table.add_row(row!["Guest status:", self.status]);
59 table.add_row(row!["Guest uptime:", uptime_to_string(Some(self.uptime_nanos))]);
60
61 if self.status == "Not started" {
62 write!(f, "{}", table)?;
63 return Ok(());
64 }
65
66 table.add_empty_row();
67
68 if let Some(stop_reason) = &self.stop_reason {
69 table.add_row(row!["Stop reason:", stop_reason]);
70 }
71 if let Some(cpu_count) = self.cpu_count {
72 table.add_row(row!["CPU count:", cpu_count]);
73 }
74 if let Some(memory_bytes) = self.memory_bytes {
75 let gib = f64::trunc(memory_bytes as f64 / (1 << 30) as f64) * 100.0 / 100.0;
76 table.add_row(row!["Guest memory:", format!("{} GiB ({} bytes)", gib, memory_bytes)]);
77 }
78
79 if self.cpu_count.is_some() && self.memory_bytes.is_some() {
80 table.add_empty_row();
81
82 let mut active = Table::new();
83 active.set_format(*FORMAT_CLEAN);
84 let mut inactive = Table::new();
85 inactive.set_format(*FORMAT_CLEAN);
86
87 for (device, count) in self.device_counts.iter() {
88 if *count == 0 {
89 inactive.add_row(row![device]);
90 } else if *count == 1 {
91 active.add_row(row![device]);
92 } else {
93 active.add_row(row![format!("{} ({} devices)", device, *count)]);
94 }
95 }
96
97 if active.len() == 0 {
98 active.add_row(row!["None"]);
99 }
100
101 if inactive.len() == 0 {
102 inactive.add_row(row!["None"]);
103 }
104
105 table.add_row(row!["Active devices:", active]);
106 table.add_empty_row();
107 table.add_row(row!["Inactive devices:", inactive]);
108 }
109 write!(f, "{}", table)?;
110
111 if !self.problems.is_empty() {
112 let mut problem_table = Table::new();
113 problem_table.set_format(*FORMAT_CLEAN);
114 problem_table.add_empty_row();
115 problem_table.add_row(row![
116 format!(
117 "{} problem{} detected:",
118 self.problems.len(),
119 if self.problems.len() > 1 { "s" } else { "" }
120 ),
121 " "
122 ]);
123 for problem in self.problems.iter() {
124 problem_table.add_row(row![format!("* {}", problem), " "]);
125 }
126 write!(f, "{}", problem_table)?;
127 }
128 return Ok(());
129 }
130}
131
132async fn get_detailed_information(
133 guest_type: arguments::GuestType,
134 manager: GuestManagerProxy,
135) -> Result<GuestDetails, Error> {
136 let guest_info = manager.get_info().await;
137 if let Err(_) = guest_info {
138 return Err(anyhow!("Failed to query guest information: {}", guest_type.to_string()));
139 }
140 let guest_info = guest_info.unwrap();
141 let guest_status = guest_info.guest_status.expect("guest status should always be set");
142
143 let mut details: GuestDetails = Default::default();
144 details.package_url = guest_type.package_url().to_string();
145 details.status = guest_status_to_string(guest_status).to_string();
146 details.uptime_nanos = guest_info.uptime.unwrap_or(0);
147
148 if guest_status == GuestStatus::NotStarted {
149 return Ok(details);
150 }
151
152 if guest_status == GuestStatus::Stopped {
153 let stop_reason = guest_info
154 .stop_error
155 .map_or_else(|| "Clean shutdown".to_string(), |err| format!("{:?}", err));
156 details.stop_reason = Some(stop_reason);
157 } else {
158 if let Some(config) = guest_info.guest_descriptor {
159 details.cpu_count = config.num_cpus;
160 details.memory_bytes = config.guest_memory;
161
162 let add_to_table =
163 |device: &str, is_active: Option<bool>, table: &mut Vec<(String, u32)>| -> () {
164 let count = is_active.map(|b| b as u32).unwrap_or(0);
165 table.push((device.to_string(), count));
166 };
167
168 add_to_table("balloon", config.balloon, &mut details.device_counts);
169 add_to_table("console", config.console, &mut details.device_counts);
170 add_to_table("gpu", config.gpu, &mut details.device_counts);
171 add_to_table("rng", config.rng, &mut details.device_counts);
172 add_to_table("vsock", config.vsock, &mut details.device_counts);
173 add_to_table("sound", config.sound, &mut details.device_counts);
174 let networks = config.networks.map(|v| v.len()).unwrap_or(0);
175 details.device_counts.push(("network".to_string(), networks as u32));
176 }
177 }
178
179 if let Some(problems) = guest_info.detected_problems {
180 details.problems = problems;
181 }
182 Ok(details)
183}
184
185#[derive(serde::Serialize, serde::Deserialize)]
186pub struct GuestOverview {
187 name: String,
188 status: String,
189 uptime_nanos: Option<i64>,
190}
191
192#[derive(serde::Serialize, serde::Deserialize)]
193pub struct GuestSummary {
194 guests: Vec<GuestOverview>,
195}
196
197impl fmt::Display for GuestSummary {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 let mut table = Table::new();
200 table.set_titles(row!["Guest", "Status", "Uptime"]);
201 for guest in &self.guests {
202 table.add_row(row![guest.name, guest.status, uptime_to_string(guest.uptime_nanos)]);
203 }
204 write!(f, "{}", table)
205 }
206}
207
208async fn get_environment_summary(
209 managers: Vec<(String, GuestManagerProxy)>,
210) -> Result<GuestSummary, Error> {
211 let mut summary = GuestSummary { guests: Vec::new() };
212 for (name, manager) in managers {
213 match manager.get_info().await {
214 Ok(guest_info) => summary.guests.push(GuestOverview {
215 name,
216 status: guest_status_to_string(
217 guest_info.guest_status.expect("guest status should always be set"),
218 )
219 .to_string(),
220 uptime_nanos: guest_info.uptime,
221 }),
222 Err(_) => summary.guests.push(GuestOverview {
223 name,
224 status: "Unavailable".to_string(),
225 uptime_nanos: None,
226 }),
227 }
228 }
229 Ok(summary)
230}
231
232#[derive(serde::Serialize, serde::Deserialize)]
233pub enum GuestList {
234 Summary(GuestSummary),
235 Details(GuestDetails),
236}
237
238impl fmt::Display for GuestList {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 match self {
241 GuestList::Summary(summary) => write!(f, "{}", summary)?,
242 GuestList::Details(details) => write!(f, "{}", details)?,
243 }
244 Ok(())
245 }
246}
247
248pub async fn handle_list<P: PlatformServices>(
249 services: &P,
250 args: &arguments::list_args::ListArgs,
251) -> Result<GuestList, Error> {
252 match args.guest_type {
253 Some(guest_type) => {
254 let manager = services.connect_to_manager(guest_type).await?;
255 Ok(GuestList::Details(get_detailed_information(guest_type, manager).await?))
256 }
257 None => {
258 let mut managers = Vec::new();
259 for guest_type in arguments::GuestType::all_guests() {
260 let manager = services.connect_to_manager(guest_type).await?;
261 managers.push((guest_type.to_string(), manager));
262 }
263 Ok(GuestList::Summary(get_environment_summary(managers).await?))
264 }
265 }
266}
267
268#[cfg(test)]
269mod test {
270 use super::*;
271 use fidl::endpoints::create_proxy_and_stream;
272 use fidl_fuchsia_net::MacAddress;
273 use fidl_fuchsia_virtualization::{
274 GuestDescriptor, GuestError, GuestInfo, GuestManagerMarker, NetSpec,
275 };
276 use fuchsia_async as fasync;
277 use futures::StreamExt;
278
279 fn serve_mock_manager(response: Option<GuestInfo>) -> GuestManagerProxy {
280 let (proxy, mut stream) = create_proxy_and_stream::<GuestManagerMarker>();
281 fasync::Task::local(async move {
282 let responder = stream
283 .next()
284 .await
285 .expect("mock manager expected a request")
286 .unwrap()
287 .into_get_info()
288 .expect("unexpected call to mock manager");
289
290 if let Some(guest_info) = response {
291 responder.send(&guest_info).expect("failed to send mock response");
292 } else {
293 drop(responder);
294 }
295 })
296 .detach();
297
298 proxy
299 }
300
301 #[fasync::run_until_stalled(test)]
302 async fn negative_uptime() {
303 let duration = -5;
306 let actual = uptime_to_string(Some(duration));
307 let expected = "Invalid negative uptime!";
308
309 assert_eq!(actual, expected);
310 }
311
312 #[fasync::run_until_stalled(test)]
313 async fn very_large_uptime() {
314 let hours = std::time::Duration::from_secs(123 * 60 * 60);
315 let minutes = std::time::Duration::from_secs(45 * 60);
316 let seconds = std::time::Duration::from_secs(54);
317 let duration = hours + minutes + seconds;
318
319 let actual = uptime_to_string(Some(duration.as_nanos() as i64));
320 let expected = "123:45:54 HH:MM:SS";
321
322 assert_eq!(actual, expected);
323 }
324
325 #[fasync::run_until_stalled(test)]
326 async fn summarize_existing_managers() {
327 let managers = vec![
328 ("zircon".to_string(), serve_mock_manager(None)),
329 ("termina".to_string(), serve_mock_manager(None)),
330 (
331 "debian".to_string(),
332 serve_mock_manager(Some(GuestInfo {
333 guest_status: Some(GuestStatus::Running),
334 uptime: Some(std::time::Duration::from_secs(123).as_nanos() as i64),
335 ..Default::default()
336 })),
337 ),
338 ];
339
340 let actual = format!("{}", get_environment_summary(managers).await.unwrap());
341 let expected = concat!(
342 "+---------+-------------+-------------------+\n",
343 "| Guest | Status | Uptime |\n",
344 "+=========+=============+===================+\n",
345 "| zircon | Unavailable | --:--:-- HH:MM:SS |\n",
346 "+---------+-------------+-------------------+\n",
347 "| termina | Unavailable | --:--:-- HH:MM:SS |\n",
348 "+---------+-------------+-------------------+\n",
349 "| debian | Running | 00:02:03 HH:MM:SS |\n",
350 "+---------+-------------+-------------------+\n"
351 );
352
353 assert_eq!(actual, expected);
354 }
355
356 #[fasync::run_until_stalled(test)]
357 async fn get_detailed_info_stopped_clean() {
358 let manager = serve_mock_manager(Some(GuestInfo {
359 guest_status: Some(GuestStatus::Stopped),
360 uptime: Some(std::time::Duration::from_secs(5).as_nanos() as i64),
361 ..Default::default()
362 }));
363
364 let actual = format!(
365 "{}",
366 get_detailed_information(arguments::GuestType::Termina, manager).await.unwrap()
367 );
368 let expected = concat!(
369 " Guest package: fuchsia-pkg://fuchsia.com/termina_guest#meta/termina_guest.cm \n",
370 " Guest status: Stopped \n",
371 " Guest uptime: 00:00:05 HH:MM:SS \n",
372 " \n",
373 " Stop reason: Clean shutdown \n",
374 );
375
376 assert_eq!(actual, expected);
377 }
378
379 #[fasync::run_until_stalled(test)]
380 async fn get_detailed_info_stopped_guest_failure() {
381 let manager = serve_mock_manager(Some(GuestInfo {
382 guest_status: Some(GuestStatus::Stopped),
383 uptime: Some(std::time::Duration::from_secs(65).as_nanos() as i64),
384 stop_error: Some(GuestError::InternalError),
385 ..Default::default()
386 }));
387
388 let actual = format!(
389 "{}",
390 get_detailed_information(arguments::GuestType::Zircon, manager).await.unwrap()
391 );
392 let expected = concat!(
393 " Guest package: fuchsia-pkg://fuchsia.com/zircon_guest#meta/zircon_guest.cm \n",
394 " Guest status: Stopped \n",
395 " Guest uptime: 00:01:05 HH:MM:SS \n",
396 " \n",
397 " Stop reason: InternalError \n",
398 );
399
400 assert_eq!(actual, expected);
401 }
402
403 #[fasync::run_until_stalled(test)]
404 async fn get_detailed_info_running_guest() {
405 let manager = serve_mock_manager(Some(GuestInfo {
406 guest_status: Some(GuestStatus::Running),
407 uptime: Some(std::time::Duration::from_secs(125 * 60).as_nanos() as i64),
408 guest_descriptor: Some(GuestDescriptor {
409 num_cpus: Some(4),
410 guest_memory: Some(1073741824),
411 networks: Some(vec![
412 NetSpec {
413 mac_address: MacAddress { octets: [0u8; 6] },
414 enable_bridge: true,
415 };
416 2
417 ]),
418 balloon: Some(true),
419 console: Some(true),
420 gpu: Some(false),
421 rng: Some(true),
422 vsock: Some(true),
423 sound: Some(false),
424 ..Default::default()
425 }),
426 detected_problems: Some(vec![
427 "Host is experiencing heavy memory pressure".to_string(),
428 "No bridge between guest and host network interaces".to_string(),
429 ]),
430 ..Default::default()
431 }));
432
433 let actual = format!(
434 "{}",
435 get_detailed_information(arguments::GuestType::Debian, manager).await.unwrap()
436 );
437 let expected = concat!(
438 " Guest package: fuchsia-pkg://fuchsia.com/debian_guest#meta/debian_guest.cm \n",
439 " Guest status: Running \n",
440 " Guest uptime: 02:05:00 HH:MM:SS \n",
441 " \n",
442 " CPU count: 4 \n",
443 " Guest memory: 1 GiB (1073741824 bytes) \n",
444 " \n",
445 " Active devices: balloon \n",
446 " console \n",
447 " rng \n",
448 " vsock \n",
449 " network (2 devices) \n",
450 " \n",
451 " Inactive devices: gpu \n",
452 " sound \n",
453 " \n",
454 " 2 problems detected: \n",
455 " * Host is experiencing heavy memory pressure \n",
456 " * No bridge between guest and host network interaces \n",
457 );
458
459 assert_eq!(actual, expected);
460 }
461}