From 19ebad7d0d4fc266a8ddc33e8a2442ce623fb738 Mon Sep 17 00:00:00 2001 From: Ryan Kurtz Date: Tue, 14 Apr 2026 06:49:04 -0400 Subject: [PATCH] GP-6664: PyGhidra now outputs full Java stacktrace --- .../Features/PyGhidra/src/main/py/README.md | 3 ++ .../src/main/py/src/pyghidra/__init__.py | 2 +- .../py/src/pyghidra/internal/plugin/plugin.py | 8 ++--- .../src/main/py/src/pyghidra/launcher.py | 12 ++++++- .../src/main/py/src/pyghidra/script.py | 34 ++++++++++++------- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/Ghidra/Features/PyGhidra/src/main/py/README.md b/Ghidra/Features/PyGhidra/src/main/py/README.md index 1099a18291..6e67fce69f 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/README.md +++ b/Ghidra/Features/PyGhidra/src/main/py/README.md @@ -567,6 +567,9 @@ import pdb # imports Python's pdb import pdb_ # imports Ghidra's pdb ``` ## Change History +__3.2.0__ +* When an uncaught `JException` occurs, PyGhidra will now output the full Java stack trace. + __3.1.0__ * PyGhidra will now, by default, restore `sys.modules` to its prior state after a PyGhidra script is run (or the interactive interpreter is reset) so the next time a script is run, it freshly loads diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py index 7d6bfe7e41..0121f52864 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ## -__version__ = "3.1.0" +__version__ = "3.2.0" # stub for documentation and typing # this is mostly to hide the function parameter diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/internal/plugin/plugin.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/internal/plugin/plugin.py index 76d1e93c56..d3b79fec24 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/internal/plugin/plugin.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/internal/plugin/plugin.py @@ -20,6 +20,7 @@ import logging import re import sys import threading +import traceback import types from code import InteractiveConsole from typing import Generator @@ -35,10 +36,10 @@ from java.lang import String # type:ignore @UnresolvedImport from java.lang import Thread as JThread # type:ignore @UnresolvedImport from java.util import Collections # type:ignore @UnresolvedImport from java.util.function import Consumer # type:ignore @UnresolvedImport -from jpype import JClass, JImplements, JOverride +from jpype import JClass, JImplements, JOverride, JException from pyghidra.internal.plugin.completions import PythonCodeCompleter -from pyghidra.script import PyGhidraScript +from pyghidra.script import PyGhidraScript, print_stacktrace logger = logging.getLogger(__name__) @@ -296,8 +297,7 @@ class PyConsole(InteractiveConsole): super().showsyntaxerror(filename=filename, **kwargs) def showtraceback(self) -> None: - with self.redirect_writer(): - super().showtraceback() + print_stacktrace(self._script, "") @contextlib.contextmanager def _run_context(self) -> Generator[None, None, None]: diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/launcher.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/launcher.py index e8a3ebee7e..5b7f30afb5 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/launcher.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/launcher.py @@ -32,7 +32,7 @@ from pathlib import Path from typing import Generator, List, NoReturn, Tuple, Union import jpype -from jpype import imports, _jpype +from jpype import imports, _jpype, JException from packaging.version import Version from pyghidra.javac import java_compile @@ -176,6 +176,12 @@ def _lastrun() -> Path: return None +def _pyghidra_excepthook(exc_type, exc_value, tb): + sys.__excepthook__(exc_type, exc_value, tb) + if isinstance(exc_type, JException): + # Remove the first line of the Java stack trace...it's already output + sys.stderr.write(exc_value.stacktrace().partition("\n")[2]) + class PyGhidraLauncher: """ Base pyghidra launcher @@ -221,6 +227,10 @@ class PyGhidraLauncher: self.vm_args = self._jvm_args() self.args = [] self.app_info = ApplicationInfo.from_file(ghidra_dir / "application.properties") + + # Install our custom excepthook (only if no one else already has) + if sys.excepthook == sys.__excepthook__: + sys.excepthook = _pyghidra_excepthook def _setup_dev_classpath(self, utility_dir: Path): """ diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py index 200e4fdb4b..e30018b5b0 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py @@ -22,7 +22,7 @@ import traceback from collections.abc import ItemsView, KeysView from importlib.machinery import ModuleSpec, SourceFileLoader from pathlib import Path -from jpype import JClass, JImplementationFor +from jpype import JClass, JImplementationFor, JException from typing import List @@ -255,18 +255,7 @@ class PyGhidraScript(dict): spec.loader.exec_module(m) # pylint: disable=bare-except except: - # filter the traceback so that it stops at the script - exc_type, exc_value, exc_tb = sys.exc_info() - i = 0 - tb = traceback.extract_tb(exc_tb) - for fs in tb: - if fs.filename == script_path: - break - i += 1 - ss = traceback.StackSummary.from_list(tb[i:]) - e = traceback.TracebackException(exc_type, exc_value, exc_tb) - e.stack = ss - self._script.printerr(''.join(e.format())) + print_stacktrace(self._script, script_path) finally: sys.argv = orig_argv @@ -320,3 +309,22 @@ def get_current_interpreter(): except ImportError: return None + + +def print_stacktrace(script, script_path): + exc_type, exc_value, exc_tb = sys.exc_info() + i = 0 + tb = traceback.extract_tb(exc_tb) + for fs in tb: + if fs.filename == script_path: + break + i += 1 + ss = traceback.StackSummary.from_list(tb[i:]) + te = traceback.TracebackException(exc_type, exc_value, exc_tb) + te.stack = ss + output = ''.join(te.format()) + if isinstance(exc_type, JException): + # Remove the first line of the Java stack trace...it's already output + output += exc_value.stacktrace().partition('\n')[2] + script.printerr(output) +