diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 197fae7184..b197639840 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -607,6 +607,7 @@ src/main/help/help/topics/Trees/images/FilterOptions.png||GHIDRA||||END| src/main/help/help/topics/Trees/images/TableColumnFilter.png||GHIDRA||||END| src/main/help/help/topics/Trees/images/TableColumnFilterAfterFilterApplied.png||GHIDRA||||END| src/main/help/help/topics/Trees/images/TableColumnFilterDialog.png||GHIDRA||||END| +src/main/help/help/topics/VSCodeIntegration/VSCodeIntegration.htm||GHIDRA||||END| src/main/help/help/topics/ValidateProgram/ValidateProgram.html||GHIDRA||||END| src/main/help/help/topics/ValidateProgram/images/ValidateProgram.png||GHIDRA||||END| src/main/help/help/topics/ValidateProgram/images/ValidateProgramDone.png||GHIDRA||||END| @@ -934,6 +935,7 @@ src/main/resources/images/view_left_right.png||Nuvola Icons - LGPL 2.1|||Nuvola src/main/resources/images/view_top_bottom.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| src/main/resources/images/viewmag+.png||Crystal Clear Icons - LGPL 2.1||||END| src/main/resources/images/viewmag.png||GHIDRA||||END| +src/main/resources/images/vscode.png||MIT||||END| src/main/resources/images/window.png||GHIDRA||||END| src/main/resources/images/wizard.png||Nuvola Icons - LGPL 2.1|||nuvola|END| src/main/resources/images/x-office-document-template.png||Tango Icons - Public Domain|||tango icon set|END| diff --git a/Ghidra/Features/Base/data/base.icons.theme.properties b/Ghidra/Features/Base/data/base.icons.theme.properties index 05628777bb..cad3f267d1 100644 --- a/Ghidra/Features/Base/data/base.icons.theme.properties +++ b/Ghidra/Features/Base/data/base.icons.theme.properties @@ -267,6 +267,7 @@ icon.plugin.scriptmanager.run = icon.run icon.plugin.scriptmanager.run.again = play_again.png icon.plugin.scriptmanager.edit = accessories-text-editor.png icon.plugin.scriptmanager.edit.eclipse = eclipse.png +icon.plugin.scriptmanager.edit.vscode = vscode.png icon.plugin.scriptmanager.keybinding = key.png icon.plugin.scriptmanager.delete = icon.delete icon.plugin.scriptmanager.rename = icon.rename diff --git a/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm b/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm index 884ee56e12..0a759195eb 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm @@ -210,6 +210,17 @@ Ghidra scripts in Eclipse, see Extensions/Eclipse/GhidraDev/GhidraDev_README.html.

+ +

Edit Script with Visual Studio Code

+
+

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.

+
+

Assign Key Binding

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/VSCodeIntegration/VSCodeIntegration.htm b/Ghidra/Features/Base/src/main/help/help/topics/VSCodeIntegration/VSCodeIntegration.htm new file mode 100644 index 0000000000..5e4b05751b --- /dev/null +++ b/Ghidra/Features/Base/src/main/help/help/topics/VSCodeIntegration/VSCodeIntegration.htm @@ -0,0 +1,57 @@ + + + + + Visual Studio Code Integration + + + + + +

Visual Studio Code Integration

+ +

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

+
OptionDescription
Visual Studio Code Executable PathPath 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 0000000000..e3e8c09e29 Binary files /dev/null and b/Ghidra/Features/Base/src/main/resources/images/vscode.png differ