symfonyのProcess componentで遊ぶ

はいさい

この記事は、["ちゅらデータアドベントカレンダー"](https://qiita.com/advent-calendar/2020/churadata)の19日目です。

## 概要

- symfonyのProcess componentで最近 
遊ぶ
 使う機会がありました
- 先に知っておけば楽だった的な内容です
- ただ、いまさらだけどやっぱり全部ドキュメントに載っていたので、詳しくはドキュメントよんでおくれ
    - https://symfony.com/doc/current/components/process.html


## 環境

```bash
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.7
BuildVersion:	19H15

$ docker --version
Docker version 19.03.13, build 4484c46d9d
```

## Dockerfileとimageを用意

シュッと環境を用意したいので、Dockerfileを用意します。

```bash
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する

```bash
$ docker build -t a-calendar-php .
```

コンテナ起動します

```bash
$ docker run --rm -v $(pwd):/app -it a-calendar-php /bin/bash

root@bb8dba6afa9a:/#
```

おｋ

## Process componentを試す

### 準備

**ちとながいです**

#### Process componentのインストール

composerでcomponentをインストールします

```bash
$ composer require symfony/process
```

そしたら動かすPHPファイルも用意


```bash
$ cat cmd1.php
```

```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ファイルを叩いてみる

```bash
# 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` じゃ面白みにかけるので面白そうなコマンドをインストールしてみる

文字、テキストのアスキーアート生成するコマンドをインストールしてみます。

```bash
# apt update -y && apt install -y figlet
# figlet churadata
      _                         _       _
  ___| |__  _   _ _ __ __ _  __| | __ _| |_ __ _
 / __| '_ \| | | | '__/ _` |/ _` |/ _` | __/ _` |
| (__| | | | |_| | | | (_| | (_| | (_| | || (_| |
 \___|_| |_|\__,_|_|  \__,_|\__,_|\__,_|\__\__,_|

```

helpをみてみます

```bash
# figlet --help
figlet: invalid option -- '-'
Usage: figlet [ -cklnoprstvxDELNRSWX ] [ -d fontdirectory ]
              [ -f fontfile ] [ -m smushmode ] [ -w outputwidth ]
              [ -C controlfile ] [ -I infocode ] [ message ]
```

ふむふむー


```bash
# figlet -w 30 churadata
      _
  ___| |__  _   _ _ __ __ _
 / __| '_ \| | | | '__/ _` |
| (__| | | | |_| | | | (_| |
 \___|_| |_|\__,_|_|  \__,_|

     _       _
  __| | __ _| |_ __ _
 / _` |/ _` | __/ _` |
| (_| | (_| | || (_| |
 \__,_|\__,_|\__\__,_|

```

fontも変更できるようです。

http://www.jave.de/figlet/fonts/overview.html

ここからダウンロードしてきましょう。


```bash
# wget http://www.jave.de/figlet/fonts/details/starwars.flf
# figlet -w60 -f starwars starwars
     _______.___________.    ___      .______
    /       |           |   /   \     |   _  \
   |   (----`---|  |----`  /  ^  \    |  |_)  |
    \   \       |  |      /  /_\  \   |      /
.----)   |      |  |     /  _____  \  |  |\  \----.
|_______/       |__|    /__/     \__\ | _| `._____|

____    __    ____  ___      .______          _______.
\   \  /  \  /   / /   \     |   _  \        /       |
 \   \/    \/   / /  ^  \    |  |_)  |      |   (----`
  \            / /  /_\  \   |      /        \   \
   \    /\    / /  _____  \  |  |\  \----.----)   |
    \__/  \__/ /__/     \__\ | _| `._____|_______/

# figlet -w60 -f starwars churadata
  ______  __    __   __    __  .______          ___
 /      ||  |  |  | |  |  |  | |   _  \        /   \
|  ,----'|  |__|  | |  |  |  | |  |_)  |      /  ^  \
|  |     |   __   | |  |  |  | |      /      /  /_\  \
|  `----.|  |  |  | |  `--'  | |  |\  \----./  _____  \
 \______||__|  |__|  \______/  | _| `._____/__/     \__\

 _______       ___   .___________.    ___
|       \     /   \  |           |   /   \
|  .--.  |   /  ^  \ `---|  |----`  /  ^  \
|  |  |  |  /  /_\  \    |  |      /  /_\  \
|  '--'  | /  _____  \   |  |     /  _____  \
|_______/ /__/     \__\  |__|    /__/     \__\

```

良いですね


#### `cmd1.php` を修正する

```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();
```

```bash
# php cmd1.php
      _                         _       _
  ___| |__  _   _ _ __ __ _  __| | __ _| |_ __ _
 / __| '_ \| | | | '__/ _` |/ _` |/ _` | __/ _` |
| (__| | | | |_| | | | (_| | (_| | (_| | || (_| |
 \___|_| |_|\__,_|_|  \__,_|\__,_|\__,_|\__\__,_|
```

はい。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;
```

```bash
# php cmd1.php
  ______  __    __   __    __  .______          ___
 /      ||  |  |  | |  |  |  | |   _  \        /   \
|  ,----'|  |__|  | |  |  |  | |  |_)  |      /  ^  \
|  |     |   __   | |  |  |  | |      /      /  /_\  \
|  `----.|  |  |  | |  `--'  | |  |\  \----./  _____  \
 \______||__|  |__|  \______/  | _| `._____/__/     \__\

 _______       ___   .___________.    ___
|       \     /   \  |           |   /   \
|  .--.  |   /  ^  \ `---|  |----`  /  ^  \
|  |  |  |  /  /_\  \    |  |      /  /_\  \
|  '--'  | /  _____  \   |  |     /  _____  \
|_______/ /__/     \__\  |__|    /__/     \__\

```


おｋ（長いへろーわーるどだった）


#### まだだ！まだおわらんよ


`// Usage` のところを毎回ファイル編集するのはめんどくさいので、REPLな何かを導入しましょう！


https://psysh.org/


```bash
# 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みたいな画面になりました。


するとですね。。。

```php
>>> require_once('cmd1.php');
  ______  __    __   __    __  .______          ___
 /      ||  |  |  | |  |  |  | |   _  \        /   \
|  ,----'|  |__|  | |  |  |  | |  |_)  |      /  ^  \
|  |     |   __   | |  |  |  | |      /      /  /_\  \
|  `----.|  |  |  | |  `--'  | |  |\  \----./  _____  \
 \______||__|  |__|  \______/  | _| `._____/__/     \__\

 _______       ___   .___________.    ___
|       \     /   \  |           |   /   \
|  .--.  |   /  ^  \ `---|  |----`  /  ^  \
|  |  |  |  /  /_\  \    |  |      /  /_\  \
|  '--'  | /  _____  \   |  |     /  _____  \
|_______/ /__/     \__\  |__|    /__/     \__\


=> 1
```

となります。

と、ここで、`Co`と入力後、tabキーを2回押してみてください。

```php
>>> Co
Command                                                               Composer\Autoload\Composer  # ...省略...
```

なんと！先程用意した `Command` classが使えるようになっています！

というわけで、ここからインタラクティブに操作できます

```php
>>> $cmd = new Command()
>>> $process = $cmd->setHelpOption()->run()
>>> $process->getOutput()
=> ""
```

あれ・・・・？ と思いましたが、shellで確認してみると


```bash
# 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()` で取得することができます。

```php
>>> $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()` で取得することができます。

```php
>>> $cmd = new Command()
>>> $process = $cmd->setDisplayWord('hogehoge')->run()
>>> $process->getOutput()
=> """
    _                      _                      \n
   | |__   ___   __ _  ___| |__   ___   __ _  ___ \n
   | '_ \ / _ \ / _` |/ _ \ '_ \ / _ \ / _` |/ _ \\n
   | | | | (_) | (_| |  __/ | | | (_) | (_| |  __/\n
   |_| |_|\___/ \__, |\___|_| |_|\___/ \__, |\___|\n
                |___/                  |___/      \n
   """
```

### コマンドのエラー結果取得

なんらかの操作でコマンドがエラーを出力した場合は `getErrorOutput()` で取得することができます。

エラーハンドリングがなっていないコードなので、こうするとコマンドがエラーとなります。

```php
>>> $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` コマンドを実行してみます。

```php
>>> require_once "vendor/autoload.php"
>>> use Symfony\Component\Process\Process
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->run()
# ... 待たされる ...
=> 0
```

`run()` で実行すると、同期処理されるのでコマンドの結果が終了するまで待機させられます。バックグラウンドで実行する場合は `start()` を利用しましょう。

```php
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->start()
=> null
```

### コマンドのステータスを確認する

さて、バックグラウンドに処理を回すことができたら、コマンドが終了したかどうかはどう確認すれば良いでしょうか。

`isRunning()` で確認することができます

```php
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->start()
=> null
>>> $process->isRunning()
=> true

# ...10秒後...
>>> $process->isRunning()
=> false
```

実際にサービスで利用する場合は `while($process->isRunning())` のようにすると良いでしょう。


また、`start()` で実行した場合でも待ちたい場合は `wait()` という関数が利用できます。

```php
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->start()
=> null
>>> $process->wait()
=> 0
```

また、見ての通り、コマンドが正常終了したかどうかは `run()`、`wait()` ともに、返り値で確認することができます。


コマンドの開始前、実行中、終了のステータスは `getStatus()` で取得が可能です。

```php
>>> $process = new Process(['/bin/sleep', '10'])
>>> $process->getStatus()
=> "ready"
>>> $process->start()
=> null
>>> $process->getStatus()
=> "started"
>>> $process->wait()
=> 0
>>> $process->getStatus()
=> "terminated"
```

### timeout

実行したコマンドのタイムアウト値を設定することも可能です。デフォルトで60sです。

```php
>>> $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.'
```

処理に時間がかかるコマンドは、この値を長めに修正しましょう。

### 長い期間出力し続けるコマンドの注意点

数十分間動き続ける + その間ログを出力し続けるタイプのコマンドを想定した場合の注意点です。

例えば、下記コマンドを実行するとターミナルの画面が大変なことになります。

```bash
# /bin/cat /dev/urandom
```

要は延々と乱数文字列を出力するコマンドなのですが、こいつをProcess componentで実行してみてみようと思います。


例えばこれを時間がかかるバッチコマンドだとしましょう。

```php
>>> $process = new Process(['/bin/cat', '/dev/urandom'])
=> Symfony\Component\Process\Process {#2595}
>>> $process->setTimeout(60 * 60)->start()
=> null
>>> $process->isRunning()
=> true
```

よし。明日の朝になったら終わるはずだ

```
　　　∧∧
　　（　･ω･）
　 ＿|　⊃／(＿＿_　
／　└-(＿＿＿_／
￣￣￣￣￣￣￣

ｽﾔｧ
　 ＜⌒／ヽ-、_＿_
／＜_/＿＿＿＿／
```


次の日


```php
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::}　ちむどんどんしていない！！？？
　　　　　（　´・ω・）ｏ,_
　　　　　 /::つi'" ,' 3　 `ヽｰっ
　　　　 　し─l　　 ⊃　⌒_つ
　　　　　　　　`'ｰ----'''''"
```

**ちむどんどんが止まってしまう（？？？）**かもしれません

さて、メモリエラーが発生しました。Process componentはひたすらコマンドの出力結果をメモリに保持するので出力結果が多いコマンドの場合、発生しうることだと思います。

今回は終わることのないコマンドですが、実際出力結果が多いコマンドだとどうすれば良いでしょうか。

phpのデフォルトは128MBなので、この上限を上げれば良いでしょうか。しかし、それだと省メモリの環境では扱えないので解決できません。なので、別途対応が必要です。

ネタバレすると、これもドキュメントに書いてあります！ `clearOutput()` と `clearErrorOutput()` です。こいつらを使って、メモリに保持された出力を結果を定期的にclearしていきます。

```php
>>> strlen($process->getOutput()
=> 131072
>>> $process->clearOutput()
>>> strlen($process->getOutput())
=> 131072
```

これでながーいコマンドの出力結果にも対応することができます！


ちなみに、最初からいらない！って振り切る場合は `disableOutput()` すればおｋです。（もちろんですが、この場合 `getOutput()` や `getErrorOutput()` は利用できなくなります）


```php
>>> $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.'
````

## 雑な感想

最初におもしろコマンドと連係して遊べるーとか想像してましたが、途中で色々諦めたので準備が半分無駄になった気持ち


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101690/208420aa-463e-8ed6-764d-b396457b2804.png)


以上ですー