◆ Nodist の node.exe は指定バージョンの node.exe を起動するだけの実行ファイル
◆ 実行中 node.exe は 2 つ存在する
◆ sub.kill では親側だけで実際の Node.js プロセスの子側は kill できない
◆ taskkill コマンドで /T (treekill) すればできたけど

kill しても止まらない

ある PC で作って動いていた Node.js のプログラムを別の PC で動かすと動きませんでした
child_process を使って 別のスクリプトを実行したり停止したりして実行状態を管理するものです
おかしいところは kill を行っているのに 停止せず出力が続きます
しかし kill 自体は成功してるようです

よくわからずタスクマネージャを見ていると Nodist が原因であることがわかりました

Nodist で実行すると

Nodist を使っていると node コマンドで実行されるものは Nodist のバージョンセレクタのような exe です
node という名前ですが この実行ファイル自体は Node.js ではありません
これを実行すると別プロセスとして 現在選択中のバージョンの node.exe を実行します
Nodist 経由で Node.js を実行している間は 2 つの node.exe プロセスが存在します

タスクマネージャでみても 2 つ node.exe があります
Nodist の方はアイコンが未指定のものになっていて Node.js のアイコンではないのでわかりやすいです
「コマンドライン」列でも 自分で実行したものは

node script.js

のようになっていて もうひとつの内部で実行される方は

"C:\Program Files (x86)\Nodist/v-x64/12.13.0/node.exe" script.js

のように Node.js 実行ファイルの絶対パス表記になっています
あとなぜか Nodist 以下のパス区切り文字が / です
こんな混在してても動くんですね……

実行ファイルの場所はこういう感じです

C:\Program Files (x86)\Nodist\bin\node.exe
C:\Program Files (x86)\Nodist\v-x64\12.13.0\node.exe

1 つめが Nodist のもので これが 2 つめのような実際の Node.js の node.exe を実行します
2 つ目はバージョンによってフォルダ名は変わります

原因

Node.js の child_process.spawn で得られるインスタンスの kill メソッドでは Nodist の方の exe しか停止できていません
なので 実際の Node.js プロセスの方は動き続けています
タスクマネージャを見ても 「node script.js」 の方だけが消えていてもう一つは残ったままです

対処

node.exe を起動するメインのプロセスからすれば 子プロセスと一緒に孫プロセスも止めないといけないです
Nodist の node.exe を kill するときに それが起動した exe も合わせて tree kill できればいいのですが kill メソッドはシグナルを指定できるだけで tree kill オプションはありませんでした
tree kill のパッケージが npm にあるくらいなので標準機能だけじゃ簡単にできなさそうです

これだけのために npm パッケージを入れるのは避けたかったので何か方法がないか色々試してみたのですがダメそうでした
メインのプロセスを Ctrl-C で止めると孫も全部止まるのでシグナルによっては伝わるのかなと思って シグナルを SIGINT や SIGTERM や SIGKILL などいくつか試しても孫まで伝わりません
そもそも Windows で kill シグナル指定して意味あるのでしょうか
停止は Ctrl-D (Ctrl-Z) もあったっけと stdin を閉じればどうだろうと stdio に "pipe" を指定して child.stdin.end() を実行してみてもダメでした

Node.js 上で kill を使っては無理そうなので taskkill コマンドを使います

sub.kill()



child_process.exec(`taskkill /T /F /PID ${sub.pid}`, () => {})

に変更します
/T が tree kill なので孫プロセスまで止めれます
/F は強制終了オプションで これがないと Node.js の方を止められませんでした

[sub.js]
setInterval(() => console.log(new Date), 1000)

[main.js]
const cp = require("child_process")

const sub = cp.spawn("node", ["sub.js"], { stdio: "inherit" })

setTimeout(() => {
sub.on("close", () => console.log("KILLED"))
cp.exec(`taskkill /T /F /PID ${sub.pid}`, console.log)
}, 3000)

とりあえず結果を console.log してますが文字コードの問題でまず読めません
バイナリで受け取って iconv-lite などで SJIS → UTF-8 の変換が必要です

Nodist 使わなければ

とりあえず動くようにはできました
しかし taskkill は Windows 前提なので Linux に移して実行するときに問題がでます
そもそも Nodist のためだけに特別対応するのが気が進みません

Nodist は必須というわけでもなく バージョン変えたいときに便利かなー くらいで今のところは気になる機能が追加された新しいバージョンがでたら Node.js の新しいバージョンをインストールして切り替えるくらいです
Nodist 入れてない PC では公式インストーラでアップデートしてるのもありますがそれで全然困ってないです
なので不便な Nodist を使わないようにしました

Nodist のままでも Nodist より優先度の高いパスが通った適当なフォルダに node.exe という名前で目的のバージョンの node.exe へのシンボリックリンクを作ればそれでも十分そうです
その場合 Nodist は Node.js の指定バージョンを取得するためだけに使います

追記:今更ながら

読み返していて もっと簡単な解決策あるなと思ったので追記です

child_process の起動時に 「node」 コマンドを使うからパスの通っている Nodist の node.exe が使われます
今と同じ実行ファイルで Node.js プロセスを起動するなら Nodist を経由する必要はありません
child_process.fork を使えば Node.js の exe を指定する必要はなく 自動で今のプロセスと同じ node.exe が使われます
spawn を使う場合でも process.execPath に Node.js の exe のフルパスが入っているのでこれを 1 つめの引数に渡せば良いです

ツリー形式で表すと こういう構造にしているのが問題で

- node.exe (Nodist)
- node.exe (Node.js 12)
- node.exe (Nodist)
- node.exe (Node.js 12)

Nodist を使っていても

- node.exe (Nodist)
- node.exe (Node.js 12)
- node.exe (Node.js 12)

こうなっていれば問題ないはずです