php でコマンドのプロセス実行したい。
シェルコマンドの呼び出しをどうしてもやる必要があって、ずっと proc_open について調べてたり、pakagist / pear などのライブラリを見てたんだけど、代表的なものをいくつか試した。しかしコレと思えるものや使い方が気に入るものがなかった。仕方ないので自作した。
php のシェルコマンド実行の問題点
いくつか、気づいた問題点があり、それを解消できるパッケージがなかった。
プロセスを実行するライブラリには限界がある。
php には memory_limtit と max_execution_time という、プロセスを実行する妨げになる設定がある。この制限があるので、途中で処理が止まる。特に巨大なアウトプットで時間がかかる処理(たとえば ffmpeg ) を php から実行すると、予想外の箇所で停止することがある。途中でコマンド実行が止まってしまうと本当に駄目。巨大なアウトプットはメモリ使い果たすどころか、エラーにならないことすらある。
プロセスを実行するライブラリはほとんどが、結果を文字列で返そうとする。そのためメモリ制限に掛かりエラーになる。phpのmemory exhausted とか excess of memory とか急に出てくるのでライブラリを使うのを諦めた。
パイプで処理を渡したい
パイプで処理を渡す( echo hello | cat
のようなもの ) 、このためのシェルコマンドのエスケープがうまく出来ない。または面倒だったり、またパイプを使うとなぜか詰まることがある。先のプロセスのoutput をうまく後者のstdin に繋げなかったphp のライブラリがある。
ライブラリの使い方が煩雑。
ライブラリを眺めてると、手順が煩雑だったり、インストール後のコードが依存関係で面倒になったりしてた。
vanilla な phpでストリームを扱いたい。
php でちゃんとstdout を扱うとしたら proc_open に合わせてストリームで処理する必要があるだろう。文字列で渡す前提なのでトラブルが多い。また一時ファイルを大量に使うことになって不便だった。
php の proc_openがきれいじゃない。
php でプロセスをある程度制御したいなら proc_open
を使うのだが、これがC言語スタイルで良くない。手続きが煩雑なのでコードの見通しが悪い。
作った
仕方ないので、コロナ春休みを活用して作ることにして、完成した。
使い方
<?php $proc = new Process(); $proc->setCmd('php') ->setInput('<?php echo "Hello World"') ->pipe('cat') ->pipe('cat') ->pipe('cat') ->wait(); // $fd = $proc->getOutput(); $out = stream_get_contents($fd); print($out);// -> Hello World
コマンド実行をしたり、パイプで渡したり、ファイルディスクリプタを受け渡してストリームで処理する。
これで巨大なアウトプットを一時ファイル(php:/temp)を使って受け渡ししたり、プロセスをちゃんと終了させたり、タイムアウト待ちができるようになった。
シェルコマンドのリダイレクトの代わり
シェルコマンドのリダイレクトを文字列でやらずに、こう書くことができる
STDINのリダイレクト
標準入力のインプットを渡すときはシェルコマンドはこう
cat < out.txt
これと同じ、プロセスのコマンド実行を、次のように書けるようにした。
<?php $proc = new Process(); $proc->setCmd('cat') $proc->setInput('/tmp/out.txt"') $proc->run(); // $fd = $proc->getOutput();
STDOUTのリダイレクト
標準出力のアウトプットを渡すときはシェルコマンドはこう
date > out.txt
これと同じ、標準出力のリダイレクトでプロセスのコマンド実行は、 次のように書けるようにした。
<?php $proc = new Process('date'); $proc->setOutput('/tmp/out.txt"') $proc->run();
パイプでつなぐ
パイプは、コマンド実行できるプロセスを2つ起動して、input と output をつなぐだけなので、そのように書けるようにした。
date | cat
これと同じ実行を php でできるようになった。
<?php $proc1 = new Process('date'); $proc1->setInput($str); [$p1_out, $p1_err] = $proc1->start(); $proc2 = new Process('cat'); $proc2->setInput($p1_out); $proc2->run(); $p2_out = $proc2->getOutput(); $str = stream_get_contents($p2_out);
stdout / stdin をつなぐだけなら、定形なのでもっと簡単にメソッドチェーンで書けるようにした。
パイプを複数つなげる。
メソッドチェーンで書けると書きやすいだろうし、そう書けるようにした。
<?php $proc = new Process('date'); ->pipe('cat') ->pipe('cat') ->wait(); // $fd = $proc->getOutput(); $out = stream_get_contents($fd); print($out);// -> Hello World
標準入力に文字列を渡す。
コマンド実行時に、標準入力として文字列を渡して、プロセスに実行させる。標準入力からファイルの中身を受け取るコマンドを実行する。
echo 'echo Hello World' | sh
標準入力に文字列を渡してコマンドを実行する。
<?php $str = 'echo "Hello World"'; $proc = new Process('sh'); $proc->setInput($str ); $proc->run(); $fd = $proc->getOutput(); $out = stream_get_contents($fd);
標準入力に文字列を渡せば、標準入力読み取りに対応するコマンドなら実行できる。grepやsedはもちろん、phpもpython や ruby もだ。
php も実行できる
<?php $str = '<?php echo "Hello World"'; $proc = new Process('php'); $proc->setInput($str ); $proc->run(); $fd = $proc->getOutput(); $out = stream_get_contents($fd);
コマンドに標準入力をちゃんと渡せるなら、phpから phpの proc_openもできるの。ライブラリを作ったことでだいぶ書きやすくなった。
python 実行も同様に
<?php $proc = new Process('python'); $proc->setInput(' import sys print(sys.path) '); $proc->run(); $fd = $proc->getOutput(); $out = stream_get_contents($fd); var_dump($out);
同様に、標準入力にソースコードを渡せば実行できるpython もちゃんと動かせてる。
ssh 経由でコマンドを実行する
ssh 経由での実行は、php の標準のプロセス実行をそのまま使うとエスケープ面倒 。それもやりたいので対応させた。
<?php $proc = new Process(['ssh','root@192.168.2.1','sh -c date']); $proc->run(); $fd = $proc->getOutput(); $out = stream_get_contents($fd); var_dump($out);// -> Sat Mar 14 09:32:18 JST 2020
実行中にリアルタイムでなにか処理をいれる
プロセスの実行中に何かをやりたい。expect みたいに入力を判定させたりするコールバック関数を渡せるようにもした。
<?php $proc = new Process('php'); $proc->setInput('<? sleep(1); echo "Hello World"'); $proc->start(); $proc->wait( function ($status,$pipes){ var_dump('wating'); usleep(1000*10); }, function ($status,$pipes){ var_dump('success.'); }, function ($status,$pipes){ var_dump('error occured'); } );
実行エラーを例外に。
プロセスに実行エラーを例外として取得したいとき、コールバックで例外を投げればできる。
<?php try{ $proc = new Process('___noexists_command_'); $proc->setOnError(function($pr,$io){ throw new \Exception('error'); }); $proc->run(); }catch (\Exception $e){ echo 'error occured'; }
proc_open 自体には例外がないので、ラッピングするしかなかった。
まとめ
シェルコマンドのプロセスの実行をあれこれするには、STDIN/STDOUTのパイプをファイルのディスクリプタで繋ぐ必要がある。phpではIOを stream 系のファイルディスクリプタの関数群でこれを使う必要がある。ところがコマンド実行ができるライブラリやパッケージで、ファイルディスクリプタをIOを扱えるパッケージはあまり見当たらなかった。
proc_open で確保されるデフォルトのpipeストリームは数MBを超える出力を扱うとなぜか固まってた。また起動したプロセスにうまく stdin を fclose して渡せないものだったり、fcloseのeof 待ちになってフリーズしてた。それの辺をどうしても解消したかった。併せてコードとしてシェル実行を書きやすい形にできるライブラリがほしかった。