vagrant-qemu 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +1 -1
- data/lib/vagrant-qemu/driver.rb +224 -57
- data/lib/vagrant-qemu/errors.rb +12 -0
- data/lib/vagrant-qemu/version.rb +1 -1
- data/locales/en.yml +15 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29dd41685f09eaee73da2f6a10070679da9eb5220150efa47e4c016159264e0f
|
|
4
|
+
data.tar.gz: 65a2a89ee494f735f5de4b347517ad744a2c26b9dfdae55832a7572c216183c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb872c571092692bdcf7d5946ce85eeea75dede69ee619a56c34dd062ace730b36b0aba3c414e0b6c75ff2e2e530dd50ad77c574b5fe0294fae4deab63147d9d
|
|
7
|
+
data.tar.gz: deecca7a10a369f259df3c517db2b9d96a0b39b27a1050997897bb592944bcb678937fa8e4daadf28882543f4813626a95dd304bef94550bfd52ce54d25a131b
|
data/CHANGELOG.md
CHANGED
|
@@ -292,3 +292,41 @@ Project:
|
|
|
292
292
|
vmnet, socket_vmnet, VirtioFS) and VirtioFS in WSL2. (#85)
|
|
293
293
|
* The gem now ships only `lib/`, `locales/`, `LICENSE`, `README.md` and
|
|
294
294
|
`CHANGELOG.md`.
|
|
295
|
+
|
|
296
|
+
# 0.8.0 (2026-11-09)
|
|
297
|
+
|
|
298
|
+
Behavior changes:
|
|
299
|
+
|
|
300
|
+
* The QEMU pidfile and the runtime record `options.yml` moved from
|
|
301
|
+
`~/.vagrant.d/tmp/vagrant-qemu/<id>/` into the project, under
|
|
302
|
+
`.vagrant/machines/<name>/qemu/<id>/run/`. A removed tmp directory, or a
|
|
303
|
+
later command run under another `VAGRANT_HOME`, no longer makes a running VM
|
|
304
|
+
look stopped, which had left `vagrant halt` doing nothing and
|
|
305
|
+
`vagrant destroy` leaving QEMU running. The monitor and serial sockets stay
|
|
306
|
+
in the tmp directory, since macOS limits unix socket paths to 104 bytes;
|
|
307
|
+
their paths are recorded at launch, so halt and `vagrant serial-config`
|
|
308
|
+
still reach them. (#96)
|
|
309
|
+
* A VM started by 0.3.7 to 0.7.0 keeps its old layout while it runs: it is
|
|
310
|
+
still found, halted and destroyed as before, and moves to the new layout the
|
|
311
|
+
next time it starts. Only then is it protected as above. Reading the old
|
|
312
|
+
layout will be kept for at least one minor release.
|
|
313
|
+
* Halt your VMs before downgrading the plugin: older releases look only in the
|
|
314
|
+
tmp directory and report a VM started by 0.8.0 as stopped.
|
|
315
|
+
|
|
316
|
+
Fixes:
|
|
317
|
+
|
|
318
|
+
* A pidfile now counts only if the command line of its process contains that
|
|
319
|
+
pidfile, so a pidfile left behind by a crash or SIGKILL whose PID was reused
|
|
320
|
+
no longer reads as a running VM, and halt never signals that process.
|
|
321
|
+
* A VM whose QEMU runs as another user (for example started with
|
|
322
|
+
`sudo vagrant up`) no longer fails with a bare `Errno::EPERM` or
|
|
323
|
+
`Errno::EACCES`: the provider says to run the command as that user.
|
|
324
|
+
* When the QEMU monitor cannot be reached, halt sends SIGTERM right away, so
|
|
325
|
+
QEMU closes its disk images (the guest gets no shutdown request), and
|
|
326
|
+
SIGKILLs it if it is still running 10 seconds later. It no longer waits out
|
|
327
|
+
`graceful_timeout` twice and then SIGKILLs.
|
|
328
|
+
* A pidfile and sockets left after a SIGKILL are removed once QEMU is gone.
|
|
329
|
+
* If QEMU is still running 10 seconds after SIGKILL, halt and destroy now fail
|
|
330
|
+
with an error naming the PID. Before, halt reported success, destroy deleted
|
|
331
|
+
the VM's files and reload left the old QEMU running. A QEMU that has exited
|
|
332
|
+
and only waits to be reaped counts as gone.
|
data/README.md
CHANGED
|
@@ -188,7 +188,7 @@ This provider exposes a few provider-specific configuration options:
|
|
|
188
188
|
* `firmware_format` - The format of aarch64 firmware images (`edk2-aarch64-code.fd` and `edk2-arm-vars.fd`) loaded from `qemu_dir`, default: `raw`
|
|
189
189
|
* `other_default` - The other default arguments used by this plugin, default: `%W(-parallel none -monitor none -display none -vga none)`
|
|
190
190
|
* `extra_image_opts` - Options passed via `-o` to `qemu-img` when the base qcow2 images are created, default: `[]`
|
|
191
|
-
* `graceful_timeout` - Seconds to wait at each `vagrant halt` stage before escalating, default: `60`. Halt sends ACPI `system_powerdown`, waits up to this long, then sends QEMU's `quit` monitor command (a clean shutdown that flushes and closes the disk images), waits again, and finally SIGKILLs QEMU as a last resort — so halt still completes even when the guest was already halted from inside (e.g. `sudo systemctl halt`), where `system_powerdown` is a no-op
|
|
191
|
+
* `graceful_timeout` - Seconds to wait at each `vagrant halt` stage before escalating, default: `60`. Halt sends ACPI `system_powerdown`, waits up to this long, then sends QEMU's `quit` monitor command (a clean shutdown that flushes and closes the disk images), waits again, and finally SIGKILLs QEMU as a last resort — so halt still completes even when the guest was already halted from inside (e.g. `sudo systemctl halt`), where `system_powerdown` is a no-op. If the monitor cannot be reached (for example its socket was deleted), halt stops waiting on it: it sends SIGTERM, which makes QEMU close its disk images like `quit` but gives the guest no shutdown request, and SIGKILLs QEMU if it is still running 10 seconds later. If QEMU is still running 10 seconds after SIGKILL, halt and destroy fail with an error instead of reporting success or deleting the VM's files
|
|
192
192
|
* advanced networking (requires `advanced_network = true`)
|
|
193
193
|
* `advanced_network` - Enable dual-NIC advanced networking with `private_network` support, default: `false`
|
|
194
194
|
* `net_mode` - Network backend: `:auto` (detect by platform), `:vmnet_shared`, `:vmnet_host`, `:vmnet_bridged` (macOS, sudo), `:socket_vmnet` (macOS, no sudo — via the socket_vmnet daemon), `:tap` (Linux), `:socket` (QEMU `socket` netdev — multicast or point-to-point, see `socket_opts`), default: `:auto`
|
data/lib/vagrant-qemu/driver.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'log4r'
|
|
2
2
|
require 'childprocess'
|
|
3
|
+
require 'open3'
|
|
3
4
|
require 'securerandom'
|
|
4
5
|
require 'yaml'
|
|
5
6
|
|
|
@@ -11,10 +12,17 @@ require "vagrant/util/which"
|
|
|
11
12
|
|
|
12
13
|
require_relative "plugin"
|
|
13
14
|
require_relative "network"
|
|
15
|
+
require_relative "version"
|
|
14
16
|
|
|
15
17
|
module VagrantPlugins
|
|
16
18
|
module QEMU
|
|
17
19
|
class Driver
|
|
20
|
+
# Seconds to wait for QEMU to exit after SIGTERM before SIGKILL.
|
|
21
|
+
TERM_TIMEOUT = 10
|
|
22
|
+
# Seconds to wait after SIGKILL; a large guest's memory takes time to
|
|
23
|
+
# release.
|
|
24
|
+
KILL_TIMEOUT = 10
|
|
25
|
+
|
|
18
26
|
# @return [String] VM ID
|
|
19
27
|
attr_reader :vm_id
|
|
20
28
|
attr_reader :data_dir
|
|
@@ -45,10 +53,21 @@ module VagrantPlugins
|
|
|
45
53
|
|
|
46
54
|
def delete
|
|
47
55
|
if created?
|
|
56
|
+
sockets = socket_paths(runtime_dir)
|
|
48
57
|
id_dir = @data_dir.join(@vm_id)
|
|
49
58
|
FileUtils.rm_rf(id_dir)
|
|
50
59
|
id_tmp_dir = @tmp_dir.join(@vm_id)
|
|
51
60
|
FileUtils.rm_rf(id_tmp_dir)
|
|
61
|
+
|
|
62
|
+
# Sockets recorded under another VAGRANT_HOME's tmp dir.
|
|
63
|
+
sockets.each do |path|
|
|
64
|
+
FileUtils.rm_f(path)
|
|
65
|
+
begin
|
|
66
|
+
Dir.rmdir(File.dirname(path))
|
|
67
|
+
rescue SystemCallError
|
|
68
|
+
# Not empty, or already removed with id_tmp_dir.
|
|
69
|
+
end
|
|
70
|
+
end
|
|
52
71
|
end
|
|
53
72
|
end
|
|
54
73
|
|
|
@@ -65,30 +84,37 @@ module VagrantPlugins
|
|
|
65
84
|
|
|
66
85
|
id_tmp_dir = @tmp_dir.join(@vm_id)
|
|
67
86
|
FileUtils.mkdir_p(id_tmp_dir)
|
|
87
|
+
remove_legacy_runtime_files
|
|
88
|
+
FileUtils.mkdir_p(run_dir)
|
|
89
|
+
|
|
90
|
+
monitor_socket = id_tmp_dir.join("qemu_socket").to_s if options[:control_port].nil?
|
|
91
|
+
serial_socket = id_tmp_dir.join("qemu_socket_serial").to_s if options[:debug_port].nil?
|
|
68
92
|
|
|
69
|
-
# Persist only the runtime state we need to read back later
|
|
93
|
+
# Persist only the runtime state we need to read back later. The
|
|
94
|
+
# socket paths are recorded so that commands run under another
|
|
95
|
+
# VAGRANT_HOME still reach the sockets this launch created.
|
|
70
96
|
persisted_state = {
|
|
71
97
|
:ssh_port => options[:ssh_port],
|
|
72
98
|
:control_port => options[:control_port],
|
|
73
99
|
:debug_port => options[:debug_port],
|
|
100
|
+
:monitor_socket => monitor_socket,
|
|
101
|
+
:serial_socket => serial_socket,
|
|
102
|
+
:plugin_version => VERSION,
|
|
74
103
|
}
|
|
75
|
-
|
|
76
|
-
File.write(options_file, persisted_state.to_yaml)
|
|
104
|
+
File.write(run_dir.join("options.yml"), persisted_state.to_yaml)
|
|
77
105
|
|
|
78
106
|
control_socket = ""
|
|
79
107
|
if !options[:control_port].nil?
|
|
80
108
|
control_socket = "port=#{options[:control_port]},host=localhost,ipv4=on"
|
|
81
109
|
else
|
|
82
|
-
|
|
83
|
-
control_socket = "path=#{unix_socket_path}"
|
|
110
|
+
control_socket = "path=#{monitor_socket}"
|
|
84
111
|
end
|
|
85
112
|
|
|
86
113
|
debug_socket = ""
|
|
87
114
|
if !options[:debug_port].nil?
|
|
88
115
|
debug_socket = "port=#{options[:debug_port]},host=localhost,ipv4=on"
|
|
89
116
|
else
|
|
90
|
-
|
|
91
|
-
debug_socket = "path=#{unix_socket_serial_path}"
|
|
117
|
+
debug_socket = "path=#{serial_socket}"
|
|
92
118
|
end
|
|
93
119
|
|
|
94
120
|
cmd = []
|
|
@@ -196,7 +222,7 @@ module VagrantPlugins
|
|
|
196
222
|
end
|
|
197
223
|
|
|
198
224
|
# control
|
|
199
|
-
pid_file =
|
|
225
|
+
pid_file = run_dir.join("qemu.pid").to_s
|
|
200
226
|
cmd += %W(-chardev socket,id=mon0,#{control_socket},server=on,wait=off)
|
|
201
227
|
cmd += %W(-mon chardev=mon0,mode=readline)
|
|
202
228
|
cmd += %W(-chardev socket,id=ser0,#{debug_socket},server=on,wait=off)
|
|
@@ -222,80 +248,218 @@ module VagrantPlugins
|
|
|
222
248
|
end
|
|
223
249
|
|
|
224
250
|
def stop(options)
|
|
225
|
-
|
|
251
|
+
dir, pid = live_instance
|
|
252
|
+
return if pid.nil?
|
|
226
253
|
|
|
227
|
-
opts =
|
|
254
|
+
opts = with_persisted_monitor(options, dir)
|
|
228
255
|
timeout = options[:graceful_timeout] || 60
|
|
256
|
+
monitor = true
|
|
229
257
|
|
|
230
258
|
# 1. ACPI power button (graceful). Only works if a live guest OS is
|
|
231
259
|
# there to act on it -- e.g. NOT after `systemctl halt`, which leaves
|
|
232
260
|
# QEMU running with a halted, unresponsive guest.
|
|
233
261
|
unless options[:quit_only]
|
|
234
|
-
send_monitor(opts, "system_powerdown")
|
|
235
|
-
|
|
236
|
-
|
|
262
|
+
monitor = send_monitor(opts, "system_powerdown")
|
|
263
|
+
if monitor
|
|
264
|
+
return unless still_running_after?(timeout)
|
|
265
|
+
@logger.warn("VM did not power down within #{timeout}s; sending 'quit' to QEMU")
|
|
266
|
+
end
|
|
237
267
|
end
|
|
238
268
|
|
|
239
269
|
# 2. Powerdown didn't take. Ask QEMU itself to quit: it stops the VM,
|
|
240
270
|
# flushes and closes the qcow2 images, then exits cleanly. Does not
|
|
241
271
|
# depend on the guest, only on the monitor being reachable.
|
|
242
|
-
send_monitor(opts, "quit")
|
|
243
|
-
|
|
272
|
+
if monitor && send_monitor(opts, "quit")
|
|
273
|
+
return unless still_running_after?(timeout)
|
|
274
|
+
@logger.warn("VM still running after 'quit'; forcing kill")
|
|
275
|
+
else
|
|
276
|
+
# 3. The monitor is unreachable (e.g. its socket was removed), so
|
|
277
|
+
# there is nothing to wait for: SIGTERM makes QEMU shut down and
|
|
278
|
+
# close its images like 'quit', without telling the guest.
|
|
279
|
+
@logger.warn("QEMU monitor unreachable; sending SIGTERM")
|
|
280
|
+
return if terminate(pid)
|
|
281
|
+
@logger.warn("VM still running after SIGTERM; forcing kill")
|
|
282
|
+
end
|
|
244
283
|
|
|
245
|
-
#
|
|
246
|
-
|
|
247
|
-
|
|
284
|
+
# 4. Last resort: SIGKILL the QEMU process (no flush/cleanup). If it is
|
|
285
|
+
# still there, stop here rather than let halt report success or
|
|
286
|
+
# destroy delete the files of a running QEMU. Only a zombie counts
|
|
287
|
+
# as gone: one still releasing a large guest's memory no longer
|
|
288
|
+
# shows its argv to ps, so the identity check cannot tell.
|
|
289
|
+
return if force_kill(pid)
|
|
290
|
+
raise Errors::QemuDidNotExit, pid: pid if process_exists?(pid) && !zombie?(pid)
|
|
291
|
+
ensure
|
|
292
|
+
remove_runtime_leftovers(dir, pid) if pid
|
|
248
293
|
end
|
|
249
294
|
|
|
250
295
|
private
|
|
251
296
|
|
|
252
297
|
# QEMU unlinks its pidfile on exit, so the file can vanish between any
|
|
253
298
|
# existence check and the read: read it once and treat ENOENT as gone.
|
|
254
|
-
|
|
255
|
-
|
|
299
|
+
# QEMU creates it mode 0600, so a VM started under sudo leaves one that
|
|
300
|
+
# other users cannot read.
|
|
301
|
+
def read_pid(dir)
|
|
302
|
+
File.read(dir.join("qemu.pid")).to_i
|
|
256
303
|
rescue Errno::ENOENT
|
|
257
304
|
nil
|
|
305
|
+
rescue Errno::EACCES
|
|
306
|
+
raise Errors::PidfileNotReadable, path: dir.join("qemu.pid").to_s
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Runtime files (qemu.pid, options.yml) live in the project's machine
|
|
310
|
+
# data dir so a cleaned tmp dir or another VAGRANT_HOME cannot hide a
|
|
311
|
+
# running VM. The unix sockets stay in Vagrant's tmp dir: macOS caps
|
|
312
|
+
# socket paths at 104 bytes and project paths can be longer.
|
|
313
|
+
def run_dir
|
|
314
|
+
@data_dir.join(@vm_id, "run")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Where each layout keeps qemu.pid, with the ASCII tail of that path to
|
|
318
|
+
# look for in QEMU's command line. 0.3.7 to 0.7.0 used the tmp dir.
|
|
319
|
+
def runtime_layouts
|
|
320
|
+
{
|
|
321
|
+
run_dir => "/#{@vm_id}/run/qemu.pid",
|
|
322
|
+
@tmp_dir.join(@vm_id) => "/vagrant-qemu/#{@vm_id}/qemu.pid",
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# QEMU removes its pidfile and sockets on a clean exit, but not after
|
|
327
|
+
# SIGKILL. An older version's options.yml goes too, so the next start
|
|
328
|
+
# reads only the project-local layout. Nothing is removed while the PID
|
|
329
|
+
# still exists.
|
|
330
|
+
def remove_runtime_leftovers(dir, pid)
|
|
331
|
+
return if process_exists?(pid)
|
|
332
|
+
# Another VAGRANT_HOME holds no machine lock against us and may have
|
|
333
|
+
# started the VM again meanwhile.
|
|
334
|
+
current = read_pid(dir)
|
|
335
|
+
return if current && current != pid
|
|
336
|
+
files = [dir.join("qemu.pid").to_s, *socket_paths(dir)]
|
|
337
|
+
files << dir.join("options.yml").to_s if dir != run_dir
|
|
338
|
+
FileUtils.rm_f(files)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Unix sockets of the instance whose runtime files are in dir. Versions
|
|
342
|
+
# before the runtime record always used the tmp dir.
|
|
343
|
+
def socket_paths(dir)
|
|
344
|
+
persisted = persisted_options(dir)
|
|
345
|
+
if persisted.is_a?(Hash) && persisted.key?(:monitor_socket)
|
|
346
|
+
return [persisted[:monitor_socket], persisted[:serial_socket]].compact
|
|
347
|
+
end
|
|
348
|
+
[@tmp_dir.join(@vm_id, "qemu_socket").to_s, @tmp_dir.join(@vm_id, "qemu_socket_serial").to_s]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# 0.3.7 to 0.7.0 kept qemu.pid and options.yml in the tmp dir. Remove
|
|
352
|
+
# them before starting under the project-local layout, unless their PID
|
|
353
|
+
# still exists: a failed identity check alone never justifies deleting
|
|
354
|
+
# files a live process may own.
|
|
355
|
+
def remove_legacy_runtime_files
|
|
356
|
+
dir = @tmp_dir.join(@vm_id)
|
|
357
|
+
pid = read_pid(dir)
|
|
358
|
+
return if pid && pid > 0 && process_exists?(pid)
|
|
359
|
+
FileUtils.rm_f([dir.join("qemu.pid").to_s, dir.join("options.yml").to_s])
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# [runtime dir, PID] of this VM's running QEMU, or nil. Never cached:
|
|
363
|
+
# a reload stops an old-layout VM and starts it under the new one.
|
|
364
|
+
def live_instance
|
|
365
|
+
runtime_layouts.each do |dir, token|
|
|
366
|
+
pid = read_pid(dir)
|
|
367
|
+
next if pid.nil? || pid <= 0 || !process_exists?(pid)
|
|
368
|
+
return [dir, pid] if owned_by_vm?(pid, token)
|
|
369
|
+
end
|
|
370
|
+
nil
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# EPERM means the process exists but belongs to another user, e.g. a VM
|
|
374
|
+
# started with sudo.
|
|
375
|
+
def process_exists?(pid)
|
|
376
|
+
Process.kill(0, pid)
|
|
377
|
+
true
|
|
378
|
+
rescue Errno::ESRCH
|
|
379
|
+
false
|
|
380
|
+
rescue Errno::EPERM
|
|
381
|
+
true
|
|
258
382
|
end
|
|
259
383
|
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
384
|
+
# Guards against a stale pidfile whose PID was reused. Matches only the
|
|
385
|
+
# ASCII tail of the -pidfile argument: ps escapes non-ASCII bytes under
|
|
386
|
+
# the C locale, and argv keeps the launch-time path if the project
|
|
387
|
+
# directory is moved while the VM runs.
|
|
388
|
+
def owned_by_vm?(pid, token)
|
|
389
|
+
out, status = Open3.capture2e("ps", "-ww", "-p", pid.to_s, "-o", "args=")
|
|
390
|
+
return out.b.include?(token.b) if status.success?
|
|
391
|
+
process_exists?(pid)
|
|
392
|
+
rescue SystemCallError
|
|
393
|
+
# No usable ps (e.g. on Windows): keep the kill(0) answer.
|
|
394
|
+
true
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# ps state Z: killed and only waiting for its parent to reap it.
|
|
398
|
+
def zombie?(pid)
|
|
399
|
+
out, status = Open3.capture2e("ps", "-o", "stat=", "-p", pid.to_s)
|
|
400
|
+
status.success? && out.strip.start_with?("Z")
|
|
401
|
+
rescue SystemCallError
|
|
402
|
+
false
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Prefer the monitor endpoint the VM was actually started with
|
|
406
|
+
# (persisted in options.yml) so halt still works after a Vagrantfile
|
|
407
|
+
# edit or under another VAGRANT_HOME.
|
|
408
|
+
def with_persisted_monitor(options, dir)
|
|
409
|
+
persisted = persisted_options(dir)
|
|
410
|
+
return options unless persisted.is_a?(Hash)
|
|
411
|
+
|
|
412
|
+
options.merge(persisted.slice(:control_port, :monitor_socket))
|
|
413
|
+
end
|
|
265
414
|
|
|
266
|
-
|
|
267
|
-
|
|
415
|
+
# The live instance's runtime dir; for a stopped VM, the layout that
|
|
416
|
+
# last left an options.yml behind (project-local first).
|
|
417
|
+
def runtime_dir
|
|
418
|
+
live = live_instance
|
|
419
|
+
return live.first if live
|
|
420
|
+
run_dir.join("options.yml").file? ? run_dir : @tmp_dir.join(@vm_id)
|
|
421
|
+
end
|
|
268
422
|
|
|
269
|
-
|
|
423
|
+
def persisted_options(dir = runtime_dir)
|
|
424
|
+
# safe_load + File.read (not safe_load_file) so older Psych works too
|
|
425
|
+
YAML.safe_load(File.read(dir.join("options.yml")), permitted_classes: [Symbol])
|
|
426
|
+
rescue StandardError
|
|
427
|
+
nil
|
|
270
428
|
end
|
|
271
429
|
|
|
272
430
|
# Send a single QEMU monitor command over the control channel (TCP
|
|
273
431
|
# control_port if set, else the unix monitor socket). Best-effort: the
|
|
274
432
|
# socket may already be gone (e.g. QEMU exited from a prior command), so
|
|
275
|
-
# connection errors are swallowed.
|
|
433
|
+
# connection errors are swallowed. Returns whether the command was
|
|
434
|
+
# written; a reply cut short after that still counts as delivered.
|
|
276
435
|
# Drain replies until EOF with a deadline: immediately closing both
|
|
277
436
|
# halves can discard the command on QEMU's hangup event, while an
|
|
278
437
|
# unbounded read can bypass every shutdown timeout below.
|
|
279
438
|
def send_monitor(options, command)
|
|
439
|
+
sent = false
|
|
280
440
|
monitor_timeout = [[options[:graceful_timeout] || 60, 1].max, 5].min
|
|
281
441
|
if !options[:control_port].nil?
|
|
282
442
|
# start binds the TCP monitor with ipv4=on; avoid an IPv6 localhost
|
|
283
443
|
# attempt (and its refused-connection delay on Windows).
|
|
284
444
|
Socket.tcp("127.0.0.1", options[:control_port], connect_timeout: 5) do |sock|
|
|
285
445
|
sock.print "#{command}\n"
|
|
446
|
+
sent = true
|
|
286
447
|
sock.close_write
|
|
287
448
|
drain_monitor(sock, monitor_timeout)
|
|
288
449
|
end
|
|
289
450
|
else
|
|
290
|
-
unix_socket_path = @tmp_dir.join(@vm_id).join("qemu_socket").to_s
|
|
451
|
+
unix_socket_path = options[:monitor_socket] || @tmp_dir.join(@vm_id).join("qemu_socket").to_s
|
|
291
452
|
Socket.unix(unix_socket_path) do |sock|
|
|
292
453
|
sock.print "#{command}\n"
|
|
454
|
+
sent = true
|
|
293
455
|
sock.close_write
|
|
294
456
|
drain_monitor(sock, monitor_timeout)
|
|
295
457
|
end
|
|
296
458
|
end
|
|
459
|
+
sent
|
|
297
460
|
rescue => e
|
|
298
461
|
@logger.debug("monitor command '#{command}' failed: #{e}") if @logger
|
|
462
|
+
sent
|
|
299
463
|
end
|
|
300
464
|
|
|
301
465
|
def drain_monitor(sock, timeout)
|
|
@@ -317,21 +481,38 @@ module VagrantPlugins
|
|
|
317
481
|
running?
|
|
318
482
|
end
|
|
319
483
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
484
|
+
# True once QEMU is gone; false while it still runs or where SIGTERM is
|
|
485
|
+
# not supported (e.g. Windows), leaving SIGKILL to the caller.
|
|
486
|
+
def terminate(pid)
|
|
487
|
+
return true if pid.nil? || pid <= 0
|
|
488
|
+
Process.kill("TERM", pid)
|
|
489
|
+
!still_running_after?(TERM_TIMEOUT)
|
|
490
|
+
rescue Errno::ESRCH
|
|
491
|
+
true
|
|
492
|
+
rescue Errno::EPERM
|
|
493
|
+
raise Errors::QemuProcessNotOwned, pid: pid
|
|
494
|
+
rescue SystemCallError, ArgumentError
|
|
495
|
+
false
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# True once the process is gone.
|
|
499
|
+
def force_kill(pid)
|
|
500
|
+
return true if pid.nil? || pid <= 0
|
|
323
501
|
|
|
324
502
|
begin
|
|
325
503
|
Process.kill("KILL", pid)
|
|
326
504
|
rescue Errno::ESRCH
|
|
327
505
|
# Process already gone
|
|
506
|
+
rescue Errno::EPERM
|
|
507
|
+
raise Errors::QemuProcessNotOwned, pid: pid
|
|
328
508
|
end
|
|
329
509
|
|
|
330
510
|
# SIGKILL is asynchronous; callers check running? right after stop.
|
|
331
|
-
|
|
332
|
-
|
|
511
|
+
(KILL_TIMEOUT * 10).times do
|
|
512
|
+
return true unless process_exists?(pid)
|
|
333
513
|
sleep 0.1
|
|
334
514
|
end
|
|
515
|
+
!process_exists?(pid)
|
|
335
516
|
end
|
|
336
517
|
|
|
337
518
|
public
|
|
@@ -341,29 +522,23 @@ module VagrantPlugins
|
|
|
341
522
|
def serial_endpoint(default_port = nil)
|
|
342
523
|
return nil if @vm_id.nil?
|
|
343
524
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
options = YAML.safe_load(File.read(options_file), permitted_classes: [Symbol]) rescue nil
|
|
525
|
+
options = persisted_options
|
|
526
|
+
options = {} unless options.is_a?(Hash)
|
|
347
527
|
port = default_port
|
|
348
|
-
port = options[:debug_port] if options.
|
|
528
|
+
port = options[:debug_port] if options.key?(:debug_port)
|
|
349
529
|
|
|
350
530
|
if port.nil?
|
|
351
|
-
|
|
531
|
+
path = options[:serial_socket] || @tmp_dir.join(@vm_id, "qemu_socket_serial").to_s
|
|
532
|
+
{ type: :unix, path: path }
|
|
352
533
|
else
|
|
353
534
|
{ type: :tcp, host: "127.0.0.1", port: port }
|
|
354
535
|
end
|
|
355
536
|
end
|
|
356
537
|
|
|
357
538
|
def get_ssh_port(default_port)
|
|
358
|
-
|
|
359
|
-
options_file = id_tmp_dir.join("options.yml")
|
|
360
|
-
|
|
539
|
+
options = persisted_options
|
|
361
540
|
port = default_port
|
|
362
|
-
if
|
|
363
|
-
# safe_load + File.read (not safe_load_file) so older Psych works too
|
|
364
|
-
options = YAML.safe_load(File.read(options_file), permitted_classes: [Symbol]) rescue nil
|
|
365
|
-
port = options[:ssh_port] if !options.nil? && options.key?(:ssh_port)
|
|
366
|
-
end
|
|
541
|
+
port = options[:ssh_port] if options.is_a?(Hash) && options.key?(:ssh_port)
|
|
367
542
|
|
|
368
543
|
@ssh_port = port
|
|
369
544
|
end
|
|
@@ -419,15 +594,7 @@ module VagrantPlugins
|
|
|
419
594
|
end
|
|
420
595
|
|
|
421
596
|
def running?
|
|
422
|
-
|
|
423
|
-
return false if pid.nil? || pid <= 0
|
|
424
|
-
|
|
425
|
-
begin
|
|
426
|
-
Process.kill(0, pid)
|
|
427
|
-
true
|
|
428
|
-
rescue Errno::ESRCH
|
|
429
|
-
false
|
|
430
|
-
end
|
|
597
|
+
!live_instance.nil?
|
|
431
598
|
end
|
|
432
599
|
|
|
433
600
|
def execute(*cmd, **opts, &block)
|
data/lib/vagrant-qemu/errors.rb
CHANGED
|
@@ -45,6 +45,18 @@ module VagrantPlugins
|
|
|
45
45
|
error_key(:destroy_error)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
class QemuProcessNotOwned < VagrantQEMUError
|
|
49
|
+
error_key(:qemu_process_not_owned)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class PidfileNotReadable < VagrantQEMUError
|
|
53
|
+
error_key(:pidfile_not_readable)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class QemuDidNotExit < VagrantQEMUError
|
|
57
|
+
error_key(:qemu_did_not_exit)
|
|
58
|
+
end
|
|
59
|
+
|
|
48
60
|
class SocketVmnetNotMacos < VagrantQEMUError
|
|
49
61
|
error_key(:socket_vmnet_not_macos)
|
|
50
62
|
end
|
data/lib/vagrant-qemu/version.rb
CHANGED
data/locales/en.yml
CHANGED
|
@@ -105,6 +105,21 @@ en:
|
|
|
105
105
|
|
|
106
106
|
The machine ID has been preserved. Fix the underlying issue and
|
|
107
107
|
run `vagrant destroy` again.
|
|
108
|
+
qemu_process_not_owned: |-
|
|
109
|
+
The QEMU process of this VM (PID %{pid}) belongs to another user, so
|
|
110
|
+
it cannot be stopped from here. Run the command as the user that
|
|
111
|
+
started the VM, e.g. with `sudo` if `vagrant up` ran under sudo.
|
|
112
|
+
pidfile_not_readable: |-
|
|
113
|
+
The QEMU pidfile of this VM belongs to another user:
|
|
114
|
+
|
|
115
|
+
%{path}
|
|
116
|
+
|
|
117
|
+
This usually means the VM was started with `sudo vagrant up`. Run
|
|
118
|
+
this command as that user too, e.g. with `sudo`.
|
|
119
|
+
qemu_did_not_exit: |-
|
|
120
|
+
QEMU (PID %{pid}) is still running 10 seconds after SIGKILL, so the
|
|
121
|
+
VM's files were left in place. Check the process, e.g. with
|
|
122
|
+
`ps -p %{pid}`, and run the command again once it has exited.
|
|
108
123
|
socket_vmnet_not_macos: |-
|
|
109
124
|
net_mode = :socket_vmnet is only supported on macOS (it relies on
|
|
110
125
|
the socket_vmnet daemon and vmnet.framework).
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vagrant-qemu
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ppggff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Enables Vagrant to manage machines with QEMU.
|
|
14
14
|
email: pgf00a@gmail.com
|