From 6d1a4d8bfc8c921533d9dbc4b14596c8cb276cd9 Mon Sep 17 00:00:00 2001 From: Nose Date: Sun, 22 Mar 2026 00:59:03 -0700 Subject: [PATCH] hkhk --- MPV/LUA/main.lua | 62 ++++++++++++++++++++--- MPV/pipeline_helper.py | 108 +++++++++++++++++++++++++++++------------ 2 files changed, 132 insertions(+), 38 deletions(-) diff --git a/MPV/LUA/main.lua b/MPV/LUA/main.lua index a8bb333..98ed348 100644 --- a/MPV/LUA/main.lua +++ b/MPV/LUA/main.lua @@ -4,7 +4,7 @@ local msg = require 'mp.msg' local M = {} -local MEDEIA_LUA_VERSION = '2026-03-21.4' +local MEDEIA_LUA_VERSION = '2026-03-22.1' -- Expose a tiny breadcrumb for debugging which script version is loaded. pcall(mp.set_property, 'user-data/medeia-lua-version', MEDEIA_LUA_VERSION) @@ -880,7 +880,7 @@ local function _is_pipeline_helper_ready() helper_version = mp.get_property_native('user-data/medeia-pipeline-helper-version') end helper_version = tostring(helper_version or '') - if helper_version ~= '2026-03-22.5' then + if helper_version ~= '2026-03-22.6' then return false end @@ -947,7 +947,7 @@ local function _helper_ready_diagnostics() end return 'ready=' .. tostring(ready or '') .. ' helper_version=' .. tostring(helper_version or '') - .. ' required_version=2026-03-22.5' + .. ' required_version=2026-03-22.6' .. ' last_value=' .. tostring(_helper_ready_last_value or '') .. ' last_seen_age=' .. tostring(age) end @@ -1037,8 +1037,53 @@ local function attempt_start_pipeline_helper_async(callback) return end - local args = { python, '-m', 'MPV.pipeline_helper', '--ipc', get_mpv_ipc_path(), '--timeout', '30' } - _lua_log('attempt_start_pipeline_helper_async: spawning helper python=' .. tostring(python)) + local helper_script = '' + local repo_root = _detect_repo_root() + if repo_root ~= '' then + local direct = utils.join_path(repo_root, 'MPV/pipeline_helper.py') + if _path_exists(direct) then + helper_script = direct + end + end + if helper_script == '' then + local candidates = {} + local seen = {} + local source_dir = _get_lua_source_path():match('(.*)[/\\]') or '' + local script_dir = mp.get_script_directory() or '' + local cwd = utils.getcwd() or '' + _append_unique_path(candidates, seen, find_file_upwards(source_dir, 'MPV/pipeline_helper.py', 8)) + _append_unique_path(candidates, seen, find_file_upwards(script_dir, 'MPV/pipeline_helper.py', 8)) + _append_unique_path(candidates, seen, find_file_upwards(cwd, 'MPV/pipeline_helper.py', 8)) + for _, candidate in ipairs(candidates) do + if _path_exists(candidate) then + helper_script = candidate + break + end + end + end + if helper_script == '' then + _lua_log('attempt_start_pipeline_helper_async: pipeline_helper.py not found') + finish(false) + return + end + + local launch_root = repo_root + if launch_root == '' then + launch_root = helper_script:match('(.*)[/\\]MPV[/\\]') or (helper_script:match('(.*)[/\\]') or '') + end + + local bootstrap = table.concat({ + 'import os, runpy, sys', + 'script = sys.argv[1]', + 'root = sys.argv[2]', + 'if root:', + ' os.chdir(root)', + ' sys.path.insert(0, root) if root not in sys.path else None', + 'sys.argv = [script] + sys.argv[3:]', + 'runpy.run_path(script, run_name="__main__")', + }, '\n') + local args = { python, '-c', bootstrap, helper_script, launch_root, '--ipc', get_mpv_ipc_path(), '--timeout', '30' } + _lua_log('attempt_start_pipeline_helper_async: spawning helper python=' .. tostring(python) .. ' script=' .. tostring(helper_script) .. ' root=' .. tostring(launch_root)) -- Spawn detached; don't wait for it here (async). local ok, result, detail = _run_subprocess_command({ name = 'subprocess', args = args, detach = true }) @@ -1056,7 +1101,9 @@ local function attempt_start_pipeline_helper_async(callback) end -- Wait for helper to become ready in background (non-blocking). - local deadline = mp.get_time() + 3.0 + -- 12 s gives Python time to kill a stale lock holder (PS scan + taskkill) + -- and publish its first ready heartbeat before we give up. + local deadline = mp.get_time() + 12.0 local timer timer = mp.add_periodic_timer(0.1, function() if _is_pipeline_helper_ready() then @@ -1068,6 +1115,9 @@ local function attempt_start_pipeline_helper_async(callback) if mp.get_time() >= deadline then timer:kill() _lua_log('attempt_start_pipeline_helper_async: timeout waiting for ready') + -- Reset debounce so the next attempt is not immediate; gives the + -- still-running Python helper time to die or acquire the lock. + _helper_start_debounce_ts = mp.get_time() finish(false) end end) diff --git a/MPV/pipeline_helper.py b/MPV/pipeline_helper.py index 159f6ea..704db71 100644 --- a/MPV/pipeline_helper.py +++ b/MPV/pipeline_helper.py @@ -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(