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:
Florian Wilde
2026-10-10 21:56:18 +02:00
co-authored by Claude Opus 5.5
parent 1bf0fae2fc
commit 4f9c5bdc1d
2 changed files with 60 additions and 1 deletions
+25 -1
View File
@@ -8,7 +8,9 @@ Optionszeile – die stünde sonst in der Prozessliste.
from __future__ import annotations from __future__ import annotations
import contextlib
import os import os
import signal
import subprocess import subprocess
import uuid import uuid
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
@@ -24,6 +26,28 @@ log = get_logger(__name__)
Runner = Callable[..., "subprocess.CompletedProcess[str]"] 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"} _ENV = {"LC_ALL": "C", "PATH": "/usr/sbin:/usr/bin:/sbin:/bin"}
@@ -60,7 +84,7 @@ class LinuxMountBackend(MountBackend):
self, self,
run_dir: Path, run_dir: Path,
mountinfo_path: Path = MOUNTINFO, mountinfo_path: Path = MOUNTINFO,
runner: Runner = subprocess.run, runner: Runner = run_process_group,
) -> None: ) -> None:
self.run_dir = run_dir self.run_dir = run_dir
self.mountinfo_path = mountinfo_path self.mountinfo_path = mountinfo_path
+35
View File
@@ -115,3 +115,38 @@ def test_is_mounted_reads_mountinfo(tmp_path: Path) -> None:
) )
def test_explain(fstype: str, msg: str, expect: str) -> None: def test_explain(fstype: str, msg: str, expect: str) -> None:
assert expect in explain_mount_error(fstype, msg) 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")