emu_cli/
launcher.rs

1use super::{
2    config_storage::XDGConfigStorage,
3    image::QEMU_IMG_DEFAULT_FORMAT,
4    qmp::messages::GenericReturn,
5    traits::{ConfigStorageHandler, Launcher},
6    vm::VM,
7};
8use crate::{qmp::client::Client, util::pid_running};
9use anyhow::{anyhow, Result};
10use fork::{daemon, Fork};
11use std::{
12    fs::{read_to_string, remove_file},
13    path::PathBuf,
14    process::Command,
15    process::ExitStatus,
16    sync::Arc,
17    thread::sleep,
18    time::Duration,
19};
20
21const QEMU_BIN_NAME: &str = "qemu-system-x86_64";
22
23macro_rules! append_vec {
24    ( $v:expr, $( $x:expr ),* ) => {
25        {
26            $(
27                $v.push($x.into());
28            )*
29        }
30    };
31}
32
33macro_rules! into_vec {
34    ( $( $x:expr ),* ) => {
35        {
36            let mut temp_vec = Vec::new();
37
38            $(
39                temp_vec.push($x.into());
40            )*
41
42            temp_vec
43        }
44    };
45}
46
47#[derive(Debug, Clone)]
48pub struct QEmuLauncher {
49    config: Arc<Box<dyn ConfigStorageHandler>>,
50}
51
52impl Default for QEmuLauncher {
53    fn default() -> Self {
54        Self {
55            config: Arc::new(Box::new(XDGConfigStorage::default())),
56        }
57    }
58}
59
60impl QEmuLauncher {
61    fn hostfwd_rules(&self, vm: &VM) -> Result<String> {
62        let config = vm.config();
63        let mut res = String::new();
64        for (host, guest) in config.ports {
65            res += &format!(",hostfwd=tcp:127.0.0.1:{}-:{}", host, guest);
66        }
67
68        Ok(res)
69    }
70
71    fn cdrom_rules(&self, v: &mut Vec<String>, disk: Option<PathBuf>, index: u8) -> Result<()> {
72        if let Some(cd) = disk {
73            match std::fs::metadata(&cd) {
74                Ok(_) => {
75                    append_vec!(
76                        v,
77                        "-drive",
78                        format!("file={},media=cdrom,index={}", cd.display(), index)
79                    );
80                }
81                Err(e) => return Err(anyhow!("error locating cdrom file: {}", e)),
82            }
83        }
84        Ok(())
85    }
86
87    fn display_rule(&self, v: &mut Vec<String>, headless: bool) {
88        append_vec!(v, "-display");
89        if !headless {
90            append_vec!(v, "gtk");
91        } else {
92            append_vec!(v, "none");
93        }
94    }
95
96    fn args(&self, vm: &VM) -> Result<Vec<String>> {
97        let config = vm.config();
98        let disk_list = self.config.disk_list(vm)?;
99        let mut disks = Vec::new();
100        for (x, disk) in disk_list.iter().enumerate() {
101            disks.push("-drive".to_string());
102            disks.push(format!(
103                "driver={},if={},file={},cache=none,media=disk,index={}",
104                QEMU_IMG_DEFAULT_FORMAT,
105                config.machine.image_interface,
106                disk.display(),
107                x
108            ));
109        }
110
111        let mon = self.config.monitor_path(vm);
112
113        let mut v: Vec<String> = into_vec![
114            "-nodefaults",
115            "-chardev",
116            format!("socket,server=on,wait=off,id=char0,path={}", mon.display()),
117            "-mon",
118            "chardev=char0,mode=control,pretty=on",
119            "-machine",
120            "accel=kvm",
121            "-vga",
122            config.machine.vga,
123            "-m",
124            format!("{}M", config.machine.memory),
125            "-cpu",
126            config.machine.cpu_type,
127            "-smp",
128            format!(
129                "cpus={},cores={},maxcpus={}",
130                config.machine.cpus, config.machine.cpus, config.machine.cpus
131            ),
132            "-nic",
133            format!("user{}", self.hostfwd_rules(vm)?)
134        ];
135
136        v.append(&mut disks);
137
138        self.display_rule(&mut v, vm.headless());
139        self.cdrom_rules(&mut v, vm.cdrom(), (disks.len() + 2) as u8)?;
140        self.cdrom_rules(&mut v, vm.extra_disk(), (disks.len() + 3) as u8)?;
141
142        Ok(v)
143    }
144
145    pub fn qmp_command(&self, vm: &VM, mut f: impl FnMut(Client) -> Result<()>) -> Result<()> {
146        match Client::new(self.config.monitor_path(vm)) {
147            Ok(mut us) => {
148                us.handshake()?;
149                us.send_command::<GenericReturn>("qmp_capabilities", None)?;
150                f(us)?;
151            }
152            Err(_) => return Err(anyhow!("{} is not running or not monitored", vm)),
153        }
154
155        Ok(())
156    }
157}
158
159impl Launcher for QEmuLauncher {
160    fn delete_snapshot(&self, vm: &VM, name: String) -> Result<()> {
161        self.qmp_command(vm, |mut c| c.snapshot_delete(&name))?;
162        println!("Deleted snapshot '{}'", name);
163        Ok(())
164    }
165
166    fn snapshot(&self, vm: &VM, name: String) -> Result<()> {
167        self.qmp_command(vm, |mut c| c.snapshot_save(&name))?;
168        println!("Saved current state to snapshot '{}'", name);
169        Ok(())
170    }
171
172    fn restore(&self, vm: &VM, name: String) -> Result<()> {
173        self.qmp_command(vm, |mut c| c.snapshot_load(&name))?;
174        println!("Restored from snapshot '{}'", name);
175        Ok(())
176    }
177
178    fn reset(&self, vm: &VM) -> Result<()> {
179        self.qmp_command(vm, |mut c| {
180            c.send_command::<GenericReturn>("system_reset", None)?;
181            Ok(())
182        })
183    }
184
185    fn shutdown_immediately(&self, vm: &VM) -> Result<()> {
186        self.qmp_command(vm, |mut c| {
187            c.send_command::<GenericReturn>("system_powerdown", None)?;
188            Ok(())
189        })
190    }
191
192    fn shutdown_wait(&self, vm: &VM) -> Result<ExitStatus> {
193        self.shutdown_immediately(vm)?;
194
195        let pidfile = self.config.pidfile(vm);
196        let pid = read_to_string(pidfile.clone())?.parse::<u32>()?;
197        let mut total = Duration::new(0, 0);
198        let amount = Duration::new(0, 50);
199        while pid_running(pid) {
200            total += amount;
201            sleep(amount);
202            if amount > Duration::new(10, 0) {
203                println!("Waiting for qemu to quit...");
204                total = Duration::new(0, 0);
205            }
206        }
207        remove_file(pidfile)?;
208
209        Ok(ExitStatus::default())
210    }
211
212    fn launch_attached(&self, vm: &VM) -> Result<ExitStatus> {
213        let args = self.args(vm)?;
214        let mut cmd = Command::new(QEMU_BIN_NAME);
215        Ok(cmd.args(args).spawn()?.wait()?)
216    }
217
218    fn launch_detached(&self, vm: &VM) -> Result<()> {
219        let args = self.args(vm)?;
220        let mut cmd = Command::new(QEMU_BIN_NAME);
221        if let Ok(Fork::Child) = daemon(false, false) {
222            match cmd.args(args).spawn() {
223                Ok(mut child) => {
224                    std::fs::write(
225                        &self.config.pidfile(vm),
226                        format!("{}", child.id()).as_bytes(),
227                    )?;
228                    child.wait()?;
229                    Ok(())
230                }
231                Err(e) => Err(anyhow!(e)),
232            }
233        } else {
234            return Err(anyhow!("could not fork"));
235        }
236    }
237}