1"""Abruptly stop a disposable lab VM while preserving its disk for recovery."""
2import argparse
3import json
4import os
5import signal
6import subprocess
7import time
8
9import linux_vm
10import vm
11
12
13def stop(kind, name):
14 backend = {'windows': vm, 'linux': linux_vm}[kind]
15 backend.load_instance(name)
16 if not backend.running(name):
17 raise ValueError('Choose a running disposable VM.')
18 _, socket_path, pid_path, *_ = backend.runtime(name)
19 pid = int(pid_path.read_text())
20 expected = ('OneNote Windows 7 ' if kind == 'windows' else 'OneNote Linux Samba ') + name
21 if backend.qmp(name, 'query-name').get('name') != expected:
22 raise ValueError('The VM identity differs from its registered name.')
23 command = subprocess.check_output(['ps', '-p', str(pid), '-o', 'command='], text=True)
24 if str(socket_path) not in command or 'qemu-system-' not in command:
25 raise ValueError('The registered process does not own this VM socket.')
26 started = time.time_ns()
27 os.kill(pid, signal.SIGKILL)
28 deadline = time.monotonic() + 15
29 while time.monotonic() < deadline:
30 try:
31 os.waitpid(pid, os.WNOHANG)
32 except ChildProcessError:
33 pass
34 if not backend.running(name):
35 break
36 time.sleep(.1)
37 if backend.running(name):
38 raise TimeoutError('The VM process did not stop.')
39 return {'kind': kind, 'name': name, 'pid': pid, 'signal': 'SIGKILL',
40 'started_ns': started, 'stopped_ns': time.time_ns(),
41 'machine_preserved': backend.instance_path(name).exists()}
42
43
44if __name__ == '__main__':
45 parser = argparse.ArgumentParser(description=__doc__)
46 parser.add_argument('kind', choices=['windows', 'linux'])
47 parser.add_argument('name')
48 args = parser.parse_args()
49 print(json.dumps(stop(args.kind, args.name), indent=2))