child_process.spawn
はコマンド実行が比較的長い時間掛かる事もあると予想される場合に子プロセスを起動して並行実行が可能です。しかしながら後どれくらい時間が掛かるかをユーザーに伝えたい等の理由で途中で状況を把握したいことがあるかもしれません。今回、サンプルとしてコマンドは標準出力に進捗率が
progress: 1.51%
progress: 2.34%
...
と各行に表示されて状況が把握可能になっているケースを想定してみます。onProgressハンドラを渡して受け取った側でユーザーに進捗率をGUIで表示するイメージで以下を実装してみました(※バグあり):
const child_process = require("child_process")
function exec(path, onProgress) {
return new Promise((resolve) => {
let child_process = child_process.spawn(path)
child_process.stdout.on("data", (data) => {
let progress = data.toString().match(/^progress: (\d+\.?\d+)%/)
onProgress(Number(progress[1]))
})
child_process.on("exit", (code) => {
resolve({result: (code === 0 ? "success" : "failed")})
})
})
}
一見、子プロセスを実行して標準出力に書き込まれたら進捗率をonProgressで通知していて問題無さそうに思います。が、進捗率が100%まで表示されないことがあり、原因が分からず対策を一晩寝かしました...
exitイベントはstdioが閉じるのを待たない
dataイベントでprogressが終了間際にロストしているので終了のタイミングが早いとかそんなんだろうなと予想していましたが、ビンゴでした。Child Process | Node.js v9.3.0 Documentationをみると、ん?close
というイベントが・・・ということで仕様をみてみると、
と書いてあるようです。どうも、'exit'イベントは子プロセスがSIGTEMを受けたり子プロセス内でexit(x)を呼ばれて終了すれば即呼ばれるようです。そのために、resolve呼び出しが標準出力を受け取りきる前に呼ばれてPromiseでの処理が完了してしまい、後はどう挙動するか保証できない状態になったということのようです。
紛らわしい...。確かにプロセスは終わってるのかもしれないけどstdioデータを渡し切れてないタイミングをexitと呼んでいいのかはちょっと疑問に思いました。むしろ、'exit'→'will-exit'、'close'→'exit'ってイメージです。exitイベントはv0.1.90、closeイベントはv0.7.7で仕様定義されてるのでもしかしたら仕様後付けで悩んだのかもしれないですね。
というわけで、子プロセスの標準出力は全て受け取りたいので終了はcloseイベントを使いましょう:
const child_process = require("child_process")
function exec(path, onProgress) {
return new Promise((resolve) => {
let child_process = child_process.spawn(path)
child_process.stdout.on("data", (data) => {
let progress = data.toString().match(/^progress: (\d+\.?\d+)%/)
onProgress(Number(progress[1]))
})
child_process.on("close", (code) => {
resolve({result: (code === 0 ? "success" : "failed")})
})
})
}
めでたしめでたし。