LoginSignup
8
6

More than 3 years have passed since last update.

symfonyのProcess componentで遊ぶ

Last updated at Posted at 2020-12-18

はいさい

この記事は、"ちゅらデータアドベントカレンダー"の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.'

雑な感想

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

image.png

以上ですー

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6