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