Skip to content

Commit 4bceec0

Browse files
committed
Fix action hanging after execution by improving process handling
1 parent 85a7535 commit 4bceec0

File tree

1 file changed

+122
-11
lines changed

1 file changed

+122
-11
lines changed

index.js

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,43 @@ const axios = require("axios");
77
const unzipper = require("unzipper");
88
const yaml = require("js-yaml");
99

10+
// Handle signals to ensure clean exit
11+
process.on('SIGINT', () => {
12+
console.log('Action wrapper received SIGINT, exiting...');
13+
process.exit(0);
14+
});
15+
16+
process.on('SIGTERM', () => {
17+
console.log('Action wrapper received SIGTERM, exiting...');
18+
process.exit(0);
19+
});
20+
21+
// Set an absolute maximum timeout for the entire action (30 minutes)
22+
const MAX_ACTION_DURATION_MS = 30 * 60 * 1000;
23+
const actionTimeoutId = setTimeout(() => {
24+
core.warning(`Action timed out after ${MAX_ACTION_DURATION_MS/60000} minutes. This is likely a bug in the action wrapper. Forcing exit.`);
25+
process.exit(1);
26+
}, MAX_ACTION_DURATION_MS);
27+
28+
// Make sure the timeout doesn't prevent the process from exiting naturally
29+
actionTimeoutId.unref();
30+
1031
async function run() {
32+
// Track any child processes created
33+
const childProcesses = [];
34+
35+
// Register for process exits to ensure we clean up
36+
process.on('beforeExit', () => {
37+
core.debug(`Cleaning up any remaining child processes: ${childProcesses.length}`);
38+
childProcesses.forEach(pid => {
39+
try {
40+
process.kill(pid, 'SIGTERM');
41+
} catch (e) {
42+
// Ignore errors when killing processes that might be gone
43+
}
44+
});
45+
});
46+
1147
try {
1248
// Get inputs
1349
const actionRef = core.getInput("action-ref");
@@ -196,11 +232,32 @@ async function processAction(actionDir, extraArgs) {
196232
core.info(`Strace output will be saved to: ${stracelLogFile}`);
197233
}
198234

199-
// Use strace to wrap the node process
200-
await exec.exec("strace", [...straceOptionsList, "node", entryFile, ...args], {
235+
// Use strace to wrap the node process with explicit exit handling
236+
const options = {
201237
cwd: actionDir,
202-
env: envVars
203-
});
238+
env: envVars,
239+
listeners: {
240+
stdout: (data) => {
241+
const output = data.toString();
242+
// Just pass through stdout from the child process
243+
process.stdout.write(output);
244+
},
245+
stderr: (data) => {
246+
const output = data.toString();
247+
// Just pass through stderr from the child process
248+
process.stderr.write(output);
249+
},
250+
debug: (message) => {
251+
core.debug(message);
252+
}
253+
},
254+
// This will provide access to the child process object
255+
ignoreReturnCode: false
256+
};
257+
258+
// Use the exec implementation that gives us access to the child process
259+
const cp = await exec.getExecOutput("strace", [...straceOptionsList, "node", entryFile, ...args], options);
260+
core.debug(`Strace process completed with exit code: ${cp.exitCode}`);
204261

205262
// Export the strace log path as an output
206263
core.setOutput("strace-log", stracelLogFile);
@@ -209,18 +266,56 @@ async function processAction(actionDir, extraArgs) {
209266
// If strace is not available, fall back to running without it
210267
core.warning(`Strace is not available: ${error.message}`);
211268
core.info(`Executing nested action without strace: node ${entryFile} ${args.join(" ")}`);
212-
await exec.exec("node", [entryFile, ...args], {
269+
270+
const options = {
213271
cwd: actionDir,
214-
env: envVars
215-
});
272+
env: envVars,
273+
listeners: {
274+
stdout: (data) => {
275+
const output = data.toString();
276+
process.stdout.write(output);
277+
},
278+
stderr: (data) => {
279+
const output = data.toString();
280+
process.stderr.write(output);
281+
},
282+
debug: (message) => {
283+
core.debug(message);
284+
}
285+
},
286+
ignoreReturnCode: false
287+
};
288+
289+
// Use getExecOutput to get access to the child process
290+
const cp = await exec.getExecOutput("node", [entryFile, ...args], options);
291+
core.debug(`Node process completed with exit code: ${cp.exitCode}`);
216292
}
217293
} else {
218294
// Run without strace
219295
core.info(`Strace disabled. Executing nested action: node ${entryFile} ${args.join(" ")}`);
220-
await exec.exec("node", [entryFile, ...args], {
296+
297+
const options = {
221298
cwd: actionDir,
222-
env: envVars
223-
});
299+
env: envVars,
300+
listeners: {
301+
stdout: (data) => {
302+
const output = data.toString();
303+
process.stdout.write(output);
304+
},
305+
stderr: (data) => {
306+
const output = data.toString();
307+
process.stderr.write(output);
308+
},
309+
debug: (message) => {
310+
core.debug(message);
311+
}
312+
},
313+
ignoreReturnCode: false
314+
};
315+
316+
// Use getExecOutput to get access to the child process
317+
const cp = await exec.getExecOutput("node", [entryFile, ...args], options);
318+
core.debug(`Node process completed with exit code: ${cp.exitCode}`);
224319
}
225320
}
226321

@@ -232,4 +327,20 @@ function parseActionRef(refString) {
232327
return parts;
233328
}
234329

235-
run();
330+
run()
331+
.then(() => {
332+
core.debug('Action wrapper completed successfully');
333+
// Force exit to ensure we don't hang
334+
setTimeout(() => {
335+
core.debug('Forcing process exit to prevent hanging');
336+
process.exit(0);
337+
}, 500);
338+
})
339+
.catch(error => {
340+
core.setFailed(`Action wrapper failed: ${error.message}`);
341+
// Force exit to ensure we don't hang
342+
setTimeout(() => {
343+
core.debug('Forcing process exit to prevent hanging');
344+
process.exit(1);
345+
}, 500);
346+
});

0 commit comments

Comments
 (0)