GoogleHomePlayerを非同期実行させる

# １．はじめに

　ジュークボックスサーバーを造ったのですが、言っても再生リスト順に再生開始、停止させているだけです。ただ、リクエスト元のgoogleAssistantからはジュークボックスの状態とは（だいたい）無関係に要求を投げて来るので、適時応答を返してあげる必要があります。
　その仕掛けとしてフルフィルメントとジュークボックスの間にキューを置いて、同期しないようにしています。
　ジュークボックスは定期的にキューを参照してリクエストがあればその要求に応じた動作をする――わけですが、キューの参照間隔（下図の①）が長いとレスポンスが悪くなり、キューサイズも有限ですからパンクする可能性が高まります。
　キューの参照間隔を大きく左右するのはgoogleHomePlayerの実行動作になります。そもそも１曲の再生が終わるまで次の曲に移れません。実装では無駄にＣＰＵ利用率が上昇しないように１秒以下のスリープを入れていたりするのですが、曲の再生では３分くらいはかかるのでスパンが２桁ほど違います。
　つまり、以下２点を実装する必要があります。
１）曲の再生終了は拾わなければならない
２）キューの読み込みサイクルは曲の再生長以下にしなければならない
　ここまで長く書いてきましたが、ぶっちゃけ子プロセスの非同期実行管理が必要になったということで、本稿はその実装について説明する内容となります。
![asyncPlay.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2696131/bac4a527-1070-30d2-3f7e-ead6eb524ab3.png)

# ２．ＰＨＰでのＦｏｒｋ＆Ｅｘｅｃ

## ２．１．子プロセスの非同期実行
　ＰＨＰで子プロセスを実行するには`exec()`関数や`system()`関数などがありますが、どちらも実行した子プロセスが終了するまで後続処理ができません。それはそれで有用な機能ですが、今回の用途には合いません。
　非同期実行させるためには、古典的な`fork()`＆`exec()`を行う必要があります。ＰＨＰでは`pcntl_fork()`と`pcntl_exec()`で実現されます。`fork()`が呼ばれると、その時点の実行状態のまま自プロセスの複製が子プロセスとして生成されます。オリジナルのプロセスか、複製プロセスかは`fork()`(`pcntl_fork()`)関数の戻り値で識別できて、オリジナル（親プロセス）であれば複製プロセスのプロセスＩＤ（ＰＩＤ）、複製プロセス側では０が返されます。
　`exec()`(`pcntl_exec()`)が呼ばれると、呼び出したプロセスは引数で与えた実行ファイルにＰＩＤはそのままで切り替わります。
　今回はジュークボックスサーバーがGoogleHomePlayerをバックグラウンドプロセスとして実行させるために使います。

## ２．２．子プロセスの終了監視
　Fork&Execで実行された子プロセスの終了監視には`pcntl_wait()`か`pcntl_waitpid()`が使えます。使い分けに悩みますが、今回は子プロセスが単体なので丁寧に`pcntl_waitpid()`を使うことにしました。関数の名前からして指定したＰＩＤのプロセスが実行終了するまで待たせるもののようですが、今回は待ちに入って実行が止まるのは困るので`WNOHANG`フラグを指定します。
　ただ、問題は返り値の扱いです。[ＰＨＰのドキュメント](https://www.php.net/manual/ja/function.pcntl-waitpid.php)には次のようにあります。
> pcntl_waitpid() は、終了した子プロセスの プロセス ID を返します。エラーの場合は -1、WNOHANG が使用され、 子プロセスが利用できない場合に 0 を返します。

　そのため、戻り値に子プロセスのＰＩＤを期待して実行中かどうかの判定を行ったところ、曲が次々切り替わって行きます。`$status`に終了ステータスが格納されるとあるのですが、設定はされていないようです。
　どういう動きをするのかドキュメントからは読み切れなかったので、確認のためにfork&execの後に１秒間隔で`pcntl_waitpid()`を呼び出してみたところ、実行中は０、終了直後あたりでＰＩＤが戻り、それ以降はー１が戻りました。今回は実行中かどうかを知りたいので、戻り値が０かどうかで真偽判定するようにしました。

## ２．３．実装
　ジュークボックスから見るとGoogleHomePlayerはオブジェクトとして振る舞うことになるので、クラスとして扱うようにしました。
```
class childProcess {
  public  $pid;
  public  $argv;
  
  public function __construct(){
    $this->preExec();
  }
  
  public function exec($dst, $mp3Path){
    $this->argv = array(gpd_playModuleJs, $dst, $mp3Path);
    if(($this->pid = pcntl_fork()) == 0){
      pcntl_exec("node", $this->argv);
    } 
  }

  public function is_running(){
    if($this->pid == 0) return false;
    $rc = pcntl_waitpid($this->pid, $status, WNOHANG);
    if($rc == -1){
      $this->preExec();
    }
    return ($rc == 0);
  }
  
  public function preExec(){
    $this->pid = 0;
  }
  
  public function kill(){
    if($this->pid > 0){
      posix_kill($this->pid, SIGKILL);
      $this->preExec();
    }
  }
}
```
　実行本体は`childProcess::exec()`で、引数として再生対象のgoogleHomeのIPアドレスとmp3のURIを与えるようにしています。
　実行中かどうかの判定は`childProcess::is_running()`です。他には実行中のGoogleHomePlayerを停止する`childProcess::kill()`もありますが、説明は不用だと思います。ただ、楽曲再生そのものはgoogleHomeで行われているのでプロセスを停止しても再生は続きます。再生を止めるには前稿で触れましたが無音のmp3を再生させるのが手っ取り早いように思います。

# ３．おわりに
　自分だけで使う分にはpcntl_fork()&pcntl_exec()ではなくexec()だけでも十分使えるのですが、動作確認のために一度再生を始めると曲が終わるまで次に移れないのでそれはそれでストレスになります。
　それと、どこにも説明していませんが、ジュークボックスに再生させるmp3のパスを次々渡し、ジュークボックス内部に再生リストを作成する機能を持たせているのですが、再生中にその機能が使えないのは地味に不便でした。
　ジュークボックスは最終的にはsystemdからサービス起動できるようにするつもりで、その時にpcntl_fork()がまた出て来るはずですが、そちらはあまり特筆するようなことは無い様に思っています。