概要
Ruby言語から、コマンドラインツールを呼び出すときには、system
や open3
を使うが、非同期的に呼び出したい場合はspawn
を使うことが多い。しかし、引数のコマンドに空白などの文字が入ると、シェル経由の実行になってしまい、プロセスIDを記録してプログラムを終了しようとしても、思ったように終了できないことがある。これを防ぐためには、コマンドに空白などの文字を入れず、引数を分割する。
pid = spawn("command which you want to execute")
↓
pid = spawn("command", "which", "you", "want", "to", "execute")
もう少し詳しく
Rubyでコマンドを非同期的に実行するためには、spawnを利用して、
pid = spawn("command which you want to execute")
detach
を呼び出せばいい。
thr = Process.detach(pid)
これで非同期的にコマンドを実行してくれる。
さらに、Threadの状態をみることで、実行が継続しているかその様子をうかがうこともできる。
thr.alive?
プロセスを終了するときには、
Process.kill(:TERM, pid)
とすればいい。ところが、これを実行して、spawn
したプロセスを終了させたにも関わらず、ゾンビが残ってしまうことがある。
調べたところ次のことが判明した。
command が shell のメタ文字
* ? {} [] <> () ~ & | \ $ ; ' ` " \n
を含む場合、shell 経由で実行されます。そうでなければインタプリタから直接実行されます。
どういうことかというと、空白などのメタ文字が含まれている場合は、
sh -c 'command which you want to execute'
みたいに感じで実行されてしまうのである。こうなってしまうと、pid を記録して、Process.kill(:TERM, pid)
しても、sh -c "command"
が終了するだけで、肝心の "command"
は別のプロセスIDで動いているので終了しない。
これを防ぐためには
pid = spawn("command", "which", "you", "want", "to", "execute")
のようにスペースを入れる変わりに、引数を分割すればいい。これでシェル経由での実行を防ぐことができため、得られたプロセスIDをkillすれば無事に標的のプログラムを終了させることができる。spawnは便利だけどこんな罠があることを知ったので注意したい。
この記事は以上です。
プロセスグループを作成するという方法もある
別の解決策として、起動する子プロセスのプロセスグループを作成するという方法がある。
pid = spawn("command which you want to execute", pgroup: true)
この場合、プロセスグループをキルすれば、子プロセスまで削除することができる。
pgid = Process.getpgid(pid)
Process.kill(:TERM, -pgid) # マイナス値でプロセスグループを指定できる