モブ沢工房

プログラミングとかLinux関連(特にOSSのグラフィックツール関連)とかレトロゲームとか3Dプリンタやら日曜大工等、色々。

systemdでstopした時にpythonのfinallyが呼ばれない問題について

systemdでsystemctl stop hoge.serviceすると、基本的にSIGTERM -> SIGKILLが送られてdaemonプロセスが終了します。

しかしながら、pythonのfinally節はSIGTERMで終了した場合は呼びだされません。 そのため、pythondaemonを作った時、finallyで必ず呼ばれることを期待して記述された終了処理は呼ばれずに終わるという…

これでハメられた…まぁ具体的にはRaspberry PiのGPIOをお手軽にRPi.GPIOでadd_event_detectしていたのですが、サービス化していたので何も考えずにstopしたら、テストでRuntimeErrorを吐くようになってしまいました。

finallyでcleanup()を呼んでたので、SIGTERMで終了時は呼ばれなかった結果、プロセス終了後も握り続けているのか?こうなった

dothiko@raspberrypi:~/python/usbcamcap $ ./usbcamcap.py -p 12345 -f camsconf.json 
- GPIO pin 4 as Button, 3 as LED.
./usbcamcap.py:296: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  gp.setup(LED, gp.OUT)
Traceback (most recent call last):
  File "./usbcamcap.py", line 439, in <module>
    camera = Camera(args.cameras, confs)
  File "./usbcamcap.py", line 204, in __init__
    self.init_gpio()
  File "./usbcamcap.py", line 298, in init_gpio
    gp.add_event_detect(BTN, gp.BOTH, callback=self._gpio_callback)  
RuntimeError: Failed to add edge detection

ちな、このusbcamcap.pyというのは自作のUSBカメラキャプチャスクリプトで、カメラを握り続けてhttpでリクエストした時に画像を送るものです。んでもってこの中で同時にGPIOでボタンを見ていて、ボタンが押されるとシャットダウンするようにしています。 普段はラズパイを監視カメラとして使い、整備・移動する時はボタンでシャットダウンして電源断という目論見です。

最近のネットカメラはゴミばかりあまり気に入ったものがなく、スマホでアプリというものばかり。それもお手軽で良いのでしょうが外部のサーバや専用アプリを必要としたりして、しかもそれがOSのバージョンに相性があったりするなど、使い勝手が実に良くない。 もはや自分的には監視カメラはラズパイで自作する他無いという感じです。

話がそれてしまいました。

ともかく、どうすれば直るのか悩みましたがあっさり諦めてリブートしました(^^;

んで調べた結果、対策は2つあって

  • serviceファイルのservice節でKillSignal=SIGINTを記述して、syetemdのシグナルを変える。
  • python側でSIGTERMを捕捉してsys.exit()を呼び出す

後者の方はsignalモジュールでSIGTERMにハンドラを付けて、その中からsys.exit()をコールするわけです。これでsys.exit()がfinallyを呼んでくれるので問題がなくなります。

以下参考用のテストプログラム

import time
import signal
import sys

def termed(signum, frame):
    print("SIGTERM!")
    sys.exit(0)

def main():
    signal.signal(signal.SIGTERM, termed)
    try:
        while True:
            print('loop')
            time.sleep(3)
    finally:
        print("CALL FINALLY")

if __name__ == '__main__':
    main()

まぁどっちでもいいんですけど、後でうっかりパターンを考えるとSIGTERMを捕捉したほうがいいかな?