@@ -20,6 +20,13 @@ import { exercisesPath } from "../exercises/index.js"
2020
2121import { getTag , isDockerContainer } from "./utils.js"
2222
23+ class SubprocessTimeoutError extends Error {
24+ constructor ( timeout : number ) {
25+ super ( `Subprocess timeout after ${ timeout } ms` )
26+ this . name = "SubprocessTimeoutError"
27+ }
28+ }
29+
2330type RunTaskOptions = {
2431 run : Run
2532 task : Task
@@ -196,7 +203,9 @@ export const runTask = async ({ run, task, publish }: RunTaskOptions) => {
196203 await updateTask ( task . id , { finishedAt : new Date ( ) } )
197204 }
198205
199- if ( ! isClientDisconnected ) {
206+ if ( isClientDisconnected ) {
207+ logError ( "client disconnected before task finished" )
208+ } else {
200209 if ( rooTaskId ) {
201210 log ( "closing task" )
202211 client . sendCommand ( { commandName : TaskCommandName . CloseTask , data : rooTaskId } )
@@ -206,6 +215,36 @@ export const runTask = async ({ run, task, publish }: RunTaskOptions) => {
206215 client . disconnect ( )
207216 }
208217
218+ log ( "waiting for subprocess to finish" )
209219 controller . abort ( )
210- await subprocess
220+
221+ // Wait for subprocess to finish gracefully, with a timeout.
222+ const SUBPROCESS_TIMEOUT = 10_000
223+
224+ try {
225+ await Promise . race ( [
226+ subprocess ,
227+ new Promise ( ( _ , reject ) =>
228+ setTimeout ( ( ) => reject ( new SubprocessTimeoutError ( SUBPROCESS_TIMEOUT ) ) , SUBPROCESS_TIMEOUT ) ,
229+ ) ,
230+ ] )
231+
232+ log ( "subprocess finished gracefully" )
233+ } catch ( error ) {
234+ if ( error instanceof SubprocessTimeoutError ) {
235+ logError ( "subprocess did not finish within timeout, force killing" )
236+
237+ try {
238+ if ( subprocess . kill ( "SIGKILL" ) ) {
239+ log ( "SIGKILL sent to subprocess" )
240+ } else {
241+ logError ( "failed to send SIGKILL to subprocess" )
242+ }
243+ } catch ( killError ) {
244+ logError ( "subprocess.kill(SIGKILL) failed:" , killError )
245+ }
246+ } else {
247+ throw error
248+ }
249+ }
211250}
0 commit comments