mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 01:36:15 -04:00
python: kill the sidecar when its parent dies
PR_SET_PDEATHSIG on the spawned lightpanda mcp process (Linux) so a SIGKILLed or crashed interpreter cannot leak a browser process — a 2h44m orphan surfaced during the v6 benchmark campaign. Regression test kills a parent and asserts the sidecar exits.
This commit is contained in:
@@ -29,6 +29,19 @@ _SPAWN_ATTEMPTS = 3
|
||||
_READY_TIMEOUT = 15.0
|
||||
|
||||
|
||||
def _die_with_parent():
|
||||
"""Linux: ask the kernel to SIGTERM the sidecar when the parent dies,
|
||||
so a SIGKILLed interpreter can't leak a browser process."""
|
||||
try:
|
||||
import ctypes
|
||||
import signal as _signal
|
||||
|
||||
PR_SET_PDEATHSIG = 1
|
||||
ctypes.CDLL(None, use_errno=True).prctl(PR_SET_PDEATHSIG, _signal.SIGTERM)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def find_binary(explicit: str | os.PathLike | None = None) -> Path:
|
||||
"""Locate the lightpanda binary: explicit arg, $LIGHTPANDA_BIN, the
|
||||
bundled package copy, then PATH."""
|
||||
@@ -91,6 +104,7 @@ class Client:
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=stderr,
|
||||
env=child_env,
|
||||
preexec_fn=_die_with_parent if sys.platform == "linux" else None,
|
||||
)
|
||||
try:
|
||||
self._wait_ready(proc, port)
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from lightpanda import ToolError, run_script
|
||||
@@ -64,6 +71,40 @@ def test_extra_args_passthrough(binary, fixture_url, tmp_path):
|
||||
assert cache_dir.exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="PDEATHSIG is Linux-only")
|
||||
def test_sidecar_dies_with_killed_parent(binary, tmp_path):
|
||||
child_src = tmp_path / "spawn_and_hang.py"
|
||||
child_src.write_text(
|
||||
"import os, sys, time\n"
|
||||
"from lightpanda import Browser\n"
|
||||
"b = Browser(binary=sys.argv[1])\n"
|
||||
"print(b._client._proc.pid, flush=True)\n"
|
||||
"time.sleep(60)\n"
|
||||
)
|
||||
env = dict(os.environ, PYTHONPATH=str(Path(__file__).parent.parent))
|
||||
child = subprocess.Popen(
|
||||
[sys.executable, str(child_src), binary],
|
||||
stdout=subprocess.PIPE, text=True, env=env,
|
||||
)
|
||||
sidecar_pid = int(child.stdout.readline())
|
||||
assert _alive(sidecar_pid)
|
||||
|
||||
os.kill(child.pid, signal.SIGKILL)
|
||||
child.wait()
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline and _alive(sidecar_pid):
|
||||
time.sleep(0.1)
|
||||
assert not _alive(sidecar_pid), "sidecar survived its parent's SIGKILL"
|
||||
|
||||
|
||||
def _alive(pid):
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
|
||||
|
||||
def test_run_script(binary, fixture_url, tmp_path):
|
||||
script = tmp_path / "visit.js"
|
||||
script.write_text('const page = new Page();\nawait page.goto("$LP_TEST_URL");\n')
|
||||
|
||||
Reference in New Issue
Block a user