Open5

Deno.exit() の修正 watcherも含めて

magurotunamagurotuna

https://github.com/denoland/deno/issues/7590

上記issueにて「Deno.exit() でDenoプロセスを終了させると、watcherも死んでしまう」という問題が報告されている。つまり、

$ deno run --unstable --watch foo.ts
foo.ts
Deno.exit(1);

とすると、watcherも死んでしまうためファイルの変更を監視できなくなる、ということ。
期待する挙動としては、Deno.exit() でプロセスを終了させたとしても、watcherは生き残って、ファイルの変更を監視し続ける、というもの。

これを修正するために行った調査結果などをまとめていきたい

magurotunamagurotuna

まず、伝家の宝刀†プリントデバッグ†を使いながら、状況を探る。

cli/file_watcher.rs にいろいろ プリントを仕込んでみると、

cli/file_watcher.rs
async fn error_handler(watch_future: FileWatcherFuture<Result<(), AnyError>>) {
  eprintln!("44444444444");
  let result = watch_future.await;
  dbg!(&result);
  if let Err(err) = result {
    let msg = format!("{}: {}", colors::red_bold("error"), err.to_string(),);
    eprintln!("{}", msg);
  }
}

ここが怪しいということがわかった。具体的には、44444444444 は出力されるものの、&result は出力されない。つまり watch_future.await でプロセスが強制終了している。
調査する前の予想としては、Deno.exit() をしたとしたら Rust 側では Err 型として取り扱われると思っていたのだが、どうやらそうではない模様。意外と大変な修正になるのかもしれないという予感がし始める。

magurotunamagurotuna

TS/JS で Deno.exit() を呼んだときにどういう処理が走るのか、をもとに追ってみることにする。

まず、Deno.exit() の処理内容は、JSサイドでは以下のように書かれている

cli/rt/30_os.js
  function exit(code = 0) {
    core.jsonOpSync("op_exit", { code });
    throw new Error("Code not reachable");
  }

op_exit という op をRust側に送っていることがわかるので、Rust側の処理を探すと↓が見つかる

cli/ops/os.rs
fn op_exit(
  _state: &mut OpState,
  args: Value,
  _zero_copy: &mut [ZeroCopyBuf],
) -> Result<Value, AnyError> {
  let args: Exit = serde_json::from_value(args)?;
  std::process::exit(args.code)
}

はは〜ん、std::process::exit(args.code) があるので問答無用でプロセスが終了していたというわけだ。

magurotunamagurotuna

Deno コアチームメンバーの Bartek に「この issue も関連するかも」と教えてもらったのが、以下である

https://github.com/denoland/deno/issues/3603

要約すると「Deno.exit したときとか SIGINT が来たときに window.onunload で設定したイベントハンドラが動かない!」ということ。なるほど、確かに Deno.exit() 周りの実装を変える必要があるという点で関連している気がする。

ちょっと横道に逸れるかもしれないが、SIGINT などのシグナルを Rust でうまいこと処理する方法は
https://rust-cli.github.io/book/in-depth/signals.html
が参考になるかも?

magurotunamagurotuna

さて、Deno.exit をしてもwatcherが死なないようにするためには、watcherを親プロセス、スクリプトをrunするのを子プロセスとして起動してあげないといけないっぽいことがわかってきた。(今は同一プロセスでやっている)

  • --watch フラグ付きで起動した場合には、--watch を取り除いたコマンドを使って 子プロセスを起動する
  • Rustの標準ライブラリにある std::process::Child などは同期的なAPIなので、Denoの実装と相性が悪い。tokioの process モジュール を使うのが良さそう
  • tokioのprocessモジュールを使って子プロセスを起動し終了をawait, 同時に file watcher(これは無限streamになっていて、監視対象ファイルに変更があると Ready が返ってくる)をawait する。この2つを tokio::select で待ち受ける。

みたいな感じになりそう