mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 01:00:18 +02:00
Merge pull request #65 from saphid/fix/server-stdin-abort
A stop signal no longer crashes the server, and the app restarts it by itself
This commit is contained in:
4 files changed
+39
-8
No files matched your search
+19
-5
@@ -149,7 +149,7 @@ async function startServer() {
|
|||||||
const target = `http://127.0.0.1:${port}/`;
|
const target = `http://127.0.0.1:${port}/`;
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
if (exited !== null) throw new Error(`The server exited (${exited}). See ${LOG}.`);
|
if (exited !== null) throw new Error(`The server exited (${exited}). See ${LOG}.`);
|
||||||
if (await ping(target)) { url = target; return; }
|
if (await ping(target)) { url = target; serverStarted = Date.now(); return; }
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
if (server === child) server = null;
|
if (server === child) server = null;
|
||||||
@@ -175,18 +175,27 @@ function stopServer() {
|
|||||||
if (server) endServer(server);
|
if (server) endServer(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorPage(message) {
|
function errorPage(message, title = "Frame Control couldn't start") {
|
||||||
const esc = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
const esc = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
||||||
const html = `<!doctype html><meta charset="utf-8"><body style="margin:0;height:100vh;display:grid;
|
const html = `<!doctype html><meta charset="utf-8"><body style="margin:0;height:100vh;display:grid;
|
||||||
place-items:center;background:${BG};color:#e6edf3;font:14px -apple-system,sans-serif">
|
place-items:center;background:${BG};color:#e6edf3;font:14px -apple-system,sans-serif">
|
||||||
<div style="max-width:560px;padding:32px;line-height:1.5"><h2>Frame Control couldn't start</h2>
|
<div style="max-width:560px;padding:32px;line-height:1.5"><h2>${esc(title)}</h2>
|
||||||
<p>${esc(message)}</p><p style="color:#8b98a8">Fix it, then choose Frame → Restart Server.</p></div>`;
|
<p>${esc(message)}</p>
|
||||||
|
<p><button onclick="this.disabled = true; frameApp.restartServer()" style="font:inherit;padding:6px 16px;
|
||||||
|
border-radius:6px;border:1px solid #30363d;background:#21262d;color:inherit;cursor:pointer">Try Again</button></p>
|
||||||
|
<p style="color:#8b98a8">Frame → Restart Server does the same.</p></div>`;
|
||||||
return "data:text/html;charset=utf-8," + encodeURIComponent(html);
|
return "data:text/html;charset=utf-8," + encodeURIComponent(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A server that had been running starts again by itself (something stopped it: a
|
||||||
|
// signal, a crash). One that stops again within a minute shows the error instead,
|
||||||
|
// so a server that can't stay up doesn't restart forever.
|
||||||
|
let serverStarted = 0;
|
||||||
function serverDied(why) {
|
function serverDied(why) {
|
||||||
url = null;
|
url = null;
|
||||||
if (win) win.loadURL(errorPage(`The server stopped unexpectedly (${why}). See ${LOG}.`));
|
if (!win) return;
|
||||||
|
if (Date.now() - serverStarted > 60000) restartServer();
|
||||||
|
else win.loadURL(errorPage(`Its server stopped unexpectedly (${why}). See ${LOG}.`, "Frame Control stopped"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restarts that overlap share one: two could each start a server, and the one
|
// Restarts that overlap share one: two could each start a server, and the one
|
||||||
@@ -263,6 +272,11 @@ function fromUi(e) {
|
|||||||
} catch { return false; }
|
} catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The error page's Try Again button. The error page is the only data: page the window
|
||||||
|
// shows (`url` can still be set then: the server answered but the page failed to load).
|
||||||
|
ipcMain.handle("server:restart", (e) => {
|
||||||
|
if (win && e.sender === win.webContents && e.senderFrame && e.senderFrame.url.startsWith("data:")) restartServer();
|
||||||
|
});
|
||||||
ipcMain.handle("clipboard:read", (e) => fromUi(e) ? clipboard.readText() : "");
|
ipcMain.handle("clipboard:read", (e) => fromUi(e) ? clipboard.readText() : "");
|
||||||
// A PNG or JPEG (a screenshot) onto the clipboard as an image.
|
// A PNG or JPEG (a screenshot) onto the clipboard as an image.
|
||||||
ipcMain.handle("clipboard:writeImage", (e, bytes) => {
|
ipcMain.handle("clipboard:writeImage", (e, bytes) => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ contextBridge.exposeInMainWorld("frameApp", {
|
|||||||
readClipboard: () => ipcRenderer.invoke("clipboard:read"),
|
readClipboard: () => ipcRenderer.invoke("clipboard:read"),
|
||||||
writeImage: (bytes) => ipcRenderer.invoke("clipboard:writeImage", bytes),
|
writeImage: (bytes) => ipcRenderer.invoke("clipboard:writeImage", bytes),
|
||||||
setUpConnection: () => ipcRenderer.invoke("connection:setup"),
|
setUpConnection: () => ipcRenderer.invoke("connection:setup"),
|
||||||
|
restartServer: () => ipcRenderer.invoke("server:restart"), // the "couldn't start" page's Try Again
|
||||||
// The Frame menu's headset switcher: the page tells it the headsets, and hears picks.
|
// The Frame menu's headset switcher: the page tells it the headsets, and hears picks.
|
||||||
devicesChanged: (list) => ipcRenderer.send("devices:changed", list),
|
devicesChanged: (list) => ipcRenderer.send("devices:changed", list),
|
||||||
onUseDevice: (cb) => {
|
onUseDevice: (cb) => {
|
||||||
|
|||||||
@@ -285,6 +285,18 @@ class OneServer(unittest.TestCase):
|
|||||||
r = conn.getresponse()
|
r = conn.getresponse()
|
||||||
self.assertEqual(r.status, 403, r.read())
|
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):
|
class ArtworkSettings(unittest.TestCase):
|
||||||
"""The settings panel's endpoints, with and without the page's X-Frame-UI key."""
|
"""The settings panel's endpoints, with and without the page's X-Frame-UI key."""
|
||||||
|
|||||||
+7
-3
@@ -2667,12 +2667,16 @@ def main():
|
|||||||
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt))
|
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt))
|
||||||
if args.exit_on_eof:
|
if args.exit_on_eof:
|
||||||
def watch_stdin():
|
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=httpd.shutdown, daemon=True).start()
|
||||||
threading.Thread(target=watch_stdin, 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:
|
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()
|
httpd.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in new issue
Block a user