Commit
Author: Kevin Schoon [me@kevinschoon.com]
Hash: fb80372226bb12b7708a7d3fb538bda48320716a
Timestamp: Fri, 07 Apr 2023 13:01:32 +0000 (1 year ago)

+199 -240 +/-8 browse
finished working implemention
1diff --git a/Makefile b/Makefile
2index b3fbc92..afc3b96 100644
3--- a/Makefile
4+++ b/Makefile
5 @@ -1,8 +1,35 @@
6- default: bin/qemu-shim
7+ CC := gcc
8
9- .PHONY: bin/qemu-shim
10- bin/qemu-shim:
11- gcc main.c -o $@
12+ TEST_SOCKET := /tmp/qemu-shim-test.socket
13+
14+ ANALYZE := -fanalyzer
15+
16+ bin/qemu-shim: bin
17+ ${CC} main.c -pedantic -std=c11 -Wall -o $@
18+
19+ test: clean bin/qemu-shim bin/fake-qemu
20+ TEST_SOCKET=${TEST_SOCKET} ./test.sh
21+
22+ clean:
23+ rm -r bin 2>/dev/null || true
24+
25+ install: bin/qemu-shim
26+ install -m 0755 bin/qemu-shim /usr/bin
27+ install -m 0644 contrib/qemu-shim@.service /usr/lib/systemd/user
28+
29+ uninstall:
30+ [[ -f /usr/bin/qemu-shim ]] && rm -v /usr/bin/qemu-shim || true
31+ [[ -f /usr/lib/systemd/user/qemu-shim@.service ]] && \
32+ rm -v /usr/lib/systemd/user/qemu-shim@.service || true
33+
34+ bin/fake-qemu: bin
35+ echo "#!/bin/sh" > $@
36+ echo "socat -v unix-listen:${TEST_SOCKET} /dev/null" >> $@
37+ echo "exit 0" >> $@
38+ chmod +x $@
39+
40+ analyze: bin
41+ ${CC} main.c ${ANALYZE} -pedantic -std=c11 -Wall -o /dev/null
42
43 bin:
44 mkdir -p bin
45 diff --git a/PKGBUILD b/PKGBUILD
46deleted file mode 100644
47index 5aa3003..0000000
48--- a/PKGBUILD
49+++ /dev/null
50 @@ -1,18 +0,0 @@
51- pkgname=qemu-shim
52- pkgver=0.0.2
53- pkgrel=1
54- pkgdesc="tiny qemu helper script"
55- arch=(x86_64)
56- license=('MIT')
57- md5sums=()
58- validpgpkeys=()
59-
60- build() {
61- cp ../qemu-shim .
62- cp ../qemu-shim@.service .
63- }
64-
65- package() {
66- install -Dm755 "${pkgname}" -t "${pkgdir}"/usr/bin/
67- install -Dm644 "qemu-shim@.service" -t "${pkgdir}"/usr/lib/systemd/user/
68- }
69 diff --git a/README.md b/README.md
70index 9a45199..aa6f6dd 100644
71--- a/README.md
72+++ b/README.md
73 @@ -1,23 +1,15 @@
74 # qemu-shim
75
76- A tiny script for running VMs with QEMU with graceful shutdown in a way that
77- is compatible with init systems like `systemd` or `OpenRC`. It was inspired by
78- Arch Linux's [QEMU wiki](https://wiki.archlinux.org/title/QEMU#With_systemd_service).
79- This script makes a few assumptions about the arguments it is given:
80+ A tiny process manager for QEMU that can gracefully shutdown VMs with interrupt
81+ signals. It was inspired by Arch Linux's [QEMU wiki](https://wiki.archlinux.org/title/QEMU#With_systemd_service).
82
83- * The args are a valid QEMU command
84- * They contain the -name flag
85- * They contain a -monitor flag with a unix socket specified. TCP/Telnet is not yet supported by the script.
86- * The guest OS supports ACPI shutdown signals
87+ The script makes a few assumptions about the arguments that it is given.
88
89+ * The args contain a valid QEMU command
90+ * The args contain a `-monitor` flag configured with a UNIX socket. TCP/Telnet is not supported.
91+ * The guest OS supports ACPI shutdown signals
92
93 ## Example
94 ```
95- NAME=fuu
96- qemu-shim qemu-system-x86_64 -name $NAME -m 2G -enable-kvm \
97- -boot order=d -drive ~/qemu/disks/$NAME.qcow \
98- -cpu host -smp 2 \
99- -vga qxl \
100- -spice unix=on,addr="/var/run/$UID/$NAME.socket,disable-ticketing=on" \
101- -monitor unix:/var/run/$UID/$NAME.socket,server,nowait
102+ qemu-shim qemu-system-x86_64 -monitor unix:/tmp/fuu.socket,server,nowait
103 ```
104 diff --git a/contrib/qemu-shim@.service b/contrib/qemu-shim@.service
105new file mode 100644
106index 0000000..1568c1b
107--- /dev/null
108+++ b/contrib/qemu-shim@.service
109 @@ -0,0 +1,16 @@
110+ [Unit]
111+ Description=QEMU Virtual Machine (qemu-shim)
112+
113+ [Service]
114+ KillMode=process
115+ ExecStart=/usr/bin/qemu-shim \
116+ qemu-system-x86_64 \
117+ -name %i \
118+ -m 512M \
119+ -enable-kvm \
120+ -device virtio-serial-pci \
121+ -spice unix=on,addr="%t/%i.socket,disable-ticketing=on" \
122+ -monitor "unix:%t/%i-monitor.socket,server,nowait"
123+
124+ [Install]
125+ WantedBy=default.target
126 diff --git a/main.c b/main.c
127index 3cc3d2f..8a72244 100644
128--- a/main.c
129+++ b/main.c
130 @@ -1,27 +1,122 @@
131+ #define _POSIX_SOURCE
132+ #include <errno.h>
133 #include <regex.h>
134+ #include <signal.h>
135+ #include <stdbool.h>
136 #include <stdio.h>
137 #include <stdlib.h>
138 #include <string.h>
139- #include <unistd.h>
140- #include <errno.h>
141- #include <signal.h>
142+ #include <sys/socket.h>
143+ #include <sys/time.h>
144+ #include <sys/types.h>
145+ #include <sys/un.h>
146 #include <sys/wait.h>
147+ #include <unistd.h>
148
149 #define _MONITOR_REGEXP "unix:\\([-/.[:alnum:]]*\\)"
150 #define _SPICE_REGEXP ".*addr=\\([-/.[:alnum:]]*\\)"
151+ #define _SHUTDOWN_CMD "system_powerdown\n"
152+ #define _GRACE_PERIOD 15
153+ #define _MAX_SOCKET_TIMEOUT 5
154+
155+ struct _runtime {
156+ char *socket_path;
157+ int *pid;
158+ };
159+
160+ struct _runtime _opts;
161+
162+ static void _check_code_and_exit(int status) {
163+ int result = WEXITSTATUS(status);
164+ if (result == 0) {
165+ puts("child process exited normally");
166+ exit(EXIT_SUCCESS);
167+ } else {
168+ printf("child process exited with code: %d\n", result);
169+ exit(EXIT_FAILURE);
170+ }
171+ }
172+
173+ static void _send_shutdown(char *path) {
174+ int s, len;
175+ struct sockaddr_un remote = {
176+ .sun_family = AF_UNIX,
177+ };
178+ char str[100];
179+ if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
180+ perror("socket");
181+ exit(1);
182+ }
183+ struct timeval tv;
184+ tv.tv_sec = _MAX_SOCKET_TIMEOUT;
185+ tv.tv_usec = 0;
186+ setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv);
187+ strcpy(remote.sun_path, _opts.socket_path);
188+ len = strlen(remote.sun_path) + sizeof(remote.sun_family);
189+ if (connect(s, (struct sockaddr *)&remote, len) == -1) {
190+ perror("connect");
191+ return;
192+ }
193+ printf("connected to %s\n", _opts.socket_path);
194+ if (send(s, _SHUTDOWN_CMD, strlen(_SHUTDOWN_CMD) + 1, 0) == -1) {
195+ perror("send");
196+ return;
197+ }
198+
199+ for (;;) {
200+ len = recv(s, str, sizeof(str) - 1, 0);
201+ switch (len) {
202+ case -1:
203+ perror("recv");
204+ close(s);
205+ return;
206+ case 0:
207+ close(s);
208+ return;
209+ }
210+ }
211+
212+ close(s);
213+ }
214+
215+ static void _shutdown() {
216+ _send_shutdown(_opts.socket_path);
217+ printf("process %d has %d seconds to shutdown\n", *_opts.pid, _GRACE_PERIOD);
218+ int n_seconds = 0;
219+ while (n_seconds < _GRACE_PERIOD) {
220+ int stat;
221+ if (waitpid(*_opts.pid, &stat, WNOHANG) == -1) {
222+ printf("child process stopped, good bye!\n");
223+ _check_code_and_exit(stat);
224+ };
225+ sleep(2);
226+ n_seconds = n_seconds + 2;
227+ }
228+ printf("grace period ended, sending SIGTERM\n");
229+ kill(*_opts.pid, SIGTERM);
230+ }
231+
232+ static void signal_handler(int sig) {
233+ printf("handling signal %d\n", sig);
234+ switch (sig) {
235+ case SIGTERM:
236+ _shutdown();
237+ case SIGINT:
238+ _shutdown();
239+ }
240+ }
241
242 char *_get_arg_by_name(int argc, char *argv[], char *name) {
243 char *res;
244 int i;
245 for (i = 0; i < argc; i++) {
246 if (strcmp(argv[i], name) == 0 && (i < argc)) {
247- res = malloc(strlen(argv[i + 1]));
248- strcpy(res, argv[i + 1]);
249+ res = argv[i + 1];
250 return res;
251 }
252 }
253 return NULL;
254- };
255+ }
256
257 char *_get_arg_with_regexp(int argc, char *argv[], char *name,
258 char *expression) {
259 @@ -43,14 +138,14 @@ char *_get_arg_with_regexp(int argc, char *argv[], char *name,
260
261 if (regexec(&regex, arg_raw, 2, groups, 0) == 0) {
262 if (groups[1].rm_so == (size_t)-1) {
263- fprintf(stderr, "cannot match against -montor flag:'%s'\n", arg_raw);
264+ fprintf(stderr, "cannot match against flag:'%s'\n", arg_raw);
265 exit(1);
266 }
267 strcpy(result, arg_raw);
268 result[groups[1].rm_eo] = 0;
269 result = result + groups[1].rm_so;
270 } else {
271- fprintf(stderr, "cannot match against -montor flag:'%s'\n", arg_raw);
272+ fprintf(stderr, "cannot match against flag:'%s'\n", arg_raw);
273 exit(1);
274 }
275
276 @@ -58,95 +153,48 @@ char *_get_arg_with_regexp(int argc, char *argv[], char *name,
277 return result;
278 }
279
280- FILE *run_qemu(int *pid) {
281+ void _do_fork(char *argv[], int *pid) {
282 pid_t child_pid;
283- int fd[2];
284- pipe(fd);
285 if ((child_pid = fork()) == -1) {
286- fprintf(stderr, "failed to fork\n");
287+ perror("fork");
288 exit(1);
289 }
290 if (child_pid == 0) {
291- close(fd[0]);
292- dup2(fd[1], 1);
293-
294 setpgid(child_pid, child_pid);
295- execl("/bin/sh", "/bin/sh", "-c", "for i in $(seq 1 10); do echo $i; sleep .5; done",
296- NULL);
297- exit(0);
298+ execvp(argv[0], argv);
299 } else {
300- close(fd[1]);
301+ *pid = child_pid;
302 }
303-
304- *pid = child_pid;
305- return fdopen(fd[0], "r");
306- }
307-
308- int shutdown(FILE *fp, pid_t pid) {
309- int stat;
310- fclose(fp);
311- while (waitpid(pid, &stat, 0) == -1) {
312- if (errno != EINTR) {
313- stat = -1;
314- break;
315- }
316- }
317- return stat;
318 }
319
320 int main(int argc, char *argv[]) {
321- char *name;
322- name = _get_arg_by_name(argc, argv, "-name");
323- int result;
324- if (name == NULL) {
325- printf("-name flag was not specified\n");
326- return EXIT_FAILURE;
327- }
328- printf("NAME: %s\n", name);
329- char *monitor_path;
330+ char *monitor_path = NULL;
331 monitor_path = _get_arg_with_regexp(argc, argv, "-monitor", _MONITOR_REGEXP);
332 if (monitor_path == NULL) {
333- printf("-monitor flag was not specified");
334+ printf("-monitor flag was not specified\n");
335 return EXIT_FAILURE;
336 }
337- printf("MONITOR: %s\n", monitor_path);
338- char *spice_path;
339- spice_path = _get_arg_with_regexp(argc, argv, "-spice", _SPICE_REGEXP);
340- if (spice_path != NULL) {
341- printf("SPICE: %s\n", spice_path);
342+ _opts.socket_path = monitor_path;
343+ char *_copy_args[argc];
344+ _copy_args[argc - 1] = NULL;
345+ for (int i = 1; i < argc; ++i) {
346+ _copy_args[i - 1] = argv[i];
347 }
348- int pid;
349- FILE *fp;
350- fp = run_qemu(&pid);
351- char output[100] = {0};
352
353- while (read(fileno(fp), output, sizeof(output)-1) != 0) {
354- printf("%s", output);
355- }
356+ signal(SIGTERM, signal_handler);
357+ signal(SIGINT, signal_handler);
358
359- shutdown(fp, pid);
360- printf("..Done?\n");
361-
362- /* CATCH SIGNAL TODO */
363- /* POPEN TODO */
364+ int pid = 0;
365+ int stat;
366
367- /*
368- FILE *fp;
369- int status;
370- char buf[128];
371- fp = popen("ls", "r");
372- if (fp == NULL) {
373- puts("failed to open process");
374- return EXIT_FAILURE;
375- }
376- while (fgets(buf, 128, fp) != NULL) {
377- printf("%s", buf);
378- }
379- status = pclose(fp);
380- if (status != 0) {
381- printf("non-zero exit code");
382- return status;
383+ _do_fork(_copy_args, &pid);
384+ _opts.pid = &pid;
385+ printf("monitoring pid %d\n", pid);
386+ while (waitpid(pid, &stat, 0) == -1) {
387+ if (errno != EINTR) {
388+ stat = -1;
389+ break;
390+ }
391 }
392- return EXIT_SUCCESS;
393- */
394+ _check_code_and_exit(stat);
395 }
396 diff --git a/qemu-shim b/qemu-shim
397deleted file mode 100755
398index a5ee06c..0000000
399--- a/qemu-shim
400+++ /dev/null
401 @@ -1,108 +0,0 @@
402- #!/usr/bin/env python
403- # tiny script for running VMs with QEMU with graceful shutdown
404-
405- import os
406- import re
407- import signal
408- import socket
409- import subprocess
410- import sys
411- import time
412-
413-
414- _shutdown_timeout = os.getenv("QEMU_SHIM_TIMEOUT")
415- if _shutdown_timeout:
416- _shutdown_timeout = int(_shutdown_timeout)
417- else:
418- _shutdown_timeout = 15
419-
420-
421- def _read_all(client: socket.socket):
422- while True:
423- try:
424- client.recv(1024)
425- except TimeoutError:
426- break
427-
428- def _get_arg_by_name(args, name):
429- for i, arg in enumerate(args):
430- if arg == name:
431- try:
432- return args[i+1]
433- except IndexError:
434- pass
435-
436- def _get_first_group(match):
437- if match and len(match.groups()) > 0:
438- return match.group(1)
439-
440- def _get_name(args: list[str]):
441- return _get_arg_by_name(args, "-name")
442-
443- def _get_spice_path(args: list[str]):
444- spice_arg = _get_arg_by_name(args, "-spice")
445- if spice_arg:
446- return _get_first_group(
447- re.compile("^.*addr=([\w\/\-\.]*)").match(spice_arg))
448-
449- def _get_monitor_path(name):
450- monitor_arg = _get_arg_by_name(args, "-monitor")
451- if monitor_arg:
452- return _get_first_group(
453- re.compile("^unix:([\w\/\-\.]*)").match(monitor_arg))
454-
455- def run_vm(args):
456-
457- name = _get_name(args)
458- monitor_path = _get_monitor_path(args)
459- spice_path = _get_spice_path(args)
460-
461- if name is None:
462- print("-name flag was not specified in qemu command")
463- sys.exit(1)
464- if monitor_path is None:
465- print("-monitor flag was not specified in qemu command")
466- sys.exit(1)
467-
468- print(f"launching QEMU virtual machine {name}")
469-
470- proc = subprocess.Popen(args=args, stdout=sys.stderr, stderr=sys.stderr,
471- preexec_fn=os.setpgrp)
472-
473- while True:
474- try:
475- os.stat(monitor_path)
476- break
477- except FileNotFoundError:
478- time.sleep(1)
479-
480- def cleanup(signum, frame):
481- print(f"caught signal, shutting down via QMP @ {monitor_path}")
482- with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
483- client.connect(monitor_path)
484- client.setblocking(True)
485- client.settimeout(1)
486- _read_all(client)
487- client.send(b"system_powerdown\n")
488- _read_all(client)
489- client.close()
490-
491- print(f"waiting {_shutdown_timeout}s")
492- time.sleep(_shutdown_timeout)
493- print("timeout exceeded, signaling QEMU process")
494- proc.send_signal(signal.SIGHUP)
495-
496- signal.signal(signal.SIGINT, cleanup)
497- signal.signal(signal.SIGTERM, cleanup)
498-
499- retcode = proc.wait()
500- if spice_path:
501- os.remove(spice_path)
502-
503- print(f"QEMU process ended with code {retcode}")
504- sys.exit(retcode)
505-
506-
507- if __name__ == "__main__":
508- args = sys.argv[1:]
509- run_vm(args)
510 diff --git a/qemu-shim@.service b/qemu-shim@.service
511deleted file mode 100644
512index 1568c1b..0000000
513--- a/qemu-shim@.service
514+++ /dev/null
515 @@ -1,16 +0,0 @@
516- [Unit]
517- Description=QEMU Virtual Machine (qemu-shim)
518-
519- [Service]
520- KillMode=process
521- ExecStart=/usr/bin/qemu-shim \
522- qemu-system-x86_64 \
523- -name %i \
524- -m 512M \
525- -enable-kvm \
526- -device virtio-serial-pci \
527- -spice unix=on,addr="%t/%i.socket,disable-ticketing=on" \
528- -monitor "unix:%t/%i-monitor.socket,server,nowait"
529-
530- [Install]
531- WantedBy=default.target
532 diff --git a/test.sh b/test.sh
533new file mode 100755
534index 0000000..b9faa6e
535--- /dev/null
536+++ b/test.sh
537 @@ -0,0 +1,18 @@
538+ #!/bin/sh
539+ set -xe
540+
541+ [[ -z "$TEST_SOCKET" ]] && {
542+ echo "need to specify TEST_SOCKET"
543+ }
544+
545+ [[ -S "$TEST_SOCKET" ]] && rm -v "$TEST_SOCKET"
546+
547+ bin/qemu-shim bin/fake-qemu -monitor "unix:$TEST_SOCKET" &
548+
549+ QEMU_SHIM_PID="$!"
550+
551+ sleep 1
552+
553+ kill -INT "$QEMU_SHIM_PID"
554+
555+ wait "$QEMU_SHIM_PID"