From 6581a9ef33d8273654cde57dcf2553aba1936d87 Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 22:23:30 +0200 Subject: [PATCH] fix(agent): await child completion in /agent/v1/exec route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fastify-handler returnde direct na het attachen van event handlers. Fastify finaliseerde dan de reply waardoor `req.raw.on('close')` direct firede en `child.kill()` aanriep voordat het kind iets kon produceren. Symptoom: SSE bevatte alleen `event:exit code:null` zonder stdout/stderr, audit-log toonde `exit_code:null duration_ms:0`, dashboard-modules toonden "No containers running" / "No data" terwijl handmatige command prima werkte. Wrap de event-handlers in een Promise zodat de async route-handler wacht op child close/error voordat ie returnt. Verplaats client-disconnect detectie van `req.raw.on('close')` naar `reply.raw.on('close')` — die fired bij echte connectie-sluiting, niet bij request body parse. Bevestigd: `docker_ps` retourneert nu volledige container-lijst, dashboard /docker pagina rendert alle 6 containers. Co-Authored-By: Claude Opus 4.7 (1M context) --- ops-agent/src/routes/exec.ts | 43 +++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/ops-agent/src/routes/exec.ts b/ops-agent/src/routes/exec.ts index 9a393ec..d017246 100644 --- a/ops-agent/src/routes/exec.ts +++ b/ops-agent/src/routes/exec.ts @@ -108,20 +108,33 @@ export async function execRoutes(app: FastifyInstance): Promise { sendEvent('stderr', chunk.toString()); }); - child.on('close', (code) => { - auditLog(command_key, args, code, Date.now() - startedAt); - reply.raw.write(`event: exit\ndata: ${JSON.stringify({ code })}\n\n`); - reply.raw.end(); - }); - - child.on('error', (err) => { - auditLog(command_key, args, null, Date.now() - startedAt); - reply.raw.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`); - reply.raw.end(); - }); - - req.raw.on('close', () => { - child.kill(); - }); + // Houd de route-handler open totdat het kind klaar is. Zonder dit return-t + // de async functie meteen, finaliseert Fastify de reply, en triggert dat + // `req.raw.on('close')` → `child.kill()` voordat het kind iets kon doen. + await new Promise((resolve) => { + let settled = false + const finish = () => { + if (settled) return + settled = true + resolve() + } + child.on('close', (code) => { + auditLog(command_key, args, code, Date.now() - startedAt) + reply.raw.write(`event: exit\ndata: ${JSON.stringify({ code })}\n\n`) + reply.raw.end() + finish() + }) + child.on('error', (err) => { + auditLog(command_key, args, null, Date.now() - startedAt) + reply.raw.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`) + reply.raw.end() + finish() + }) + // Detect client disconnect via response stream (niet request stream — + // die fired al direct na request body parse). + reply.raw.on('close', () => { + if (!settled) child.kill() + }) + }) }); }