Commit
+199 -240 +/-8 browse
1 | diff --git a/Makefile b/Makefile |
2 | index 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 |
46 | deleted file mode 100644 |
47 | index 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 |
70 | index 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 |
105 | new file mode 100644 |
106 | index 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 |
127 | index 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(®ex, 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 |
397 | deleted file mode 100755 |
398 | index 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 |
511 | deleted file mode 100644 |
512 | index 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 |
533 | new file mode 100755 |
534 | index 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" |