概要
既に動いているPythonプロセスにアタッチして生きているオブジェクトを操作したい場面は往々にしてあります。
- 前処理に時間のかかるグラフを出してみたは良いけどデータを保存する処理を書き忘れた。
- 数日かかるモデルの学習がエラーも吐かず止まっている。どこで止まったか知りたい。
- 巨大なサービスからspawnされているPythonスクリプトのデバッグ
- ゾンビや孤児になってしまったプロセスにアタッチしたい
ローカル環境でIDEが動く場合は簡単かもしれませんが、GPUマシンなどにSSHしている場合はより問題が込み入ってきます。
今回はこうした問題を、lightweightかつportableなツールの組み合わせで解決する方法を紹介します。
- Small is beautiful. 小さいものは美しい ――The UNIX philosophy――
条件
例として1秒おきに経過秒数を表示し続けるプログラムをターゲットとします。
追加のライブラリをインポートしたりスニペットを前もって挿入する必要がないところがポイントです。
import time
n = 0
while True:
print(n)
time.sleep(1)
n += 1
意図して孤立させ、制御端末を持たないプロセスを作り実験してみます。
$ nohup python roop.py & ; exit
reptyr(りぴーたー)
まずターゲットの標準入出力を奪取して用意した端末につけかえます。
# -s ターゲットのファイルディスクリプタ0,1,2がTTYに繋がっていなくても横取りする。
$ reptyr -s $PID
pyrasite(ぱらさいと)
次にターゲットに下記のデバッガ起動コードの注入を行います。
$ pyrasite $PID set_pudb.py
PuDB(ぴーゆーでーびー)
最後に起動したデバッガの内部シェルから変数n
を書き換え、ステップ実行してみます。
import pudb
pudb.set_trace()
結果
reptyrが奪取したことでこれまで虚無に消えていたログに触れるようになりました。
さらにブレークポイントを貼ってループごとの実行やn=-100
と変更したのが反映されているのが分かると思います。
補足:途中no source code available
とか出ますがCの関数から帰ってくるまでn
とか押しとけば大丈夫です。
最後に、これまでの内容をいい感じにやってくれるスクリプトを載せておきます。
#!/bin/bash
tmpfile=`mktemp`
PID=`pgrep -a python|fzf|awk '{print $1}'`
[ -z "$PID" ] && exit 1
cat << EOF > $tmpfile
import pudb
pudb.set_trace()
EOF
pyrasite $PID $tmpfile &
reptyr -s $PID