Skip to content

Commit 01105a8

Browse files
committed
feat: enhance Electron harness with debugging setup, new scripts, and improved error handling
1 parent fb01f13 commit 01105a8

8 files changed

Lines changed: 450 additions & 9 deletions

.vscode/launch.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Electron: Debug Main (Harness)",
6+
"type": "node",
7+
"request": "launch",
8+
"cwd": "${workspaceFolder}",
9+
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
10+
"windows": {
11+
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
12+
},
13+
"runtimeArgs": [
14+
"--remote-debugging-port=9222"
15+
],
16+
"args": [
17+
"${workspaceFolder}/scripts/electron-harness-main.mjs"
18+
],
19+
"env": {
20+
"HARNESS_URL": "http://127.0.0.1:4173",
21+
"CORTEX_ELECTRON_HEADLESS": "0",
22+
"CORTEX_ELECTRON_SHOW": "1",
23+
"CORTEX_DISABLE_VULKAN": "0",
24+
"CORTEX_ENABLE_UNSAFE_WEBGPU": "0",
25+
"CORTEX_IGNORE_GPU_BLOCKLIST": "0"
26+
},
27+
"outputCapture": "std",
28+
"console": "integratedTerminal",
29+
"internalConsoleOptions": "neverOpen",
30+
"skipFiles": [
31+
"<node_internals>/**"
32+
],
33+
"preLaunchTask": "harness:serve",
34+
"postDebugTask": "harness:stop"
35+
},
36+
{
37+
"name": "Electron: Attach Renderer",
38+
"type": "pwa-chrome",
39+
"request": "attach",
40+
"port": 9222,
41+
"webRoot": "${workspaceFolder}",
42+
"timeout": 30000
43+
}
44+
],
45+
"compounds": [
46+
{
47+
"name": "Electron: Main + Renderer",
48+
"configurations": [
49+
"Electron: Debug Main (Harness)",
50+
"Electron: Attach Renderer"
51+
]
52+
}
53+
]
54+
}

.vscode/tasks.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "harness:serve",
6+
"type": "shell",
7+
"command": "node scripts/runtime-harness-server.mjs",
8+
"isBackground": true,
9+
"problemMatcher": {
10+
"owner": "cortex-harness",
11+
"pattern": {
12+
"regexp": "^$"
13+
},
14+
"background": {
15+
"activeOnStart": true,
16+
"beginsPattern": ".*",
17+
"endsPattern": "\\[runtime-harness\\] serving .*"
18+
}
19+
},
20+
"presentation": {
21+
"reveal": "always",
22+
"panel": "dedicated",
23+
"focus": false,
24+
"clear": false
25+
}
26+
},
27+
{
28+
"label": "harness:stop",
29+
"type": "shell",
30+
"command": "pkill -f 'node scripts/runtime-harness-server.mjs' || true",
31+
"presentation": {
32+
"reveal": "never",
33+
"panel": "shared"
34+
}
35+
}
36+
]
37+
}

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ Session-close update checklist (required):
3434
3. Record blockers with file path, failure symptom, and next action.
3535
4. Confirm README priorities still match the real top blocker.
3636

37+
VS Code debugging setup (Electron docs aligned):
38+
1. Launch config file: `.vscode/launch.json`
39+
2. Task file: `.vscode/tasks.json`
40+
3. Main-process debug entry: `Electron: Debug Main (Harness)`
41+
4. Renderer attach entry: `Electron: Attach Renderer`
42+
5. Combined session: `Electron: Main + Renderer`
43+
6. Shell fallback launcher: `./scripts/launch-electron-harness.sh`
44+
3745
Docs note:
3846
1. Numeric examples in design docs are illustrative unless explicitly sourced from model metadata.
3947
2. Legacy sketch docs were retired; canonical architecture lives in `CORTEX-DESIGN-PLAN-TODO.md` and execution sequencing lives in `PROJECT-EXECUTION-PLAN.md`.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
"test:watch": "vitest tests/*.test.ts tests/**/*.test.ts",
1313
"dev:harness": "node scripts/runtime-harness-server.mjs",
1414
"test:browser": "playwright test tests/runtime/browser-harness.spec.mjs",
15-
"test:electron": "node scripts/run-electron-runtime-tests.mjs",
15+
"test:electron": "node scripts/run-electron-runtime-smoke.mjs",
16+
"test:electron:playwright": "node scripts/run-electron-runtime-tests.mjs",
17+
"test:electron:desktop": "CORTEX_ELECTRON_HEADLESS=0 CORTEX_ELECTRON_SHOW=1 node scripts/run-electron-runtime-smoke.mjs",
1618
"test:runtime": "npm run test:browser && npm run test:electron",
1719
"test:all": "npm run test:unit && npm run test:runtime",
1820
"guard:model-derived": "node scripts/guard-model-derived.mjs",

scripts/electron-harness-main.mjs

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,50 @@ import { app, BrowserWindow } from "electron";
44
import process from "node:process";
55

66
const HARNESS_URL = process.env.HARNESS_URL ?? "http://127.0.0.1:4173";
7-
const SHOW_WINDOW = process.env.CORTEX_ELECTRON_SHOW === "1";
8-
const OZONE_PLATFORM = process.env.CORTEX_OZONE_PLATFORM ?? "x11";
9-
const DISABLE_VULKAN = process.env.CORTEX_DISABLE_VULKAN !== "0";
7+
const HEADLESS_MODE = process.env.CORTEX_ELECTRON_HEADLESS === "1";
8+
const SHOW_WINDOW = !HEADLESS_MODE && process.env.CORTEX_ELECTRON_SHOW === "1";
9+
const EXIT_ON_READY = process.env.CORTEX_EXIT_ON_READY === "1";
10+
const READY_TIMEOUT_MS = Number.parseInt(
11+
process.env.CORTEX_READY_TIMEOUT_MS ?? "15000",
12+
10,
13+
);
14+
const OZONE_PLATFORM =
15+
process.env.CORTEX_OZONE_PLATFORM ??
16+
(process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
17+
const DISABLE_VULKAN = process.env.CORTEX_DISABLE_VULKAN === "1";
18+
const ENABLE_UNSAFE_WEBGPU = process.env.CORTEX_ENABLE_UNSAFE_WEBGPU === "1";
19+
const IGNORE_GPU_BLOCKLIST = process.env.CORTEX_IGNORE_GPU_BLOCKLIST === "1";
1020

11-
app.commandLine.appendSwitch("enable-unsafe-webgpu");
12-
app.commandLine.appendSwitch("ignore-gpu-blocklist");
13-
app.commandLine.appendSwitch("ozone-platform", OZONE_PLATFORM);
21+
if (HEADLESS_MODE) {
22+
// Headless mode targets CI/editor sandboxes where hardware DRI may be absent.
23+
app.disableHardwareAcceleration();
24+
app.commandLine.appendSwitch("headless");
25+
app.commandLine.appendSwitch("disable-gpu");
26+
app.commandLine.appendSwitch("use-gl", "swiftshader");
27+
app.commandLine.appendSwitch("enable-unsafe-swiftshader");
28+
}
29+
30+
if (ENABLE_UNSAFE_WEBGPU) {
31+
app.commandLine.appendSwitch("enable-unsafe-webgpu");
32+
}
33+
if (IGNORE_GPU_BLOCKLIST) {
34+
app.commandLine.appendSwitch("ignore-gpu-blocklist");
35+
}
36+
if (OZONE_PLATFORM.length > 0) {
37+
app.commandLine.appendSwitch("ozone-platform", OZONE_PLATFORM);
38+
}
1439
if (DISABLE_VULKAN) {
1540
app.commandLine.appendSwitch("disable-vulkan");
1641
}
1742

43+
process.on("unhandledRejection", (error) => {
44+
globalThis.console.error("[electron-harness] unhandledRejection", error);
45+
});
46+
47+
process.on("uncaughtException", (error) => {
48+
globalThis.console.error("[electron-harness] uncaughtException", error);
49+
});
50+
1851
app.on("gpu-info-update", async () => {
1952
try {
2053
const featureStatus = app.getGPUFeatureStatus();
@@ -26,25 +59,98 @@ app.on("gpu-info-update", async () => {
2659
}
2760
});
2861

62+
function sleep(ms) {
63+
return new Promise((resolve) => {
64+
globalThis.setTimeout(resolve, ms);
65+
});
66+
}
67+
68+
async function waitForHarnessReady(mainWindow, timeoutMs) {
69+
const started = Date.now();
70+
while (Date.now() - started < timeoutMs) {
71+
try {
72+
const ready = await mainWindow.webContents.executeJavaScript(
73+
"globalThis.__cortexHarnessReady === true",
74+
);
75+
if (ready === true) {
76+
const report = await mainWindow.webContents.executeJavaScript(
77+
"globalThis.__cortexHarnessReport ?? null",
78+
);
79+
return { ready: true, report };
80+
}
81+
} catch {
82+
// Renderer can briefly reject while booting; keep polling.
83+
}
84+
85+
await sleep(100);
86+
}
87+
88+
return { ready: false, report: null };
89+
}
90+
2991
async function createWindow() {
3092
const mainWindow = new BrowserWindow({
3193
width: 1280,
3294
height: 860,
3395
show: SHOW_WINDOW,
3496
webPreferences: {
97+
offscreen: HEADLESS_MODE,
3598
contextIsolation: true,
3699
sandbox: true,
37100
nodeIntegration: false,
38101
},
39102
});
40103

104+
mainWindow.on("closed", () => {
105+
globalThis.console.log("[electron-harness] window closed");
106+
});
107+
108+
mainWindow.webContents.on("render-process-gone", (_event, details) => {
109+
globalThis.console.error("[electron-harness] render-process-gone", JSON.stringify(details));
110+
});
111+
112+
mainWindow.webContents.on("did-fail-load", (_event, code, description, url) => {
113+
globalThis.console.error(
114+
`[electron-harness] did-fail-load code=${code} description=${description} url=${url}`,
115+
);
116+
});
117+
41118
await mainWindow.loadURL(HARNESS_URL);
42119
}
43120

44121
app.whenReady().then(async () => {
45-
globalThis.console.log(`[electron-harness] loading ${HARNESS_URL}`);
122+
globalThis.console.log(
123+
`[electron-harness] loading ${HARNESS_URL} headless=${HEADLESS_MODE} ozone=${OZONE_PLATFORM} disableVulkan=${DISABLE_VULKAN} unsafeWebgpu=${ENABLE_UNSAFE_WEBGPU} ignoreGpuBlocklist=${IGNORE_GPU_BLOCKLIST}`,
124+
);
46125
await createWindow();
47126

127+
if (EXIT_ON_READY) {
128+
const window = BrowserWindow.getAllWindows()[0];
129+
if (!window) {
130+
globalThis.console.error("[electron-harness] no window created in exit-on-ready mode");
131+
process.exitCode = 2;
132+
app.quit();
133+
return;
134+
}
135+
136+
const { ready, report } = await waitForHarnessReady(window, READY_TIMEOUT_MS);
137+
if (!ready) {
138+
globalThis.console.error(
139+
`[electron-harness] probe did not become ready within ${READY_TIMEOUT_MS}ms`,
140+
);
141+
process.exitCode = 3;
142+
app.quit();
143+
return;
144+
}
145+
146+
const selectedProvider = report?.selectedProvider ?? "unknown";
147+
globalThis.console.log(
148+
`[electron-harness] ready selectedProvider=${selectedProvider}`,
149+
);
150+
app.quit();
151+
return;
152+
}
153+
48154
app.on("activate", async () => {
49155
if (BrowserWindow.getAllWindows().length === 0) {
50156
await createWindow();
@@ -55,3 +161,7 @@ app.whenReady().then(async () => {
55161
app.on("window-all-closed", () => {
56162
app.quit();
57163
});
164+
165+
app.on("child-process-gone", (_event, details) => {
166+
globalThis.console.error("[electron-harness] child-process-gone", JSON.stringify(details));
167+
});

scripts/launch-electron-harness.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
HARNESS_HOST="${HARNESS_HOST:-127.0.0.1}"
6+
HARNESS_PORT="${HARNESS_PORT:-4173}"
7+
HARNESS_URL="${HARNESS_URL:-http://${HARNESS_HOST}:${HARNESS_PORT}}"
8+
9+
LOG_DIR="${ROOT_DIR}/.tmp/electron-harness"
10+
SERVER_LOG="${LOG_DIR}/server.log"
11+
ELECTRON_LOG="${LOG_DIR}/electron.log"
12+
13+
mkdir -p "${LOG_DIR}"
14+
15+
if [[ -x "${ROOT_DIR}/node_modules/electron/dist/electron" ]]; then
16+
ELECTRON_BIN="${ROOT_DIR}/node_modules/electron/dist/electron"
17+
elif command -v electron >/dev/null 2>&1; then
18+
ELECTRON_BIN="$(command -v electron)"
19+
else
20+
echo "Electron executable not found."
21+
echo "Install with: npm install -D electron"
22+
exit 1
23+
fi
24+
25+
cleanup() {
26+
if [[ -n "${SERVER_PID:-}" ]] && kill -0 "${SERVER_PID}" 2>/dev/null; then
27+
kill "${SERVER_PID}" >/dev/null 2>&1 || true
28+
wait "${SERVER_PID}" 2>/dev/null || true
29+
fi
30+
}
31+
32+
trap cleanup EXIT INT TERM
33+
34+
echo "[launcher] starting runtime harness server at ${HARNESS_URL}"
35+
node "${ROOT_DIR}/scripts/runtime-harness-server.mjs" >"${SERVER_LOG}" 2>&1 &
36+
SERVER_PID=$!
37+
38+
sleep 1
39+
if ! kill -0 "${SERVER_PID}" 2>/dev/null; then
40+
echo "[launcher] harness server failed to start"
41+
tail -n 200 "${SERVER_LOG}" || true
42+
exit 1
43+
fi
44+
45+
echo "[launcher] launching electron binary: ${ELECTRON_BIN}"
46+
echo "[launcher] server log: ${SERVER_LOG}"
47+
echo "[launcher] electron log: ${ELECTRON_LOG}"
48+
49+
env_args=(
50+
"HARNESS_URL=${HARNESS_URL}"
51+
"CORTEX_ELECTRON_SHOW=${CORTEX_ELECTRON_SHOW:-1}"
52+
)
53+
54+
if [[ -n "${CORTEX_OZONE_PLATFORM:-}" ]]; then
55+
env_args+=("CORTEX_OZONE_PLATFORM=${CORTEX_OZONE_PLATFORM}")
56+
fi
57+
if [[ -n "${CORTEX_DISABLE_VULKAN:-}" ]]; then
58+
env_args+=("CORTEX_DISABLE_VULKAN=${CORTEX_DISABLE_VULKAN}")
59+
fi
60+
if [[ -n "${CORTEX_ENABLE_UNSAFE_WEBGPU:-}" ]]; then
61+
env_args+=("CORTEX_ENABLE_UNSAFE_WEBGPU=${CORTEX_ENABLE_UNSAFE_WEBGPU}")
62+
fi
63+
if [[ -n "${CORTEX_IGNORE_GPU_BLOCKLIST:-}" ]]; then
64+
env_args+=("CORTEX_IGNORE_GPU_BLOCKLIST=${CORTEX_IGNORE_GPU_BLOCKLIST}")
65+
fi
66+
67+
env "${env_args[@]}" \
68+
"${ELECTRON_BIN}" "${ROOT_DIR}/scripts/electron-harness-main.mjs" \
69+
2>&1 | tee "${ELECTRON_LOG}"
70+
71+
ELECTRON_EXIT=${PIPESTATUS[0]}
72+
if [[ ${ELECTRON_EXIT} -ne 0 ]]; then
73+
echo "[launcher] electron exited with code ${ELECTRON_EXIT}"
74+
echo "[launcher] last electron log lines:"
75+
tail -n 80 "${ELECTRON_LOG}" || true
76+
fi
77+
78+
exit "${ELECTRON_EXIT}"

0 commit comments

Comments
 (0)