{"id":22446784,"url":"https://github.com/takuya/php-process-exec","last_synced_at":"2025-08-01T21:32:14.373Z","repository":{"id":265021526,"uuid":"894873431","full_name":"takuya/php-process-exec","owner":"takuya","description":"php process execution with event handling","archived":false,"fork":false,"pushed_at":"2024-12-05T02:00:14.000Z","size":92,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-12-05T03:17:30.198Z","etag":null,"topics":["command","php-library","php8","pipe","process","processing","shell"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/takuya.png","metadata":{"files":{"readme":"README.ja.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-27T06:41:19.000Z","updated_at":"2024-12-05T02:00:18.000Z","dependencies_parsed_at":"2024-11-27T08:36:28.716Z","dependency_job_id":null,"html_url":"https://github.com/takuya/php-process-exec","commit_stats":null,"previous_names":["takuya/php-process-exec"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/takuya%2Fphp-process-exec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/takuya%2Fphp-process-exec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/takuya%2Fphp-process-exec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/takuya%2Fphp-process-exec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/takuya","download_url":"https://codeload.github.com/takuya/php-process-exec/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228224655,"owners_count":17887848,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["command","php-library","php8","pipe","process","processing","shell"],"created_at":"2024-12-06T04:11:22.119Z","updated_at":"2024-12-06T04:11:22.789Z","avatar_url":"https://github.com/takuya.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"## プロセス起動をする\n\n時間のかかるプロセスをイベントハンドラで呼び出せるようにした。\n\n### 使用例\n\nbashで文字列を実行する。\n```php\n\u003c?php\n$executor = new ProcessExecutor(new ExecArgStruct('bash'));\n$executor-\u003esetInput('\nfor i in {0..4}; do\n  echo $i\ndone;\n');\n$executor-\u003estart();\n//blocking io\necho $executor-\u003egetOutput();\n```\n文字列出力で`なにか`する。\n```php\n\u003c?php\n$arg = new ExecArgStruct('php');\n$src =\u003c\u003c\u003c'EOS'\n\u003c?php\nforeach(range(0,4) as $i){\n  printf(\"%d\\n\",$i);\n}\nEOS;\n$arg-\u003esetInput( $src );\n$executor = new ProcessExecutor( $arg );\n$executor-\u003eonStdOut(function ($line){ //=\u003e１行ごとに処理\n  echo $line.PHP_EOL;\n});\n$executor-\u003estart();\n```\n\nプロセスの終了時に`なにか`する。\n```php\n\u003c?php\n// プロセスを起動する基本設定\n$arg = new ExecArgStruct();\n$arg-\u003esetCmd( ['php'] );\n$src = \u003c\u003c\u003c'EOS'\n\u003c?php echo 'Hello World';\nEOS;\n$arg-\u003esetInput( $src );\n// プロセス状態によってコールバック\n$observer = new ProcessObserver();\n$observer-\u003eaddEventListener( ProcessErrorOccurred::class, fn()=\u003e fwrite(\"php://stderr\",\"エラー\") );\n$observer-\u003eaddEventListener( ProcessFinished::class, fn($ev) =\u003eprint($ev-\u003egetExecutor()-\u003egetOutput()) );\n// プロセス起動\n$executor = new ProcessExecutor( $arg );\n$executor-\u003eaddObserver( $observer );\n$executor-\u003estart();\n```\n### シェルを経由しないコマンド実行\n\n「コマンド実行は**避けるべき**。」と習った人もいるだろう。コマンド実行のコードを書くこと自体を禁止される職場もあるそうだ。\nコマンド実行禁止される理由の一つに、シェルのエスケープがある。文字列のエスケープ処理に問題がある。\n\nだったら、エスケープが不要な実行方法で実行すればいい。\n\nphpであれば、`proc_open`にArrayを渡すのがそれに相当する。\n```php\n\u003c?php\n$file_name = 'my long spaced doc.txt';\nproc_open(['cat',$file_name]...);\n```\n\nほかにもディレクトリトラバーサルに関する脆弱性考えられる。\nこちらに関しては、シェル実行しないのであれば事前にプログラミングでチェックが可能である。\n```php\n\u003c?php\n$file_name = '../../../../../../../../etc/shadow';\n$file_name = realpath('/my/app_root/'.basename($file_name);\nproc_open(['cat',$file_name]...);\n```\n\nこれらのことから、`proc_open`をちゃんと使ったコマンド実行は安全である。と言える。\n\nそれでも`proc_open`自体が面倒なので、`ProcOpen`というラッパーを作った。\n```php\n\u003c?php\n$proc = new ProcOpen(['cat',$fname]);\n$proc-\u003estart();\necho stream_get_contents($proc-\u003estdout());\n```\nこれでも、まだ不満だった。\nコマンドの起動設定（引数＋配列＋出力先）を使いまわすのが大変なので、コマンドに関する情報をStructにまとめた。\n```php\n\u003c?php\n$struct = new ExecArgStruct(['cat',$fname]);\n$proc = new ProcessExecutor($struct);\n$proc-\u003estart();\necho $proc-\u003egetOutput();\n// 使いまわし\n$proc = new ProcessExecutor($struct);\n$proc-\u003estart();\necho $proc-\u003egetOutput();\n```\n\nこれで、起動可能なコマンドを制限するアクセス可能なファイル制限や必須オプション追加チェックなどを`ExecArgStruct`を継承したクラス内でチェックができるようになる。\n役割分担をすることでコードがスッキリする。\n\nたとえば、次のように、利用可能なコマンドをチェックしたり。\n```php\n\u003c?php\nclass RestrictedArg extends ExecArgStruct {\n  public function __construct(...){\n    // check allow command.\n    $this-\u003echeck() || throw new InvalidArgumentException();\n  }\n}\n// InvalidArgumentException\n$struct = new RestrictedArg(['passwd',$name]);\n```\n実行するコマンドの必須オプションをチェックしたり。\n```php\n\u003c?php\nclass MyFFmpegStruct extends ExecArgStruct {\n  public function __construct(...){\n    // check command options.\n    $this-\u003echeckOptions() || throw new \\Exception('you need \"-f\" option ');\n  }\n}\n//-\u003e InvalidArgumentException\n$struct = new MyFFmpegStruct(['ffmpeg','-i',$name]);\n```\n\nこのパッケージでは、安全な「コマンド実行」のための工夫ができるようにした。\n### イベント・リスナ\n\nイベントをつかって「プロセスが〇〇したら、〇〇する。」\n\nproc_openでプロセスの実行中・終了・失敗処理を書くとコードが煩雑になる。 解決のため、すべてを事前にコールバック登録できるようにした。\n```php\n\u003c?php\n// コマンド構造を定義\n$arg = new ExecArgStruct('php -i');\n$executor = new ProcessExecutor($arg);\n// オブザーバー(リスナ集合体）を準備\n$observer = new ProcessObserver();\n$observer-\u003eaddEventListener(ProcessStarted::class, fn()=\u003edump('started')));\n$observer-\u003eaddEventListener(ProcessSuccess::class, fn()=\u003edump('successfully finisihed')));\n$observer-\u003eaddEventListener(ProcessRunning::class, fn()=\u003edump('running')));\n$observer-\u003eaddEventListener(ProcessRunning::class, function(ProcessRunning $ev){\n  // 同一イベントに複数登録も可能\n  printf(\"pid=%d\",$ev-\u003egetExecutor()-\u003egetProcess()-\u003einfo-\u003epid);\n});\n// 複数のオブザーバも利用可能\n$streamObs = new ProcessObserver();\n$streamObs-\u003eaddEventListener(StdoutChanged::class, fn()=\u003edump('stdout changed')));\n// 紐付ける。\n$executor-\u003eaddObserver($observer);\n$executor-\u003eaddObserver($streamObs);\n$executor-\u003estart();\n```\n\nイベントは、次の通り準備した。\n\n| クラス                  | 説明                                                       |\n|:---------------------|:---------------------------------------------------------|\n| ProcessReady         | 初期化時                                                     |\n| ProcessStarted       | ProcessStartedは、プロセス起動時の初回だけ呼ばれる。以降はProcessRunningが呼ばれる。 |\n| ProcessRunning       | 実行中(約0.001secごと)                                         |\n| ProcessErrorOccurred | エラー時                                                     |\n| ProcessCanceled      | シグナル検出                                                   |\n| ProcessSucceed       | 正常終了時                                                    |\n| ProcessFinished      | ProcessFinishedは、成功時も失敗時も両方。                             |\n| StdoutChanged        | STDOUTに変化があったとき                                          |\n| StderrChanged        | STDERRに変化があったとき                                          |\n\n\nイベント・オブザーバーは、ProcessExecutor自身がストリームイベントの検出にも利用している。\n\n#### onStdOut / onStdErr\nStdoutChangedでオブザーバーを記述せずに済むようにオブザーバーを使ったシンプルなリスナ機構をビルトインしてある。\n\n```php\n\u003c?php\n$executor = new ProcessExecutor( new ExecArgStruct(['cat','file']) );\n$executor-\u003eonStdOut(function ($line){\n  echo $line.PHP_EOL;\n});\n$executor-\u003estart();\n\n```\n### onInputProgress / progress input (pv)\npvコマンドのパーセント表示の相当機能をビルトインした。\n```php\n\u003c?php\n$executor = new ProcessExecutor( new ExecArgStruct(['cat','-']) );\n$executor-\u003esetInput(fopen('file','r'));\n$executor-\u003eonInputProgress(fn($percent)=\u003eprintf(\"%s%%\\n\",$percent));\n$executor-\u003estart();\n```\n\nコマンドがどこまで元ファイルを読み込んだかパーセントで測定できる。\n\nただし、まだ不安定。速度調整がいまいち。\n\n### パイプ(pipe)起動\nシェルでパイプを呼び出す。`pipe(|)`の例。\n```shell\ncat /etc/passwd | grep takuya\n```\nパイプを使ったシェルコマンドを`純粋なproc_open()`で書くと、**とてもめんどくさい**.\n```php\n\u003c?php\n$p1_fd_res = [['pipe','r'],['pipe','w'],['pipe','w']];\n$p1 = proc_open(['ls','/etc'],$p1_fd_res,$p1_pipes);\nfclose($p1_pipes[0]);\n$p2_fd_res = [$p1_pipes[1],['pipe','w'],['pipe','w']];\n$p2 = proc_open(['grep','su'],$p2_fd_res,$p2_pipes);\n\nwhile(proc_get_status($p1)[\"running\"]){\nusleep(100);\n}\nwhile(proc_get_status($p2)[\"running\"]){\nusleep(100);\n}\n//\n$str = fread($p2_pipes[1],1024);\nvar_dump($str);\n```\n\n`ProcOpen`クラスを作って`proc_open()`からのパイプ起動を使いやすくした。\n```php\n\u003c?php\n$p1 = new ProcOpen(['/bin/echo','\u003c?php echo \"Hello\";']);\n$p1-\u003estart();\n$p2 = new ProcOpen(['/usr/bin/php']);\n$p2-\u003esetInput($p1-\u003estdout());\n$p2-\u003estart();\n$p1-\u003ewait();\n$p2-\u003ewait();\n//\necho stream_get_contents($p2-\u003estdout()); //=\u003e Hello\n```\nさらに抽象度を高めた書き方をサポートした。\n```php\n\u003c?php\n$arg1 = new ExecArgStruct('bash');\n$arg1-\u003esetInput( \u003c\u003c\u003cEOS\n  echo -n '\u003c?php echo \"Hello World\".PHP_EOL;';\n  EOS );\n$e1 = new ProcessExecutor( $arg1 );\n$e2 = new ProcessExecutor( new ExecArgStruct('php') );\n$e1-\u003epipe($e2);\n$out = $e2-\u003egetOutput();\necho $out\n```\n### パイプ起動で２つのStdErrorを読み込む。\n\nたとえば、次のように`pv x.mp4| ffmpeg -i pipe:0` を起動する場合\n```shell\n\"pv -f -L 2M work.mp4 | ffmpeg -y -i pipe:0 -s 1280x720 -movflags faststart out.mp4\"\n```\n通常のシェルでは次のように、STDERRに出力されて、お互いに消し合ってしまう。\n```shell\n500KiB 0:00:02 [ 251KiB/s] [==============\u003e                   ] 46% ETA 0:00:02\\r\nframe=  0 fps=0.0 q=0.0  size=   0kB time=00:00:01.42 bitrate=   0.3kbits/s speed=1.47x\\r\n```\n\nプロセスごとに、STDEERを別に書き出せばいい。(`cmd 2\u003eerr.txt`) しかしログ閲覧は煩雑である。\n\n```shell\n## バックグラウンドで起動して\npv -f -L 2M work.mp4 2\u003eerr.1.txt | \\\nffmpeg -y -i pipe:0 -s 1280x720 -movflags faststart out.mp4\" 2\u003e err.2.txt \\\n\u0026\n\n## tailで２つ起動してログを見る。\ntail -f err.1.txt err.2.txt\n```\n\nシェル(bash)で出力ファイルを使わず、プログラミングで直接的にErrorストリームを扱えれば、ログ取得はスッキリする。 それをこのパッケージで実現する。\n\n```php\n\u003c?php\n// パイプでつないで\n$pv = new ExecArgStruct( 'pv -f -L 2M work.mp4' );\n$ffmpeg = new ExecArgStruct('ffmpeg -i pipe:0 -s 1280x720 -movflags faststart out.mp4');\n$p1 = new ProcessExecutor( $pv );\n$p2 = new ProcessExecutor( $ffmpeg );\n$p1-\u003epipe( $p2 );\n// STDERRのログをそれぞれで管理する。\n$p1-\u003eonStderr( fn( $progress ) =\u003e dump(\"pv: \".$progress) , \"\\r\" );\n$p2-\u003eonStderr( fn( $enc_stat ) =\u003e dump(\"ffmpeg: \".$enc_stat), \"\\r\" );\n```\n結果は次のように、プロセス別個にSTDERRを取得できる。\n```text\n\"pv:  500KiB 0:00:02 [ 251KiB/s] [==============\u003e                   ] 46% ETA 0:00:02\"\n\"pv:  750KiB 0:00:03 [ 251KiB/s] [======================\u003e           ] 70% ETA 0:00:01\"\n\"pv: 1000KiB 0:00:04 [ 251KiB/s] [==============================\u003e   ] 93% ETA 0:00:00\"\n\"pv: 1.05MiB 0:00:04 [ 251KiB/s] [================================\u003e] 100%            \"\n\"ffmpeg: frame=  0 fps=0.0 q=0.0  size=   0kB time=00:00:01.42 bitrate=   0.3kbits/s speed=1.47x\"\n\"ffmpeg: frame= 44 fps= 16 q=29.0 size=   0kB time=00:00:03.32 bitrate=   0.1kbits/s speed=1.24x\"\n\"ffmpeg: frame= 80 fps= 21 q=29.0 size= 256kB time=00:00:04.54 bitrate= 461.6kbits/s speed=1.21x\"\n```\n上記の例のように、STDERRを別々のストリームとして処理できる。\n\n## このパッケージに含まれるクラス\n\n|               クラス | 説明                               |\n|------------------:|:---------------------------------|\n|        `ProcOpen` | `proc_open()`のラッパー               |\n|   `ExecArgStruct` | コマンド構造を定義するクラス                   |\n| `ProcessExecutor` | `ProcOpen`のラッパーでイベントを管理する        |\n| `ProcessObserver` | `ProcessExecutor`で発火するイベントのリスナ管理 |\n|        `StreamIO` | fopenされた`stream resource`を隠蔽する   |\n|    `StreamReader` | ジェネレーター。`StreamIO`から１行単位で読み出す。   |\n\n## 注意点\n\n#### 注意１\nLinux の PIPE_BUF / PIPE_SIZE に影響を受けるので注意。\nstdout や stderr を読み出さずに大量に書き出すと、Linuxのpipeはバッファが詰まってプロセスから書き出せずに、プロセスが停止します。\n\n`PIPE_SIZE=65,536 bytes`なので、64kBがstdoutに貯まると、プロセスはそれ以上をStdoutに書き出しできずにストップする。\n\n適切に読み出すか、出力ファイルを指定する。`ffmpeg`や`imagemagick`を`proc_open`してSTDOUT書き出しすると、停止してしまいます。\n\nこのパッケージでは、**意図的に**、敢えて止まように設計している。出力先は自分で指定する。\n```php\n\u003c?php\n// stdout の io blocking で止まる\n$arg = ExecArgStruct('ffmpeg -i input.mp4 -s 1280x720 -f mp4 pipe:1');\n$ffmpeg = new ProcessExecutor( $arg );\n$ffmpeg-\u003estart();\n// output.binに書かれるので、止まらない。\n$arg = ExecArgStruct('ffmpeg -i input.mp4 -s 1280x720 -f mp4 pipe:1');\n$arg-\u003esetStdout(fopen('output.bin','w'));\n$ffmpeg = new ProcessExecutor( $arg );\n$ffmpeg-\u003estart();\n```\n\n#### 注意２\n\ndaemon化させる`ForkedExecutor`はsemaphoreやSharedMemoryを使う。\n\nセマフォ(semaphore)や共有メモリ(SharedMemory)を使う場合。確保したまま終了するとサイズ不足で新規作成で着なくなります。\n確保できずにプログラムが固まります。\n\nCtrl-Cでなどで中断したあとは、セマフォ・SharedMemoryを確認すること\n\n次のコマンドを使って手動で管理を徹底する（とくにmacOS。macOSは利用可能なサイズが少ないので完璧な片付けを徹底する。）\n```shell\nipcs -a\nipcmr -m $id\n## 例\nipcs -a | \\grep `whoami` | awk '{print $2}' | xargs  -I@ ipcrm -m @\nipcs -a | \\grep `whoami` | awk '{print $2}' | xargs  -I@ ipcrm -s @\n```\n\n#### 注意３\nPOSIXシグナルを検出するために、次の１行を書いた方がいい。\n\nこれを書かないと、OSシグナル検出ができない。（ 参考資料:[PHPとシグナル、その裏側\n](https://www.slideshare.net/do_aki/20171008-signal-onphp) )\n```\npcntl_async_signals( true )\n```\n`ProcessExecutor`では暗黙的に実行するが、`ProcOpen`では明示的に実行する必要がある。\n\n## インストール\n\nTODO\n\n## テスト\nphpunit でテストする。\n```shell\ncomposer install \nvendor/bin/phpunit \nvendor/bin/phpunit --filter ProcOpenTest\n```\n\n## コードカバレッジ\n\nコードカバレッジをphpunitで出す場合。\n\n```shell\nXDEBUG_MODE=debug,coverage vendor/bin/phpunit --coverage-html coverage\n```\n\n## TODO:\n- Linux pip max に達した時点で stdout / stderr を読み込みバッファリングする。\n- tty のサポート\n    -  stream_isatty で調べて、Y/Nを送信できるようにする。\n\n## todo:\n2024-09-15\n出力のバッファリングはやっぱりデフォルトでいれる必要がある。\n\nffprobeとか出力数が少ないと思って、適当に決め打ちで書いて詰まった。\n\n```sh\n// ffprobeを起動するだけでもめんどくさい。\n//\n$args = $this-\u003ebuildCmd( $path, $opts );\nif ( !is_readable($path)){\n  throw new \\RuntimeException(\"path is not readable ( {$path} ) \");\n}\n$p = new ProcessExecutor( $args );\n$out_buff='';\n$p-\u003eonStdout(function($line)use(\u0026$out_buff){ $out_buff.=$line.PHP_EOL;}, PHP_EOL );\n$p-\u003estart();\nreturn [1 =\u003e $out_buff, 2 =\u003e $p-\u003egetErrout()];\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftakuya%2Fphp-process-exec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftakuya%2Fphp-process-exec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftakuya%2Fphp-process-exec/lists"}