GP-6664: PyGhidra now outputs full Java stacktrace

This commit is contained in:
Ryan Kurtz
2026-04-14 06:49:04 -04:00
parent 5a209ba931
commit 19ebad7d0d
5 changed files with 40 additions and 19 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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, "<console>")
@contextlib.contextmanager
def _run_context(self) -> Generator[None, None, None]:

View File

@@ -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):
"""

View File

@@ -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)