systemdでバックグラウンド動作するpythonスクリプトを作る

# はじめに

ラズパイでベビーモニタのような監視システムを作っているのですが、妻が利用することを想定しているため、電源ONで実行されて電源OFFまで起動しっぱなしという仕様になりそうです。

pythonで実装中ですがデーモン動作する仕組みはsystemdがよさそうなので、pythonをsystemdから呼び出す方法を調べてみました。

以下２点について、まとめていきます。

 - シグナルを受け取り正しく終了処理が行えるpythonスクリプト
 - systemdへの登録


## 環境
 - Ubuntu or Raspbian
 - Python 3.5.2


# シグナルの受信

想定する運用ではいきなり電源OFFする予定なので、シグナルを受けてからの終了処理はできないですが、サービス再起動する仕組みはデバック時に使うから無駄にはならないはず。

シグナルの受信を解説したサイトやサンプルは結構あるようですので、ぐぐりながら作ってみました。

ざっくりと以下のようなポイントに気を付けて作りました。

- signal生成時に渡した関数が呼ばれるが、その関数内で複雑な処理はしない
- バックグラウンドで行う処理は別スレッドにしておきeventで通知する
- スレッド内で終了処理を行うようにする
- スレッドの終了を待機してからメインの処理を終了する


```python:service_template.py

import signal
import threading
import os

class service():
    '''デーモンとして動作させるためのサービスクラス
       event: 停止を通知するためのイベント(threading.Event)
       call_function: サービスから呼び出すメソッド
    '''

    def __init__(self, event, call_function):
        '''コンストラクタ'''
        self.event = event
        self.call_function = call_function

        self.pid = os.getpid()
        self.thread = threading.Thread(target=self._thread_function)

        signal.signal(signal.SIGTERM, self._trem_handler)

    def start(self):
        '''処理の開始'''
        self.thread.start()

    def stop(self):
        '''処理の停止'''
        self.event.set()

    def wait(self):
        '''スレッド終了待ち'''
        self.thread.join()

    def _trem_handler(self, signum, frame):
        '''シグナルのハンドラ'''
        self.stop()

    def _thread_function(self):
        '''渡されたメソッドを呼び出すスレッド'''
        self.call_function()

    def save_pid(self, pid_file_path='./pid'):
        '''pidファイル保存'''
        with open(pid_file_path, mode='w') as pid_file:
            pid_file.write("{}".format(self.pid))


if __name__=='__main__':
    # service動作確認用hogeクラス
    # 実行したい処理をfuga()メソッドに記述する
    class hoge():
        def __init__(self, event):
            self.event = event

        def fuga(self):
            index = 0
            # 停止要求を受信した場合、event.wait()がTrueを返す
            while not self.event.wait(1):
                # タイムアウト１秒でループ内の処理を行う
                index += 1
                print("hoge.fuga() index={}".format(index))

    # 停止要求を送受信するためのイベントを生成
    stop_event = threading.Event()

    # 実行したい処理fuga()を持つhogeインスタンスを生成
    hoge_object = hoge(stop_event)

    # hoge.fuga()をバックグラウンドで実行するためのserviceインスタンス生成
    service_test = service(stop_event, hoge_object.fuga)
    # 外部にpidを渡すためにpidをファイルに保存する
    service_test.save_pid()
    # start()するとスレッドでhoge.fuga()が実行される
    service_test.start()
    # スレッドの終了待機
    service_test.wait()

```

これを/opt/にコピーしておきます。


### 実行
```
$ python3 service_template.py
hoge.fuga() index=1
hoge.fuga() index=2
hoge.fuga() index=3

※繰り返し
```

書き込み権限不足でpidファイルが生成できない場合は、書き込み権限を付けてください。

```
$ sudo touch ./pid
$ sudo chmod o+w ./pid
```


### 停止

停止するときは別なターミナルで以下を実行すれば繰り返しの処理が停止します。

```
$ kill $(cat ./pid)
```


# systemdへの登録

OS起動時にスクリプトが動作するようにsystemdへの登録を行います。

手順としては以下の３ステップ
1. 登録したいスクリプトを作成
2. serviceの記述
3. systemdのコマンドで登録


### サービスのスクリプト

上で作成したserviceクラスを使用してサービスの処理を実装します。
Shebangを付ける必要があるので/usr/bin/python3を指定しておきます。

```python:service_sample.py
#!/usr/bin/python3

import threading
from service_template import service

class service_sample():
    def __init__(self, event):
        self.event = event

    def test_function(self):
        index = 0
        # 停止要求が来るまでループ
        while not self.event.wait(1):
            # ここに処理を記述
            index += 1
            print("service_sample.test_function() index={}".format(index))
        # ここに終了処理を記述
        print("service_sample.test_function() - exit")


if __name__=='__main__':

    event = threading.Event()

    service_main = service_sample(event)

    service_object = service(event, service_main.test_function)

    # service_sample.serviceファイル [Service] PIDFile のパスに合わせる
    service_object.save_pid('/opt/service_sample.pid')

    service_object.start()
    service_object.wait()

```

こちらも/opt/の下に配置し、実行権限を付けておきます。

### 実行権限の設定

```
$ sudo chmod +x service_sample.py
```

### 実行
```
$ python3 service_sample.py
service_sample.test_function() index=1
service_sample.test_function() index=2
service_sample.test_function() index=3

※繰り返し
```

### 停止
```
$ kill $(cat ./service_sample.pid)
```

### 実行側で終了のログが出力されることを確認する
```
service_sample.test_function() - exit
```

## serviceファイル

systemdに登録するにはserviceファイルが必要になります。

以下の２つのパスが一致する必要があります

 - PIDFileに指定したファイルのパス
 - python側で生成するpidが記載されたファイルのパス（save_pidメソッドの引数）


```/usr/lib/systemd/system/service_sample.service

[Unit]
Description=service_sample

[Service]
ExecStart=/opt/service_sample.py
Restart=always
PIDFile=/opt/service_sample.pid

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


## systemdのコマンドで登録

以下のコマンドで登録します。

１．serviceファイルを認識させる

  - serviceファイルのコピー後にリロードする。

```
$ sudo systemctl daemon-reload
```

２．serviveの動作確認（開始、停止）

  - serviveの書式やスクリプトが間違ってないか、開始、停止を行い確認する
  - エラーにならないこと、終了時に正しく終了しているか等を確認します

```
開始
$ sudo systemctl start service_sample.service
停止
$ sudo systemctl stop service_sample.service
状態確認
$ sudo systemctl status service_sample.service
```

３．serviceをenableにしてOS起動時に立ち上がるようにする

```
有効化
$ sudo systemctl enable service_sample.service
状態確認
$ sudo systemctl status service_sample.service

再起動
$ sudo reboot

再起動後に状態を確認
$ sudo systemctl status service_sample.service
```

OS起動時に処理が実行されていることが確認できました。

自動起動をやめる場合

```
無効化
$ sudo systemctl disable service_sample.service
```

# 課題

家庭内のLAN内で使用するラズパイなので実行ユーザーや権限はあまり気にしていません。
サーバーで動作させる場合はユーザー指定で起動するなどのセキュリティ対策が必要になります。
serviceファイルでユーザー、グループ指定は可能のようですが未確認です。


# おわりに

pythonスクリプトのデーモン化のテンプレートに使えるようにsystemdの登録とシグナル受けての終了処理を繋げてまとめてみました。

systemdに登録するのは初めてやりましたが、思ったよりシンプルですね。もっと早くに仕組みを理解しておけばよかった。cronから定期実行しているスクリプトの一部は、systemdに移行して独自の定期実行したほうが管理しやすくなる気がする。


# 参考

[pythonをデーモン化するメモ](https://qiita.com/croquisdukke/items/9c5d8933496ba6729c78) ※コメント欄も大変参考になりました
[systemdの*.serviceファイルの書き方](https://qiita.com/masami256/items/ef0f23125cf8255e4857)