History

Harden Electron startup waits in dev and CI

- add configurable `T3CODE_ELECTRON_STARTUP_TIMEOUT_MS` to desktop startup and CI perf jobs - replace simple TCP wait with explicit renderer host probes and clearer timeout errors - include Electron startup timeout env in Turborepo global env passthrough

Julius Marminge committed 9 months ago

Commit 30f8a391 parent d45fe793 files · +82 −8Browse files

Unified · Split

3 files
  1. .github/workflows/ci.yml +3 −0
  2. apps/desktop/scripts/dev-electron.mjs +77 −7
  3. turbo.json +2 −1
.github/workflows/ci.yml+3−0
@@ -79,6 +79,7 @@ jobs: · 78 unmodified lines
7979 env:
8080 T3CODE_PERF_TIMEOUT_MS: "420000"
8181 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
82+ T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
8283 run: xvfb-run -a bun run test:desktop-perf
8384
8485 - name: Desktop perf trace test (fork PR)
@@ -86,6 +87,7 @@ jobs: · 1 unmodified line
8687 env:
8788 T3CODE_PERF_TIMEOUT_MS: "420000"
8889 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
90+ T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
8991 run: xvfb-run -a bun run test:desktop-perf
9092
9193 - name: Desktop perf trace test and PR summary
@@ -94,6 +96,7 @@ jobs: · 2 unmodified lines
9496 GH_TOKEN: ${{ github.token }}
9597 T3CODE_PERF_TIMEOUT_MS: "420000"
9698 T3CODE_PERF_MAX_LONG_DISPATCH_COUNT: "5"
99+ T3CODE_ELECTRON_STARTUP_TIMEOUT_MS: "180000"
97100 run: xvfb-run -a bun run test:desktop-perf:post
98101
99102 - name: Upload desktop perf artifacts
apps/desktop/scripts/dev-electron.mjs+77−7
@@ -1,19 +1,84 @@
11 import { spawn } from "node:child_process";
2+import net from "node:net";
23
34 import waitOn from "wait-on";
45
56 const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);
6−const devServerUrl = `http://localhost:${port}`;
7+const STARTUP_TIMEOUT_MS = Number(process.env.T3CODE_ELECTRON_STARTUP_TIMEOUT_MS ?? 120_000);
78
9+async function canConnect(host, probePort, timeoutMs = 1_000) {
10+ return new Promise((resolve) => {
11+ const socket = new net.Socket();
12+ let settled = false;
13+
14+ const finish = (value) => {
15+ if (settled) return;
16+ settled = true;
17+ socket.destroy();
18+ resolve(value);
19+ };
20+
21+ socket.setTimeout(timeoutMs);
22+ socket.once("connect", () => finish(true));
23+ socket.once("timeout", () => finish(false));
24+ socket.once("error", () => finish(false));
25+ socket.connect(probePort, host);
26+ });
27+}
28+
29+function waitForDevServer(probePort, timeoutMs) {
30+ const startedAt = Date.now();
31+ const candidates = [
32+ { host: "127.0.0.1", url: `http://127.0.0.1:${probePort}` },
33+ { host: "::1", url: `http://[::1]:${probePort}` },
34+ { host: "localhost", url: `http://localhost:${probePort}` },
35+ ];
36+
37+ return new Promise((resolve, reject) => {
38+ const tick = () => {
39+ void Promise.all(
40+ candidates.map(async (candidate) => ({
41+ candidate,
42+ connected: await canConnect(candidate.host, probePort),
43+ })),
44+ )
45+ .then((results) => {
46+ const ready = results.find((entry) => entry.connected);
47+ if (ready) {
48+ resolve(ready.candidate.url);
49+ return;
50+ }
51+
52+ if (Date.now() - startedAt >= timeoutMs) {
53+ reject(
54+ new Error(
55+ `[dev-electron] timed out after ${timeoutMs}ms waiting for renderer dev server on port ${probePort}`,
56+ ),
57+ );
58+ return;
59+ }
60+
61+ setTimeout(tick, 250);
62+ })
63+ .catch((error) => {
64+ reject(error);
65+ });
66+ };
67+
68+ tick();
69+ });
70+}
71+
72+console.log("[dev-electron] waiting for desktop/server bundles");
873 await waitOn({
9− resources: [
10− `tcp:${port}`,
11− "file:dist-electron/main.js",
12− "file:dist-electron/preload.js",
13− "file:../server/dist/index.mjs",
14− ],
74+ resources: ["file:dist-electron/main.js", "file:dist-electron/preload.js", "file:../server/dist/index.mjs"],
75+ timeout: STARTUP_TIMEOUT_MS,
1576 });
1677
78+console.log(`[dev-electron] waiting for renderer dev server on port ${port}`);
79+const devServerUrl = await waitForDevServer(port, STARTUP_TIMEOUT_MS);
80+console.log(`[dev-electron] launching electron with renderer url ${devServerUrl}`);
81+
1782 const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon";
1883 const childEnv = { ...process.env };
1984 delete childEnv.ELECTRON_RUN_AS_NODE;
@@ -29,3 +94,8 @@ const child = spawn(command, ["dist-electron/main.js"], { · 9 unmodified lines
2994 child.on("exit", (code) => {
3095 process.exit(code ?? 0);
3196 });
97+
98+child.on("error", (error) => {
99+ console.error("[dev-electron] failed to launch electronmon:", error.message);
100+ process.exit(1);
101+});
turbo.json+2−1
@@ -15,7 +15,8 @@ · 14 unmodified lines
1515 "T3CODE_DESKTOP_PERF_AUTOMATION",
1616 "T3CODE_DESKTOP_PERF_TRACE_OUT",
1717 "T3CODE_DESKTOP_PERF_DONE_OUT",
18− "T3CODE_DESKTOP_PERF_SEED_PATH"
18+ "T3CODE_DESKTOP_PERF_SEED_PATH",
19+ "T3CODE_ELECTRON_STARTUP_TIMEOUT_MS"
1920 ],
2021 "tasks": {
2122 "build": {