deno: Deno.exit terminates entire process when using run --watch

I’m running my script with deno run --watch --unstable myscript.ts. When it encounters a call to Deno.exit, the script terminates, including the file watcher.

I expected Deno.exit to terminate the script, but keep the file watcher alive. This is how Node’s process.exit behaves when used with nodemon.

It seems culprit is the current implementation, which is just a passthrough to std::process::exit. I’m still familiarizing myself with Deno and V8, but is the solution something like tearing down the active runtime without exiting the main process?

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 3
  • Comments: 21 (18 by maintainers)

Most upvoted comments

If you exit your programme, you exit your program. I personally would find it surprising that if my programme has exited, that it suddenly restarts.

Like a “watch” in a browser, if the code changes and the page is still open, it reloads. If you close the page, you close the page. Anything else would be surprising in my opinion.

I agree that this is pretty unexpected - when the program exits by natural means (no more tasks to do), it also restarts when the file changes. I think that should also apply to Deno.exit(). The program should not be concerned with if it was started in --watch mode or not.

I just found this thread while dealing with a different issue. For me, the current behaviour of Deno.exit() is actually desired/useful.

TL;DR

Calling Deno.addSignalListener("SIGINT", ...) makes it so CTRL-C no longer terminates a deno run --watch process. Deno.exit() is a workaround to stop the watcher. If you change the --watch/Deno.exit() interaction, please provide an alternative way to terminate a --watch process with signal listeners.

Long version

Here is a real example, where I encountered this issue.

// server.ts
const kv = await Deno.openKv("./db.sqlite");

const server = Deno.serve(async () => {
  await kv.atomic().sum(["visitor_count"], 1n).commit();
  const { value } = await kv.get(["visitor_count"]);
  return new Response(`Hello world! You are the ${value}th visitor.`, { status: 200 });
});

I built an HTTP server with Deno.serve() and Deno KV. I run this server locally with deno run --unstable -A ./server.ts. When I’m done, I stop the server with CTRL-C. But stopping the server in this way leaves behind some KV temp files: db.sqlite-shm and db.sqlite-wal. So I added the following code for a clean shutdown when I press CTRL-C:

// server.ts continuation
Deno.addSignalListener("SIGINT", async () => {
  kv.close();
  await server.shutdown();
});

This performs a clean KV shutdown when I press CTRL-C, and leaves no KV temp files behind on my hard drive.

However, once I run this with --watch, the process can’t be stopped with CTRL-C at all anymore! Pressing CTRL-C will close the KV and stop the HTTP server, but then Deno prints this message to stdout: Watcher Process finished. Restarting on file change... and I need to kill the deno process. (I’m using deno 1.38.5 by the way.) My workaround for now is to add a Deno.exit() call inside the signal listener.

Is #14787 still the PR that will effectively resolve this issue? If it’s reopened and merged of course 😃

Possibly, but I haven’t found a reliable way to handle Deno.exit() to terminate execution and do cleanup before calling a relevant syscall. I might revisit it in the future

It seems like in this case deno should create a child process with the exact same arguments sans --watch. The main process should then monitor the child process. If a file changes before the child process exits, then the parent should kill the child process. Once the child process is exited, deno should re-create the child process or wait for a file to change.

If the parent process receives any signals, it should forward them to the child process and once the child exits the main process should exit.

@jespertheend I’m going to work on https://github.com/denoland/deno/pull/12938 this week, which will require similar effort to this issue. I’ll add pointers on how to tackle this issue after finishing my PR.

@pschiffmann That makes sense.

It seems almost like it would be beneficial for the signal listener to continue on with its normal behavior after the callback function is finished executing.

Your function is async, if the returned promise was awaited inside the Deno runtime and then continued on with handling SIGINT as normal then that would allow the user to override the behavior still by just not returning and also in your case what you want is for the normal Ctrl+C behavior to happen after you’re done shutting down but without having to call exit.

Because it seems like the cruxt of the problem in this case is that when Ctrl+C signal is sent to the Deno process the intent is for everything to shutdown but when a Deno.exit() is called from within the watched application I would kind of expect it to kill the app child process but not the top level deno process which should continue watching for files.

Is there a way to detect that deno is running in watch mode for now? It would be a helpful workaround for now, I could just not call deno.exit in that case for now.

Unfortunately there is no way to tell that. I’ll try to make time to fix this issue this week.

Is there a way to detect that deno is running in watch mode? It would be a helpful workaround, I could just not call deno.exit in that case.

@jespertheend sorry for late response. I did some experiments in https://github.com/denoland/deno/pull/12938 and although I have some idea how we could tackle this issue, it’s definitely on the “very hard” spectrum of the problem; so I’m not sure I can provide pointers on how to implement that. I will be tackling https://github.com/denoland/deno/issues/13093 in this quarter, after that issues is addressed fixing this issue will be much more approachable.