GP-3823: TraceRmi Launcher framework + dbgeng for Windows.

This commit is contained in:
Dan 2023-11-28 10:38:27 -05:00
parent 80d92aa32f
commit c126cf51c0
33 changed files with 1206 additions and 1303 deletions

View file

@ -0,0 +1,109 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.io.File;
import java.net.SocketAddress;
import java.util.*;
import javax.swing.Icon;
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
import ghidra.dbg.target.TargetMethod.ParameterDescription;
import ghidra.debug.api.tracermi.TerminalSession;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
import ghidra.util.task.TaskMonitor;
public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer {
protected final File script;
protected final String configName;
protected final ScriptAttributes attrs;
public AbstractScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program,
File script, String configName, ScriptAttributes attrs) {
super(plugin, program);
this.script = script;
this.configName = configName;
this.attrs = attrs;
}
@Override
public String getConfigName() {
return configName;
}
@Override
public String getTitle() {
return attrs.title();
}
@Override
public String getDescription() {
return attrs.description();
}
@Override
public List<String> getMenuPath() {
return attrs.menuPath();
}
@Override
public String getMenuGroup() {
return attrs.menuGroup();
}
@Override
public String getMenuOrder() {
return attrs.menuOrder();
}
@Override
public Icon getIcon() {
return attrs.icon();
}
@Override
public HelpLocation getHelpLocation() {
return attrs.helpLocation();
}
@Override
public Map<String, ParameterDescription<?>> getParameters() {
return attrs.parameters();
}
protected abstract void prepareSubprocess(List<String> commandLine, Map<String, String> env,
Map<String, ?> args, SocketAddress address);
@Override
protected void launchBackEnd(TaskMonitor monitor, Map<String, TerminalSession> sessions,
Map<String, ?> args, SocketAddress address) throws Exception {
List<String> commandLine = new ArrayList<>();
Map<String, String> env = new HashMap<>(System.getenv());
prepareSubprocess(commandLine, env, args, address);
for (String tty : attrs.extraTtys()) {
NullPtyTerminalSession ns = nullPtyTerminal();
env.put(tty, ns.name());
sessions.put(ns.name(), ns);
}
sessions.put("Shell",
runInTerminal(commandLine, env, script.getParentFile(), sessions.values()));
}
}

View file

@ -420,8 +420,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
}
protected PtyTerminalSession runInTerminal(List<String> commandLine, Map<String, String> env,
Collection<TerminalSession> subordinates)
throws IOException {
File workingDirectory, Collection<TerminalSession> subordinates) throws IOException {
PtyFactory factory = getPtyFactory();
Pty pty = factory.openpty();
@ -432,13 +431,19 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
TerminalListener resizeListener = new TerminalListener() {
@Override
public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows);
try {
parent.setWindowSize(cols, rows);
}
catch (Exception e) {
Msg.error(this, "Could not resize pty: " + e);
}
}
};
terminal.addTerminalListener(resizeListener);
env.put("TERM", "xterm-256color");
PtySession session = pty.getChild().session(commandLine.toArray(String[]::new), env);
PtySession session =
pty.getChild().session(commandLine.toArray(String[]::new), env, workingDirectory);
Thread waiter = new Thread(() -> {
try {

View file

@ -0,0 +1,60 @@
/* ###
* 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.stream.Stream;
import generic.jar.ResourceFile;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
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.util.HelpLocation;
public abstract class AbstractTraceRmiLaunchOpinion 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<ResourceFile> getModuleScriptPaths() {
return Application.findModuleSubDirectories("data/debugger-launchers").stream();
}
protected Stream<ResourceFile> 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<ResourceFile> getScriptPaths(PluginTool tool) {
return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool));
}
}

View file

@ -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.
*
* <p>
* 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<String> commandLine, Map<String, String> env,
Map<String, ?> args, SocketAddress address) {
ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args,
address);
}
}

View file

@ -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<TraceRmiLaunchOffer> 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<TraceRmiLaunchOffer> 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();
}
}
}

View file

@ -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<DockingActionIf> actions = new ArrayList<>();
ProgramUserData userData = program.getProgramUserData();
Map<String, Long> 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();
}
}

View file

@ -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:
* <ul>
* <li>{@code @menu-path}: <b>(Required)</b></li>
* </ul>
*
*/
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<T> {
static OptType<?> parse(Location loc, String typeName,
Map<String, UserType<?>> 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<T> withCastDefault(Object defaultValue) {
return new TypeAndDefault<>(this, cls().cast(defaultValue));
}
Class<T> cls();
T decode(Location loc, String str);
ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description);
}
protected interface BaseType<T> extends OptType<T> {
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> STRING = new BaseType<>() {
@Override
public Class<String> cls() {
return String.class;
}
@Override
public String decode(Location loc, String str) {
return str;
}
};
public static final BaseType<BigInteger> INT = new BaseType<>() {
@Override
public Class<BigInteger> 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<Boolean> BOOL = new BaseType<>() {
@Override
public Class<Boolean> 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<T> withCastChoices(List<?> choices) {
return new UserType<>(this, choices.stream().map(cls()::cast).toList());
}
@Override
default ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.create(cls(), name, false, defaultValue, display,
description);
}
}
protected record UserType<T>(BaseType<T> base, List<T> choices) implements OptType<T> {
@Override
public Class<T> cls() {
return base.cls();
}
@Override
public T decode(Location loc, String str) {
return base.decode(loc, str);
}
@Override
public ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
description);
}
}
protected record TypeAndDefault<T>(OptType<T> type, T defaultValue) {
public static TypeAndDefault<?> parse(Location loc, String typeName, String defaultString,
Map<String, UserType<?>> 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<T> 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<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
Map<String, ParameterDescription<?>> parameters, Collection<String> 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<String> commandLine, Map<String, String> env,
File script, Map<String, ParameterDescription<?>> parameters, Map<String, ?> 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<String, ParameterDescription<?>> 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<String> menuPath;
private String menuGroup;
private String menuOrder;
private String iconId;
private HelpLocation helpLocation;
private final Map<String, UserType<?>> userTypes = new HashMap<>();
private final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
private final Set<String> 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
*
* <p>
* 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<String> 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<String> 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<String> 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<String> parts = ShellUtils.parseArgs(str);
if (parts.size() != 2) {
Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
return;
}
ParameterDescription<String> 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();
}
}

View file

@ -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.
*
* <p>
* The script must start with an attributes header in a comment block. Some attributes are required.
* Others are optional:
* <ul>
* <li>{@code @menu-path}: <b>(Required)</b></li>
* </ul>
* 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<T> {
static OptType<?> parse(Location loc, String typeName,
Map<String, UserType<?>> 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<T> withCastDefault(Object defaultValue) {
return new TypeAndDefault<>(this, cls().cast(defaultValue));
}
Class<T> cls();
T decode(Location loc, String str);
ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description);
}
protected interface BaseType<T> extends OptType<T> {
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> STRING = new BaseType<>() {
@Override
public Class<String> cls() {
return String.class;
}
@Override
public String decode(Location loc, String str) {
return str;
}
};
public static final BaseType<BigInteger> INT = new BaseType<>() {
@Override
public Class<BigInteger> 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<Boolean> BOOL = new BaseType<>() {
@Override
public Class<Boolean> 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<T> withCastChoices(List<?> choices) {
return new UserType<>(this, choices.stream().map(cls()::cast).toList());
}
@Override
default ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.create(cls(), name, false, defaultValue, display,
description);
}
}
protected record UserType<T> (BaseType<T> base, List<T> choices) implements OptType<T> {
@Override
public Class<T> cls() {
return base.cls();
}
@Override
public T decode(Location loc, String str) {
return base.decode(loc, str);
}
@Override
public ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
description);
}
}
protected record TypeAndDefault<T> (OptType<T> type, T defaultValue) {
public static TypeAndDefault<?> parse(Location loc, String typeName, String defaultString,
Map<String, UserType<?>> 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<T> 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<String> menuPath;
protected String menuGroup;
protected String menuOrder;
protected String iconId;
protected HelpLocation helpLocation;
protected final Map<String, UserType<?>> userTypes = new HashMap<>();
protected final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
protected final Set<String> 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<String> 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<String> 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<String> 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<String> parts = ShellUtils.parseArgs(str);
if (parts.size() != 2) {
Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
return;
}
ParameterDescription<String> 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<String> menuPath;
protected final String menuGroup;
protected final String menuOrder;
protected final Icon icon;
protected final HelpLocation helpLocation;
protected final Map<String, ParameterDescription<?>> parameters;
protected final List<String> extraTtys;
public UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program,
File script, String configName, String title, String description, List<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
Map<String, ParameterDescription<?>> parameters, Collection<String> 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<String> 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<String, ParameterDescription<?>> 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<String, TerminalSession> sessions,
Map<String, ?> args, SocketAddress address) throws Exception {
List<String> commandLine = new ArrayList<>();
Map<String, String> 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<String, ParameterDescription<?>> 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<String> commandLine, Map<String, String> env,
Map<String, ?> args, SocketAddress address) {
ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args,
address);
}
}

View file

@ -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<ResourceFile> getModuleScriptPaths() {
return Application.findModuleSubDirectories("data/debugger-launchers").stream();
}
protected Stream<ResourceFile> 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<ResourceFile> getScriptPaths(PluginTool tool) {
return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool));
}
public class UnixShellScriptTraceRmiLaunchOpinion extends AbstractTraceRmiLaunchOpinion {
@Override
public Collection<TraceRmiLaunchOffer> 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<TraceRmiLaunchOffer> 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();
}
}
}

View file

@ -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<String> path)
throws InvalidNameException, IOException {
return createFolders(parent, path, 0);
}
protected DomainFolder createFolders(DomainFolder parent, Path path, int index)
protected DomainFolder createFolders(DomainFolder parent, List<String> 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<String> 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<String> 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());