Edits the selected script in Visual Studio Code.
+
+
Before a script can be edited in
+ Visual Studio Code, a Visual Studio Code executable path must be defined in the Tool's
+ Visual Studio Code Integration
+ options if Visual Studio Code is installed in a non-default location.
Ghidra is capable of integrating with an existing Visual Studio Code installation to aid
+ in the development of Ghidra scripts and modules.
+
+
Visual Studio Code Integration Tool Options
+
The following Front-End tool options (Edit -> Tool Options) may need to be configured for
+ Ghidra to successfully launch Visual Studio Code on your platform.
+
+
+
+
+
+
+
+
Tool Options
+
+
+
+
+
Option
+
Description
+
+
+
+
Visual Studio Code Executable Path
+
Path to a Visual Studio Code executable file. It defaults
+ to the most commonly used location on your specific platform.
+
+
+
+
+
+
Create Visual Studio Code Module Project
+
This action creates a new Visual Studio Code project folder which can be used as a convenient
+ starting point to develop a new Ghidra module. The new project will be linked against the
+ version of Ghidra that was used to create the project. Once the project is created, the
+ associated Ghidra version cannot change. This behavior may become more flexible in the
+ future.
+
+
Visual Studio Code launchers are provided which will allow you to debug your module's code.
+ Also, a Gradle task named ghidra/distributeExtension is provided that will allow you to
+ build a distributable Ghidra extension.
+
+
+
+
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/eclipse/EclipseIntegrationOptionsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/eclipse/EclipseIntegrationOptionsPlugin.java
index 811d3545ae..a89678bd35 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/eclipse/EclipseIntegrationOptionsPlugin.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/eclipse/EclipseIntegrationOptionsPlugin.java
@@ -4,9 +4,9 @@
* 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.
@@ -35,7 +35,7 @@ import ghidra.util.HelpLocation;
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.COMMON,
shortDescription = "Eclipse Integration Options",
- description = "Options Eclipse Integration"
+ description = "Options for Eclipse Integration"
)
//@formatter:on
public class EclipseIntegrationOptionsPlugin extends Plugin implements ApplicationLevelOnlyPlugin {
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java
index d7170bf635..d7eff4e9ac 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java
@@ -4,9 +4,9 @@
* 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.
@@ -226,6 +226,9 @@ class GhidraScriptActionManager {
createScriptAction("EditEclipse", "Edit with Eclipse", "Edit Script with Eclipse",
new GIcon("icon.plugin.scriptmanager.edit.eclipse"), null, provider::editScriptEclipse);
+ createScriptAction("EditVSCode", "Edit with VSCode", "Edit Script with Visual Studio Code",
+ new GIcon("icon.plugin.scriptmanager.edit.vscode"), null, provider::editScriptVSCode);
+
keyBindingAction =
createScriptAction("Key Binding", "Assign Key Binding", "Assign Key Binding",
new GIcon("icon.plugin.scriptmanager.keybinding"), null,
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java
index 9e98940341..b11b61e6a6 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java
@@ -927,6 +927,20 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
plugin.tryToEditFileInEclipse(script);
}
+ void editScriptVSCode() {
+ ResourceFile script = getSelectedScript();
+ if (script == null) {
+ plugin.getTool().setStatusInfo("Script is null.");
+ return;
+ }
+ if (!script.exists()) {
+ plugin.getTool().setStatusInfo("Script " + script.getName() + " does not exist.");
+ return;
+ }
+
+ plugin.tryToEditFileInVSCode(script);
+ }
+
GhidraScriptEditorComponentProvider editScriptInGhidra(ResourceFile script) {
GhidraScriptEditorComponentProvider editor = editorMap.get(script);
if (editor == null) {
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java
index 1aba81aaae..cc3eb2302c 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java
@@ -4,9 +4,9 @@
* 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.
@@ -155,6 +155,13 @@ public class GhidraScriptMgrPlugin extends ProgramPlugin implements GhidraScript
}
}
+ @Override
+ public boolean tryToEditFileInVSCode(ResourceFile file) {
+ VSCodeIntegrationService service = tool.getService(VSCodeIntegrationService.class);
+ service.launchVSCode(file.getFile(false));
+ return true;
+ }
+
@Override
protected void programClosed(Program program) {
provider.programClosed(program);
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationOptionsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationOptionsPlugin.java
new file mode 100644
index 0000000000..749be4ba4d
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationOptionsPlugin.java
@@ -0,0 +1,77 @@
+/* ###
+ * 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.vscode;
+
+import java.io.File;
+
+import ghidra.app.CorePluginPackage;
+import ghidra.app.plugin.PluginCategoryNames;
+import ghidra.framework.OperatingSystem;
+import ghidra.framework.main.ApplicationLevelOnlyPlugin;
+import ghidra.framework.options.OptionType;
+import ghidra.framework.options.ToolOptions;
+import ghidra.framework.plugintool.*;
+import ghidra.framework.plugintool.util.PluginStatus;
+import ghidra.util.HelpLocation;
+
+/**
+ * {@link Plugin} responsible for registering Visual Studio Code-related options
+ */
+//@formatter:off
+@PluginInfo(
+ status = PluginStatus.RELEASED,
+ packageName = CorePluginPackage.NAME,
+ category = PluginCategoryNames.COMMON,
+ shortDescription = "Visual Studio Code Integration Options",
+ description = "Options for Visual Studio Code Integration"
+)
+//@formatter:on
+public class VSCodeIntegrationOptionsPlugin extends Plugin implements ApplicationLevelOnlyPlugin {
+
+ public static final String PLUGIN_OPTIONS_NAME = "Visual Studio Code Integration";
+
+ public static final String VSCODE_EXE_PATH_OPTION = "Visual Studio Code Executable Path";
+ private static final String VSCODE_EXE_PATH_DESC = "Path to Visual Studio Code executable";
+ private static final File VSCODE_EXE_PATH_DEFAULT = getDefaultVSCodeExecutable();
+
+ public VSCodeIntegrationOptionsPlugin(PluginTool tool) {
+ super(tool);
+ }
+
+ @Override
+ public void init() {
+ super.init();
+ ToolOptions options = tool.getOptions(PLUGIN_OPTIONS_NAME);
+ options.registerOption(VSCODE_EXE_PATH_OPTION, OptionType.FILE_TYPE,
+ VSCODE_EXE_PATH_DEFAULT, null, VSCODE_EXE_PATH_DESC);
+ options.setOptionsHelpLocation(
+ new HelpLocation("VSCodeIntegration", "VSCodeIntegrationOptions"));
+ }
+
+ /**
+ * {@return the default Visual Studio Code executable location for the current platform}
+ */
+ private static File getDefaultVSCodeExecutable() {
+ return switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) {
+ case WINDOWS -> new File(System.getenv("LOCALAPPDATA"),
+ "Programs/Microsoft VS Code/bin/code.cmd");
+ case MAC_OS_X -> new File(
+ "/Applications/Visual Studio Code.app/Contents/MacOS/Electron");
+ case LINUX -> new File("/usr/bin/code");
+ default -> null;
+ };
+ }
+}
diff --git a/Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationPlugin.java
similarity index 53%
rename from Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java
rename to Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationPlugin.java
index e805374144..d322f664bb 100644
--- a/Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeIntegrationPlugin.java
@@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// Creates a new VSCode project for Ghidra script and module development.
-// @category Development
+package ghidra.app.plugin.core.vscode;
-import java.io.File;
-import java.io.IOException;
+import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
@@ -25,22 +23,81 @@ import org.apache.commons.io.*;
import com.google.gson.*;
+import docking.DockingWindowManager;
+import docking.action.builder.ActionBuilder;
+import docking.options.OptionsService;
+import docking.tool.ToolConstants;
+import docking.widgets.OptionDialog;
+import docking.widgets.values.ValuesMapDialog;
+import generic.theme.GIcon;
import ghidra.*;
-import ghidra.app.script.GhidraScript;
+import ghidra.app.CorePluginPackage;
+import ghidra.app.plugin.PluginCategoryNames;
+import ghidra.app.plugin.ProgramPlugin;
+import ghidra.app.services.VSCodeIntegrationService;
import ghidra.features.base.values.GhidraValuesMap;
import ghidra.framework.Application;
import ghidra.framework.ApplicationProperties;
-import ghidra.util.SystemUtilities;
+import ghidra.framework.main.AppInfo;
+import ghidra.framework.options.ToolOptions;
+import ghidra.framework.plugintool.*;
+import ghidra.framework.plugintool.util.PluginStatus;
+import ghidra.util.*;
+import ghidra.util.task.TaskLauncher;
import utilities.util.FileUtilities;
-public class VSCodeProjectScript extends GhidraScript {
+/**
+ * {@link Plugin} responsible integrating Ghidra with Visual Studio Code
+ */
+//@formatter:off
+@PluginInfo(
+ status = PluginStatus.RELEASED,
+ packageName = CorePluginPackage.NAME,
+ category = PluginCategoryNames.COMMON,
+ shortDescription = "Visual Studio Code Integration",
+ description = "Allows Ghidra to integrate with Visual Studio Code.",
+ servicesRequired = { OptionsService.class },
+ servicesProvided = { VSCodeIntegrationService.class }
+)
+//@formatter:on
+public class VSCodeIntegrationPlugin extends ProgramPlugin implements VSCodeIntegrationService {
+ private ToolOptions options;
private Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+ /**
+ * Create a new {@link VSCodeIntegrationPlugin}
+ *
+ * @param tool The associated {@link PluginTool tool}
+ */
+ public VSCodeIntegrationPlugin(PluginTool tool) {
+ super(tool);
+ }
+
@Override
- protected void run() throws Exception {
+ public void init() {
+ super.init();
+
+ options = AppInfo.getFrontEndTool().getOptions(
+ VSCodeIntegrationOptionsPlugin.PLUGIN_OPTIONS_NAME);
+
+ new ActionBuilder("CreateVSCodeModuleProject", name)
+ .menuPath(ToolConstants.MENU_TOOLS, "Create VSCode Module Project...")
+ .menuIcon(new GIcon("icon.plugin.scriptmanager.edit.vscode"))
+ .description("Creates a new Visual Studio Code module project.")
+ .helpLocation(new HelpLocation("VSCodeIntegration", "VSCodeModuleProject"))
+ .onAction(context -> showNewProjectDialog())
+ .buildAndInstall(tool);
+ }
+
+ /**
+ * Present the user with a dialog that allows them to select a name and location for their new
+ * Visual Studio Code Module Project
+ */
+ private void showNewProjectDialog() {
if (!SystemUtilities.isInReleaseMode()) {
- printerr("This script may only run from a built Ghidra release.");
+ Msg.showInfo(this, tool.getToolFrame(), name,
+ "This action may only run from a built Ghidra release.");
return;
}
@@ -50,27 +107,132 @@ public class VSCodeProjectScript extends GhidraScript {
GhidraValuesMap values = new GhidraValuesMap();
values.defineString(PROJECT_NAME_PROMPT);
values.defineDirectory(PROJECT_ROOT_PROMPT, new File(System.getProperty("user.home")));
- values = askValues("Setup New VSCode Project", null, values);
+ ValuesMapDialog dialog =
+ new ValuesMapDialog("Create New Visual Studio Code Module Project", null, values);
+ DockingWindowManager.showDialog(dialog);
+ if (dialog.isCancelled()) {
+ return;
+ }
+ values = (GhidraValuesMap) dialog.getValues();
String projectName = values.getString(PROJECT_NAME_PROMPT);
File projectRootDir = values.getFile(PROJECT_ROOT_PROMPT);
File projectDir = new File(projectRootDir, projectName);
if (projectDir.exists()) {
- printerr("Directory '%s' already exists...exiting".formatted(projectDir));
+ Msg.showError(this, tool.getToolFrame(), name,
+ "Directory '%s' already exists...exiting".formatted(projectDir));
return;
}
+ try {
+ createVSCodeModuleProject(projectDir);
+ Msg.showInfo(this, tool.getToolFrame(), name,
+ "Successfully created Visual Studio Code module project directory at: " +
+ projectDir);
+ }
+ catch (IOException e) {
+ Msg.showError(this, tool.getToolFrame(), name,
+ "Failed to create Visual Studio Code module project directory at: " + projectDir,
+ e);
+ }
+ }
+
+ @Override
+ public ToolOptions getVSCodeIntegrationOptions() {
+ return options;
+ }
+
+ @Override
+ public File getVSCodeExecutableFile() throws FileNotFoundException {
+ File vscodeExecutableFile =
+ options.getFile(VSCodeIntegrationOptionsPlugin.VSCODE_EXE_PATH_OPTION, null);
+ if (vscodeExecutableFile == null || !vscodeExecutableFile.isFile()) {
+ throw new FileNotFoundException(
+ "Visual Studio Code installation executable file does not exist.");
+ }
+ return vscodeExecutableFile;
+ }
+
+ @Override
+ public void launchVSCode(File file) {
+ TaskLauncher.launch(new VSCodeLauncherTask(this, file));
+ }
+
+ @Override
+ public void handleVSCodeError(String error, boolean askAboutOptions, Throwable t) {
+ if (askAboutOptions && !SystemUtilities.isInHeadlessMode()) {
+ SystemUtilities.runSwingNow(() -> {
+ int choice =
+ OptionDialog.showYesNoDialog(null, "Failed to launch Visual Studio Code",
+ error + "\nWould you like to verify your \"" +
+ VSCodeIntegrationOptionsPlugin.PLUGIN_OPTIONS_NAME +
+ "\" options now?");
+ if (choice == OptionDialog.YES_OPTION) {
+ AppInfo.getFrontEndTool()
+ .getService(OptionsService.class)
+ .showOptionsDialog(
+ VSCodeIntegrationOptionsPlugin.PLUGIN_OPTIONS_NAME, null);
+ }
+ });
+ }
+ else {
+ Msg.showError(VSCodeIntegrationPlugin.class, null,
+ "Failed to launch Visual Studio Code", error, t);
+ }
+ }
+
+ @Override
+ public void createVSCodeModuleProject(File projectDir) throws IOException {
+
File installDir = Application.getInstallationDirectory().getFile(false);
Map classpathSourceMap = getClasspathSourceMap();
- writeSettings(installDir, projectDir, classpathSourceMap);
- writeLaunch(installDir, projectDir, classpathSourceMap);
+
+ JsonObject settings = createSettings(installDir, projectDir, classpathSourceMap,
+ List.of("src/main/java", "ghidra_scripts"), "bin/main");
+ JsonObject launch =
+ createLaunch(installDir, projectDir, classpathSourceMap, "${workspaceFolder}");
+
+ File vscodeDir = new File(projectDir, ".vscode");
+ if (!FileUtilities.mkdirs(vscodeDir)) {
+ throw new IOException("Failed to create: " + vscodeDir);
+ }
+ File settingsFile = new File(vscodeDir, "settings.json");
+ File launchFile = new File(vscodeDir, "launch.json");
+ FileUtils.writeStringToFile(settingsFile, gson.toJson(settings), StandardCharsets.UTF_8);
+ FileUtils.writeStringToFile(launchFile, gson.toJson(launch), StandardCharsets.UTF_8);
+
writeSampleScriptJava(projectDir);
writeSampleScriptPyGhidra(projectDir);
writeSampleModule(installDir, projectDir);
+ }
- println("Successfully created VSCode project directory at: " + projectDir);
- println(
- "To debug, please close Ghidra and relaunch from the VSCode Ghidra launch configuration.");
+ @Override
+ public void addToVSCodeWorkspace(File workspaceFile, File projectDir) throws IOException {
+
+ File installDir = Application.getInstallationDirectory().getFile(false);
+ Map classpathSourceMap = getClasspathSourceMap();
+ JsonObject settings =
+ createSettings(installDir, projectDir, classpathSourceMap, List.of("."), null);
+ JsonObject launch = createLaunch(installDir, projectDir, classpathSourceMap, null);
+
+ JsonObject workspace;
+ if (workspaceFile.isFile()) {
+ String str = FileUtils.readFileToString(workspaceFile, StandardCharsets.UTF_8);
+ JsonElement element = JsonParser.parseString(str);
+ if (!(element instanceof JsonObject json)) {
+ throw new IOException("'%s' was not a JsonObject".formatted(workspaceFile));
+ }
+ workspace = addToExistingWorkspace(projectDir, json, settings, launch);
+ }
+ else {
+ workspace = addToExistingWorkspace(projectDir, createNewWorkspace(settings, launch),
+ settings, launch);
+ }
+
+ if (!FileUtilities.mkdirs(workspaceFile.getParentFile())) {
+ throw new IOException("Failed to create: " + workspaceFile.getParentFile());
+ }
+ FileUtils.writeStringToFile(workspaceFile, gson.toJson(workspace), StandardCharsets.UTF_8);
}
/**
@@ -94,21 +256,22 @@ public class VSCodeProjectScript extends GhidraScript {
}
/**
- * Write the .vscode/settings.json file
+ * Creates the VSCode settings json
*
* @param installDir The Ghidra installation directory
* @param projectDir The VSCode project directory
* @param classpathSourceMap The classpath/source map (see {@link #getClasspathSourceMap()})
+ * @param sourcePaths A {@link List} of source paths
+ * @param outputPath The output path (null for default)
+ * @return The VSCode settings json
* @throws IOException if an IO-related error occurs
*/
- private void writeSettings(File installDir, File projectDir,
- Map classpathSourceMap) throws IOException {
- File vscodeDir = new File(projectDir, ".vscode");
- File settingsFile = new File(vscodeDir, "settings.json");
+ private JsonObject createSettings(File installDir, File projectDir,
+ Map classpathSourceMap, List sourcePaths, String outputPath)
+ throws IOException {
String gradleVersion = Application
.getApplicationProperty(ApplicationProperties.APPLICATION_GRADLE_MIN_PROPERTY);
- String pythonInterpreterPath = System.getProperty("pyghidra.sys.prefix", null);
-
+
// Build settings json object
JsonObject json = new JsonObject();
json.addProperty("java.import.maven.enabled", false);
@@ -120,12 +283,13 @@ public class VSCodeProjectScript extends GhidraScript {
JsonArray sourcePathArray = new JsonArray();
json.add("java.project.sourcePaths", sourcePathArray);
- sourcePathArray.add("src/main/java");
- sourcePathArray.add("ghidra_scripts");
+ sourcePaths.forEach(sourcePathArray::add);
+
+ if (outputPath != null) {
+ json.addProperty("java.project.outputPath", outputPath);
+ }
- json.addProperty("java.project.outputPath", "bin/main");
JsonObject referencedLibrariesObject = new JsonObject();
-
json.add("java.project.referencedLibraries", referencedLibrariesObject);
JsonArray includeArray = new JsonArray();
referencedLibrariesObject.add("include", includeArray);
@@ -139,29 +303,22 @@ public class VSCodeProjectScript extends GhidraScript {
json.addProperty("python.analysis.stubPath",
new File(installDir, "docs/ghidra_stubs/typestubs").getAbsolutePath());
- if (pythonInterpreterPath != null) {
- json.addProperty("python.defaultInterpreterPath", pythonInterpreterPath);
- }
- // Write settings json object
- if (!FileUtilities.mkdirs(settingsFile.getParentFile())) {
- throw new IOException("Failed to create: " + settingsFile.getParentFile());
- }
- FileUtils.writeStringToFile(settingsFile, gson.toJson(json), StandardCharsets.UTF_8);
+ return json;
}
/**
- * Write the .vscode/launch.json file
+ * Creates the VSCode launch json
*
* @param installDir The Ghidra installation directory
* @param projectDir The VSCode project directory
* @param classpathSourceMap The classpath/source map (see {@link #getClasspathSourceMap()})
+ * @param externalModules The Ghidra external modules to pass to Ghidra (could be null)
+ * @return The VSCode launch json
* @throws IOException if an IO-related error occurs
*/
- private void writeLaunch(File installDir, File projectDir,
- Map classpathSourceMap) throws IOException {
- File vscodeDir = new File(projectDir, ".vscode");
- File launchFile = new File(vscodeDir, "launch.json");
+ private JsonObject createLaunch(File installDir, File projectDir,
+ Map classpathSourceMap, String externalModules) throws IOException {
// Get the path of Utility.jar so we can put it on the classpath
String utilityJarPath = classpathSourceMap.keySet()
@@ -201,7 +358,9 @@ public class VSCodeProjectScript extends GhidraScript {
classPathsArray.add(utilityJarPath);
JsonArray vmArgsArray = new JsonArray();
ghidraConfigObject.add("vmArgs", vmArgsArray);
- vmArgsArray.add("-Dghidra.external.modules=${workspaceFolder}");
+ if (externalModules != null) {
+ vmArgsArray.add("-Dghidra.external.modules=" + externalModules);
+ }
vmArgs.forEach(vmArgsArray::add);
// PyGhidra launcher
@@ -222,20 +381,63 @@ public class VSCodeProjectScript extends GhidraScript {
pyghidraConfigObject.add("env", envObject);
envObject.addProperty("PYGHIDRA_DEBUG", "1");
- // PyGhidra Java Attach
- JsonObject pyghidraAttachObject = new JsonObject();
- configurationsArray.add(pyghidraAttachObject);
- pyghidraAttachObject.addProperty("type", "java");
- pyghidraAttachObject.addProperty("name", "PyGhidra Java Attach");
- pyghidraAttachObject.addProperty("request", "attach");
- pyghidraAttachObject.addProperty("hostName", "localhost");
- pyghidraAttachObject.addProperty("port", 18001);
+ // Ghidra Attach
+ JsonObject ghidraAttachObject = new JsonObject();
+ configurationsArray.add(ghidraAttachObject);
+ ghidraAttachObject.addProperty("type", "java");
+ ghidraAttachObject.addProperty("name", "Ghidra Attach");
+ ghidraAttachObject.addProperty("request", "attach");
+ ghidraAttachObject.addProperty("hostName", "localhost");
+ ghidraAttachObject.addProperty("port", 18001);
+
+ return json;
+ }
- // Write launch json object
- if (!FileUtilities.mkdirs(launchFile.getParentFile())) {
- throw new IOException("Failed to create: " + launchFile.getParentFile());
+ /**
+ * Creates a new VSCode workspace with no folders added
+ *
+ * @param settings The VSCode settings JSON
+ * @param launch The VSCode launch JSON
+ * @return The new workspace JSON
+ */
+ private JsonObject createNewWorkspace(JsonObject settings, JsonObject launch) {
+ JsonObject json = new JsonObject();
+ JsonArray foldersArray = new JsonArray();
+ json.add("folders", foldersArray);
+ JsonObject folderObject = new JsonObject();
+ foldersArray.add(folderObject);
+ json.add("settings", settings);
+ json.add("launch", launch);
+
+ return json;
+ }
+
+ /**
+ * Adds the given project directory to the given workspace
+ *
+ * @param projectDir The VSCode project directory to add
+ * @param workspace The VSCode workspace to add to
+ * @param settings The VSCode settings JSON
+ * @param launch The VSCode launch JSON
+ * @return The new workspace JSON with the project added
+ */
+ private JsonObject addToExistingWorkspace(File projectDir, JsonObject workspace, JsonObject settings,
+ JsonObject launch) {
+ File projectParentDir = projectDir.getParentFile();
+ String folderName = projectDir.getName();
+ if (projectParentDir != null) {
+ folderName = projectParentDir.getName() + "/" + folderName;
}
- FileUtils.writeStringToFile(launchFile, gson.toJson(json), StandardCharsets.UTF_8);
+
+ JsonArray foldersArray = workspace.getAsJsonArray("folders");
+ JsonObject folderObject = new JsonObject();
+ folderObject.addProperty("name",
+ projectDir.getParentFile().getName() + "/" + projectDir.getName());
+ folderObject.addProperty("path", projectDir.getAbsolutePath());
+ if (!foldersArray.contains(folderObject)) {
+ foldersArray.add(folderObject);
+ }
+ return workspace;
}
/**
@@ -265,7 +467,7 @@ public class VSCodeProjectScript extends GhidraScript {
}
FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8);
}
-
+
private void writeSampleScriptPyGhidra(File projectDir) throws IOException {
File scriptsDir = new File(projectDir, "ghidra_scripts");
File scriptFile = new File(scriptsDir, "sample_script.py");
@@ -309,11 +511,11 @@ public class VSCodeProjectScript extends GhidraScript {
// Rename java files and text replace their contents
for (File f : newPackageDir.listFiles()) {
- String name = f.getName();
- if (!name.startsWith(skeleton)) {
+ String oldName = f.getName();
+ if (!oldName.startsWith(skeleton)) {
continue;
}
- String newName = projectName + name.substring(skeleton.length(), name.length());
+ String newName = projectName + oldName.substring(skeleton.length(), oldName.length());
File newFile = new File(f.getParentFile(), newName);
if (!f.renameTo(newFile)) {
throw new IOException("Failed to rename: " + f);
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeLauncherTask.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeLauncherTask.java
new file mode 100644
index 0000000000..9f75f89b72
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/vscode/VSCodeLauncherTask.java
@@ -0,0 +1,107 @@
+/* ###
+ * 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.vscode;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import ghidra.app.services.VSCodeIntegrationService;
+import ghidra.framework.Application;
+import ghidra.util.SystemUtilities;
+import ghidra.util.task.Task;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * A {@link Task} to launch Visual Studio Code
+ */
+class VSCodeLauncherTask extends Task {
+
+ private VSCodeIntegrationService vscodeService;
+ private File file;
+
+ /**
+ * Constructs a new Visual Studio Code launcher task
+ *
+ * @param vscodeService The Visual Studio Code integration service
+ * @param file The file to open in Visual Studio Code
+ */
+ public VSCodeLauncherTask(VSCodeIntegrationService vscodeService, File file) {
+ super("Visual Studio Code Launcher Task", true, true, true);
+ this.vscodeService = vscodeService;
+ this.file = file;
+ }
+
+ @Override
+ public void run(TaskMonitor monitor) {
+
+ if (SystemUtilities.isInDevelopmentMode()) {
+ vscodeService.handleVSCodeError(
+ "Launching Visual Studio Code is not supported in development mode.", false, null);
+ return;
+ }
+
+ // Get required Visual Studio Code components. If VSCode isn't found at the default
+ // location present the user with the options window, and when they close that window, try
+ // again.
+ File vscodeExecutableFile;
+ try {
+ vscodeExecutableFile = vscodeService.getVSCodeExecutableFile();
+ }
+ catch (IOException e1) {
+ vscodeService.handleVSCodeError(e1.getMessage(), true, null);
+ try {
+ vscodeExecutableFile = vscodeService.getVSCodeExecutableFile();
+ }
+ catch (IOException e2) {
+ vscodeService.handleVSCodeError(
+ "Failed to launch Visual Studio Code. The required Visual Studio Code components have not been configured.",
+ false, null);
+ return;
+ }
+ }
+
+ // Setup the workspace
+ File vscodeSettingsDir = new File(Application.getUserSettingsDirectory(), "vscode");
+ File workspaceFile = new File(vscodeSettingsDir, "ghidra_scripts.code-workspace");
+ try {
+ vscodeService.addToVSCodeWorkspace(workspaceFile, file.getParentFile());
+ }
+ catch (IOException e) {
+ vscodeService.handleVSCodeError("Failed to create Visual Studio Code workspace.", false,
+ e);
+ return;
+ }
+
+ // Launch Visual Studio Code
+ monitor.setIndeterminate(true);
+ monitor.setMessage("Launching Visual Studio Code...");
+ try {
+ List args = new ArrayList<>();
+ args.add(vscodeExecutableFile.getAbsolutePath());
+ args.add("-a");
+ args.add(workspaceFile.getAbsolutePath());
+ args.add(file.getAbsolutePath());
+ new ProcessBuilder(args).redirectErrorStream(true).start();
+ }
+ catch (Exception e) {
+ vscodeService.handleVSCodeError(
+ "Unexpected exception occurred while launching Visual Studio Code.", false, null);
+ return;
+ }
+ }
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/GhidraScriptService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/GhidraScriptService.java
index a84356d8f3..befb90e337 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/GhidraScriptService.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/GhidraScriptService.java
@@ -4,9 +4,9 @@
* 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.
@@ -30,4 +30,12 @@ public interface GhidraScriptService {
* @return True if the file opened in Eclipse; otherwise, false.
*/
public boolean tryToEditFileInEclipse(ResourceFile file);
+
+ /**
+ * Attempts to edit the provided file in Visual Studio Code.
+ *
+ * @param file The file to edit in Visual Studio Code.
+ * @return True if the file opened in Visual Studio Code; otherwise, false.
+ */
+ public boolean tryToEditFileInVSCode(ResourceFile file);
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/VSCodeIntegrationService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/VSCodeIntegrationService.java
new file mode 100644
index 0000000000..4fb1db310d
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/VSCodeIntegrationService.java
@@ -0,0 +1,73 @@
+/* ###
+ * 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.services;
+
+import java.io.*;
+
+import ghidra.framework.options.ToolOptions;
+
+/**
+ * Service that provides Visual Studio Code-related functionality
+ */
+public interface VSCodeIntegrationService {
+
+ /**
+ * {@return the Visual Studio Code Integration options}
+ */
+ public ToolOptions getVSCodeIntegrationOptions();
+
+ /**
+ * {@return the Visual Studio Code executable file}
+ *
+ * @throws FileNotFoundException if the executable file does not exist
+ */
+ public File getVSCodeExecutableFile() throws FileNotFoundException;
+
+ /**
+ * Launches Visual Studio Code
+ *
+ * @param file The initial file to open in Visual Studio Code
+ */
+ public void launchVSCode(File file);
+
+ /**
+ * Displays the given Visual Studio Code related error message in an error dialog
+ *
+ * @param error The error message to display in a dialog
+ * @param askAboutOptions True if we should ask the user if they want to be taken to the Visual
+ * Studio Code options; otherwise, false
+ * @param t An optional throwable to tie to the message
+ */
+ public void handleVSCodeError(String error, boolean askAboutOptions, Throwable t);
+
+ /**
+ * Creates a new Visual Studio Code module project at the given directory
+ *
+ * @param projectDir The new directory to create
+ * @throws IOException if the directory failed to be created
+ */
+ public void createVSCodeModuleProject(File projectDir) throws IOException;
+
+ /**
+ * Adds the given project directory to the the given Visual Studio Code workspace file
+ * A new workspace will be created if it doesn't already exist
+ *
+ * @param workspaceFile The location of the workspace file
+ * @param projectDir An existing project directory to add to the workspace
+ * @throws IOException if the directory failed to be created
+ */
+ public void addToVSCodeWorkspace(File workspaceFile, File projectDir) throws IOException;
+}
diff --git a/Ghidra/Features/Base/src/main/resources/images/vscode.png b/Ghidra/Features/Base/src/main/resources/images/vscode.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3e8c09e2929d7b130e7dc8cccf5ef36987ad8df
GIT binary patch
literal 1367
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6-E$sR$z
z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n
z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVkr05M1pgl1mAh%j*h
z6I`{x0%imor0saV%ts)_S>O>_%)r1c48n{Iv*t(uO^eJ7i71Ki^|4CM&(%vz$xlkv
ztH>>0+>{umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuG
zfu4bq9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)Btk
zRH0j3nOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXGvxn!lt}p
zsJDO~)CbAv8|oS8!_5Y2wE>A*`4?rT0&NDFZ)a!&R*518wZ}#uWI2*!AU*|)0=;U-
zWup%dHajlKxQFZv3`{pXT^vI!1Y3iBJwgIS{=NGxegBMwY4XC(t|mT5*Tl%ROMG2f
z6S+IDwdT!nWlJ?ZsnEh2IWf;Ir$4#8qKav+?f|sBxabN`@d)Y?Vb5^
z#J0USF!%Ghr!~)OzMm;?_#askR~od==KUAG{l43d25o#0@~(hY%~F1%$Ib9O?S1O2
z7JZ-4_~EqPi(fu=`KAWbpX(&Q`n~OBiH*g~w-YY>^>I^4Qa+?waPCyZr-|WQ$Etbn
z%-F|r)RgfylQFld^=z}ZNf#&Wy(fHUTl%Sni!QcKZ|}<%@CWwEnH-cj{`Ch#%)LaB
zO)?vJUB70W{r$ahC0F}d-BaNzHW54ruZ4doD7>O+;5UW!$EzzGF;@?tNasmPbN-xl
zM(eDR+WO|xJTgXig|7ade|^F4EC)f=^NQ1L`?p@5&2c5p}lmy3N6Y%NX8hY+7j^@HOg06#J?l;uU`moDXHbS|@wdM{2_^!y>hPH7cS7
zfvy6RE-yae==6VH>WO=IHAKY{1i3HVW-?{_Q>qp8T7SdD+#`2`3qRVNT%gk4Y!!
zr2zA${`HLc3@fr(V)PD}H0FxzwhcM|c1cKG4wLb6{<{`DddH(S?oI1#VE>TJ>XCmU
zU{#1n5u3^R!W%PH^7QA3u;xrPoVCxwWYedK37^UXUi|gRI63F=^`&AWT>T5#L!Ou&
z5m>$Qf=HXt!)tT*$&_xD*_pE`;l*r@i}$~EEHi4`Sgd&Q?ZIPUzbdr~hg;=SV2
zNiRL)+cz%l5#J=#klW?CKxfImk7)&lX89LYOR6M=w+Jz0=
zlXN;~v>fs9a5}}LvqYyv@rkyNN9BgUd|fZSC&a4AEL%83zpy)M@`f3=mrpo7U9uxp
z==X-JEo)kh4u6ilQs(qXYgUuvB3~gB?#n_E71~ek@7Q$ix?Rbu=J}I+AHUu>O?~D5
a*MHa*6m6wGO&7NY6&IeaelF{r5}E*&N%e*R
literal 0
HcmV?d00001