【systemd】Pythonでstopを検知する&例外時に自動で再起動する

2020.11.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

カフェチームの山本です。

現在カフェで使用している機器(Jetson Xavier NX)では、OS起動時にプログラムを自動実行するためにsystemdを利用しています。

今回は、プログラムを終了する際に起きた問題点とその解決方法、また、それらを含めたエラー処理についてまとめました。プログラムはPythonで実装されています。

問題点

systemdでプログラムを起動すると、バックグラウンドプロセスになってしまい、ctrl + Cなどの操作ができません。そのため、サービスを止めるにはsystemdのstopコマンド(sudo systemctl stop SERVICE_NAME)を利用する必要があります。

しかし、stopコマンドを利用した場合、プログラムのプロセスに送られるシグナル(SIGTERM)は通常の中断シグナル(SIGKILL)と異なるため、Pythonプログラムそのままだと捕捉できず、except節が実行されません(これにより、プログラム終了時にリソースの開放などができず、再度起動した際にエラーになる場合があります)。

実現したい動作

上記の問題の解決に加え、プログラムで起きたエラー対処として、以下のように処理する必要がありました。次節でこの実装方法について述べます。

  • (1):ctrl + C (KeyIntterupt)→ リソースを開放し、通常通り終了する
  • (2):systemd stop → リソースを開放し、通常通り終了する
  • (3):その他のエラー(既知) → 個別に対処する
  • (4):その他のエラー(未知) → エラー(例外)として処理し、プログラムを再起動する

実装したコード

以下のように、2ファイルを実装(設定)しました。

.serviceファイル

systemdに登録するサービスの設定ファイルです。

[Unit]
Description=service to lanuch skeleton detection program on jetson

[Service]
WorkingDirectory=/home/USER/
ExecStart=/bin/sh /home/USER/launch_jetson_program.sh
User=USER
Restart=on-failure

[Install]
WantedBy=default.target

Restart=on-failureを付け加えることで、プログラムがエラーで終了した際に、systemdが自動でプログラムを再起動するようになります。

詳しくは、以下をご参照ください。

Ubuntu Manpage: systemd.service - Service unit configuration

Pythonスクリプト

プログラムを以下のように実装しました("# (preprocess)" や " # (execute process)" は、プログラムの処理が書かれていた箇所ですが、今回は解説のために省略しています)。今回は、動作(3)として、ネットワークエラーが起きた際の処理を書きました。

import signal
import socket

class TerminatedExecption(Exception):
    pass

def raise_exception(*_):
    raise TerminatedExecption()

if __name__ == "__main__":
    # set signal handler to detect to be stopped by systemd
    signal.signal(signal.SIGTERM, raise_exception)

    # (preprocess)    

    # execute
    try:
        # (execute process)

    # (1) if ctrl-C is pushed, stop program nomally
    except KeyboardInterrupt:
        print("KeyboardInterrupt: stopped by keyboard input (ctrl-C)")

    # (2) if stopped by systemd, stop program nomally
    except TerminatedExecption:
        print("TerminatedExecption: stopped by systemd")

    # (3) if error is caused with network, restart program by systemd
    except OSError as e:
        import traceback
        traceback.print_exc()

        print("NETWORK_ERROR")

        # program will be restarted automatically by systemd (Restart on-failure)
        raise e

    # (4) if other error, restart program by systemd
    except Exception as e:
        import traceback
        traceback.print_exc()

        print("UNKNOWN_ERROR")

        # program will be restarted automatically by systemd (Restart on-failure)
        raise e

stopコマンド(SIGTERM)への対応

main関数の最初で、SIGTERMを受信した際の動作を、raise_exception関数を呼び、TerminatedExecptionをraiseするように変更しました。これにより、stopコマンドを例外として処理できるようになりました。

各エラー処理への対応

  • 動作 (1)・(2):受け取った例外をraiseせずに終了します。これにより、プログラムが正常終了するため、systemdはプログラムを再起動しません(開発中など、意図的にプログラムを止めた場合に再起動すると不便なため、このような設定にしました)。
  • 動作 (3)・(4):受け取った例外をraiseします。これにより、プログラムがエラーで終了するため、systemdはプログラムを再起動します。

まとめ

systemdを利用した場合、Pythonのsignal処理の変更することで、stopコマンドを例外として処理できるようになりました。また、serviceファイルのRestartを利用することで、エラー時にプログラムを再起動するようになりました。

参考にさせていただいたページ・サイト

Ubuntu Manpage: systemd.service - Service unit configuration

systemdでstopした時にpythonのfinallyが呼ばれない問題について - dothikoのカクカクワールド2D REBOOT