この記事はElixir Advent Calendar2022-4の24日目です。
執筆時、新型コロナにかかっていたのでコード少なめです m(__)m
開発環境
- Windows 10 + WSL2 + Ubuntu 22.04
- Elixir 1.13.4
- Erlang/OTP 25
対象者
- Elixirの外側にある*.exeやコマンドを呼び出したい
- 呼び出しの結果を保持しながら同じコマンドを呼び出したい
- 重たい外部処理をスマートに呼び出したい
基本
System.cmd
引数が1つの外部処理を呼び出すときに使います。
iex> System.cmd("echo", [hello])
{"hello\n", 0}
:os.cmd
System.cmd
だと引数が複数あるときに記述が少しややこしくなります。
せっかく書いたのに動かないこともあります。
そんなときに:os.cmd
が便利です。
iex> :os.cmd("rm -rf hello.txt")
[]
発展
重たい処理の実行終了と確認
外部処理が重たいとき、コマンドを実行するとそのまま制御が返ってこなくなってしまいます。
私の場合は再生時間が長い動画の変換や抽出処理で体験しました。
下記はFFmpegで動画の音声を取得するコマンド例です。
iex> :os.cmd("ffmpeg -i inputfile.mp4 outputfile.mp3")
...
制御を待っている間プログラムが止まってしまいますので「コマンドを打って終わるまでは別のタスクをして、終わったら本来のタスクに戻る」という動きをしたい。
こういうときのためか、System.cmd
はPort
モジュールを使用しているようです。
Port
は下記のように使います。
iex> path = System.find_executable("ffmpeg")
iex> port = Port.open({:spawn_executable, path}, [:binary, :exit_status, {args: ["-i", "inputfile.mp4", "output.mp3"]}])
オプションにexit_status
を指定することでプログラムの終了通知を受け取ることができるようになります。
通知はPort.monitorを実行したプロセスにメッセージ送信されます。下記が受信するメッセージの例です。
iex> Port.monitor(port)
{:DOWN, ref, :port, object, reason }
連続して重たい処理
CPUに負荷がかかるような外部処理をどんどん生み出すとCPUとメモリがなくなってしまいます。
常にプロセスを監視して、終了通知が来たら次の処理をしてくれて、もしプロセスが落ちても復活してくれるそんな便利なプロセスがGenServer。
Elixir Schoolにも解説があります。
https://elixirschool.com/ja/lessons/advanced/otp_concurrency
GenServer + Portを使うと、外部に渡すデータがある限り外部処理を順次呼び出すプログラムを作ることができます。
参考記事
英語ですが、下記がGenServerとPortの参考記事になります。
-
Learning Elixir's GenServer with a real-world example
real worldはhello worldの対の意味ではなく、現実にあるプロダクトでどんな風につかっているかという記事です。GenServerを使ってDBの更新をするサンプルをみることができます。 -
Managing External Commands in Elixir with Ports
今回、紹介したかったPort + GenServerで外部の重たい処理を呼び出す方法が解説されています。
まとめ
簡単ですが、外部処理を呼び出す・管理する方法について紹介しました。
- シンプルな外部処理は
System.cmd
- 外部処理の引数が多いなってときは
:os.cmd
- 外部処理が重たい!ってときは
Port
モジュール - 重たい外部処理を管理したいってときは
Port
+GenServer