1#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
9
10#[cfg(any(feature = "composite-disk", feature = "qcow"))]
11use std::fs::OpenOptions;
12#[cfg(feature = "composite-disk")]
13use std::io::Write;
14use std::path::Path;
15
16use anyhow::anyhow;
17use anyhow::Context;
18use anyhow::Result;
19use argh::FromArgs;
20use base::debug;
21use base::error;
22use base::info;
23use base::set_thread_name;
24use base::syslog;
25use base::syslog::LogArgs;
26use base::syslog::LogConfig;
27use cmdline::RunCommand;
28mod crosvm;
29use crosvm::cmdline;
30use crosvm::config::Config;
31use devices::virtio::vhost_user_backend::run_block_device;
32#[cfg(feature = "gpu")]
33use devices::virtio::vhost_user_backend::run_gpu_device;
34#[cfg(feature = "net")]
35use devices::virtio::vhost_user_backend::run_net_device;
36#[cfg(feature = "audio")]
37use devices::virtio::vhost_user_backend::run_snd_device;
38#[cfg(feature = "composite-disk")]
39use disk::create_composite_disk;
40#[cfg(feature = "composite-disk")]
41use disk::create_zero_filler;
42#[cfg(feature = "composite-disk")]
43use disk::open_disk_file;
44#[cfg(any(feature = "composite-disk", feature = "qcow"))]
45use disk::DiskFileParams;
46#[cfg(feature = "composite-disk")]
47use disk::ImagePartitionType;
48#[cfg(feature = "composite-disk")]
49use disk::PartitionInfo;
50#[cfg(feature = "qcow")]
51use disk::QcowFile;
52mod sys;
53use crosvm::cmdline::Command;
54use crosvm::cmdline::CrossPlatformCommands;
55use crosvm::cmdline::CrossPlatformDevicesCommands;
56#[cfg(windows)]
57use sys::windows::setup_metrics_reporting;
58#[cfg(feature = "composite-disk")]
59use uuid::Uuid;
60#[cfg(feature = "gpu")]
61use vm_control::client::do_gpu_display_add;
62#[cfg(feature = "gpu")]
63use vm_control::client::do_gpu_display_list;
64#[cfg(feature = "gpu")]
65use vm_control::client::do_gpu_display_remove;
66#[cfg(feature = "gpu")]
67use vm_control::client::do_gpu_set_display_mouse_mode;
68use vm_control::client::do_modify_battery;
69#[cfg(feature = "pci-hotplug")]
70use vm_control::client::do_net_add;
71#[cfg(feature = "pci-hotplug")]
72use vm_control::client::do_net_remove;
73use vm_control::client::do_security_key_attach;
74#[cfg(feature = "audio")]
75use vm_control::client::do_snd_mute_all;
76use vm_control::client::do_swap_status;
77use vm_control::client::do_usb_attach;
78use vm_control::client::do_usb_detach;
79use vm_control::client::do_usb_list;
80#[cfg(feature = "balloon")]
81use vm_control::client::handle_request;
82use vm_control::client::vms_request;
83#[cfg(feature = "gpu")]
84use vm_control::client::ModifyGpuResult;
85use vm_control::client::ModifyUsbResult;
86#[cfg(feature = "balloon")]
87use vm_control::BalloonControlCommand;
88use vm_control::DiskControlCommand;
89use vm_control::HotPlugDeviceInfo;
90use vm_control::HotPlugDeviceType;
91use vm_control::SnapshotCommand;
92use vm_control::SwapCommand;
93use vm_control::UsbControlResult;
94use vm_control::VmRequest;
95#[cfg(feature = "balloon")]
96use vm_control::VmResponse;
97
98use crate::sys::error_to_exit_code;
99use crate::sys::init_log;
100
101#[cfg(feature = "scudo")]
102#[global_allocator]
103static ALLOCATOR: scudo::GlobalScudoAllocator = scudo::GlobalScudoAllocator;
104
105#[repr(i32)]
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107enum CommandStatus {
109 SuccessOrVmStop = 0,
111 VmReset = 32,
113 VmCrash = 33,
115 GuestPanic = 34,
117 InvalidArgs = 35,
119 WatchdogReset = 36,
121}
122
123impl CommandStatus {
124 fn message(&self) -> &'static str {
125 match self {
126 Self::SuccessOrVmStop => "exiting with success",
127 Self::VmReset => "exiting with reset",
128 Self::VmCrash => "exiting with crash",
129 Self::GuestPanic => "exiting with guest panic",
130 Self::InvalidArgs => "invalid argument",
131 Self::WatchdogReset => "exiting with watchdog reset",
132 }
133 }
134}
135
136impl From<sys::ExitState> for CommandStatus {
137 fn from(result: sys::ExitState) -> CommandStatus {
138 match result {
139 sys::ExitState::Stop => CommandStatus::SuccessOrVmStop,
140 sys::ExitState::Reset => CommandStatus::VmReset,
141 sys::ExitState::Crash => CommandStatus::VmCrash,
142 sys::ExitState::GuestPanic => CommandStatus::GuestPanic,
143 sys::ExitState::WatchdogReset => CommandStatus::WatchdogReset,
144 }
145 }
146}
147
148fn run_vm(cmd: RunCommand, log_config: LogConfig) -> Result<CommandStatus> {
149 let cfg = match TryInto::<Config>::try_into(cmd) {
150 Ok(cfg) => cfg,
151 Err(e) => {
152 eprintln!("{e}");
153 return Err(anyhow!("{}", e));
154 }
155 };
156
157 if let Some(ref name) = cfg.name {
158 set_thread_name(name).context("Failed to set the name")?;
159 }
160
161 #[cfg(feature = "crash-report")]
162 crosvm::sys::setup_emulator_crash_reporting(&cfg)?;
163
164 #[cfg(windows)]
165 setup_metrics_reporting()?;
166
167 init_log(log_config, &cfg)?;
168 cros_tracing::init();
169
170 if let Some(async_executor) = cfg.async_executor {
171 cros_async::Executor::set_default_executor_kind(async_executor)
172 .context("Failed to set the default async executor")?;
173 }
174
175 let exit_state = crate::sys::run_config(cfg)?;
176 Ok(CommandStatus::from(exit_state))
177}
178
179fn stop_vms(cmd: cmdline::StopCommand) -> std::result::Result<(), ()> {
180 vms_request(&VmRequest::Exit, cmd.socket_path)
181}
182
183fn suspend_vms(cmd: cmdline::SuspendCommand) -> std::result::Result<(), ()> {
184 if cmd.full {
185 vms_request(&VmRequest::SuspendVm, cmd.socket_path)
186 } else {
187 vms_request(&VmRequest::SuspendVcpus, cmd.socket_path)
188 }
189}
190
191fn swap_vms(cmd: cmdline::SwapCommand) -> std::result::Result<(), ()> {
192 use cmdline::SwapSubcommands::*;
193 let (req, path) = match &cmd.nested {
194 Enable(params) => (VmRequest::Swap(SwapCommand::Enable), ¶ms.socket_path),
195 Trim(params) => (VmRequest::Swap(SwapCommand::Trim), ¶ms.socket_path),
196 SwapOut(params) => (VmRequest::Swap(SwapCommand::SwapOut), ¶ms.socket_path),
197 Disable(params) => (
198 VmRequest::Swap(SwapCommand::Disable {
199 slow_file_cleanup: params.slow_file_cleanup,
200 }),
201 ¶ms.socket_path,
202 ),
203 Status(params) => (VmRequest::Swap(SwapCommand::Status), ¶ms.socket_path),
204 };
205 if let VmRequest::Swap(SwapCommand::Status) = req {
206 do_swap_status(path)
207 } else {
208 vms_request(&req, path)
209 }
210}
211
212fn resume_vms(cmd: cmdline::ResumeCommand) -> std::result::Result<(), ()> {
213 if cmd.full {
214 vms_request(&VmRequest::ResumeVm, cmd.socket_path)
215 } else {
216 vms_request(&VmRequest::ResumeVcpus, cmd.socket_path)
217 }
218}
219
220fn powerbtn_vms(cmd: cmdline::PowerbtnCommand) -> std::result::Result<(), ()> {
221 vms_request(&VmRequest::Powerbtn, cmd.socket_path)
222}
223
224fn sleepbtn_vms(cmd: cmdline::SleepCommand) -> std::result::Result<(), ()> {
225 vms_request(&VmRequest::Sleepbtn, cmd.socket_path)
226}
227
228fn inject_gpe(cmd: cmdline::GpeCommand) -> std::result::Result<(), ()> {
229 vms_request(
230 &VmRequest::Gpe {
231 gpe: cmd.gpe,
232 clear_evt: None,
233 },
234 cmd.socket_path,
235 )
236}
237
238#[cfg(feature = "balloon")]
239fn balloon_vms(cmd: cmdline::BalloonCommand) -> std::result::Result<(), ()> {
240 let command = BalloonControlCommand::Adjust {
241 num_bytes: cmd.num_bytes,
242 wait_for_success: cmd.wait,
243 };
244 vms_request(&VmRequest::BalloonCommand(command), cmd.socket_path)
245}
246
247#[cfg(feature = "balloon")]
248fn balloon_stats(cmd: cmdline::BalloonStatsCommand) -> std::result::Result<(), ()> {
249 let command = BalloonControlCommand::Stats {};
250 let request = &VmRequest::BalloonCommand(command);
251 let response = handle_request(request, cmd.socket_path)?;
252 match serde_json::to_string_pretty(&response) {
253 Ok(response_json) => println!("{response_json}"),
254 Err(e) => {
255 error!("Failed to serialize into JSON: {}", e);
256 return Err(());
257 }
258 }
259 match response {
260 VmResponse::BalloonStats { .. } => Ok(()),
261 _ => Err(()),
262 }
263}
264
265#[cfg(feature = "balloon")]
266fn balloon_ws(cmd: cmdline::BalloonWsCommand) -> std::result::Result<(), ()> {
267 let command = BalloonControlCommand::WorkingSet {};
268 let request = &VmRequest::BalloonCommand(command);
269 let response = handle_request(request, cmd.socket_path)?;
270 match serde_json::to_string_pretty(&response) {
271 Ok(response_json) => println!("{response_json}"),
272 Err(e) => {
273 error!("Failed to serialize into JSON: {e}");
274 return Err(());
275 }
276 }
277 match response {
278 VmResponse::BalloonWS { .. } => Ok(()),
279 _ => Err(()),
280 }
281}
282
283fn modify_battery(cmd: cmdline::BatteryCommand) -> std::result::Result<(), ()> {
284 do_modify_battery(
285 cmd.socket_path,
286 &cmd.battery_type,
287 &cmd.property,
288 &cmd.target,
289 )
290}
291
292fn modify_vfio(cmd: cmdline::VfioCrosvmCommand) -> std::result::Result<(), ()> {
293 let (request, socket_path, vfio_path) = match cmd.command {
294 cmdline::VfioSubCommand::Add(c) => {
295 let request = VmRequest::HotPlugVfioCommand {
296 device: HotPlugDeviceInfo {
297 device_type: HotPlugDeviceType::EndPoint,
298 path: c.vfio_path.clone(),
299 hp_interrupt: true,
300 },
301 add: true,
302 };
303 (request, c.socket_path, c.vfio_path)
304 }
305 cmdline::VfioSubCommand::Remove(c) => {
306 let request = VmRequest::HotPlugVfioCommand {
307 device: HotPlugDeviceInfo {
308 device_type: HotPlugDeviceType::EndPoint,
309 path: c.vfio_path.clone(),
310 hp_interrupt: false,
311 },
312 add: false,
313 };
314 (request, c.socket_path, c.vfio_path)
315 }
316 };
317 if !vfio_path.exists() || !vfio_path.is_dir() {
318 error!("Invalid host sysfs path: {:?}", vfio_path);
319 return Err(());
320 }
321
322 vms_request(&request, socket_path)?;
323 Ok(())
324}
325
326#[cfg(feature = "pci-hotplug")]
327fn modify_virtio_net(cmd: cmdline::VirtioNetCommand) -> std::result::Result<(), ()> {
328 match cmd.command {
329 cmdline::VirtioNetSubCommand::AddTap(c) => {
330 let bus_num = do_net_add(&c.tap_name, c.socket_path).map_err(|e| {
331 error!("{}", &e);
332 })?;
333 info!("Tap device {} plugged to PCI bus {}", &c.tap_name, bus_num);
334 }
335 cmdline::VirtioNetSubCommand::RemoveTap(c) => {
336 do_net_remove(c.bus, &c.socket_path).map_err(|e| {
337 error!("Tap device remove failed: {:?}", &e);
338 })?;
339 info!("Tap device removed from PCI bus {}", &c.bus);
340 }
341 };
342
343 Ok(())
344}
345
346#[cfg(feature = "composite-disk")]
347fn parse_composite_partition_arg(
348 partition_arg: &str,
349) -> std::result::Result<(String, String, bool, Option<Uuid>), ()> {
350 let mut partition_fields = partition_arg.split(':');
351
352 let label = partition_fields.next();
353 let path = partition_fields.next();
354 let opt = partition_fields.next();
355 let part_guid = partition_fields.next();
356
357 if let (Some(label), Some(path)) = (label, path) {
358 let writable = match opt {
360 None => false,
361 Some("") => false,
362 Some("writable") => true,
363 Some(value) => {
364 error!(
365 "Unrecognized option '{}'. Expected 'writable' or nothing.",
366 value
367 );
368 return Err(());
369 }
370 };
371
372 let part_guid = part_guid
373 .map(Uuid::parse_str)
374 .transpose()
375 .map_err(|e| error!("Invalid partition GUID: {}", e))?;
376
377 Ok((label.to_owned(), path.to_owned(), writable, part_guid))
378 } else {
379 error!(
380 "Must specify label and path for partition '{}', like LABEL:PARTITION",
381 partition_arg
382 );
383 Err(())
384 }
385}
386
387#[cfg(feature = "composite-disk")]
388fn create_composite(cmd: cmdline::CreateCompositeCommand) -> std::result::Result<(), ()> {
389 use std::io::BufWriter;
390 use std::path::PathBuf;
391
392 let composite_image_path = &cmd.path;
393 let zero_filler_path = format!("{composite_image_path}.filler");
394 let header_path = format!("{composite_image_path}.header");
395 let footer_path = format!("{composite_image_path}.footer");
396
397 let mut composite_image_file = OpenOptions::new()
398 .create(true)
399 .read(true)
400 .write(true)
401 .truncate(true)
402 .open(composite_image_path)
403 .map_err(|e| {
404 error!(
405 "Failed opening composite disk image file at '{}': {}",
406 composite_image_path, e
407 );
408 })?;
409 create_zero_filler(&zero_filler_path).map_err(|e| {
410 error!(
411 "Failed to create zero filler file at '{}': {}",
412 &zero_filler_path, e
413 );
414 })?;
415 let header_file = OpenOptions::new()
416 .create(true)
417 .read(true)
418 .write(true)
419 .truncate(true)
420 .open(&header_path)
421 .map_err(|e| {
422 error!(
423 "Failed opening header image file at '{}': {}",
424 header_path, e
425 );
426 })?;
427 let mut header_buffer = BufWriter::new(header_file);
428 let footer_file = OpenOptions::new()
429 .create(true)
430 .read(true)
431 .write(true)
432 .truncate(true)
433 .open(&footer_path)
434 .map_err(|e| {
435 error!(
436 "Failed opening footer image file at '{}': {}",
437 footer_path, e
438 );
439 })?;
440 let mut footer_buffer = BufWriter::new(footer_file);
441
442 let partitions = cmd
443 .partitions
444 .into_iter()
445 .map(|partition_arg| {
446 let (label, path, writable, part_guid) = parse_composite_partition_arg(&partition_arg)?;
447
448 let size = open_disk_file(DiskFileParams {
451 path: PathBuf::from(&path),
452 is_read_only: !writable,
453 is_sparse_file: true,
454 is_overlapped: false,
455 is_direct: false,
456 lock: true,
457 depth: 0,
458 })
459 .map_err(|e| error!("Failed to create DiskFile instance: {}", e))?
460 .get_len()
461 .map_err(|e| error!("Failed to get length of partition image: {}", e))?;
462
463 Ok(PartitionInfo {
464 label,
465 path: Path::new(&path).to_owned(),
466 partition_type: ImagePartitionType::LinuxFilesystem,
467 writable,
468 size,
469 part_guid,
470 })
471 })
472 .collect::<Result<Vec<PartitionInfo>, ()>>()?;
473
474 create_composite_disk(
475 &partitions,
476 &PathBuf::from(zero_filler_path),
477 &PathBuf::from(header_path),
478 &mut header_buffer,
479 &PathBuf::from(footer_path),
480 &mut footer_buffer,
481 &mut composite_image_file,
482 )
483 .map_err(|e| {
484 error!(
485 "Failed to create composite disk image at '{}': {}",
486 composite_image_path, e
487 );
488 })?;
489 header_buffer.flush().map_err(|e| {
490 error!("Failed to flush header buffer: {}", e);
491 })?;
492 footer_buffer.flush().map_err(|e| {
493 error!("Failed to flush footer buffer: {}", e);
494 })?;
495
496 Ok(())
497}
498
499#[cfg(feature = "qcow")]
500fn create_qcow2(cmd: cmdline::CreateQcow2Command) -> std::result::Result<(), ()> {
501 use std::path::PathBuf;
502
503 if !(cmd.size.is_some() ^ cmd.backing_file.is_some()) {
504 println!(
505 "Create a new QCOW2 image at `PATH` of either the specified `SIZE` in bytes or
506 with a '--backing_file'."
507 );
508 return Err(());
509 }
510
511 let file = OpenOptions::new()
512 .create(true)
513 .read(true)
514 .write(true)
515 .truncate(true)
516 .open(&cmd.file_path)
517 .map_err(|e| {
518 error!("Failed opening qcow file at '{}': {}", cmd.file_path, e);
519 })?;
520
521 let params = DiskFileParams {
522 path: PathBuf::from(&cmd.file_path),
523 is_read_only: false,
524 is_sparse_file: false,
525 is_overlapped: false,
526 is_direct: false,
527 lock: true,
528 depth: 0,
529 };
530 match (cmd.size, cmd.backing_file) {
531 (Some(size), None) => QcowFile::new(file, params, size).map_err(|e| {
532 error!("Failed to create qcow file at '{}': {}", cmd.file_path, e);
533 })?,
534 (None, Some(backing_file)) => QcowFile::new_from_backing(file, params, &backing_file)
535 .map_err(|e| {
536 error!("Failed to create qcow file at '{}': {}", cmd.file_path, e);
537 })?,
538 _ => unreachable!(),
539 };
540 Ok(())
541}
542
543fn start_device(opts: cmdline::DeviceCommand) -> std::result::Result<(), ()> {
544 if let Some(async_executor) = opts.async_executor {
545 cros_async::Executor::set_default_executor_kind(async_executor)
546 .map_err(|e| error!("Failed to set the default async executor: {:#}", e))?;
547 }
548
549 let result = match opts.command {
550 cmdline::DeviceSubcommand::CrossPlatform(command) => match command {
551 CrossPlatformDevicesCommands::Block(cfg) => run_block_device(cfg),
552 #[cfg(feature = "gpu")]
553 CrossPlatformDevicesCommands::Gpu(cfg) => run_gpu_device(cfg),
554 #[cfg(feature = "net")]
555 CrossPlatformDevicesCommands::Net(cfg) => run_net_device(cfg),
556 #[cfg(feature = "audio")]
557 CrossPlatformDevicesCommands::Snd(cfg) => run_snd_device(cfg),
558 },
559 cmdline::DeviceSubcommand::Sys(command) => sys::start_device(command),
560 };
561
562 result.map_err(|e| {
563 error!("Failed to run device: {:#}", e);
564 })
565}
566
567fn disk_cmd(cmd: cmdline::DiskCommand) -> std::result::Result<(), ()> {
568 match cmd.command {
569 cmdline::DiskSubcommand::Resize(cmd) => {
570 let request = VmRequest::DiskCommand {
571 disk_index: cmd.disk_index,
572 command: DiskControlCommand::Resize {
573 new_size: cmd.disk_size,
574 },
575 };
576 vms_request(&request, cmd.socket_path)
577 }
578 }
579}
580
581fn make_rt(cmd: cmdline::MakeRTCommand) -> std::result::Result<(), ()> {
582 vms_request(&VmRequest::MakeRT, cmd.socket_path)
583}
584
585#[cfg(feature = "gpu")]
586fn gpu_display_add(cmd: cmdline::GpuAddDisplaysCommand) -> ModifyGpuResult {
587 do_gpu_display_add(cmd.socket_path, cmd.gpu_display)
588}
589
590#[cfg(feature = "gpu")]
591fn gpu_display_list(cmd: cmdline::GpuListDisplaysCommand) -> ModifyGpuResult {
592 do_gpu_display_list(cmd.socket_path)
593}
594
595#[cfg(feature = "gpu")]
596fn gpu_display_remove(cmd: cmdline::GpuRemoveDisplaysCommand) -> ModifyGpuResult {
597 do_gpu_display_remove(cmd.socket_path, cmd.display_id)
598}
599
600#[cfg(feature = "gpu")]
601fn gpu_set_display_mouse_mode(cmd: cmdline::GpuSetDisplayMouseModeCommand) -> ModifyGpuResult {
602 do_gpu_set_display_mouse_mode(cmd.socket_path, cmd.display_id, cmd.mouse_mode)
603}
604
605#[cfg(feature = "gpu")]
606fn modify_gpu(cmd: cmdline::GpuCommand) -> std::result::Result<(), ()> {
607 let result = match cmd.command {
608 cmdline::GpuSubCommand::AddDisplays(cmd) => gpu_display_add(cmd),
609 cmdline::GpuSubCommand::ListDisplays(cmd) => gpu_display_list(cmd),
610 cmdline::GpuSubCommand::RemoveDisplays(cmd) => gpu_display_remove(cmd),
611 cmdline::GpuSubCommand::SetDisplayMouseMode(cmd) => gpu_set_display_mouse_mode(cmd),
612 };
613 match result {
614 Ok(response) => {
615 println!("{response}");
616 Ok(())
617 }
618 Err(e) => {
619 println!("error {e}");
620 Err(())
621 }
622 }
623}
624
625#[cfg(feature = "audio")]
626fn modify_snd(cmd: cmdline::SndCommand) -> std::result::Result<(), ()> {
627 match cmd.command {
628 cmdline::SndSubCommand::MuteAll(cmd) => do_snd_mute_all(cmd.socket_path, cmd.muted),
629 }
630}
631
632fn usb_attach(cmd: cmdline::UsbAttachCommand) -> ModifyUsbResult<UsbControlResult> {
633 let dev_path = Path::new(&cmd.dev_path);
634
635 do_usb_attach(cmd.socket_path, dev_path)
636}
637
638fn security_key_attach(cmd: cmdline::UsbAttachKeyCommand) -> ModifyUsbResult<UsbControlResult> {
639 let dev_path = Path::new(&cmd.dev_path);
640
641 do_security_key_attach(cmd.socket_path, dev_path)
642}
643
644fn usb_detach(cmd: cmdline::UsbDetachCommand) -> ModifyUsbResult<UsbControlResult> {
645 do_usb_detach(cmd.socket_path, cmd.port)
646}
647
648fn usb_list(cmd: cmdline::UsbListCommand) -> ModifyUsbResult<UsbControlResult> {
649 do_usb_list(cmd.socket_path)
650}
651
652fn modify_usb(cmd: cmdline::UsbCommand) -> std::result::Result<(), ()> {
653 let result = match cmd.command {
654 cmdline::UsbSubCommand::Attach(cmd) => usb_attach(cmd),
655 cmdline::UsbSubCommand::SecurityKeyAttach(cmd) => security_key_attach(cmd),
656 cmdline::UsbSubCommand::Detach(cmd) => usb_detach(cmd),
657 cmdline::UsbSubCommand::List(cmd) => usb_list(cmd),
658 };
659 match result {
660 Ok(response) => {
661 println!("{response}");
662 Ok(())
663 }
664 Err(e) => {
665 println!("error {e}");
666 Err(())
667 }
668 }
669}
670
671fn snapshot_vm(cmd: cmdline::SnapshotCommand) -> std::result::Result<(), ()> {
672 use cmdline::SnapshotSubCommands::*;
673 let (socket_path, request) = match cmd.snapshot_command {
674 Take(take_cmd) => {
675 let req = VmRequest::Snapshot(SnapshotCommand::Take {
676 snapshot_path: take_cmd.snapshot_path,
677 compress_memory: take_cmd.compress_memory,
678 encrypt: take_cmd.encrypt,
679 });
680 (take_cmd.socket_path, req)
681 }
682 };
683 let socket_path = Path::new(&socket_path);
684 vms_request(&request, socket_path)
685}
686
687#[allow(clippy::unnecessary_wraps)]
688fn pkg_version() -> std::result::Result<(), ()> {
689 const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
690 const PKG_VERSION: Option<&'static str> = option_env!("PKG_VERSION");
691
692 print!("crosvm {}", VERSION.unwrap_or("UNKNOWN"));
693 match PKG_VERSION {
694 Some(v) => println!("-{v}"),
695 None => println!(),
696 }
697 Ok(())
698}
699
700fn is_flag(arg: &str) -> bool {
705 arg.len() > 1 && arg.starts_with('-')
706}
707
708fn prepare_argh_args<I: IntoIterator<Item = String>>(args_iter: I) -> Vec<String> {
710 let mut args: Vec<String> = Vec::default();
711 for arg in args_iter {
713 match arg.as_str() {
714 "--host_ip" => {
715 eprintln!("`--host_ip` option is deprecated!");
716 eprintln!("Please use `--host-ip` instead");
717 args.push("--host-ip".to_string());
718 }
719 "-h" => args.push("--help".to_string()),
720 arg if is_flag(arg) => {
721 if let Some((key, value)) = arg.split_once('=') {
723 args.push(key.to_string());
724 args.push(value.to_string());
725 } else {
726 args.push(arg.to_string());
727 }
728 }
729 arg => args.push(arg.to_string()),
730 }
731 }
732
733 args
734}
735
736fn shorten_usage(help: &str) -> String {
737 let mut lines = help.lines().collect::<Vec<_>>();
738 let first_line = lines[0].split(char::is_whitespace).collect::<Vec<_>>();
739
740 let run_usage = format!("Usage: {} run <options> KERNEL", first_line[1]);
742 if first_line[0] == "Usage:" && first_line[2] == "run" {
743 lines[0] = &run_usage;
744 }
745
746 lines.join("\n")
747}
748
749fn crosvm_main<I: IntoIterator<Item = String>>(args: I) -> Result<CommandStatus> {
750 #[cfg(not(feature = "crash-report"))]
753 sys::set_panic_hook();
754
755 #[cfg(windows)]
757 let _metrics_destructor = metrics::get_destructor();
758
759 let args = prepare_argh_args(args);
760 let args = args.iter().map(|s| s.as_str()).collect::<Vec<_>>();
761 let args = match crosvm::cmdline::CrosvmCmdlineArgs::from_args(&args[..1], &args[1..]) {
762 Ok(args) => args,
763 Err(e) if e.status.is_ok() => {
764 let help = shorten_usage(&e.output);
767 println!("{help}");
768 return Ok(CommandStatus::SuccessOrVmStop);
769 }
770 Err(e) => {
771 error!("arg parsing failed: {}", e.output);
772 return Ok(CommandStatus::InvalidArgs);
773 }
774 };
775 let extended_status = args.extended_status;
776
777 debug!("CLI arguments parsed.");
778
779 let mut log_config = LogConfig {
780 log_args: LogArgs {
781 filter: args.log_level,
782 proc_name: args.syslog_tag.unwrap_or("crosvm".to_string()),
783 syslog: !args.no_syslog,
784 ..Default::default()
785 },
786
787 ..Default::default()
788 };
789
790 let ret = match args.command {
791 Command::CrossPlatform(command) => {
792 if let CrossPlatformCommands::Run(cmd) = command {
794 if let Some(syslog_tag) = &cmd.syslog_tag {
795 base::warn!(
796 "`crosvm run --syslog-tag` is deprecated; please use \
797 `crosvm --syslog-tag=\"{}\" run` instead",
798 syslog_tag
799 );
800 log_config.log_args.proc_name.clone_from(syslog_tag);
801 }
802 run_vm(cmd, log_config)
805 } else if let CrossPlatformCommands::Device(cmd) = command {
806 if cfg!(unix) {
809 syslog::init_with(log_config).context("failed to initialize syslog")?;
810 }
811 start_device(cmd)
812 .map_err(|_| anyhow!("start_device subcommand failed"))
813 .map(|_| CommandStatus::SuccessOrVmStop)
814 } else {
815 syslog::init_with(log_config).context("failed to initialize syslog")?;
816
817 match command {
818 #[cfg(feature = "balloon")]
819 CrossPlatformCommands::Balloon(cmd) => {
820 balloon_vms(cmd).map_err(|_| anyhow!("balloon subcommand failed"))
821 }
822 #[cfg(feature = "balloon")]
823 CrossPlatformCommands::BalloonStats(cmd) => {
824 balloon_stats(cmd).map_err(|_| anyhow!("balloon_stats subcommand failed"))
825 }
826 #[cfg(feature = "balloon")]
827 CrossPlatformCommands::BalloonWs(cmd) => {
828 balloon_ws(cmd).map_err(|_| anyhow!("balloon_ws subcommand failed"))
829 }
830 CrossPlatformCommands::Battery(cmd) => {
831 modify_battery(cmd).map_err(|_| anyhow!("battery subcommand failed"))
832 }
833 #[cfg(feature = "composite-disk")]
834 CrossPlatformCommands::CreateComposite(cmd) => create_composite(cmd)
835 .map_err(|_| anyhow!("create_composite subcommand failed")),
836 #[cfg(feature = "qcow")]
837 CrossPlatformCommands::CreateQcow2(cmd) => {
838 create_qcow2(cmd).map_err(|_| anyhow!("create_qcow2 subcommand failed"))
839 }
840 CrossPlatformCommands::Device(_) => unreachable!(),
841 CrossPlatformCommands::Disk(cmd) => {
842 disk_cmd(cmd).map_err(|_| anyhow!("disk subcommand failed"))
843 }
844 #[cfg(feature = "gpu")]
845 CrossPlatformCommands::Gpu(cmd) => {
846 modify_gpu(cmd).map_err(|_| anyhow!("gpu subcommand failed"))
847 }
848 #[cfg(feature = "audio")]
849 CrossPlatformCommands::Snd(cmd) => {
850 modify_snd(cmd).map_err(|_| anyhow!("snd command failed"))
851 }
852 CrossPlatformCommands::MakeRT(cmd) => {
853 make_rt(cmd).map_err(|_| anyhow!("make_rt subcommand failed"))
854 }
855 CrossPlatformCommands::Resume(cmd) => {
856 resume_vms(cmd).map_err(|_| anyhow!("resume subcommand failed"))
857 }
858 CrossPlatformCommands::Run(_) => unreachable!(),
859 CrossPlatformCommands::Stop(cmd) => {
860 stop_vms(cmd).map_err(|_| anyhow!("stop subcommand failed"))
861 }
862 CrossPlatformCommands::Suspend(cmd) => {
863 suspend_vms(cmd).map_err(|_| anyhow!("suspend subcommand failed"))
864 }
865 CrossPlatformCommands::Swap(cmd) => {
866 swap_vms(cmd).map_err(|_| anyhow!("swap subcommand failed"))
867 }
868 CrossPlatformCommands::Powerbtn(cmd) => {
869 powerbtn_vms(cmd).map_err(|_| anyhow!("powerbtn subcommand failed"))
870 }
871 CrossPlatformCommands::Sleepbtn(cmd) => {
872 sleepbtn_vms(cmd).map_err(|_| anyhow!("sleepbtn subcommand failed"))
873 }
874 CrossPlatformCommands::Gpe(cmd) => {
875 inject_gpe(cmd).map_err(|_| anyhow!("gpe subcommand failed"))
876 }
877 CrossPlatformCommands::Usb(cmd) => {
878 modify_usb(cmd).map_err(|_| anyhow!("usb subcommand failed"))
879 }
880 CrossPlatformCommands::Version(_) => {
881 pkg_version().map_err(|_| anyhow!("version subcommand failed"))
882 }
883 CrossPlatformCommands::Vfio(cmd) => {
884 modify_vfio(cmd).map_err(|_| anyhow!("vfio subcommand failed"))
885 }
886 #[cfg(feature = "pci-hotplug")]
887 CrossPlatformCommands::VirtioNet(cmd) => {
888 modify_virtio_net(cmd).map_err(|_| anyhow!("virtio subcommand failed"))
889 }
890 CrossPlatformCommands::Snapshot(cmd) => {
891 snapshot_vm(cmd).map_err(|_| anyhow!("snapshot subcommand failed"))
892 }
893 }
894 .map(|_| CommandStatus::SuccessOrVmStop)
895 }
896 }
897 cmdline::Command::Sys(command) => {
898 let log_args = log_config.log_args.clone();
899 if cfg!(unix) {
902 syslog::init_with(log_config).context("failed to initialize syslog")?;
903 }
904 sys::run_command(command, log_args).map(|_| CommandStatus::SuccessOrVmStop)
905 }
906 };
907
908 sys::cleanup();
909
910 ret.map(|s| {
913 if extended_status {
914 s
915 } else {
916 CommandStatus::SuccessOrVmStop
917 }
918 })
919}
920
921fn main() {
922 syslog::early_init();
923 debug!("crosvm started.");
924 let res = crosvm_main(std::env::args());
925
926 let exit_code = match &res {
927 Ok(code) => {
928 info!("{}", code.message());
929 *code as i32
930 }
931 Err(e) => {
932 let exit_code = error_to_exit_code(&res);
933 error!("exiting with error {}: {:?}", exit_code, e);
934 exit_code
935 }
936 };
937 std::process::exit(exit_code);
938}
939
940#[cfg(test)]
941mod tests {
942 use super::*;
943
944 #[test]
945 fn args_is_flag() {
946 assert!(is_flag("--test"));
947 assert!(is_flag("-s"));
948
949 assert!(!is_flag("-"));
950 assert!(!is_flag("no-leading-dash"));
951 }
952
953 #[test]
954 fn args_split_long() {
955 assert_eq!(
956 prepare_argh_args(
957 ["crosvm", "run", "--something=options", "vm_kernel"].map(|x| x.to_string())
958 ),
959 ["crosvm", "run", "--something", "options", "vm_kernel"]
960 );
961 }
962
963 #[test]
964 fn args_split_short() {
965 assert_eq!(
966 prepare_argh_args(
967 ["crosvm", "run", "-p=init=/bin/bash", "vm_kernel"].map(|x| x.to_string())
968 ),
969 ["crosvm", "run", "-p", "init=/bin/bash", "vm_kernel"]
970 );
971 }
972
973 #[test]
974 fn args_host_ip() {
975 assert_eq!(
976 prepare_argh_args(
977 ["crosvm", "run", "--host_ip", "1.2.3.4", "vm_kernel"].map(|x| x.to_string())
978 ),
979 ["crosvm", "run", "--host-ip", "1.2.3.4", "vm_kernel"]
980 );
981 }
982
983 #[test]
984 fn args_h() {
985 assert_eq!(
986 prepare_argh_args(["crosvm", "run", "-h"].map(|x| x.to_string())),
987 ["crosvm", "run", "--help"]
988 );
989 }
990
991 #[test]
992 fn args_battery_option() {
993 assert_eq!(
994 prepare_argh_args(
995 [
996 "crosvm",
997 "run",
998 "--battery",
999 "type=goldfish",
1000 "-p",
1001 "init=/bin/bash",
1002 "vm_kernel"
1003 ]
1004 .map(|x| x.to_string())
1005 ),
1006 [
1007 "crosvm",
1008 "run",
1009 "--battery",
1010 "type=goldfish",
1011 "-p",
1012 "init=/bin/bash",
1013 "vm_kernel"
1014 ]
1015 );
1016 }
1017
1018 #[test]
1019 fn help_success() {
1020 let args = ["crosvm", "--help"];
1021 let res = crosvm_main(args.iter().map(|s| s.to_string()));
1022 let status = res.expect("arg parsing should succeed");
1023 assert_eq!(status, CommandStatus::SuccessOrVmStop);
1024 }
1025
1026 #[test]
1027 fn invalid_arg_failure() {
1028 let args = ["crosvm", "--heeeelp"];
1029 let res = crosvm_main(args.iter().map(|s| s.to_string()));
1030 let status = res.expect("arg parsing should succeed");
1031 assert_eq!(status, CommandStatus::InvalidArgs);
1032 }
1033
1034 #[test]
1035 #[cfg(feature = "composite-disk")]
1036 fn parse_composite_disk_arg() {
1037 let arg1 = String::from("LABEL1:/partition1.img:writable");
1038 let res1 = parse_composite_partition_arg(&arg1);
1039 assert_eq!(
1040 res1,
1041 Ok((
1042 String::from("LABEL1"),
1043 String::from("/partition1.img"),
1044 true,
1045 None
1046 ))
1047 );
1048
1049 let arg2 = String::from("LABEL2:/partition2.img");
1050 let res2 = parse_composite_partition_arg(&arg2);
1051 assert_eq!(
1052 res2,
1053 Ok((
1054 String::from("LABEL2"),
1055 String::from("/partition2.img"),
1056 false,
1057 None
1058 ))
1059 );
1060
1061 let arg3 =
1062 String::from("LABEL3:/partition3.img:writable:4049C8DC-6C2B-C740-A95A-BDAA629D4378");
1063 let res3 = parse_composite_partition_arg(&arg3);
1064 assert_eq!(
1065 res3,
1066 Ok((
1067 String::from("LABEL3"),
1068 String::from("/partition3.img"),
1069 true,
1070 Some(Uuid::from_u128(0x4049C8DC_6C2B_C740_A95A_BDAA629D4378))
1071 ))
1072 );
1073
1074 let arg4 = String::from("LABEL4:/partition4.img::4049C8DC-6C2B-C740-A95A-BDAA629D4378");
1076 let res4 = parse_composite_partition_arg(&arg4);
1077 assert_eq!(
1078 res4,
1079 Ok((
1080 String::from("LABEL4"),
1081 String::from("/partition4.img"),
1082 false,
1083 Some(Uuid::from_u128(0x4049C8DC_6C2B_C740_A95A_BDAA629D4378))
1084 ))
1085 );
1086
1087 let arg5 = String::from("LABEL5:/partition5.img:4049C8DC-6C2B-C740-A95A-BDAA629D4378");
1089 let res5 = parse_composite_partition_arg(&arg5);
1090 assert_eq!(res5, Err(()));
1091 }
1092
1093 #[test]
1094 fn test_shorten_run_usage() {
1095 let help = r"Usage: crosvm run [<KERNEL>] [options] <very long line>...
1096
1097Start a new crosvm instance";
1098 assert_eq!(
1099 shorten_usage(help),
1100 r"Usage: crosvm run <options> KERNEL
1101
1102Start a new crosvm instance"
1103 );
1104 }
1105}