Skip to content

Commit

Permalink
squash: kill the invocation
Browse files Browse the repository at this point in the history
  • Loading branch information
happz committed Nov 29, 2023
1 parent 44176b6 commit 6da1466
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 5 deletions.
10 changes: 8 additions & 2 deletions tmt/checks/watchdog.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ class WatchdogCheck(Check):
ssh_ping: bool = field(default=False)
ssh_ping_threshold: int = field(default=10)

def notify(self, invocation: 'TestInvocation') -> None:
""" Notify invocation that hard reboot is required """

invocation.hard_reboot_requested = True
invocation.terminate_process()

def do_ping(
self,
invocation: 'TestInvocation',
Expand Down Expand Up @@ -183,7 +189,7 @@ def _handle_output(ping_output: str) -> None:
if guest_context.ping_failures >= self.ping_threshold:
logger.fail(f'exhausted {self.ping_threshold} ping attempts')

invocation.hard_reboot_requested = True
self.notify(invocation)

try:
assert invocation.guest.guest is not None # narrow type
Expand Down Expand Up @@ -290,7 +296,7 @@ def _success(ncat_output: str) -> None:
if guest_context.ping_failures >= self.ping_threshold:
logger.fail(f'exhausted {self.ping_threshold} ping attempts')

invocation.hard_reboot_requested = True
self.notify(invocation)


@provides_check('watchdog')
Expand Down
38 changes: 37 additions & 1 deletion tmt/steps/execute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import datetime
import json
import os
import signal as _signal
import subprocess
import threading
from contextlib import suppress
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast
Expand Down Expand Up @@ -157,6 +159,7 @@ class TestInvocation:
#: implementation and the test, it may be, for example, a shell process,
#: SSH process, or a ``podman`` process.
process: Optional[subprocess.Popen[bytes]] = None
process_lock: threading.Lock = field(default_factory=threading.Lock)

check_data: dict[str, Any] = field(default_factory=dict)

Expand Down Expand Up @@ -202,6 +205,7 @@ def reboot_request_path(self) -> Path:

@property
def soft_reboot_requested(self) -> bool:
""" If set, test requested a reboot """
return self.reboot_request_path.exists()

#: If set, an asynchronous observer requested a reboot while the test was
Expand All @@ -210,7 +214,7 @@ def soft_reboot_requested(self) -> bool:

@property
def reboot_requested(self) -> bool:
""" Whether a guest reboot has been requested by the test """
""" Whether a guest reboot has been requested while the test was running """
return self.soft_reboot_requested or self.hard_reboot_requested

def handle_reboot(self) -> bool:
Expand Down Expand Up @@ -285,6 +289,38 @@ def handle_reboot(self) -> bool:

return True

def terminate_process(
self,
signal: _signal.Signals = _signal.SIGTERM,
logger: Optional[tmt.log.Logger] = None) -> None:
"""
Terminate the invocation process.
.. warning::
This method should be used carefully. Process managing the
invocation has been started by by some part of tmt code which is
responsible for its well-being. Unless you have a really good reason
to do so, doing things behind the tmt's back may lead to unexpected
results.
:param signal: signal to send to the invocation process.
:param logger: logger to use for logging.
"""

logger = logger or self.logger

with self.process_lock:
if self.process is None:
logger.debug('Test invocation process cannot be terminated because it is unset.',
level=3)

return

logger.debug(f'Terminating process with {signal.name}.', level=3)

self.process.send_signal(signal)


class ExecutePlugin(tmt.steps.Plugin[ExecuteStepDataT]):
""" Common parent of execute plugins """
Expand Down
7 changes: 5 additions & 2 deletions tmt/steps/execute/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ def _save_process(
command: Command,
process: subprocess.Popen[bytes],
logger: tmt.log.Logger) -> None:
invocation.process = process
with invocation.process_lock:
invocation.process = process

# TODO: do we want timestamps? Yes, we do, leaving that for refactoring later,
# to use some reusable decorator.
Expand Down Expand Up @@ -363,7 +364,9 @@ def _save_process(
elif tmt.utils.ProcessExitCodes.is_pidfile(invocation.return_code):
logger.warn('Test failed to manage its pidfile.')

invocation.process = None
with invocation.process_lock:
invocation.process = None

invocation.end_time = self.format_timestamp(timer.end_time)
invocation.real_duration = self.format_duration(timer.duration)

Expand Down

0 comments on commit 6da1466

Please sign in to comment.