使用pyrasite進行python進程調試,改變運行中進程的代碼

後端開發中有時會遇到這種狀況:進程運行中偶現,重啓進程問題就消失;或者是,進程必定要運行一段時間纔會出現問題;又或是,極難復現的問題出現了,然而已有的log不足以定位html

對於這些狀況,儘管大部分時候,咱們能夠經過在可能的地方加log,而後重啓進程等待問題復現,但這樣相對被動。咱們都知道若是要調試C/C++程序,gdb attach上進程就能夠,而python雖然有類似的工具pdb,但它沒法附加到一個進程上,必需要用pdb啓動進程,在實際環境中顯然無論用,那麼python是否有相似的辦法來改變運行中進程的代碼呢?這樣咱們就能夠經過實時加log來定位問題,這樣幾乎能夠解決python層面的任何問題python

能夠參考兩篇文章:linux

https://mozillazg.com/2018/07...
https://mozillazg.com/2017/07...git

簡單來講,能夠直接用gdb使用相似調試c程序的方式,但要求python進程是使用python-debug這種版本的python,一樣不夠實用。這裏介紹博客中提到的「純gdb」的方式,經過github上一個開源python包pyrasite,本質上是經過gdb的-eval-command和它的PyRun_SimpleString來向進程注入代碼。github

這個庫有一些附加功能,能夠經過它的文檔去了解。這裏只說實現進程注入的核心,是其中一個很短的文件injector.py,這裏去掉了原文件中用於windows平臺的一段代碼,咱們這裏只考慮linux,核心代碼以下:shell

import os
import subprocess
import platform

def inject(pid, filename, verbose=False, gdb_prefix=''):
    """Executes a file in a running Python process."""
    filename = os.path.abspath(filename)
    gdb_cmds = [
        'PyGILState_Ensure()',
        'PyRun_SimpleString("'
            'import sys; sys.path.insert(0, \\"%s\\"); '
            'sys.path.insert(0, \\"%s\\"); '
            'exec(open(\\"%s\\").read())")' %
                (os.path.dirname(filename),
                os.path.abspath(os.path.join(os.path.dirname(__file__), '..')),
                filename),
        'PyGILState_Release($1)',
        ]
    p = subprocess.Popen('%sgdb -p %d -batch %s' % (gdb_prefix, pid,
        ' '.join(["-eval-command='call %s'" % cmd for cmd in gdb_cmds])),
        shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.communicate()
    if verbose:
        print(out)
        print(err)

這個函數作的事很簡單,不難看懂,因此,咱們須要作的就是調用這個函數,傳入pid和文件名,文件是一個你要對這個進程執行的python代碼。如今咱們運行一個很簡單的python進程test.pywindows

import time
def b():
  print('b')

while 1:
  b()
  time.sleep(1)

而後建立一個文件patch.py後端

print('injecting')
def newb():
  print('new b')
b = newb

injector.py的末尾加上一段,以便接收命令行調用:函數

import sys
pid = sys.argv[1]
filename = sys.argv[2]

inject(int(pid), filename)

經過ps aux|grep test.py查看上面進程的pid,而後執行python injector.py pid patch.py,爲方便反覆測試能夠這樣:工具

pid=`ps aux | grep test.py | grep -v grep | awk '{print $2}'`;python injector.py $pid patch.py;echo $pid injected

輸出以下:

clipboard.png

至此就實現了進程注入。

注意點:

  • 修改類或類方法和函數同理,改變類的方法時,直接使用類名classA.method = new_method會將變化應用到全部實例,注意對類方法來講在patch.py中定義時也要加上self參數
  • patch.py中,咱們能夠直接對b賦值,由於咱們gdb進入一個進程後,所在的上下文環境就是該進程的入口模塊,能夠經過打印globals()來看到有哪些全局變量,這些就是能夠直接訪問的對象。若是是在一個普通的業務進程中,必然有大量import,這種狀況下你須要import相應模塊再對該模塊的函數或類進行修改,如import x.y.z as z; z.b = newb
  • 特別須要注意的是,若是一個模塊A使用了from B import func,那麼若是你想改變A中運行的func,須要import A; A.func = newfunc,像這樣改變B是沒有用的:import B; B.func = newfunc,由於from .. import ..會將對象複製一份到本地命名空間。反之,若是A是使用import B並經過B.func進行調用,那麼就應當import B進行修改
  • 若是一個函數內部有阻塞式的while True,那麼改變這個函數是沒有用的,顯然要應用改變的對象須要對象下一次被調用,這個不難理解可是容易漏想到
相關文章
相關標籤/搜索