getScriptPaths(PluginTool tool) {
+ return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool));
+ }
+
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java
new file mode 100644
index 0000000000..b883ae3d01
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java
@@ -0,0 +1,70 @@
+/* ###
+ * 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.debug.gui.tracermi.launcher;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.net.SocketAddress;
+import java.util.List;
+import java.util.Map;
+
+import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
+import ghidra.program.model.listing.Program;
+
+/**
+ * A launcher implemented by a simple DOS/Windows batch file.
+ *
+ *
+ * The script must start with an attributes header in a comment block.
+ */
+public class BatchScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer {
+ public static final String REM = "::";
+ public static final int REM_LEN = REM.length();
+
+ public static BatchScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
+ Program program, File script) throws FileNotFoundException {
+ ScriptAttributesParser parser = new ScriptAttributesParser() {
+ @Override
+ protected boolean ignoreLine(int lineNo, String line) {
+ return line.isBlank();
+ }
+
+ @Override
+ protected String removeDelimiter(String line) {
+ String stripped = line.stripLeading();
+ if (!stripped.startsWith(REM)) {
+ return null;
+ }
+ return stripped.substring(REM_LEN);
+ }
+ };
+ ScriptAttributes attrs = parser.parseFile(script);
+ return new BatchScriptTraceRmiLaunchOffer(plugin, program, script,
+ "BATCH_FILE:" + script.getName(), attrs);
+ }
+
+ private BatchScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program,
+ File script, String configName, ScriptAttributes attrs) {
+ super(plugin, program, script, configName, attrs);
+ }
+
+ @Override
+ protected void prepareSubprocess(List commandLine, Map env,
+ Map args, SocketAddress address) {
+ ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args,
+ address);
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOpinion.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOpinion.java
new file mode 100644
index 0000000000..c3bc5da027
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOpinion.java
@@ -0,0 +1,49 @@
+/* ###
+ * 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.debug.gui.tracermi.launcher;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import generic.jar.ResourceFile;
+import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
+import ghidra.program.model.listing.Program;
+import ghidra.util.Msg;
+
+public class BatchScriptTraceRmiLaunchOpinion extends AbstractTraceRmiLaunchOpinion {
+
+ @Override
+ public Collection getOffers(TraceRmiLauncherServicePlugin plugin,
+ Program program) {
+ return getScriptPaths(plugin.getTool())
+ .flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".bat"))))
+ .flatMap(sf -> createOffer(plugin, program, sf))
+ .collect(Collectors.toList());
+ }
+
+ protected Stream createOffer(TraceRmiLauncherServicePlugin plugin,
+ Program program, ResourceFile scriptFile) {
+ try {
+ return Stream.of(
+ BatchScriptTraceRmiLaunchOffer.create(plugin, program, scriptFile.getFile(false)));
+ }
+ catch (Exception e) {
+ Msg.error(this, "Could not offer " + scriptFile + ": " + e.getMessage(), e);
+ return Stream.of();
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java
index 09bbcb8957..c032773ce1 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java
@@ -88,6 +88,9 @@ public class LaunchAction extends MultiActionDockingAction {
ConfigLast findMostRecentConfig() {
Program program = plugin.currentProgram;
+ if (program == null) {
+ return null;
+ }
ConfigLast best = null;
ProgramUserData userData = program.getProgramUserData();
@@ -113,14 +116,16 @@ public class LaunchAction extends MultiActionDockingAction {
List actions = new ArrayList<>();
- ProgramUserData userData = program.getProgramUserData();
Map saved = new HashMap<>();
- for (String propName : userData.getStringPropertyNames()) {
- ConfigLast check = checkSavedConfig(userData, propName);
- if (check == null) {
- continue;
+ if (program != null) {
+ ProgramUserData userData = program.getProgramUserData();
+ for (String propName : userData.getStringPropertyNames()) {
+ ConfigLast check = checkSavedConfig(userData, propName);
+ if (check == null) {
+ continue;
+ }
+ saved.put(check.configName, check.last);
}
- saved.put(check.configName, check.last);
}
for (TraceRmiLaunchOffer offer : offers) {
@@ -134,6 +139,8 @@ public class LaunchAction extends MultiActionDockingAction {
.build());
Long last = saved.get(offer.getConfigName());
if (last == null) {
+ // NB. If program == null, this will always happen.
+ // Thus, no worries about program.getName() below.
continue;
}
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
@@ -172,6 +179,11 @@ public class LaunchAction extends MultiActionDockingAction {
// Make accessible to this file
return super.showPopup();
}
+
+ @Override
+ public String getToolTipText() {
+ return getDescription();
+ }
}
@Override
@@ -180,19 +192,45 @@ public class LaunchAction extends MultiActionDockingAction {
}
@Override
- public void actionPerformed(ActionContext context) {
- // See comment on super method about use of runLater
- ConfigLast last = findMostRecentConfig();
+ public boolean isEnabledForContext(ActionContext context) {
+ return plugin.currentProgram != null;
+ }
+
+ protected TraceRmiLaunchOffer findOffer(ConfigLast last) {
if (last == null) {
- Swing.runLater(() -> button.showPopup());
- return;
+ return null;
}
for (TraceRmiLaunchOffer offer : plugin.getOffers(plugin.currentProgram)) {
if (offer.getConfigName().equals(last.configName)) {
- plugin.relaunch(offer);
- return;
+ return offer;
}
}
- Swing.runLater(() -> button.showPopup());
+ return null;
+ }
+
+ @Override
+ public void actionPerformed(ActionContext context) {
+ // See comment on super method about use of runLater
+ ConfigLast last = findMostRecentConfig();
+ TraceRmiLaunchOffer offer = findOffer(last);
+ if (offer == null) {
+ Swing.runLater(() -> button.showPopup());
+ return;
+ }
+ plugin.relaunch(offer);
+ }
+
+ @Override
+ public String getDescription() {
+ Program program = plugin.currentProgram;
+ if (program == null) {
+ return "Launch (program required)";
+ }
+ ConfigLast last = findMostRecentConfig();
+ TraceRmiLaunchOffer offer = findOffer(last);
+ if (last == null) {
+ return "Configure and launch " + program.getName();
+ }
+ return "Re-launch " + program.getName() + " using " + offer.getTitle();
}
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java
new file mode 100644
index 0000000000..cf22772c62
--- /dev/null
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java
@@ -0,0 +1,569 @@
+/* ###
+ * 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.debug.gui.tracermi.launcher;
+
+import java.io.*;
+import java.math.BigInteger;
+import java.net.*;
+import java.util.*;
+import java.util.Map.Entry;
+
+import javax.swing.Icon;
+
+import generic.theme.GIcon;
+import generic.theme.Gui;
+import ghidra.dbg.target.TargetMethod.ParameterDescription;
+import ghidra.dbg.util.ShellUtils;
+import ghidra.framework.Application;
+import ghidra.util.HelpLocation;
+import ghidra.util.Msg;
+
+/**
+ * Some attributes are required. Others are optional:
+ *
+ * - {@code @menu-path}: (Required)
+ *
+ *
+ */
+public abstract class ScriptAttributesParser {
+ public static final String AT_TITLE = "@title";
+ public static final String AT_DESC = "@desc";
+ public static final String AT_MENU_PATH = "@menu-path";
+ public static final String AT_MENU_GROUP = "@menu-group";
+ public static final String AT_MENU_ORDER = "@menu-order";
+ public static final String AT_ICON = "@icon";
+ public static final String AT_HELP = "@help";
+ public static final String AT_ENUM = "@enum";
+ public static final String AT_ENV = "@env";
+ public static final String AT_ARG = "@arg";
+ public static final String AT_ARGS = "@args";
+ public static final String AT_TTY = "@tty";
+
+ public static final String PREFIX_ENV = "env:";
+ public static final String PREFIX_ARG = "arg:";
+ public static final String KEY_ARGS = "args";
+
+ public static final String MSGPAT_INVALID_HELP_SYNTAX =
+ "%s: Invalid %s syntax. Use Topic#anchor";
+ public static final String MSGPAT_INVALID_ENUM_SYNTAX =
+ "%s: Invalid %s syntax. Use NAME:type Choice1 [ChoiceN...]";
+ public static final String MSGPAT_INVALID_ENV_SYNTAX =
+ "%s: Invalid %s syntax. Use NAME:type=default \"Display\" \"Tool Tip\"";
+ public static final String MSGPAT_INVALID_ARG_SYNTAX =
+ "%s: Invalid %s syntax. Use :type \"Display\" \"Tool Tip\"";
+ public static final String MSGPAT_INVALID_ARGS_SYNTAX =
+ "%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\"";
+
+ protected record Location(String fileName, int lineNo) {
+ @Override
+ public String toString() {
+ return "%s:%d".formatted(fileName, lineNo);
+ }
+ }
+
+ protected interface OptType {
+ static OptType> parse(Location loc, String typeName,
+ Map> userEnums) {
+ OptType> type = switch (typeName) {
+ case "str" -> BaseType.STRING;
+ case "int" -> BaseType.INT;
+ case "bool" -> BaseType.BOOL;
+ default -> userEnums.get(typeName);
+ };
+ if (type == null) {
+ Msg.error(ScriptAttributesParser.class,
+ "%s: Invalid type %s".formatted(loc, typeName));
+ return null;
+ }
+ return type;
+ }
+
+ default TypeAndDefault withCastDefault(Object defaultValue) {
+ return new TypeAndDefault<>(this, cls().cast(defaultValue));
+ }
+
+ Class cls();
+
+ T decode(Location loc, String str);
+
+ ParameterDescription createParameter(String name, T defaultValue, String display,
+ String description);
+ }
+
+ protected interface BaseType extends OptType {
+ public static BaseType> parse(Location loc, String typeName) {
+ BaseType> type = switch (typeName) {
+ case "str" -> BaseType.STRING;
+ case "int" -> BaseType.INT;
+ case "bool" -> BaseType.BOOL;
+ default -> null;
+ };
+ if (type == null) {
+ Msg.error(ScriptAttributesParser.class,
+ "%s: Invalid base type %s".formatted(loc, typeName));
+ return null;
+ }
+ return type;
+ }
+
+ public static final BaseType STRING = new BaseType<>() {
+ @Override
+ public Class cls() {
+ return String.class;
+ }
+
+ @Override
+ public String decode(Location loc, String str) {
+ return str;
+ }
+ };
+
+ public static final BaseType INT = new BaseType<>() {
+ @Override
+ public Class cls() {
+ return BigInteger.class;
+ }
+
+ @Override
+ public BigInteger decode(Location loc, String str) {
+ try {
+ if (str.startsWith("0x")) {
+ return new BigInteger(str.substring(2), 16);
+ }
+ return new BigInteger(str);
+ }
+ catch (NumberFormatException e) {
+ Msg.error(ScriptAttributesParser.class,
+ ("%s: Invalid int for %s: %s. You may prefix with 0x for hexadecimal. " +
+ "Otherwise, decimal is used.").formatted(loc, AT_ENV, str));
+ return null;
+ }
+ }
+ };
+
+ public static final BaseType BOOL = new BaseType<>() {
+ @Override
+ public Class cls() {
+ return Boolean.class;
+ }
+
+ @Override
+ public Boolean decode(Location loc, String str) {
+ Boolean result = switch (str) {
+ case "true" -> true;
+ case "false" -> false;
+ default -> null;
+ };
+ if (result == null) {
+ Msg.error(ScriptAttributesParser.class,
+ "%s: Invalid bool for %s: %s. Only true or false (in lower case) is allowed."
+ .formatted(loc, AT_ENV, str));
+ return null;
+ }
+ return result;
+ }
+ };
+
+ default UserType withCastChoices(List> choices) {
+ return new UserType<>(this, choices.stream().map(cls()::cast).toList());
+ }
+
+ @Override
+ default ParameterDescription createParameter(String name, T defaultValue, String display,
+ String description) {
+ return ParameterDescription.create(cls(), name, false, defaultValue, display,
+ description);
+ }
+ }
+
+ protected record UserType(BaseType base, List choices) implements OptType {
+ @Override
+ public Class cls() {
+ return base.cls();
+ }
+
+ @Override
+ public T decode(Location loc, String str) {
+ return base.decode(loc, str);
+ }
+
+ @Override
+ public ParameterDescription createParameter(String name, T defaultValue, String display,
+ String description) {
+ return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
+ description);
+ }
+ }
+
+ protected record TypeAndDefault(OptType type, T defaultValue) {
+ public static TypeAndDefault> parse(Location loc, String typeName, String defaultString,
+ Map> userEnums) {
+ OptType> tac = OptType.parse(loc, typeName, userEnums);
+ if (tac == null) {
+ return null;
+ }
+ Object value = tac.decode(loc, defaultString);
+ if (value == null) {
+ return null;
+ }
+ return tac.withCastDefault(value);
+ }
+
+ public ParameterDescription createParameter(String name, String display,
+ String description) {
+ return type.createParameter(name, defaultValue, display, description);
+ }
+ }
+
+ protected static String addrToString(InetAddress address) {
+ if (address.isAnyLocalAddress()) {
+ return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost.
+ }
+ return address.getHostAddress();
+ }
+
+ protected static String sockToString(SocketAddress address) {
+ if (address instanceof InetSocketAddress tcp) {
+ return addrToString(tcp.getAddress()) + ":" + tcp.getPort();
+ }
+ throw new AssertionError("Unhandled address type " + address);
+ }
+
+ public record ScriptAttributes(String title, String description, List menuPath,
+ String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
+ Map> parameters, Collection extraTtys) {
+ }
+
+ /**
+ * Convert an arguments map into a command line and environment variables
+ *
+ * @param commandLine a mutable list to add command line parameters into
+ * @param env a mutable map to place environment variables into. This should likely be
+ * initialized to {@link System#getenv()} so that Ghidra's environment is inherited
+ * by the script's process.
+ * @param script the script file
+ * @param parameters the descriptions of the parameters
+ * @param args the arguments to process
+ * @param address the address of the listening TraceRmi socket
+ */
+ public static void processArguments(List commandLine, Map env,
+ File script, Map> parameters, Map args,
+ SocketAddress address) {
+
+ commandLine.add(script.getAbsolutePath());
+ env.put("GHIDRA_HOME", Application.getInstallationDirectory().getAbsolutePath());
+ if (address != null) {
+ env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address));
+ if (address instanceof InetSocketAddress tcp) {
+ env.put("GHIDRA_TRACE_RMI_HOST", tcp.getAddress().toString());
+ env.put("GHIDRA_TRACE_RMI_PORT", Integer.toString(tcp.getPort()));
+ }
+ }
+
+ ParameterDescription> paramDesc;
+ for (int i = 1; (paramDesc = parameters.get("arg:" + i)) != null; i++) {
+ commandLine.add(Objects.toString(paramDesc.get(args)));
+ }
+
+ paramDesc = parameters.get("args");
+ if (paramDesc != null) {
+ commandLine.addAll(ShellUtils.parseArgs((String) paramDesc.get(args)));
+ }
+
+ for (Entry> ent : parameters.entrySet()) {
+ String key = ent.getKey();
+ if (key.startsWith(PREFIX_ENV)) {
+ String varName = key.substring(PREFIX_ENV.length());
+ env.put(varName, Objects.toString(ent.getValue().get(args)));
+ }
+ }
+ }
+
+ private int argc = 0;
+ private String title;
+ private StringBuilder description;
+ private List menuPath;
+ private String menuGroup;
+ private String menuOrder;
+ private String iconId;
+ private HelpLocation helpLocation;
+ private final Map> userTypes = new HashMap<>();
+ private final Map> parameters = new LinkedHashMap<>();
+ private final Set extraTtys = new LinkedHashSet<>();
+
+ /**
+ * Check if a line should just be ignored, e.g., blank lines, or the "shebang" line on UNIX.
+ *
+ * @param lineNo the line number, counting 1 up
+ * @param line the full line, excluding the new-line characters
+ * @return true to ignore, false to parse
+ */
+ protected abstract boolean ignoreLine(int lineNo, String line);
+
+ /**
+ * Check if a line is a comment and extract just the comment
+ *
+ *
+ * If null is returned, the parser assumes the attributes header is ended
+ *
+ * @param line the full line, excluding the new-line characters
+ * @return the comment, or null if the line is not a comment
+ */
+ protected abstract String removeDelimiter(String line);
+
+ public ScriptAttributes parseFile(File script) throws FileNotFoundException {
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
+ String line;
+ for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) {
+ if (ignoreLine(lineNo, line)) {
+ continue;
+ }
+ String comment = removeDelimiter(line);
+ if (comment == null) {
+ break;
+ }
+ parseComment(new Location(script.getName(), lineNo), comment);
+ }
+ return validate(script.getName());
+ }
+ catch (FileNotFoundException e) {
+ // Avoid capture by IOException
+ throw e;
+ }
+ catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Process a line in the metadata comment block
+ *
+ * @param line the line, excluding any comment delimiters
+ */
+ public void parseComment(Location loc, String comment) {
+ if (comment.isBlank()) {
+ return;
+ }
+ String[] parts = comment.split("\\s+", 2);
+ if (!parts[0].startsWith("@")) {
+ return;
+ }
+ if (parts.length < 2) {
+ Msg.error(this, "%s: Too few tokens: %s".formatted(loc, comment));
+ return;
+ }
+ switch (parts[0].trim()) {
+ case AT_TITLE -> parseTitle(loc, parts[1]);
+ case AT_DESC -> parseDesc(loc, parts[1]);
+ case AT_MENU_PATH -> parseMenuPath(loc, parts[1]);
+ case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]);
+ case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]);
+ case AT_ICON -> parseIcon(loc, parts[1]);
+ case AT_HELP -> parseHelp(loc, parts[1]);
+ case AT_ENUM -> parseEnum(loc, parts[1]);
+ case AT_ENV -> parseEnv(loc, parts[1]);
+ case AT_ARG -> parseArg(loc, parts[1], ++argc);
+ case AT_ARGS -> parseArgs(loc, parts[1]);
+ case AT_TTY -> parseTty(loc, parts[1]);
+ default -> parseUnrecognized(loc, comment);
+ }
+ }
+
+ protected void parseTitle(Location loc, String str) {
+ if (title != null) {
+ Msg.warn(this, "%s: Duplicate @title".formatted(loc));
+ }
+ title = str;
+ }
+
+ protected void parseDesc(Location loc, String str) {
+ if (description == null) {
+ description = new StringBuilder();
+ }
+ description.append(str);
+ description.append("\n");
+ }
+
+ protected void parseMenuPath(Location loc, String str) {
+ if (menuPath != null) {
+ Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_PATH));
+ }
+ menuPath = List.of(str.trim().split("\\."));
+ if (menuPath.isEmpty()) {
+ Msg.error(this,
+ "%s: Empty %s. Ignoring.".formatted(loc, AT_MENU_PATH));
+ }
+ }
+
+ protected void parseMenuGroup(Location loc, String str) {
+ if (menuGroup != null) {
+ Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_GROUP));
+ }
+ menuGroup = str;
+ }
+
+ protected void parseMenuOrder(Location loc, String str) {
+ if (menuOrder != null) {
+ Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_ORDER));
+ }
+ menuOrder = str;
+ }
+
+ protected void parseIcon(Location loc, String str) {
+ if (iconId != null) {
+ Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_ICON));
+ }
+ iconId = str.trim();
+ if (!Gui.hasIcon(iconId)) {
+ Msg.error(this,
+ "%s: Icon id %s not registered in the theme".formatted(loc, iconId));
+ }
+ }
+
+ protected void parseHelp(Location loc, String str) {
+ if (helpLocation != null) {
+ Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_HELP));
+ }
+ String[] parts = str.trim().split("#", 2);
+ if (parts.length != 2) {
+ Msg.error(this, MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP));
+ return;
+ }
+ helpLocation = new HelpLocation(parts[0].trim(), parts[1].trim());
+ }
+
+ protected void parseEnum(Location loc, String str) {
+ List parts = ShellUtils.parseArgs(str);
+ if (parts.size() < 2) {
+ Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
+ return;
+ }
+ String[] nameParts = parts.get(0).split(":", 2);
+ if (nameParts.length != 2) {
+ Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
+ return;
+ }
+ String name = nameParts[0].trim();
+ BaseType> baseType = BaseType.parse(loc, nameParts[1]);
+ if (baseType == null) {
+ return;
+ }
+ List> choices = parts.stream().skip(1).map(s -> baseType.decode(loc, s)).toList();
+ if (choices.contains(null)) {
+ return;
+ }
+ UserType> userType = baseType.withCastChoices(choices);
+ if (userTypes.put(name, userType) != null) {
+ Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name));
+ }
+ }
+
+ protected void parseEnv(Location loc, String str) {
+ List parts = ShellUtils.parseArgs(str);
+ if (parts.size() != 3) {
+ Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
+ return;
+ }
+ String[] nameParts = parts.get(0).split(":", 2);
+ if (nameParts.length != 2) {
+ Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
+ return;
+ }
+ String trimmed = nameParts[0].trim();
+ String name = PREFIX_ENV + trimmed;
+ String[] tadParts = nameParts[1].split("=", 2);
+ if (tadParts.length != 2) {
+ Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
+ return;
+ }
+ TypeAndDefault> tad =
+ TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes);
+ ParameterDescription> param = tad.createParameter(name, parts.get(1), parts.get(2));
+ if (parameters.put(name, param) != null) {
+ Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed));
+ }
+ }
+
+ protected void parseArg(Location loc, String str, int argNum) {
+ List parts = ShellUtils.parseArgs(str);
+ if (parts.size() != 3) {
+ Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
+ return;
+ }
+ String colonType = parts.get(0).trim();
+ if (!colonType.startsWith(":")) {
+ Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
+ return;
+ }
+ OptType> type = OptType.parse(loc, colonType.substring(1), userTypes);
+ if (type == null) {
+ return;
+ }
+ String name = PREFIX_ARG + argNum;
+ parameters.put(name, ParameterDescription.create(type.cls(), name, true, null,
+ parts.get(1), parts.get(2)));
+ }
+
+ protected void parseArgs(Location loc, String str) {
+ List parts = ShellUtils.parseArgs(str);
+ if (parts.size() != 2) {
+ Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
+ return;
+ }
+ ParameterDescription parameter = ParameterDescription.create(String.class,
+ "args", false, "", parts.get(0), parts.get(1));
+ if (parameters.put(KEY_ARGS, parameter) != null) {
+ Msg.warn(this, "%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS));
+ }
+ }
+
+ protected void parseTty(Location loc, String str) {
+ if (!extraTtys.add(str)) {
+ Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY));
+ }
+ }
+
+ protected void parseUnrecognized(Location loc, String line) {
+ Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line));
+ }
+
+ protected ScriptAttributes validate(String fileName) {
+ if (title == null) {
+ Msg.error(this, "%s is required. Using script file name.".formatted(AT_TITLE));
+ title = fileName;
+ }
+ if (menuPath == null) {
+ menuPath = List.of(title);
+ }
+ if (menuGroup == null) {
+ menuGroup = "";
+ }
+ if (menuOrder == null) {
+ menuOrder = "";
+ }
+ if (iconId == null) {
+ iconId = "icon.debugger";
+ }
+ return new ScriptAttributes(title, getDescription(), List.copyOf(menuPath), menuGroup,
+ menuOrder, new GIcon(iconId), helpLocation,
+ Collections.unmodifiableMap(new LinkedHashMap<>(parameters)), List.copyOf(extraTtys));
+ }
+
+ private String getDescription() {
+ return description == null ? null : description.toString();
+ }
+}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java
index 787eb7a54a..bb9a379cd0 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java
@@ -15,454 +15,25 @@
*/
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
-import java.io.*;
-import java.math.BigInteger;
-import java.net.*;
-import java.util.*;
-import java.util.Map.Entry;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.net.SocketAddress;
+import java.util.List;
+import java.util.Map;
-import javax.swing.Icon;
-
-import generic.theme.GIcon;
-import generic.theme.Gui;
-import ghidra.dbg.target.TargetMethod.ParameterDescription;
-import ghidra.dbg.util.ShellUtils;
-import ghidra.debug.api.tracermi.TerminalSession;
-import ghidra.framework.Application;
+import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
import ghidra.program.model.listing.Program;
-import ghidra.util.HelpLocation;
-import ghidra.util.Msg;
-import ghidra.util.task.TaskMonitor;
/**
* A launcher implemented by a simple UNIX shell script.
*
*
- * The script must start with an attributes header in a comment block. Some attributes are required.
- * Others are optional:
- *
- * - {@code @menu-path}: (Required)
- *
+ * The script must start with an attributes header in a comment block. See
+ * {@link ScriptAttributesParser}.
*/
-public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer {
+public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer {
public static final String SHEBANG = "#!";
- public static final String AT_TITLE = "@title";
- public static final String AT_DESC = "@desc";
- public static final String AT_MENU_PATH = "@menu-path";
- public static final String AT_MENU_GROUP = "@menu-group";
- public static final String AT_MENU_ORDER = "@menu-order";
- public static final String AT_ICON = "@icon";
- public static final String AT_HELP = "@help";
- public static final String AT_ENUM = "@enum";
- public static final String AT_ENV = "@env";
- public static final String AT_ARG = "@arg";
- public static final String AT_ARGS = "@args";
- public static final String AT_TTY = "@tty";
-
- public static final String PREFIX_ENV = "env:";
- public static final String PREFIX_ARG = "arg:";
- public static final String KEY_ARGS = "args";
-
- public static final String MSGPAT_INVALID_HELP_SYNTAX =
- "%s: Invalid %s syntax. Use Topic#anchor";
- public static final String MSGPAT_INVALID_ENUM_SYNTAX =
- "%s: Invalid %s syntax. Use NAME:type Choice1 [ChoiceN...]";
- public static final String MSGPAT_INVALID_ENV_SYNTAX =
- "%s: Invalid %s syntax. Use NAME:type=default \"Display\" \"Tool Tip\"";
- public static final String MSGPAT_INVALID_ARG_SYNTAX =
- "%s: Invalid %s syntax. Use :type \"Display\" \"Tool Tip\"";
- public static final String MSGPAT_INVALID_ARGS_SYNTAX =
- "%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\"";
-
- protected record Location(String fileName, int lineNo) {
- @Override
- public String toString() {
- return "%s:%d".formatted(fileName, lineNo);
- }
- }
-
- protected interface OptType {
- static OptType> parse(Location loc, String typeName,
- Map> userEnums) {
- OptType> type = switch (typeName) {
- case "str" -> BaseType.STRING;
- case "int" -> BaseType.INT;
- case "bool" -> BaseType.BOOL;
- default -> userEnums.get(typeName);
- };
- if (type == null) {
- Msg.error(AttributesParser.class, "%s: Invalid type %s".formatted(loc, typeName));
- return null;
- }
- return type;
- }
-
- default TypeAndDefault withCastDefault(Object defaultValue) {
- return new TypeAndDefault<>(this, cls().cast(defaultValue));
- }
-
- Class cls();
-
- T decode(Location loc, String str);
-
- ParameterDescription createParameter(String name, T defaultValue, String display,
- String description);
- }
-
- protected interface BaseType extends OptType {
- static BaseType> parse(Location loc, String typeName) {
- BaseType> type = switch (typeName) {
- case "str" -> BaseType.STRING;
- case "int" -> BaseType.INT;
- case "bool" -> BaseType.BOOL;
- default -> null;
- };
- if (type == null) {
- Msg.error(AttributesParser.class,
- "%s: Invalid base type %s".formatted(loc, typeName));
- return null;
- }
- return type;
- }
-
- public static final BaseType STRING = new BaseType<>() {
- @Override
- public Class cls() {
- return String.class;
- }
-
- @Override
- public String decode(Location loc, String str) {
- return str;
- }
- };
- public static final BaseType INT = new BaseType<>() {
- @Override
- public Class cls() {
- return BigInteger.class;
- }
-
- @Override
- public BigInteger decode(Location loc, String str) {
- try {
- if (str.startsWith("0x")) {
- return new BigInteger(str.substring(2), 16);
- }
- return new BigInteger(str);
- }
- catch (NumberFormatException e) {
- Msg.error(AttributesParser.class,
- ("%s: Invalid int for %s: %s. You may prefix with 0x for hexadecimal. " +
- "Otherwise, decimal is used.").formatted(loc, AT_ENV, str));
- return null;
- }
- }
- };
- public static final BaseType BOOL = new BaseType<>() {
- @Override
- public Class cls() {
- return Boolean.class;
- }
-
- @Override
- public Boolean decode(Location loc, String str) {
- Boolean result = switch (str) {
- case "true" -> true;
- case "false" -> false;
- default -> null;
- };
- if (result == null) {
- Msg.error(AttributesParser.class,
- "%s: Invalid bool for %s: %s. Only true or false (in lower case) is allowed."
- .formatted(loc, AT_ENV, str));
- return null;
- }
- return result;
- }
- };
-
- default UserType withCastChoices(List> choices) {
- return new UserType<>(this, choices.stream().map(cls()::cast).toList());
- }
-
- @Override
- default ParameterDescription createParameter(String name, T defaultValue, String display,
- String description) {
- return ParameterDescription.create(cls(), name, false, defaultValue, display,
- description);
- }
- }
-
- protected record UserType (BaseType base, List choices) implements OptType {
- @Override
- public Class cls() {
- return base.cls();
- }
-
- @Override
- public T decode(Location loc, String str) {
- return base.decode(loc, str);
- }
-
- @Override
- public ParameterDescription createParameter(String name, T defaultValue, String display,
- String description) {
- return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
- description);
- }
- }
-
- protected record TypeAndDefault (OptType type, T defaultValue) {
- public static TypeAndDefault> parse(Location loc, String typeName, String defaultString,
- Map> userEnums) {
- OptType> tac = OptType.parse(loc, typeName, userEnums);
- if (tac == null) {
- return null;
- }
- Object value = tac.decode(loc, defaultString);
- if (value == null) {
- return null;
- }
- return tac.withCastDefault(value);
- }
-
- public ParameterDescription createParameter(String name, String display,
- String description) {
- return type.createParameter(name, defaultValue, display, description);
- }
- }
-
- protected static class AttributesParser {
- protected int argc = 0;
- protected String title;
- protected StringBuilder description;
- protected List menuPath;
- protected String menuGroup;
- protected String menuOrder;
- protected String iconId;
- protected HelpLocation helpLocation;
- protected final Map> userTypes = new HashMap<>();
- protected final Map> parameters = new LinkedHashMap<>();
- protected final Set extraTtys = new LinkedHashSet<>();
-
- /**
- * Process a line in the metadata comment block
- *
- * @param line the line, excluding any comment delimiters
- */
- public void parseLine(Location loc, String line) {
- String afterHash = line.stripLeading().substring(1);
- if (afterHash.isBlank()) {
- return;
- }
- String[] parts = afterHash.split("\\s+", 2);
- if (!parts[0].startsWith("@")) {
- return;
- }
- if (parts.length < 2) {
- Msg.error(this, "%s: Too few tokens: %s".formatted(loc, line));
- return;
- }
- switch (parts[0].trim()) {
- case AT_TITLE -> parseTitle(loc, parts[1]);
- case AT_DESC -> parseDesc(loc, parts[1]);
- case AT_MENU_PATH -> parseMenuPath(loc, parts[1]);
- case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]);
- case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]);
- case AT_ICON -> parseIcon(loc, parts[1]);
- case AT_HELP -> parseHelp(loc, parts[1]);
- case AT_ENUM -> parseEnum(loc, parts[1]);
- case AT_ENV -> parseEnv(loc, parts[1]);
- case AT_ARG -> parseArg(loc, parts[1], ++argc);
- case AT_ARGS -> parseArgs(loc, parts[1]);
- case AT_TTY -> parseTty(loc, parts[1]);
- default -> parseUnrecognized(loc, line);
- }
- }
-
- protected void parseTitle(Location loc, String str) {
- if (title != null) {
- Msg.warn(this, "%s: Duplicate @title".formatted(loc));
- }
- title = str;
- }
-
- protected void parseDesc(Location loc, String str) {
- if (description == null) {
- description = new StringBuilder();
- }
- description.append(str);
- description.append("\n");
- }
-
- protected void parseMenuPath(Location loc, String str) {
- if (menuPath != null) {
- Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_PATH));
- }
- menuPath = List.of(str.trim().split("\\."));
- if (menuPath.isEmpty()) {
- Msg.error(this,
- "%s: Empty %s. Ignoring.".formatted(loc, AT_MENU_PATH));
- }
- }
-
- protected void parseMenuGroup(Location loc, String str) {
- if (menuGroup != null) {
- Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_GROUP));
- }
- menuGroup = str;
- }
-
- protected void parseMenuOrder(Location loc, String str) {
- if (menuOrder != null) {
- Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_ORDER));
- }
- menuOrder = str;
- }
-
- protected void parseIcon(Location loc, String str) {
- if (iconId != null) {
- Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_ICON));
- }
- iconId = str.trim();
- if (!Gui.hasIcon(iconId)) {
- Msg.error(this,
- "%s: Icon id %s not registered in the theme".formatted(loc, iconId));
- }
- }
-
- protected void parseHelp(Location loc, String str) {
- if (helpLocation != null) {
- Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_HELP));
- }
- String[] parts = str.trim().split("#", 2);
- if (parts.length != 2) {
- Msg.error(this, MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP));
- return;
- }
- helpLocation = new HelpLocation(parts[0].trim(), parts[1].trim());
- }
-
- protected void parseEnum(Location loc, String str) {
- List parts = ShellUtils.parseArgs(str);
- if (parts.size() < 2) {
- Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
- return;
- }
- String[] nameParts = parts.get(0).split(":", 2);
- if (nameParts.length != 2) {
- Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
- return;
- }
- String name = nameParts[0].trim();
- BaseType> baseType = BaseType.parse(loc, nameParts[1]);
- if (baseType == null) {
- return;
- }
- List> choices = parts.stream().skip(1).map(s -> baseType.decode(loc, s)).toList();
- if (choices.contains(null)) {
- return;
- }
- UserType> userType = baseType.withCastChoices(choices);
- if (userTypes.put(name, userType) != null) {
- Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name));
- }
- }
-
- protected void parseEnv(Location loc, String str) {
- List parts = ShellUtils.parseArgs(str);
- if (parts.size() != 3) {
- Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
- return;
- }
- String[] nameParts = parts.get(0).split(":", 2);
- if (nameParts.length != 2) {
- Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
- return;
- }
- String trimmed = nameParts[0].trim();
- String name = PREFIX_ENV + trimmed;
- String[] tadParts = nameParts[1].split("=", 2);
- if (tadParts.length != 2) {
- Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV));
- return;
- }
- TypeAndDefault> tad =
- TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes);
- ParameterDescription> param = tad.createParameter(name, parts.get(1), parts.get(2));
- if (parameters.put(name, param) != null) {
- Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed));
- }
- }
-
- protected void parseArg(Location loc, String str, int argNum) {
- List parts = ShellUtils.parseArgs(str);
- if (parts.size() != 3) {
- Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
- return;
- }
- String colonType = parts.get(0).trim();
- if (!colonType.startsWith(":")) {
- Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
- return;
- }
- OptType> type = OptType.parse(loc, colonType.substring(1), userTypes);
- if (type == null) {
- return;
- }
- String name = PREFIX_ARG + argNum;
- parameters.put(name, ParameterDescription.create(type.cls(), name, true, null,
- parts.get(1), parts.get(2)));
- }
-
- protected void parseArgs(Location loc, String str) {
- List parts = ShellUtils.parseArgs(str);
- if (parts.size() != 2) {
- Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
- return;
- }
- ParameterDescription parameter = ParameterDescription.create(String.class,
- "args", false, "", parts.get(0), parts.get(1));
- if (parameters.put(KEY_ARGS, parameter) != null) {
- Msg.warn(this, "%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS));
- }
- }
-
- protected void parseTty(Location loc, String str) {
- if (!extraTtys.add(str)) {
- Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY));
- }
- }
-
- protected void parseUnrecognized(Location loc, String line) {
- Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line));
- }
-
- protected void validate(String fileName) {
- if (title == null) {
- Msg.error(this, "%s is required. Using script file name.".formatted(AT_TITLE));
- title = fileName;
- }
- if (menuPath == null) {
- menuPath = List.of(title);
- }
- if (menuGroup == null) {
- menuGroup = "";
- }
- if (menuOrder == null) {
- menuOrder = "";
- }
- if (iconId == null) {
- iconId = "icon.debugger";
- }
- }
-
- public String getDescription() {
- return description == null ? null : description.toString();
- }
- }
-
/**
* Create a launch offer from the given shell script.
*
@@ -473,161 +44,36 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
*/
public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
Program program, File script) throws FileNotFoundException {
- try (BufferedReader reader =
- new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
- AttributesParser attrs = new AttributesParser();
- String line;
- for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) {
- if (line.startsWith(SHEBANG) && lineNo == 1) {
- }
- else if (line.isBlank()) {
- continue;
- }
- else if (line.stripLeading().startsWith("#")) {
- attrs.parseLine(new Location(script.getName(), lineNo), line);
- }
- else {
- break;
- }
+ ScriptAttributesParser parser = new ScriptAttributesParser() {
+ @Override
+ protected boolean ignoreLine(int lineNo, String line) {
+ return line.isBlank() || line.startsWith(SHEBANG) && lineNo == 1;
}
- attrs.validate(script.getName());
- return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script,
- "UNIX_SHELL:" + script.getName(), attrs.title, attrs.getDescription(),
- attrs.menuPath, attrs.menuGroup, attrs.menuOrder, new GIcon(attrs.iconId),
- attrs.helpLocation, attrs.parameters, attrs.extraTtys);
- }
- catch (FileNotFoundException e) {
- // Avoid capture by IOException
- throw e;
- }
- catch (IOException e) {
- throw new AssertionError(e);
- }
- }
- protected final File script;
- protected final String configName;
- protected final String title;
- protected final String description;
- protected final List menuPath;
- protected final String menuGroup;
- protected final String menuOrder;
- protected final Icon icon;
- protected final HelpLocation helpLocation;
- protected final Map> parameters;
- protected final List extraTtys;
-
- public UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program,
- File script, String configName, String title, String description, List menuPath,
- String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
- Map> parameters, Collection extraTtys) {
- super(plugin, program);
- this.script = script;
- this.configName = configName;
- this.title = title;
- this.description = description;
- this.menuPath = List.copyOf(menuPath);
- this.menuGroup = menuGroup;
- this.menuOrder = menuOrder;
- this.icon = icon;
- this.helpLocation = helpLocation;
- this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
- this.extraTtys = List.copyOf(extraTtys);
- }
-
- @Override
- public String getConfigName() {
- return configName;
- }
-
- @Override
- public String getTitle() {
- return title;
- }
-
- @Override
- public String getDescription() {
- return description;
- }
-
- @Override
- public List getMenuPath() {
- return menuPath;
- }
-
- @Override
- public String getMenuGroup() {
- return menuGroup;
- }
-
- @Override
- public String getMenuOrder() {
- return menuOrder;
- }
-
- @Override
- public Icon getIcon() {
- return icon;
- }
-
- @Override
- public HelpLocation getHelpLocation() {
- return helpLocation;
- }
-
- @Override
- public Map> getParameters() {
- return parameters;
- }
-
- protected static String addrToString(InetAddress address) {
- if (address.isAnyLocalAddress()) {
- return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost.
- }
- return address.getHostAddress();
- }
-
- protected static String sockToString(SocketAddress address) {
- if (address instanceof InetSocketAddress tcp) {
- return addrToString(tcp.getAddress()) + ":" + tcp.getPort();
- }
- throw new AssertionError("Unhandled address type " + address);
- }
-
- @Override
- protected void launchBackEnd(TaskMonitor monitor, Map sessions,
- Map args, SocketAddress address) throws Exception {
- List commandLine = new ArrayList<>();
- Map env = new HashMap<>(System.getenv());
-
- commandLine.add(script.getAbsolutePath());
- env.put("GHIDRA_HOME", Application.getInstallationDirectory().getAbsolutePath());
- env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address));
-
- ParameterDescription> paramDesc;
- for (int i = 1; (paramDesc = parameters.get("arg:" + i)) != null; i++) {
- commandLine.add(Objects.toString(paramDesc.get(args)));
- }
-
- paramDesc = parameters.get("args");
- if (paramDesc != null) {
- commandLine.addAll(ShellUtils.parseArgs((String) paramDesc.get(args)));
- }
-
- for (Entry> ent : parameters.entrySet()) {
- String key = ent.getKey();
- if (key.startsWith(PREFIX_ENV)) {
- String varName = key.substring(PREFIX_ENV.length());
- env.put(varName, Objects.toString(ent.getValue().get(args)));
+ @Override
+ protected String removeDelimiter(String line) {
+ String stripped = line.stripLeading();
+ if (!stripped.startsWith("#")) {
+ return null;
+ }
+ return stripped.substring(1);
}
- }
+ };
+ ScriptAttributes attrs = parser.parseFile(script);
+ return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script,
+ "UNIX_SHELL:" + script.getName(), attrs);
+ }
- for (String tty : extraTtys) {
- NullPtyTerminalSession ns = nullPtyTerminal();
- env.put(tty, ns.name());
- sessions.put(ns.name(), ns);
- }
+ private UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin,
+ Program program,
+ File script, String configName, ScriptAttributes attrs) {
+ super(plugin, program, script, configName, attrs);
+ }
- sessions.put("Shell", runInTerminal(commandLine, env, sessions.values()));
+ @Override
+ protected void prepareSubprocess(List commandLine, Map env,
+ Map args, SocketAddress address) {
+ ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args,
+ address);
}
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOpinion.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOpinion.java
index d1ac0dc9f5..aebfa87aa2 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOpinion.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOpinion.java
@@ -20,63 +20,30 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import generic.jar.ResourceFile;
-import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
-import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
-import ghidra.framework.Application;
-import ghidra.framework.options.OptionType;
-import ghidra.framework.options.Options;
-import ghidra.framework.plugintool.PluginTool;
-import ghidra.framework.plugintool.util.PluginUtils;
import ghidra.program.model.listing.Program;
-import ghidra.util.HelpLocation;
import ghidra.util.Msg;
-public class UnixShellScriptTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion {
-
- @Override
- public void registerOptions(Options options) {
- String pluginName = PluginUtils.getPluginNameFromClass(TraceRmiLauncherServicePlugin.class);
- options.registerOption(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS,
- OptionType.STRING_TYPE, "", new HelpLocation(pluginName, "options"),
- "Paths to search for user-created debugger launchers", new ScriptPathsPropertyEditor());
- }
-
- @Override
- public boolean requiresRefresh(String optionName) {
- return TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS.equals(optionName);
- }
-
- protected Stream getModuleScriptPaths() {
- return Application.findModuleSubDirectories("data/debugger-launchers").stream();
- }
-
- protected Stream getUserScriptPaths(PluginTool tool) {
- Options options = tool.getOptions(DebuggerPluginPackage.NAME);
- String scriptPaths =
- options.getString(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS, "");
- return scriptPaths.lines().filter(d -> !d.isBlank()).map(ResourceFile::new);
- }
-
- protected Stream getScriptPaths(PluginTool tool) {
- return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool));
- }
+public class UnixShellScriptTraceRmiLaunchOpinion extends AbstractTraceRmiLaunchOpinion {
@Override
public Collection getOffers(TraceRmiLauncherServicePlugin plugin,
Program program) {
return getScriptPaths(plugin.getTool())
.flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".sh"))))
- .flatMap(sf -> {
- try {
- return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(plugin, program,
- sf.getFile(false)));
- }
- catch (Exception e) {
- Msg.error(this, "Could not offer " + sf + ":" + e.getMessage(), e);
- return Stream.of();
- }
- })
+ .flatMap(sf -> createOffer(plugin, program, sf))
.collect(Collectors.toList());
}
+
+ protected Stream createOffer(TraceRmiLauncherServicePlugin plugin,
+ Program program, ResourceFile scriptFile) {
+ try {
+ return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(plugin, program,
+ scriptFile.getFile(false)));
+ }
+ catch (Exception e) {
+ Msg.error(this, "Could not offer " + scriptFile + ": " + e.getMessage(), e);
+ return Stream.of();
+ }
+ }
}
diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java
index ba7cfec453..821e8d96db 100644
--- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java
+++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java
@@ -20,8 +20,6 @@ import java.math.BigInteger;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.*;
@@ -329,17 +327,17 @@ public class TraceRmiHandler implements TraceRmiConnection {
}
}
- protected DomainFolder createFolders(DomainFolder parent, Path path)
+ protected DomainFolder createFolders(DomainFolder parent, List path)
throws InvalidNameException, IOException {
return createFolders(parent, path, 0);
}
- protected DomainFolder createFolders(DomainFolder parent, Path path, int index)
+ protected DomainFolder createFolders(DomainFolder parent, List path, int index)
throws InvalidNameException, IOException {
- if (path == null && index == 0 || index == path.getNameCount()) {
+ if (path == null && index == 0 || index == path.size()) {
return parent;
}
- String name = path.getName(index).toString();
+ String name = path.get(index);
return createFolders(getOrCreateFolder(parent, name), path, index + 1);
}
@@ -859,10 +857,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
protected ReplyCreateTrace handleCreateTrace(RequestCreateTrace req)
throws InvalidNameException, IOException, CancelledException {
DomainFolder traces = getOrCreateNewTracesFolder();
- Path path = Paths.get(req.getPath().getPath());
- DomainFolder folder = createFolders(traces, path.getParent());
+ List path = sanitizePath(req.getPath().getPath());
+ DomainFolder folder = createFolders(traces, path.subList(0, path.size() - 1));
CompilerSpec cs = requireCompilerSpec(req.getLanguage(), req.getCompiler());
- DBTrace trace = new DBTrace(path.getFileName().toString(), cs, this);
+ DBTrace trace = new DBTrace(path.get(path.size() - 1), cs, this);
TraceRmiTarget target = new TraceRmiTarget(plugin.getTool(), this, trace);
DoId doId = requireAvailableDoId(req.getOid());
openTraces.put(new OpenTrace(doId, trace, target));
@@ -870,6 +868,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
return ReplyCreateTrace.getDefaultInstance();
}
+ protected static List sanitizePath(String path) {
+ return Stream.of(path.split("\\\\|/")).filter(p -> !p.isBlank()).toList();
+ }
+
protected ReplyDeleteBytes handleDeleteBytes(RequestDeleteBytes req)
throws AddressOverflowException {
OpenTrace open = requireOpenTrace(req.getOid());
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java
index b29fa6cfec..aad2a60ee3 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java
@@ -251,7 +251,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
if (context instanceof DebuggerObjectActionContext ctx) {
return DebuggerModulesPanel.getSelectedModulesFromContext(ctx);
}
- return null;
+ return Set.of();
}
protected static Set getSelectedSections(ActionContext context) {
@@ -264,7 +264,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
if (context instanceof DebuggerObjectActionContext ctx) {
return DebuggerModulesPanel.getSelectedSectionsFromContext(ctx);
}
- return null;
+ return Set.of();
}
protected static AddressSetView getSelectedAddresses(ActionContext context) {
@@ -299,7 +299,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
}
AddressSetView sel = getSelectedAddresses(context);
- if (sel == null) {
+ if (sel == null || sel.isEmpty()) {
return;
}
@@ -540,9 +540,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
.onAction(this::activatedMapModules)
.buildAndInstallLocal(this);
actionMapModuleTo = MapModuleToAction.builder(plugin)
- .withContext(DebuggerModuleActionContext.class)
- .enabledWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1)
- .popupWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1)
+ .enabledWhen(ctx -> currentProgram != null && getSelectedModules(ctx).size() == 1)
+ .popupWhen(ctx -> currentProgram != null && getSelectedModules(ctx).size() == 1)
.onAction(this::activatedMapModuleTo)
.buildAndInstallLocal(this);
actionMapSections = MapSectionsAction.builder(plugin)
@@ -551,9 +550,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
.onAction(this::activatedMapSections)
.buildAndInstallLocal(this);
actionMapSectionTo = MapSectionToAction.builder(plugin)
- .withContext(DebuggerSectionActionContext.class)
- .enabledWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1)
- .popupWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1)
+ .enabledWhen(ctx -> currentProgram != null && getSelectedSections(ctx).size() == 1)
+ .popupWhen(ctx -> currentProgram != null && getSelectedSections(ctx).size() == 1)
.onAction(this::activatedMapSectionTo)
.buildAndInstallLocal(this);
actionMapSectionsTo = MapSectionsToAction.builder(plugin)
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java
index a8c45398cf..3a3465f593 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java
@@ -25,6 +25,7 @@ import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.memory.TraceMemoryRegion;
+import ghidra.trace.model.memory.TraceObjectMemoryRegion;
import ghidra.trace.model.modules.TraceModule;
public class DefaultModuleMapProposal
@@ -207,10 +208,14 @@ public class DefaultModuleMapProposal
catch (AddressOverflowException e) {
return; // Just score it as having no matches?
}
+ Lifespan lifespan = module.getLifespan();
for (TraceMemoryRegion region : module.getTrace()
.getMemoryManager()
- .getRegionsIntersecting(module.getLifespan(), moduleRange)) {
- getMatcher(region.getMinAddress().subtract(moduleBase)).region = region;
+ .getRegionsIntersecting(lifespan, moduleRange)) {
+ Address min = region instanceof TraceObjectMemoryRegion objReg
+ ? objReg.getMinAddress(lifespan.lmin())
+ : region.getMinAddress();
+ getMatcher(min.subtract(moduleBase)).region = region;
}
}
diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ShellUtils.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ShellUtils.java
index c4c9df8d61..5164c557d8 100644
--- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ShellUtils.java
+++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ShellUtils.java
@@ -15,8 +15,8 @@
*/
package ghidra.dbg.util;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
public class ShellUtils {
enum State {
@@ -139,4 +139,11 @@ public class ShellUtils {
}
return line.toString();
}
+
+ public static String generateEnvBlock(Map env) {
+ return env.entrySet()
+ .stream()
+ .map(e -> e.getKey() + "=" + e.getValue() + "\0")
+ .collect(Collectors.joining()); // NB. JNA adds final terminator
+ }
}
diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java
index 04f8c3e5ad..b84f1e1e9a 100644
--- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java
+++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java
@@ -195,14 +195,22 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra
}
}
+ @Override
+ public AddressRange getRange(long snap) {
+ try (LockHold hold = object.getTrace().lockRead()) {
+ // TODO: Caching without regard to snap seems bad
+ return range = TraceObjectInterfaceUtils.getValue(object, snap,
+ TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, AddressRange.class, range);
+ }
+ }
+
@Override
public AddressRange getRange() {
try (LockHold hold = object.getTrace().lockRead()) {
if (object.getLife().isEmpty()) {
return range;
}
- return range = TraceObjectInterfaceUtils.getValue(object, getCreationSnap(),
- TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, AddressRange.class, range);
+ return getRange(getCreationSnap());
}
}
@@ -213,6 +221,12 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra
}
}
+ @Override
+ public Address getMinAddress(long snap) {
+ AddressRange range = getRange(snap);
+ return range == null ? null : range.getMinAddress();
+ }
+
@Override
public Address getMinAddress() {
AddressRange range = getRange();
@@ -226,6 +240,12 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra
}
}
+ @Override
+ public Address getMaxAddress(long snap) {
+ AddressRange range = getRange(snap);
+ return range == null ? null : range.getMaxAddress();
+ }
+
@Override
public Address getMaxAddress() {
AddressRange range = getRange();
diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceObjectMemoryRegion.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceObjectMemoryRegion.java
index 5ebc7a7d88..effd53dbe7 100644
--- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceObjectMemoryRegion.java
+++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceObjectMemoryRegion.java
@@ -20,6 +20,7 @@ import java.util.Set;
import ghidra.dbg.target.TargetMemoryRegion;
import ghidra.dbg.target.TargetObject;
+import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.target.TraceObjectInterface;
@@ -39,6 +40,12 @@ public interface TraceObjectMemoryRegion extends TraceMemoryRegion, TraceObjectI
void setRange(Lifespan lifespan, AddressRange range);
+ AddressRange getRange(long snap);
+
+ Address getMinAddress(long snap);
+
+ Address getMaxAddress(long snap);
+
void setFlags(Lifespan lifespan, Collection flags);
void addFlags(Lifespan lifespan, Collection flags);
diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java
index 32f6a3aec8..43314a2c6f 100644
--- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java
+++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java
@@ -15,6 +15,7 @@
*/
package ghidra.pty;
+import java.io.File;
import java.io.IOException;
import java.util.*;
@@ -51,19 +52,28 @@ public interface PtyChild extends PtyEndpoint {
*
* @param args the image path and arguments
* @param env the environment
+ * @param workingDirectory the working directory
* @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
* @return a handle to the subprocess
* @throws IOException if the session could not be started
*/
- PtySession session(String[] args, Map env, Collection mode)
- throws IOException;
+ PtySession session(String[] args, Map env, File workingDirectory,
+ Collection mode) throws IOException;
/**
- * @see #session(String[], Map, Collection)
+ * @see #session(String[], Map, File, Collection)
+ */
+ default PtySession session(String[] args, Map env, File workingDirectory,
+ TermMode... mode) throws IOException {
+ return session(args, env, workingDirectory, List.of(mode));
+ }
+
+ /**
+ * @see #session(String[], Map, File, Collection)
*/
default PtySession session(String[] args, Map env, TermMode... mode)
throws IOException {
- return session(args, env, List.of(mode));
+ return session(args, env, null, List.of(mode));
}
/**
diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java
index 8f0089ec77..8a89555d15 100644
--- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java
+++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java
@@ -57,13 +57,13 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
* program is active before sending special characters.
*/
@Override
- public PtySession session(String[] args, Map env, Collection mode)
- throws IOException {
- return sessionUsingJavaLeader(args, env, mode);
+ public PtySession session(String[] args, Map env, File workingDirectory,
+ Collection mode) throws IOException {
+ return sessionUsingJavaLeader(args, env, workingDirectory, mode);
}
protected PtySession sessionUsingJavaLeader(String[] args, Map env,
- Collection mode) throws IOException {
+ File workingDirectory, Collection mode) throws IOException {
final List argsList = new ArrayList<>();
String javaCommand =
System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
@@ -78,6 +78,9 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
if (env != null) {
builder.environment().putAll(env);
}
+ if (workingDirectory != null) {
+ builder.directory(workingDirectory);
+ }
builder.inheritIO();
applyMode(mode);
diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java
index b3421fa2c5..9d792a097b 100644
--- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java
+++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java
@@ -48,8 +48,11 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
}
@Override
- public SshPtySession session(String[] args, Map env, Collection mode)
- throws IOException {
+ public SshPtySession session(String[] args, Map env, File workingDirectory,
+ Collection mode) throws IOException {
+ if (workingDirectory != null) {
+ throw new UnsupportedOperationException();
+ }
/**
* TODO: This syntax assumes a UNIX-style shell, and even among them, this may not be
* universal. This certainly works for my version of bash :)
diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java
index 7f84402b86..76ff4a3f39 100644
--- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java
+++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java
@@ -15,6 +15,7 @@
*/
package ghidra.pty.windows;
+import java.io.File;
import java.io.IOException;
import java.util.*;
@@ -75,7 +76,7 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
@Override
public LocalWindowsNativeProcessPtySession session(String[] args, Map env,
- Collection mode) throws IOException {
+ File workingDirectory, Collection mode) throws IOException {
/**
* TODO: How to incorporate environment into CreateProcess?
*
@@ -91,9 +92,11 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
null /*lpProcessAttributes*/,
null /*lpThreadAttributes*/,
false /*bInheritHandles*/,
- ConPty.EXTENDED_STARTUPINFO_PRESENT /*dwCreationFlags*/,
- null /*lpEnvironment*/,
- null /*lpCurrentDirectory*/,
+ new DWORD(Kernel32.EXTENDED_STARTUPINFO_PRESENT |
+ Kernel32.CREATE_UNICODE_ENVIRONMENT) /*dwCreationFlags*/,
+ env == null ? null : new WString(ShellUtils.generateEnvBlock(env)),
+ workingDirectory == null ? null
+ : new WString(workingDirectory.getAbsolutePath()) /*lpCurrentDirectory*/,
si /*lpStartupInfo*/,
pi /*lpProcessInformation*/).booleanValue()) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java
index 3df225168c..312e6b9ccf 100644
--- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java
+++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java
@@ -83,7 +83,9 @@ public class HandleInputStream extends InputStream {
public synchronized int read(byte[] b, int off, int len) throws IOException {
byte[] temp = new byte[len];
int read = read(temp);
- System.arraycopy(temp, 0, b, off, read);
+ if (read > 0) {
+ System.arraycopy(temp, 0, b, off, read);
+ }
return read;
}
diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java
index baf59de4be..72efc340b4 100644
--- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java
+++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java
@@ -57,7 +57,7 @@ public interface ConsoleApiNative extends StdCallLibrary {
WinBase.SECURITY_ATTRIBUTES lpThreadAttributes,
boolean bInheritHandles,
DWORD dwCreationFlags,
- Pointer lpEnvironment,
+ WString lpEnvironment,
WString lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
WinBase.PROCESS_INFORMATION lpProcessInformation);
diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java
index ced7165f55..8d90b7050a 100644
--- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java
+++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java
@@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assume.assumeTrue;
import java.util.*;
@@ -28,6 +29,7 @@ import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.services.TraceRmiLauncherService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*;
+import ghidra.framework.OperatingSystem;
import ghidra.util.task.ConsoleTaskMonitor;
public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebuggerTest {
@@ -50,7 +52,7 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
assertFalse(launcherService.getOffers(program).isEmpty());
}
- protected LaunchConfigurator fileOnly(String file) {
+ protected LaunchConfigurator gdbFileOnly(String file) {
return new LaunchConfigurator() {
@Override
public Map configureLauncher(TraceRmiLaunchOffer offer,
@@ -65,16 +67,18 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
// @Test // This is currently hanging the test machine. The gdb process is left running
public void testLaunchLocalGdb() throws Exception {
+ assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.LINUX);
+
createProgram(getSLEIGH_X86_64_LANGUAGE());
try (Transaction tx = program.openTransaction("Rename")) {
program.setName("bash");
}
programManager.openProgram(program);
- TraceRmiLaunchOffer gdbOffer = findByTitle(launcherService.getOffers(program), "gdb");
+ TraceRmiLaunchOffer offer = findByTitle(launcherService.getOffers(program), "gdb");
try (LaunchResult result =
- gdbOffer.launchProgram(new ConsoleTaskMonitor(), fileOnly("/usr/bin/bash"))) {
+ offer.launchProgram(new ConsoleTaskMonitor(), gdbFileOnly("/usr/bin/bash"))) {
if (result.exception() != null) {
throw new AssertionError(result.exception());
}
@@ -83,4 +87,39 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
assertEquals(getSLEIGH_X86_64_LANGUAGE(), result.trace().getBaseLanguage());
}
}
+
+ protected LaunchConfigurator dbgengFileOnly(String file) {
+ return new LaunchConfigurator() {
+ @Override
+ public Map configureLauncher(TraceRmiLaunchOffer offer,
+ Map arguments, RelPrompt relPrompt) {
+ Map args = new HashMap<>(arguments);
+ args.put("env:OPT_TARGET_IMG", file);
+ return args;
+ }
+ };
+ }
+
+ @Test
+ public void testLaunchLocalDbgeng() throws Exception {
+ assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS);
+
+ createProgram(getSLEIGH_X86_64_LANGUAGE());
+ try (Transaction tx = program.openTransaction("Rename")) {
+ program.setName("notepad.exe");
+ }
+ programManager.openProgram(program);
+
+ TraceRmiLaunchOffer offer = findByTitle(launcherService.getOffers(program), "dbgeng");
+
+ try (LaunchResult result =
+ offer.launchProgram(new ConsoleTaskMonitor(), dbgengFileOnly("notepad.exe"))) {
+ if (result.exception() != null) {
+ throw new AssertionError(result.exception());
+ }
+
+ assertEquals("notepad.exe", result.trace().getName());
+ assertEquals(getSLEIGH_X86_64_LANGUAGE(), result.trace().getBaseLanguage());
+ }
+ }
}