Eclipse: PyDevメモ: LibreOfficeのPythonマクロのデバッグ

2017-08-11

旧ブログ

t f B! P L
LibreOfficeのPythonマクロのデバッグについてこれまでの知識を整理しておきます。

前の関連記事:Eclipse: PyDevメモ: ブレークしたところでコマンドを実行する


Pythonマクロにブレークポイントを張るまでの手順


1. Eclipseのインストール。
(linuxBean14.04(133)Eclipse4.6(Neon)とPyDev5.4のインストール)

2. PyDevのインストール。
(linuxBean14.04(133)Eclipse4.6(Neon)とPyDev5.4のインストール)

3. PyDevプロジェクトをマイマクロフォルダに作成してPythonマクロを作成。
(Gitリポジトリを利用する方法: linuxBean14.04(149)LibreOfficeマクロ開発環境の構築:その1、workspaceをマイマクロフォルダ下に作成する方法: linuxBean14.04(140)LibreOfficeのマイマクロフォルダをEclipse4.6のworkspaceにする、マイマクロフォルダのマクロをリンクしたPyDevプロジェクトを作成する方法: linuxBean14.04(134)Eclipse4.6とLibreOffice5.2のPythonマクロ)

4. pydevd.pyがあるフォルダをマクロのPYTHONPATHに追加する。
(sites.pthにパスを追記する方法: linuxBean14.04(135)LibreOffice5.2のPythonマクロをリモートデバッグする、マクロのあるフォルダにあるpythonpathフォルダにpydevd.pyがあるフォルダを置いてインポートする方法: linuxBean14.04(136)WindowsのLibreOfficeマクロをlinuxBeanでリモートデバッグする

5. ブレークしたいところにデバッグコードを挿入する。
(Eclipse: PyDevメモ: ブレークしたところでコマンドを実行する)

import pydevd; pydevd.settrace(stdoutToServer=True, stderrToServer=True)

挿入するデバッグコードはこれになりますが、import pydevdの部分は適宜pydevd.pyを置いたパスに変更します。

引数のstderrToServer=TrueはPyDevのコンソールでtraceback.print_exc()の出力を表示させるのに必要です。

6. EclipseでDebugパースペクティブに切り替えてPyDev→Start Debug Server。

7. LibreOfficeでマクロを起動。

これでEclipseのDegugパースペクティブをみるとデバッグコードを挿入したところでブレークされているはずです。

オートメーションで起動している場合でも、リスナーのメソッドのブレークは、マクロで起動しているのと同様にしてリモートデバッグしないといけません、といってもオートメーションから起動したランタイムダイアログでは発火しないリスナーがあります。

以下は3でGitリポジトリを利用する方法、4でsites.pthにパスを追記する方法、を採用した環境で行っています。

例外が発生したときにLibreOfficeが表示するエラーメッセージウィンドウ


マクロセレクターに表示させている関数のスコープにraiseを挿入して例外を発生させてみます。

raise RuntimeError("Something bad happened")



例外が発生するとLibreOfficeがこのエラーメッセージウィンドウが出現します。

com.sun.star.uno.RuntimeExceptionError during invoking function macro in module file:///home/pq/.config/libreoffice/4/user/Scripts/python/GUI/GUI/src/unodialogsample_tmp.py (<class 'RuntimeError'>: Something bad happened

ここまでで、例外が発生したファイルのフルパス、発生した例外とそのメッセージがわかります。

/home/pq/.config/libreoffice/4/user/Scripts/python/GUI/GUI/src/unodialogsample_tmp.py:32 in function macro() [raise RuntimeError("Something bad happened")]

これで発生した例外のファイルのパスと、関数名、行番号、その行のコードがわかります。

ここまでわかれば情報としては十分です。

ただ、LibreOfficeがクラッシュするようなエラーでは、このエラーメッセージも表示されないのでどこでエラーが起きたのかはわかりません。

  /opt/libreoffice5.2/program/pythonscript.py:870 in function invoke() [ret = self.func( *args )]
)

これはマクロファイルの外のScripting Frameworkのコードになって、その部分のコードがわかっても自分ではどうしようもありません。

今度はこのraise文の前にデバッグコードを挿入してブレークしてみます。

import pydevd; pydevd.settrace(stdoutToServer=True, stderrToServer=True)

このデバッグコードを挿入しました。

ブレークしたところからステップオーバーして例外が発生すると先ほど出現したエラーメッセージで出てきた通りにScripting Frameworkのコードに飛んで、先ほどと同様のエラーメッセージが表示されます。

PyDevのコンソールには何も出力されません。

これではリモートデバッグする意味がありませんのでトレースバックを取得するには次のようにtraceback.print_exc()を挿入する必要があります。

try except文でPyDevコンソールにトレースバックを出力する


PyDevコンソールでトレースバックを取得するには、例外が発生する部分をtry~except文で囲んで例外を受け取ってtraceback.print_exc()で出力します。

import tracebackが必要です。
 try:
  import pydevd; pydevd.settrace(stdoutToServer=True, stderrToServer=True)
  raise RuntimeError("Something bad happened")
 except:
#   import pydevd; pydevd.settrace(stdoutToServer=True, stderrToServer=True)  # リスナー内でブレークしてもトレースバックは取得できないので、ブレークしたところでコンソールでコマンド実行する。
  traceback.print_exc()
これでPyDevのコンソールにトレースバックが出力されます。
Traceback (most recent call last):
  File "/home/pq/.config/libreoffice/4/user/Scripts/python/GUI/GUI/src/unodialogsample_tmp.py", line 32, in macro
    raise RuntimeError("Something bad happened")
RuntimeError: Something bad happened
これでLibreOfficeのエラーメッセージと同じ情報が取得できました。

LibreOfficeのエラーメッセージウィンドウは出力されません。

この方法だとLibreOfficeがクラッシュしても情報が取得できます。

トレースバックを取得したいだけなら、except文内のtraceback.print_exc()より前でブレークさえすれば同じ結果になます。

またメインの関数内をtry~except文に囲んで置けば、どこで例外が発生してもトレースバックを取得できます。

といってもランタイムダイアログのコントロールに付けたリスナーの__init__()メソッド以外のメソッドで起こった例外は補足できません。

この方法の問題点は、リモートデバッグしないときに、デバッグコードだけでなく、try except文も削除しておかないと、マクロを実行したときに例外が発生しても何も反応がないことです。

try except文の削除はコメントアウトするだけでなく、インデントも変更しないといけないので面倒ですので、一行をコメントアウトするだけで切り替えられる方法を考えました。

デコレーターのオンオフでリモートデバッグのオンオフを切り替えられるようにする

def enableRemoteDebugging(func):  # デバッグサーバーに接続したい関数やメソッドにつけるデコレーター。主にリスナーのメソッドのデバッグ目的。
#     if __name__ == "__main__":  # オートメーションのときはデバッグサーバーに接続しない。
#         return func
    def wrapper(*args, **kwargs):
        import pydevd
        frame = None
        doc = XSCRIPTCONTEXT.getDocument()
        if doc:  # ドキュメントが取得できた時
            frame = doc.getCurrentController().getFrame()  # ドキュメントのフレームを取得。
        else:
            currentframe = XSCRIPTCONTEXT.getDesktop().getCurrentFrame()  # モードレスダイアログのときはドキュメントが取得できないので、モードレスダイアログのフレームからCreatorのフレームを取得する。
            frame = currentframe.getCreator()
        if frame:   
            import time
            indicator = frame.createStatusIndicator()  # フレームからステータスバーを取得する。
            maxrange = 2  # ステータスバーに表示するプログレスバーの目盛りの最大値。2秒ロスするが他に適当な告知手段が思いつかない。
            indicator.start("Trying to connect to the PyDev Debug Server for about 20 seconds.", maxrange)  # ステータスバーに表示する文字列とプログレスバーの目盛りを設定。
            t = 1  # プレグレスバーの初期値。
            while t<=maxrange:  # プログレスバーの最大値以下の間。
                indicator.setValue(t)  # プレグレスバーの位置を設定。
                time.sleep(1)  # 1秒待つ。
                t += 1  # プログレスバーの目盛りを増やす。
            indicator.end()  # reset()の前にend()しておかないと元に戻らない。
            indicator.reset()  # ここでリセットしておかないと例外が発生した時にリセットする機会がない。
        pydevd.settrace(stdoutToServer=True, stderrToServer=True)  # デバッグサーバーを起動していた場合はここでブレークされる。
        try:
            func(*args, **kwargs)  # Step Intoして中に入る。
        except:
            import traceback; traceback.print_exc()  # これがないとPyDevのコンソールにトレースバックが表示されない。stderrToServer=Trueが必須。
    return wrapper
このenableRemoteDebuggingをデコレーターとして関数やメソッドに付ければデバッグサーバーに接続してブレークします。

(2017.8.20追記。オートメーションの時もリモートデバッグしないといけないとき(notifyContextMenuExecute()など)があるので2-3行目はコメントアウトしました。しかしnotifyContextMenuExecute()をデコレーターでブレークしたときはなぜかコンテクストメニューの項目が表示されません。デコレーターを使わずにブレークしたときはちゃんと項目が表示されます。)

(2017.8.18追記。モードレスダイアログ上のリスナーのメソッドにenableRemoteDebuggingデコレーターを付けたときはドキュメントのステータスバーを取得できないので、モードレスダイアログの親フレーム(Creator)を取得するように変更しました。ActiveFrameはCurrentFrameとは異なると知りました(LibreOffice: theDesktop Singleton Reference)。)

(2017.9.9追記。import pydevdは時間がかかることがわかったので、LibreOffice5(81)Javaの例:GUIをPythonにする その7でその位置を変更しました。)

(2017.9.10追記。リスナークラスの__init__()そのものにenableRemoteDebuggingデコレーターをつけるとpydevdがエラーを出して動きません。インスタンス化しているコードから入るようにすれば問題は起きません。)

デコレーターをつけた関数を起動したことを気づかせるためにステータスバーに2秒間だけそれを知らせる文字列を表示させています。


プログレスバーも表示させていますが、デバッグサーバーの処理の過程を反映させる方法は思いつかず、単に2秒間ロスしているだけです
(2017.8.11追記。なんと単なる時間のロスではなく、XAdjustmentListenerのadjustmentValueChanged()メソッド内でブレークするとマウスボタンが無効になる問題の回避策となりました。Eclipse: PyDevメモ: LibreOfficeのPythonマクロのリスナーの問題点参照。)


デバッグサーバーを起動していない場合は、0.2秒間隔で100回接続を試みますので、20秒経ってようやくこのようなLibreofficeのエラーメッセージウィンドウが表示されます。

コメントの日本語は文字化けしていますね。

待っている時間はかなり長く感じますが、この長さはorg.python.pydev_xxxxxxxxx/pysrc/_pydevd_bundle/pydevd_comm.pyの中の関数start_client()で定義されています。

デバッグサーバーを忘れずに起動させておけばよいだけの問題ですが。

Terminateでデバッグを終わらせたあとに再度デバッグサーバーにつなぐにはLibreOfficeを再起動しないといけない


デバッグサーバーを起動しているのにEclipseのDebugパースペクティブでブレークポイントで止まらない時があります。

1 デバッグサーバーを起動する。
2 LibreOfficeを起動する。
3 マクロを起動する。
4 デバッグパースペクティブでブレークポイントが表示される。


この手順だと確実に成功すると思います。

(2017.8.11追記。うまくいかないパターンを以下に書いてみましたが、デバッグサーバーを起動したまま、何回もマクロを起動しているとうまくつながらないことがあり、やっぱり上記の手順を踏まないといけなくなるときがあります。)

EclipseでRun→Resumeでマクロを終了して、再度マクロを起動しても成功します。


これに対してEclipseでRun→Terminateでマクロを途中で終わらせてみます。

デバッグサーバーは起動したままです。

この状態で再度マクロを起動してもEclipseのDebugパースペクティブでブレークポイントが表示されません。

マクロに例外が発生したときはデバッグサーバーに接続していないときと同様にLibreOfficeのエラーメッセージウィンドウにトレースバックが表示されますし、デバッグコードがあるにもかかわらずマクロが普通に動きます。

上のデコレーターを使っているときはステータスバーにメッセージが表示されるのでpydevdが読み込まれていることがわかります。


DebugビューでDebug Serverを選択してこれもTerminateします。

それでも同じ結果です。

LibreOfficeを再起動してマクロを実行すると今度はpydevdと繋がらないというLibreOfficeのエラーメッセージウィンドウが表示されます。

デバッグサーバーを再起動して、LibreOfficeのマクロを実行すると今度はちゃんとブレークポイントで止まりました。

ということでLibreOfficeからデバッグサーバーのプロセスに接続すると、Eclipse側でデバッグサーバーのプロセスを終了させたとしても、LibreOfficeからは元のデバッグサーバーのプロセスにつながったままのようにふるまうことがわかりました。

ということで、Eclipse側でデバッグサーバーのプロセスを終了させたあとはLibreOfficeを再起動しないといけないことがわかりました。

LibreOfficeを再起動するとマクロセレクターからまたマクロの階層まで開いていかないので、少し手間が増えます。

LibreOfficeを再起動してマクロをマクロセレクターで選択する手間をとらないためには、可能であればTerminateは使わずResumeでマクロを終了させるようにします。

しかし、実際は動くのかわからない書きかけのマクロをデバッグすることが多いので、デバッグサーバーでLibreOfficeとの接続をTerminateして、マクロを編集後にLibreOfficeを再起動することが多いです。

import pydevd; pydevd.settrace(stdoutToServer=True, stderrToServer=True, overwrite_prev_trace=True)

overwrite_prev_trace=Trueとしてもダメでした。

LibreOffice側からは繋がっているつもりなのにデバッグサーバーではTerminateされているので、すでに繋がっていない接続を上書きしても結果は同じということですね。


TerminateしたあとDebugビューをクリアするにはRemove All Teminated Launchesボタンをクリックします。

次の関連記事:Eclipse: PyDevメモ: LibreOfficeのPythonマクロのリスナーの問題点

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ