hkhk
This commit is contained in:
@@ -20,7 +20,7 @@ This helper is intentionally minimal: one request at a time, last-write-wins.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.5"
|
||||
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.6"
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@@ -1156,6 +1156,37 @@ def _helper_log_path() -> str:
|
||||
return str((Path(tmp) / "medeia-mpv-helper.log").resolve())
|
||||
|
||||
|
||||
def _get_ipc_lock_path(ipc_path: str) -> Path:
|
||||
"""Return the lock file path for a given IPC path."""
|
||||
safe = re.sub(r"[^a-zA-Z0-9_.-]+", "_", str(ipc_path or ""))
|
||||
if not safe:
|
||||
safe = "mpv"
|
||||
lock_dir = Path(tempfile.gettempdir()) / "medeia-mpv-helper"
|
||||
lock_dir.mkdir(parents=True, exist_ok=True)
|
||||
return lock_dir / f"medeia-mpv-helper-{safe}.lock"
|
||||
|
||||
|
||||
def _read_lock_file_pid(ipc_path: str) -> Optional[int]:
|
||||
"""Return the PID recorded in the lock file by the current holder, or None.
|
||||
|
||||
The lock file can be opened for reading even while another process holds the
|
||||
byte-range lock (msvcrt.locking is advisory, not a file-open exclusive lock).
|
||||
This lets a challenger identify the exact holder PID and kill only that process,
|
||||
avoiding the race where concurrent sibling helpers all kill each other.
|
||||
"""
|
||||
try:
|
||||
lock_path = _get_ipc_lock_path(ipc_path)
|
||||
with open(str(lock_path), "r", encoding="utf-8", errors="replace") as fh:
|
||||
content = fh.read().strip()
|
||||
if not content:
|
||||
return None
|
||||
data = json.loads(content)
|
||||
pid = int(data.get("pid") or 0)
|
||||
return pid if pid > 0 else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _acquire_ipc_lock(ipc_path: str) -> Optional[Any]:
|
||||
"""Best-effort singleton lock per IPC path.
|
||||
|
||||
@@ -1163,13 +1194,7 @@ def _acquire_ipc_lock(ipc_path: str) -> Optional[Any]:
|
||||
log output. Use a tiny file lock to ensure one helper per mpv instance.
|
||||
"""
|
||||
try:
|
||||
safe = re.sub(r"[^a-zA-Z0-9_.-]+", "_", str(ipc_path or ""))
|
||||
if not safe:
|
||||
safe = "mpv"
|
||||
# Keep lock files out of the repo's Log/ directory to avoid clutter.
|
||||
lock_dir = Path(tempfile.gettempdir()) / "medeia-mpv-helper"
|
||||
lock_dir.mkdir(parents=True, exist_ok=True)
|
||||
lock_path = lock_dir / f"medeia-mpv-helper-{safe}.lock"
|
||||
lock_path = _get_ipc_lock_path(ipc_path)
|
||||
fh = open(lock_path, "a+", encoding="utf-8", errors="replace")
|
||||
|
||||
if os.name == "nt":
|
||||
@@ -1322,33 +1347,16 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
# path used by this helper (which comes from the running MPV instance).
|
||||
os.environ["MEDEIA_MPV_IPC"] = str(args.ipc)
|
||||
|
||||
lock_wait_deadline = time.time() + min(max(1.5, float(args.timeout or 0.0)), 8.0)
|
||||
# Generous deadline: the kill + OS-lock-release cycle can take several seconds,
|
||||
# especially when a stale helper is running as a different process image.
|
||||
lock_wait_deadline = time.time() + 12.0
|
||||
lock_wait_logged = False
|
||||
last_killed_pid_signature = ""
|
||||
_lock_fh = None
|
||||
_kill_attempted = False # kill at most once; re-killing on every loop causes sibling helpers to kill each other
|
||||
|
||||
while _lock_fh is None:
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
sibling_pids = [
|
||||
pid
|
||||
for pid in _windows_list_pipeline_helper_pids(str(args.ipc))
|
||||
if pid and pid != os.getpid()
|
||||
]
|
||||
if sibling_pids:
|
||||
signature = ",".join(str(pid) for pid in sibling_pids)
|
||||
if signature != last_killed_pid_signature:
|
||||
_append_helper_log(
|
||||
f"[helper] terminating older helper pids for ipc={args.ipc}: {signature}"
|
||||
)
|
||||
last_killed_pid_signature = signature
|
||||
_windows_kill_pids(sibling_pids)
|
||||
time.sleep(0.35)
|
||||
except Exception as exc:
|
||||
_append_helper_log(
|
||||
f"[helper] failed to terminate older helpers: {type(exc).__name__}: {exc}"
|
||||
)
|
||||
|
||||
# Try to acquire the lock first — avoids unnecessary process enumeration
|
||||
# when there is no contention (normal cold-start path).
|
||||
_lock_fh = _acquire_ipc_lock(str(args.ipc))
|
||||
if _lock_fh is not None:
|
||||
break
|
||||
@@ -1365,7 +1373,43 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
)
|
||||
return 0
|
||||
|
||||
time.sleep(0.20)
|
||||
# Kill the lock holder at most once. Repeatedly scanning for all matching
|
||||
# processes on every iteration caused concurrent sibling helpers (spawned by
|
||||
# the Lua 3-second timeout cycle) to kill each other before any could start.
|
||||
if platform.system() == "Windows" and not _kill_attempted:
|
||||
_kill_attempted = True
|
||||
try:
|
||||
# Prefer targeted kill via PID recorded in the lock file.
|
||||
# msvcrt byte-range locking does not prevent reading the file from
|
||||
# another process, so we can always identify the exact holder PID.
|
||||
holder_pid = _read_lock_file_pid(str(args.ipc))
|
||||
if holder_pid and holder_pid != os.getpid():
|
||||
_append_helper_log(
|
||||
f"[helper] killing lock holder pid={holder_pid} ipc={args.ipc}"
|
||||
)
|
||||
_windows_kill_pids([holder_pid])
|
||||
else:
|
||||
# Fallback: old helpers (pre-PID-in-lock-file) left no PID.
|
||||
# Scan once by command-line pattern.
|
||||
sibling_pids = [
|
||||
p for p in _windows_list_pipeline_helper_pids(str(args.ipc))
|
||||
if p and p != os.getpid()
|
||||
]
|
||||
if sibling_pids:
|
||||
_append_helper_log(
|
||||
f"[helper] killing old-style sibling pids={sibling_pids} ipc={args.ipc}"
|
||||
)
|
||||
_windows_kill_pids(sibling_pids)
|
||||
else:
|
||||
_append_helper_log(
|
||||
f"[helper] no identifiable lock holder for ipc={args.ipc}; waiting"
|
||||
)
|
||||
except Exception as exc:
|
||||
_append_helper_log(
|
||||
f"[helper] kill failed: {type(exc).__name__}: {exc}"
|
||||
)
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
try:
|
||||
_append_helper_log(
|
||||
|
||||
Reference in New Issue
Block a user