fix(mounts): bei Zeitüberschreitung ganze Prozessgruppe von mount beenden
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1bf0fae2fc
commit
4f9c5bdc1d
@@ -8,7 +8,9 @@ Optionszeile – die stünde sonst in der Prozessliste.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import uuid
|
||||
from collections.abc import Callable, Sequence
|
||||
@@ -24,6 +26,28 @@ log = get_logger(__name__)
|
||||
|
||||
Runner = Callable[..., "subprocess.CompletedProcess[str]"]
|
||||
|
||||
|
||||
def run_process_group(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
||||
"""Wie ``subprocess.run``, aber in eigener Prozessgruppe: Bei Zeitüberschreitung
|
||||
wird die ganze Gruppe beendet. ``mount`` startet Hilfsprogramme wie
|
||||
``mount.nfs``; würde nur ``mount`` beendet, liefe ``mount.nfs`` weiter und
|
||||
hängte womöglich Minuten später doch noch ein (auf der Test-VM beobachtet)."""
|
||||
timeout = kwargs.pop("timeout", None)
|
||||
kwargs.pop("check", None)
|
||||
capture = kwargs.pop("capture_output", False)
|
||||
if capture:
|
||||
kwargs["stdout"] = kwargs["stderr"] = subprocess.PIPE
|
||||
with subprocess.Popen(argv, start_new_session=True, **kwargs) as proc:
|
||||
try:
|
||||
out, err = proc.communicate(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
with contextlib.suppress(ProcessLookupError):
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
proc.communicate()
|
||||
raise
|
||||
return subprocess.CompletedProcess(argv, proc.returncode, out, err)
|
||||
|
||||
|
||||
_ENV = {"LC_ALL": "C", "PATH": "/usr/sbin:/usr/bin:/sbin:/bin"}
|
||||
|
||||
|
||||
@@ -60,7 +84,7 @@ class LinuxMountBackend(MountBackend):
|
||||
self,
|
||||
run_dir: Path,
|
||||
mountinfo_path: Path = MOUNTINFO,
|
||||
runner: Runner = subprocess.run,
|
||||
runner: Runner = run_process_group,
|
||||
) -> None:
|
||||
self.run_dir = run_dir
|
||||
self.mountinfo_path = mountinfo_path
|
||||
|
||||
@@ -115,3 +115,38 @@ def test_is_mounted_reads_mountinfo(tmp_path: Path) -> None:
|
||||
)
|
||||
def test_explain(fstype: str, msg: str, expect: str) -> None:
|
||||
assert expect in explain_mount_error(fstype, msg)
|
||||
|
||||
|
||||
def _alive(pid: int) -> bool:
|
||||
try:
|
||||
with open(f"/proc/{pid}/stat") as fh:
|
||||
return fh.read().split(")")[-1].split()[0] != "Z"
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
def test_timeout_kills_whole_process_group(tmp_path: Path) -> None:
|
||||
"""mount startet Helfer (mount.nfs) – die dürfen nach dem Zeitlimit nicht weiterlaufen."""
|
||||
from netmover.mounting.linux import run_process_group
|
||||
|
||||
pidfile = tmp_path / "helper.pid"
|
||||
script = f"sleep 30 & echo $! > {pidfile}; wait"
|
||||
with pytest.raises(subprocess.TimeoutExpired):
|
||||
run_process_group(["sh", "-c", script], capture_output=True, text=True, timeout=0.5)
|
||||
helper = int(pidfile.read_text())
|
||||
for _ in range(20):
|
||||
if not _alive(helper):
|
||||
break
|
||||
import time
|
||||
|
||||
time.sleep(0.05)
|
||||
assert not _alive(helper)
|
||||
|
||||
|
||||
def test_process_group_runner_returns_output() -> None:
|
||||
from netmover.mounting.linux import run_process_group
|
||||
|
||||
r = run_process_group(
|
||||
["sh", "-c", "echo out; echo err >&2; exit 3"], capture_output=True, text=True, timeout=5
|
||||
)
|
||||
assert (r.returncode, r.stdout, r.stderr) == (3, "out\n", "err\n")
|
||||
|
||||
Reference in New Issue
Block a user