|
1 | 1 | # @aws-blocks/core |
2 | 2 |
|
| 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 | + |
3 | 87 | ## 0.1.6 |
4 | 88 |
|
5 | 89 | ### Patch Changes |
|
0 commit comments