Devices: the app waits for its old server before starting the new one; clipboard sends keep their headset (review round 31)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-29 08:27:52 +10:00
1 parent cb6f294299
commit a9740ad6f2
4 files changed
+13 -5

No files matched your search

+8 -2
View File
@@ -154,10 +154,16 @@ async function startServer() {
// Closing stdin lets server.py close its SSH connections and exit (the only clean
// way on Windows); SIGTERM does the same elsewhere.
// Resolves once it has exited (or after 20 s), so a replacement can take the server
// lock: server.py allows one per user.
function endServer(child) {
const gone = child.exitCode !== null || child.signalCode !== null ? Promise.resolve()
: new Promise((resolve) => child.once("exit", resolve));
try { child.stdin.end(); } catch {}
if (!IS_WIN) child.kill("SIGTERM");
setTimeout(() => { if (child.exitCode === null && child.signalCode === null) child.kill(); }, 5000).unref();
// server.py ignores a second SIGTERM while it shuts down, so the fallback is a hard kill.
setTimeout(() => { if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); }, 12000).unref();
return Promise.race([gone, new Promise((resolve) => setTimeout(resolve, 20000).unref())]);
}
function stopServer() {
@@ -182,7 +188,7 @@ async function restartServer() {
const old = server;
server = null;
url = null;
if (old) endServer(old);
if (old) await endServer(old);
await load();
}
+2 -1
View File
@@ -248,7 +248,8 @@ class OneServer(unittest.TestCase):
def test_a_second_server_is_refused_until_the_first_exits(self):
data = tempfile.mkdtemp(prefix="frame-one-server-")
env = {**os.environ, "FRAME_CONTROL_DATA_DIR": data, "FRAME_ALIAS": "frame-control-test.invalid"}
env = {**os.environ, "FRAME_CONTROL_DATA_DIR": data, "FRAME_ALIAS": "frame-control-test.invalid",
"FRAME_CONTROL_SERVER_WAIT": "1"}
first = self.start(env)
self.assertIn("Frame Control on", first.stdout.readline())
second = subprocess.run([sys.executable, str(ROOT / "ui" / "server.py"), "--port", "0"], env=env,
+2 -1
View File
@@ -1503,9 +1503,10 @@ $("clipSend").onclick = () => {
// In the app, Electron reads the clipboard; in a browser, the server does.
async function sendComputerClipboard() {
if (!window.frameApp) return api("/api/clipboard", { fromComputer: true });
const device = window.connDevice; // the headset it was sent to, not one switched to while reading
const text = await window.frameApp.readClipboard();
if (!text) throw new Error("The clipboard is empty (or holds something other than text)");
return api("/api/clipboard", { text });
return api("/api/clipboard", { text }, device);
}
$("clipMac").onclick = () => act($("clipMac").textContent, sendComputerClipboard, $("clipMac"));
+1 -1
View File
@@ -1662,7 +1662,7 @@ def one_server():
edit the headsets on their own, and could move each other's installs to another
headset. Held until this process exits. (FRAME_CONTROL_DATA_DIR gives a second,
separate one, as the tests do.)"""
lock = frame_devices.file_lock(frame_host.data_dir("server.lock"), timeout=8) # the app restarting its server
lock = frame_devices.file_lock(frame_host.data_dir("server.lock"), timeout=float(os.environ.get("FRAME_CONTROL_SERVER_WAIT") or 20)) # while the app restarts it
try:
lock.__enter__()
except OSError: