daemon=悪魔にはもうさせない

# 始めに
こんにちは、美味しいしです。
[python advent calender2020](https://qiita.com/advent-calendar/2020/python)に何とか間に合わせることができました！
[「私、デスクトップが汚い人とはお付き合いしたくないの」と言われないために](https://qiita.com/pn8128/items/58533224bb8c6d7ceea0)
という記事でpythonを使ったスクリーンショット自動化ライブラリを公開していますので、そちらもぜひご覧いただければと思います。

## そもそもdaemon化って？
デーモン化と読みます。悪魔化ではありません。daemonは英語で守護神をさし、守護神みたいにいつも動いてくれるものです。
メーラーデーモンとかよく聞きますよね。
でもこいつは、自前で作ろうとすると結構悪魔化します。
typoしてるだけのエラーに引っかかってサーバーを落としたり、存在しないurlを叩き続けてサーバーを落としたり、sudoじゃないと動かないコマンド関数を呼ぼうとしてそいつらもろともサーバーを落としたり...
やっぱり悪魔じゃないかと思うんですが、悪魔さんにもう一度守護神に戻ってもらうべく奮闘してみました。

### 困った点1:とにかく予期しないエラーの時にサーバーやパソコンが落ちる。
解決法：複数回連続でエラーが起こったらアプリケーションを落とすようにする。
結構単純な話でした。無限ループに陥らないようにしてあげればいいだけでしたね。

そこらへんを解決ずみのコードがこちら。
[@KosukeJin様の記事](https://qiita.com/KosukeJin/items/e626ddda850aa8407cee)を大半は参考にさせていただきました。誠にありがとうございました。

```python
import sys
import time
from datetime import datetime, timedelta
import daemon
import daemon.pidfile

__all__ = ['start_daemon', 'log_to_stdout', 'interval_run']


def log_to_stdout(*msg, end="\n", date=True):
    '''ログを標準(&エラー)出力

    Parameter
    ------------------------------
        *msg: object(ログ内容)
        end: str(print末尾の文字)

    Return
    ------------------------------
    '''
    if end is None:
        end = ""
    if date:
        out = str(datetime.now()) + " "
    else:
        out = ""
    for m in msg:
        out += f"{m} "
    sys.stdout.write(out + end)
    sys.stdout.flush()
    sys.stderr.flush()


def interval_run(interval):
    '''interval分ごとに関数実行
    intervalに1または60を割り切れない数を入れるとバグる(すいません)

    Parameter
    ------------------------------
        interval: int(実行間隔 -分)

    Return
    ------------------------------
        function(デコレーション後の関数)
    '''
    def _deco(f):
        def _wrapper(*args, **kws):
            while True:
                # 現在時刻と次回起動時刻
                t = datetime.now().replace(second=0, microsecond=0)
                n = t + timedelta(minutes=interval)

                # intervalの倍数ジャストに起動
                if t.minute % interval == 0:
                    f(*args, **kws)

                # 1周目 次回実行時間まで調整
                else:
                    stop = stop = interval - t.minute % interval
                    n = t + timedelta(minutes=stop)

                w = (n - datetime.now()).total_seconds()
                log_to_stdout('[INFO]', 'Sleep', w, 'seconds.')
                time.sleep(w)

        return _wrapper
    return _deco


def start_daemon(pf, maxRepeat=10, errorsleep=10):
    def wrapper(func, *args, pidpath=None, logpath=None, **kws):
        '''対象の関数をデーモン化
        SIGTERMで終了 関数内でのエラーでは停止しない(1分sleep)

        Parameter
        ------------------------------
            func: function(デーモンにしたい関数)
            *args: list(funcで使う引数)
            pidpath: str(PIDファイルパス -default:fg)
            logpath: str(ログファイルパス -default:stdout)
            **kws: dict(funcで使うキーワード引数)

        Return
        ------------------------------
        '''

        # 多重起動防止
        if pidpath:
            pid = daemon.pidfile.PIDLockFile(pidpath)
            if pid.is_locked():
                raise Exception('Process is already started.')

        # PID指定がなければフォアグラウンド
        else:
            pid = None

        # ログ
        if logpath:
            std_out = open(logpath, mode='a+', encoding='utf-8')
            std_err = std_out

        # 指定がなければ標準出力
        else:
            std_out = sys.stdout
            std_err = sys.stderr

        dc = daemon.DaemonContext(
            umask=0o002,
            pidfile=pid,
            stdout=std_out,
            stderr=std_err,
        )

        # 内部で実行される
        def forever():
            pf("*Python daemon Started*")
            repeat = 0
            while True:
                try:
                    func(*args, **kws)

                # kill -SIGTERM {pid} で停止する
                except SystemExit:
                    pf('*Killed by SIGTERM.*')
                    raise

                # それ以外のエラーは無視して動き続ける
                except Exception as e:
                    if repeat < maxRepeat:
                        repeat += 1
                        pf('Uncaught exception was raised, but process continue.', e)
                        pf(f'sleep:{errorsleep}s')
                        time.sleep(errorsleep)
                        continue
                    else:
                        pf('Uncaught exception was repeating. Process stop.', e)
                        break
                repeat = 0
        with dc:
            forever()
    return wrapper

```