はいさい
この記事は、"ちゅらデータアドベントカレンダー"の19日目です。
概要
- symfonyのProcess componentで最近
遊ぶ使う機会がありました - 先に知っておけば楽だった的な内容です
- ただ、いまさらだけどやっぱり全部ドキュメントに載っていたので、詳しくはドキュメントよんでおくれ
環境
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.7
BuildVersion: 19H15
$ docker --version
Docker version 19.03.13, build 4484c46d9d
Dockerfileとimageを用意
シュッと環境を用意したいので、Dockerfileを用意します。
FROM php:7.4-cli
# https://hub.docker.com/_/php
RUN pecl install redis-5.1.1 \
&& pecl install xdebug-2.8.1 \
&& docker-php-ext-enable redis xdebug
# https://qiita.com/yatsbashy/items/02bbbebbfe7e5a5976bc
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN apt update -y && apt install git wget -y
buildする
$ docker build -t a-calendar-php .
コンテナ起動します
$ docker run --rm -v $(pwd):/app -it a-calendar-php /bin/bash
root@bb8dba6afa9a:/#
おk
Process componentを試す
準備
ちとながいです
Process componentのインストール
composerでcomponentをインストールします
$ composer require symfony/process
そしたら動かすPHPファイルも用意
$ cat cmd1.php
<?php
require_once "vendor/autoload.php";
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
$process = new Process(['ls', '-lsa']);
$process->run();
// executes after the command finishes
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
echo $process->getOutput();
Hello world
用意したPHPファイルを叩いてみる
# php cmd1.php
total 24
0 drwxr-xr-x 7 root root 224 Dec 14 06:51 .
4 drwxr-xr-x 1 root root 4096 Dec 16 17:47 ..
4 -rw-r--r-- 1 root root 313 Dec 14 06:47 Dockerfile
4 -rw-r--r-- 1 root root 355 Dec 14 06:51 cmd1.php
4 -rw-r--r-- 1 root root 61 Dec 14 06:45 composer.json
8 -rw-r--r-- 1 root root 5584 Dec 14 06:45 composer.lock
0 drwxr-xr-x 5 root root 160 Dec 14 06:46 vendor
はい。ls -lsa
の結果が表示されたね
ls
じゃ面白みにかけるので面白そうなコマンドをインストールしてみる
文字、テキストのアスキーアート生成するコマンドをインストールしてみます。
# apt update -y && apt install -y figlet
# figlet churadata
_ _ _
___| |__ _ _ _ __ __ _ __| | __ _| |_ __ _
/ __| '_ \| | | | '__/ _` |/ _` |/ _` | __/ _` |
| (__| | | | |_| | | | (_| | (_| | (_| | || (_| |
\___|_| |_|\__,_|_| \__,_|\__,_|\__,_|\__\__,_|
helpをみてみます
# figlet --help
figlet: invalid option -- '-'
Usage: figlet [ -cklnoprstvxDELNRSWX ] [ -d fontdirectory ]
[ -f fontfile ] [ -m smushmode ] [ -w outputwidth ]
[ -C controlfile ] [ -I infocode ] [ message ]
ふむふむー
# figlet -w 30 churadata
_
___| |__ _ _ _ __ __ _
/ __| '_ \| | | | '__/ _` |
| (__| | | | |_| | | | (_| |
\___|_| |_|\__,_|_| \__,_|
_ _
__| | __ _| |_ __ _
/ _` |/ _` | __/ _` |
| (_| | (_| | || (_| |
\__,_|\__,_|\__\__,_|
fontも変更できるようです。
ここからダウンロードしてきましょう。
# wget http://www.jave.de/figlet/fonts/details/starwars.flf
# figlet -w60 -f starwars starwars
_______.___________. ___ .______
/ | | / \ | _ \
| (----`---| |----` / ^ \ | |_) |
\ \ | | / /_\ \ | /
.----) | | | / _____ \ | |\ \----.
|_______/ |__| /__/ \__\ | _| `._____|
____ __ ____ ___ .______ _______.
\ \ / \ / / / \ | _ \ / |
\ \/ \/ / / ^ \ | |_) | | (----`
\ / / /_\ \ | / \ \
\ /\ / / _____ \ | |\ \----.----) |
\__/ \__/ /__/ \__\ | _| `._____|_______/
# figlet -w60 -f starwars churadata
______ __ __ __ __ .______ ___
/ || | | | | | | | | _ \ / \
| ,----'| |__| | | | | | | |_) | / ^ \
| | | __ | | | | | | / / /_\ \
| `----.| | | | | `--' | | |\ \----./ _____ \
\______||__| |__| \______/ | _| `._____/__/ \__\
_______ ___ .___________. ___
| \ / \ | | / \
| .--. | / ^ \ `---| |----` / ^ \
| | | | / /_\ \ | | / /_\ \
| '--' | / _____ \ | | / _____ \
|_______/ /__/ \__\ |__| /__/ \__\
良いですね
cmd1.php
を修正する
<?php
require_once "vendor/autoload.php";
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
$process = new Process(['figlet', 'churadata']); // ここを修正する
$process->run();
// executes after the command finishes
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
echo $process->getOutput();
# php cmd1.php
_ _ _
___| |__ _ _ _ __ __ _ __| | __ _| |_ __ _
/ __| '_ \| | | | '__/ _` |/ _` |/ _` | __/ _` |
| (__| | | | |_| | | | (_| | (_| | (_| | || (_| |
\___|_| |_|\__,_|_| \__,_|\__,_|\__,_|\__\__,_|
はい。PHPから叩けるようになりました。
もう少し改良する
メソッドチェーンでいじれるようにします
<?php
require_once "vendor/autoload.php";
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class Command
{
protected $command = '/usr/bin/figlet';
protected $options = [];
public function setHelpOption(){
$this->options[] = '--help';
return $this;
}
public function setDisplayWord($word) {
$this->options[] = $word;
return $this;
}
public function setFont($fontName) {
$this->options[] = '-f';
$this->options[] = $fontName;
return $this;
}
public function setWidth($widthSize = 60) {
$this->options[] = '-w';
$this->options[] = $widthSize;
return $this;
}
// 上記の様なオプションをセットするメソッドを羅列
public function run(){
$it = new RecursiveIteratorIterator(new RecursiveArrayIterator([$this->command, $this->options]));
$cmd_array = [];
foreach($it as $v) {
$cmd_array[] = $v;
}
$process = new Process($cmd_array);
$process->run();
return $process;
}
}
// Usage
$command = new Command();
$process = $command->setDisplayWord('churadata')
->setWidth('60')
->setFont('starwars')
->run();
echo $process->getOutput().PHP_EOL;
# php cmd1.php
______ __ __ __ __ .______ ___
/ || | | | | | | | | _ \ / \
| ,----'| |__| | | | | | | |_) | / ^ \
| | | __ | | | | | | / / /_\ \
| `----.| | | | | `--' | | |\ \----./ _____ \
\______||__| |__| \______/ | _| `._____/__/ \__\
_______ ___ .___________. ___
| \ / \ | | / \
| .--. | / ^ \ `---| |----` / ^ \
| | | | / /_\ \ | | / /_\ \
| '--' | / _____ \ | | / _____ \
|_______/ /__/ \__\ |__| /__/ \__\
おk(長いへろーわーるどだった)
まだだ!まだおわらんよ
// Usage
のところを毎回ファイル編集するのはめんどくさいので、REPLな何かを導入しましょう!
# wget https://psysh.org/psysh
# chmod +x psysh
# ./psysh
Psy Shell v0.10.5 (PHP 7.4.13 — cli) by Justin Hileman
>>> $a = 'hogehgoe'
=> "hogehgoe"
Laravelのtinkerみたいな画面になりました。
するとですね。。。
>>> require_once('cmd1.php');
______ __ __ __ __ .______ ___
/ || | | | | | | | | _ \ / \
| ,----'| |__| | | | | | | |_) | / ^ \
| | | __ | | | | | | / / /_\ \
| `----.| | | | | `--' | | |\ \----./ _____ \
\______||__| |__| \______/ | _| `._____/__/ \__\
_______ ___ .___________. ___
| \ / \ | | / \
| .--. | / ^ \ `---| |----` / ^ \
| | | | / /_\ \ | | / /_\ \
| '--' | / _____ \ | | / _____ \
|_______/ /__/ \__\ |__| /__/ \__\
=> 1
となります。
と、ここで、Co
と入力後、tabキーを2回押してみてください。
>>> Co
Command Composer\Autoload\Composer # ...省略...
なんと!先程用意した Command
classが使えるようになっています!
というわけで、ここからインタラクティブに操作できます
>>> $cmd = new Command()
>>> $process = $cmd->setHelpOption()->run()
>>> $process->getOutput()
=> ""
あれ・・・・? と思いましたが、shellで確認してみると
# figlet --help
figlet: invalid option -- '-'
Usage: figlet [ -cklnoprstvxDELNRSWX ] [ -d fontdirectory ]
[ -f fontfile ] [ -m smushmode ] [ -w outputwidth ]
[ -C controlfile ] [ -I infocode ] [ message ]
# echo $?
1
helpオプションの終了ステータスは1(失敗時とかに利用されるやつ)らしいです。
Process
では、終了ステータスが0以外は getErrorOutput()
で取得することができます。
>>> $process->getErrorOutput()
=> """
/usr/bin/figlet: invalid option -- '-'\n
Usage: figlet [ -cklnoprstvxDELNRSWX ] [ -d fontdirectory ]\n
[ -f fontfile ] [ -m smushmode ] [ -w outputwidth ]\n
[ -C controlfile ] [ -I infocode ] [ message ]\n
"""
はい。できました。(cmd1.php
の// Usage
のコードは削除しておいてください)
長かったですが、準備完了です!ここから 遊んで Process componentを確認していきたいと思います。
コマンドの正常結果取得
コマンドの正常結果取得は、getOutput()
で取得することができます。
>>> $cmd = new Command()
>>> $process = $cmd->setDisplayWord('hogehoge')->run()
>>> $process->getOutput()
=> """
_ _ \n
| |__ ___ __ _ ___| |__ ___ __ _ ___ \n
| '_ \ / _ \ / _` |/ _ \ '_ \ / _ \ / _` |/ _ \\n
| | | | (_) | (_| | __/ | | | (_) | (_| | __/\n
|_| |_|\___/ \__, |\___|_| |_|\___/ \__, |\___|\n
|___/ |___/ \n
"""
コマンドのエラー結果取得
なんらかの操作でコマンドがエラーを出力した場合は getErrorOutput()
で取得することができます。
エラーハンドリングがなっていないコードなので、こうするとコマンドがエラーとなります。
>>> $cmd = new Command()
>>> $process = $cmd->setDisplayWord('--word')->run() // わざと失敗する文字列を渡す
>>> $process->getOutput()
=> "" // とれない
>>> $process->getErrorOutput()
=> """
/usr/bin/figlet: invalid option -- '-'\n
Usage: figlet [ -cklnoprstvxDELNRSWX ] [ -d fontdirectory ]\n
[ -f fontfile ] [ -m smushmode ] [ -w outputwidth ]\n
[ -C controlfile ] [ -I infocode ] [ message ]\n
"""
getOutput()
だけ確認していると、「あれれ〜?結果がとれないー」(実際はエラー出力しているけど気づけない)ことになるので注意です。
バックグラウンドで実行する
ここからは準備したPHPファイルではなく、sleep
コマンドを実行してみます。
>>> require_once "vendor/autoload.php"
>>> use Symfony\Component\Process\Process
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->run()
# ... 待たされる ...
=> 0
run()
で実行すると、同期処理されるのでコマンドの結果が終了するまで待機させられます。バックグラウンドで実行する場合は start()
を利用しましょう。
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->start()
=> null
コマンドのステータスを確認する
さて、バックグラウンドに処理を回すことができたら、コマンドが終了したかどうかはどう確認すれば良いでしょうか。
isRunning()
で確認することができます
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->start()
=> null
>>> $process->isRunning()
=> true
# ...10秒後...
>>> $process->isRunning()
=> false
実際にサービスで利用する場合は while($process->isRunning())
のようにすると良いでしょう。
また、start()
で実行した場合でも待ちたい場合は wait()
という関数が利用できます。
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->start()
=> null
>>> $process->wait()
=> 0
また、見ての通り、コマンドが正常終了したかどうかは run()
、wait()
ともに、返り値で確認することができます。
コマンドの開始前、実行中、終了のステータスは getStatus()
で取得が可能です。
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->getStatus()
=> "ready"
>>> $process->start()
=> null
>>> $process->getStatus()
=> "started"
>>> $process->wait()
=> 0
>>> $process->getStatus()
=> "terminated"
timeout
実行したコマンドのタイムアウト値を設定することも可能です。デフォルトで60sです。
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->setTimeout(3) // 10秒かかるコマンドだけど3sで設定してみる
>>> $process->run()
Symfony\Component\Process\Exception\ProcessTimedOutException with message 'The process "'/bin/sleep' '10'" exceeded the timeout of 3 seconds.'
処理に時間がかかるコマンドは、この値を長めに修正しましょう。
長い期間出力し続けるコマンドの注意点
数十分間動き続ける + その間ログを出力し続けるタイプのコマンドを想定した場合の注意点です。
例えば、下記コマンドを実行するとターミナルの画面が大変なことになります。
# /bin/cat /dev/urandom
要は延々と乱数文字列を出力するコマンドなのですが、こいつをProcess componentで実行してみてみようと思います。
例えばこれを時間がかかるバッチコマンドだとしましょう。
>>> $process = new Process(['/bin/cat', '/dev/urandom'])
=> Symfony\Component\Process\Process {#2595}
>>> $process->setTimeout(60 * 60)->start()
=> null
>>> $process->isRunning()
=> true
よし。明日の朝になったら終わるはずだ
∧∧
( ・ω・)
_| ⊃/(___
/ └-(____/
 ̄ ̄ ̄ ̄ ̄ ̄ ̄
スヤァ
<⌒/ヽ-、___
/<_/____/
次の日
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 67108872 bytes) in phar:///app/psysh/src/VarDumper/Dumper.php on line 43
Call Stack:
0.0023 412952 1. {main}() /app/psysh:0
0.0659 1813848 2. Psy\{closure:phar:///app/psysh/src/functions.php:128-207}() /app/psysh:155
0.1618 5455336 3. Psy\Shell->run() phar:///app/psysh/src/functions.php:203
0.1787 5665656 4. Psy\Shell->run() phar:///app/psysh/src/Shell.php:150
# ...省略...
ふぁ!???? そんな。。。。
(~)
γ´⌒`ヽ
{i:i:i:i:} ハッ!!!
( ´・ω・) キキーッ!
O┬O:::)
◎┴し'-◎ ≡
_,,..,,,,_
./ ,' 3 `ヽーっ
l ⊃ ⌒_つ
`'ー---‐'''''"
(~)
γ´⌒`ヽ
{i:i:i::} ちむどんどんしていない!!??
( ´・ω・)o,_
/::つi'" ,' 3 `ヽーっ
し─l ⊃ ⌒_つ
`'ー----'''''"
**ちむどんどんが止まってしまう(???)**かもしれません
さて、メモリエラーが発生しました。Process componentはひたすらコマンドの出力結果をメモリに保持するので出力結果が多いコマンドの場合、発生しうることだと思います。
今回は終わることのないコマンドですが、実際出力結果が多いコマンドだとどうすれば良いでしょうか。
phpのデフォルトは128MBなので、この上限を上げれば良いでしょうか。しかし、それだと省メモリの環境では扱えないので解決できません。なので、別途対応が必要です。
ネタバレすると、これもドキュメントに書いてあります! clearOutput()
と clearErrorOutput()
です。こいつらを使って、メモリに保持された出力を結果を定期的にclearしていきます。
>>> strlen($process->getOutput()
=> 131072
>>> $process->clearOutput()
>>> strlen($process->getOutput())
=> 131072
これでながーいコマンドの出力結果にも対応することができます!
ちなみに、最初からいらない!って振り切る場合は disableOutput()
すればおkです。(もちろんですが、この場合 getOutput()
や getErrorOutput()
は利用できなくなります)
>>> $process = new Process(['/bin/cat', '/dev/urandom'])
>>> $process->setTimeout(60 * 60)->disableOutput()->start()
=> null
>>> $process->getOutput()
Symfony\Component\Process\Exception\LogicException with message 'Output has been disabled.'
雑な感想
最初におもしろコマンドと連係して遊べるーとか想像してましたが、途中で色々諦めたので準備が半分無駄になった気持ち
以上ですー