Skip to content

Commit 193bf1c

Browse files
jnsnowphilmd
authored andcommitted
python/machine.py: split shutdown into hard and soft flavors
This is done primarily to avoid the 'bare except' pattern, which suppresses all exceptions during shutdown and can obscure errors. Replace this with a pattern that isolates the different kind of shutdown paradigms (_hard_shutdown and _soft_shutdown), and a new fallback shutdown handler (_do_shutdown) that gracefully attempts one before the other. This split now also ensures that no matter what happens, _post_shutdown() is always invoked. shutdown() changes in behavior such that if it attempts to do a graceful shutdown and is unable to, it will now always raise an exception to indicate this. This can be avoided by the test writer in three ways: 1. If the VM is expected to have already exited or is in the process of exiting, wait() can be used instead of shutdown() to clean up resources instead. This helps avoid race conditions in shutdown. 2. If a test writer is expecting graceful shutdown to fail, shutdown should be called in a try...except block. 3. If the test writer has no interest in performing a graceful shutdown at all, kill() can be used instead. Handling shutdown in this way makes it much more explicit which type of shutdown we want and allows the library to report problems with this process. Signed-off-by: John Snow <[email protected]> Reviewed-by: Philippe Mathieu-Daudé <[email protected]> Reviewed-by: Cleber Rosa <[email protected]> Tested-by: Cleber Rosa <[email protected]> Message-Id: <[email protected]> Signed-off-by: Philippe Mathieu-Daudé <[email protected]>
1 parent fdb87f0 commit 193bf1c

File tree

1 file changed

+83
-15
lines changed

1 file changed

+83
-15
lines changed

python/qemu/machine.py

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ class QEMUMachineAddDeviceError(QEMUMachineError):
4949
"""
5050

5151

52+
class AbnormalShutdown(QEMUMachineError):
53+
"""
54+
Exception raised when a graceful shutdown was requested, but not performed.
55+
"""
56+
57+
5258
class MonitorResponseError(qmp.QMPError):
5359
"""
5460
Represents erroneous QMP monitor reply
@@ -376,6 +382,7 @@ def _early_cleanup(self) -> None:
376382
"""
377383
Perform any cleanup that needs to happen before the VM exits.
378384
385+
May be invoked by both soft and hard shutdown in failover scenarios.
379386
Called additionally by _post_shutdown for comprehensive cleanup.
380387
"""
381388
# If we keep the console socket open, we may deadlock waiting
@@ -385,32 +392,93 @@ def _early_cleanup(self) -> None:
385392
self._console_socket.close()
386393
self._console_socket = None
387394

395+
def _hard_shutdown(self) -> None:
396+
"""
397+
Perform early cleanup, kill the VM, and wait for it to terminate.
398+
399+
:raise subprocess.Timeout: When timeout is exceeds 60 seconds
400+
waiting for the QEMU process to terminate.
401+
"""
402+
self._early_cleanup()
403+
self._popen.kill()
404+
self._popen.wait(timeout=60)
405+
406+
def _soft_shutdown(self, has_quit: bool = False,
407+
timeout: Optional[int] = 3) -> None:
408+
"""
409+
Perform early cleanup, attempt to gracefully shut down the VM, and wait
410+
for it to terminate.
411+
412+
:param has_quit: When True, don't attempt to issue 'quit' QMP command
413+
:param timeout: Optional timeout in seconds for graceful shutdown.
414+
Default 3 seconds, A value of None is an infinite wait.
415+
416+
:raise ConnectionReset: On QMP communication errors
417+
:raise subprocess.TimeoutExpired: When timeout is exceeded waiting for
418+
the QEMU process to terminate.
419+
"""
420+
self._early_cleanup()
421+
422+
if self._qmp is not None:
423+
if not has_quit:
424+
# Might raise ConnectionReset
425+
self._qmp.cmd('quit')
426+
427+
# May raise subprocess.TimeoutExpired
428+
self._popen.wait(timeout=timeout)
429+
430+
def _do_shutdown(self, has_quit: bool = False,
431+
timeout: Optional[int] = 3) -> None:
432+
"""
433+
Attempt to shutdown the VM gracefully; fallback to a hard shutdown.
434+
435+
:param has_quit: When True, don't attempt to issue 'quit' QMP command
436+
:param timeout: Optional timeout in seconds for graceful shutdown.
437+
Default 3 seconds, A value of None is an infinite wait.
438+
439+
:raise AbnormalShutdown: When the VM could not be shut down gracefully.
440+
The inner exception will likely be ConnectionReset or
441+
subprocess.TimeoutExpired. In rare cases, non-graceful termination
442+
may result in its own exceptions, likely subprocess.TimeoutExpired.
443+
"""
444+
try:
445+
self._soft_shutdown(has_quit, timeout)
446+
except Exception as exc:
447+
self._hard_shutdown()
448+
raise AbnormalShutdown("Could not perform graceful shutdown") \
449+
from exc
450+
388451
def shutdown(self, has_quit: bool = False,
389452
hard: bool = False,
390453
timeout: Optional[int] = 3) -> None:
391454
"""
392-
Terminate the VM and clean up
455+
Terminate the VM (gracefully if possible) and perform cleanup.
456+
Cleanup will always be performed.
457+
458+
If the VM has not yet been launched, or shutdown(), wait(), or kill()
459+
have already been called, this method does nothing.
460+
461+
:param has_quit: When true, do not attempt to issue 'quit' QMP command.
462+
:param hard: When true, do not attempt graceful shutdown, and
463+
suppress the SIGKILL warning log message.
464+
:param timeout: Optional timeout in seconds for graceful shutdown.
465+
Default 3 seconds, A value of None is an infinite wait.
393466
"""
394467
if not self._launched:
395468
return
396469

397-
self._early_cleanup()
398-
399-
if self.is_running():
470+
try:
400471
if hard:
401-
self._popen.kill()
402-
elif self._qmp:
403-
try:
404-
if not has_quit:
405-
self._qmp.cmd('quit')
406-
self._popen.wait(timeout=timeout)
407-
except:
408-
self._popen.kill()
409-
self._popen.wait(timeout=timeout)
410-
411-
self._post_shutdown()
472+
self._hard_shutdown()
473+
else:
474+
self._do_shutdown(has_quit, timeout=timeout)
475+
finally:
476+
self._post_shutdown()
412477

413478
def kill(self):
479+
"""
480+
Terminate the VM forcefully, wait for it to exit, and perform cleanup.
481+
"""
414482
self.shutdown(hard=True)
415483

416484
def wait(self, timeout: Optional[int] = None) -> None:

0 commit comments

Comments
 (0)