adonisjs / adonisjs/assembler

Ctrl+C does not stop the dev server: raw mode suppresses SIGINT and close() never exits the process

Open
#98 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
41
Forks
24
Avg merge
13d 22h
Merged PRs (30d)
2

Description

Summary

With node ace serve --watch, pressing Ctrl+C does not stop the dev server. The terminal stays wedged and the only way out is closing the terminal tab, which leaves orphaned node ace serve --watch processes re-parented to init (ppid=1).

Crucially, pressing Ctrl+C repeatedly (quickly or slowly) has no effect either, because in this state no signal is ever generated — so there is no default-terminate fallback to rely on.

Steps to reproduce

  1. node ace serve --watch
  2. Wait for watching file system for changes...
  3. Press Ctrl+C

Expected: the dev server shuts down and the shell prompt returns.

Actual: ^C is echoed, nothing else happens. The process keeps running. Closing the terminal tab leaves the server running in the background.

Analysis

ShortcutsManager.setup() puts stdin into raw mode so it can implement the r/c/o/h shortcuts:

setup() {
  if (!process.stdin.isTTY) return;
  process.stdin.setRawMode(true);
  this.#keyPressHandler = (data) => this.#handleKeyPress(data.toString());
  process.stdin.on("data", this.#keyPressHandler);
}

Raw mode clears ISIG, so the terminal no longer translates Ctrl+C into SIGINT. The keypress is instead delivered as a raw byte and handled explicitly:

#handleKeyPress(key) {
  if (key === "\x03" || key === "\x04") return this.#callbacks.onQuit();
  ...
}

onQuit is () => this.close(), and DevServer.close() is:

async close() {
  this.#cleanupKeyboardShortcuts();
  await this.#watcher?.close();
  if (this.#httpServer) {
    this.#httpServer.removeAllListeners();
    this.#httpServer.kill("SIGKILL");
  }
}

close() never calls process.exit() — it relies on the event loop draining naturally. If anything still holds a referenced handle in the parent process (or close() itself does not settle), the process simply never exits. And because raw mode has suppressed SIGINT, the usual safety net — the OS killing the process on a second Ctrl+C — is gone.

Note removeAllListeners() is called on the child immediately before kill("SIGKILL"), which also removes the exit/close listeners that would normally let the parent observe and reap it.

Not fixed by upgrading

I diffed the published builds: ShortcutsManager.setup, #handleKeyPress and DevServer.close() are byte-identical in 8.0.0 (what we run) and 8.4.0 (current latest), so this is not a regression that a version bump resolves.

Workaround

Making stdin a non-TTY causes setup() to return before enabling raw mode, which restores normal terminal signal handling:

// package.json
"dev": "node ace serve --watch < /dev/null"

Ctrl+C then works as expected. The cost is losing the keyboard shortcuts, so we keep the original as a second script for anyone who wants them.

Worth noting for others hitting this: if your app registers its own SIGINT handler (job runners commonly do, to drain in-flight work), you may need to disable that in development too — otherwise the restored signal is trapped by the child instead.

Suggested fix

Guarantee the process actually exits on the quit shortcut, e.g. await this.close() then process.exit(0) in the onQuit path; or restore raw mode and re-raise SIGINT (process.kill(process.pid, 'SIGINT')) so normal signal semantics apply. Either would also make a second Ctrl+C effective as a fallback.

Environment

@adonisjs/assembler 8.0.0 (behaviour confirmed unchanged in 8.4.0)
@adonisjs/core 7.3.1
Node v24.14.0
npm 11.9.0
OS macOS 26.3 (arm64)

Happy to test a patch or open a PR if the suggested direction looks right.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at ShortcutsManager.setup(), #handleKeyPress(), and DevServer.close(), then reproduce the issue with node ace serve --watch. Trace the quit shortcut and watcher/server cleanup; done means Ctrl+C returns the shell prompt without leaving an orphaned dev-server process.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
cli, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.