Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Processes

A spawnite command that starts a game’s Vite, its room, its bots or a browser ends every one of them when its work ends. That holds when the command finishes, when it fails, and when whatever ran it kills it, as a harness does to a command past its time. It holds on Windows, macOS and Linux alike, with one exception: on macOS and Linux, a kill -9 of the command may leave the browser it launched, below.

This page says when each process the cli starts ends, and what a script you write yourself has to do to end its own.

A command that runs to an end, such as spawnite play profile, spawnite play room or the playtest, starts its Vite, room and bots as its own children and ends them with itself:

  • On Windows, Node puts each child in a job object that ends it, with every process it started, when the command’s process ends in any way.
  • On macOS and Linux, each child leads a process group of its own, and a small guard process ends each group once the command’s process ends in any way, a kill -9 included.

On Windows, the browser such a command launches ends with it the same way. On macOS and Linux, Playwright closes it when the command takes a signal it can handle, such as Ctrl+C or a plain kill.

A spawnite play start session is meant to outlive the command that starts it, so its browser, Vite, room and bots run detached. A second spawnite play start of the game refuses while the session runs. Three things end them:

What When
spawnite play stop At once.
spawnite play start --replace At once, before the new session starts.
The session’s keeper Once the session goes --idle minutes, 60 by default, with no command on it.

Every command on the session, such as step, screenshot or dump, counts as a use, and so does the dashboard while it shows the session. --idle 0 keeps the session until spawnite play stop.

A Playwright script, or any script that starts a browser or a server, ends them itself. Close the browser in a finally block, and bound the script’s wait with a timeout of its own rather than an outer timeout command:

import { chromium } from "playwright";
const browser = await chromium.launch({ headless: true, channel: "chromium" });
try {
const page = await browser.newPage();
await page.goto("http://localhost:9240/");
// The script's own bound: it throws, and the finally still runs.
await page.waitForFunction(() => window.__GAME_DEVTOOLS__ !== undefined, {
timeout: 60_000,
});
} finally {
await browser.close();
}

An outer timeout kills the script before its finally runs. On Windows the browser still ends with the Node process that launched it, but a server the script started detached keeps running, and on macOS and Linux any server it started does.

A server you start in the background from a shell, such as npx vite & or a room, is yours to end: stop it before the session ends, or run it through the command that ends it for you, spawnite play start.

A vitest test that runs a command, such as the cli or git, runs it through @spawnite/testing/process, with @spawnite/testing as a dev dependency:

import { runProcess, spawnProcess } from "@spawnite/testing/process";
// Resolves once the command ends, and rejects on a failed exit with its stderr.
const { stdout } = await runProcess("git", ["rev-parse", "HEAD"], {
cwd: game,
});
// Resolves on a failed exit too, for a test that checks the refusal.
const { status, stderr } = await runProcess(process.execPath, [script], {
input: "{}",
reject: false,
});
// Node's spawn, for a process the test keeps running beside it.
const server = spawnProcess(process.execPath, ["server.js"], {
stdio: "ignore",
});

Each child opens no console window on Windows. A child still running when its test ends is ended then, so a test that fails or times out leaves nothing behind. A child started outside a test, or in a concurrent test, is the test’s to end, because vitest cannot tell which of several concurrent tests is running; runProcess takes a timeout in milliseconds for those. createDeadPid() returns the id of a process that has ended, for a test of code that reads a recorded process that is gone.