Compare commits

...
Author SHA1 Message Date
Alex Southwell 2b6b05a175 Let SIGTERM request server shutdown without interrupting cleanup 2026-10-26 13:59:17 +11:00
Alex Southwell 67b9151513 Merge pull request #282 from saphid/feat/framey-library-context
Framey: add read-only Steam library context for plugin panels
2026-10-26 02:37:07 +00:00
2 changed files with 27 additions and 7 deletions

No files matched your search

+20 -3
View File
@@ -426,10 +426,27 @@ class OneServer(unittest.TestCase):
# Stopped as the app stops it: by closing stdin (the only way on Windows,
# where terminate() is a hard kill) and, elsewhere, by SIGTERM too.
for stop in ["stdin"] + (["sigterm"] if os.name != "nt" else []):
for stop in ["stdin"] + (["sigterm", "stdin+late-sigterm"] if os.name != "nt" else []):
with self.subTest(stop=stop):
left_by_dead = [staged(tmp, "frame-vr-", dead.pid), staged(index_dir, ".download-", dead.pid)]
proc = subprocess.Popen([sys.executable, str(ROOT / "ui" / "server.py"), "--port", "0", "--exit-on-eof"],
command = [sys.executable, str(ROOT / "ui" / "server.py"), "--port", "0", "--exit-on-eof"]
if stop == "stdin+late-sigterm":
# Deliver the app's second stop mechanism exactly as cleanup
# replaces the signal handler: the race observed on a real Mac.
wrapper = (
"import os, runpy, signal, sys; from pathlib import Path\n"
"original = signal.signal\n"
"def install(sig, handler):\n"
" if sig == signal.SIGTERM and handler == signal.SIG_IGN:\n"
" os.kill(os.getpid(), signal.SIGTERM)\n"
" return original(sig, handler)\n"
"signal.signal = install\n"
"sys.argv = sys.argv[1:]\n"
"sys.path.insert(0, str(Path(sys.argv[0]).parent))\n"
"runpy.run_path(sys.argv[0], run_name='__main__')\n"
)
command = [sys.executable, "-c", wrapper, *command[1:]]
proc = subprocess.Popen(command,
env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True)
try:
@@ -437,7 +454,7 @@ class OneServer(unittest.TestCase):
self.assertEqual([d for d in left_by_dead if d.exists()], [])
in_flight = [staged(tmp, "frame-vr-", proc.pid), staged(tmp, "frame-agent-", proc.pid),
staged(index_dir, ".download-", proc.pid)]
if stop == "stdin":
if stop.startswith("stdin"):
proc.stdin.close()
else:
proc.terminate()
+7 -4
View File
@@ -4172,8 +4172,12 @@ def main():
LINK.start()
if not PRIVATE: # one worker per user, in the app's server
threading.Thread(target=autosync.run, args=(_stop_sync,), daemon=True).start()
def request_shutdown(*_):
# shutdown() must run outside the serve_forever thread. Do not raise
# from SIGTERM: the app can send it after stdin EOF has begun cleanup.
threading.Thread(target=httpd.shutdown, daemon=True).start()
if not frame_host.WINDOWS:
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt))
signal.signal(signal.SIGTERM, request_shutdown)
if args.exit_on_eof:
def watch_stdin():
# os.read, not sys.stdin.buffer.read: a buffered read holds stdin's lock,
@@ -4181,7 +4185,7 @@ def main():
# when it can't take that lock back from this thread.
while os.read(0, 4096):
pass
threading.Thread(target=httpd.shutdown, daemon=True).start()
request_shutdown()
threading.Thread(target=watch_stdin, daemon=True).start()
try:
# The real port, which --port 0 leaves to the system (the iPhone app reads it from here).
@@ -4190,8 +4194,7 @@ def main():
except KeyboardInterrupt:
pass
finally:
# The app both closes stdin and sends SIGTERM on quit; a second signal
# mid-cleanup would abort it and leave the SSH master running.
# Cleanup is already underway; later stop signals need no more workers.
if not frame_host.WINDOWS:
signal.signal(signal.SIGTERM, signal.SIG_IGN)
_power.shutdown()