Skip to content

Commit 5d2cd0e

Browse files
chore: version packages [skip ci]
1 parent a40e840 commit 5d2cd0e

5 files changed

Lines changed: 86 additions & 88 deletions

File tree

.changeset/dev-server-restart-race-502.md

Lines changed: 0 additions & 81 deletions
This file was deleted.

.changeset/fix-wsl-dev-server.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/CHANGELOG.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,89 @@
11
# @aws-blocks/core
22

3+
## 0.1.7
4+
5+
### Patch Changes
6+
7+
- 6eb731a: fix(dev-server): auto-respawn frontend and kill the whole process group on restart
8+
9+
The dev server spawns the frontend (Vite) with `shell: true`, making the real
10+
Vite process a **grandchild** (shell → npx → node vite). On a `tsx watch`
11+
restart, cleanup sent `SIGTERM` to only the shell parent, orphaning the Vite
12+
grandchild — it survived still bound to `:3100`. The freshly launched Vite then
13+
hit `--strictPort`, failed to bind, and exited; the `exit` handler only logged,
14+
so `/` served a permanent `502 Frontend server unavailable` with no recovery.
15+
16+
Fixes:
17+
18+
- **Process-group kill** — the frontend is spawned `detached` on POSIX (its own
19+
process group) and cleanup/restart now signal the entire group via
20+
`process.kill(-pid, …)`, reaping the Vite grandchild and freeing `:3100`.
21+
Windows (no POSIX groups) reaps the tree with `taskkill /T /F /PID <pid>`,
22+
which walks the child tree by PID so the Vite grandchild is killed too; it
23+
degrades to a direct child kill only if `taskkill` cannot be spawned.
24+
- **Bounded auto-respawn** — an unexpected frontend exit now respawns Vite with
25+
exponential backoff, capped at 5 restarts / 10s to avoid hot loops, and is
26+
suppressed during intentional shutdown via an `isShuttingDown` guard. The
27+
budget counts only _consecutive failing_ restarts: it resets only when **our
28+
own** freshly spawned child is the process now bound to the port. A liveness
29+
probe alone cannot tell our Vite from a foreign listener (a leftover Vite or a
30+
second dev server), and crediting a foreign one would make every
31+
`--strictPort`-failing respawn look successful, neutralizing the cap and
32+
hot-looping forever. A frontend that legitimately restarts many times (e.g.
33+
editor-triggered full reloads) is still never permanently left down. Before
34+
each relaunch the supervisor now also waits (bounded) for `:3100` to be
35+
released — the same port-free drain the graceful shutdown path uses — so a slow
36+
socket teardown can't hand the relaunched `--strictPort` Vite an `EADDRINUSE`
37+
and burn a restart-budget slot; that wait re-checks the `isShuttingDown` guard,
38+
so a shutdown arriving mid-wait still cancels the relaunch (and the budget is
39+
debited once, at exit time, so the wait never double-counts a restart).
40+
- **Robust shutdown** — cleanup is idempotent, wired to `SIGINT`/`SIGTERM`/
41+
`SIGHUP`, removes its own listeners, and waits (bounded) for the group to die
42+
**and for `:3100` to actually be released** before exiting: SIGTERM→SIGKILL
43+
escalation, then a port-free poll that runs on _both_ the live and the
44+
already-exited paths (the post-exit path previously skipped it, so a relaunch
45+
could race the kernel's socket teardown into `--strictPort` `EADDRINUSE`). A
46+
synchronous `process.on('exit')` safety net remains for paths that bypass
47+
cleanup — now routed through the shared tree-kill so it reaps on Windows
48+
(`taskkill`) too instead of early-returning and leaking the Vite tree.
49+
- **Consistent post-exit reaping** — the failure being fixed is the _shell
50+
exiting while the detached grandchild survives_, so every post-exit path
51+
(the respawn handler, graceful shutdown, and the `exit` safety net) now
52+
issues one best-effort process-group kill even after the shell has gone,
53+
rather than skipping it. A surviving grandchild keeps the group's id reserved
54+
on POSIX, so `process.kill(-pid)` still targets our own group; the kills are
55+
issued synchronously on observing the exit to keep the PID-reuse window
56+
minimal. The single rationale lives next to the supervisor as the
57+
"POST-EXIT GROUP-KILL POLICY" so all three sites stay in agreement.
58+
- **Sandbox entrypoint parity**`sandbox.ts` (the sibling dev entrypoint) now
59+
spawns **both** long-running children — the dev server _and_ `cdk watch` — in
60+
their own process groups and `await`s a bounded group teardown for each
61+
(run concurrently) on `SIGINT`/`SIGTERM`, replacing the synchronous
62+
`cdkWatch.kill()` + single dev-server `kill()` + `process.exit(0)` that
63+
signalled only the npx/shell parents and exited immediately. A bare
64+
`cdkWatch.kill()` could orphan the real `cdk watch` node process
65+
(npx → cdk → node) — the same shell-only-kill leak this PR fixes for the dev
66+
server — so it now routes through the shared `terminateProcessTree` too. Only
67+
the dev-server drain (the longer 6s budget) owns the `:3100` port-free wait, via
68+
its own SIGTERM handler, so the next `npm run sandbox` no longer races a
69+
survivor on `:3100`.
70+
- **Single tree-kill primitive** — the POSIX group-kill, the Windows `taskkill`
71+
tree-kill, and the bounded SIGTERM→SIGKILL teardown now live in one shared
72+
`process-tree.ts` module used by every entrypoint (dev server, respawn
73+
handler, `exit` net, and sandbox), so the reaping behavior can no longer drift
74+
between hand-rolled copies. Its bounded teardown documents that its boolean
75+
reflects only the **direct child's** exit (not whole-group teardown or port
76+
release — callers needing a freed port must follow with `waitForPortFree`), and
77+
its post-SIGKILL grace is a named `KILL_GRACE_MS` constant kept deliberately
78+
shorter than the injectable SIGTERM grace (SIGKILL is uncatchable, so only a
79+
brief beat is needed to observe the exit).
80+
81+
`--strictPort` is intentionally retained: the proxy target is hardcoded to
82+
`:3100`, so the port is reliably freed rather than letting Vite drift to another
83+
port the proxy wouldn't follow.
84+
85+
- a40e840: fix: bind dev server to all interfaces (0.0.0.0) for WSL2 compatibility
86+
387
## 0.1.6
488

589
### Patch Changes

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aws-blocks/core",
3-
"version": "0.1.6",
3+
"version": "0.1.7",
44
"author": "Amazon Web Services",
55
"license": "Apache-2.0",
66
"type": "module",

0 commit comments

Comments
 (0)