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 import pdb_ # imports Ghidra's pdb
``` ```
## Change History ## Change History
__3.2.0__
* When an uncaught `JException` occurs, PyGhidra will now output the full Java stack trace.
__3.1.0__ __3.1.0__
* PyGhidra will now, by default, restore `sys.modules` to its prior state after a PyGhidra script is * 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 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
## ##
__version__ = "3.1.0" __version__ = "3.2.0"
# stub for documentation and typing # stub for documentation and typing
# this is mostly to hide the function parameter # this is mostly to hide the function parameter

View File

@@ -20,6 +20,7 @@ import logging
import re import re
import sys import sys
import threading import threading
import traceback
import types import types
from code import InteractiveConsole from code import InteractiveConsole
from typing import Generator 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.lang import Thread as JThread # type:ignore @UnresolvedImport
from java.util import Collections # type:ignore @UnresolvedImport from java.util import Collections # type:ignore @UnresolvedImport
from java.util.function import Consumer # 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.internal.plugin.completions import PythonCodeCompleter
from pyghidra.script import PyGhidraScript from pyghidra.script import PyGhidraScript, print_stacktrace
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -296,8 +297,7 @@ class PyConsole(InteractiveConsole):
super().showsyntaxerror(filename=filename, **kwargs) super().showsyntaxerror(filename=filename, **kwargs)
def showtraceback(self) -> None: def showtraceback(self) -> None:
with self.redirect_writer(): print_stacktrace(self._script, "<console>")
super().showtraceback()
@contextlib.contextmanager @contextlib.contextmanager
def _run_context(self) -> Generator[None, None, None]: 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 from typing import Generator, List, NoReturn, Tuple, Union
import jpype import jpype
from jpype import imports, _jpype from jpype import imports, _jpype, JException
from packaging.version import Version from packaging.version import Version
from pyghidra.javac import java_compile from pyghidra.javac import java_compile
@@ -176,6 +176,12 @@ def _lastrun() -> Path:
return None 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: class PyGhidraLauncher:
""" """
Base pyghidra launcher Base pyghidra launcher
@@ -221,6 +227,10 @@ class PyGhidraLauncher:
self.vm_args = self._jvm_args() self.vm_args = self._jvm_args()
self.args = [] self.args = []
self.app_info = ApplicationInfo.from_file(ghidra_dir / "application.properties") 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): def _setup_dev_classpath(self, utility_dir: Path):
""" """

View File

@@ -22,7 +22,7 @@ import traceback
from collections.abc import ItemsView, KeysView from collections.abc import ItemsView, KeysView
from importlib.machinery import ModuleSpec, SourceFileLoader from importlib.machinery import ModuleSpec, SourceFileLoader
from pathlib import Path from pathlib import Path
from jpype import JClass, JImplementationFor from jpype import JClass, JImplementationFor, JException
from typing import List from typing import List
@@ -255,18 +255,7 @@ class PyGhidraScript(dict):
spec.loader.exec_module(m) spec.loader.exec_module(m)
# pylint: disable=bare-except # pylint: disable=bare-except
except: except:
# filter the traceback so that it stops at the script print_stacktrace(self._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:])
e = traceback.TracebackException(exc_type, exc_value, exc_tb)
e.stack = ss
self._script.printerr(''.join(e.format()))
finally: finally:
sys.argv = orig_argv sys.argv = orig_argv
@@ -320,3 +309,22 @@ def get_current_interpreter():
except ImportError: except ImportError:
return None 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)