Skip to content

Run bun run dev/demo with Bun alone, without fish - #85

Closed
firsttris wants to merge 6 commits into
masterfrom
dev-without-fish
Closed

firsttris wants to merge 6 commits into
masterfrom
dev-without-fish

Conversation

@firsttris

Copy link
Copy Markdown
Owner

bun run dev and bun run demo ran scripts/start.sh, a fish script, so anyone without fish could not start the app. This replaces it with scripts/dev.ts (bun scripts/dev.ts [--demo]), so Bun is the only requirement.

Behaviour kept from start.sh

  • --demo: runs bash scripts/dev/setup.sh first and aborts with exit code 1 if it fails; sets SNAPRAID_BASE_PATH=scripts/dev/sandbox, SNAPRAID_BIN=scripts/dev/bin/snapraid, SNAPRAID_EXTRA_ARGS=--test-skip-device, SNAPRAID_DEMO=1.
  • Without it: SNAPRAID_BASE_PATH=<root>/snapraid, created with mkdir -p.
  • Every path comes from the repository's root, whatever the working directory.
  • Runs bun run dev in apps/server and apps/web. When one exits, the other is stopped. On SIGINT/SIGTERM it exits with 128 + n (130 for Ctrl+C).

Changed or added

  • Each server runs in its own process group (the pattern in ccu-addon-mui's scripts/dev.ts). SIGINT, SIGTERM and SIGHUP go to both whole groups, so Ctrl+C, closing the terminal and an IDE's stop or kill all stop the grandchildren too (bun --watch, Vite/node). A group still running 5 s after the signal gets SIGKILL. The script waits until both groups are empty before it exits.
  • When one server exits, the script now exits with that server's exit code. start.sh exited with its loop's status instead.
  • The children get no stdin (ignore), so Vite's keyboard shortcuts are off.
  • The "Bun not found" check is gone, since the script runs on Bun. The children run on the same Bun (process.execPath).

Tests: scripts/dev.test.ts (bun:test, 8 tests). They run a copy of the script in a temporary copy of the repo layout, with stand-ins for both apps' bun run dev and for setup.sh. Each stand-in records the pids of bun run, of its shell and of a grandchild. The tests cover:

  • SIGTERM, SIGINT and SIGHUP stop all of these processes, with exit codes 143, 130 and 129.
  • A grandchild that ignores SIGTERM gets SIGKILL.
  • When the server exits with 7, the web tree is stopped and the script exits with 7.
  • The base path and mkdir without --demo.
  • The four demo variables reach both apps.
  • When setup fails, the script exits with 1 and starts nothing.

I checked the tests against a mutant that signals only the direct child: 4 of them fail. CI runs them as a new step bun test ./scripts (from the root) in the backend job. The server's bun test keeps its count (234 pass / 2 skip). The server's tsconfig.json now includes ../../scripts/*.ts, so bun run typecheck checks the script.

Docs: README and docs/development.md no longer list fish. The development docs describe how the script stops the servers and the new tests. The comments that mentioned start.sh now point to dev.ts.

Checked locally

  • bun run dev: :8080/api/health returned ok and :3000 returned 200. SIGINT to the foreground process group (the same as Ctrl+C) stopped all processes in all three sessions within about 2 s, and both ports were closed afterwards.
  • bun run demo: setup found the existing sandbox, the children got the four SNAPRAID_* variables, and both servers were up. SIGHUP sent to dev.ts alone left nothing running, and the exit code was 129.
  • bun run dev, then SIGTERM sent to dev.ts alone: nothing left running, and the exit code was 143.
  • Other checks: bun install --frozen-lockfile. In the server, bun test (234 pass / 2 skip) and typecheck passed. In the web app, biome, typecheck, 60 tests and build passed.

Generated by Claude Code

scripts/start.sh was a fish script, so `bun run dev` and `bun run demo` needed fish
installed. scripts/dev.ts does the same with Bun.spawn: the demo runs scripts/dev/setup.sh
first and aborts if it fails, sets the sandbox's SNAPRAID_* variables (or creates ./snapraid
without --demo), all paths from the repository's root, then runs `bun run dev` in apps/server
and apps/web.

Each runs in a process group of its own (as scripts/dev.ts in firsttris/ccu-addon-mui), and
SIGINT, SIGTERM and SIGHUP go on to both groups, so a Ctrl+C, an IDE's stop or a closed
terminal also stops what bun --watch and Vite started instead of leaving them on :8080 and
:3000; what ignores SIGTERM gets SIGKILL after 5 s. When one exits, the other is stopped and
its exit code is passed on; on a signal the script exits with 128 + n (130 for Ctrl+C), as before.

scripts/dev.test.ts runs it against stand-ins for both apps and setup.sh; CI runs it in the
backend job (`bun test ./scripts` in the root), and the backend's typecheck covers scripts/*.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z
The server stand-in exited as soon as the web app's grandchild had written
its pid, so dev.ts could stop the web app before the test had seen its
processes alive, and running(['web']) failed now and then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z
After the grace time dev.ts sends SIGKILL and exits right away, but the
kernel takes a while to tear a process down, the longer the more memory it
holds. The stand-in holds 1.5 GiB and ignores SIGTERM; the existing
SIGKILL test fails now and then for the same reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z
After SIGKILL dev.ts went on at once, so bun run dev could return while
Vite or the backend were still being torn down (and still held their
ports). It now polls the group for up to a second more; longer only an
orphan's zombie can keep the group alive until init reaps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z
Bun's detached children run in sessions of their own, out of the
terminal's reach, so its SIGTSTP stops only dev.ts; with scripts/start.sh
on master Ctrl+Z stopped the servers too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z
In sessions of their own the servers are out of the terminal's reach, so
SIGTSTP stopped only dev.ts. dev.ts now stops their groups with SIGSTOP
(the kernel drops SIGTSTP sent to an orphaned group, which a session of
its own is) before stopping itself, and passes SIGCONT on, as haul's
scripts/dev.ts does. Checked in a pty as well: fish scripts/start.sh and
the fixed dev.ts both leave all processes in state T after Ctrl+Z.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z

Copy link
Copy Markdown
Owner Author

TDD-Review

Nur Befunde, die ein fehlschlagender Test belegt. Fixes sind als eigene Commits auf dev-without-fish (df73db0, 898b9d6 → 96b15ea, 0a7ae20 → 69481bd).

1. Nach SIGKILL beendet sich dev.ts, bevor die Prozesse weg sind

scripts/dev.ts, reap (Z. 64–69 im PR-Stand): nach signalGroup(child, 'SIGKILL') folgt nur await child.exited (der Gruppenleiter ist da längst beendet), das Script endet sofort. SIGKILL ist nicht augenblicklich: der Kernel muss den Prozess noch abbauen, je mehr Speicher, desto länger (gemessen 40–140 ms bei 0,5–1,5 GiB). bun run dev kann also zurückkehren, während Vite/Backend noch laufen und ihre Ports halten. Der PR-eigene Test a process that ignores SIGTERM gets SIGKILL after the grace time schlägt deshalb hier etwa in jedem dritten Lauf der Suite fehl.

Test: after SIGKILL it waits until the group is gone before it exits (Stand-in hält 1,5 GiB und ignoriert SIGTERM):

181 |   dev.kill('SIGTERM')
182 |   expect(await dev.exited).toBe(143)
184 |   expect(started.filter(alive)).toEqual([])
error: expect(received).toEqual(expected)
- []
+ [
+   21550,
+ ]
(fail) after SIGKILL it waits until the group is gone before it exits [6313.54ms]

Fix 96b15ea: nach SIGKILL bis zu 1 s weiter pollen, bis die Gruppe weg ist (länger kann sie nur ein Zombie eines Waisen halten, bis init ihn abholt). 5/5 Läufe der Suite grün.

2. Ctrl+Z hält nur dev.ts an, beide Server laufen weiter (Regression gegenüber start.sh)

scripts/dev.ts, spawn mit detached: true (Z. 94): Bun legt die Kinder in eigene Sessions, außerhalb der Reichweite des Terminals. SIGTSTP von Ctrl+Z erreicht nur dev.ts; mit fish scripts/start.sh auf master wurden alle Prozesse angehalten.

Test: Ctrl+Z (SIGTSTP) stops both servers with dev.ts, fg (SIGCONT) resumes them:

error: expect(received).toEqual(expected)
  [
-   "T", "T", "T", "T", "T", "T",
+   "S", "S", "S", "S", "S", "S",
  ]
(fail) Ctrl+Z (SIGTSTP) stops both servers with dev.ts, fg (SIGCONT) resumes them [408.43ms]

Gegenprobe in einem echten pty (interaktive bash, Stand-ins für beide Apps), Zustände der Prozesse nach ^Z:

fish scripts/start.sh | after ^Z: ['T', 'T', 'T', 'T']
bun scripts/dev.ts    | after ^Z: ['S', 'S', 'S', 'S']   (PR-Stand)
bun scripts/dev.ts    | after ^Z: ['T', 'T', 'T', 'T']   (nach 69481bd; fg → S, ^C → alle weg)

Fix 69481bd (wie in haul's scripts/dev.ts): auf SIGTSTP die Gruppen mit SIGSTOP anhalten (SIGTSTP an eine verwaiste Gruppe verwirft der Kernel, eine eigene Session ist eine), dann sich selbst mit SIGSTOP; SIGCONT wird an die Gruppen weitergegeben. Doku in docs/development.md ergänzt.

3. Testfehler: when one server exits… ist flaky

Der Server-Stand-in endete, sobald der Enkel der Web-App seine pid geschrieben hatte; dev.ts konnte die Web-App stoppen, bevor running(['web']) sie lebend gesehen hatte:

130 |   expect(pids.filter(alive)).toEqual(pids)
- [ 14366, 14370, 14372 ]
+ []
      at running (scripts/dev.test.ts:130:30)
(fail) when one server exits, the other is stopped and its exit code is passed on [118.67ms]

Fix df73db0 (nur Test): der Stand-in endet erst, wenn der Test server.exit schreibt.

4. Nicht gefixt, Entscheidung beim Autor: Vite bekommt kein Terminal mehr als stdin

stdio: ['ignore', …] (Z. 93): Vite bindet seine Tastenkürzel (r, o, c, q, h) nur bei process.stdin.isTTY (vite 8.3.5, bindCLIShortcuts). Mit start.sh hatten die Hintergrundjobs das Terminal als stdin. Script-Test im pty, Web-Stand-in schreibt [ -t 0 ]:

fish scripts/start.sh: tty
bun scripts/dev.ts:    no-tty

Nicht gefixt: das Terminal zu erben ginge in einer eigenen Session nur so, dass Vite der Shell nach Ctrl+Z die Eingaben wegliest (so in haul#47 belegt); dafür müsste die Web-App ohne eigene Session laufen und anders gestoppt werden. Wenn die Kürzel gebraucht werden, ist das eine Designfrage.

Nicht belegt (Tests grün, verworfen)

  • Signal während --demo-Setup: setup.sh und seine Kinder werden gestoppt, Server starten nicht, Exitcode 130/143 (auch mit Kind, das SIGTERM ignoriert).
  • Zwei Signale nacheinander (20 ms Abstand): Exitcode des ersten, alles weg. (Gleichzeitig zugestellte SIGINT+SIGTERM liefern 143 – Reihenfolge in Bun, kein Fehler des Scripts.)
  • Signal, nachdem ein Server schon beendet ist: Exitcode des Servers bleibt.
  • Signal direkt nach dem Start, bevor die Apps laufen: nichts bleibt zurück.
  • process.kill(-pid) auf leere Gruppe: ESRCH wird gefangen.
  • Kind durch Signal beendet: exited liefert 128+n (137/143), wird weitergegeben.
  • pid-Wiederverwendung beim SIGKILL-Fallback: nicht nachstellbar, verworfen.
  • Aufruf aus anderem Arbeitsverzeichnis, Umgebung und Basis-Pfad mit/ohne --demo: durch die vorhandenen Tests abgedeckt, grün.
  • tsconfig-Include ../../scripts/*.ts: bun run typecheck im Server grün; das Dockerfile führt keinen Typecheck aus und kopiert nur apps/server/tsconfig.json, Bun ignoriert das include zur Laufzeit.

Checks (nach den Fixes)

bun test ./scripts 10 pass (5 Läufe), apps/server: bun run typecheck grün, bun test 234 pass; apps/web: bunx biome check grün, bun run build grün. Branch ist auf aktuellem master.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Geschlossen: Statt eigener Signal- und Prozessgruppen-Logik (detached, SIGTSTP/SIGCONT von Hand) nutzen wir Buns eingebautes bun run --parallel. Im echten Terminal geprüft: Strg+C, Ctrl+Z, fg funktionieren damit von selbst, ein abstürzendes Script stoppt die anderen. Ersatz kommt als eigener PR.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants