Skip to content

Commit 95fa269

Browse files
committed
fix(ci): prevent Node test hangs
1 parent c36fa57 commit 95fa269

5 files changed

Lines changed: 94 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
rust-and-smoke:
2020
name: Rust and Smoke Tests
2121
runs-on: ubuntu-latest
22+
timeout-minutes: 30
2223
steps:
2324
- uses: actions/checkout@v7
2425

@@ -38,6 +39,7 @@ jobs:
3839
bash -n scripts/ci/validate-nix-pins.sh
3940
4041
- name: Check Node syntax and tests
42+
timeout-minutes: 10
4143
run: |
4244
set -euo pipefail
4345
bash scripts/ci/run-node-checks.sh

linux-features/global-dictation/test.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const hostPathEntries = (process.env.PATH ?? "")
2525
.split(path.delimiter)
2626
.filter((entry) => path.isAbsolute(entry));
2727
const hostPath = [...new Set(hostPathEntries)].join(path.delimiter);
28+
const X11_RELEASE_MONITOR_TEST_TIMEOUT_MS = 3000;
2829

2930
function findHostExecutable(name) {
3031
const executable = hostPathEntries
@@ -115,7 +116,13 @@ function writeExecutable(filePath, body) {
115116
fs.writeFileSync(filePath, body, { mode: 0o755 });
116117
}
117118

118-
function runX11ReleaseMonitor({ accelerator, xmodmapLines, queryStateLines, testLines, timeout = 1000 }) {
119+
function runX11ReleaseMonitor({
120+
accelerator,
121+
xmodmapLines,
122+
queryStateLines,
123+
testLines,
124+
timeout = X11_RELEASE_MONITOR_TEST_TIMEOUT_MS,
125+
}) {
119126
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "global-dictation-x11-map-"));
120127
const binDir = path.join(tempDir, "bin");
121128
fs.mkdirSync(binDir);
@@ -411,7 +418,7 @@ test("X11 release watcher exits when the primary key is released while the modif
411418
DISPLAY: ":99",
412419
PATH: `${binDir}${path.delimiter}${hostPath}`,
413420
},
414-
timeout: 1000,
421+
timeout: X11_RELEASE_MONITOR_TEST_TIMEOUT_MS,
415422
},
416423
);
417424
assert.equal(result.status, 0, result.stderr);
@@ -453,7 +460,7 @@ test("X11 release watcher prefers resolved keycodes over fallback codes", () =>
453460
DISPLAY: ":99",
454461
PATH: `${binDir}${path.delimiter}${hostPath}`,
455462
},
456-
timeout: 1000,
463+
timeout: X11_RELEASE_MONITOR_TEST_TIMEOUT_MS,
457464
},
458465
);
459466
assert.equal(result.status, 0, result.stderr);

linux-features/remote-mobile-control/patch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ function linuxDeviceKeyProviderSource({ childProcessVar, cryptoVar, fsVar, pathV
133133
"}",
134134
`function codexLinuxRemoteControlOpenKeyStoreLock(){let e=codexLinuxRemoteControlDeviceKeyStorePath()+\`.lock\`,t,n=!1;try{try{t=${fsVar}.openSync(e,${fsVar}.constants.O_RDWR|${fsVar}.constants.O_CREAT|${fsVar}.constants.O_EXCL|${fsVar}.constants.O_NOFOLLOW,384),n=!0}catch(r){if(r?.code!==\`EEXIST\`)throw r;t=${fsVar}.openSync(e,${fsVar}.constants.O_RDWR|${fsVar}.constants.O_NOFOLLOW)}n&&${fsVar}.fchmodSync(t,384),codexLinuxRemoteControlAssertOwnedRegularStat(${fsVar}.fstatSync(t));return t}catch(r){try{t!=null&&${fsVar}.closeSync(t)}catch{}throw r}}`,
135135
`function codexLinuxRemoteControlResolveExecutable(codexLinuxRemoteControlExecutableName){let codexLinuxRemoteControlSearchDirectories=[...(process.env.PATH??\`\`).split(${pathVar}.delimiter),\`/run/current-system/sw/bin\`,\`/nix/var/nix/profiles/default/bin\`,\`/usr/local/bin\`,\`/usr/bin\`,\`/bin\`],codexLinuxRemoteControlSeenDirectories=new Set;for(let codexLinuxRemoteControlDirectory of codexLinuxRemoteControlSearchDirectories){if(!codexLinuxRemoteControlDirectory||!${pathVar}.isAbsolute(codexLinuxRemoteControlDirectory)||codexLinuxRemoteControlSeenDirectories.has(codexLinuxRemoteControlDirectory))continue;codexLinuxRemoteControlSeenDirectories.add(codexLinuxRemoteControlDirectory);try{let codexLinuxRemoteControlExecutablePath=${fsVar}.realpathSync(${pathVar}.join(codexLinuxRemoteControlDirectory,codexLinuxRemoteControlExecutableName)),codexLinuxRemoteControlExecutableStat=${fsVar}.statSync(codexLinuxRemoteControlExecutablePath);if(!codexLinuxRemoteControlExecutableStat.isFile())continue;${fsVar}.accessSync(codexLinuxRemoteControlExecutablePath,${fsVar}.constants.X_OK);return codexLinuxRemoteControlExecutablePath}catch{}}return null}`,
136-
`function codexLinuxWithRemoteControlKeyStoreLock(codexLinuxRemoteControlLockedOperation){let codexLinuxRemoteControlLockFd=codexLinuxRemoteControlOpenKeyStoreLock(),codexLinuxRemoteControlFlockPath=codexLinuxRemoteControlResolveExecutable(\`flock\`),codexLinuxRemoteControlShellPath=codexLinuxRemoteControlResolveExecutable(\`sh\`);if(codexLinuxRemoteControlFlockPath==null||codexLinuxRemoteControlShellPath==null){${fsVar}.closeSync(codexLinuxRemoteControlLockFd);throw Error(\`Linux remote control device key store requires flock and sh\`)}return new Promise((codexLinuxRemoteControlResolve,codexLinuxRemoteControlReject)=>{let codexLinuxRemoteControlLockProcess;try{codexLinuxRemoteControlLockProcess=${childProcessVar}.spawn(codexLinuxRemoteControlFlockPath,[\`-x\`,\`-w\`,\`5\`,\`/proc/self/fd/3\`,codexLinuxRemoteControlShellPath,\`-c\`,\`printf 'ready\\n'; cat >/dev/null\`],{stdio:[\`pipe\`,\`pipe\`,\`pipe\`,codexLinuxRemoteControlLockFd]})}finally{${fsVar}.closeSync(codexLinuxRemoteControlLockFd)}let codexLinuxRemoteControlStdout=\`\`,codexLinuxRemoteControlStderr=\`\`,codexLinuxRemoteControlResult,codexLinuxRemoteControlReady=!1,codexLinuxRemoteControlOperationDone=!1,codexLinuxRemoteControlProcessDone=!1,codexLinuxRemoteControlExitCode=null,codexLinuxRemoteControlFailure=null,codexLinuxRemoteControlTimer=setTimeout(()=>{codexLinuxRemoteControlReady||(codexLinuxRemoteControlFailure=Error(\`Timed out waiting for Linux remote control device key store lock\`),codexLinuxRemoteControlOperationDone=!0,codexLinuxRemoteControlLockProcess.kill())},5500),codexLinuxRemoteControlSettle=()=>{if(!codexLinuxRemoteControlOperationDone||!codexLinuxRemoteControlProcessDone)return;codexLinuxRemoteControlFailure?codexLinuxRemoteControlReject(codexLinuxRemoteControlFailure):codexLinuxRemoteControlExitCode===0?codexLinuxRemoteControlResolve(codexLinuxRemoteControlResult):codexLinuxRemoteControlReject(Error(\`Linux remote control device key store lock failed\`))};codexLinuxRemoteControlLockProcess.stderr.on(\`data\`,codexLinuxRemoteControlChunk=>{codexLinuxRemoteControlStderr=(codexLinuxRemoteControlStderr+String(codexLinuxRemoteControlChunk)).slice(-4096)}),codexLinuxRemoteControlLockProcess.on(\`error\`,codexLinuxRemoteControlError=>{clearTimeout(codexLinuxRemoteControlTimer),codexLinuxRemoteControlFailure=codexLinuxRemoteControlError,codexLinuxRemoteControlOperationDone=!0,codexLinuxRemoteControlProcessDone=!0,codexLinuxRemoteControlSettle()}),codexLinuxRemoteControlLockProcess.on(\`exit\`,codexLinuxRemoteControlCode=>{clearTimeout(codexLinuxRemoteControlTimer),codexLinuxRemoteControlExitCode=codexLinuxRemoteControlCode,codexLinuxRemoteControlProcessDone=!0,codexLinuxRemoteControlReady||(codexLinuxRemoteControlFailure=Error(codexLinuxRemoteControlStderr.trim()||\`Timed out waiting for Linux remote control device key store lock\`),codexLinuxRemoteControlOperationDone=!0),codexLinuxRemoteControlSettle()}),codexLinuxRemoteControlLockProcess.stdout.on(\`data\`,codexLinuxRemoteControlChunk=>{if(codexLinuxRemoteControlReady)return;codexLinuxRemoteControlStdout+=String(codexLinuxRemoteControlChunk);if(!codexLinuxRemoteControlStdout.includes(\`ready\\n\`))return;codexLinuxRemoteControlReady=!0,clearTimeout(codexLinuxRemoteControlTimer),Promise.resolve().then(codexLinuxRemoteControlLockedOperation).then(codexLinuxRemoteControlValue=>{codexLinuxRemoteControlResult=codexLinuxRemoteControlValue,codexLinuxRemoteControlOperationDone=!0,codexLinuxRemoteControlLockProcess.stdin.end(),codexLinuxRemoteControlSettle()},codexLinuxRemoteControlError=>{codexLinuxRemoteControlFailure=codexLinuxRemoteControlError,codexLinuxRemoteControlOperationDone=!0,codexLinuxRemoteControlLockProcess.stdin.end(),codexLinuxRemoteControlSettle()})})})}`,
136+
`function codexLinuxWithRemoteControlKeyStoreLock(codexLinuxRemoteControlLockedOperation){let codexLinuxRemoteControlLockFd=codexLinuxRemoteControlOpenKeyStoreLock(),codexLinuxRemoteControlFlockPath=codexLinuxRemoteControlResolveExecutable(\`flock\`),codexLinuxRemoteControlShellPath=codexLinuxRemoteControlResolveExecutable(\`sh\`);if(codexLinuxRemoteControlFlockPath==null||codexLinuxRemoteControlShellPath==null){${fsVar}.closeSync(codexLinuxRemoteControlLockFd);throw Error(\`Linux remote control device key store requires flock and sh\`)}return new Promise((codexLinuxRemoteControlResolve,codexLinuxRemoteControlReject)=>{let codexLinuxRemoteControlLockProcess;try{codexLinuxRemoteControlLockProcess=${childProcessVar}.spawn(codexLinuxRemoteControlFlockPath,[\`-x\`,\`-w\`,\`5\`,\`/proc/self/fd/3\`,codexLinuxRemoteControlShellPath,\`-c\`,\`printf 'ready\\n'; cat >/dev/null\`],{stdio:[\`pipe\`,\`pipe\`,\`pipe\`,codexLinuxRemoteControlLockFd]})}finally{${fsVar}.closeSync(codexLinuxRemoteControlLockFd)}let codexLinuxRemoteControlStdout=\`\`,codexLinuxRemoteControlStderr=\`\`,codexLinuxRemoteControlResult,codexLinuxRemoteControlReady=!1,codexLinuxRemoteControlOperationDone=!1,codexLinuxRemoteControlProcessDone=!1,codexLinuxRemoteControlExitCode=null,codexLinuxRemoteControlFailure=null,codexLinuxRemoteControlTimer=setTimeout(()=>{codexLinuxRemoteControlReady||(codexLinuxRemoteControlFailure=Error(\`Timed out waiting for Linux remote control device key store lock\`),codexLinuxRemoteControlOperationDone=!0,codexLinuxRemoteControlLockProcess.kill())},5500),codexLinuxRemoteControlSettle=()=>{if(!codexLinuxRemoteControlOperationDone||!codexLinuxRemoteControlProcessDone)return;codexLinuxRemoteControlFailure?codexLinuxRemoteControlReject(codexLinuxRemoteControlFailure):codexLinuxRemoteControlExitCode===0?codexLinuxRemoteControlResolve(codexLinuxRemoteControlResult):codexLinuxRemoteControlReject(Error(\`Linux remote control device key store lock failed\`))};codexLinuxRemoteControlLockProcess.stderr.on(\`data\`,codexLinuxRemoteControlChunk=>{codexLinuxRemoteControlStderr=(codexLinuxRemoteControlStderr+String(codexLinuxRemoteControlChunk)).slice(-4096)}),codexLinuxRemoteControlLockProcess.on(\`error\`,codexLinuxRemoteControlError=>{clearTimeout(codexLinuxRemoteControlTimer),codexLinuxRemoteControlFailure=codexLinuxRemoteControlError,codexLinuxRemoteControlOperationDone=!0,codexLinuxRemoteControlProcessDone=!0,codexLinuxRemoteControlSettle()}),codexLinuxRemoteControlLockProcess.on(\`close\`,codexLinuxRemoteControlCode=>{clearTimeout(codexLinuxRemoteControlTimer),codexLinuxRemoteControlExitCode=codexLinuxRemoteControlCode,codexLinuxRemoteControlProcessDone=!0,codexLinuxRemoteControlReady||(codexLinuxRemoteControlFailure=Error(codexLinuxRemoteControlStderr.trim()||\`Timed out waiting for Linux remote control device key store lock\`),codexLinuxRemoteControlOperationDone=!0),codexLinuxRemoteControlSettle()}),codexLinuxRemoteControlLockProcess.stdout.on(\`data\`,codexLinuxRemoteControlChunk=>{if(codexLinuxRemoteControlReady)return;codexLinuxRemoteControlStdout+=String(codexLinuxRemoteControlChunk);if(!codexLinuxRemoteControlStdout.includes(\`ready\\n\`))return;codexLinuxRemoteControlReady=!0,clearTimeout(codexLinuxRemoteControlTimer),Promise.resolve().then(codexLinuxRemoteControlLockedOperation).then(codexLinuxRemoteControlValue=>{codexLinuxRemoteControlResult=codexLinuxRemoteControlValue,codexLinuxRemoteControlOperationDone=!0,codexLinuxRemoteControlLockProcess.stdin.end(),codexLinuxRemoteControlSettle()},codexLinuxRemoteControlError=>{codexLinuxRemoteControlFailure=codexLinuxRemoteControlError,codexLinuxRemoteControlOperationDone=!0,codexLinuxRemoteControlLockProcess.stdin.end(),codexLinuxRemoteControlSettle()})})})}`,
137137
"function codexLinuxWriteRemoteControlDeviceKeyStore(codexLinuxRemoteControlKeyStore){",
138138
`codexLinuxRemoteControlValidateDeviceKeyStore(codexLinuxRemoteControlKeyStore);let codexLinuxRemoteControlKeyStorePath=codexLinuxRemoteControlDeviceKeyStorePath(),codexLinuxRemoteControlKeyStoreDirectory=${pathVar}.dirname(codexLinuxRemoteControlKeyStorePath),codexLinuxRemoteControlTempPath=codexLinuxRemoteControlKeyStorePath+\`.tmp-\`+${cryptoVar}.randomUUID(),codexLinuxRemoteControlKeyStoreText=JSON.stringify(codexLinuxRemoteControlKeyStore,null,2)+\`\\n\`,codexLinuxRemoteControlTempFd=null,codexLinuxRemoteControlDirectoryFd=null,codexLinuxRemoteControlTempCreated=!1;if(Buffer.byteLength(codexLinuxRemoteControlKeyStoreText,\`utf8\`)>codexLinuxRemoteControlKeyStoreMaxBytes)throw Error(\`Linux remote control device key store exceeds size limit\`);`,
139139
`try{codexLinuxRemoteControlTempFd=${fsVar}.openSync(codexLinuxRemoteControlTempPath,${fsVar}.constants.O_WRONLY|${fsVar}.constants.O_CREAT|${fsVar}.constants.O_EXCL|${fsVar}.constants.O_NOFOLLOW,384),codexLinuxRemoteControlTempCreated=!0,${fsVar}.writeFileSync(codexLinuxRemoteControlTempFd,codexLinuxRemoteControlKeyStoreText,\`utf8\`),${fsVar}.fsyncSync(codexLinuxRemoteControlTempFd),${fsVar}.closeSync(codexLinuxRemoteControlTempFd),codexLinuxRemoteControlTempFd=null;${fsVar}.existsSync(codexLinuxRemoteControlKeyStorePath)&&codexLinuxRemoteControlAssertOwnedRegularFile(codexLinuxRemoteControlKeyStorePath,${fsVar});${fsVar}.renameSync(codexLinuxRemoteControlTempPath,codexLinuxRemoteControlKeyStorePath),codexLinuxRemoteControlDirectoryFd=${fsVar}.openSync(codexLinuxRemoteControlKeyStoreDirectory,${fsVar}.constants.O_RDONLY),${fsVar}.fsyncSync(codexLinuxRemoteControlDirectoryFd),${fsVar}.closeSync(codexLinuxRemoteControlDirectoryFd),codexLinuxRemoteControlDirectoryFd=null}catch(codexLinuxRemoteControlWriteError){try{codexLinuxRemoteControlTempFd!=null&&${fsVar}.closeSync(codexLinuxRemoteControlTempFd)}catch{}try{codexLinuxRemoteControlDirectoryFd!=null&&${fsVar}.closeSync(codexLinuxRemoteControlDirectoryFd)}catch{}try{codexLinuxRemoteControlTempCreated&&${fsVar}.rmSync(codexLinuxRemoteControlTempPath,{force:!0})}catch{}throw codexLinuxRemoteControlWriteError}`,

linux-features/remote-mobile-control/test.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
const assert = require("node:assert/strict");
55
const { spawn, spawnSync } = require("node:child_process");
6+
const { EventEmitter, once } = require("node:events");
67
const fs = require("node:fs");
78
const os = require("node:os");
89
const path = require("node:path");
@@ -2906,16 +2907,54 @@ test("Linux device-key store serializes concurrent updates", async () => {
29062907
}
29072908
});
29082909

2909-
test("Linux device-key store contends on its validated lock file", async () => {
2910+
test("Linux device-key operations wait for lock process stdio to close", async () => {
2911+
const configHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-remote-mobile-key-close-"));
2912+
try {
2913+
const child = new EventEmitter();
2914+
child.stdin = { end() {} };
2915+
child.stdout = new EventEmitter();
2916+
child.stderr = new EventEmitter();
2917+
child.kill = () => true;
2918+
2919+
const client = createPatchedDeviceKeyClient(configHome, {
2920+
"node:child_process": {
2921+
spawn() {
2922+
return child;
2923+
},
2924+
},
2925+
});
2926+
let settled = false;
2927+
const creation = client.createDeviceKey("allow_os_protected_nonextractable").then((value) => {
2928+
settled = true;
2929+
return value;
2930+
});
2931+
2932+
child.stdout.emit("data", Buffer.from("ready\n"));
2933+
await new Promise((resolve) => setImmediate(resolve));
2934+
child.emit("exit", 0, null);
2935+
await new Promise((resolve) => setImmediate(resolve));
2936+
2937+
assert.equal(settled, false, "the lock operation must not resolve before child stdio closes");
2938+
2939+
child.emit("close", 0, null);
2940+
await creation;
2941+
} finally {
2942+
fs.rmSync(configHome, { recursive: true, force: true });
2943+
}
2944+
});
2945+
2946+
test("Linux device-key store contends on its validated lock file", { timeout: 10_000 }, async () => {
29102947
const configHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-remote-mobile-key-lock-"));
29112948
let holder;
2949+
let holderClosed;
29122950
try {
29132951
const client = createPatchedDeviceKeyClient(configHome);
29142952
await client.createDeviceKey("test");
29152953
const { lock } = remoteControlKeyStorePaths(configHome);
29162954
holder = spawn("flock", ["-x", lock, "sh", "-c", "printf 'ready\\n'; sleep 0.25"], {
29172955
stdio: ["ignore", "pipe", "pipe"],
29182956
});
2957+
holderClosed = once(holder, "close");
29192958
await new Promise((resolve, reject) => {
29202959
let output = "";
29212960
holder.once("error", reject);
@@ -2927,9 +2966,14 @@ test("Linux device-key store contends on its validated lock file", async () => {
29272966

29282967
const startedAt = Date.now();
29292968
await client.createDeviceKey("test");
2969+
const [holderExitCode] = await holderClosed;
29302970
assert.ok(Date.now() - startedAt >= 150, "key update must wait for the existing file lock");
2971+
assert.equal(holderExitCode, 0);
29312972
} finally {
2932-
holder?.kill();
2973+
if (holder?.exitCode == null && holder?.signalCode == null) {
2974+
holder.kill("SIGKILL");
2975+
}
2976+
await holderClosed?.catch(() => {});
29332977
fs.rmSync(configHome, { recursive: true, force: true });
29342978
}
29352979
});

scripts/ci/run-node-checks.sh

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ set -euo pipefail
33

44
REPO_DIR="$(git rev-parse --show-toplevel)"
55
MODE="${1:-all}"
6+
NODE_TEST_TIMEOUT_SECONDS="${NODE_TEST_TIMEOUT_SECONDS:-300}"
7+
NODE_TEST_KILL_AFTER_SECONDS="${NODE_TEST_KILL_AFTER_SECONDS:-30}"
68

79
cd "$REPO_DIR"
810

@@ -23,6 +25,7 @@ run_node_syntax_checks() {
2325

2426
run_node_tests() {
2527
local file
28+
local status
2629
local -a node_test_args=(--test)
2730
local -a test_files=()
2831

@@ -39,10 +42,40 @@ run_node_tests() {
3942
if [ -n "${NODE_TEST_REPORTER:-}" ]; then
4043
node_test_args+=(--test-reporter="$NODE_TEST_REPORTER")
4144
elif [ "${GITHUB_ACTIONS:-}" = "true" ] && node --help | grep -q -- "--test-reporter"; then
42-
node_test_args+=(--test-reporter=dot)
45+
# Keep the last completed test visible when a worker leaks a handle.
46+
node_test_args+=(--test-reporter=spec)
4347
fi
4448

45-
node "${node_test_args[@]}" "${test_files[@]}"
49+
case "$NODE_TEST_TIMEOUT_SECONDS:$NODE_TEST_KILL_AFTER_SECONDS" in
50+
*[!0-9:]*|0:*|*:0)
51+
echo "NODE_TEST_TIMEOUT_SECONDS and NODE_TEST_KILL_AFTER_SECONDS must be positive integers" >&2
52+
return 2
53+
;;
54+
esac
55+
56+
if ! command -v timeout >/dev/null 2>&1; then
57+
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
58+
echo "GNU timeout is required to bound Node tests in GitHub Actions" >&2
59+
return 2
60+
fi
61+
node "${node_test_args[@]}" "${test_files[@]}"
62+
return
63+
fi
64+
65+
if timeout \
66+
--signal=TERM \
67+
--kill-after="${NODE_TEST_KILL_AFTER_SECONDS}s" \
68+
"${NODE_TEST_TIMEOUT_SECONDS}s" \
69+
node "${node_test_args[@]}" "${test_files[@]}"; then
70+
return 0
71+
else
72+
status=$?
73+
fi
74+
75+
if [ "$status" -eq 124 ] || [ "$status" -eq 137 ]; then
76+
echo "Node test suite exceeded ${NODE_TEST_TIMEOUT_SECONDS}s; terminated to prevent a hung CI job" >&2
77+
fi
78+
return "$status"
4679
}
4780

4881
case "$MODE" in

0 commit comments

Comments
 (0)