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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 462be36106f57c7732a3376ec6984c823196782401f5bd0d5efd6ef14869b525
4
- data.tar.gz: d4e3ef79a87a6349fb48a195c02252de0911ab0c3682dfba6552db011d248d39
3
+ metadata.gz: 29dd41685f09eaee73da2f6a10070679da9eb5220150efa47e4c016159264e0f
4
+ data.tar.gz: 65a2a89ee494f735f5de4b347517ad744a2c26b9dfdae55832a7572c216183c9
5
5
  SHA512:
6
- metadata.gz: ded7ec9fcddf013a9acb8caa8962bb4d84cd6f6126b306cdba508fca1ffe6457e9aa0e58c4594c9de5dccb74d6615da78d4733d2cdfb95d7aa21553cbf03fc00
7
- data.tar.gz: 3fa5f0ddddaeec9f9c26199ef80695760dffd65294b8ff2218257f3376ebe831ac62665ca26a6810116017bb612fe490d7521783d76438f63435532ac3e5e361
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`
@@ -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
- options_file = id_tmp_dir.join("options.yml")
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
- unix_socket_path = id_tmp_dir.join("qemu_socket").to_s
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
- unix_socket_serial_path = id_tmp_dir.join("qemu_socket_serial").to_s
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 = id_tmp_dir.join("qemu.pid").to_s
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
- return unless running?
251
+ dir, pid = live_instance
252
+ return if pid.nil?
226
253
 
227
- opts = with_persisted_control_port(options)
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
- return unless still_running_after?(timeout)
236
- @logger.warn("VM did not power down within #{timeout}s; sending 'quit' to QEMU")
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
- return unless still_running_after?(timeout)
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
- # 3. Last resort: SIGKILL the QEMU process (no flush/cleanup).
246
- @logger.warn("VM still running after 'quit'; forcing kill")
247
- force_kill
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
- def read_pid
255
- File.read(@tmp_dir.join(@vm_id).join("qemu.pid")).to_i
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
- # Prefer the control_port the VM was actually started with (persisted
261
- # in options.yml) so halt still works after a Vagrantfile edit.
262
- def with_persisted_control_port(options)
263
- options_file = @tmp_dir.join(@vm_id).join("options.yml")
264
- return options if !options_file.file?
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
- persisted = YAML.safe_load(File.read(options_file), permitted_classes: [Symbol]) rescue nil
267
- return options if persisted.nil? || !persisted.key?(:control_port)
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
- options.merge(:control_port => persisted[:control_port])
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
- def force_kill
321
- pid = read_pid
322
- return if pid.nil? || pid <= 0
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
- 20.times do
332
- break unless running?
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
- runtime_dir = @tmp_dir.join(@vm_id)
345
- options_file = runtime_dir.join("options.yml")
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.is_a?(Hash) && options.key?(:debug_port)
528
+ port = options[:debug_port] if options.key?(:debug_port)
349
529
 
350
530
  if port.nil?
351
- { type: :unix, path: runtime_dir.join("qemu_socket_serial").to_s }
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
- id_tmp_dir = @tmp_dir.join(@vm_id)
359
- options_file = id_tmp_dir.join("options.yml")
360
-
539
+ options = persisted_options
361
540
  port = default_port
362
- if options_file.file?
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
- pid = read_pid
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  module VagrantPlugins
2
2
  module QEMU
3
- VERSION = '0.7.0'
3
+ VERSION = '0.8.0'
4
4
  end
5
5
  end
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.7.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-10-15 00:00:00.000000000 Z
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