Windows で外部プログラムを非同期に起動したり落としたりする

私は普段、テストツールとしてrubyを使っているわけですが、その中で非同期に外部プログラムを起動したり、落としたりしたいことがあります。

windows の標準コマンドを使用するなら`start`コマンドとと`tasklist`を駆使して起動を制御し、`taskkill`によって何とか終了させることができます。

# Windows標準コマンドで頑張る

```rb:start/tasklist/taskkillで起動/終了
def pids(pname)
  ret = `tasklist /fi "imagename eq #{pname}"`
  lines = ret.split("\n")
  ids = []
  if(lines.size >= 3)
    lines[2..-1].each do |line|
      ids << line.split[1]
    end
  end
  ids
end

prev = pids("ruby.exe")
system('start "" ruby -e "puts 100; loop { sleep(100) }"')
current = nil
loop do
  current = pids("ruby.exe")
  if current.size - prev.size == 0
    sleep(1)
    next
  end
  break
end
puts "process started!!"

cur_pid = (current - prev).first
`taskkill /f /pid #{cur_pid}`
puts "process killed!!"

```

かなり力業ですが、こんな感じでしょうか。`start "" <外部コマンド>`とすることで別cmdが起動して非同期に実行できます。ただ、同じ名前のコマンドが実行されていた場合も考慮して、事前にpidを保持したり、tasklistを起動が完了するまで何度も実行していたりと、無駄が多いですし、何より遅い。もっといえば、差分をとったタイミングで別のプロセスが終了したりした時点で上記のコードは動かなくなるので、場合によっては使い物にならないですね。

# Kernel.#spawnを使う

ruby は プロセスを扱うためのクラスが用意されていて、幸いにもWindowsでも動作します。外部コマンドを実行する方法は以下の記事が参考になります。

[Rubyで外部コマンドを実行して結果を受け取る方法あれこれ](https://qiita.com/tyabe/items/56c9fa81ca89088c5627)

今回は、上記の記事に記載されていないメソッド `Kernel.#spawn`を使用します。

```rb:Kernel.#spawn
pid = spawn('ruby -e "puts 100; loop { sleep(100) }"')
`taskkill /f /pid #{pid}`
```
かなりスッキリしましたね。戻り値がpidなので、起動を待ち合わせる必要もなくなりました。ただ、このままだと入出力が親のそれと共有してしまうので、親側で標準出力になにか出力させつつ、子側でも出力してしまうと体裁が崩れてしまう可能性はあります。その場合はリダイレクトを行うことで回避できるかもしれません。

```rb:Kernel.#spawn(リダイレクト)
pid = spawn('ruby -e "puts 100; loop { sleep(100) }"', :in => "NUL", :out => "NUL", :err => "NUL")
```

上記の例では、すべてを闇に葬ってますが、ファイルに落としたり、別の入出力とつなげたりできるので、用途に合わせて使い分けるのがいいと思います。詳しくは、リファレンスマニュアル参照ということで。

https://docs.ruby-lang.org/ja/latest/method/Kernel/m/spawn.html

# Process.kill

rubyのメソッドを使用して非同期での起動はできました。終了もrubyのメソッドを使用して行うことが可能です。

```Process.kill
  pid = spawn('ruby -e "puts 100; loop { sleep(100) }"', :in => "NUL", :out => "NUL", :err => "NUL")
  Process.kill("KILL", pid)
```

ただ、WindowsでのこのメソッドはLinux系のそれよりも機能が制限されているようで、"TERM"シグナルの送信ができません。よって、"KILL"で強制終了させるしかないようです。それが嫌なら、`taskkill`を使うほかないようです。

[rubyで非同期に実行した外部プログラムのプロセスIDを取得し、強制終了させたい。
](https://ja.stackoverflow.com/questions/34258/ruby%E3%81%A7%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%81%AB%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%9F%E5%A4%96%E9%83%A8%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9id%E3%82%92%E5%8F%96%E5%BE%97%E3%81%97-%E5%BC%B7%E5%88%B6%E7%B5%82%E4%BA%86%E3%81%95%E3%81%9B%E3%81%9F%E3%81%84)

# bamchoh/kick, bamchoh/krsp の紹介(供養)

`spawn`がリダイレクトできることを知る前までは、一番初めに紹介した方法で今まで非同期実行を行ってきていたわけなんですが、それに嫌気がさして、`golang`でspawnとkillのようなコマンドを作りました。(その数時間後にはspawn/Process.killでできることに気付いて泣きそうになったのは秘密)

https://github.com/bamchoh/kick

https://github.com/bamchoh/krsp

`kick` はその後に続く引数の最初をコマンドとみなして、コマンドを非同期実行します。標準出力は親とは共有せず、標準エラーのみ共有させています。

`krsp` は与えられた引数のpidを KILLシグナルで強制終了させるものです。

rubyスクリプトでは上記の方法で記述すれば問題ないので、活用方法がないけど、バッチファイル等であれば、陽の目を見る日もあるかもしれない。。。

# まとめ

Rubyを使って、Windowsで外部プログラムを非同期に起動/終了する方法をご紹介しました。
Windows では start/tasklist で非同期実行が可能ですが、懸念点が多いので、rubyの標準ライブラリであるKill.#spawnやProcess.killを使いましょう。Kill.#spawnはリダイレクトもできるので、状況に応じて使い分けましょう。Process.killは強制終了させるシグナルが送信されるので、正常終了が必要な場合はtaskkillを使うのが無難だと思います。

本記事に関して何があればお気軽に編集リクエストなりコメントなりしてください、できる限り対応します！

kick/krspも使いどころがあればお使いください。