diff --git a/tests/test_server.py b/tests/test_server.py index 9805806..93087b4 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -279,6 +279,18 @@ class OneServer(unittest.TestCase): r = conn.getresponse() self.assertEqual(r.status, 403, r.read()) + @unittest.skipIf(os.name == "nt", "no SIGTERM on Windows") + def test_sigterm_while_the_app_holds_stdin_exits_cleanly(self): + """The app keeps stdin open; a stop signal used to abort Python (SIGABRT) at exit.""" + env = {**os.environ, "FRAME_CONTROL_DATA_DIR": tempfile.mkdtemp(prefix="frame-one-server-"), + "FRAME_ALIAS": "frame-control-test.invalid"} + proc = subprocess.Popen([sys.executable, str(ROOT / "ui" / "server.py"), "--port", "0", "--exit-on-eof"], + env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + self.addCleanup(lambda: (proc.stdin.close(), proc.stdout.close())) + self.assertIn("Frame Control on", proc.stdout.readline()) + proc.terminate() + self.assertEqual(proc.wait(30), 0, proc.stdout.read()) + class ArtworkSettings(unittest.TestCase): """The settings panel's endpoints, with and without the page's X-Frame-UI key.""" diff --git a/ui/server.py b/ui/server.py index 7771b72..b25975a 100755 --- a/ui/server.py +++ b/ui/server.py @@ -2656,12 +2656,16 @@ def main(): signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt)) if args.exit_on_eof: def watch_stdin(): - sys.stdin.buffer.read() + # os.read, not sys.stdin.buffer.read: a buffered read holds stdin's lock, + # and if a signal stops the server first, Python aborts (SIGABRT) at exit + # 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() threading.Thread(target=watch_stdin, daemon=True).start() - # The real port, which --port 0 leaves to the system (the iPhone app reads it from here). - print(f"Frame Control on http://127.0.0.1:{httpd.server_address[1]} (alias: {FRAME}; Ctrl-C to stop)", flush=True) try: + # The real port, which --port 0 leaves to the system (the iPhone app reads it from here). + print(f"Frame Control on http://127.0.0.1:{httpd.server_address[1]} (alias: {FRAME}; Ctrl-C to stop)", flush=True) httpd.serve_forever() except KeyboardInterrupt: pass