diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-01 09:53:34 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-01 09:53:34 +0200 |
| commit | 0673922e704f718037eeffd2724debcfa8ac0b4b (patch) | |
| tree | 78fee9a5c04542f006c1757bc76c0af7f57a79db /packages/meshbay-node/tests/test_ffprobe_is_bounded.py | |
| parent | 6426912946270bb02e7b94508008edf8f949d993 (diff) | |
| download | meshbay-0673922e704f718037eeffd2724debcfa8ac0b4b.tar.gz | |
fix(node): ffprobe over a member's file is bounded, and stopped when it is
probe_video waits 30 s at most and kills ffprobe on a timeout or when its
caller gives up — a cancelled wait left the process running. The seek probe
kills what it timed out on. Stream, subtitle and enrichment requests no longer
hang on a file that keeps ffprobe busy (F-18, timeouts; the protocol
whitelist was dropped: ffmpeg already confines nested protocols of a local
input, measured on 8.0 against HLS and concat inputs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_ffprobe_is_bounded.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_ffprobe_is_bounded.py | 61 |
1 files changed, 61 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_ffprobe_is_bounded.py b/packages/meshbay-node/tests/test_ffprobe_is_bounded.py new file mode 100644 index 0000000..c96a19a --- /dev/null +++ b/packages/meshbay-node/tests/test_ffprobe_is_bounded.py @@ -0,0 +1,61 @@ +""" +ffprobe over a member's file is bounded, and a bounded wait stops the process. + +`probe_video` runs before every stream and subtitle request and in the +enrichment pool. A file that keeps ffprobe busy must not hold any of them, and +giving up on the wait is not enough: an asyncio subprocess whose wait was +cancelled keeps running. These run a stand-in ffprobe that never answers and +check both — the call returns, and the process is gone. +""" + +import asyncio +import os +import sys +import time + +import pytest +from meshbay_node import media_probe, platform + +pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="a POSIX shell stand-in") + + +@pytest.fixture +def hanging_ffprobe(tmp_path, monkeypatch): + pid_file = tmp_path / "pid" + tool = tmp_path / "ffprobe" + tool.write_text(f"#!/bin/sh\necho $$ > {pid_file}\nexec sleep 600\n") + tool.chmod(0o755) + monkeypatch.setattr(platform, "_ffprobe_path", str(tool)) + return pid_file + + +def _gone(pid: int) -> bool: + try: + os.kill(pid, 0) + except ProcessLookupError: + return True + # A zombie still answers kill(0); its state says it has exited. + try: + with open(f"/proc/{pid}/stat") as f: + return f.read().split()[2] == "Z" + except OSError: + return True + + +async def test_a_probe_that_never_answers_times_out_and_is_stopped(hanging_ffprobe, monkeypatch): + monkeypatch.setattr(media_probe, "FFPROBE_TIMEOUT_SECS", 0.5) + started = time.monotonic() + with pytest.raises(RuntimeError, match="timed out"): + await media_probe.probe_video("/nonexistent/file.mkv") + assert time.monotonic() - started < 5 + assert _gone(int(hanging_ffprobe.read_text())) + + +async def test_a_caller_giving_up_stops_it_too(hanging_ffprobe): + with pytest.raises(TimeoutError): + await asyncio.wait_for(media_probe.probe_video("/nonexistent/file.mkv"), 0.5) + for _ in range(50): + if hanging_ffprobe.exists(): + break + await asyncio.sleep(0.05) + assert _gone(int(hanging_ffprobe.read_text())) |