Process Lifecycle

What actually happens on startup, shutdown, a port conflict, and a dev-mode file change.

Startup: listen() blocks the process

app.listen() blocks the calling thread by default (options.block = true). Node keeps a CLI process alive via its event loop; BoxLang's CLI runtime has no equivalent, so listen() blocks itself rather than requiring every caller to remember a keep-alive loop:

bxsapp.listen( 3000, ( port ) => println( "listening on #port#" ) )
// process stays alive here until app.close() is called or the process is signaled

Pass { block: false } for non-blocking startup — useful for a test suite, or embedding the server inside a larger app that manages its own lifecycle. app.close() stops the server either way.

Graceful shutdown (Ctrl-C / SIGTERM)

The server registers a JVM shutdown hook when it starts, so Ctrl-C or SIGTERM always triggers a clean close() — the listening socket is released and the dev-mode file watcher (if enabled) is stopped, whichever way the process ends. close() itself is safe to call more than once.

Port already in use

If the requested port is already bound, the server exits with a short, readable message instead of a raw Java stack trace:

plain — stdout[BoxExpress] Port 3000 is already in use — exiting.

The process exits with status code 1. This is detected by inspecting the actual java.net.BindException cause (not by string-matching the error message), so it's specific to a real port conflict — any other startup failure still surfaces normally.

Dev-mode auto-reload

bxsapp.set( "reloadOnChange", true )

This watches the current working directory (recursively, skipping dotfiles and directories like node_modules/boxlang_modules/target/build/dist) for .bx/.bxs/.bxm changes, debouncing bursty save events (most editors fire two or three filesystem events per save) into one restart:

  1. A file change is detected and debounced (150ms).
  2. [reloadOnChange] <path> changed — restarting... is logged.
  3. The replacement process is launched first, replaying the exact original JVM invocation via ProcessHandle.current() — this works whatever the entry script is named and however it was launched (bvm-managed boxlang, a raw java -jar, custom JVM flags), rather than assuming a fixed boxlang <script> shape.
  4. Only once that launch succeeds is the current server closed (releasing the port) and the old process exits — the new one binds the same port with the updated code.

Registration is a one-time recursive snapshot taken at startup — a directory created after the server starts won't be picked up until the next restart. That's an accepted limitation for a dev-only convenience feature.

Watching .bxm alongside .bx/.bxs is broader than a restart actually requires. A restart is only necessary for app.bxs itself (or any other .bx class file it loads) — that code runs once at process start, so editing app.get(...) registrations or anything else at the top level does nothing until the script re-executes. A .bxm view is different: res.render() runs it via include, which re-reads the file from disk on every request as long as trustedCache is off (the default) — confirmed directly, editing a view and requesting it again immediately picked up the change with no restart at all, even with reloadOnChange turned off entirely. Restarting on a view-only edit isn't wrong, just redundant work the file already didn't need.