Skip to content

Spawn Primitives

tavlean edited this page May 28, 2026 · 3 revisions

Spawn Primitives

How startDevServer(cwd) actually starts a dev server. In src/servers.ts.

spawn("/bin/zsh", ["-ilc", 'exec "$0" "$@"', cmd, ...args], {
  cwd,
  detached: true,
  stdio: ["ignore", out, out],
});
child.unref();

/bin/zsh -ilc

Raycast's GUI-app subprocess inherits a minimal PATH that doesn't include user-installed tools (nvm, pnpm, bun via Homebrew, etc.).

  • -l reads ~/.zprofile
  • -i reads ~/.zshrc

Both flags are required: we don't know which file the user puts their PATH extensions in. Most put it in ~/.zshrc.

cwd as a spawn option

Not cd path && cmd. The path goes through the OS spawn syscall, not the shell. Removes one injection surface for paths with quotes/spaces/newlines.

Command and args via $0/$@, not string interpolation

The command string is the fixed literal exec "$0" "$@". The package manager, run, and the script name are passed as positional args after it (cmd, ...args), so zsh binds $0 to cmd and $@ to the rest, then re-quotes them on exec.

We do not build `exec ${cmd} ${args.join(" ")}` (the earlier form). A package.json script key containing a space ("dev server") or a shell metacharacter ("dev; rm -rf ~") would otherwise either break the command or inject. With the argv form the key is run verbatim; it's just an argument, never parsed as shell.

Note this isn't a privilege boundary: npm run <script> executes whatever the repo's script says anyway. It's defense-in-depth plus correctness for unusual-but-legal script names.

detached: true + unref()

The spawned dev server needs to outlive the extension command. Without these, the FDs would close when Raycast tears down the command, killing the server.

SIGKILL, not SIGTERM

killServer(pid) uses SIGKILL. A graceful-shutdown window races the new spawn for the same port. SIGKILL releases the listener immediately. Then we poll process.kill(pid, 0) until ESRCH (max 500ms) to confirm exit before spawning the replacement.

Kill-then-spawn order matters

In a restart, the old server's cwd is what we'll watch for the new server at. If we don't kill-then-confirm-exit-then-spawn, the watch effect can see the old server momentarily and declare success before the new one has even started. Order:

  1. SIGKILL old PIDs (parallel for multi-target)
  2. Wait for each process.kill(pid, 0) to throw ESRCH
  3. spawn new
  4. Watch for new servers to appear in fetchServers

Spawn log path

dev-servers-spawn-<cwdSlug>.log in os.tmpdir(). Keyed by cwd slug so the file stays meaningful after the PID has been replaced. spawnLogPath(cwd) returns this path for error toasts.

Clone this wiki locally