hkhk
This commit is contained in:
@@ -4,7 +4,7 @@ local msg = require 'mp.msg'
|
|||||||
|
|
||||||
local M = {}
|
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.
|
-- Expose a tiny breadcrumb for debugging which script version is loaded.
|
||||||
pcall(mp.set_property, 'user-data/medeia-lua-version', MEDEIA_LUA_VERSION)
|
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')
|
helper_version = mp.get_property_native('user-data/medeia-pipeline-helper-version')
|
||||||
end
|
end
|
||||||
helper_version = tostring(helper_version or '')
|
helper_version = tostring(helper_version or '')
|
||||||
if helper_version ~= '2026-03-22.5' then
|
if helper_version ~= '2026-03-22.6' then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -947,7 +947,7 @@ local function _helper_ready_diagnostics()
|
|||||||
end
|
end
|
||||||
return 'ready=' .. tostring(ready or '')
|
return 'ready=' .. tostring(ready or '')
|
||||||
.. ' helper_version=' .. tostring(helper_version 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_value=' .. tostring(_helper_ready_last_value or '')
|
||||||
.. ' last_seen_age=' .. tostring(age)
|
.. ' last_seen_age=' .. tostring(age)
|
||||||
end
|
end
|
||||||
@@ -1037,8 +1037,53 @@ local function attempt_start_pipeline_helper_async(callback)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local args = { python, '-m', 'MPV.pipeline_helper', '--ipc', get_mpv_ipc_path(), '--timeout', '30' }
|
local helper_script = ''
|
||||||
_lua_log('attempt_start_pipeline_helper_async: spawning helper python=' .. tostring(python))
|
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).
|
-- Spawn detached; don't wait for it here (async).
|
||||||
local ok, result, detail = _run_subprocess_command({ name = 'subprocess', args = args, detach = true })
|
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
|
end
|
||||||
|
|
||||||
-- Wait for helper to become ready in background (non-blocking).
|
-- 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
|
local timer
|
||||||
timer = mp.add_periodic_timer(0.1, function()
|
timer = mp.add_periodic_timer(0.1, function()
|
||||||
if _is_pipeline_helper_ready() then
|
if _is_pipeline_helper_ready() then
|
||||||
@@ -1068,6 +1115,9 @@ local function attempt_start_pipeline_helper_async(callback)
|
|||||||
if mp.get_time() >= deadline then
|
if mp.get_time() >= deadline then
|
||||||
timer:kill()
|
timer:kill()
|
||||||
_lua_log('attempt_start_pipeline_helper_async: timeout waiting for ready')
|
_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)
|
finish(false)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ This helper is intentionally minimal: one request at a time, last-write-wins.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.5"
|
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.6"
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
@@ -1156,6 +1156,37 @@ def _helper_log_path() -> str:
|
|||||||
return str((Path(tmp) / "medeia-mpv-helper.log").resolve())
|
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]:
|
def _acquire_ipc_lock(ipc_path: str) -> Optional[Any]:
|
||||||
"""Best-effort singleton lock per IPC path.
|
"""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.
|
log output. Use a tiny file lock to ensure one helper per mpv instance.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
safe = re.sub(r"[^a-zA-Z0-9_.-]+", "_", str(ipc_path or ""))
|
lock_path = _get_ipc_lock_path(ipc_path)
|
||||||
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"
|
|
||||||
fh = open(lock_path, "a+", encoding="utf-8", errors="replace")
|
fh = open(lock_path, "a+", encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
if os.name == "nt":
|
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).
|
# path used by this helper (which comes from the running MPV instance).
|
||||||
os.environ["MEDEIA_MPV_IPC"] = str(args.ipc)
|
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
|
lock_wait_logged = False
|
||||||
last_killed_pid_signature = ""
|
|
||||||
_lock_fh = None
|
_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:
|
while _lock_fh is None:
|
||||||
if platform.system() == "Windows":
|
# Try to acquire the lock first — avoids unnecessary process enumeration
|
||||||
try:
|
# when there is no contention (normal cold-start path).
|
||||||
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}"
|
|
||||||
)
|
|
||||||
|
|
||||||
_lock_fh = _acquire_ipc_lock(str(args.ipc))
|
_lock_fh = _acquire_ipc_lock(str(args.ipc))
|
||||||
if _lock_fh is not None:
|
if _lock_fh is not None:
|
||||||
break
|
break
|
||||||
@@ -1365,7 +1373,43 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
)
|
)
|
||||||
return 0
|
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:
|
try:
|
||||||
_append_helper_log(
|
_append_helper_log(
|
||||||
|
|||||||
Reference in New Issue
Block a user