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}