Skip to content

fix: reap orphaned dev-server child on process exit#1695

Merged
tejaskash merged 1 commit into
aws:mainfrom
hagaym1:fix/dev-orphaned-child-on-exit
Jul 8, 2026
Merged

fix: reap orphaned dev-server child on process exit#1695
tejaskash merged 1 commit into
aws:mainfrom
hagaym1:fix/dev-orphaned-child-on-exit

Conversation

@hagaym1

@hagaym1 hagaym1 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

agentcore dev spawns the dev server detached (its own process group, added in #1438) and relies on the dev command's SIGINT/SIGTERM handler (onShutdownSignal, registered with process.once) to call server.kill() and tree-kill the group. But setupAltScreenCleanup() (src/cli/cli.ts, called first thing in main()) registers a global process.on('SIGINT'/'SIGTERM') handler that calls process.exit(0). Node runs signal listeners in registration order, and this global handler is registered before the dev command's — so it fires first, process.exit(0) terminates the process, and server.kill() never runs. The detached child is then reparented to PID 1 and keeps holding the port.

This reproduces on every stop — both interactive Ctrl+C (SIGINT) and programmatic kill $PID (SIGTERM) — for both Python (uvicorn) and TypeScript/Node dev servers. #1438 correctly fixed the reparenting mechanics (detached spawn + process-group tree-kill in kill()), but the compensating handler it added is preempted by the pre-existing global process.exit(0), so the orphan is back in 0.22.0 (and now also on Ctrl+C, since the detached child no longer receives the terminal's group SIGINT directly).

Fix

Register a process.on('exit') reaper in DevServer.start() that sends SIGKILL to the detached child's process group. The 'exit' event always fires on process.exit(), so the child is reaped no matter which path exits the process (the global cleanup handler, an uncaught exception in index.ts, or a normal exit). kill()'s graceful SIGTERM-then-SIGKILL path is unchanged and still runs when it is reached; this is a last-resort net for when it isn't. The listener is removed when the child exits on its own, to avoid a stale reaper firing at final process exit. POSIX-only, matching the POSIX-only detached spawn (on Windows the child is not detached and dies with the parent).

Related Issue

Closes #1690

Documentation PR

N/A

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Other (please describe):

Testing

Added unit tests in dev-server.test.ts covering the reaper: it SIGKILLs the child's process group on exit, is skipped when there is no pid, is removed once the child exits on its own, and swallows ESRCH when the group is already gone.

Verified end-to-end on macOS with a TypeScript agent, before vs. after this change, mirroring the repro in #1690 / #1440:

agentcore dev --logs --port 9137 &
DEV_PID=$!
# wait for the port to bind, then:
kill $DEV_PID
sleep 4
lsof -ti :9137
# before this change: orphaned node still listening (reparented to PPID 1)
# after this change:  empty — port freed
  • I ran npm run test:unit and npm run test:integ
  • I ran npm run typecheck
  • I ran npm run lint
  • If I modified src/assets/, I ran npm run test:update-snapshots and committed the updated snapshots (N/A — no asset changes)

Note on the local runs: typecheck, lint, and the added dev-server unit tests all pass. The remaining test:unit / test:integ failures on my machine are environmental and outside the changed dev-server area: the unit failures are AWS SSO credential-resolution tests (resolveSSOCredentials, need live AWS credentials), and the integ failures are Python-project create tests that require the uv toolchain, which isn't installed in my sandbox ('uv' is required for Python projects). The repo's own integ-tests/dev-server.test.ts is skipIf(!hasUv) and skips cleanly here.

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the
terms of your choice.

`agentcore dev` spawns the dev server detached (its own process group) and
relies on the dev command's SIGINT/SIGTERM handler to call server.kill().
But setupAltScreenCleanup() registers a global SIGINT/SIGTERM handler at
startup that calls process.exit(0), and it runs first — so kill() never runs
and the detached child is orphaned, holding the port on every stop (Ctrl+C
or `kill $PID`), for both Python and TypeScript dev servers.

Register a process.on('exit') reaper in DevServer.start() that SIGKILLs the
child's process group. The 'exit' event always fires on process.exit(), so
the child is reaped regardless of which shutdown path exits the process. The
listener is removed when the child exits on its own.

Closes aws#1690
@hagaym1 hagaym1 requested a review from a team July 7, 2026 08:09
@github-actions github-actions Bot added the size/s PR size: S label Jul 7, 2026
@hagaym1

hagaym1 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

This directly closes the gap left by #1438. That PR made the dev child detached (its own process group) and added a SIGINT/SIGTERM handler to tree-kill it — but the global process.on('SIGINT'/'SIGTERM') in setupAltScreenCleanup (registered first, in main()) calls process.exit(0) and runs before the dev command's handler, so server.kill() never runs and the child is orphaned on every stop (Ctrl+C and kill $PID alike, Python and TypeScript). The fix reaps the child's process group in a process.on('exit') hook, which always fires on process.exit().

Root cause, mechanism, and a before/after A/B are in the description; unit tests added; typecheck/lint pass locally.

@avi-alpert (author of #1438) / @jariy17 — could you take a look? And could a maintainer apply safe-to-review so CI and the security review can run? Happy to adjust anything.

@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Jul 8, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@tejaskash tejaskash left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you for your contribution

@tejaskash tejaskash merged commit 0f115e7 into aws:main Jul 8, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: agentcore dev leaves orphaned Uvicorn process on Ctrl+C

2 participants