Pythonプログラムのcattコマンドを高速化する

## cattとは

[catt](https://pypi.org/project/catt/)はPythonで書かれたプログラムで、指定したURLの音楽・動画・WebページをGoogle Chromecast/Home/Nest Hubで再生・表示させることができます。
Raspberry Piからcattコマンドを実行することで、Google Homeに音声メッセージを再生させたり、Nest Hubに好きなWebページを表示させたりできるので、DIYスマートホームシステムを作るのに役立ちます。

## cattの問題点 - 起動が遅い

高機能なのですが、起動がかなりもっさりしています。ボトルネックになっているのはPhtonモジュールのimportで、以下のようにRaspberry Pi4でも一秒以上。Raspberry Pi3では2秒弱の時間がかかっています。通常の動画再生ならそれほど気になりませんが、スマートホーム用に使うとなると1秒でもレスポンスを早くしたいところです。

```
PYTHONPROFILEIMPORTTIME=1 catt -d 192.168.x.xxx cast_site https://www.google.com
：
import time:      1652 |     795904 |       youtube_dl
import time:       976 |     796880 |     catt.stream_info
import time:      2753 |    1213944 |   catt.controllers
import time:      1570 |       1570 |   catt.http_server
import time:       711 |        711 |   catt.subs_info
import time:      6028 |    1296239 | catt.cli
```

## 高速化の方法

## cattコマンド相当のサービスをPythonで作成する 

importでの起動待ち時間の影響をなくすために、catt相当のサービス(Pythonで記述)をあらかじめ立ち上げておくことにします。コマンドラインのオプション処理を自前で行う必要があるかと思ったのですが、`click.testing.CliRunner`というテスト用のメソッドを使って、catt本来のコマンドラインオプション処理を流用することができました。

以下が作成したサービスプログラムです。port 50007に、cattコマンドと同じコマンドラインパラメータを送信すると処理してくれます。

なお、終了時に子プロセスを終了させるため、psutilモジュールを使用しています。`pip3 install psutil`でインストールをお願いします。

:::note info
2023/03/08追記
子プロセスの終了をハンドリングしてなかったせいで、Zombieプロセスが発生する不具合を修正しました。
:::

```python:~/pi/bin/catt_service.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# catt_service : Service to launch catt

import os
import socket
import signal
import sys
import syslog
import traceback

import psutil

from click.testing import CliRunner
from catt.cli import get_config_as_dict
from catt.cli import cli

def onexit(signum, stack):
    syslog.syslog('stopped')
    for child in psutil.Process(os.getpid()).children():
        child.kill()
    sys.exit(signum)

def sigchld_handler(signum, stack):
    #syslog.syslog(syslog.LOG_DEBUG, 'Got SIGCHLD')
    while True:
        try:
            pid, status = os.waitpid(-1, os.WNOHANG)
            if pid <= 0:
                break
        except ChildProcessError:
            break
    #syslog.syslog(syslog.LOG_DEBUG, 'SIGCHLD hadled')

HOST = ''                 # Symbolic name meaning all available interfaces
PORT = 50007              # Arbitrary non-privileged port

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(5)

syslog.syslog('started. port=' + str(PORT))

while True:
    syslog.syslog('waiting connection...')
    conn, addr = s.accept()
    syslog.syslog('connected from' + str(addr))

    pid = os.fork()

    if pid != 0:
        signal.signal(signal.SIGHUP, onexit)
        signal.signal(signal.SIGINT, onexit)
        signal.signal(signal.SIGQUIT, onexit)
        signal.signal(signal.SIGTERM, onexit)
        signal.signal(signal.SIGCHLD, sigchld_handler)
    else:
        try:
            runner = CliRunner()
            #conn.setTimeout(3)
            data = conn.recv(1024)
            conn.close()

            if data:
                syslog.syslog('args: ' + data.decode('utf-8'))
                #args = data.decode('utf-8').split()
                result = runner.invoke(cli, data.decode('utf-8'), obj=get_config_as_dict())
                syslog.syslog('catt exit code {0:d}, output: {1}'.format(result.exit_code, result.output))
            else:
                pass# syslog.syslog(syslog.LOG_INFO, 'no data received')

            sys.exit(0)

        except Exception as e:
            syslog.syslog(syslog.LOG_ERR, 'exception:' + traceback.format_exc())
            conn.close()
            sys.exit(1)
```

## サービスの登録と実行

- serviceファイルの作成

```/usr/lib/systemd/system/catt.service
[Unit]
Description=Catt - Cast All The Things - Service

[Service]
ExecStart=/home/pi/bin/catt_service.py
KillMode=process
Restart=always

[Install]
WantedBy=multi-user.target
```

- catt serviceを有効化する

```
sudo systemctl daemon-reload
sudo systemctl enable catt.service
sudo systemctl start catt.service
```

- catt serviceが実行されていることを確認

```
sudo systemctl status catt.service
```

## 動作確認

- まずnetcatをインストール

```
sudo apt-get -y install netcat
```

- port 50007 にコマンドライン内容を送信。以下の例ではNest HubにGoogleのホームページを表示させて見ます。

```
nc localhost 50007
-d <Google Nest HubのIPアドレス> cast_site https://www.google.co.jp
```

## Node-REDのtcp-outノードで使う際の注意

自分の場合、実際にはnode-redのtcp outノードを使ってGoogle Nest HubへWebページの表示リクエストを行ってます。

このtcp outノードを使う際は、ノードのPropertyの`Close connection after each message is sent?` をチェックして使用してください。そうしないと、2回目以降のリクエストが無視されます。