+ GhidraGo is a mechanism to cause Ghidra to display a previously imported program within a
+ local or multi-user project using a ghidraURL hyperlink similar to an http reference. In
+ practice ghidraURL's work very similarly to selecting a URL reference which displays a PDF.
+ Once setup correctly, GhidraURL links can be placed in web pages, external project
+ documentation files, or any other place a URL hyperlink can be placed.
+
+
+ When a GhidraURL is selected, GhidraGo will startup Ghidra if it isn't already running as
+ well as prompt to login to the multi-user project if necessary. The program is displayed in
+ the default tool, usually the codebrowser, and can be configured to re-use an open default
+ tool or to use a new default tool. The GhidraURL must currently be locating a DomainFile
+ that is either in a Remote, Shared project, or a local project.
+
+
+ GhidraGo is a combination of a command line program to send a link, a plugin running within
+ the Ghidra project manager, and the configuration of the default handling for the ghidraURL
+ within the user environment. The ghidraURL is sent as the first and only parameter to the
+ ghidraGo command line interface.
+
+
+ GhidraGo passes information through a simple filesystem mechanism vice an open port for
+ security and simplicity. GhidraGo works on Windows, Linux, and MacOS.
+
+
+
GhidraURL's have the format:
+
+
+ Remote Ghidra Server File:
+ ghidra://<host>[:<port>]/<repository-name>/<program-path>
+ [#<address-or-symbol-ref>]
+
+
Example: ghidra://hostname/Repo/notepad.exe#main
+
+
+ Local Ghidra Project File:
+ ghidra:/[<project-path>/]<project-name>?/<program-path>
+ [#<address-or-symbol-ref>]
+
Choose File > Configuration in the Project Window (not the Codebrowser Window)
+
Click the Plug Icon in the upper right to display all plugins
+
Search for GhidraGoPlugin and select it
+
Press OK
+
+
Ghidra is now configured to listen to GhidraGo Requests. You can execute a GhidraGo request
+ using the "ghidraGo" shell/batch script in
+ /path/to/ghidra/support/GhidraGo/ghidraGo
+ Configuring your platform to handle the protocol is what
+ enables the ghidraGo command line interface to be associated with a ghidraURL. Once
+ configured, clicking hyperlinks that start with the protocol
+ will execute the ghidraGo CLI with that hyperlink as the first argument. The
+ configuration is platform specific.
+
+
+ *NOTE: changes to your path to ghidra, such as upgrading ghidra to a new version,
+ will require the path you set in this configuration to be updated.
+
After the steps above, you should be able to click a GhidraURL href, get the same
+ xdg-open prompt, and upon clicking "Open xdg-open" GhidraGo should execute and open
+ Ghidra to the given GhidraURL.
Open Script Editor and past the following into the editor.
+
+
+ on open location schemeUrl
+ set ghidraUrl to quoted form of schemeUrl
+ do shell script "/path/to/ghidraGo " & ghidraUrl
+ end open location
+
+
+
Save the script as an Application named GhidraGo in either
+ /Applications or ~/Applications
+
Right click on the saved Application and click Show Package Contents
+
Open Contents > Info.plist and under
+ <string>com.apple.ScriptEditor.id.GhidraGo</string>
+ paste the following:
+
+Last modified: Oct 26 2023
+
+
+
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java
new file mode 100644
index 0000000000..e37ad4b395
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java
@@ -0,0 +1,168 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import docking.framework.DockingApplicationConfiguration;
+import generic.jar.ResourceFile;
+import ghidra.app.plugin.core.go.GhidraGoSender;
+import ghidra.app.plugin.core.go.exception.*;
+import ghidra.framework.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.util.*;
+
+/**
+ *
GhidraGo Client
+ *
The first argument is expected to be non-null and a valid {@link GhidraURL}
+ *
If the {@link GhidraURL} is valid, the URL is processed in an existing Ghidra, or
+ * a new Ghidra is started and used to process the URL.
+ *
A valid {@link GhidraURL} in this case must be pointing to a remote (shared project)
+ * Program.
+ *
In the event that a Ghidra is running and does not have an active project, the URL cannot be
+ * processed.
+ */
+public class GhidraGo implements GhidraLaunchable {
+
+ private GhidraGoSender sender;
+
+ /**
+ * Initializes a new GhidraGoSender and processes the {@link GhidraURL}
+ * @param layout the layout passed from main.Ghidra
+ * @param args the CLI args passed to GhidraGo. args should contain a single {@link GhidraURL}.
+ * @throws Exception in the event of an error
+ */
+ @Override
+ public void launch(GhidraApplicationLayout layout, String[] args) throws Exception {
+ try {
+ ApplicationConfiguration configuration = null;
+ if (!Application.isInitialized()) {
+ System.setProperty(ApplicationProperties.APPLICATION_NAME_PROPERTY, "GhidraGo");
+ configuration = new DockingApplicationConfiguration();
+ Application.initializeApplication(layout, configuration);
+ }
+ if (args != null && args.length > 0) {
+ ghidra.framework.protocol.ghidra.Handler.registerHandler();
+ sender = new GhidraGoSender();
+
+ startGhidraIfNeeded(layout);
+
+ sender.send(args[0]);
+ // if configuration is null, probably running inside a test
+ if (configuration != null) {
+ // calling System.exit explicitly is necessary, otherwise the Loading... screen
+ // persists instead of closing when complete.
+ System.exit(0);
+ }
+ }
+ else {
+ throw new IllegalArgumentException(
+ "A valid GhidraURL locating a program, program name, or path to a program name " +
+ "must be specified as the first command line argument.");
+ }
+ }
+ catch (FailedToStartGhidraException e) {
+ logOrShowError("GhidraGo Start Ghidra Exception",
+ "Failed to start Ghidra from GhidraGo", e);
+ System.exit(-1);
+ }
+ catch (StopWaitingException e) {
+ System.exit(-1);
+ }
+ catch (Exception e) {
+ logOrShowError("GhidraGo Exception", "An unexpected exception occurred in GhidraGo", e);
+ // calling System.exit explicitly is necessary, otherwise the Loading... screen
+ // persists instead of closing when complete.
+ System.exit(-1);
+ }
+ }
+
+ private void logOrShowError(String errorTitle, String errorMessage, Exception e) {
+ if (SystemUtilities.isInHeadlessMode()) {
+ Msg.error(this, errorMessage, e);
+ }
+ else {
+ Swing.runNow(() -> Msg.showError(this, null, errorTitle, errorMessage, e));
+ }
+ }
+
+ private void startGhidraIfNeeded(GhidraApplicationLayout layout)
+ throws StopWaitingException, FailedToStartGhidraException {
+ // if there is no listening Ghidra
+ if (!sender.isGhidraListening()) {
+
+ // attempt to start a Ghidra within a locked action
+ // do not wait for the lock if another GhidraGo has been started.
+ try {
+ boolean success = sender.doLockedAction(false, () -> {
+ try {
+ Process ghidraProcess = startGhidra(layout);
+ sender.waitForListener(ghidraProcess);
+ return true;
+ }
+ catch (StopWaitingException e) {
+ return true;
+ }
+ catch (StartedGhidraProcessExitedException | IOException e) {
+ return false;
+ }
+ });
+ if (!success) {
+ // GhidraGo attempted to start ghidra and failed
+ throw new FailedToStartGhidraException();
+ }
+ }
+ catch (UnableToGetLockException e) {
+ // When another GhidraGo has the lock,
+ // wait for there to be a listener without starting the process
+ sender.waitForListener();
+ }
+ }
+ }
+
+ /**
+ * Determines the execution platform and executes the appropriate shell/bash script to start
+ * Ghidra.
+ * @throws IOException in the event that the execution failed
+ */
+ private Process startGhidra(GhidraApplicationLayout layout) throws IOException {
+ ResourceFile file = layout.getApplicationInstallationDir();
+ Path ghidraRunPath;
+
+ if (SystemUtilities.isInDevelopmentMode()) {
+ if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) {
+ ghidraRunPath = Path.of(file.getAbsolutePath(),
+ "/ghidra/Ghidra/RuntimeScripts/Windows/ghidraRun.bat");
+ }
+ else {
+ ghidraRunPath = Path.of(file.getAbsolutePath(),
+ "/ghidra/Ghidra/RuntimeScripts/Linux/ghidraRun");
+ }
+ }
+ else {
+ if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) {
+ ghidraRunPath = Path.of(file.getAbsolutePath(), "/ghidraRun.bat");
+ }
+ else {
+ ghidraRunPath = Path.of(file.getAbsolutePath(), "/ghidraRun");
+ }
+ }
+
+ Msg.info(this, "Starting new Ghidra using ghidraRun script at " + ghidraRunPath);
+ return Runtime.getRuntime().exec(ghidraRunPath.toString());
+ }
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java
new file mode 100644
index 0000000000..70d8647115
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java
@@ -0,0 +1,106 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go;
+
+import java.io.IOException;
+import java.net.URL;
+
+import ghidra.app.CorePluginPackage;
+import ghidra.app.plugin.PluginCategoryNames;
+import ghidra.app.plugin.core.go.ipc.GhidraGoListener;
+import ghidra.framework.main.AppInfo;
+import ghidra.framework.main.ApplicationLevelOnlyPlugin;
+import ghidra.framework.model.ToolServices;
+import ghidra.framework.plugintool.*;
+import ghidra.framework.plugintool.util.PluginStatus;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.util.Msg;
+import ghidra.util.SystemUtilities;
+
+//@formatter:off
+@PluginInfo(
+ category = PluginCategoryNames.COMMON,
+ status = PluginStatus.UNSTABLE,
+ packageName = CorePluginPackage.NAME,
+ shortDescription = "Listens for new GhidraURL's to launch using ToolServices",
+ description = "Polls the ghidraGo directory for any url files written by the GhidraGoClient and " +
+ "processes them in Ghidra",
+ eventsConsumed = {ProjectPluginEvent.class})
+//@formatter:on
+public class GhidraGoPlugin extends Plugin implements ApplicationLevelOnlyPlugin {
+ private GhidraGoListener listener;
+
+ public GhidraGoPlugin(PluginTool tool) {
+ super(tool);
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ }
+
+ @Override
+ protected void dispose() {
+ if (this.listener != null) {
+ listener.dispose();
+ listener = null;
+ }
+ super.dispose();
+ }
+
+ @Override
+ public void processEvent(PluginEvent event) {
+ if (event instanceof ProjectPluginEvent) {
+ if (((ProjectPluginEvent) event).getProject() == null) {
+ dispose();
+ }
+ else {
+ try {
+ listener = new GhidraGoListener((url) -> {
+ processGhidraURL(url);
+ });
+ }
+ catch (IOException e) {
+ Msg.showError(this, null, "GhidraGoPlugin Exception",
+ "Unable to create Listener", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * If the active project is null, do nothing.
+ * Otherwise, try and open the url using {@link ToolServices} launchDefaultToolWithURL function.
+ * @param ghidraURL the GhidraURL to open.
+ */
+ private void processGhidraURL(URL ghidraURL) {
+
+ Msg.info(GhidraGoPlugin.class, "GhidraGo processing " + ghidraURL);
+
+ try {
+ Msg.info(GhidraGoPlugin.class,
+ "Accepting the resource at " + GhidraURL.getProjectURL(ghidraURL));
+ SystemUtilities.runSwingNow(() -> {
+ AppInfo.getFrontEndTool().toFront();
+ AppInfo.getFrontEndTool().getToolServices().launchDefaultToolWithURL(ghidraURL);
+ });
+ }
+ catch (IllegalArgumentException e) {
+ Msg.showError(GhidraGoPlugin.class, null, "GhidraGo Unable to process GhidraURL",
+ "GhidraGo could not process " + ghidraURL, e);
+ }
+ }
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoSender.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoSender.java
new file mode 100644
index 0000000000..c06550d83d
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoSender.java
@@ -0,0 +1,151 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.app.plugin.core.go.exception.*;
+import ghidra.app.plugin.core.go.ipc.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.util.Msg;
+import ghidra.util.Swing;
+
+public class GhidraGoSender extends GhidraGoIPC {
+
+ public GhidraGoSender() throws IOException {
+ super();
+ }
+
+ @Override
+ public void dispose() {
+ // empty
+ }
+
+ /**
+ * performs the given action once the sender lock has been acquired. Using this method ensures
+ * only one sender will perform the given action.
+ * @param waitForLock whether to block until the lock is available
+ * @param action the action to be performed once a lock is acquired. Returns true if successful.
+ * @return true if action was successfully performed; false otherwise.
+ * @throws UnableToGetLockException if the lock was unobtainable
+ */
+ public boolean doLockedAction(boolean waitForLock, Supplier action)
+ throws UnableToGetLockException {
+ return GhidraGoIPC.doLockedAction(senderLockPath, waitForLock, action);
+ }
+
+ /**
+ * Send the url to an existing, listening Ghidra
+ * @param url a valid {@link GhidraURL} in string form for a remote Ghidra program. An error is
+ * displayed if the url is null.
+ * @throws StopWaitingException in the event the stop waiting dialog is shown and answered No.
+ */
+ public void send(String url) throws StopWaitingException {
+ if (StringUtils.isEmpty(url)) {
+ Swing.runNow(() -> Msg.showError(this, null, "GhidraGo Empty URL Error",
+ "An empty GhidraURL cannot be sent."));
+ return;
+ }
+ // create a random file and write the url in it
+ String fileName = UUID.randomUUID().toString();
+ Path randomFilePath = channelPath.resolve(fileName);
+ Path writtenFilePath = urlFilesPath.resolve(fileName);
+
+ try (FileOutputStream fos = new FileOutputStream(randomFilePath.toFile());) {
+ fos.write(url.getBytes());
+ // need to close the file so that it can be moved on window's host
+ fos.close();
+ Files.move(randomFilePath, writtenFilePath);
+ }
+ catch (IOException e) {
+ randomFilePath.toFile().delete();
+ Swing.runNow(() -> Msg.showError(this, null, "GhidraGo Error Sending URL",
+ "There was a file system error preventing the url from being sent.", e));
+ }
+
+ Msg.info(this, "Wrote " + url + " to random file " + writtenFilePath);
+ if (writtenFilePath.toFile().exists()) {
+ waitForFileToBeProcessed(writtenFilePath);
+ }
+
+ }
+
+ /**
+ * waits for the file located at the given file path to be deleted.
+ * @param filePath the path to the file to wait for deletion of
+ * @throws StopWaitingException in the event the stop waiting dialog is shown and answered No.
+ */
+ private void waitForFileToBeProcessed(Path filePath) throws StopWaitingException {
+ // check without dialogs every 100 milliseconds
+ if (filePath.toFile().exists()) {
+
+ // set up periodic check for file
+ CheckForFileProcessedRunnable checkForFile =
+ new CheckForFileProcessedRunnable(filePath, 100, TimeUnit.MILLISECONDS);
+
+ // start checking for file
+ checkForFile.startChecking(100, TimeUnit.MILLISECONDS);
+
+ // block until file has been processed or user answers dialog with No.
+ checkForFile.awaitTermination();
+ }
+ }
+
+ /**
+ * wait for a Ghidra to be listening and ready.
+ * @throws StopWaitingException in the event waiting for a listener was stopped
+ */
+ public void waitForListener() throws StopWaitingException {
+ try {
+ waitForListener(null);
+ }
+ catch (StartedGhidraProcessExitedException e) {
+ // this will never happen when the process sent is null
+ }
+ }
+
+ /**
+ * wait for a Ghidra to be listening and ready.
+ * @param p ghidraRun process that is being waited for in the event that GhidraGo
+ * started Ghidra
+ * @throws StopWaitingException in the event waiting for a listener was stopped
+ * @throws StartedGhidraProcessExitedException in the event a Ghidra was started and exited
+ * unexpectedly.
+ */
+ public void waitForListener(Process p)
+ throws StopWaitingException, StartedGhidraProcessExitedException {
+ if (!isGhidraListening()) {
+ // set up periodic check for listener
+ CheckForListenerRunnable checkForListener = new CheckForListenerRunnable(p, 100,
+ TimeUnit.MILLISECONDS,
+ () -> !isGhidraListening());
+
+ // start checking for listener
+ checkForListener.startChecking(100, TimeUnit.MILLISECONDS);
+
+ // block until listener has been processed or user answers dialog with No.
+ checkForListener.awaitTermination();
+ }
+ }
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitDialog.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitDialog.java
new file mode 100644
index 0000000000..a473ee9f38
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitDialog.java
@@ -0,0 +1,105 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.dialog;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.*;
+
+import docking.DialogComponentProvider;
+import docking.DockingWindowManager;
+import docking.widgets.MultiLineLabel;
+import docking.widgets.OptionDialog;
+import docking.widgets.label.GIconLabel;
+import ghidra.app.plugin.core.go.exception.StopWaitingException;
+
+public abstract class GhidraGoWaitDialog extends DialogComponentProvider {
+
+ public static final int WAIT = 0;
+ public static final int DO_NOT_WAIT = 1;
+
+ protected int actionID = DO_NOT_WAIT;
+ protected boolean answered = false;
+
+ public GhidraGoWaitDialog(String title, String msgText, boolean modal) {
+ super(title, modal);
+
+ addWorkPanel(buildMainPanel(msgText));
+
+ JButton waitButton = new JButton("Wait");
+ waitButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ actionID = WAIT;
+ answered = true;
+ close();
+ }
+ });
+ addButton(waitButton);
+
+ JButton noWaitButton = new JButton("No");
+ noWaitButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ actionID = DO_NOT_WAIT;
+ answered = true;
+ close();
+ }
+ });
+ addButton(noWaitButton);
+ }
+
+ public void showDialog() throws StopWaitingException {
+ answered = false;
+ if (!isShowing()) {
+ DockingWindowManager.showDialog(null, this);
+ }
+
+ if (answered && actionID == DO_NOT_WAIT) {
+ throw new StopWaitingException();
+ }
+ }
+
+ public boolean isAnsweredNo() {
+ return answered && actionID == DO_NOT_WAIT;
+ }
+
+ public void reset() {
+ answered = false;
+ actionID = WAIT;
+ close();
+ }
+
+ protected JPanel buildMainPanel(String msgTextString) {
+ JPanel innerPanel = new JPanel();
+ innerPanel.setLayout(new BorderLayout());
+ innerPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
+
+ JPanel msgPanel = new JPanel(new BorderLayout());
+ msgPanel.add(
+ new GIconLabel(OptionDialog.getIconForMessageType(OptionDialog.WARNING_MESSAGE)),
+ BorderLayout.WEST);
+
+ MultiLineLabel msgText = new MultiLineLabel(msgTextString);
+ msgText.setMaximumSize(msgText.getPreferredSize());
+ msgPanel.add(msgText, BorderLayout.CENTER);
+
+ innerPanel.add(msgPanel, BorderLayout.CENTER);
+ return innerPanel;
+ }
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitForListenerDialog.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitForListenerDialog.java
new file mode 100644
index 0000000000..45d279ad02
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitForListenerDialog.java
@@ -0,0 +1,29 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.dialog;
+
+public class GhidraGoWaitForListenerDialog extends GhidraGoWaitDialog {
+
+ public GhidraGoWaitForListenerDialog() {
+ super("GhidraGo Taking Longer Than Expected to Listen",
+ "If Ghidra has started, please confirm the GhidraGoPlugin has been added in " +
+ "File->Configure in the Ghidra project manager.\n" +
+ "If GhidraGoPlugin has been configured, make sure Ghidra has an active project.\n" +
+ "Would you like to keep waiting?",
+ true);
+ }
+
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/FailedToStartGhidraException.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/FailedToStartGhidraException.java
new file mode 100644
index 0000000000..c8dd6eb1c3
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/FailedToStartGhidraException.java
@@ -0,0 +1,20 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.exception;
+
+public class FailedToStartGhidraException extends Exception {
+ // empty
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StartedGhidraProcessExitedException.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StartedGhidraProcessExitedException.java
new file mode 100644
index 0000000000..a1318238ee
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StartedGhidraProcessExitedException.java
@@ -0,0 +1,24 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.exception;
+
+public class StartedGhidraProcessExitedException extends Exception {
+
+ public StartedGhidraProcessExitedException(int exitValue) {
+ super("Started Ghidra process exited early with exit-value: " + exitValue);
+ }
+
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StopWaitingException.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StopWaitingException.java
new file mode 100644
index 0000000000..2f8e2b91ce
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StopWaitingException.java
@@ -0,0 +1,20 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.exception;
+
+public class StopWaitingException extends Exception {
+ // empty
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/UnableToGetLockException.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/UnableToGetLockException.java
new file mode 100644
index 0000000000..1d4313dd5d
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/UnableToGetLockException.java
@@ -0,0 +1,20 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.exception;
+
+public class UnableToGetLockException extends Exception {
+ // empty
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForFileProcessedRunnable.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForFileProcessedRunnable.java
new file mode 100644
index 0000000000..01d39e44e1
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForFileProcessedRunnable.java
@@ -0,0 +1,131 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.ipc;
+
+import java.nio.file.Path;
+import java.util.concurrent.*;
+
+import ghidra.app.plugin.core.go.exception.StopWaitingException;
+import ghidra.util.Msg;
+import ghidra.util.Swing;
+
+public class CheckForFileProcessedRunnable extends CheckPeriodicallyRunnable {
+
+ /**
+ * How long to wait before asking to continue waiting after the url has been sent to a listening
+ * Ghidra
+ */
+ public static int WAIT_FOR_PROCESSING_DELAY_MS = 500;
+
+ /**
+ * How frequently to ask to continue waiting after Wait is selected
+ */
+ public static int WAIT_FOR_PROCESSING_PERIOD_MS = 60_000;
+
+ /**
+ * Maximum amount of time to wait for the file to be processed
+ */
+ public static int MAX_WAIT_FOR_PROCESSING_MIN = 1;
+
+ private Path filePath;
+ private StopWaitingException stopWaitingException;
+
+ public CheckForFileProcessedRunnable(Path filePath, int period, TimeUnit timeUnit) {
+ super(false, period, timeUnit, () -> filePath.toFile().exists());
+ this.filePath = filePath;
+ }
+
+ /**
+ * This constructor is used to create the thread that will show the wait dialog
+ * @param executor the internal executor that should have 2 threads
+ * @param filePath the path to the file to check
+ * @param period the interval to show the dialog
+ * @param timeUnit the units for the period
+ */
+ private CheckForFileProcessedRunnable(ScheduledExecutorService executor, Path filePath,
+ int period, TimeUnit timeUnit) {
+ super(executor, true, period, timeUnit, () -> filePath.toFile().exists());
+ this.filePath = filePath;
+ }
+
+ public void run() {
+ try {
+ if (checkCondition.call()) {
+ try {
+ if (showDialog) {
+ // show the dialog in a blocking action. This will throw a StopWaitingException
+ // if they answer No. Otherwise, they want to keep waiting.
+ dialog.showDialog();
+ }
+
+ // if their response was WAIT, reset the timer
+ executor.schedule(this, period, timeUnit);
+ }
+ catch (StopWaitingException e) {
+ this.stopWaitingException = e;
+ dispose();
+ }
+ catch (RejectedExecutionException e) {
+ // this is okay, executor has been shutdown
+ dispose();
+ }
+ }
+ else {
+ dispose();
+ }
+ }
+ catch (Exception e) {
+ Swing.runNow(() -> Msg.showError(this, null, "GhidraGo Unable to Check File",
+ "GhidraGo could not check existence of file at " + filePath, e));
+ dispose();
+ }
+ }
+
+ @Override
+ public void startChecking(int delay, TimeUnit delayTimeUnit) throws StopWaitingException {
+ dialog.reset();
+ try {
+ // start thread to check frequently
+ super.startChecking(delay, delayTimeUnit);
+
+ // start thread for showing dialog
+ executor.schedule(new CheckForFileProcessedRunnable(executor, filePath,
+ WAIT_FOR_PROCESSING_PERIOD_MS, TimeUnit.MILLISECONDS), WAIT_FOR_PROCESSING_DELAY_MS,
+ TimeUnit.MILLISECONDS);
+ }
+ catch (RejectedExecutionException e) {
+ if (dialog.isAnsweredNo()) {
+ throw new StopWaitingException();
+ }
+ }
+ }
+
+ public void awaitTermination() throws StopWaitingException {
+ try {
+ executor.awaitTermination(MAX_WAIT_FOR_PROCESSING_MIN, TimeUnit.MINUTES);
+ if (dialog.isAnsweredNo()) {
+ throw new StopWaitingException();
+ }
+
+ if (this.stopWaitingException != null) {
+ throw this.stopWaitingException;
+ }
+ }
+ catch (InterruptedException e) {
+ // this is okay
+ }
+ }
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForListenerRunnable.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForListenerRunnable.java
new file mode 100644
index 0000000000..4421ff8a6b
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForListenerRunnable.java
@@ -0,0 +1,156 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.ipc;
+
+import java.util.concurrent.*;
+
+import ghidra.app.plugin.core.go.exception.StartedGhidraProcessExitedException;
+import ghidra.app.plugin.core.go.exception.StopWaitingException;
+import ghidra.util.Msg;
+import ghidra.util.Swing;
+
+public class CheckForListenerRunnable extends CheckPeriodicallyRunnable {
+ /**
+ * If a listening Ghidra needs to be started, how long to wait before asking to continue
+ * waiting
+ */
+ public static int WAIT_FOR_LISTENER_DELAY_MS = 30_000;
+
+ /**
+ * How frequently to ask to continue waiting after Wait is selected
+ */
+ public static int WAIT_FOR_LISTENER_PERIOD_MS = 60_000;
+
+ /**
+ * Maximum amount of time to wait for a listening Ghidra
+ */
+ public static int MAX_WAIT_FOR_LISTENER_MIN = 5;
+
+ private Process process;
+ private StopWaitingException stopWaitingException;
+ private StartedGhidraProcessExitedException startedGhidraProcessExitedException;
+
+ public CheckForListenerRunnable(Process p, int period, TimeUnit timeUnit,
+ Callable checkCondition) {
+ super(false, period, timeUnit, checkCondition);
+ this.process = p;
+ }
+
+ private CheckForListenerRunnable(ScheduledExecutorService executor, Process p, int period,
+ TimeUnit timeUnit, Callable checkCondition) {
+ super(executor, true, period, timeUnit, checkCondition);
+ this.process = p;
+ }
+
+ public void run() {
+ try {
+ if (checkCondition.call()) {
+ try {
+ checkProcessDidNotExit(process);
+ if (showDialog) {
+ Msg.info(this, "Waiting for GhidraGo to listen for new files...");
+ // show the dialog in a blocking action. This will throw a StopWaitingException
+ // if they answer No. Otherwise, they want to keep waiting.
+ dialog.showDialog();
+ }
+ executor.schedule(this, period, timeUnit);
+ }
+ catch (StopWaitingException e) {
+ this.stopWaitingException = e;
+ dispose();
+ }
+ catch (StartedGhidraProcessExitedException e) {
+ this.startedGhidraProcessExitedException = e;
+ dispose();
+ }
+ catch (RejectedExecutionException e) {
+ // this is okay, executor has been shutdown
+ dispose();
+ }
+ }
+ else {
+ dispose();
+ }
+ }
+ catch (Exception e) {
+ Swing.runNow(() -> Msg.showError(this, null, "GhidraGo Unable to Check For Listener",
+ "GhidraGo could not check for a listener.", e));
+ dispose();
+ }
+ }
+
+ /**
+ * checks to see if Ghidra process exited early, In the event Ghidra exits early,
+ * a runtime exception is thrown
+ * @param p Ghidra process
+ * @throws StartedGhidraProcessExitedException if the Ghidra process has an exit value
+ */
+ private void checkProcessDidNotExit(Process p) throws StartedGhidraProcessExitedException {
+ if (p != null) {
+ try {
+ int exitValue = p.exitValue();
+ if (exitValue != 0)
+ throw new StartedGhidraProcessExitedException(exitValue);
+ }
+ catch (IllegalThreadStateException e) {
+ // this is okay, ghidraRun hasn't exited
+ }
+ }
+ }
+
+ @Override
+ public void startChecking(int delay, TimeUnit delayTimeUnit) throws StopWaitingException {
+ dialog.reset();
+ try {
+ // start thread to check frequently
+ super.startChecking(delay, delayTimeUnit);
+
+ // start thread for showing dialog
+ executor.schedule(
+ new CheckForListenerRunnable(executor, process, WAIT_FOR_LISTENER_PERIOD_MS,
+ TimeUnit.MILLISECONDS, checkCondition),
+ WAIT_FOR_LISTENER_DELAY_MS, TimeUnit.MILLISECONDS);
+ }
+ catch (RejectedExecutionException e) {
+ if (dialog.isAnsweredNo()) {
+ throw new StopWaitingException();
+ }
+ }
+ }
+
+ @Override
+ public void awaitTermination()
+ throws StopWaitingException, StartedGhidraProcessExitedException {
+ try {
+ executor.awaitTermination(MAX_WAIT_FOR_LISTENER_MIN, TimeUnit.MINUTES);
+
+ if (dialog.isAnsweredNo()) {
+ throw new StopWaitingException();
+ }
+
+ if (this.stopWaitingException != null) {
+ throw this.stopWaitingException;
+ }
+ if (this.startedGhidraProcessExitedException != null) {
+ throw this.startedGhidraProcessExitedException;
+ }
+ }
+ catch (InterruptedException e) {
+ // this is okay
+ }
+ }
+
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckPeriodicallyRunnable.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckPeriodicallyRunnable.java
new file mode 100644
index 0000000000..5d223965ce
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckPeriodicallyRunnable.java
@@ -0,0 +1,71 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.ipc;
+
+import java.util.concurrent.*;
+
+import ghidra.app.plugin.core.go.dialog.GhidraGoWaitForListenerDialog;
+import ghidra.app.plugin.core.go.exception.StartedGhidraProcessExitedException;
+import ghidra.app.plugin.core.go.exception.StopWaitingException;
+import ghidra.util.Swing;
+
+public abstract class CheckPeriodicallyRunnable implements Runnable {
+
+ protected static GhidraGoWaitForListenerDialog dialog = new GhidraGoWaitForListenerDialog();
+
+ protected boolean showDialog;
+ protected int period;
+ protected TimeUnit timeUnit;
+ protected ScheduledExecutorService executor;
+ protected Callable checkCondition;
+
+ public CheckPeriodicallyRunnable(boolean showDialog,
+ int period, TimeUnit timeUnit, Callable checkCondition) {
+ this.showDialog = showDialog;
+ this.period = period;
+ this.timeUnit = timeUnit;
+ this.checkCondition = checkCondition;
+
+ // two threads; one for checking quickly without showing dialog, another for showing dialog
+ this.executor = Executors.newScheduledThreadPool(2);
+ }
+
+ protected CheckPeriodicallyRunnable(ScheduledExecutorService executor, boolean showDialog,
+ int period,
+ TimeUnit timeUnit, Callable checkCondition) {
+ this(showDialog, period, timeUnit, checkCondition);
+ this.executor = executor;
+ }
+
+ /**
+ * Begins checking the check condition in a thread
+ * @param delay the amount of time to wait to being checking
+ * @param delayTimeUnit the units for the amount of time
+ * @throws StopWaitingException in the event a dialog is answered to stop waiting
+ */
+ public void startChecking(int delay,
+ TimeUnit delayTimeUnit) throws StopWaitingException {
+ executor.schedule(this, delay, delayTimeUnit);
+ }
+
+ public abstract void awaitTermination()
+ throws StopWaitingException, StartedGhidraProcessExitedException;
+
+ public void dispose() {
+ executor.shutdownNow();
+ Swing.runNow(dialog::close);
+ }
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPC.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPC.java
new file mode 100644
index 0000000000..62c6db1083
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPC.java
@@ -0,0 +1,109 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.ipc;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.*;
+import java.nio.file.Path;
+import java.util.function.Supplier;
+
+import ghidra.app.plugin.core.go.exception.UnableToGetLockException;
+import ghidra.framework.Application;
+import ghidra.util.Msg;
+import ghidra.util.Swing;
+import utilities.util.FileUtilities;
+
+/**
+ * Ghidra Go Inter-Process Communication
+ */
+public abstract class GhidraGoIPC {
+
+ protected final Path channelPath =
+ Path.of(Application.getUserTempDirectory().getPath(), "ghidraGo");
+ protected final Path urlFilesPath = channelPath.resolve("urls");
+
+ protected final Path listenerLockPath = channelPath.resolve("listenerLock");
+ protected final Path listenerReadyLockPath = channelPath.resolve("listenerReadyLock");
+ protected final Path senderLockPath = channelPath.resolve("senderLock");
+
+ protected GhidraGoIPC() throws IOException {
+ // make the directories that will be needed
+ try {
+ FileUtilities.checkedMkdir(channelPath.toFile());
+ FileUtilities.checkedMkdir(urlFilesPath.toFile());
+ }
+ catch (IOException e) {
+ Msg.error(this, "Unable to create IPC directories.");
+ throw e;
+ }
+ }
+
+ public abstract void dispose();
+
+ /**
+ * @return true if a Ghidra is listening and ready. false otherwise
+ */
+ public boolean isGhidraListening() {
+ if (listenerLockPath.toFile().exists() && listenerReadyLockPath.toFile().exists()) {
+ return isFileLocked(listenerLockPath) && isFileLocked(listenerReadyLockPath);
+ }
+ return false;
+ }
+
+ private boolean isFileLocked(Path lockPath) {
+ try {
+ return !doLockedAction(lockPath, false, () -> true);
+ }
+ catch (OverlappingFileLockException | UnableToGetLockException e) {
+ return true;
+ }
+ }
+
+ /**
+ * perform the given action after acquiring the client lock successfully. This method is used
+ * to ensure that only one actor for the given lock path is performing the action.
+ * @param lockPath the path of the file to lock
+ * @param action the action taken after acquiring the lock.
+ * @param waitForLock if true blocks until the lock is acquired. otherwise, if the lock can't be
+ * acquired, the method returns false and does not do any blocking actions
+ * @return true if the action succeeded. false otherwise.
+ * @throws OverlappingFileLockException if another process currently controls the lock
+ * @throws UnableToGetLockException if the lock was unobtainable
+ */
+ public static boolean doLockedAction(Path lockPath, boolean waitForLock,
+ Supplier action)
+ throws OverlappingFileLockException, UnableToGetLockException {
+ try (FileOutputStream fos = new FileOutputStream(lockPath.toFile());
+ FileChannel channel = fos.getChannel();
+ FileLock lock = waitForLock ? channel.lock() : channel.tryLock();) {
+ if (lock == null) {
+ throw new UnableToGetLockException();
+ }
+ return action.get();
+ }
+ catch (FileLockInterruptionException e) {
+ // this is okay, user interrupted locking action
+ }
+ catch (IOException e) {
+ Swing.runNow(
+ () -> Msg.showError(GhidraGoIPC.class, null, "Could not perform exclusive action",
+ "Another process is currently holding the lock at " + lockPath, e));
+ }
+ return false;
+ }
+
+}
diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoListener.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoListener.java
new file mode 100644
index 0000000000..a2257d78e6
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoListener.java
@@ -0,0 +1,182 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.ipc;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.URL;
+import java.nio.channels.FileLockInterruptionException;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.*;
+import java.util.function.Consumer;
+
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.app.plugin.core.go.exception.UnableToGetLockException;
+import ghidra.framework.main.AppInfo;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.util.Msg;
+import ghidra.util.Swing;
+
+public class GhidraGoListener extends GhidraGoIPC implements Runnable {
+ public static int WAIT_FOR_ACTIVE_PROJECT_TIMEOUT_S = 30;
+
+ private Thread t;
+ private Consumer onNewUrl;
+
+ /**
+ * Begin listening for urls in a non-blocking thread. If a listener already exists, the thread
+ * will wait until no listener exists and attempt to get the lock. Once the lock has been acquired
+ * the listener will start watching for new urls and create a ready lock. Upon a new url being found,
+ * the onNewUrl Consumer will be executed.
+ * @param onNewUrl consumer method to execute upon finding a new url
+ * @throws IOException if the Runnable cannot be created
+ */
+ public GhidraGoListener(Consumer onNewUrl) throws IOException {
+ super();
+ this.onNewUrl = onNewUrl;
+ t = new Thread(this, "GhidraGo Handler");
+ t.start();
+ }
+
+ @Override
+ public void run() {
+ try {
+ doLockedAction(listenerLockPath, true, () -> {
+ try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
+ urlFilesPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
+ Msg.info(this, "Listening for GhidraGo Requests.");
+ doLockedAction(listenerReadyLockPath, true, () -> {
+ try {
+ WatchKey key;
+ while ((key = watchService.take()) != null) {
+ for (WatchEvent> event : key.pollEvents()) {
+ // only process events that are not null. null events could happen
+ // when event.kind() is OVERFLOW.
+ if (event.context() != null) {
+ Msg.trace(this, event.context() + " is a new file!");
+ // get the url from the new url
+ Path urlFilePath =
+ urlFilesPath.resolve(event.context().toString());
+ URL url = getGhidraURL(urlFilePath);
+ urlFilePath.toFile().delete();
+ if (url != null) {
+ onNewUrl.accept(url);
+ }
+ }
+ }
+ key.reset();
+ }
+ }
+ catch (InterruptedException e) {
+ // watch service interrupted
+ }
+ return true;
+ });
+ }
+ catch (FileLockInterruptionException | InterruptedIOException e) {
+ return false;
+ }
+ catch (IOException | UnableToGetLockException e) {
+ Swing.runNow(() -> Msg.showError(this, null,
+ "GhidraGo Unable to Watch for New GhidraURL's", e));
+ return false;
+ }
+ catch (ClosedWatchServiceException e) {
+ // do nothing
+ }
+ finally {
+ Msg.info(this, "No longer listening for GhidraGo Requests.");
+ }
+ return true;
+ });
+ }
+ catch (OverlappingFileLockException | UnableToGetLockException e) {
+ Swing.runNow(
+ () -> Msg.showError(this, null, "GhidraGo Unable to Watch for New GhidraURL's", e));
+ }
+ }
+
+ /**
+ * Returns a URL given the first argument from GhidraGo.
+ * @param ghidraGoArgument could be a GhidraURL or a projectFilePath.
+ * @return the GhidraURL to a program
+ * @throws IllegalArgumentException in the event the given GhidraGo argument is invalid
+ */
+ private URL toURL(String ghidraGoArgument) throws IllegalArgumentException {
+ try {
+
+ if (ghidraGoArgument.startsWith(GhidraURL.PROTOCOL + ":?")) {
+ String projectFilePath =
+ ghidraGoArgument.substring(ghidraGoArgument.indexOf("?") + 1);
+ if (!projectFilePath.startsWith("/")) {
+ projectFilePath = "/" + projectFilePath;
+ }
+ return GhidraURL.makeURL(AppInfo.getActiveProject().getProjectLocator(),
+ projectFilePath, null);
+ }
+ return GhidraURL.toURL(ghidraGoArgument);
+
+ }
+ catch (IllegalArgumentException e) {
+ if (ghidraGoArgument.startsWith(GhidraURL.PROTOCOL + "://") ||
+ AppInfo.getActiveProject() == null)
+ throw e;
+ if (!ghidraGoArgument.startsWith("/")) {
+ ghidraGoArgument = "/" + ghidraGoArgument;
+ }
+ return GhidraURL.makeURL(AppInfo.getActiveProject().getProjectLocator(),
+ ghidraGoArgument, null);
+ }
+ }
+
+ /**
+ * Reads the url file for the url string and returns it.
+ * @param urlFilePath the path for the url file
+ * @return the url string, or null if the file cannot be read.
+ */
+ private URL getGhidraURL(Path urlFilePath) {
+ try {
+ String urlContents = new String(Files.readAllBytes(urlFilePath));
+ if (StringUtils.isEmpty(urlContents)) {
+ Swing.runNow(() -> Msg.showError(GhidraGoIPC.class, null,
+ "GhidraGo Empty GhidraURL Read",
+ "The GhidraURL read from url file was null or empty. This should not happen, " +
+ "ensure ghidraGo is being used properly."));
+ return null;
+ }
+
+ return toURL(urlContents);
+ }
+ catch (IOException e) {
+ Swing.runNow(() -> Msg.showError(GhidraGoIPC.class, null, "GhidraGo Error",
+ "Failed to read the url from " + urlFilePath, e));
+ }
+ catch (IllegalArgumentException e) {
+ Swing.runNow(
+ () -> Msg.showError(GhidraGoIPC.class, null, "GhidraGo Illegal Argument Given", e));
+ }
+ return null;
+ }
+
+ @Override
+ public void dispose() {
+ if (t != null) {
+ t.interrupt();
+ }
+ }
+
+}
diff --git a/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java
new file mode 100644
index 0000000000..1e78a4c725
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java
@@ -0,0 +1,135 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go;
+
+import static org.junit.Assert.*;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import org.junit.*;
+
+import docking.AbstractErrDialog;
+import ghidra.GhidraApplicationLayout;
+import ghidra.GhidraGo;
+import ghidra.app.plugin.core.go.ipc.CheckForFileProcessedRunnable;
+import ghidra.app.plugin.core.go.ipc.CheckForListenerRunnable;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainFolder;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.program.model.listing.Program;
+import ghidra.test.*;
+import ghidra.util.Swing;
+import ghidra.util.task.TaskMonitor;
+
+public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private TestEnv env;
+ private PluginTool tool;
+ private GhidraGo ghidraGo;
+ private URL url;
+
+ private GhidraApplicationLayout layout;
+
+ @Before
+ public void setUp() throws Exception {
+
+ env = new TestEnv();
+ tool = env.getFrontEndTool();
+ tool.addPlugin(GhidraGoPlugin.class.getName());
+ showTool(tool);
+
+ DomainFolder rootFolder = env.getProject().getProjectData().getRootFolder();
+
+ Program p = createNotepadProgram();
+
+ rootFolder.createFile("notepad", p, TaskMonitor.DUMMY);
+
+ env.release(p);
+
+ url = GhidraURL.makeURL(env.getProjectManager().getActiveProject().getProjectLocator(),
+ "/notepad", null);
+
+ layout = (GhidraApplicationLayout) createApplicationLayout();
+ ghidraGo = new GhidraGo();
+
+
+ CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 1000;
+ CheckForFileProcessedRunnable.MAX_WAIT_FOR_PROCESSING_MIN = 1;
+ CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_PERIOD_MS = 10;
+
+ CheckForListenerRunnable.WAIT_FOR_LISTENER_DELAY_MS = 1000;
+ CheckForListenerRunnable.MAX_WAIT_FOR_LISTENER_MIN = 1;
+ CheckForListenerRunnable.WAIT_FOR_LISTENER_PERIOD_MS = 10;
+ }
+
+ private Program createNotepadProgram() throws Exception {
+ ClassicSampleX86ProgramBuilder builder =
+ new ClassicSampleX86ProgramBuilder("notepad", false, this);
+
+ return builder.getProgram();
+ }
+
+ @After
+ public void tearDown() {
+ env.dispose();
+ }
+
+ @Test
+ public void testProcessingUrl() throws Exception {
+ Swing.runLater(() -> {
+ try {
+ ghidraGo.launch(layout, new String[] { url.toString() });
+ }
+ catch (Exception e) {
+ // empty
+ }
+ });
+ waitForSwing();
+ waitFor(() -> Arrays.asList(tool.getToolServices().getRunningTools())
+ .stream()
+ .map(PluginTool::getName)
+ .anyMatch(Predicate.isEqual("CodeBrowser")));
+ Optional cb = Arrays.asList(tool.getToolServices().getRunningTools())
+ .stream()
+ .filter(p -> p.getName().equals("CodeBrowser"))
+ .findFirst();
+
+ assertTrue(cb.isPresent());
+ assertTrue(Arrays.asList(cb.get().getDomainFiles())
+ .stream()
+ .map(DomainFile::getName)
+ .anyMatch(Predicate.isEqual("notepad")));
+ }
+
+ @Test
+ public void testLaunchingWithInvalidUrl() throws Exception {
+ Swing.runLater(() -> {
+ try {
+ ghidraGo.launch(layout, new String[] { "ghidra:/test" });
+ }
+ catch (Exception e) {
+ // empty
+ }
+ });
+ AbstractErrDialog err = waitForErrorDialog();
+ assertEquals("Unsupported Content", err.getTitle());
+ }
+
+}
diff --git a/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPCTest.java b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPCTest.java
new file mode 100644
index 0000000000..0f213304c2
--- /dev/null
+++ b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPCTest.java
@@ -0,0 +1,160 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.go.ipc;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.junit.*;
+
+import docking.DialogComponentProvider;
+import ghidra.app.plugin.core.go.GhidraGoSender;
+import ghidra.app.plugin.core.go.dialog.GhidraGoWaitForListenerDialog;
+import ghidra.app.plugin.core.go.exception.StopWaitingException;
+import ghidra.test.AbstractGhidraHeadedIntegrationTest;
+import ghidra.test.TestEnv;
+import ghidra.util.Msg;
+
+public class GhidraGoIPCTest extends AbstractGhidraHeadedIntegrationTest {
+ private GhidraGoSender sender;
+ private GhidraGoListener listener;
+ private String url = "ghidra://testing/testProject";
+ private TestEnv env;
+
+ public GhidraGoIPCTest() throws IOException {
+ sender = new GhidraGoSender();
+ }
+
+ @Before
+ public void setUp() throws IOException {
+ env = new TestEnv(); // need this so that Application is initialized
+
+ CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 1000;
+ CheckForFileProcessedRunnable.MAX_WAIT_FOR_PROCESSING_MIN = 1;
+ CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_PERIOD_MS = 10;
+
+ CheckForListenerRunnable.WAIT_FOR_LISTENER_DELAY_MS = 1000;
+ CheckForListenerRunnable.MAX_WAIT_FOR_LISTENER_MIN = 1;
+ CheckForListenerRunnable.WAIT_FOR_LISTENER_PERIOD_MS = 10;
+ }
+
+ @After
+ public void tearDown() {
+ if (env != null) {
+ env.dispose();
+ }
+
+ sender.dispose();
+ if (listener != null) {
+ listener.dispose();
+ }
+
+ waitFor(() -> !sender.isGhidraListening());
+ }
+
+ public Thread sendExpectingStopWaitingException() {
+ Thread t = new Thread(() -> {
+ try {
+ sender.send(url);
+ assertFalse(true); // fail
+ }
+ catch (StopWaitingException e) {
+ // passed
+ }
+ });
+ t.start();
+ return t;
+ }
+
+ public Thread sendExpectingSuccess() {
+ Thread t = new Thread(() -> {
+ try {
+ sender.send(url);
+ // passed
+ }
+ catch (StopWaitingException e) {
+ assertFalse(true); // fail
+ }
+ });
+ t.start();
+ return t;
+ }
+
+ @Test
+ public void testSendingWithNoListener() throws InterruptedException {
+ // given no listener is listening and the timeout is 0
+ waitFor(() -> !sender.isGhidraListening());
+ CheckForListenerRunnable.WAIT_FOR_LISTENER_DELAY_MS = 0;
+ CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 0;
+
+ // then a the wait for listener dialog should appear on send
+ Thread t = sendExpectingStopWaitingException();
+
+ DialogComponentProvider dialog =
+ waitForDialogComponent(GhidraGoWaitForListenerDialog.class);
+
+ // when Wait is pressed, the dialog should reappear with no timeout
+ pressButtonByText(dialog, "Wait");
+
+ // then pressing No when the dialog appears again should stop waiting
+ dialog = waitForDialogComponent(GhidraGoWaitForListenerDialog.class);
+ pressButtonByText(dialog, "No");
+
+ t.join();
+ }
+
+ @Test
+ public void testSendingWithListener() throws IOException, InterruptedException {
+ // given a listener is listening and processing new urls
+ listener = new GhidraGoListener((passedURL) -> {
+ Msg.info(this, "Found " + passedURL + " in test");
+ });
+ waitFor(sender::isGhidraListening);
+
+ // then the sender should not throw an exception when sending a url
+ Thread t = sendExpectingSuccess();
+ t.join();
+ }
+
+ @Test
+ public void testInterruptingListener() throws IOException, InterruptedException {
+ // given a listener is listening and processing new urls
+ listener = new GhidraGoListener((passedURL) -> {
+ Msg.info(this, "Found " + passedURL + " in test");
+ });
+ waitFor(sender::isGhidraListening);
+
+ // then sending a url before disposing the listener should succeed
+ Thread t = sendExpectingSuccess();
+ t.join();
+
+ // when the listener is disposed
+ listener.dispose();
+
+ // given no listener is listening and the timeout is 0
+ waitFor(() -> !sender.isGhidraListening());
+ CheckForListenerRunnable.WAIT_FOR_LISTENER_DELAY_MS = 0;
+ CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 0;
+
+ // then sending a url should fail
+ t = sendExpectingStopWaitingException();
+ DialogComponentProvider dialog =
+ waitForDialogComponent(GhidraGoWaitForListenerDialog.class);
+ pressButtonByText(dialog, "No");
+ t.join();
+ }
+}
diff --git a/Ghidra/RuntimeScripts/Common/support/GhidraGo/ghidraGoREADME.html b/Ghidra/RuntimeScripts/Common/support/GhidraGo/ghidraGoREADME.html
new file mode 100644
index 0000000000..2c6fc3e372
--- /dev/null
+++ b/Ghidra/RuntimeScripts/Common/support/GhidraGo/ghidraGoREADME.html
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+GhidraGo README
+
+
+
+
+ GhidraGo is a mechanism to cause Ghidra to display a previously imported program within a
+ local or multi-user project using a ghidraURL hyperlink similar to an http reference. In
+ practice ghidraURL's work very similarly to selecting a URL reference which displays a PDF.
+ Once setup correctly, GhidraURL links can be placed in web pages, external project
+ documentation files, or any other place a URL hyperlink can be placed.
+
+
+ When a GhidraURL is selected, GhidraGo will startup Ghidra if it isn't already running as
+ well as prompt to login to the multi-user project if necessary. The program is displayed in
+ the default tool, usually the codebrowser, and can be configured to re-use an open default
+ tool or to use a new default tool. The GhidraURL must currently be locating a DomainFile
+ that is either in a Remote, Shared project, or a local project.
+
+
+ GhidraGo is a combination of a command line program to send a link, a plugin running within
+ the Ghidra project manager, and the configuration of the default handling for the ghidraURL
+ within the user environment. The ghidraURL is sent as the first and only parameter to the
+ ghidraGo command line interface.
+
+
+ GhidraGo passes information through a simple filesystem mechanism vice an open port for
+ security and simplicity. GhidraGo works on Windows, Linux, and MacOS.
+
+
+
GhidraURL's have the format:
+
+
+ Remote Ghidra Server File:
+ ghidra://<host>[:<port>]/<repository-name>/<program-path>
+ [#<address-or-symbol-ref>]
+
+
Example: ghidra://hostname/Repo/notepad.exe#main
+
+
+ Local Ghidra Project File:
+ ghidra:/[<project-path>/]<project-name>?/<program-path>
+ [#<address-or-symbol-ref>]
+
Choose File > Configuration in the Project Window (not the Codebrowser Window)
+
Click the Plug Icon in the upper right to display all plugins
+
Search for GhidraGoPlugin and select it
+
Press OK
+
+
Ghidra is now configured to listen to GhidraGo Requests. You can execute a GhidraGo request
+ using the "ghidraGo" shell/batch script in
+ /path/to/ghidra/support/GhidraGo/ghidraGo
+ Configuring your platform to handle the protocol is what
+ enables the ghidraGo command line interface to be associated with a ghidraURL. Once
+ configured, clicking hyperlinks that start with the protocol
+ will execute the ghidraGo CLI with that hyperlink as the first argument. The
+ configuration is platform specific.
+
+
+ *NOTE: changes to your path to ghidra, such as upgrading ghidra to a new version,
+ will require the path you set in this configuration to be updated.
+
After the steps above, you should be able to click a GhidraURL href, get the same
+ xdg-open prompt, and upon clicking "Open xdg-open" GhidraGo should execute and open
+ Ghidra to the given GhidraURL.
Open Script Editor and past the following into the editor.
+
+
+ on open location schemeUrl
+ set ghidraUrl to quoted form of schemeUrl
+ do shell script "/path/to/ghidraGo " & ghidraUrl
+ end open location
+
+
+
Save the script as an Application named GhidraGo in either
+ /Applications or ~/Applications
+
Right click on the saved Application and click Show Package Contents
+
Open Contents > Info.plist and under
+ <string>com.apple.ScriptEditor.id.GhidraGo</string>
+ paste the following:
+
+Last modified: Oct 26 2023
+
+
+
diff --git a/Ghidra/RuntimeScripts/Linux/support/GhidraGo/ghidraGo b/Ghidra/RuntimeScripts/Linux/support/GhidraGo/ghidraGo
new file mode 100755
index 0000000000..ecbe275fac
--- /dev/null
+++ b/Ghidra/RuntimeScripts/Linux/support/GhidraGo/ghidraGo
@@ -0,0 +1,24 @@
+#!/bin/bash
+#
+# Command-line script for starting GhidraGo
+
+# launch mode (fg, bg, debug, debug-suspend, debug-suspend-launcher)
+LAUNCH_MODE=fg
+
+# Resolve symbolic link if present and get the directory this script lives in.
+# NOTE: "readlink -f" is best but works on Linux only, "readlink" will only work if your PWD
+# contains the link you are calling (which is the best we can do on macOS), and the "echo" is the
+# fallback, which doesn't attempt to do anything with links.
+SCRIPT_FILE="$(readlink -f "$0" 2>/dev/null || readlink "$0" 2>/dev/null || echo "$0")"
+
+# in dev mode, SCRIPT_FILE is Ghidra/RuntimeScripts_U/Linux/support/GhidraGo/ghidraGo
+# in release mode, SCRIPT_FILE is support/GhidraGo/ghidraGo
+# BASE_DIR is the base directory of ext-u.
+# Initally assume to be in release mode.
+BASE_DIR="${SCRIPT_FILE%/*}/../.."
+
+if [ ! -d "$BASE_DIR/Ghidra" ]; then
+ BASE_DIR="${BASE_DIR}/../../../../ghidra/Ghidra/RuntimeScripts/Linux"
+fi
+
+"${BASE_DIR}"/support/launch.sh $LAUNCH_MODE jdk GhidraGo "" "" ghidra.GhidraGo "$@"
diff --git a/Ghidra/RuntimeScripts/Windows/support/GhidraGo/ghidraGo.bat b/Ghidra/RuntimeScripts/Windows/support/GhidraGo/ghidraGo.bat
new file mode 100644
index 0000000000..091f8bfb8b
--- /dev/null
+++ b/Ghidra/RuntimeScripts/Windows/support/GhidraGo/ghidraGo.bat
@@ -0,0 +1,19 @@
+:: GhidraGo launch
+
+@echo off
+setlocal
+
+set SCRIPT_FILE=%~dp0%
+:: in dev mode, SCRIPT_FILE is Ghidra/RuntimeScripts_U/Windows/support/GhidraGo/ghidraGo
+:: in release mode, SCRIPT_FILE is support/GhidraGo/ghidraGo
+:: BASE_DIR is the base directory of ext-u
+:: Initially assume to be in release mode.
+set BASE_DIR=%SCRIPT_FILE:~0,-1%\..\..
+
+if not exist %BASE_DIR%\Ghidra (
+ :: set base dir to location of windows base script dir
+ set BASE_DIR=%BASE_DIR%\..\..\..\..\ghidra\Ghidra\RuntimeScripts\Windows
+)
+
+call "%BASE_DIR%\support\launch.bat" fg jdk GhidraGo "" "" ghidra.GhidraGo "%*"
+
diff --git a/Ghidra/RuntimeScripts/certification.manifest b/Ghidra/RuntimeScripts/certification.manifest
index 58661dbd10..52d9b254d8 100644
--- a/Ghidra/RuntimeScripts/certification.manifest
+++ b/Ghidra/RuntimeScripts/certification.manifest
@@ -3,6 +3,7 @@
Common/server/jaas.conf||GHIDRA||||END|
Common/server/server.conf||GHIDRA||||END|
Common/server/svrREADME.html||GHIDRA||||END|
+Common/support/GhidraGo/ghidraGoREADME.html||GHIDRA||||END|
Common/support/analyzeHeadlessREADME.html||GHIDRA||||END|
Common/support/buildGhidraJarREADME.txt||GHIDRA||||END|
Common/support/debug.log4j.xml||GHIDRA||||END|
@@ -12,6 +13,7 @@ Linux/server/ghidraSvr||GHIDRA||||END|
Linux/server/svrAdmin||GHIDRA||||END|
Linux/server/svrInstall||GHIDRA||||END|
Linux/server/svrUninstall||GHIDRA||||END|
+Linux/support/GhidraGo/ghidraGo||GHIDRA||||END|
Linux/support/analyzeHeadless||GHIDRA||||END|
Linux/support/buildGhidraJar||GHIDRA||||END|
Linux/support/buildNatives||GHIDRA||||END|
@@ -25,6 +27,7 @@ Windows/server/ghidraSvr.bat||GHIDRA||||END|
Windows/server/svrAdmin.bat||GHIDRA||||END|
Windows/server/svrInstall.bat||GHIDRA||||END|
Windows/server/svrUninstall.bat||GHIDRA||||END|
+Windows/support/GhidraGo/ghidraGo.bat||GHIDRA||||END|
Windows/support/README_createPdbXmlFiles.txt||GHIDRA||||END|
Windows/support/analyzeHeadless.bat||GHIDRA||||END|
Windows/support/buildGhidraJar.bat||GHIDRA||||END|