TL;DR
faulthandler.enable()
を使おう。これを実行した部分以降は、Python自体がクラッシュするような状況(=シグナルが飛んできた など) になると、スタックトレースが標準エラーに出力された上でクラッシュするようになる。
背景
macOSで requests.get
を使うスクリプトをデーモン化したところ、何も出力せずにクラッシュするようになって困った。
タチの悪いことに、このクラッシュはデーモン化した時だけに起こる。
(環境は Python 3.11, macOS 12.6.5)
from daemon import DaemonContext
from daemon.pidfile import PIDLockFile
import requests
import time
pid = PIDLockFile(path.join(home_dir, 'pidfile_watcher'))
fp = open("out.log", "a+")
fpe = open("err.log", "a+")
with DaemonContext(
pidfile=pid,
stdout=fp,
stderr=fpe
):
while True:
response = requests.get("https://example.com");
print(response.text)
time.sleep(60*10)
fp.close()
fpe.close()
print
をあっちゃこっちゃに仕込んだり"コンソール.app"を起動したりして1時間ほど調べまくった結果、どうやらrequests.get()
で Segmentation Fault していそうだということがわかった。
なぜそんなに時間がかかったのか
DaemonContext
に標準エラーの向き先を渡しているにもかかわらず、Python自体がクラッシュするような状況ではログが全く出力されなかったからである。
また、コンソール.appで見つけたクラッシュレポートは、自分でプログラムの全てを書いていない限り、あまり役に立たない。ここで「全て」とは、Pythonスクリプトに加えて使ったパッケージやPythonそのものも含まれてくる。クラッシュレポートは文字通り全てのコールスタックを出力する (= 情報が多すぎる) ので、よほどPythonそのものに関わっていなければ、何がどれに対応するのか解読するのは難しいだろう。
この時点では自分に残されたデバッグ方法は print
を仕込みまくることだけだった。出力されなくなったprint
のタイミングがクラッシュのタイミングだ。
手動で二分法的に調査するなど多少の工夫はできるだろうが、最初自分はDaemonContext
のstdout
にファイルを指定しておらず、print
の出力がロストするような状況だったため、結構面倒だった。
解決法
faulthandler.enable()
が正解だった。
この関数を呼び出した後は、シグナルが飛んできた場合などのPython自体が異常終了する状況になった時、そのままクラッシュする前にスタックトレースを標準エラーに流してくれる。
(例えばmacOSでのSegmentation faultはSIGSEGVが飛ぶため、この関数の機能が発動する。)
この関数で、確実に「どこでクラッシュしたのか」を掴むことができた。
ちなみに
先ほどのスクリプトのクラッシュはmacOSにおいて不可避らしい。
回避するには、プロキシを使えなくする必要がある。どうやら、macOSのプロキシの設定を取得する機能が「フォークに対して安全ではなく」、segmentaion faultを起こしてしまうらしい。
プロキシを使わないようにするには、環境変数 no_proxy
に *
を設定すれば良い。
import os
os.environ['no_proxy'] = "*"