5
8

More than 3 years have passed since last update.

SSH先のPythonプロセスにアタッチしてデバッグ

Posted at

概要

既に動いているPythonプロセスにアタッチして生きているオブジェクトを操作したい場面は往々にしてあります。

  • 前処理に時間のかかるグラフを出してみたは良いけどデータを保存する処理を書き忘れた。
  • 数日かかるモデルの学習がエラーも吐かず止まっている。どこで止まったか知りたい。
  • 巨大なサービスからspawnされているPythonスクリプトのデバッグ
  • ゾンビや孤児になってしまったプロセスにアタッチしたい

ローカル環境でIDEが動く場合は簡単かもしれませんが、GPUマシンなどにSSHしている場合はより問題が込み入ってきます。

今回はこうした問題を、lightweightかつportableなツールの組み合わせで解決する方法を紹介します。

  1. Small is beautiful. 小さいものは美しい ――The UNIX philosophy――

条件

例として1秒おきに経過秒数を表示し続けるプログラムをターゲットとします。
追加のライブラリをインポートしたりスニペットを前もって挿入する必要がないところがポイントです。

roop.py
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を書き換え、ステップ実行してみます。

set_pudb.py
import pudb
pudb.set_trace()

結果

output.gif
reptyrが奪取したことでこれまで虚無に消えていたログに触れるようになりました。
さらにブレークポイントを貼ってループごとの実行やn=-100と変更したのが反映されているのが分かると思います。

補足:途中no source code availableとか出ますがCの関数から帰ってくるまでnとか押しとけば大丈夫です。


最後に、これまでの内容をいい感じにやってくれるスクリプトを載せておきます。

injector.sh
#!/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
5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8