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
|
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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user