crosvm/
main.rs

1// Copyright 2017 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Runs a virtual machine
6//!
7//! ## Feature flags
8#![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)]
108/// Exit code from crosvm,
109enum CommandStatus {
110    /// Exit with success. Also used to mean VM stopped successfully.
111    SuccessOrVmStop = 0,
112    /// VM requested reset.
113    VmReset = 32,
114    /// VM crashed.
115    VmCrash = 33,
116    /// VM exit due to kernel panic in guest.
117    GuestPanic = 34,
118    /// Invalid argument was given to crosvm.
119    InvalidArgs = 35,
120    /// VM exit due to vcpu stall detection.
121    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), &params.socket_path),
196        Trim(params) => (VmRequest::Swap(SwapCommand::Trim), &params.socket_path),
197        SwapOut(params) => (VmRequest::Swap(SwapCommand::SwapOut), &params.socket_path),
198        Disable(params) => (
199            VmRequest::Swap(SwapCommand::Disable {
200                slow_file_cleanup: params.slow_file_cleanup,
201            }),
202            &params.socket_path,
203        ),
204        Status(params) => (VmRequest::Swap(SwapCommand::Status), &params.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        // By default, composite disk is read-only
360        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            // Sparseness for composite disks is not user provided on Linux
450            // (e.g. via an option), and it has no runtime effect.
451            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
693// Returns true if the argument is a flag (e.g. `-s` or `--long`).
694//
695// As a special case, `-` is not treated as a flag, since it is typically used to represent
696// `stdin`/`stdout`.
697fn is_flag(arg: &str) -> bool {
698    arg.len() > 1 && arg.starts_with('-')
699}
700
701// Perform transformations on `args_iter` to produce arguments suitable for parsing by `argh`.
702fn prepare_argh_args<I: IntoIterator<Item = String>>(args_iter: I) -> Vec<String> {
703    let mut args: Vec<String> = Vec::default();
704    // http://b/235882579
705    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                // Split `--arg=val` into `--arg val`, since argh doesn't support the former.
715                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    // Shorten the usage line if it's for `crovm run` command that has so many options.
734    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    // The following panic hook will stop our crashpad hook on windows.
744    // Only initialize when the crash-pad feature is off.
745    #[cfg(not(feature = "crash-report"))]
746    sys::set_panic_hook();
747
748    // Ensure all processes detach from metrics on exit.
749    #[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            // If parsing succeeded and the user requested --help, print the usage message to stdout
758            // and exit with success.
759            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            // Past this point, usage of exit is in danger of leaking zombie processes.
786            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                // We handle run_vm separately because it does not simply signal success/error
796                // but also indicates whether the guest requested reset or stop.
797                run_vm(cmd, log_config)
798            } else if let CrossPlatformCommands::Device(cmd) = command {
799                // On windows, the device command handles its own logging setup, so we can't handle
800                // it below otherwise logging will double init.
801                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            // On windows, the sys commands handle their own logging setup, so we can't handle it
893            // below otherwise logging will double init.
894            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    // WARNING: Any code added after this point is not guaranteed to run
904    // since we may forcibly kill this process (and its children) above.
905    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        // third argument is an empty string. writable: false.
1068        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        // third argument is not "writable" or an empty string
1081        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}