feat: add step-by-step visual replay and harden process lifecycle

- UI: Implement dual-mode playback in FloatingVideoPlayer supporting video and step-by-step frame replay with scrub controls and auto-fallback
- Runtime: Prevent asyncio event loop hangs on Darwin and Linux by avoiding psutil.wait_procs on direct child processes
- Task Queue: Add worker exit watchdog and automatic recording recovery for completed or interrupted sessions
- Recording: Add ffmpeg ultrafast transcode fallback, shield recording finalization against cancellation, and handle on-the-fly MP4 conversion
- Docs: Polish README_CN styling and streamline flash runner agent prompt
This commit is contained in:
somew1nd committed 2026-08-26 00:53:24 -07:00
1 parent 06643e955b
commit a36a66ce6d
26 files changed
+1152 -117

No files matched your search

+42 -6
View File
@@ -19,6 +19,7 @@ from pathlib import Path
import shutil
import signal
import tempfile
import time
from artemis.platform.base import IPlatform, IPlatformPaths, IPlatformProcess, OSType
@@ -113,12 +114,47 @@ class LinuxPlatformProcess(IPlatformProcess):
pass
parent.send_signal(signal.SIGTERM)
gone, alive = psutil.wait_procs(children + [parent], timeout=timeout_seconds)
for p in alive:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# Avoid calling wait_procs on direct children of the current process.
# In POSIX, psutil.Process.wait() on a child of os.getpid() invokes
# os.waitpid(), which reaps the zombie and prevents asyncio / uvloop
# event loops from detecting that the child exited, causing proc.wait()
# to hang indefinitely.
current_pid = os.getpid()
external_procs = [
p
for p in children + [parent]
if p.pid != current_pid and getattr(p, "ppid", lambda: None)() != current_pid
]
direct_children = [
p
for p in children + [parent]
if getattr(p, "ppid", lambda: None)() == current_pid
]
if external_procs:
gone, alive = psutil.wait_procs(external_procs, timeout=timeout_seconds)
for p in alive:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
deadline = time.time() + timeout_seconds
for p in direct_children:
while time.time() < deadline:
try:
if not p.is_running() or p.status() == psutil.STATUS_ZOMBIE:
break
except (psutil.NoSuchProcess, psutil.AccessDenied):
break
time.sleep(0.05)
else:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return True
except Exception:
try: