From 189117e7d4b9820bc5df94c6a5cdc7079700e11c Mon Sep 17 00:00:00 2001 From: Ryan Kurtz Date: Sun, 25 May 2025 06:39:36 -0400 Subject: [PATCH] GP-5677: Ghidra now writes a "lastrun" file that PyGhidra can look for to help find the Ghidra installation dir --- .../Base/src/main/java/ghidra/GhidraRun.java | 21 +++++++++++++++ .../Features/PyGhidra/src/main/py/README.md | 8 ++++-- .../PyGhidra/src/main/py/src/pyghidra/core.py | 6 ++--- .../src/main/py/src/pyghidra/launcher.py | 27 +++++++++++++++++-- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java b/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java index b0237bb0d5..b5e1baf93e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java @@ -16,6 +16,8 @@ package ghidra; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import javax.swing.ToolTipManager; @@ -23,6 +25,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import docking.framework.SplashScreen; +import generic.jar.ResourceFile; import ghidra.base.help.GhidraHelpService; import ghidra.framework.Application; import ghidra.framework.GhidraApplicationConfiguration; @@ -77,6 +80,8 @@ public class GhidraRun implements GhidraLaunchable { log.info("User temp directory: " + Application.getUserTempDirectory()); log.info("User cache directory: " + Application.getUserCacheDirectory()); + writeLastRun(); + initializeTooltips(); updateSplashScreenStatusMessage("Populating Ghidra help..."); @@ -99,6 +104,22 @@ public class GhidraRun implements GhidraLaunchable { mainThread.start(); } + private void writeLastRun() { + // Write the Ghidra installation location to the "lastrun" file + File settingsDir = Application.getUserSettingsDirectory(); + ResourceFile rootDir = Application.getApplicationRootDirectory(); + if (settingsDir != null && rootDir != null) { + File lastRunFile = new File(settingsDir.getParentFile(), "lastrun"); + try { + Files.writeString(lastRunFile.toPath(), + rootDir.getParentFile().getCanonicalPath() + "\n"); + } + catch (IOException e) { + log.error("Failed to write 'lastrun' file", e); + } + } + } + private String processArguments(String[] args) { //TODO remove this special handling when possible if (args.length == 1 && (args[0].startsWith("-D") || args[0].indexOf(" -D") >= 0)) { diff --git a/Ghidra/Features/PyGhidra/src/main/py/README.md b/Ghidra/Features/PyGhidra/src/main/py/README.md index c04f30c2ee..b68c86131d 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/README.md +++ b/Ghidra/Features/PyGhidra/src/main/py/README.md @@ -55,7 +55,7 @@ def start(verbose=False, *, install_dir: Path = None) -> "PyGhidraLauncher": :param verbose: Enable verbose output during JVM startup (Defaults to False) :param install_dir: The path to the Ghidra installation directory. - (Defaults to the GHIDRA_INSTALL_DIR environment variable) + (Defaults to the GHIDRA_INSTALL_DIR environment variable or "lastrun" file) :return: The PyGhidraLauncher used to start the JVM """ ``` @@ -222,7 +222,7 @@ def run_script( This may be either a Java class or its path. (Defaults to None) :param install_dir: The path to the Ghidra installation directory. This parameter is only used if Ghidra has not been started yet. - (Defaults to the GHIDRA_INSTALL_DIR environment variable) + (Defaults to the GHIDRA_INSTALL_DIR environment variable or "lastrun" file) :param program_name: The name of the program to open in Ghidra. (Defaults to None, which results in the name being derived from "binary_path") :param nested_project_location: If True, assumes "project_location" contains an extra nested @@ -330,6 +330,10 @@ __2.2.0:__ [`pyghidra.run_script()`](#pyghidrarun_script) now accept a `nested_project_location` parameter which can be set to `False` to open existing Ghidra projects that were created with the Ghidra GUI. +* If a Ghidra installation directory is not specified by the `install_dir` parameter or + `GHIDRA_INSTALL_DIR` environment variable, PyGhidra will look for a `lastrun` file in the + Ghidra user settings parent directory, and use the installation directory it specifies. The + `lastrun` file is created by Ghidra 11.4 and later. __2.1.0:__ * [`pyghidra.open_program()`](#pyghidraopen_program) now accepts a `program_name` parameter, which diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/core.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/core.py index a3473a3c53..e813fe41a2 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/core.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/core.py @@ -33,7 +33,7 @@ def start(verbose=False, *, install_dir: Path = None) -> "PyGhidraLauncher": :param verbose: Enable verbose output during JVM startup (Defaults to False) :param install_dir: The path to the Ghidra installation directory. - (Defaults to the GHIDRA_INSTALL_DIR environment variable) + (Defaults to the GHIDRA_INSTALL_DIR environment variable or "lastrun" file) :return: The PyGhidraLauncher used to start the JVM """ from pyghidra.launcher import HeadlessPyGhidraLauncher @@ -302,7 +302,7 @@ def _flat_api( This may be either a Java class or its path. (Defaults to None) :param install_dir: The path to the Ghidra installation directory. This parameter is only used if Ghidra has not been started yet. - (Defaults to the GHIDRA_INSTALL_DIR environment variable) + (Defaults to the GHIDRA_INSTALL_DIR environment variable or "lastrun" file) :raises ValueError: If the provided language, compiler or loader is invalid. :raises TypeError: If the provided loader does not implement `ghidra.app.util.opinion.Loader`. """ @@ -378,7 +378,7 @@ def run_script( This may be either a Java class or its path. (Defaults to None) :param install_dir: The path to the Ghidra installation directory. This parameter is only used if Ghidra has not been started yet. - (Defaults to the GHIDRA_INSTALL_DIR environment variable) + (Defaults to the GHIDRA_INSTALL_DIR environment variable or "lastrun" file) :param program_name: The name of the program to open in Ghidra. (Defaults to None, which results in the name being derived from "binary_path") :param nested_project_location: If True, assumes "project_location" contains an extra nested 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 03365ed161..a8864d65f1 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/launcher.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/launcher.py @@ -152,6 +152,29 @@ def _plugin_lock(): # it will be removed by said process when done pass +def _lastrun() -> Path: + + lastrun_file: Path = None + lastrun_rel: Path = Path('ghidra/lastrun') + + # Check for XDG_CONFIG_HOME environment variable + xdg_config_home: str = os.environ.get('XDG_CONFIG_HOME') + if xdg_config_home: + lastrun_file = Path(xdg_config_home) / lastrun_rel + else: + # Default to platform-specific locations + if platform.system() == 'Windows': + lastrun_file = Path(os.environ['APPDATA']) / lastrun_rel + elif platform.system() == 'Darwin': + lastrun_file = Path.home() / 'Library' / lastrun_rel + else: + lastrun_file = Path.home() / '.config' / lastrun_rel + + if lastrun_file is not None and lastrun_file.is_file(): + with open(lastrun_file, "r") as file: + return Path(file.readline().strip()) + + return None class PyGhidraLauncher: """ @@ -164,7 +187,7 @@ class PyGhidraLauncher: :param verbose: True to enable verbose output when starting Ghidra. :param install_dir: Ghidra installation directory. - (Defaults to the GHIDRA_INSTALL_DIR environment variable) + (Defaults to the GHIDRA_INSTALL_DIR environment variable or "lastrun" file) :raises ValueError: If the Ghidra installation directory is invalid. """ self._layout = None @@ -173,7 +196,7 @@ class PyGhidraLauncher: self._dev_mode = False self._extension_path = None - install_dir = install_dir or os.getenv("GHIDRA_INSTALL_DIR") + install_dir = install_dir or os.getenv("GHIDRA_INSTALL_DIR") or _lastrun() self._install_dir = self._validate_install_dir(install_dir) java_home_override = os.getenv("JAVA_HOME_OVERRIDE")