mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-21 19:42:14 +00:00
GP-4847: Unify Debugger param dialogs. Prefixed integers allowed.
This commit is contained in:
parent
e0bf7b4c53
commit
16ff4c4d08
@ -1,18 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
## ###
|
||||
# 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.
|
||||
# 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.
|
||||
##
|
||||
#@title remote gdb
|
||||
#@no-image
|
||||
@ -29,7 +29,7 @@
|
||||
#@enum TargetType:str remote extended-remote
|
||||
#@env OPT_TARGET_TYPE:TargetType="remote" "Target" "The type of remote target"
|
||||
#@env OPT_HOST:str="localhost" "Host" "The hostname of the target"
|
||||
#@env OPT_PORT:str="9999" "Port" "The host's listening port"
|
||||
#@env OPT_PORT:int=9999 "Port" "The host's listening port"
|
||||
#@env OPT_ARCH:str="" "Architecture (optional)" "Target architecture override"
|
||||
#@env OPT_GDB_PATH:file="gdb" "gdb command" "The path to gdb on the local system. Omit the full path to resolve using the system PATH."
|
||||
|
||||
@ -60,6 +60,7 @@ fi
|
||||
-ex "show version" \
|
||||
-ex "python import ghidragdb" \
|
||||
$archcmd \
|
||||
-ex "echo Connecting to $OPT_HOST:$OPT_PORT... " \
|
||||
-ex "target $OPT_TARGET_TYPE $OPT_HOST:$OPT_PORT" \
|
||||
-ex "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \
|
||||
-ex "ghidra trace start" \
|
||||
|
@ -0,0 +1,59 @@
|
||||
/* ###
|
||||
* 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.debug.api;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public record ValStr<T>(T val, String str) {
|
||||
|
||||
public interface Decoder<T> {
|
||||
default ValStr<T> decodeValStr(String string) {
|
||||
return new ValStr<>(decode(string), string);
|
||||
}
|
||||
|
||||
T decode(String string);
|
||||
}
|
||||
|
||||
public static ValStr<String> str(String value) {
|
||||
return new ValStr<>(value, value);
|
||||
}
|
||||
|
||||
public static <T> ValStr<T> from(T value) {
|
||||
return new ValStr<>(value, value == null ? "" : value.toString());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> ValStr<T> cast(Class<T> cls, ValStr<?> value) {
|
||||
if (cls.isInstance(value.val)) {
|
||||
return (ValStr<T>) value;
|
||||
}
|
||||
return new ValStr<>(cls.cast(value.val), value.str);
|
||||
}
|
||||
|
||||
public static Map<String, ValStr<?>> fromPlainMap(Map<String, ?> map) {
|
||||
return map.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Entry::getKey, e -> ValStr.from(e.getValue())));
|
||||
}
|
||||
|
||||
public static Map<String, ? super Object> toPlainMap(Map<String, ValStr<?>> map) {
|
||||
return map.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Entry::getKey, e -> e.getValue().val()));
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -24,6 +24,7 @@ import ghidra.dbg.DebuggerModelFactory;
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.target.TargetLauncher;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
@ -117,8 +118,8 @@ public interface DebuggerProgramLaunchOffer {
|
||||
* @param relPrompt describes the timing of this callback relative to prompting the user
|
||||
* @return the adjusted arguments
|
||||
*/
|
||||
default Map<String, ?> configureLauncher(TargetLauncher launcher,
|
||||
Map<String, ?> arguments, RelPrompt relPrompt) {
|
||||
default Map<String, ValStr<?>> configureLauncher(TargetLauncher launcher,
|
||||
Map<String, ValStr<?>> arguments, RelPrompt relPrompt) {
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,106 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.debug.api.tracermi;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.debug.api.ValStr;
|
||||
|
||||
public record LaunchParameter<T>(Class<T> type, String name, String display, String description,
|
||||
boolean required, List<T> choices, ValStr<T> defaultValue, ValStr.Decoder<T> decoder) {
|
||||
|
||||
public static <T> LaunchParameter<T> create(Class<T> type, String name, String display,
|
||||
String description, boolean required, ValStr<T> defaultValue,
|
||||
ValStr.Decoder<T> decoder) {
|
||||
return new LaunchParameter<>(type, name, display, description, required, List.of(),
|
||||
defaultValue, decoder);
|
||||
}
|
||||
|
||||
public static <T> LaunchParameter<T> choices(Class<T> type, String name, String display,
|
||||
String description, Collection<T> choices, ValStr<T> defaultValue) {
|
||||
return new LaunchParameter<>(type, name, display, description, false,
|
||||
List.copyOf(new LinkedHashSet<>(choices)), defaultValue, str -> {
|
||||
for (T t : choices) {
|
||||
if (t.toString().equals(str)) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static Map<String, LaunchParameter<?>> mapOf(Collection<LaunchParameter<?>> parameters) {
|
||||
Map<String, LaunchParameter<?>> result = new LinkedHashMap<>();
|
||||
for (LaunchParameter<?> param : parameters) {
|
||||
LaunchParameter<?> exists = result.put(param.name(), param);
|
||||
if (exists != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Duplicate names in parameter map: first=%s, second=%s".formatted(exists,
|
||||
param));
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
public static Map<String, ValStr<?>> validateArguments(
|
||||
Map<String, LaunchParameter<?>> parameters, Map<String, ValStr<?>> arguments) {
|
||||
if (!parameters.keySet().containsAll(arguments.keySet())) {
|
||||
Set<String> extraneous = new TreeSet<>(arguments.keySet());
|
||||
extraneous.removeAll(parameters.keySet());
|
||||
throw new IllegalArgumentException("Extraneous parameters: " + extraneous);
|
||||
}
|
||||
|
||||
Map<String, String> typeErrors = null;
|
||||
for (Map.Entry<String, ValStr<?>> ent : arguments.entrySet()) {
|
||||
String name = ent.getKey();
|
||||
ValStr<?> val = ent.getValue();
|
||||
LaunchParameter<?> param = parameters.get(name);
|
||||
if (val.val() != null && !param.type.isAssignableFrom(val.val().getClass())) {
|
||||
if (typeErrors == null) {
|
||||
typeErrors = new LinkedHashMap<>();
|
||||
}
|
||||
typeErrors.put(name, "val '%s' is not a %s".formatted(val.val(), param.type()));
|
||||
}
|
||||
}
|
||||
if (typeErrors != null) {
|
||||
throw new IllegalArgumentException("Type errors: " + typeErrors);
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public static Map<String, LaunchParameter<?>> mapOf(LaunchParameter<?>... parameters) {
|
||||
return mapOf(Arrays.asList(parameters));
|
||||
}
|
||||
|
||||
public ValStr<T> decode(String string) {
|
||||
return decoder.decodeValStr(string);
|
||||
}
|
||||
|
||||
public ValStr<T> get(Map<String, ValStr<?>> arguments) {
|
||||
if (arguments.containsKey(name)) {
|
||||
return ValStr.cast(type, arguments.get(name));
|
||||
}
|
||||
if (required) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing required parameter '%s' (%s)".formatted(display, name));
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void set(Map<String, ValStr<?>> arguments, ValStr<T> value) {
|
||||
arguments.put(name, value);
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -20,7 +20,7 @@ import java.util.Map;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.util.HelpLocation;
|
||||
@ -150,8 +150,8 @@ public interface TraceRmiLaunchOffer {
|
||||
* @param relPrompt describes the timing of this callback relative to prompting the user
|
||||
* @return the adjusted arguments
|
||||
*/
|
||||
default Map<String, ?> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ?> arguments, RelPrompt relPrompt) {
|
||||
default Map<String, ValStr<?>> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ValStr<?>> arguments, RelPrompt relPrompt) {
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
@ -293,7 +293,7 @@ public interface TraceRmiLaunchOffer {
|
||||
*
|
||||
* @return the parameters
|
||||
*/
|
||||
Map<String, ParameterDescription<?>> getParameters();
|
||||
Map<String, LaunchParameter<?>> getParameters();
|
||||
|
||||
/**
|
||||
* Check if this offer requires an open program
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -28,6 +28,7 @@ import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
|
||||
import ghidra.dbg.target.TargetLauncher.TargetCmdLineLauncher;
|
||||
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.model.DebuggerProgramLaunchOffer;
|
||||
import ghidra.debug.api.model.DebuggerProgramLaunchOffer.*;
|
||||
import ghidra.debug.api.model.TraceRecorder;
|
||||
@ -578,10 +579,10 @@ public interface FlatDebuggerRecorderAPI extends FlatDebuggerAPI {
|
||||
try {
|
||||
return waitOn(offer.launchProgram(monitor, PromptMode.NEVER, new LaunchConfigurator() {
|
||||
@Override
|
||||
public Map<String, ?> configureLauncher(TargetLauncher launcher,
|
||||
Map<String, ?> arguments, RelPrompt relPrompt) {
|
||||
Map<String, Object> adjusted = new HashMap<>(arguments);
|
||||
adjusted.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, commandLine);
|
||||
public Map<String, ValStr<?>> configureLauncher(TargetLauncher launcher,
|
||||
Map<String, ValStr<?>> arguments, RelPrompt relPrompt) {
|
||||
Map<String, ValStr<?>> adjusted = new HashMap<>(arguments);
|
||||
adjusted.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME, ValStr.str(commandLine));
|
||||
return adjusted;
|
||||
}
|
||||
}));
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -16,8 +16,10 @@
|
||||
package ghidra.debug.flatapi;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import ghidra.app.services.TraceRmiLauncherService;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*;
|
||||
import ghidra.program.model.listing.Program;
|
||||
@ -116,13 +118,15 @@ public interface FlatDebuggerRmiAPI extends FlatDebuggerAPI {
|
||||
TaskMonitor monitor) {
|
||||
return offer.launchProgram(monitor, new LaunchConfigurator() {
|
||||
@Override
|
||||
public Map<String, ?> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ?> arguments, RelPrompt relPrompt) {
|
||||
public Map<String, ValStr<?>> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ValStr<?>> arguments, RelPrompt relPrompt) {
|
||||
if (arguments.isEmpty()) {
|
||||
return arguments;
|
||||
}
|
||||
Map<String, Object> args = new HashMap<>(arguments);
|
||||
args.putAll(overrideArgs);
|
||||
Map<String, ValStr<?>> args = new HashMap<>(arguments);
|
||||
for (Entry<String, ?> ent : overrideArgs.entrySet()) {
|
||||
args.put(ent.getKey(), ValStr.from(ent.getValue()));
|
||||
}
|
||||
return args;
|
||||
}
|
||||
});
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -15,397 +15,128 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.tracermi;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.beans.PropertyEditor;
|
||||
import java.beans.PropertyEditorManager;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.beans.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.DialogComponentProvider;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget;
|
||||
import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils;
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractDebuggerParameterDialog;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget.Missing;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.SchemaContext;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.RemoteParameter;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.layout.PairLayout;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
|
||||
public class RemoteMethodInvocationDialog extends DialogComponentProvider
|
||||
implements PropertyChangeListener {
|
||||
private static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments";
|
||||
public class RemoteMethodInvocationDialog extends AbstractDebuggerParameterDialog<RemoteParameter> {
|
||||
|
||||
static class ChoicesPropertyEditor implements PropertyEditor {
|
||||
private final List<?> choices;
|
||||
private final String[] tags;
|
||||
|
||||
private final List<PropertyChangeListener> listeners = new ArrayList<>();
|
||||
|
||||
private Object value;
|
||||
|
||||
public ChoicesPropertyEditor(Set<?> choices) {
|
||||
this.choices = List.copyOf(choices);
|
||||
this.tags = choices.stream().map(Objects::toString).toArray(String[]::new);
|
||||
}
|
||||
/**
|
||||
* TODO: Make this a proper editor which can browse and select objects of a required schema.
|
||||
*/
|
||||
public static class TraceObjectEditor extends PropertyEditorSupport {
|
||||
private final JLabel unmodifiableField = new JLabel();
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (Objects.equals(value, this.value)) {
|
||||
super.setValue(value);
|
||||
if (value == null) {
|
||||
unmodifiableField.setText("");
|
||||
return;
|
||||
}
|
||||
if (!choices.contains(value)) {
|
||||
throw new IllegalArgumentException("Unsupported value: " + value);
|
||||
if (!(value instanceof TraceObject obj)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
Object oldValue;
|
||||
List<PropertyChangeListener> listeners;
|
||||
synchronized (this.listeners) {
|
||||
oldValue = this.value;
|
||||
this.value = value;
|
||||
if (this.listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
listeners = List.copyOf(this.listeners);
|
||||
}
|
||||
PropertyChangeEvent evt = new PropertyChangeEvent(this, null, oldValue, value);
|
||||
for (PropertyChangeListener l : listeners) {
|
||||
l.propertyChange(evt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPaintable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintValue(Graphics gfx, Rectangle box) {
|
||||
// Not paintable
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJavaInitializationString() {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
if (value instanceof String str) {
|
||||
return "\"" + StringEscapeUtils.escapeJava(str) + "\"";
|
||||
}
|
||||
return Objects.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAsText() {
|
||||
return Objects.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
int index = ArrayUtils.indexOf(tags, text);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("Unsupported value: " + text);
|
||||
}
|
||||
setValue(choices.get(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return tags.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getCustomEditor() {
|
||||
return null;
|
||||
unmodifiableField.setText(obj.getCanonicalPath().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCustomEditor() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPropertyChangeListener(PropertyChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePropertyChangeListener(PropertyChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
public Component getCustomEditor() {
|
||||
return unmodifiableField;
|
||||
}
|
||||
}
|
||||
|
||||
record NameTypePair(String name, Class<?> type) {
|
||||
public static NameTypePair fromParameter(SchemaContext ctx, RemoteParameter parameter) {
|
||||
return new NameTypePair(parameter.name(), ctx.getSchema(parameter.type()).getType());
|
||||
}
|
||||
|
||||
public static NameTypePair fromString(String name) throws ClassNotFoundException {
|
||||
String[] parts = name.split(",", 2);
|
||||
if (parts.length != 2) {
|
||||
// This appears to be a bad assumption - empty fields results in solitary labels
|
||||
return new NameTypePair(parts[0], String.class);
|
||||
//throw new IllegalArgumentException("Could not parse name,type");
|
||||
}
|
||||
return new NameTypePair(parts[0], Class.forName(parts[1]));
|
||||
}
|
||||
static {
|
||||
PropertyEditorManager.registerEditor(TraceObject.class, TraceObjectEditor.class);
|
||||
}
|
||||
|
||||
private final BidiMap<RemoteParameter, PropertyEditor> paramEditors =
|
||||
new DualLinkedHashBidiMap<>();
|
||||
private final SchemaContext ctx;
|
||||
|
||||
private JPanel panel;
|
||||
private JLabel descriptionLabel;
|
||||
private JPanel pairPanel;
|
||||
private PairLayout layout;
|
||||
|
||||
protected JButton invokeButton;
|
||||
protected JButton resetButton;
|
||||
|
||||
private final PluginTool tool;
|
||||
private SchemaContext ctx;
|
||||
private Map<String, RemoteParameter> parameters;
|
||||
private Map<String, Object> defaults;
|
||||
|
||||
// TODO: Not sure this is the best keying, but I think it works.
|
||||
private Map<NameTypePair, Object> memorized = new HashMap<>();
|
||||
private Map<String, Object> arguments;
|
||||
|
||||
public RemoteMethodInvocationDialog(PluginTool tool, String title, String buttonText,
|
||||
Icon buttonIcon) {
|
||||
super(title, true, true, true, false);
|
||||
this.tool = tool;
|
||||
|
||||
populateComponents(buttonText, buttonIcon);
|
||||
setRememberSize(false);
|
||||
}
|
||||
|
||||
protected Object computeMemorizedValue(RemoteParameter parameter) {
|
||||
return memorized.computeIfAbsent(NameTypePair.fromParameter(ctx, parameter),
|
||||
ntp -> parameter.getDefaultValue());
|
||||
}
|
||||
|
||||
public Map<String, Object> promptArguments(SchemaContext ctx,
|
||||
Map<String, RemoteParameter> parameterMap, Map<String, Object> defaults) {
|
||||
setParameters(ctx, parameterMap);
|
||||
setDefaults(defaults);
|
||||
tool.showDialog(this);
|
||||
|
||||
return getArguments();
|
||||
}
|
||||
|
||||
public void setParameters(SchemaContext ctx, Map<String, RemoteParameter> parameterMap) {
|
||||
public RemoteMethodInvocationDialog(PluginTool tool, SchemaContext ctx, String title,
|
||||
String buttonText, Icon buttonIcon) {
|
||||
super(tool, title, buttonText, buttonIcon);
|
||||
this.ctx = ctx;
|
||||
this.parameters = parameterMap;
|
||||
populateOptions();
|
||||
}
|
||||
|
||||
public void setDefaults(Map<String, Object> defaults) {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
private void populateComponents(String buttonText, Icon buttonIcon) {
|
||||
panel = new JPanel(new BorderLayout());
|
||||
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
layout = new PairLayout(5, 5);
|
||||
pairPanel = new JPanel(layout);
|
||||
|
||||
JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER));
|
||||
JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
//scrolling.setPreferredSize(new Dimension(100, 130));
|
||||
panel.add(scrolling, BorderLayout.CENTER);
|
||||
centering.add(pairPanel);
|
||||
|
||||
descriptionLabel = new JLabel();
|
||||
descriptionLabel.setMaximumSize(new Dimension(300, 100));
|
||||
panel.add(descriptionLabel, BorderLayout.NORTH);
|
||||
|
||||
addWorkPanel(panel);
|
||||
|
||||
invokeButton = new JButton(buttonText, buttonIcon);
|
||||
addButton(invokeButton);
|
||||
resetButton = new JButton("Reset", DebuggerResources.ICON_REFRESH);
|
||||
addButton(resetButton);
|
||||
addCancelButton();
|
||||
|
||||
invokeButton.addActionListener(this::invoke);
|
||||
resetButton.addActionListener(this::reset);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelCallback() {
|
||||
this.arguments = null;
|
||||
close();
|
||||
protected String parameterName(RemoteParameter parameter) {
|
||||
return parameter.name();
|
||||
}
|
||||
|
||||
protected void invoke(ActionEvent evt) {
|
||||
this.arguments = collectArguments();
|
||||
close();
|
||||
}
|
||||
|
||||
private void reset(ActionEvent evt) {
|
||||
this.arguments = new HashMap<>();
|
||||
for (RemoteParameter param : parameters.values()) {
|
||||
if (defaults.containsKey(param.name())) {
|
||||
arguments.put(param.name(), defaults.get(param.name()));
|
||||
}
|
||||
else {
|
||||
arguments.put(param.name(), param.getDefaultValue());
|
||||
}
|
||||
@Override
|
||||
protected Class<?> parameterType(RemoteParameter parameter) {
|
||||
Class<?> type = ctx.getSchema(parameter.type()).getType();
|
||||
if (TargetObject.class.isAssignableFrom(type)) {
|
||||
return TraceObject.class;
|
||||
}
|
||||
populateValues();
|
||||
return type;
|
||||
}
|
||||
|
||||
protected PropertyEditor createEditor(RemoteParameter param) {
|
||||
Class<?> type = ctx.getSchema(param.type()).getType();
|
||||
PropertyEditor editor = PropertyEditorManager.findEditor(type);
|
||||
if (editor != null) {
|
||||
return editor;
|
||||
}
|
||||
Msg.warn(this, "No editor for " + type + "? Trying String instead");
|
||||
return PropertyEditorManager.findEditor(String.class);
|
||||
@Override
|
||||
protected String parameterLabel(RemoteParameter parameter) {
|
||||
return "".equals(parameter.display()) ? parameter.name() : parameter.display();
|
||||
}
|
||||
|
||||
void populateOptions() {
|
||||
pairPanel.removeAll();
|
||||
paramEditors.clear();
|
||||
for (RemoteParameter param : parameters.values()) {
|
||||
String text = param.display().equals("") ? param.name() : param.display();
|
||||
JLabel label = new JLabel(text);
|
||||
label.setToolTipText(param.description());
|
||||
pairPanel.add(label);
|
||||
|
||||
PropertyEditor editor = createEditor(param);
|
||||
Object val = computeMemorizedValue(param);
|
||||
if (val == null || val.equals(TraceRmiTarget.Missing.MISSING)) {
|
||||
editor.setValue("");
|
||||
} else {
|
||||
editor.setValue(val);
|
||||
}
|
||||
editor.addPropertyChangeListener(this);
|
||||
pairPanel.add(MiscellaneousUtils.getEditorComponent(editor));
|
||||
paramEditors.put(param, editor);
|
||||
}
|
||||
@Override
|
||||
protected String parameterToolTip(RemoteParameter parameter) {
|
||||
return parameter.description();
|
||||
}
|
||||
|
||||
void populateValues() {
|
||||
for (Map.Entry<String, Object> ent : arguments.entrySet()) {
|
||||
RemoteParameter param = parameters.get(ent.getKey());
|
||||
if (param == null) {
|
||||
Msg.warn(this, "No parameter for argument: " + ent);
|
||||
continue;
|
||||
}
|
||||
PropertyEditor editor = paramEditors.get(param);
|
||||
editor.setValue(ent.getValue());
|
||||
}
|
||||
@Override
|
||||
protected ValStr<?> parameterDefault(RemoteParameter parameter) {
|
||||
return ValStr.from(parameter.getDefaultValue());
|
||||
}
|
||||
|
||||
protected Map<String, Object> collectArguments() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
for (RemoteParameter param : paramEditors.keySet()) {
|
||||
Object val = memorized.get(NameTypePair.fromParameter(ctx, param));
|
||||
if (val != null) {
|
||||
map.put(param.name(), val);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@Override
|
||||
protected Collection<?> parameterChoices(RemoteParameter parameter) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
public Map<String, Object> getArguments() {
|
||||
@Override
|
||||
protected Map<String, ValStr<?>> validateArguments(Map<String, RemoteParameter> parameters,
|
||||
Map<String, ValStr<?>> arguments) {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public <T> void setMemorizedArgument(String name, Class<T> type, T value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
memorized.put(new NameTypePair(name, type), value);
|
||||
}
|
||||
|
||||
public <T> T getMemorizedArgument(String name, Class<T> type) {
|
||||
return type.cast(memorized.get(new NameTypePair(name, type)));
|
||||
@Override
|
||||
protected void parameterSaveValue(RemoteParameter parameter, SaveState state, String key,
|
||||
ValStr<?> value) {
|
||||
ConfigStateField.putState(state, parameterType(parameter).asSubclass(Object.class), key,
|
||||
value.val());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
PropertyEditor editor = (PropertyEditor) evt.getSource();
|
||||
RemoteParameter param = paramEditors.getKey(editor);
|
||||
memorized.put(NameTypePair.fromParameter(ctx, param), editor.getValue());
|
||||
protected ValStr<?> parameterLoadValue(RemoteParameter parameter, SaveState state, String key) {
|
||||
return ValStr.from(
|
||||
ConfigStateField.getState(state, parameterType(parameter), key));
|
||||
}
|
||||
|
||||
public void writeConfigState(SaveState saveState) {
|
||||
SaveState subState = new SaveState();
|
||||
for (Map.Entry<NameTypePair, Object> ent : memorized.entrySet()) {
|
||||
NameTypePair ntp = ent.getKey();
|
||||
ConfigStateField.putState(subState, ntp.type().asSubclass(Object.class), ntp.name(),
|
||||
ent.getValue());
|
||||
}
|
||||
saveState.putXmlElement(KEY_MEMORIZED_ARGUMENTS, subState.saveToXml());
|
||||
}
|
||||
|
||||
public void readConfigState(SaveState saveState) {
|
||||
Element element = saveState.getXmlElement(KEY_MEMORIZED_ARGUMENTS);
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
SaveState subState = new SaveState(element);
|
||||
for (String name : subState.getNames()) {
|
||||
try {
|
||||
NameTypePair ntp = NameTypePair.fromString(name);
|
||||
memorized.put(ntp, ConfigStateField.getState(subState, ntp.type(), ntp.name()));
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Error restoring memorized parameter " + name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDescription(String htmlDescription) {
|
||||
if (htmlDescription == null) {
|
||||
descriptionLabel.setBorder(BorderFactory.createEmptyBorder());
|
||||
descriptionLabel.setText("");
|
||||
}
|
||||
else {
|
||||
descriptionLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
|
||||
descriptionLabel.setText(htmlDescription);
|
||||
}
|
||||
@Override
|
||||
protected void setEditorValue(PropertyEditor editor, RemoteParameter param, ValStr<?> val) {
|
||||
ValStr<?> v = switch (val.val()) {
|
||||
case Missing __ -> new ValStr<>(null, "");
|
||||
case TraceObject obj -> new ValStr<>(obj, obj.getCanonicalPath().toString());
|
||||
default -> val;
|
||||
};
|
||||
super.setEditorValue(editor, param, v);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
/* ###
|
||||
* 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.connection;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLaunchDialog;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.LaunchParameter;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
|
||||
public class TraceRmiConnectDialog extends TraceRmiLaunchDialog {
|
||||
|
||||
static final LaunchParameter<String> PARAM_ADDRESS =
|
||||
LaunchParameter.create(String.class, "address",
|
||||
"Host/Address", "Address or hostname for interface(s) to listen on",
|
||||
true, ValStr.str("localhost"), str -> str);
|
||||
static final LaunchParameter<Integer> PARAM_PORT =
|
||||
LaunchParameter.create(Integer.class, "port",
|
||||
"Port", "TCP port number, 0 for ephemeral",
|
||||
true, ValStr.from(0), Integer::decode);
|
||||
private static final Map<String, LaunchParameter<?>> PARAMETERS =
|
||||
LaunchParameter.mapOf(PARAM_ADDRESS, PARAM_PORT);
|
||||
|
||||
public TraceRmiConnectDialog(PluginTool tool, String title, String buttonText) {
|
||||
super(tool, title, buttonText, DebuggerResources.ICON_CONNECTION);
|
||||
}
|
||||
|
||||
public Map<String, ValStr<?>> promptArguments() {
|
||||
return promptArguments(PARAMETERS, Map.of(), Map.of());
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -34,11 +34,9 @@ import docking.action.builder.ActionBuilder;
|
||||
import docking.widgets.tree.*;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.*;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.control.ControlMode;
|
||||
import ghidra.debug.api.target.Target;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
@ -62,16 +60,6 @@ public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter
|
||||
private static final String GROUP_CONNECT = "1. Connect";
|
||||
private static final String GROUP_MAINTENANCE = "3. Maintenance";
|
||||
|
||||
private static final ParameterDescription<String> PARAM_ADDRESS =
|
||||
ParameterDescription.create(String.class, "address", true, "localhost",
|
||||
"Host/Address", "Address or hostname for interface(s) to listen on");
|
||||
private static final ParameterDescription<Integer> PARAM_PORT =
|
||||
ParameterDescription.create(Integer.class, "port", true, 0,
|
||||
"Port", "TCP port number, 0 for ephemeral");
|
||||
private static final TargetParameterMap PARAMETERS = TargetParameterMap.ofEntries(
|
||||
Map.entry(PARAM_ADDRESS.name, PARAM_ADDRESS),
|
||||
Map.entry(PARAM_PORT.name, PARAM_PORT));
|
||||
|
||||
interface StartServerAction {
|
||||
String NAME = "Start Server";
|
||||
String DESCRIPTION = "Start a TCP server for incoming connections (indefinitely)";
|
||||
@ -344,25 +332,24 @@ public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter
|
||||
return traceRmiService != null && !traceRmiService.isServerStarted();
|
||||
}
|
||||
|
||||
private InetSocketAddress promptSocketAddress(String title, String okText) {
|
||||
DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool,
|
||||
title, okText, DebuggerResources.ICON_CONNECTION);
|
||||
Map<String, ?> arguments;
|
||||
do {
|
||||
dialog.forgetMemorizedArguments();
|
||||
arguments = dialog.promptArguments(PARAMETERS);
|
||||
}
|
||||
while (dialog.isResetRequested());
|
||||
private InetSocketAddress promptSocketAddress(String title, String okText,
|
||||
HelpLocation helpLocation) {
|
||||
TraceRmiConnectDialog dialog = new TraceRmiConnectDialog(tool, title, okText);
|
||||
dialog.setHelpLocation(helpLocation);
|
||||
Map<String, ValStr<?>> arguments = dialog.promptArguments();
|
||||
|
||||
if (arguments == null) {
|
||||
// Cancelled
|
||||
return null;
|
||||
}
|
||||
String address = PARAM_ADDRESS.get(arguments);
|
||||
int port = PARAM_PORT.get(arguments);
|
||||
String address = TraceRmiConnectDialog.PARAM_ADDRESS.get(arguments).val();
|
||||
int port = TraceRmiConnectDialog.PARAM_PORT.get(arguments).val();
|
||||
return new InetSocketAddress(address, port);
|
||||
}
|
||||
|
||||
private void doActionStartServerActivated(ActionContext __) {
|
||||
InetSocketAddress sockaddr = promptSocketAddress("Start Trace RMI Server", "Start");
|
||||
InetSocketAddress sockaddr = promptSocketAddress("Start Trace RMI Server", "Start",
|
||||
actionStartServer.getHelpLocation());
|
||||
if (sockaddr == null) {
|
||||
return;
|
||||
}
|
||||
@ -395,7 +382,8 @@ public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter
|
||||
}
|
||||
|
||||
private void doActionConnectAcceptActivated(ActionContext __) {
|
||||
InetSocketAddress sockaddr = promptSocketAddress("Accept Trace RMI Connection", "Listen");
|
||||
InetSocketAddress sockaddr = promptSocketAddress("Accept Trace RMI Connection", "Listen",
|
||||
actionConnectAccept.getHelpLocation());
|
||||
if (sockaddr == null) {
|
||||
return;
|
||||
}
|
||||
@ -420,7 +408,8 @@ public class TraceRmiConnectionManagerProvider extends ComponentProviderAdapter
|
||||
}
|
||||
|
||||
private void doActionConnectOutboundActivated(ActionContext __) {
|
||||
InetSocketAddress sockaddr = promptSocketAddress("Connect to Trace RMI", "Connect");
|
||||
InetSocketAddress sockaddr = promptSocketAddress("Connect to Trace RMI", "Connect",
|
||||
actionConnectOutbound.getHelpLocation());
|
||||
if (sockaddr == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -23,7 +23,8 @@ import javax.swing.Icon;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.TtyCondition;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.LaunchParameter;
|
||||
import ghidra.debug.api.tracermi.TerminalSession;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.HelpLocation;
|
||||
@ -84,7 +85,7 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ParameterDescription<?>> getParameters() {
|
||||
public Map<String, LaunchParameter<?>> getParameters() {
|
||||
return attrs.parameters();
|
||||
}
|
||||
|
||||
@ -93,12 +94,15 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi
|
||||
return attrs.timeoutMillis();
|
||||
}
|
||||
|
||||
protected abstract void prepareSubprocess(List<String> commandLine, Map<String, String> env,
|
||||
Map<String, ?> args, SocketAddress address);
|
||||
protected void prepareSubprocess(List<String> commandLine, Map<String, String> env,
|
||||
Map<String, ValStr<?>> args, SocketAddress address) {
|
||||
ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args,
|
||||
address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void launchBackEnd(TaskMonitor monitor, Map<String, TerminalSession> sessions,
|
||||
Map<String, ?> args, SocketAddress address) throws Exception {
|
||||
Map<String, ValStr<?>> args, SocketAddress address) throws Exception {
|
||||
List<String> commandLine = new ArrayList<>();
|
||||
Map<String, String> env = new HashMap<>(System.getenv());
|
||||
prepareSubprocess(commandLine, env, args, address);
|
||||
@ -112,7 +116,7 @@ public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmi
|
||||
}
|
||||
NullPtyTerminalSession ns = nullPtyTerminal();
|
||||
env.put(ent.getKey(), ns.name());
|
||||
sessions.put(ns.name(), ns);
|
||||
sessions.put(ent.getKey(), ns);
|
||||
}
|
||||
|
||||
sessions.put("Shell",
|
||||
|
@ -28,7 +28,7 @@ import java.util.concurrent.*;
|
||||
import javax.swing.Icon;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
|
||||
import ghidra.app.plugin.core.debug.gui.action.ByModuleAutoMapSpec;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.LaunchFailureDialog.ErrPromptResponse;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.DefaultTraceRmiAcceptor;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler;
|
||||
@ -36,8 +36,8 @@ import ghidra.app.plugin.core.terminal.TerminalListener;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.app.services.DebuggerTraceManagerService.ActivationCause;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.util.ShellUtils;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.action.AutoMapSpec;
|
||||
import ghidra.debug.api.modules.DebuggerMissingProgramActionContext;
|
||||
import ghidra.debug.api.modules.DebuggerStaticMappingChangeListener;
|
||||
@ -212,14 +212,13 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
return mappingService.getOpenMappedLocation(trace, probe, snap) != null;
|
||||
}
|
||||
|
||||
protected SaveState saveLauncherArgsToState(Map<String, ?> args,
|
||||
Map<String, ParameterDescription<?>> params) {
|
||||
protected SaveState saveLauncherArgsToState(Map<String, ValStr<?>> args,
|
||||
Map<String, LaunchParameter<?>> params) {
|
||||
SaveState state = new SaveState();
|
||||
for (ParameterDescription<?> param : params.values()) {
|
||||
Object val = args.get(param.name);
|
||||
for (LaunchParameter<?> param : params.values()) {
|
||||
ValStr<?> val = args.get(param.name());
|
||||
if (val != null) {
|
||||
ConfigStateField.putState(state, param.type.asSubclass(Object.class),
|
||||
"param_" + param.name, val);
|
||||
state.putString("param_" + param.name(), val.str());
|
||||
}
|
||||
}
|
||||
return state;
|
||||
@ -233,56 +232,56 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
plugin.writeProgramLaunchConfig(program, getConfigName(), state);
|
||||
}
|
||||
|
||||
protected void saveLauncherArgs(Map<String, ?> args,
|
||||
Map<String, ParameterDescription<?>> params) {
|
||||
protected void saveLauncherArgs(Map<String, ValStr<?>> args,
|
||||
Map<String, LaunchParameter<?>> params) {
|
||||
saveState(saveLauncherArgsToState(args, params));
|
||||
}
|
||||
|
||||
interface ImageParamSetter {
|
||||
@SuppressWarnings("unchecked")
|
||||
static ImageParamSetter get(ParameterDescription<?> param) {
|
||||
if (param.type == String.class) {
|
||||
return new StringImageParamSetter((ParameterDescription<String>) param);
|
||||
static ImageParamSetter get(LaunchParameter<?> param) {
|
||||
if (param.type() == String.class) {
|
||||
return new StringImageParamSetter((LaunchParameter<String>) param);
|
||||
}
|
||||
if (param.type == PathIsFile.class) {
|
||||
return new FileImageParamSetter((ParameterDescription<PathIsFile>) param);
|
||||
if (param.type() == PathIsFile.class) {
|
||||
return new FileImageParamSetter((LaunchParameter<PathIsFile>) param);
|
||||
}
|
||||
Msg.warn(ImageParamSetter.class,
|
||||
"'Image' parameter has unsupported type: " + param.type);
|
||||
"'Image' parameter has unsupported type: " + param.type());
|
||||
return null;
|
||||
}
|
||||
|
||||
void setImage(Map<String, Object> map, Program program);
|
||||
void setImage(Map<String, ValStr<?>> map, Program program);
|
||||
}
|
||||
|
||||
static class StringImageParamSetter implements ImageParamSetter {
|
||||
private final ParameterDescription<String> param;
|
||||
private final LaunchParameter<String> param;
|
||||
|
||||
public StringImageParamSetter(ParameterDescription<String> param) {
|
||||
public StringImageParamSetter(LaunchParameter<String> param) {
|
||||
this.param = param;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(Map<String, Object> map, Program program) {
|
||||
public void setImage(Map<String, ValStr<?>> map, Program program) {
|
||||
// str-type Image is a hint that the launcher is remote
|
||||
String value = TraceRmiLauncherServicePlugin.getProgramPath(program, false);
|
||||
param.set(map, value);
|
||||
param.set(map, ValStr.str(value));
|
||||
}
|
||||
}
|
||||
|
||||
static class FileImageParamSetter implements ImageParamSetter {
|
||||
private final ParameterDescription<PathIsFile> param;
|
||||
private final LaunchParameter<PathIsFile> param;
|
||||
|
||||
public FileImageParamSetter(ParameterDescription<PathIsFile> param) {
|
||||
public FileImageParamSetter(LaunchParameter<PathIsFile> param) {
|
||||
this.param = param;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(Map<String, Object> map, Program program) {
|
||||
public void setImage(Map<String, ValStr<?>> map, Program program) {
|
||||
// file-type Image is a hint that the launcher is local
|
||||
String str = TraceRmiLauncherServicePlugin.getProgramPath(program, true);
|
||||
PathIsFile value = str == null ? null : new PathIsFile(Paths.get(str));
|
||||
param.set(map, value);
|
||||
param.set(map, new ValStr<>(value, str));
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,33 +296,34 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
* @return the default arguments
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Map<String, ?> generateDefaultLauncherArgs(
|
||||
Map<String, ParameterDescription<?>> params) {
|
||||
Map<String, Object> map = new LinkedHashMap<String, Object>();
|
||||
protected Map<String, ValStr<?>> generateDefaultLauncherArgs(
|
||||
Map<String, LaunchParameter<?>> params) {
|
||||
Map<String, ValStr<?>> map = new LinkedHashMap<>();
|
||||
ImageParamSetter imageSetter = null;
|
||||
for (Entry<String, ParameterDescription<?>> entry : params.entrySet()) {
|
||||
ParameterDescription<?> param = entry.getValue();
|
||||
map.put(entry.getKey(), param.defaultValue);
|
||||
if (PARAM_DISPLAY_IMAGE.equals(param.display)) {
|
||||
for (Entry<String, LaunchParameter<?>> entry : params.entrySet()) {
|
||||
LaunchParameter<?> param = entry.getValue();
|
||||
map.put(entry.getKey(), ValStr.cast(Object.class, param.defaultValue()));
|
||||
if (PARAM_DISPLAY_IMAGE.equals(param.display())) {
|
||||
imageSetter = ImageParamSetter.get(param);
|
||||
// May still be null if type is not supported
|
||||
}
|
||||
else if (param.name.startsWith(PREFIX_PARAM_EXTTOOL)) {
|
||||
String tool = param.name.substring(PREFIX_PARAM_EXTTOOL.length());
|
||||
else if (param.name().startsWith(PREFIX_PARAM_EXTTOOL)) {
|
||||
String tool = param.name().substring(PREFIX_PARAM_EXTTOOL.length());
|
||||
List<String> names =
|
||||
program.getLanguage().getLanguageDescription().getExternalNames(tool);
|
||||
if (names != null && !names.isEmpty()) {
|
||||
if (param.type == String.class) {
|
||||
var paramStr = (ParameterDescription<String>) param;
|
||||
paramStr.set(map, names.get(0));
|
||||
String toolName = names.get(0);
|
||||
if (param.type() == String.class) {
|
||||
var paramStr = (LaunchParameter<String>) param;
|
||||
paramStr.set(map, ValStr.str(toolName));
|
||||
}
|
||||
else if (param.type == PathIsFile.class) {
|
||||
var paramPIF = (ParameterDescription<PathIsFile>) param;
|
||||
paramPIF.set(map, PathIsFile.fromString(names.get(0)));
|
||||
else if (param.type() == PathIsFile.class) {
|
||||
var paramPIF = (LaunchParameter<PathIsFile>) param;
|
||||
paramPIF.set(map, new ValStr<>(PathIsFile.fromString(toolName), toolName));
|
||||
}
|
||||
else if (param.type == PathIsDir.class) {
|
||||
var paramPID = (ParameterDescription<PathIsDir>) param;
|
||||
paramPID.set(map, PathIsDir.fromString(names.get(0)));
|
||||
else if (param.type() == PathIsDir.class) {
|
||||
var paramPID = (LaunchParameter<PathIsDir>) param;
|
||||
paramPID.set(map, new ValStr<>(PathIsDir.fromString(toolName), toolName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -337,50 +337,33 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
/**
|
||||
* Prompt the user for arguments, showing those last used or defaults
|
||||
*
|
||||
* @param lastExc
|
||||
*
|
||||
* @param params the parameters of the model's launcher
|
||||
* @param configurator a thing to generate/modify the (default) arguments
|
||||
* @param lastExc if re-prompting, an error to display
|
||||
* @return the arguments given by the user, or null if cancelled
|
||||
*/
|
||||
protected Map<String, ?> promptLauncherArgs(LaunchConfigurator configurator,
|
||||
protected Map<String, ValStr<?>> promptLauncherArgs(LaunchConfigurator configurator,
|
||||
Throwable lastExc) {
|
||||
Map<String, ParameterDescription<?>> params = getParameters();
|
||||
DebuggerMethodInvocationDialog dialog =
|
||||
new DebuggerMethodInvocationDialog(tool, getTitle(), "Launch", getIcon());
|
||||
Map<String, LaunchParameter<?>> params = getParameters();
|
||||
TraceRmiLaunchDialog dialog =
|
||||
new TraceRmiLaunchDialog(tool, getTitle(), "Launch", getIcon());
|
||||
dialog.setDescription(getDescription());
|
||||
dialog.setHelpLocation(getHelpLocation());
|
||||
if (lastExc != null) {
|
||||
dialog.setStatusText(lastExc.toString(), MessageType.ERROR);
|
||||
}
|
||||
else {
|
||||
dialog.setStatusText("");
|
||||
}
|
||||
|
||||
// NB. Do not invoke read/writeConfigState
|
||||
Map<String, ?> args;
|
||||
boolean reset = false;
|
||||
do {
|
||||
args =
|
||||
configurator.configureLauncher(this, loadLastLauncherArgs(true), RelPrompt.BEFORE);
|
||||
for (ParameterDescription<?> param : params.values()) {
|
||||
Object val = args.get(param.name);
|
||||
if (val != null) {
|
||||
dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class),
|
||||
val);
|
||||
}
|
||||
}
|
||||
if (lastExc != null) {
|
||||
dialog.setStatusText(lastExc.toString(), MessageType.ERROR);
|
||||
}
|
||||
else {
|
||||
dialog.setStatusText("");
|
||||
}
|
||||
args = dialog.promptArguments(params);
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
return null;
|
||||
}
|
||||
reset = dialog.isResetRequested();
|
||||
if (reset) {
|
||||
args = generateDefaultLauncherArgs(params);
|
||||
}
|
||||
|
||||
Map<String, ValStr<?>> defaultArgs = generateDefaultLauncherArgs(params);
|
||||
Map<String, ValStr<?>> lastArgs =
|
||||
configurator.configureLauncher(this, loadLastLauncherArgs(true), RelPrompt.BEFORE);
|
||||
Map<String, ValStr<?>> args = dialog.promptArguments(params, lastArgs, defaultArgs);
|
||||
if (args != null) {
|
||||
saveLauncherArgs(args, params);
|
||||
}
|
||||
while (reset);
|
||||
return args;
|
||||
}
|
||||
|
||||
@ -398,31 +381,40 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
* @param forPrompt true if the user will be confirming the arguments
|
||||
* @return the loaded arguments, or defaults
|
||||
*/
|
||||
protected Map<String, ?> loadLastLauncherArgs(boolean forPrompt) {
|
||||
Map<String, ParameterDescription<?>> params = getParameters();
|
||||
Map<String, ?> args = loadLauncherArgsFromState(loadState(forPrompt), params);
|
||||
protected Map<String, ValStr<?>> loadLastLauncherArgs(boolean forPrompt) {
|
||||
Map<String, LaunchParameter<?>> params = getParameters();
|
||||
Map<String, ValStr<?>> args = loadLauncherArgsFromState(loadState(forPrompt), params);
|
||||
saveLauncherArgs(args, params);
|
||||
return args;
|
||||
}
|
||||
|
||||
protected Map<String, ?> loadLauncherArgsFromState(SaveState state,
|
||||
Map<String, ParameterDescription<?>> params) {
|
||||
Map<String, ?> defaultArgs = generateDefaultLauncherArgs(params);
|
||||
protected Map<String, ValStr<?>> loadLauncherArgsFromState(SaveState state,
|
||||
Map<String, LaunchParameter<?>> params) {
|
||||
Map<String, ValStr<?>> defaultArgs = generateDefaultLauncherArgs(params);
|
||||
if (state == null) {
|
||||
return defaultArgs;
|
||||
}
|
||||
List<String> names = List.of(state.getNames());
|
||||
Map<String, Object> args = new LinkedHashMap<>();
|
||||
for (ParameterDescription<?> param : params.values()) {
|
||||
String key = "param_" + param.name;
|
||||
Object configState =
|
||||
names.contains(key) ? ConfigStateField.getState(state, param.type, key) : null;
|
||||
if (configState != null) {
|
||||
args.put(param.name, configState);
|
||||
Map<String, ValStr<?>> args = new LinkedHashMap<>();
|
||||
Set<String> names = Set.of(state.getNames());
|
||||
for (LaunchParameter<?> param : params.values()) {
|
||||
String key = "param_" + param.name();
|
||||
if (!names.contains(key)) {
|
||||
args.put(param.name(), defaultArgs.get(param.name()));
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
args.put(param.name, defaultArgs.get(param.name));
|
||||
String str = state.getString(key, null);
|
||||
if (str != null) {
|
||||
args.put(param.name(), param.decode(str));
|
||||
continue;
|
||||
}
|
||||
// Perhaps wrong type; was saved in older version.
|
||||
Object fallback = ConfigStateField.getState(state, param.type(), param.name());
|
||||
if (fallback != null) {
|
||||
args.put(param.name(), ValStr.from(fallback));
|
||||
continue;
|
||||
}
|
||||
Msg.warn(this, "Could not load saved launcher arg '%s' (%s)".formatted(param.name(),
|
||||
param.display()));
|
||||
}
|
||||
return args;
|
||||
}
|
||||
@ -435,7 +427,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the launcher args
|
||||
* Obtain the launcher arguments
|
||||
*
|
||||
* <p>
|
||||
* This should either call {@link #promptLauncherArgs(LaunchConfigurator, Throwable)} or
|
||||
@ -447,7 +439,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
* @param lastExc if retrying, the last exception to display as an error message
|
||||
* @return the chosen arguments, or null if the user cancels at the prompt
|
||||
*/
|
||||
public Map<String, ?> getLauncherArgs(boolean prompt, LaunchConfigurator configurator,
|
||||
public Map<String, ValStr<?>> getLauncherArgs(boolean prompt, LaunchConfigurator configurator,
|
||||
Throwable lastExc) {
|
||||
return prompt
|
||||
? configurator.configureLauncher(this, promptLauncherArgs(configurator, lastExc),
|
||||
@ -543,8 +535,8 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
}
|
||||
|
||||
protected abstract void launchBackEnd(TaskMonitor monitor,
|
||||
Map<String, TerminalSession> sessions, Map<String, ?> args, SocketAddress address)
|
||||
throws Exception;
|
||||
Map<String, TerminalSession> sessions, Map<String, ValStr<?>> args,
|
||||
SocketAddress address) throws Exception;
|
||||
|
||||
static class NoStaticMappingException extends Exception {
|
||||
public NoStaticMappingException(String message) {
|
||||
@ -557,9 +549,18 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
}
|
||||
}
|
||||
|
||||
protected void initializeMonitor(TaskMonitor monitor) {
|
||||
protected AutoMapSpec getAutoMapSpec() {
|
||||
DebuggerAutoMappingService auto = tool.getService(DebuggerAutoMappingService.class);
|
||||
AutoMapSpec spec = auto.getAutoMapSpec();
|
||||
return auto == null ? ByModuleAutoMapSpec.instance() : auto.getAutoMapSpec();
|
||||
}
|
||||
|
||||
protected AutoMapSpec getAutoMapSpec(Trace trace) {
|
||||
DebuggerAutoMappingService auto = tool.getService(DebuggerAutoMappingService.class);
|
||||
return auto == null ? ByModuleAutoMapSpec.instance() : auto.getAutoMapSpec(trace);
|
||||
}
|
||||
|
||||
protected void initializeMonitor(TaskMonitor monitor) {
|
||||
AutoMapSpec spec = getAutoMapSpec();
|
||||
if (requiresImage() && spec.hasTask()) {
|
||||
monitor.setMaximum(6);
|
||||
}
|
||||
@ -574,8 +575,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
if (!requiresImage()) {
|
||||
return;
|
||||
}
|
||||
DebuggerAutoMappingService auto = tool.getService(DebuggerAutoMappingService.class);
|
||||
AutoMapSpec spec = auto.getAutoMapSpec(trace);
|
||||
AutoMapSpec spec = getAutoMapSpec(trace);
|
||||
if (!spec.hasTask()) {
|
||||
return;
|
||||
}
|
||||
@ -625,7 +625,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
while (true) {
|
||||
try {
|
||||
monitor.setMessage("Gathering arguments");
|
||||
Map<String, ?> args = getLauncherArgs(prompt, configurator, lastExc);
|
||||
Map<String, ValStr<?>> args = getLauncherArgs(prompt, configurator, lastExc);
|
||||
if (args == null) {
|
||||
if (lastExc == null) {
|
||||
lastExc = new CancelledException();
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -22,18 +22,31 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
|
||||
import ghidra.debug.api.ValStr;
|
||||
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.
|
||||
* The script must start with an attributes header in a comment block. See
|
||||
* {@link ScriptAttributesParser}.
|
||||
*/
|
||||
public class BatchScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer {
|
||||
public static final String REM = "::";
|
||||
public static final int REM_LEN = REM.length();
|
||||
|
||||
/**
|
||||
* Create a launch offer from the given batch file.
|
||||
*
|
||||
* @param plugin the launcher service plugin
|
||||
* @param program the current program, usually the target image. In general, this should be used
|
||||
* for at least two purposes. 1) To populate the default command line. 2) To ensure
|
||||
* the target image is mapped in the resulting target trace.
|
||||
* @param script the batch file that implements this offer
|
||||
* @return the offer
|
||||
* @throws FileNotFoundException if the batch file does not exist
|
||||
*/
|
||||
public static BatchScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
|
||||
Program program, File script) throws FileNotFoundException {
|
||||
ScriptAttributesParser parser = new ScriptAttributesParser() {
|
||||
@ -60,11 +73,4 @@ public class BatchScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunch
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -27,18 +27,23 @@ 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.ValStr;
|
||||
import ghidra.debug.api.tracermi.LaunchParameter;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.plugintool.AutoConfigState.PathIsDir;
|
||||
import ghidra.framework.plugintool.AutoConfigState.PathIsFile;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.*;
|
||||
|
||||
/**
|
||||
* A parser for reading attributes from a script header
|
||||
*/
|
||||
public abstract class ScriptAttributesParser {
|
||||
public static final String ENV_GHIDRA_HOME = "GHIDRA_HOME";
|
||||
public static final String ENV_GHIDRA_TRACE_RMI_ADDR = "GHIDRA_TRACE_RMI_ADDR";
|
||||
public static final String ENV_GHIDRA_TRACE_RMI_HOST = "GHIDRA_TRACE_RMI_HOST";
|
||||
public static final String ENV_GHIDRA_TRACE_RMI_PORT = "GHIDRA_TRACE_RMI_PORT";
|
||||
|
||||
public static final String AT_TITLE = "@title";
|
||||
public static final String AT_DESC = "@desc";
|
||||
public static final String AT_MENU_PATH = "@menu-path";
|
||||
@ -69,10 +74,29 @@ public abstract class ScriptAttributesParser {
|
||||
public static final String MSGPAT_INVALID_ARGS_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\"";
|
||||
public static final String MSGPAT_INVALID_TTY_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use TTY_TARGET [if env:OPT_EXTRA_TTY]";
|
||||
"%s: Invalid %s syntax. Use TTY_TARGET [if env:OPT [== VAL]]";
|
||||
public static final String MSGPAT_INVALID_TTY_NO_PARAM =
|
||||
"%s: In %s: No such parameter '%s'";
|
||||
public static final String MSGPAT_INVALID_TTY_NOT_BOOL =
|
||||
"%s: In %s: Parameter '%s' must have bool type";
|
||||
public static final String MSGPAT_INVALID_TTY_BAD_VAL =
|
||||
"%s: In %s: Parameter '%s' has type %s, but '%s' cannot be parsed as such";
|
||||
public static final String MSGPAT_INVALID_TIMEOUT_SYNTAX = "" +
|
||||
"%s: Invalid %s syntax. Use [milliseconds]";
|
||||
|
||||
public static class ParseException extends Exception {
|
||||
private Location loc;
|
||||
|
||||
public ParseException(Location loc, String message) {
|
||||
super(message);
|
||||
this.loc = loc;
|
||||
}
|
||||
|
||||
public Location getLocation() {
|
||||
return loc;
|
||||
}
|
||||
}
|
||||
|
||||
protected record Location(String fileName, int lineNo) {
|
||||
@Override
|
||||
public String toString() {
|
||||
@ -80,31 +104,36 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
}
|
||||
|
||||
protected interface OptType<T> {
|
||||
static OptType<?> parse(Location loc, String typeName,
|
||||
Map<String, UserType<?>> userEnums) {
|
||||
protected interface OptType<T> extends ValStr.Decoder<T> {
|
||||
static OptType<?> parse(Location loc, String typeName, Map<String, UserType<?>> userEnums)
|
||||
throws ParseException {
|
||||
OptType<?> type = BaseType.parseNoErr(typeName);
|
||||
if (type == null) {
|
||||
type = userEnums.get(typeName);
|
||||
}
|
||||
if (type == null) { // still
|
||||
Msg.error(ScriptAttributesParser.class,
|
||||
"%s: Invalid type %s".formatted(loc, typeName));
|
||||
return null;
|
||||
throw new ParseException(loc, "%s: Invalid type %s".formatted(loc, typeName));
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
default TypeAndDefault<T> withCastDefault(Object defaultValue) {
|
||||
return new TypeAndDefault<>(this, cls().cast(defaultValue));
|
||||
default TypeAndDefault<T> withCastDefault(ValStr<Object> defaultValue) {
|
||||
return new TypeAndDefault<>(this, ValStr.cast(cls(), defaultValue));
|
||||
}
|
||||
|
||||
Class<T> cls();
|
||||
|
||||
T decode(Location loc, String str);
|
||||
default T decode(Location loc, String str) throws ParseException {
|
||||
try {
|
||||
return decode(str);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new ParseException(loc, "%s: %s".formatted(loc, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
ParameterDescription<T> createParameter(String name, T defaultValue, String display,
|
||||
String description);
|
||||
LaunchParameter<T> createParameter(String name, String display, String description,
|
||||
boolean required, ValStr<T> defaultValue);
|
||||
}
|
||||
|
||||
protected interface BaseType<T> extends OptType<T> {
|
||||
@ -120,12 +149,10 @@ public abstract class ScriptAttributesParser {
|
||||
};
|
||||
}
|
||||
|
||||
public static BaseType<?> parse(Location loc, String typeName) {
|
||||
public static BaseType<?> parse(Location loc, String typeName) throws ParseException {
|
||||
BaseType<?> type = parseNoErr(typeName);
|
||||
if (type == null) {
|
||||
Msg.error(ScriptAttributesParser.class,
|
||||
"%s: Invalid base type %s".formatted(loc, typeName));
|
||||
return null;
|
||||
throw new ParseException(loc, "%s: Invalid base type %s".formatted(loc, typeName));
|
||||
}
|
||||
return type;
|
||||
}
|
||||
@ -137,7 +164,7 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decode(Location loc, String str) {
|
||||
public String decode(String str) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
@ -149,18 +176,14 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger decode(Location loc, String str) {
|
||||
public BigInteger decode(String str) {
|
||||
try {
|
||||
if (str.startsWith("0x")) {
|
||||
return new BigInteger(str.substring(2), 16);
|
||||
}
|
||||
return new BigInteger(str);
|
||||
return NumericUtilities.decodeBigInteger(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;
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid int %s. Prefixes 0x, 0b, and 0 (octal) are allowed."
|
||||
.formatted(str));
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -172,17 +195,16 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean decode(Location loc, String str) {
|
||||
Boolean result = switch (str) {
|
||||
public Boolean decode(String str) {
|
||||
Boolean result = switch (str.trim().toLowerCase()) {
|
||||
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;
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid bool for %s: %s. Only true or false is allowed."
|
||||
.formatted(AT_ENV, str));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -195,7 +217,7 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path decode(Location loc, String str) {
|
||||
public Path decode(String str) {
|
||||
return Paths.get(str);
|
||||
}
|
||||
};
|
||||
@ -207,7 +229,7 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathIsDir decode(Location loc, String str) {
|
||||
public PathIsDir decode(String str) {
|
||||
return new PathIsDir(Paths.get(str));
|
||||
}
|
||||
};
|
||||
@ -219,7 +241,7 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathIsFile decode(Location loc, String str) {
|
||||
public PathIsFile decode(String str) {
|
||||
return new PathIsFile(Paths.get(str));
|
||||
}
|
||||
};
|
||||
@ -228,11 +250,15 @@ public abstract class ScriptAttributesParser {
|
||||
return new UserType<>(this, choices.stream().map(cls()::cast).toList());
|
||||
}
|
||||
|
||||
default UserType<T> withChoices(List<T> choices) {
|
||||
return new UserType<>(this, choices);
|
||||
}
|
||||
|
||||
@Override
|
||||
default ParameterDescription<T> createParameter(String name, T defaultValue, String display,
|
||||
String description) {
|
||||
return ParameterDescription.create(cls(), name, false, defaultValue, display,
|
||||
description);
|
||||
default LaunchParameter<T> createParameter(String name, String display, String description,
|
||||
boolean required, ValStr<T> defaultValue) {
|
||||
return LaunchParameter.create(cls(), name, display, description, required, defaultValue,
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,62 +269,57 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public T decode(Location loc, String str) {
|
||||
return base.decode(loc, str);
|
||||
public T decode(String str) {
|
||||
return base.decode(str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParameterDescription<T> createParameter(String name, T defaultValue, String display,
|
||||
String description) {
|
||||
return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
|
||||
description);
|
||||
public LaunchParameter<T> createParameter(String name, String display, String description,
|
||||
boolean required, ValStr<T> defaultValue) {
|
||||
return LaunchParameter.choices(cls(), name, display, description, choices,
|
||||
defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
protected record TypeAndDefault<T>(OptType<T> type, T defaultValue) {
|
||||
protected record TypeAndDefault<T>(OptType<T> type, ValStr<T> defaultValue) {
|
||||
public static TypeAndDefault<?> parse(Location loc, String typeName, String defaultString,
|
||||
Map<String, UserType<?>> userEnums) {
|
||||
Map<String, UserType<?>> userEnums) throws ParseException {
|
||||
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);
|
||||
return tac.withCastDefault(new ValStr<>(value, defaultString));
|
||||
}
|
||||
|
||||
public ParameterDescription<T> createParameter(String name, String display,
|
||||
String description) {
|
||||
return type.createParameter(name, defaultValue, display, description);
|
||||
public LaunchParameter<T> createParameter(String name, String display, String description) {
|
||||
return type.createParameter(name, display, description, false, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
public interface TtyCondition {
|
||||
boolean isActive(Map<String, ?> args);
|
||||
boolean isActive(Map<String, ValStr<?>> args);
|
||||
}
|
||||
|
||||
enum ConstTtyCondition implements TtyCondition {
|
||||
ALWAYS {
|
||||
@Override
|
||||
public boolean isActive(Map<String, ?> args) {
|
||||
public boolean isActive(Map<String, ValStr<?>> args) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
record EqualsTtyCondition(String key, String repr) implements TtyCondition {
|
||||
record EqualsTtyCondition(LaunchParameter<?> param, Object value) implements TtyCondition {
|
||||
@Override
|
||||
public boolean isActive(Map<String, ?> args) {
|
||||
return Objects.toString(args.get(key)).equals(repr);
|
||||
public boolean isActive(Map<String, ValStr<?>> args) {
|
||||
ValStr<?> valStr = param.get(args);
|
||||
return Objects.equals(valStr == null ? null : valStr.val(), value);
|
||||
}
|
||||
}
|
||||
|
||||
record BoolTtyCondition(String key) implements TtyCondition {
|
||||
record BoolTtyCondition(LaunchParameter<Boolean> param) implements TtyCondition {
|
||||
@Override
|
||||
public boolean isActive(Map<String, ?> args) {
|
||||
return args.get(key) instanceof Boolean b && b.booleanValue();
|
||||
public boolean isActive(Map<String, ValStr<?>> args) {
|
||||
ValStr<Boolean> valStr = param.get(args);
|
||||
return valStr != null && valStr.val();
|
||||
}
|
||||
}
|
||||
|
||||
@ -318,9 +339,8 @@ public abstract class ScriptAttributesParser {
|
||||
|
||||
public record ScriptAttributes(String title, String description, List<String> menuPath,
|
||||
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
|
||||
Map<String, ParameterDescription<?>> parameters, Map<String, TtyCondition> extraTtys,
|
||||
int timeoutMillis, boolean noImage) {
|
||||
}
|
||||
Map<String, LaunchParameter<?>> parameters, Map<String, TtyCondition> extraTtys,
|
||||
int timeoutMillis, boolean noImage) {}
|
||||
|
||||
/**
|
||||
* Convert an arguments map into a command line and environment variables
|
||||
@ -335,34 +355,35 @@ public abstract class ScriptAttributesParser {
|
||||
* @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,
|
||||
File script, Map<String, LaunchParameter<?>> parameters, Map<String, ValStr<?>> args,
|
||||
SocketAddress address) {
|
||||
|
||||
commandLine.add(script.getAbsolutePath());
|
||||
env.put("GHIDRA_HOME", Application.getInstallationDirectory().getAbsolutePath());
|
||||
env.put(ENV_GHIDRA_HOME, Application.getInstallationDirectory().getAbsolutePath());
|
||||
if (address != null) {
|
||||
env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address));
|
||||
env.put(ENV_GHIDRA_TRACE_RMI_ADDR, sockToString(address));
|
||||
if (address instanceof InetSocketAddress tcp) {
|
||||
env.put("GHIDRA_TRACE_RMI_HOST", tcp.getAddress().getHostAddress());
|
||||
env.put("GHIDRA_TRACE_RMI_PORT", Integer.toString(tcp.getPort()));
|
||||
env.put(ENV_GHIDRA_TRACE_RMI_HOST, tcp.getAddress().getHostAddress());
|
||||
env.put(ENV_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)));
|
||||
LaunchParameter<?> param;
|
||||
for (int i = 1; (param = parameters.get("arg:" + i)) != null; i++) {
|
||||
// Don't use ValStr.str here. I'd like the script's input normalized
|
||||
commandLine.add(Objects.toString(param.get(args).val()));
|
||||
}
|
||||
|
||||
paramDesc = parameters.get("args");
|
||||
if (paramDesc != null) {
|
||||
commandLine.addAll(ShellUtils.parseArgs((String) paramDesc.get(args)));
|
||||
param = parameters.get("args");
|
||||
if (param != null) {
|
||||
commandLine.addAll(ShellUtils.parseArgs(param.get(args).str()));
|
||||
}
|
||||
|
||||
for (Entry<String, ParameterDescription<?>> ent : parameters.entrySet()) {
|
||||
for (Entry<String, LaunchParameter<?>> 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)));
|
||||
env.put(varName, Objects.toString(ent.getValue().get(args).val()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -376,7 +397,7 @@ public abstract class ScriptAttributesParser {
|
||||
private String iconId;
|
||||
private HelpLocation helpLocation;
|
||||
private final Map<String, UserType<?>> userTypes = new HashMap<>();
|
||||
private final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
|
||||
private final Map<String, LaunchParameter<?>> parameters = new LinkedHashMap<>();
|
||||
private final Map<String, TtyCondition> extraTtys = new LinkedHashMap<>();
|
||||
private int timeoutMillis = AbstractTraceRmiLaunchOffer.DEFAULT_TIMEOUT_MILLIS;
|
||||
private boolean noImage = false;
|
||||
@ -401,9 +422,17 @@ public abstract class ScriptAttributesParser {
|
||||
*/
|
||||
protected abstract String removeDelimiter(String line);
|
||||
|
||||
public ScriptAttributes parseFile(File script) throws FileNotFoundException {
|
||||
/**
|
||||
* Parse the header from the give input stream
|
||||
*
|
||||
* @param stream the stream from of the input stream file
|
||||
* @param scriptName the name of the script file
|
||||
* @return the parsed attributes
|
||||
* @throws IOException if there was an issue reading the stream
|
||||
*/
|
||||
public ScriptAttributes parseStream(InputStream stream, String scriptName) throws IOException {
|
||||
try (BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
|
||||
new BufferedReader(new InputStreamReader(stream))) {
|
||||
String line;
|
||||
for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) {
|
||||
if (ignoreLine(lineNo, line)) {
|
||||
@ -413,9 +442,22 @@ public abstract class ScriptAttributesParser {
|
||||
if (comment == null) {
|
||||
break;
|
||||
}
|
||||
parseComment(new Location(script.getName(), lineNo), comment);
|
||||
parseComment(new Location(scriptName, lineNo), comment);
|
||||
}
|
||||
return validate(script.getName());
|
||||
return validate(scriptName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the header of the given script file
|
||||
*
|
||||
* @param script the file
|
||||
* @return the parsed attributes
|
||||
* @throws FileNotFoundException if the script file could not be found
|
||||
*/
|
||||
public ScriptAttributes parseFile(File script) throws FileNotFoundException {
|
||||
try {
|
||||
return parseStream(new FileInputStream(script), script.getName());
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
// Avoid capture by IOException
|
||||
@ -468,7 +510,7 @@ public abstract class ScriptAttributesParser {
|
||||
|
||||
protected void parseTitle(Location loc, String str) {
|
||||
if (title != null) {
|
||||
Msg.warn(this, "%s: Duplicate @title".formatted(loc));
|
||||
reportWarning("%s: Duplicate %s".formatted(loc, AT_TITLE));
|
||||
}
|
||||
title = str;
|
||||
}
|
||||
@ -483,161 +525,222 @@ public abstract class ScriptAttributesParser {
|
||||
|
||||
protected void parseMenuPath(Location loc, String str) {
|
||||
if (menuPath != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_PATH));
|
||||
reportWarning("%s: Duplicate %s".formatted(loc, AT_MENU_PATH));
|
||||
}
|
||||
menuPath = List.of(str.trim().split("\\."));
|
||||
if (menuPath.isEmpty()) {
|
||||
Msg.error(this,
|
||||
reportError(
|
||||
"%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));
|
||||
reportWarning("%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));
|
||||
reportWarning("%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));
|
||||
reportWarning("%s: Duplicate %s".formatted(loc, AT_ICON));
|
||||
}
|
||||
iconId = str.trim();
|
||||
if (!Gui.hasIcon(iconId)) {
|
||||
Msg.error(this,
|
||||
reportError(
|
||||
"%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));
|
||||
reportWarning("%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));
|
||||
reportError(MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP));
|
||||
return;
|
||||
}
|
||||
helpLocation = new HelpLocation(parts[0].trim(), parts[1].trim());
|
||||
}
|
||||
|
||||
protected <T> UserType<T> parseEnumChoices(Location loc, BaseType<T> baseType,
|
||||
List<String> choiceParts) {
|
||||
List<T> choices = new ArrayList<>();
|
||||
boolean err = false;
|
||||
for (String s : choiceParts) {
|
||||
try {
|
||||
choices.add(baseType.decode(loc, s));
|
||||
}
|
||||
catch (ParseException e) {
|
||||
reportError(e.getMessage());
|
||||
}
|
||||
}
|
||||
if (err) {
|
||||
return null;
|
||||
}
|
||||
return baseType.withChoices(choices);
|
||||
}
|
||||
|
||||
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));
|
||||
reportError(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));
|
||||
reportError(MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM));
|
||||
return;
|
||||
}
|
||||
String name = nameParts[0].trim();
|
||||
BaseType<?> baseType = BaseType.parse(loc, nameParts[1]);
|
||||
if (baseType == null) {
|
||||
BaseType<?> baseType;
|
||||
try {
|
||||
baseType = BaseType.parse(loc, nameParts[1]);
|
||||
}
|
||||
catch (ParseException e) {
|
||||
reportError(e.getMessage());
|
||||
return;
|
||||
}
|
||||
List<?> choices = parts.stream().skip(1).map(s -> baseType.decode(loc, s)).toList();
|
||||
if (choices.contains(null)) {
|
||||
return;
|
||||
UserType<?> userType = parseEnumChoices(loc, baseType, parts.subList(1, parts.size()));
|
||||
if (userType == null) {
|
||||
return; // errors already reported
|
||||
}
|
||||
UserType<?> userType = baseType.withCastChoices(choices);
|
||||
if (userTypes.put(name, userType) != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name));
|
||||
reportWarning("%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));
|
||||
reportError(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));
|
||||
reportError(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));
|
||||
reportError(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));
|
||||
try {
|
||||
TypeAndDefault<?> tad =
|
||||
TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes);
|
||||
LaunchParameter<?> param = tad.createParameter(name, parts.get(1), parts.get(2));
|
||||
if (parameters.put(name, param) != null) {
|
||||
reportWarning("%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed));
|
||||
}
|
||||
}
|
||||
catch (ParseException e) {
|
||||
reportError(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
reportError(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));
|
||||
reportError(MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG));
|
||||
return;
|
||||
}
|
||||
OptType<?> type = OptType.parse(loc, colonType.substring(1), userTypes);
|
||||
if (type == null) {
|
||||
return;
|
||||
OptType<?> type;
|
||||
try {
|
||||
type = OptType.parse(loc, colonType.substring(1), userTypes);
|
||||
String name = PREFIX_ARG + argNum;
|
||||
parameters.put(name,
|
||||
type.createParameter(name, parts.get(1), parts.get(2), true,
|
||||
new ValStr<>(null, "")));
|
||||
}
|
||||
catch (ParseException e) {
|
||||
reportError(e.getMessage());
|
||||
}
|
||||
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));
|
||||
reportError(MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
|
||||
return;
|
||||
}
|
||||
ParameterDescription<String> parameter = ParameterDescription.create(String.class,
|
||||
"args", false, "", parts.get(0), parts.get(1));
|
||||
|
||||
LaunchParameter<String> parameter = BaseType.STRING.createParameter(
|
||||
"args", parts.get(0), parts.get(1), false, ValStr.str(""));
|
||||
if (parameters.put(KEY_ARGS, parameter) != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS));
|
||||
reportWarning("%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS));
|
||||
}
|
||||
}
|
||||
|
||||
protected void putTty(Location loc, String name, TtyCondition condition) {
|
||||
if (extraTtys.put(name, condition) != null) {
|
||||
Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY));
|
||||
reportWarning("%s: Duplicate %s. Ignored".formatted(loc, AT_TTY));
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseTty(Location loc, String str) {
|
||||
List<String> parts = ShellUtils.parseArgs(str);
|
||||
switch (parts.size()) {
|
||||
case 1:
|
||||
case 1 -> {
|
||||
putTty(loc, parts.get(0), ConstTtyCondition.ALWAYS);
|
||||
return;
|
||||
case 3:
|
||||
}
|
||||
case 3 -> {
|
||||
if ("if".equals(parts.get(1))) {
|
||||
putTty(loc, parts.get(0), new BoolTtyCondition(parts.get(2)));
|
||||
LaunchParameter<?> param = parameters.get(parts.get(2));
|
||||
if (param == null) {
|
||||
reportError(
|
||||
MSGPAT_INVALID_TTY_NO_PARAM.formatted(loc, AT_TTY, parts.get(2)));
|
||||
return;
|
||||
}
|
||||
if (param.type() != Boolean.class) {
|
||||
reportError(
|
||||
MSGPAT_INVALID_TTY_NOT_BOOL.formatted(loc, AT_TTY, param.name()));
|
||||
return;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
LaunchParameter<Boolean> asBoolParam = (LaunchParameter<Boolean>) param;
|
||||
putTty(loc, parts.get(0), new BoolTtyCondition(asBoolParam));
|
||||
return;
|
||||
}
|
||||
case 5:
|
||||
}
|
||||
case 5 -> {
|
||||
if ("if".equals(parts.get(1)) && "==".equals(parts.get(3))) {
|
||||
putTty(loc, parts.get(0), new EqualsTtyCondition(parts.get(2), parts.get(4)));
|
||||
return;
|
||||
LaunchParameter<?> param = parameters.get(parts.get(2));
|
||||
if (param == null) {
|
||||
reportError(
|
||||
MSGPAT_INVALID_TTY_NO_PARAM.formatted(loc, AT_TTY, parts.get(2)));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object value = param.decode(parts.get(4)).val();
|
||||
putTty(loc, parts.get(0), new EqualsTtyCondition(param, value));
|
||||
return;
|
||||
}
|
||||
catch (Exception e) {
|
||||
reportError(MSGPAT_INVALID_TTY_BAD_VAL.formatted(loc, AT_TTY,
|
||||
param.name(), param.type(), parts.get(4)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg.error(this, MSGPAT_INVALID_TTY_SYNTAX.formatted(loc, AT_TTY));
|
||||
reportError(MSGPAT_INVALID_TTY_SYNTAX.formatted(loc, AT_TTY));
|
||||
}
|
||||
|
||||
protected void parseTimeout(Location loc, String str) {
|
||||
@ -645,7 +748,7 @@ public abstract class ScriptAttributesParser {
|
||||
timeoutMillis = Integer.parseInt(str);
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
Msg.error(this, MSGPAT_INVALID_TIMEOUT_SYNTAX.formatted(loc, AT_TIMEOUT));
|
||||
reportError(MSGPAT_INVALID_TIMEOUT_SYNTAX.formatted(loc, AT_TIMEOUT));
|
||||
}
|
||||
}
|
||||
|
||||
@ -654,12 +757,13 @@ public abstract class ScriptAttributesParser {
|
||||
}
|
||||
|
||||
protected void parseUnrecognized(Location loc, String line) {
|
||||
Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line));
|
||||
reportWarning("%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));
|
||||
reportError(
|
||||
"%s is required. Using script file name: '%s'".formatted(AT_TITLE, fileName));
|
||||
title = fileName;
|
||||
}
|
||||
if (menuPath == null) {
|
||||
@ -683,4 +787,12 @@ public abstract class ScriptAttributesParser {
|
||||
private String getDescription() {
|
||||
return description == null ? null : description.toString();
|
||||
}
|
||||
|
||||
protected void reportWarning(String message) {
|
||||
Msg.warn(this, message);
|
||||
}
|
||||
|
||||
protected void reportError(String message) {
|
||||
Msg.error(this, message);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,86 @@
|
||||
/* ###
|
||||
* 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.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractDebuggerParameterDialog;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.LaunchParameter;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
|
||||
public class TraceRmiLaunchDialog extends AbstractDebuggerParameterDialog<LaunchParameter<?>> {
|
||||
|
||||
public TraceRmiLaunchDialog(PluginTool tool, String title, String buttonText, Icon buttonIcon) {
|
||||
super(tool, title, buttonText, buttonIcon);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parameterName(LaunchParameter<?> parameter) {
|
||||
return parameter.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> parameterType(LaunchParameter<?> parameter) {
|
||||
return parameter.type();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parameterLabel(LaunchParameter<?> parameter) {
|
||||
return parameter.display();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parameterToolTip(LaunchParameter<?> parameter) {
|
||||
return parameter.description();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ValStr<?> parameterDefault(LaunchParameter<?> parameter) {
|
||||
return parameter.defaultValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<?> parameterChoices(LaunchParameter<?> parameter) {
|
||||
return parameter.choices();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, ValStr<?>> validateArguments(Map<String, LaunchParameter<?>> parameters,
|
||||
Map<String, ValStr<?>> arguments) {
|
||||
return LaunchParameter.validateArguments(parameters, arguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parameterSaveValue(LaunchParameter<?> parameter, SaveState state, String key,
|
||||
ValStr<?> value) {
|
||||
state.putString(key, value.str());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ValStr<?> parameterLoadValue(LaunchParameter<?> parameter, SaveState state,
|
||||
String key) {
|
||||
String str = state.getString(key, null);
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
return parameter.decode(str);
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -22,6 +22,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.program.model.listing.Program;
|
||||
|
||||
/**
|
||||
@ -32,6 +33,8 @@ import ghidra.program.model.listing.Program;
|
||||
* {@link ScriptAttributesParser}.
|
||||
*/
|
||||
public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer {
|
||||
public static final String HASH = "#";
|
||||
public static final int HASH_LEN = HASH.length();
|
||||
public static final String SHEBANG = "#!";
|
||||
|
||||
/**
|
||||
@ -56,10 +59,10 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLa
|
||||
@Override
|
||||
protected String removeDelimiter(String line) {
|
||||
String stripped = line.stripLeading();
|
||||
if (!stripped.startsWith("#")) {
|
||||
if (!stripped.startsWith(HASH)) {
|
||||
return null;
|
||||
}
|
||||
return stripped.substring(1);
|
||||
return stripped.substring(HASH_LEN);
|
||||
}
|
||||
};
|
||||
ScriptAttributes attrs = parser.parseFile(script);
|
||||
@ -68,15 +71,7 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLa
|
||||
}
|
||||
|
||||
private UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin,
|
||||
Program program,
|
||||
File script, String configName, ScriptAttributes attrs) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,6 @@ import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.lang.*;
|
||||
import ghidra.program.util.DefaultLanguageService;
|
||||
import ghidra.rmi.trace.TraceRmi.*;
|
||||
import ghidra.rmi.trace.TraceRmi.Compiler;
|
||||
import ghidra.rmi.trace.TraceRmi.Language;
|
||||
import ghidra.trace.database.DBTrace;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
@ -129,11 +128,9 @@ public class TraceRmiHandler implements TraceRmiConnection {
|
||||
}
|
||||
}
|
||||
|
||||
protected record Tid(DoId doId, int txId) {
|
||||
}
|
||||
protected record Tid(DoId doId, int txId) {}
|
||||
|
||||
protected record OpenTx(Tid txId, Transaction tx, boolean undoable) {
|
||||
}
|
||||
protected record OpenTx(Tid txId, Transaction tx, boolean undoable) {}
|
||||
|
||||
protected class OpenTraceMap {
|
||||
private final Map<DoId, OpenTrace> byId = new HashMap<>();
|
||||
@ -388,7 +385,7 @@ public class TraceRmiHandler implements TraceRmiConnection {
|
||||
|
||||
protected static void sendDelimited(OutputStream out, RootMessage msg, long dbgSeq)
|
||||
throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(4);
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
buf.putInt(msg.getSerializedSize());
|
||||
out.write(buf.array());
|
||||
msg.writeTo(out);
|
||||
@ -867,6 +864,9 @@ public class TraceRmiHandler implements TraceRmiConnection {
|
||||
throws InvalidNameException, IOException, CancelledException {
|
||||
DomainFolder traces = getOrCreateNewTracesFolder();
|
||||
List<String> path = sanitizePath(req.getPath().getPath());
|
||||
if (path.isEmpty()) {
|
||||
throw new IllegalArgumentException("CreateTrace: path (name) cannot be empty");
|
||||
}
|
||||
DomainFolder folder = createFolders(traces, path.subList(0, path.size() - 1));
|
||||
CompilerSpec cs = requireCompilerSpec(req.getLanguage(), req.getCompiler());
|
||||
DBTrace trace = new DBTrace(path.get(path.size() - 1), cs, this);
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -37,6 +37,7 @@ import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.dbg.util.PathMatcher;
|
||||
import ghidra.dbg.util.PathPredicates;
|
||||
import ghidra.dbg.util.PathPredicates.Align;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.model.DebuggerObjectActionContext;
|
||||
import ghidra.debug.api.model.DebuggerSingleObjectPathActionContext;
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
@ -345,25 +346,15 @@ public class TraceRmiTarget extends AbstractTarget {
|
||||
}
|
||||
|
||||
private Map<String, Object> promptArgs(RemoteMethod method, Map<String, Object> defaults) {
|
||||
SchemaContext ctx = getSchemaContext();
|
||||
/**
|
||||
* TODO: RemoteMethod parameter descriptions should also use ValStr. This map conversion
|
||||
* stuff is getting onerous and hacky.
|
||||
*/
|
||||
Map<String, ValStr<?>> defs = ValStr.fromPlainMap(defaults);
|
||||
RemoteMethodInvocationDialog dialog = new RemoteMethodInvocationDialog(tool,
|
||||
method.display(), method.display(), null);
|
||||
while (true) {
|
||||
for (RemoteParameter param : method.parameters().values()) {
|
||||
Object val = defaults.get(param.name());
|
||||
if (val != null) {
|
||||
Class<?> type = ctx.getSchema(param.type()).getType();
|
||||
dialog.setMemorizedArgument(param.name(), type.asSubclass(Object.class),
|
||||
val);
|
||||
}
|
||||
}
|
||||
Map<String, Object> args = dialog.promptArguments(ctx, method.parameters(), defaults);
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
return null;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
getSchemaContext(), method.display(), method.display(), null);
|
||||
Map<String, ValStr<?>> args = dialog.promptArguments(method.parameters(), defs, defs);
|
||||
return args == null ? null : ValStr.toPlainMap(args);
|
||||
}
|
||||
|
||||
private CompletableFuture<?> invokeMethod(boolean prompt, RemoteMethod method,
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -30,7 +30,7 @@ import org.junit.Test;
|
||||
|
||||
import generic.Unique;
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
|
||||
import ghidra.app.plugin.core.debug.gui.objects.components.InvocationDialogHelper;
|
||||
import ghidra.app.plugin.core.debug.gui.InvocationDialogHelper;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.connection.tree.*;
|
||||
import ghidra.app.plugin.core.debug.service.control.DebuggerControlServicePlugin;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiClient;
|
||||
@ -60,13 +60,17 @@ public class TraceRmiConnectionManagerProviderTest extends AbstractGhidraHeadedD
|
||||
provider = waitForComponentProvider(TraceRmiConnectionManagerProvider.class);
|
||||
}
|
||||
|
||||
InvocationDialogHelper<?, ?> waitDialog() {
|
||||
return InvocationDialogHelper.waitFor(TraceRmiConnectDialog.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionAccept() throws Exception {
|
||||
performEnabledAction(provider, provider.actionConnectAccept, false);
|
||||
InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
|
||||
InvocationDialogHelper<?, ?> helper = waitDialog();
|
||||
helper.dismissWithArguments(Map.ofEntries(
|
||||
Map.entry("address", "localhost"),
|
||||
Map.entry("port", 0)));
|
||||
helper.entry("address", "localhost"),
|
||||
helper.entry("port", 0)));
|
||||
waitForPass(() -> Unique.assertOne(traceRmiService.getAllAcceptors()));
|
||||
}
|
||||
|
||||
@ -78,10 +82,10 @@ public class TraceRmiConnectionManagerProviderTest extends AbstractGhidraHeadedD
|
||||
throw new AssertionError();
|
||||
}
|
||||
performEnabledAction(provider, provider.actionConnectOutbound, false);
|
||||
InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
|
||||
InvocationDialogHelper<?, ?> helper = waitDialog();
|
||||
helper.dismissWithArguments(Map.ofEntries(
|
||||
Map.entry("address", sockaddr.getHostString()),
|
||||
Map.entry("port", sockaddr.getPort())));
|
||||
helper.entry("address", sockaddr.getHostString()),
|
||||
helper.entry("port", sockaddr.getPort())));
|
||||
try (SocketChannel channel = server.accept()) {
|
||||
TestTraceRmiClient client = new TestTraceRmiClient(channel);
|
||||
client.sendNegotiate("Test client");
|
||||
@ -94,10 +98,10 @@ public class TraceRmiConnectionManagerProviderTest extends AbstractGhidraHeadedD
|
||||
@Test
|
||||
public void testActionStartServer() throws Exception {
|
||||
performEnabledAction(provider, provider.actionStartServer, false);
|
||||
InvocationDialogHelper helper = InvocationDialogHelper.waitFor();
|
||||
InvocationDialogHelper<?, ?> helper = waitDialog();
|
||||
helper.dismissWithArguments(Map.ofEntries(
|
||||
Map.entry("address", "localhost"),
|
||||
Map.entry("port", 0)));
|
||||
helper.entry("address", "localhost"),
|
||||
helper.entry("port", 0)));
|
||||
waitForPass(() -> assertTrue(traceRmiService.isServerStarted()));
|
||||
waitForPass(() -> assertFalse(provider.actionStartServer.isEnabled()));
|
||||
|
||||
|
@ -0,0 +1,767 @@
|
||||
/* ###
|
||||
* 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;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.beans.*;
|
||||
import java.io.File;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
import docking.DialogComponentProvider;
|
||||
import docking.options.editor.FileChooserEditor;
|
||||
import docking.widgets.button.BrowseButton;
|
||||
import docking.widgets.filechooser.GhidraFileChooser;
|
||||
import docking.widgets.filechooser.GhidraFileChooserMode;
|
||||
import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.AutoConfigState.PathIsDir;
|
||||
import ghidra.framework.plugintool.AutoConfigState.PathIsFile;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.layout.PairLayout;
|
||||
|
||||
public abstract class AbstractDebuggerParameterDialog<P> extends DialogComponentProvider
|
||||
implements PropertyChangeListener {
|
||||
static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments";
|
||||
|
||||
public static class BigIntEditor extends PropertyEditorSupport {
|
||||
String asText = "";
|
||||
|
||||
@Override
|
||||
public String getJavaInitializationString() {
|
||||
Object value = getValue();
|
||||
return value == null
|
||||
? "null"
|
||||
: "new BigInteger(\"%s\")".formatted(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
/**
|
||||
* Set asText first, since setValue will fire change listener. It will call getAsText().
|
||||
*/
|
||||
asText = text;
|
||||
setValueNoAsText(text == null
|
||||
? null
|
||||
: NumericUtilities.decodeBigInteger(text));
|
||||
}
|
||||
|
||||
public void setValueNoAsText(Object value) {
|
||||
super.setValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
super.setValue(value);
|
||||
asText = value == null ? "" : value.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAsText() {
|
||||
return asText;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileChooserPanel extends JPanel {
|
||||
private final static int NUMBER_OF_COLUMNS = 20;
|
||||
|
||||
private final JTextField textField = new JTextField(NUMBER_OF_COLUMNS);
|
||||
private final JButton browseButton = new BrowseButton();
|
||||
private final Runnable propertyChange;
|
||||
|
||||
private GhidraFileChooser fileChooser; // lazy
|
||||
|
||||
public FileChooserPanel(Runnable propertyChange) {
|
||||
this.propertyChange = propertyChange;
|
||||
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
add(textField);
|
||||
add(Box.createHorizontalStrut(5));
|
||||
add(browseButton);
|
||||
setBorder(BorderFactory.createEmptyBorder());
|
||||
|
||||
textField.addActionListener(e -> propertyChange.run());
|
||||
textField.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
propertyChange.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
propertyChange.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
propertyChange.run();
|
||||
}
|
||||
});
|
||||
|
||||
browseButton.addActionListener(e -> displayFileChooser());
|
||||
}
|
||||
|
||||
public void setValue(File file) {
|
||||
textField.setText(file == null ? "" : file.getAbsolutePath());
|
||||
}
|
||||
|
||||
private void displayFileChooser() {
|
||||
if (fileChooser == null) {
|
||||
fileChooser = createFileChooser();
|
||||
}
|
||||
|
||||
String path = textField.getText().trim();
|
||||
if (!path.isEmpty()) {
|
||||
File f = new File(path);
|
||||
if (f.isDirectory()) {
|
||||
fileChooser.setCurrentDirectory(f);
|
||||
}
|
||||
else {
|
||||
File pf = f.getParentFile();
|
||||
if (pf != null && pf.isDirectory()) {
|
||||
fileChooser.setSelectedFile(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File chosen = fileChooser.getSelectedFile(true);
|
||||
if (chosen != null) {
|
||||
textField.setText(chosen.getAbsolutePath());
|
||||
propertyChange.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected String getTitle() {
|
||||
return "Choose Path";
|
||||
}
|
||||
|
||||
protected GhidraFileChooserMode getSelectionMode() {
|
||||
return GhidraFileChooserMode.FILES_AND_DIRECTORIES;
|
||||
}
|
||||
|
||||
private GhidraFileChooser createFileChooser() {
|
||||
GhidraFileChooser chooser = new GhidraFileChooser(browseButton);
|
||||
chooser.setTitle(getTitle());
|
||||
chooser.setApproveButtonText(getTitle());
|
||||
chooser.setFileSelectionMode(getSelectionMode());
|
||||
// No way for script to specify filter....
|
||||
|
||||
return chooser;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compared to {@link FileChooserEditor}, this does not require the user to enter a full path.
|
||||
* Nor will it resolve file names against the working directory. It's just a text box with a
|
||||
* file browser assist.
|
||||
*/
|
||||
public static class PathEditor extends PropertyEditorSupport {
|
||||
private final FileChooserPanel panel = newChooserPanel();
|
||||
|
||||
protected FileChooserPanel newChooserPanel() {
|
||||
return new FileChooserPanel(this::firePropertyChange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAsText() {
|
||||
return panel.textField.getText().trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
String text = panel.textField.getText().trim();
|
||||
if (text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Paths.get(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
if (text == null || text.isBlank()) {
|
||||
panel.textField.setText("");
|
||||
}
|
||||
else {
|
||||
panel.textField.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (value == null) {
|
||||
panel.textField.setText("");
|
||||
}
|
||||
else if (value instanceof String s) {
|
||||
panel.textField.setText(s);
|
||||
}
|
||||
else if (value instanceof Path p) {
|
||||
panel.textField.setText(p.toString());
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("value=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCustomEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getCustomEditor() {
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PathIsDirEditor extends PathEditor {
|
||||
@Override
|
||||
protected FileChooserPanel newChooserPanel() {
|
||||
return new FileChooserPanel(this::firePropertyChange) {
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
return "Choose Directory";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GhidraFileChooserMode getSelectionMode() {
|
||||
return GhidraFileChooserMode.DIRECTORIES_ONLY;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
Object value = super.getValue();
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Path p) {
|
||||
return new PathIsDir(p);
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (value instanceof PathIsDir dir) {
|
||||
super.setValue(dir.path());
|
||||
}
|
||||
else {
|
||||
super.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class PathIsFileEditor extends PathEditor {
|
||||
@Override
|
||||
protected FileChooserPanel newChooserPanel() {
|
||||
return new FileChooserPanel(this::firePropertyChange) {
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
return "Choose File";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GhidraFileChooserMode getSelectionMode() {
|
||||
return GhidraFileChooserMode.FILES_ONLY;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
Object value = super.getValue();
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Path p) {
|
||||
return new PathIsFile(p);
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (value instanceof PathIsFile file) {
|
||||
super.setValue(file.path());
|
||||
}
|
||||
else {
|
||||
super.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
PropertyEditorManager.registerEditor(BigInteger.class, BigIntEditor.class);
|
||||
PropertyEditorManager.registerEditor(Path.class, PathEditor.class);
|
||||
PropertyEditorManager.registerEditor(PathIsDir.class, PathIsDirEditor.class);
|
||||
PropertyEditorManager.registerEditor(PathIsFile.class, PathIsFileEditor.class);
|
||||
}
|
||||
|
||||
static class ChoicesPropertyEditor implements PropertyEditor {
|
||||
private final List<?> choices;
|
||||
private final String[] tags;
|
||||
|
||||
private final List<PropertyChangeListener> listeners = new ArrayList<>();
|
||||
|
||||
private Object value;
|
||||
|
||||
public ChoicesPropertyEditor(Collection<?> choices) {
|
||||
this.choices = choices.stream().distinct().toList();
|
||||
this.tags = choices.stream().map(Objects::toString).toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (Objects.equals(value, this.value)) {
|
||||
return;
|
||||
}
|
||||
if (!choices.contains(value)) {
|
||||
throw new IllegalArgumentException("Unsupported value: " + value);
|
||||
}
|
||||
Object oldValue;
|
||||
List<PropertyChangeListener> listeners;
|
||||
synchronized (this.listeners) {
|
||||
oldValue = this.value;
|
||||
this.value = value;
|
||||
if (this.listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
listeners = List.copyOf(this.listeners);
|
||||
}
|
||||
PropertyChangeEvent evt = new PropertyChangeEvent(this, null, oldValue, value);
|
||||
for (PropertyChangeListener l : listeners) {
|
||||
l.propertyChange(evt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPaintable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintValue(Graphics gfx, Rectangle box) {
|
||||
// Not paintable
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJavaInitializationString() {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
if (value instanceof String str) {
|
||||
return "\"" + StringEscapeUtils.escapeJava(str) + "\"";
|
||||
}
|
||||
return Objects.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAsText() {
|
||||
return Objects.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
int index = ArrayUtils.indexOf(tags, text);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("Unsupported value: " + text);
|
||||
}
|
||||
setValue(choices.get(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return tags.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getCustomEditor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCustomEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPropertyChangeListener(PropertyChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePropertyChangeListener(PropertyChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected record NameTypePair(String name, Class<?> type) {
|
||||
public static NameTypePair fromString(String name) throws ClassNotFoundException {
|
||||
String[] parts = name.split(",", 2);
|
||||
if (parts.length != 2) {
|
||||
// This appears to be a bad assumption - empty fields results in solitary labels
|
||||
return new NameTypePair(parts[0], String.class);
|
||||
//throw new IllegalArgumentException("Could not parse name,type");
|
||||
}
|
||||
return new NameTypePair(parts[0], Class.forName(parts[1]));
|
||||
}
|
||||
|
||||
public final String encodeString() {
|
||||
return name + "," + type.getName();
|
||||
}
|
||||
}
|
||||
|
||||
private final BidiMap<P, PropertyEditor> paramEditors = new DualLinkedHashBidiMap<>();
|
||||
|
||||
private JPanel panel;
|
||||
private JLabel descriptionLabel;
|
||||
private JPanel pairPanel;
|
||||
private PairLayout layout;
|
||||
|
||||
protected JButton invokeButton;
|
||||
protected JButton resetButton;
|
||||
|
||||
private final PluginTool tool;
|
||||
// package access for testing
|
||||
Map<String, P> parameters;
|
||||
|
||||
private Map<String, ValStr<?>> defaults = Map.of();
|
||||
// TODO: Not sure this is the best keying, but I think it works.
|
||||
private Map<NameTypePair, ValStr<?>> memorized = new HashMap<>();
|
||||
private Map<String, ValStr<?>> arguments;
|
||||
|
||||
public AbstractDebuggerParameterDialog(PluginTool tool, String title, String buttonText,
|
||||
Icon buttonIcon) {
|
||||
super(title, true, true, true, false);
|
||||
this.tool = tool;
|
||||
|
||||
populateComponents(buttonText, buttonIcon);
|
||||
setRememberSize(false);
|
||||
}
|
||||
|
||||
protected abstract String parameterName(P parameter);
|
||||
|
||||
protected abstract Class<?> parameterType(P parameter);
|
||||
|
||||
protected NameTypePair parameterNameAndType(P parameter) {
|
||||
return new NameTypePair(parameterName(parameter), parameterType(parameter));
|
||||
}
|
||||
|
||||
protected abstract String parameterLabel(P parameter);
|
||||
|
||||
protected abstract String parameterToolTip(P parameter);
|
||||
|
||||
protected abstract ValStr<?> parameterDefault(P parameter);
|
||||
|
||||
protected abstract Collection<?> parameterChoices(P parameter);
|
||||
|
||||
protected abstract Map<String, ValStr<?>> validateArguments(Map<String, P> parameters,
|
||||
Map<String, ValStr<?>> arguments);
|
||||
|
||||
protected abstract void parameterSaveValue(P parameter, SaveState state, String key,
|
||||
ValStr<?> value);
|
||||
|
||||
protected abstract ValStr<?> parameterLoadValue(P parameter, SaveState state, String key);
|
||||
|
||||
protected ValStr<?> computeInitialValue(P parameter) {
|
||||
ValStr<?> val = memorized.computeIfAbsent(parameterNameAndType(parameter),
|
||||
ntp -> defaults.get(parameterName(parameter)));
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user for the given arguments, all at once
|
||||
*
|
||||
* <p>
|
||||
* This displays a single dialog with each option listed. The parameter map contains the
|
||||
* description of each parameter to be displayed. The {@code initial} values are the values to
|
||||
* pre-populate the options with, e.g., because they are saved from a previous session, or
|
||||
* because they are the suggested values. If the user clicks the "Reset" button, the values are
|
||||
* revered to the defaults given in each parameter's description, unless that value is
|
||||
* overridden in {@code defaults}. This may be appropriate if a value is suggested for a
|
||||
* (perhaps required) option that otherwise has no default.
|
||||
*
|
||||
* @param parameterMap the map of parameters, keyed by {@link #parameterName(Object)}. This map
|
||||
* may be ordered to control the order of options displayed.
|
||||
* @param initial the initial values of the options. If a key is not provided, the initial value
|
||||
* is its default value. Extraneous keys are ignored.
|
||||
* @param defaults the default values to use upon reset. If a key is not provided, the default
|
||||
* is taken from the parameter description. Extraneous keys are ignored.
|
||||
* @return the arguments provided by the user
|
||||
*/
|
||||
public Map<String, ValStr<?>> promptArguments(Map<String, P> parameterMap,
|
||||
Map<String, ValStr<?>> initial, Map<String, ValStr<?>> defaults) {
|
||||
setDefaults(defaults);
|
||||
setParameters(parameterMap);
|
||||
setMemorizedArguments(initial);
|
||||
populateValues(initial);
|
||||
tool.showDialog(this);
|
||||
|
||||
return getArguments();
|
||||
}
|
||||
|
||||
protected void setParameters(Map<String, P> parameterMap) {
|
||||
this.parameters = parameterMap;
|
||||
for (P param : parameterMap.values()) {
|
||||
if (!defaults.containsKey(parameterName(param))) {
|
||||
defaults.put(parameterName(param), parameterDefault(param));
|
||||
}
|
||||
}
|
||||
populateOptions();
|
||||
}
|
||||
|
||||
protected void setMemorizedArguments(Map<String, ValStr<?>> initial) {
|
||||
for (P param : parameters.values()) {
|
||||
ValStr<?> val = initial.get(parameterName(param));
|
||||
if (val != null) {
|
||||
setMemorizedArgument(param, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void setDefaults(Map<String, ValStr<?>> defaults) {
|
||||
this.defaults = new HashMap<>(defaults);
|
||||
}
|
||||
|
||||
private void populateComponents(String buttonText, Icon buttonIcon) {
|
||||
panel = new JPanel(new BorderLayout());
|
||||
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
layout = new PairLayout(5, 5);
|
||||
pairPanel = new JPanel(layout);
|
||||
|
||||
JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER));
|
||||
JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
//scrolling.setPreferredSize(new Dimension(100, 130));
|
||||
panel.add(scrolling, BorderLayout.CENTER);
|
||||
centering.add(pairPanel);
|
||||
|
||||
descriptionLabel = new JLabel();
|
||||
descriptionLabel.setMaximumSize(new Dimension(300, 100));
|
||||
panel.add(descriptionLabel, BorderLayout.NORTH);
|
||||
|
||||
addWorkPanel(panel);
|
||||
|
||||
invokeButton = new JButton(buttonText, buttonIcon);
|
||||
addButton(invokeButton);
|
||||
resetButton = new JButton("Reset", DebuggerResources.ICON_REFRESH);
|
||||
addButton(resetButton);
|
||||
addCancelButton();
|
||||
|
||||
invokeButton.addActionListener(this::invoke);
|
||||
resetButton.addActionListener(this::reset);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelCallback() {
|
||||
this.arguments = null;
|
||||
close();
|
||||
}
|
||||
|
||||
void invoke(ActionEvent evt) {
|
||||
try {
|
||||
this.arguments = validateArguments(parameters, collectArguments());
|
||||
close();
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
setStatusText(e.getMessage(), MessageType.ERROR, true);
|
||||
}
|
||||
}
|
||||
|
||||
void reset(ActionEvent evt) {
|
||||
this.arguments = null;
|
||||
populateValues(defaults);
|
||||
}
|
||||
|
||||
protected PropertyEditor createEditor(P parameter) {
|
||||
Collection<?> choices = parameterChoices(parameter);
|
||||
if (!choices.isEmpty()) {
|
||||
return new ChoicesPropertyEditor(choices);
|
||||
}
|
||||
Class<?> type = parameterType(parameter);
|
||||
PropertyEditor editor = PropertyEditorManager.findEditor(type);
|
||||
if (editor != null) {
|
||||
return editor;
|
||||
}
|
||||
Msg.warn(this, "No editor for " + type + "? Trying String instead");
|
||||
editor = PropertyEditorManager.findEditor(String.class);
|
||||
return editor;
|
||||
}
|
||||
|
||||
// test access
|
||||
PropertyEditor getEditor(P parameter) {
|
||||
return paramEditors.get(parameter);
|
||||
}
|
||||
|
||||
protected void setEditorValue(PropertyEditor editor, P param, ValStr<?> val) {
|
||||
switch (val.val()) {
|
||||
case null -> {
|
||||
}
|
||||
case BigInteger bi -> editor.setAsText(val.str());
|
||||
default -> editor.setValue(val.val());
|
||||
}
|
||||
}
|
||||
|
||||
void populateOptions() {
|
||||
pairPanel.removeAll();
|
||||
paramEditors.clear();
|
||||
for (P param : parameters.values()) {
|
||||
JLabel label = new JLabel(parameterLabel(param));
|
||||
label.setToolTipText(parameterToolTip(param));
|
||||
pairPanel.add(label);
|
||||
|
||||
PropertyEditor editor = createEditor(param);
|
||||
ValStr<?> val = computeInitialValue(param);
|
||||
setEditorValue(editor, param, val);
|
||||
editor.addPropertyChangeListener(this);
|
||||
pairPanel.add(MiscellaneousUtils.getEditorComponent(editor));
|
||||
paramEditors.put(param, editor);
|
||||
}
|
||||
}
|
||||
|
||||
void populateValues(Map<String, ValStr<?>> values) {
|
||||
for (Map.Entry<String, ValStr<?>> ent : values.entrySet()) {
|
||||
P param = parameters.get(ent.getKey());
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
PropertyEditor editor = paramEditors.get(param);
|
||||
setEditorValue(editor, param, ent.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
protected Map<String, ValStr<?>> collectArguments() {
|
||||
Map<String, ValStr<?>> map = new LinkedHashMap<>();
|
||||
Set<String> invalid = new LinkedHashSet<>();
|
||||
for (Entry<P, PropertyEditor> ent : paramEditors.entrySet()) {
|
||||
P param = ent.getKey();
|
||||
PropertyEditor editor = ent.getValue();
|
||||
ValStr<?> val = memorized.get(parameterNameAndType(param));
|
||||
if (!Objects.equals(editor.getAsText(), val.str())) {
|
||||
invalid.add(parameterLabel(param));
|
||||
}
|
||||
if (val != null) {
|
||||
map.put(parameterName(param), val);
|
||||
}
|
||||
}
|
||||
if (!invalid.isEmpty()) {
|
||||
throw new IllegalStateException("Invalid value for " + invalid);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public Map<String, ValStr<?>> getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
void setMemorizedArgument(P parameter, ValStr<?> value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
memorized.put(parameterNameAndType(parameter), value);
|
||||
}
|
||||
|
||||
public void forgetMemorizedArguments() {
|
||||
memorized.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
PropertyEditor editor = (PropertyEditor) evt.getSource();
|
||||
P param = paramEditors.getKey(editor);
|
||||
memorized.put(parameterNameAndType(param),
|
||||
new ValStr<>(editor.getValue(), editor.getAsText()));
|
||||
}
|
||||
|
||||
public void writeConfigState(SaveState saveState) {
|
||||
SaveState subState = new SaveState();
|
||||
for (Map.Entry<NameTypePair, ValStr<?>> ent : memorized.entrySet()) {
|
||||
NameTypePair ntp = ent.getKey();
|
||||
P param = parameters.get(ntp.name());
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
parameterSaveValue(param, subState, ntp.encodeString(), ent.getValue());
|
||||
}
|
||||
saveState.putSaveState(KEY_MEMORIZED_ARGUMENTS, subState);
|
||||
}
|
||||
|
||||
public void readConfigState(SaveState saveState) {
|
||||
/**
|
||||
* TODO: This method is defunct. It is only used by the DebuggerObjectsProvider, which is
|
||||
* now deprecated, but I suspect other providers intend to use this in the same way. If
|
||||
* those providers don't manually load/compute initial and default values at the time of
|
||||
* prompting, then this will need to be fixed. The decode of the values will need to be
|
||||
* delayed until (and repeated every time) parameters are populated.
|
||||
*/
|
||||
SaveState subState = saveState.getSaveState(KEY_MEMORIZED_ARGUMENTS);
|
||||
if (subState == null) {
|
||||
return;
|
||||
}
|
||||
for (String name : subState.getNames()) {
|
||||
try {
|
||||
NameTypePair ntp = NameTypePair.fromString(name);
|
||||
P param = parameters.get(ntp.name());
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
memorized.put(ntp, parameterLoadValue(param, subState, ntp.encodeString()));
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Error restoring memorized parameter " + name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDescription(String htmlDescription) {
|
||||
if (htmlDescription == null) {
|
||||
descriptionLabel.setBorder(BorderFactory.createEmptyBorder());
|
||||
descriptionLabel.setText("");
|
||||
}
|
||||
else {
|
||||
descriptionLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
|
||||
descriptionLabel.setText(htmlDescription);
|
||||
}
|
||||
}
|
||||
}
|
@ -40,6 +40,18 @@ import ghidra.util.task.TaskMonitor;
|
||||
public class ByModuleAutoMapSpec implements AutoMapSpec {
|
||||
public static final String CONFIG_NAME = "1_MAP_BY_MODULE";
|
||||
|
||||
/**
|
||||
* Get the instance.
|
||||
*
|
||||
* <p>
|
||||
* Note this will not work until after the class searcher is done.
|
||||
*
|
||||
* @return the instance
|
||||
*/
|
||||
public static ByModuleAutoMapSpec instance() {
|
||||
return (ByModuleAutoMapSpec) AutoMapSpec.fromConfigName(CONFIG_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigName() {
|
||||
return CONFIG_NAME;
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JPanel;
|
||||
@ -63,6 +64,7 @@ import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
|
||||
import ghidra.dbg.util.DebuggerCallbackReorderer;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.model.DebuggerMemoryMapper;
|
||||
import ghidra.debug.api.model.TraceRecorder;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
@ -135,8 +137,8 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter
|
||||
private final AutoService.Wiring autoServiceWiring;
|
||||
|
||||
@AutoOptionDefined(
|
||||
name = "Default Extended Step",
|
||||
description = "The default string for the extended step command")
|
||||
name = "Default Extended Step",
|
||||
description = "The default string for the extended step command")
|
||||
String extendedStep = "";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ -1367,13 +1369,14 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter
|
||||
boolean prompt = p;
|
||||
};
|
||||
return AsyncUtils.loop(TypeSpec.VOID, (loop) -> {
|
||||
Map<String, ?> args = launchOffer.getLauncherArgs(launcher, locals.prompt);
|
||||
Map<String, ValStr<?>> args = launchOffer.getLauncherArgs(launcher, locals.prompt);
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
loop.exit();
|
||||
}
|
||||
else {
|
||||
launcher.launch(args).thenAccept(loop::exit).exceptionally(ex -> {
|
||||
Map<String, ?> a = ValStr.toPlainMap(args);
|
||||
launcher.launch(a).thenAccept(loop::exit).exceptionally(ex -> {
|
||||
loop.repeat();
|
||||
return null;
|
||||
});
|
||||
@ -1428,7 +1431,7 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter
|
||||
}
|
||||
return;
|
||||
}
|
||||
Map<String, ?> args = methodDialog.promptArguments(parameters);
|
||||
Map<String, ?> args = methodDialog.promptArguments(parameters, Map.of(), Map.of());
|
||||
if (args != null) {
|
||||
String script = (String) args.get("Script");
|
||||
if (script != null && !script.isEmpty()) {
|
||||
@ -1623,7 +1626,8 @@ public class DebuggerObjectsProvider extends ComponentProviderAdapter
|
||||
if (configParameters.isEmpty()) {
|
||||
return AsyncUtils.nil();
|
||||
}
|
||||
Map<String, ?> args = configDialog.promptArguments(configParameters);
|
||||
Map<String, ?> args =
|
||||
configDialog.promptArguments(configParameters, Map.of(), Map.of());
|
||||
if (args == null) {
|
||||
// User cancelled
|
||||
return AsyncUtils.nil();
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -15,654 +15,75 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.objects.components;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.beans.*;
|
||||
import java.io.File;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.Icon;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.DialogComponentProvider;
|
||||
import docking.options.editor.FileChooserEditor;
|
||||
import docking.widgets.button.BrowseButton;
|
||||
import docking.widgets.filechooser.GhidraFileChooser;
|
||||
import docking.widgets.filechooser.GhidraFileChooserMode;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.utils.MiscellaneousUtils;
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractDebuggerParameterDialog;
|
||||
import ghidra.dbg.target.TargetMethod;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.AutoConfigState.*;
|
||||
import ghidra.framework.plugintool.AutoConfigState.ConfigStateField;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.layout.PairLayout;
|
||||
|
||||
public class DebuggerMethodInvocationDialog extends DialogComponentProvider
|
||||
implements PropertyChangeListener {
|
||||
|
||||
public static class BigIntEditor extends PropertyEditorSupport {
|
||||
@Override
|
||||
public String getJavaInitializationString() {
|
||||
Object value = getValue();
|
||||
return value == null
|
||||
? "null"
|
||||
: "new BigInteger(\"%s\")".formatted(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
setValue(text == null
|
||||
? null
|
||||
: new BigInteger(text));
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileChooserPanel extends JPanel {
|
||||
private final static int NUMBER_OF_COLUMNS = 20;
|
||||
|
||||
private final JTextField textField = new JTextField(NUMBER_OF_COLUMNS);
|
||||
private final JButton browseButton = new BrowseButton();
|
||||
private final Runnable propertyChange;
|
||||
|
||||
private GhidraFileChooser fileChooser; // lazy
|
||||
|
||||
public FileChooserPanel(Runnable propertyChange) {
|
||||
this.propertyChange = propertyChange;
|
||||
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
add(textField);
|
||||
add(Box.createHorizontalStrut(5));
|
||||
add(browseButton);
|
||||
setBorder(BorderFactory.createEmptyBorder());
|
||||
|
||||
textField.addActionListener(e -> propertyChange.run());
|
||||
textField.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
propertyChange.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
propertyChange.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
propertyChange.run();
|
||||
}
|
||||
});
|
||||
|
||||
browseButton.addActionListener(e -> displayFileChooser());
|
||||
}
|
||||
|
||||
public void setValue(File file) {
|
||||
textField.setText(file == null ? "" : file.getAbsolutePath());
|
||||
}
|
||||
|
||||
private void displayFileChooser() {
|
||||
if (fileChooser == null) {
|
||||
fileChooser = createFileChooser();
|
||||
}
|
||||
|
||||
String path = textField.getText().trim();
|
||||
if (!path.isEmpty()) {
|
||||
File f = new File(path);
|
||||
if (f.isDirectory()) {
|
||||
fileChooser.setCurrentDirectory(f);
|
||||
}
|
||||
else {
|
||||
File pf = f.getParentFile();
|
||||
if (pf != null && pf.isDirectory()) {
|
||||
fileChooser.setSelectedFile(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File chosen = fileChooser.getSelectedFile(true);
|
||||
if (chosen != null) {
|
||||
textField.setText(chosen.getAbsolutePath());
|
||||
propertyChange.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected String getTitle() {
|
||||
return "Choose Path";
|
||||
}
|
||||
|
||||
protected GhidraFileChooserMode getSelectionMode() {
|
||||
return GhidraFileChooserMode.FILES_AND_DIRECTORIES;
|
||||
}
|
||||
|
||||
private GhidraFileChooser createFileChooser() {
|
||||
GhidraFileChooser chooser = new GhidraFileChooser(browseButton);
|
||||
chooser.setTitle(getTitle());
|
||||
chooser.setApproveButtonText(getTitle());
|
||||
chooser.setFileSelectionMode(getSelectionMode());
|
||||
// No way for script to specify filter....
|
||||
|
||||
return chooser;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compared to {@link FileChooserEditor}, this does not require the user to enter a full path.
|
||||
* Nor will it resolve file names against the working directory. It's just a text box with a
|
||||
* file browser assist.
|
||||
*/
|
||||
public static class PathEditor extends PropertyEditorSupport {
|
||||
private final FileChooserPanel panel = newChooserPanel();
|
||||
|
||||
protected FileChooserPanel newChooserPanel() {
|
||||
return new FileChooserPanel(this::firePropertyChange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAsText() {
|
||||
return panel.textField.getText().trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
String text = panel.textField.getText().trim();
|
||||
if (text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Paths.get(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
if (text == null || text.isBlank()) {
|
||||
panel.textField.setText("");
|
||||
}
|
||||
else {
|
||||
panel.textField.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (value == null) {
|
||||
panel.textField.setText("");
|
||||
}
|
||||
else if (value instanceof String s) {
|
||||
panel.textField.setText(s);
|
||||
}
|
||||
else if (value instanceof Path p) {
|
||||
panel.textField.setText(p.toString());
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("value=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCustomEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getCustomEditor() {
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PathIsDirEditor extends PathEditor {
|
||||
@Override
|
||||
protected FileChooserPanel newChooserPanel() {
|
||||
return new FileChooserPanel(this::firePropertyChange) {
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
return "Choose Directory";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GhidraFileChooserMode getSelectionMode() {
|
||||
return GhidraFileChooserMode.DIRECTORIES_ONLY;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
Object value = super.getValue();
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Path p) {
|
||||
return new PathIsDir(p);
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (value instanceof PathIsDir dir) {
|
||||
super.setValue(dir.path());
|
||||
}
|
||||
else {
|
||||
super.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class PathIsFileEditor extends PathEditor {
|
||||
@Override
|
||||
protected FileChooserPanel newChooserPanel() {
|
||||
return new FileChooserPanel(this::firePropertyChange) {
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
return "Choose File";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GhidraFileChooserMode getSelectionMode() {
|
||||
return GhidraFileChooserMode.FILES_ONLY;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
Object value = super.getValue();
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Path p) {
|
||||
return new PathIsFile(p);
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (value instanceof PathIsFile file) {
|
||||
super.setValue(file.path());
|
||||
}
|
||||
else {
|
||||
super.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
PropertyEditorManager.registerEditor(BigInteger.class, BigIntEditor.class);
|
||||
PropertyEditorManager.registerEditor(Path.class, PathEditor.class);
|
||||
PropertyEditorManager.registerEditor(PathIsDir.class, PathIsDirEditor.class);
|
||||
PropertyEditorManager.registerEditor(PathIsFile.class, PathIsFileEditor.class);
|
||||
}
|
||||
|
||||
private static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments";
|
||||
|
||||
static class ChoicesPropertyEditor implements PropertyEditor {
|
||||
private final List<?> choices;
|
||||
private final String[] tags;
|
||||
|
||||
private final List<PropertyChangeListener> listeners = new ArrayList<>();
|
||||
|
||||
private Object value;
|
||||
|
||||
public ChoicesPropertyEditor(Set<?> choices) {
|
||||
this.choices = List.copyOf(choices);
|
||||
this.tags = choices.stream().map(Objects::toString).toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
if (Objects.equals(value, this.value)) {
|
||||
return;
|
||||
}
|
||||
if (!choices.contains(value)) {
|
||||
throw new IllegalArgumentException("Unsupported value: " + value);
|
||||
}
|
||||
Object oldValue;
|
||||
List<PropertyChangeListener> listeners;
|
||||
synchronized (this.listeners) {
|
||||
oldValue = this.value;
|
||||
this.value = value;
|
||||
if (this.listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
listeners = List.copyOf(this.listeners);
|
||||
}
|
||||
PropertyChangeEvent evt = new PropertyChangeEvent(this, null, oldValue, value);
|
||||
for (PropertyChangeListener l : listeners) {
|
||||
l.propertyChange(evt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPaintable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintValue(Graphics gfx, Rectangle box) {
|
||||
// Not paintable
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJavaInitializationString() {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
if (value instanceof String str) {
|
||||
return "\"" + StringEscapeUtils.escapeJava(str) + "\"";
|
||||
}
|
||||
return Objects.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAsText() {
|
||||
return Objects.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
int index = ArrayUtils.indexOf(tags, text);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("Unsupported value: " + text);
|
||||
}
|
||||
setValue(choices.get(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return tags.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getCustomEditor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCustomEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPropertyChangeListener(PropertyChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePropertyChangeListener(PropertyChangeListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final static class NameTypePair extends MutablePair<String, Class<?>> {
|
||||
|
||||
public static NameTypePair fromParameter(ParameterDescription<?> parameter) {
|
||||
return new NameTypePair(parameter.name, parameter.type);
|
||||
}
|
||||
|
||||
public static NameTypePair fromString(String name) throws ClassNotFoundException {
|
||||
String[] parts = name.split(",", 2);
|
||||
if (parts.length != 2) {
|
||||
// This appears to be a bad assumption - empty fields results in solitary labels
|
||||
return new NameTypePair(parts[0], String.class);
|
||||
//throw new IllegalArgumentException("Could not parse name,type");
|
||||
}
|
||||
return new NameTypePair(parts[0], Class.forName(parts[1]));
|
||||
}
|
||||
|
||||
public NameTypePair(String name, Class<?> type) {
|
||||
super(name, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName() + "," + getType().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> setValue(Class<?> value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return getLeft();
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return getRight();
|
||||
}
|
||||
}
|
||||
|
||||
private final BidiMap<ParameterDescription<?>, PropertyEditor> paramEditors =
|
||||
new DualLinkedHashBidiMap<>();
|
||||
|
||||
private JPanel panel;
|
||||
private JLabel descriptionLabel;
|
||||
private JPanel pairPanel;
|
||||
private PairLayout layout;
|
||||
|
||||
protected JButton invokeButton;
|
||||
protected JButton resetButton;
|
||||
protected boolean resetRequested;
|
||||
|
||||
private final PluginTool tool;
|
||||
Map<String, ParameterDescription<?>> parameters;
|
||||
|
||||
// TODO: Not sure this is the best keying, but I think it works.
|
||||
private Map<NameTypePair, Object> memorized = new HashMap<>();
|
||||
private Map<String, ?> arguments;
|
||||
public class DebuggerMethodInvocationDialog
|
||||
extends AbstractDebuggerParameterDialog<ParameterDescription<?>> {
|
||||
|
||||
public DebuggerMethodInvocationDialog(PluginTool tool, String title, String buttonText,
|
||||
Icon buttonIcon) {
|
||||
super(title, true, true, true, false);
|
||||
this.tool = tool;
|
||||
|
||||
populateComponents(buttonText, buttonIcon);
|
||||
setRememberSize(false);
|
||||
}
|
||||
|
||||
protected Object computeMemorizedValue(ParameterDescription<?> parameter) {
|
||||
return memorized.computeIfAbsent(NameTypePair.fromParameter(parameter),
|
||||
ntp -> parameter.defaultValue);
|
||||
}
|
||||
|
||||
public Map<String, ?> promptArguments(Map<String, ParameterDescription<?>> parameterMap) {
|
||||
setParameters(parameterMap);
|
||||
tool.showDialog(this);
|
||||
|
||||
return getArguments();
|
||||
}
|
||||
|
||||
public void setParameters(Map<String, ParameterDescription<?>> parameterMap) {
|
||||
this.parameters = parameterMap;
|
||||
populateOptions();
|
||||
}
|
||||
|
||||
private void populateComponents(String buttonText, Icon buttonIcon) {
|
||||
panel = new JPanel(new BorderLayout());
|
||||
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
layout = new PairLayout(5, 5);
|
||||
pairPanel = new JPanel(layout);
|
||||
|
||||
JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER));
|
||||
JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
//scrolling.setPreferredSize(new Dimension(100, 130));
|
||||
panel.add(scrolling, BorderLayout.CENTER);
|
||||
centering.add(pairPanel);
|
||||
|
||||
descriptionLabel = new JLabel();
|
||||
descriptionLabel.setMaximumSize(new Dimension(300, 100));
|
||||
panel.add(descriptionLabel, BorderLayout.NORTH);
|
||||
|
||||
addWorkPanel(panel);
|
||||
|
||||
invokeButton = new JButton(buttonText, buttonIcon);
|
||||
addButton(invokeButton);
|
||||
resetButton = new JButton("Reset", DebuggerResources.ICON_REFRESH);
|
||||
addButton(resetButton);
|
||||
addCancelButton();
|
||||
|
||||
invokeButton.addActionListener(this::invoke);
|
||||
resetButton.addActionListener(this::reset);
|
||||
resetRequested = false;
|
||||
super(tool, title, buttonText, buttonIcon);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelCallback() {
|
||||
this.arguments = null;
|
||||
this.resetRequested = false;
|
||||
close();
|
||||
}
|
||||
|
||||
void invoke(ActionEvent evt) {
|
||||
this.arguments = TargetMethod.validateArguments(parameters, collectArguments(), false);
|
||||
this.resetRequested = false;
|
||||
close();
|
||||
}
|
||||
|
||||
void reset(ActionEvent evt) {
|
||||
this.arguments = new LinkedHashMap<>();
|
||||
this.resetRequested = true;
|
||||
close();
|
||||
}
|
||||
|
||||
protected PropertyEditor getEditor(ParameterDescription<?> param) {
|
||||
if (!param.choices.isEmpty()) {
|
||||
return new ChoicesPropertyEditor(param.choices);
|
||||
}
|
||||
Class<?> type = param.type;
|
||||
PropertyEditor editor = PropertyEditorManager.findEditor(type);
|
||||
if (editor != null) {
|
||||
return editor;
|
||||
}
|
||||
Msg.warn(this, "No editor for " + type + "? Trying String instead");
|
||||
return PropertyEditorManager.findEditor(String.class);
|
||||
}
|
||||
|
||||
void populateOptions() {
|
||||
pairPanel.removeAll();
|
||||
paramEditors.clear();
|
||||
for (ParameterDescription<?> param : parameters.values()) {
|
||||
JLabel label = new JLabel(param.display);
|
||||
label.setToolTipText(param.description);
|
||||
pairPanel.add(label);
|
||||
|
||||
PropertyEditor editor = getEditor(param);
|
||||
Object val = computeMemorizedValue(param);
|
||||
if (val == null) {
|
||||
editor.setValue("");
|
||||
}
|
||||
else {
|
||||
editor.setValue(val);
|
||||
}
|
||||
editor.addPropertyChangeListener(this);
|
||||
pairPanel.add(MiscellaneousUtils.getEditorComponent(editor));
|
||||
paramEditors.put(param, editor);
|
||||
}
|
||||
}
|
||||
|
||||
protected Map<String, ?> collectArguments() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
for (ParameterDescription<?> param : paramEditors.keySet()) {
|
||||
Object val = memorized.get(NameTypePair.fromParameter(param));
|
||||
if (val != null) {
|
||||
map.put(param.name, val);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public Map<String, ?> getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public <T> void setMemorizedArgument(String name, Class<T> type, T value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
memorized.put(new NameTypePair(name, type), value);
|
||||
}
|
||||
|
||||
public <T> T getMemorizedArgument(String name, Class<T> type) {
|
||||
return type.cast(memorized.get(new NameTypePair(name, type)));
|
||||
}
|
||||
|
||||
public void forgetMemorizedArguments() {
|
||||
memorized.clear();
|
||||
protected String parameterName(ParameterDescription<?> parameter) {
|
||||
return parameter.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
PropertyEditor editor = (PropertyEditor) evt.getSource();
|
||||
ParameterDescription<?> param = paramEditors.getKey(editor);
|
||||
memorized.put(NameTypePair.fromParameter(param), editor.getValue());
|
||||
protected Class<?> parameterType(ParameterDescription<?> parameter) {
|
||||
return parameter.type;
|
||||
}
|
||||
|
||||
public void writeConfigState(SaveState saveState) {
|
||||
SaveState subState = new SaveState();
|
||||
for (Map.Entry<NameTypePair, Object> ent : memorized.entrySet()) {
|
||||
NameTypePair ntp = ent.getKey();
|
||||
ConfigStateField.putState(subState, ntp.getType().asSubclass(Object.class),
|
||||
ntp.getName(), ent.getValue());
|
||||
}
|
||||
saveState.putXmlElement(KEY_MEMORIZED_ARGUMENTS, subState.saveToXml());
|
||||
@Override
|
||||
protected String parameterLabel(ParameterDescription<?> parameter) {
|
||||
return parameter.display;
|
||||
}
|
||||
|
||||
public void readConfigState(SaveState saveState) {
|
||||
Element element = saveState.getXmlElement(KEY_MEMORIZED_ARGUMENTS);
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
SaveState subState = new SaveState(element);
|
||||
for (String name : subState.getNames()) {
|
||||
try {
|
||||
NameTypePair ntp = NameTypePair.fromString(name);
|
||||
memorized.put(ntp,
|
||||
ConfigStateField.getState(subState, ntp.getType(), ntp.getName()));
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Error restoring memorized parameter " + name, e);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected String parameterToolTip(ParameterDescription<?> parameter) {
|
||||
return parameter.description;
|
||||
}
|
||||
|
||||
public boolean isResetRequested() {
|
||||
return resetRequested;
|
||||
@Override
|
||||
protected ValStr<?> parameterDefault(ParameterDescription<?> parameter) {
|
||||
return ValStr.from(parameter.defaultValue);
|
||||
}
|
||||
|
||||
public void setDescription(String htmlDescription) {
|
||||
if (htmlDescription == null) {
|
||||
descriptionLabel.setBorder(BorderFactory.createEmptyBorder());
|
||||
descriptionLabel.setText("");
|
||||
}
|
||||
else {
|
||||
descriptionLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
|
||||
descriptionLabel.setText(htmlDescription);
|
||||
}
|
||||
@Override
|
||||
protected Set<?> parameterChoices(ParameterDescription<?> parameter) {
|
||||
return parameter.choices;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, ValStr<?>> validateArguments(
|
||||
Map<String, ParameterDescription<?>> parameters, Map<String, ValStr<?>> arguments) {
|
||||
Map<String, ?> args = ValStr.toPlainMap(arguments);
|
||||
return ValStr.fromPlainMap(TargetMethod.validateArguments(parameters, args, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parameterSaveValue(ParameterDescription<?> parameter, SaveState state,
|
||||
String key, ValStr<?> value) {
|
||||
ConfigStateField.putState(state, parameter.type.asSubclass(Object.class), key, value.val());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ValStr<?> parameterLoadValue(ParameterDescription<?> parameter, SaveState state,
|
||||
String key) {
|
||||
return ValStr.from(ConfigStateField.getState(state, parameter.type, key));
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -16,10 +16,12 @@
|
||||
package ghidra.app.plugin.core.debug.service.model;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import docking.ActionContext;
|
||||
import ghidra.app.context.ProgramLocationActionContext;
|
||||
@ -38,6 +40,7 @@ import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||
import ghidra.dbg.target.TargetSteppable.TargetStepKind;
|
||||
import ghidra.dbg.util.PathMatcher;
|
||||
import ghidra.dbg.util.PathPredicates;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.model.*;
|
||||
import ghidra.debug.api.target.ActionName;
|
||||
import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
||||
@ -237,26 +240,12 @@ public class TraceRecorderTarget extends AbstractTarget {
|
||||
}
|
||||
|
||||
private Map<String, ?> promptArgs(TargetMethod method, Map<String, ?> defaults) {
|
||||
Map<String, ValStr<?>> defs = ValStr.fromPlainMap(defaults);
|
||||
DebuggerMethodInvocationDialog dialog = new DebuggerMethodInvocationDialog(tool,
|
||||
method.getDisplay(), method.getDisplay(), null);
|
||||
while (true) {
|
||||
for (ParameterDescription<?> param : method.getParameters().values()) {
|
||||
Object val = defaults.get(param.name);
|
||||
if (val != null) {
|
||||
dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class),
|
||||
val);
|
||||
}
|
||||
}
|
||||
Map<String, ?> args = dialog.promptArguments(method.getParameters());
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
return null;
|
||||
}
|
||||
if (dialog.isResetRequested()) {
|
||||
continue;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
Map<String, ValStr<?>> args = dialog.promptArguments(method.getParameters(), defs, defs);
|
||||
return args == null ? null : ValStr.toPlainMap(args);
|
||||
}
|
||||
|
||||
private CompletableFuture<?> invokeMethod(boolean prompt, TargetMethod method,
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -15,7 +15,7 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.service.model.launch;
|
||||
|
||||
import static ghidra.async.AsyncUtils.*;
|
||||
import static ghidra.async.AsyncUtils.loop;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -44,6 +44,7 @@ import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.util.PathUtils;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.model.DebuggerProgramLaunchOffer;
|
||||
import ghidra.debug.api.model.TraceRecorder;
|
||||
import ghidra.debug.api.modules.*;
|
||||
@ -281,14 +282,14 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
return proposal;
|
||||
}
|
||||
|
||||
private void saveLauncherArgs(Map<String, ?> args,
|
||||
private void saveLauncherArgs(Map<String, ValStr<?>> args,
|
||||
Map<String, ParameterDescription<?>> params) {
|
||||
SaveState state = new SaveState();
|
||||
for (ParameterDescription<?> param : params.values()) {
|
||||
Object val = args.get(param.name);
|
||||
ValStr<?> val = args.get(param.name);
|
||||
if (val != null) {
|
||||
ConfigStateField.putState(state, param.type.asSubclass(Object.class), param.name,
|
||||
val);
|
||||
val.val());
|
||||
}
|
||||
}
|
||||
if (program != null) {
|
||||
@ -316,19 +317,19 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
* @param params the parameters
|
||||
* @return the default arguments
|
||||
*/
|
||||
protected Map<String, ?> generateDefaultLauncherArgs(
|
||||
protected Map<String, ValStr<?>> generateDefaultLauncherArgs(
|
||||
Map<String, ParameterDescription<?>> params) {
|
||||
if (program == null) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, Object> map = new LinkedHashMap<String, Object>();
|
||||
Map<String, ValStr<?>> map = new LinkedHashMap<>();
|
||||
for (Entry<String, ParameterDescription<?>> entry : params.entrySet()) {
|
||||
map.put(entry.getKey(), entry.getValue().defaultValue);
|
||||
map.put(entry.getKey(), ValStr.from(entry.getValue().defaultValue));
|
||||
}
|
||||
String almostExecutablePath = program.getExecutablePath();
|
||||
File f = new File(almostExecutablePath);
|
||||
map.put(TargetCmdLineLauncher.CMDLINE_ARGS_NAME,
|
||||
TargetCmdLineLauncher.quoteImagePathIfSpaces(f.getAbsolutePath()));
|
||||
ValStr.from(TargetCmdLineLauncher.quoteImagePathIfSpaces(f.getAbsolutePath())));
|
||||
return map;
|
||||
}
|
||||
|
||||
@ -338,36 +339,19 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
* @param params the parameters of the model's launcher
|
||||
* @return the arguments given by the user, or null if cancelled
|
||||
*/
|
||||
protected Map<String, ?> promptLauncherArgs(TargetLauncher launcher,
|
||||
protected Map<String, ValStr<?>> promptLauncherArgs(TargetLauncher launcher,
|
||||
LaunchConfigurator configurator) {
|
||||
TargetParameterMap params = launcher.getParameters();
|
||||
DebuggerMethodInvocationDialog dialog =
|
||||
new DebuggerMethodInvocationDialog(tool, getButtonTitle(), "Launch", getIcon());
|
||||
|
||||
// NB. Do not invoke read/writeConfigState
|
||||
Map<String, ?> args;
|
||||
boolean reset = false;
|
||||
do {
|
||||
args = configurator.configureLauncher(launcher,
|
||||
loadLastLauncherArgs(launcher, true), RelPrompt.BEFORE);
|
||||
for (ParameterDescription<?> param : params.values()) {
|
||||
Object val = args.get(param.name);
|
||||
if (val != null) {
|
||||
dialog.setMemorizedArgument(param.name, param.type.asSubclass(Object.class),
|
||||
val);
|
||||
}
|
||||
}
|
||||
args = dialog.promptArguments(params);
|
||||
if (args == null) {
|
||||
// Cancelled
|
||||
return null;
|
||||
}
|
||||
reset = dialog.isResetRequested();
|
||||
if (reset) {
|
||||
args = generateDefaultLauncherArgs(params);
|
||||
}
|
||||
saveLauncherArgs(args, params);
|
||||
}
|
||||
while (reset);
|
||||
|
||||
Map<String, ValStr<?>> defaultArgs = generateDefaultLauncherArgs(params);
|
||||
Map<String, ValStr<?>> lastArgs = configurator.configureLauncher(launcher,
|
||||
loadLastLauncherArgs(launcher, true), RelPrompt.BEFORE);
|
||||
Map<String, ValStr<?>> args = dialog.promptArguments(params, lastArgs, defaultArgs);
|
||||
saveLauncherArgs(args, params);
|
||||
return args;
|
||||
}
|
||||
|
||||
@ -386,7 +370,8 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
* @param forPrompt true if the user will be confirming the arguments
|
||||
* @return the loaded arguments, or defaults
|
||||
*/
|
||||
protected Map<String, ?> loadLastLauncherArgs(TargetLauncher launcher, boolean forPrompt) {
|
||||
protected Map<String, ValStr<?>> loadLastLauncherArgs(TargetLauncher launcher,
|
||||
boolean forPrompt) {
|
||||
/**
|
||||
* TODO: Supposedly, per-program, per-user config stuff is being generalized for analyzers.
|
||||
* Re-examine this if/when that gets merged
|
||||
@ -401,13 +386,13 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
Element element = XmlUtilities.fromString(property);
|
||||
SaveState state = new SaveState(element);
|
||||
List<String> names = List.of(state.getNames());
|
||||
Map<String, Object> args = new LinkedHashMap<>();
|
||||
Map<String, ValStr<?>> args = new LinkedHashMap<>();
|
||||
for (ParameterDescription<?> param : params.values()) {
|
||||
if (names.contains(param.name)) {
|
||||
Object configState =
|
||||
ConfigStateField.getState(state, param.type, param.name);
|
||||
if (configState != null) {
|
||||
args.put(param.name, configState);
|
||||
args.put(param.name, ValStr.from(configState));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -426,7 +411,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
e);
|
||||
}
|
||||
}
|
||||
Map<String, ?> args = generateDefaultLauncherArgs(params);
|
||||
Map<String, ValStr<?>> args = generateDefaultLauncherArgs(params);
|
||||
saveLauncherArgs(args, params);
|
||||
return args;
|
||||
}
|
||||
@ -447,7 +432,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
* @param configurator a means of configuring the launcher
|
||||
* @return the chosen arguments, or null if the user cancels at the prompt
|
||||
*/
|
||||
public Map<String, ?> getLauncherArgs(TargetLauncher launcher, boolean prompt,
|
||||
public Map<String, ValStr<?>> getLauncherArgs(TargetLauncher launcher, boolean prompt,
|
||||
LaunchConfigurator configurator) {
|
||||
return prompt
|
||||
? configurator.configureLauncher(launcher,
|
||||
@ -456,7 +441,7 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
RelPrompt.NONE);
|
||||
}
|
||||
|
||||
public Map<String, ?> getLauncherArgs(TargetLauncher launcher, boolean prompt) {
|
||||
public Map<String, ValStr<?>> getLauncherArgs(TargetLauncher launcher, boolean prompt) {
|
||||
return getLauncherArgs(launcher, prompt, LaunchConfigurator.NOP);
|
||||
}
|
||||
|
||||
@ -541,13 +526,14 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
|
||||
// Eww.
|
||||
protected CompletableFuture<Void> launch(TargetLauncher launcher,
|
||||
boolean prompt, LaunchConfigurator configurator, TaskMonitor monitor) {
|
||||
Map<String, ?> args = getLauncherArgs(launcher, prompt, configurator);
|
||||
Map<String, ValStr<?>> args = getLauncherArgs(launcher, prompt, configurator);
|
||||
if (args == null) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
Map<String, ?> a = ValStr.toPlainMap(args);
|
||||
return AsyncTimer.DEFAULT_TIMER.mark()
|
||||
.timeOut(
|
||||
launcher.launch(args), getTimeoutMillis(), () -> onTimedOutLaunch(monitor));
|
||||
launcher.launch(a), getTimeoutMillis(), () -> onTimedOutLaunch(monitor));
|
||||
}
|
||||
|
||||
protected void checkCancelled(TaskMonitor monitor) {
|
||||
|
@ -33,8 +33,9 @@ public enum MiscellaneousUtils {
|
||||
* Obtain a swing component which may be used to edit the property.
|
||||
*
|
||||
* <p>
|
||||
* This has been shamelessly stolen from {@link EditorState#getEditorComponent()}, which seems
|
||||
* entangled with Ghidra's whole options system. I think this portion could be factored out.
|
||||
* This has was originally stolen from {@link EditorState#getEditorComponent()}, which seems
|
||||
* entangled with Ghidra's whole options system. Can that be factored out? Since then, the two
|
||||
* have drifted apart.
|
||||
*
|
||||
* @param editor the editor for which to obtain an interactive component for editing
|
||||
* @return the component
|
||||
@ -53,16 +54,11 @@ public enum MiscellaneousUtils {
|
||||
return new PropertyText(editor);
|
||||
}
|
||||
|
||||
Class<? extends PropertyEditor> clazz = editor.getClass();
|
||||
String clazzName = clazz.getSimpleName();
|
||||
if (clazzName.startsWith("String")) {
|
||||
// Most likely some kind of string editor with a null value. Just use a string
|
||||
// property and let the value be empty.
|
||||
return new PropertyText(editor);
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Ghidra does not know how to use PropertyEditor: " + editor.getClass().getName());
|
||||
/**
|
||||
* TODO: Would be nice to know the actual type, but alas! Just default to a PropertyText and
|
||||
* hope all goes well.
|
||||
*/
|
||||
return new PropertyText(editor);
|
||||
}
|
||||
|
||||
public static void rigFocusAndEnter(Component c, Runnable runnable) {
|
||||
|
@ -0,0 +1,94 @@
|
||||
/* ###
|
||||
* 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;
|
||||
|
||||
import java.beans.PropertyEditor;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import docking.test.AbstractDockingTest;
|
||||
import ghidra.async.SwingExecutorService;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.framework.options.SaveState;
|
||||
|
||||
public class InvocationDialogHelper<P, D extends AbstractDebuggerParameterDialog<P>> {
|
||||
|
||||
public static <P, D extends AbstractDebuggerParameterDialog<P>> InvocationDialogHelper<P, D> waitFor(
|
||||
Class<D> cls) {
|
||||
D dialog = AbstractDockingTest.waitForDialogComponent(cls);
|
||||
return new InvocationDialogHelper<>(dialog);
|
||||
}
|
||||
|
||||
private final AbstractDebuggerParameterDialog<P> dialog;
|
||||
|
||||
public InvocationDialogHelper(AbstractDebuggerParameterDialog<P> dialog) {
|
||||
this.dialog = dialog;
|
||||
}
|
||||
|
||||
public void dismissWithArguments(Map<String, ValStr<?>> args) {
|
||||
dialog.setMemorizedArguments(args);
|
||||
invoke();
|
||||
}
|
||||
|
||||
public <T> Map.Entry<String, ValStr<T>> entry(String key, T value) {
|
||||
return Map.entry(key, ValStr.from(value));
|
||||
}
|
||||
|
||||
public void setArg(P param, Object value) {
|
||||
PropertyEditor editor = dialog.getEditor(param);
|
||||
runSwing(() -> editor.setValue(value));
|
||||
}
|
||||
|
||||
protected void runSwing(Runnable r) {
|
||||
try {
|
||||
CompletableFuture.runAsync(r, SwingExecutorService.LATER).get();
|
||||
}
|
||||
catch (ExecutionException e) {
|
||||
switch (e.getCause()) {
|
||||
case RuntimeException t -> throw t;
|
||||
case Exception t -> throw new RuntimeException(t);
|
||||
default -> ExceptionUtils.rethrow(e.getCause());
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setArgAsString(P param, String value) {
|
||||
PropertyEditor editor = dialog.getEditor(param);
|
||||
runSwing(() -> editor.setAsText(value));
|
||||
}
|
||||
|
||||
public void invoke() {
|
||||
runSwing(() -> dialog.invoke(null));
|
||||
}
|
||||
|
||||
public SaveState saveState() {
|
||||
SaveState parent = new SaveState();
|
||||
runSwing(() -> dialog.writeConfigState(parent));
|
||||
return parent.getSaveState(AbstractDebuggerParameterDialog.KEY_MEMORIZED_ARGUMENTS);
|
||||
}
|
||||
|
||||
public void loadState(SaveState state) {
|
||||
SaveState parent = new SaveState();
|
||||
parent.putSaveState(AbstractDebuggerParameterDialog.KEY_MEMORIZED_ARGUMENTS, state);
|
||||
runSwing(() -> dialog.readConfigState(parent));
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/* ###
|
||||
* 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.objects.components;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import docking.test.AbstractDockingTest;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.util.Swing;
|
||||
|
||||
public class InvocationDialogHelper {
|
||||
|
||||
public static InvocationDialogHelper waitFor() {
|
||||
DebuggerMethodInvocationDialog dialog =
|
||||
AbstractDockingTest.waitForDialogComponent(DebuggerMethodInvocationDialog.class);
|
||||
return new InvocationDialogHelper(dialog);
|
||||
}
|
||||
|
||||
private final DebuggerMethodInvocationDialog dialog;
|
||||
|
||||
public InvocationDialogHelper(DebuggerMethodInvocationDialog dialog) {
|
||||
this.dialog = dialog;
|
||||
}
|
||||
|
||||
public void dismissWithArguments(Map<String, Object> args) {
|
||||
for (Map.Entry<String, Object> a : args.entrySet()) {
|
||||
ParameterDescription<?> p = dialog.parameters.get(a.getKey());
|
||||
assertNotNull(p);
|
||||
dialog.setMemorizedArgument(a.getKey(), p.type.asSubclass(Object.class), a.getValue());
|
||||
}
|
||||
Swing.runNow(() -> dialog.invoke(null));
|
||||
}
|
||||
}
|
@ -33,6 +33,8 @@ public final class NumericUtilities {
|
||||
|
||||
private final static String HEX_PREFIX_X = "0X";
|
||||
private final static String HEX_PREFIX_x = "0x";
|
||||
private final static String BIN_PREFIX = "0B";
|
||||
private final static String OCT_PREFIX = "0";
|
||||
|
||||
private final static Set<Class<? extends Number>> INTEGER_TYPES = new HashSet<>();
|
||||
static {
|
||||
@ -235,6 +237,49 @@ public final class NumericUtilities {
|
||||
return new BigInteger(s, 16);
|
||||
}
|
||||
|
||||
private static BigInteger decodeMagnitude(int p, String s) {
|
||||
// Special case, so it doesn't get chewed by octal parser
|
||||
if ("0".equals(s)) {
|
||||
return BigInteger.ZERO;
|
||||
}
|
||||
if (s.regionMatches(true, p, HEX_PREFIX_X, 0, HEX_PREFIX_X.length())) {
|
||||
return new BigInteger(s.substring(p + HEX_PREFIX_X.length()), 16);
|
||||
}
|
||||
if (s.regionMatches(true, p, BIN_PREFIX, 0, BIN_PREFIX.length())) {
|
||||
return new BigInteger(s.substring(p + BIN_PREFIX.length()), 2);
|
||||
}
|
||||
// Check last, because prefix is shortest.
|
||||
if (s.regionMatches(true, p, OCT_PREFIX, 0, OCT_PREFIX.length())) {
|
||||
return new BigInteger(s.substring(p + OCT_PREFIX.length()), 8);
|
||||
}
|
||||
return new BigInteger(s.substring(p), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a big integer in hex, binary, octal, or decimal, based on the prefix 0x, 0b, or 0.
|
||||
*
|
||||
* <p>
|
||||
* This checks for the presence of a case-insensitive prefix. 0x denotes hex, 0b denotes binary,
|
||||
* 0 denotes octal. If no prefix is given, decimal is assumed. A sign +/- may immediately
|
||||
* precede the prefix. If no sign is given, a positive value is assumed.
|
||||
*
|
||||
* @param s the string to parse
|
||||
* @return the decoded value
|
||||
*/
|
||||
public static BigInteger decodeBigInteger(String s) {
|
||||
int p = 0;
|
||||
boolean negative = false;
|
||||
if (s.startsWith("+")) {
|
||||
p = 1;
|
||||
}
|
||||
else if (s.startsWith("-")) {
|
||||
p = 1;
|
||||
negative = true;
|
||||
}
|
||||
BigInteger mag = decodeMagnitude(p, s);
|
||||
return negative ? mag.negate() : mag;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the value of the specified long as hexadecimal, prefixing with the
|
||||
* {@link #HEX_PREFIX_x} string.
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -18,6 +18,7 @@ package ghidra.util;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -332,4 +333,89 @@ public class NumericUtilitiesTest {
|
||||
assertEquals(errorMessage, expected[i], actual[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeBigInteger() {
|
||||
// Zero special cases
|
||||
assertEquals(BigInteger.ZERO, NumericUtilities.decodeBigInteger("0"));
|
||||
assertEquals(BigInteger.ZERO, NumericUtilities.decodeBigInteger("000"));
|
||||
// Decimal
|
||||
assertEquals(BigInteger.valueOf(99), NumericUtilities.decodeBigInteger("99"));
|
||||
assertEquals(BigInteger.valueOf(99), NumericUtilities.decodeBigInteger("+99"));
|
||||
assertEquals(BigInteger.valueOf(-99), NumericUtilities.decodeBigInteger("-99"));
|
||||
// Hex
|
||||
assertEquals(BigInteger.valueOf(0x99), NumericUtilities.decodeBigInteger("0x99"));
|
||||
assertEquals(BigInteger.valueOf(0x99), NumericUtilities.decodeBigInteger("+0x99"));
|
||||
assertEquals(BigInteger.valueOf(-0x99), NumericUtilities.decodeBigInteger("-0x99"));
|
||||
// Binary
|
||||
assertEquals(BigInteger.valueOf(0b110), NumericUtilities.decodeBigInteger("0b110"));
|
||||
assertEquals(BigInteger.valueOf(0b110), NumericUtilities.decodeBigInteger("+0b110"));
|
||||
assertEquals(BigInteger.valueOf(-0b110), NumericUtilities.decodeBigInteger("-0b110"));
|
||||
// Octal
|
||||
assertEquals(BigInteger.valueOf(0755), NumericUtilities.decodeBigInteger("0755"));
|
||||
assertEquals(BigInteger.valueOf(0755), NumericUtilities.decodeBigInteger("+0755"));
|
||||
assertEquals(BigInteger.valueOf(-0755), NumericUtilities.decodeBigInteger("-0755"));
|
||||
|
||||
// Errors
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("+");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("-");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("0x");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("0b");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("a01");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("081");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("0x9g");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger(" 10");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
try {
|
||||
NumericUtilities.decodeBigInteger("10 ");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -22,6 +22,7 @@ import org.junit.Test;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.objects.components.DebuggerMethodInvocationDialog;
|
||||
import ghidra.app.plugin.core.terminal.TerminalProvider;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.framework.plugintool.AutoConfigState.PathIsFile;
|
||||
import ghidra.test.ToyProgramBuilder;
|
||||
@ -30,7 +31,8 @@ import help.screenshot.GhidraScreenShotGenerator;
|
||||
public class TraceRmiLauncherServicePluginScreenShots extends GhidraScreenShotGenerator {
|
||||
TraceRmiLauncherServicePlugin servicePlugin;
|
||||
|
||||
protected void captureLauncherByTitle(String title, Map<String, ?> args) throws Throwable {
|
||||
protected void captureLauncherByTitle(String title, Map<String, ValStr<?>> args)
|
||||
throws Throwable {
|
||||
servicePlugin = addPlugin(tool, TraceRmiLauncherServicePlugin.class);
|
||||
|
||||
ToyProgramBuilder pb = new ToyProgramBuilder("demo", false);
|
||||
@ -49,10 +51,13 @@ public class TraceRmiLauncherServicePluginScreenShots extends GhidraScreenShotGe
|
||||
captureDialog(DebuggerMethodInvocationDialog.class);
|
||||
}
|
||||
|
||||
protected ValStr<PathIsFile> fileArg(String path) {
|
||||
return new ValStr<>(new PathIsFile(Paths.get(path)), path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCaptureGdbLauncher() throws Throwable {
|
||||
captureLauncherByTitle("gdb",
|
||||
Map.of("arg:1", new PathIsFile(Paths.get("/home/user/demo"))));
|
||||
captureLauncherByTitle("gdb", Map.of("arg:1", fileArg("/home/user/demo")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -0,0 +1,584 @@
|
||||
/* ###
|
||||
* 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 static org.junit.Assert.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.AbstractTraceRmiLaunchOffer.NullPtyTerminalSession;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.AbstractTraceRmiLaunchOffer.PtyTerminalSession;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ParseException;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
|
||||
import ghidra.app.plugin.core.debug.service.target.DebuggerTargetServicePlugin;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiHandler;
|
||||
import ghidra.app.plugin.core.terminal.TerminalListener;
|
||||
import ghidra.app.services.Terminal;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.TerminalSession;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*;
|
||||
import ghidra.framework.plugintool.util.PluginException;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.pty.*;
|
||||
import ghidra.rmi.trace.TraceRmi.*;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.task.ConsoleTaskMonitor;
|
||||
|
||||
public class ScriptTraceRmiLaunchOfferTest extends AbstractGhidraHeadedDebuggerTest {
|
||||
|
||||
static class TestScriptAttributesParser extends ScriptAttributesParser {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected boolean ignoreLine(int lineNo, String line) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String removeDelimiter(String line) {
|
||||
return line;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reportError(String message) {
|
||||
super.reportError(message);
|
||||
errors.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
static ScriptAttributes parse(String header, String name) throws ParseException {
|
||||
try {
|
||||
TestScriptAttributesParser parser = new TestScriptAttributesParser();
|
||||
ScriptAttributes attributes =
|
||||
parser.parseStream(new ByteArrayInputStream(header.getBytes()), name);
|
||||
if (!parser.errors.isEmpty()) {
|
||||
throw new ParseException(null, parser.errors.toString());
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
record Config(Map<String, ValStr<?>> args) implements LaunchConfigurator {
|
||||
public static final Config DEFAULTS = new Config(Map.of());
|
||||
|
||||
@Override
|
||||
public PromptMode getPromptMode() {
|
||||
return PromptMode.NEVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ValStr<?>> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ValStr<?>> arguments, RelPrompt relPrompt) {
|
||||
Map<String, ValStr<?>> mod = new HashMap<>(arguments);
|
||||
mod.putAll(args);
|
||||
return mod;
|
||||
}
|
||||
}
|
||||
|
||||
record MockTerminal() implements Terminal {
|
||||
@Override
|
||||
public void addTerminalListener(TerminalListener listener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeTerminalListener(TerminalListener listener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectDisplayOutput(ByteBuffer bb) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubTitle(String title) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSubTitle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFixedSize(short cols, short rows) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDynamicSize() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxScrollBackRows(int rows) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumns() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRows() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollBackRows() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFullText() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLineText(int line) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRangeText(int startCol, int startLine, int endCol, int endLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCursorRow() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCursorColumn() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminated() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTerminateAction(Runnable action) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toFront() {
|
||||
}
|
||||
}
|
||||
|
||||
record MockPtySession() implements PtySession {
|
||||
@Override
|
||||
public int waitExited() throws InterruptedException {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int waitExited(long timeout, TimeUnit unit)
|
||||
throws InterruptedException, TimeoutException {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyForcibly() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
record MockPty() implements Pty {
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PtyParent getParent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PtyChild getChild() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
}
|
||||
|
||||
File nameTempFile() {
|
||||
return Paths.get(getTestDirectoryPath(), name.getMethodName()).toFile();
|
||||
}
|
||||
|
||||
static class MockClient extends Thread implements AutoCloseable {
|
||||
private static final DomObjId TRACE_ID = DomObjId.newBuilder().setId(0).build();
|
||||
private final SocketAddress addr;
|
||||
private final String name;
|
||||
|
||||
private Throwable exc;
|
||||
|
||||
Socket s;
|
||||
OutputStream out;
|
||||
InputStream in;
|
||||
|
||||
public MockClient(SocketAddress addr, String name) {
|
||||
setDaemon(true);
|
||||
this.addr = addr;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
void send(RootMessage msg) throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
buf.putInt(msg.getSerializedSize());
|
||||
out.write(buf.array());
|
||||
msg.writeTo(out);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
RootMessage recv() throws IOException {
|
||||
int len = ByteBuffer.wrap(in.readNBytes(Integer.BYTES)).getInt();
|
||||
return RootMessage.parseFrom(in.readNBytes(len));
|
||||
}
|
||||
|
||||
void completeNegotiation() throws IOException {
|
||||
send(RootMessage.newBuilder()
|
||||
.setRequestNegotiate(RequestNegotiate.newBuilder()
|
||||
.setVersion(TraceRmiHandler.VERSION)
|
||||
.setDescription("Mock Client"))
|
||||
.build());
|
||||
Msg.debug(this, "Sent negotation request");
|
||||
RootMessage reply = recv();
|
||||
Msg.debug(this, "Received: " + reply);
|
||||
assertNotNull(reply.getReplyNegotiate());
|
||||
}
|
||||
|
||||
void createTrace() throws IOException {
|
||||
send(RootMessage.newBuilder()
|
||||
.setRequestCreateTrace(RequestCreateTrace.newBuilder()
|
||||
.setOid(TRACE_ID)
|
||||
.setLanguage(Language.newBuilder().setId("Toy:BE:64:default"))
|
||||
.setCompiler(Compiler.newBuilder().setId("default"))
|
||||
.setPath(FilePath.newBuilder().setPath(name)))
|
||||
.build());
|
||||
Msg.debug(this, "Sent createTrace request");
|
||||
RootMessage reply = recv();
|
||||
Msg.debug(this, "Received: " + reply);
|
||||
assertNotNull(reply.getReplyCreateTrace());
|
||||
}
|
||||
|
||||
protected void doRun() throws Throwable {
|
||||
s = new Socket();
|
||||
s.connect(addr);
|
||||
out = s.getOutputStream();
|
||||
in = s.getInputStream();
|
||||
|
||||
completeNegotiation();
|
||||
|
||||
createTrace();
|
||||
|
||||
s.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
doRun();
|
||||
}
|
||||
catch (Throwable e) {
|
||||
Msg.error(this, "Mock client crashed", e);
|
||||
this.exc = e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
join(1000);
|
||||
if (exc != null) {
|
||||
throw new RuntimeException("Exception in mock client", exc);
|
||||
}
|
||||
assertFalse(isAlive());
|
||||
}
|
||||
}
|
||||
|
||||
class TestScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer {
|
||||
int nextNullId = 0;
|
||||
|
||||
public TestScriptTraceRmiLaunchOffer(Program program, String header) throws ParseException {
|
||||
super(launchPlugin, program, nameTempFile(), name.getMethodName(),
|
||||
parse(header, name.getMethodName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NullPtyTerminalSession nullPtyTerminal() throws IOException {
|
||||
return new NullPtyTerminalSession(new MockTerminal(), new MockPty(),
|
||||
"null-" + (++nextNullId));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PtyTerminalSession runInTerminal(List<String> commandLine,
|
||||
Map<String, String> env, File workingDirectory,
|
||||
Collection<TerminalSession> subordinates) throws IOException {
|
||||
String host = env.get(ScriptAttributesParser.ENV_GHIDRA_TRACE_RMI_HOST);
|
||||
int port = Integer.parseInt(env.get(ScriptAttributesParser.ENV_GHIDRA_TRACE_RMI_PORT));
|
||||
// The plugin is waiting for a connection. Have to satisfy it to move on.
|
||||
client = new MockClient(new InetSocketAddress(host, port), name.getMethodName());
|
||||
client.start();
|
||||
return new PtyTerminalSession(new MockTerminal(), new MockPty(), new MockPtySession(),
|
||||
client);
|
||||
}
|
||||
}
|
||||
|
||||
TraceRmiLauncherServicePlugin launchPlugin;
|
||||
|
||||
MockClient client;
|
||||
|
||||
record ResultAndClient(LaunchResult result, MockClient client) implements AutoCloseable {
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
client.close();
|
||||
result.close();
|
||||
}
|
||||
|
||||
public PtyTerminalSession mockSession() {
|
||||
return new PtyTerminalSession(new MockTerminal(), new MockPty(), new MockPtySession(),
|
||||
client);
|
||||
}
|
||||
|
||||
public NullPtyTerminalSession mockNull(String name) {
|
||||
return new NullPtyTerminalSession(new MockTerminal(), new MockPty(), name);
|
||||
}
|
||||
}
|
||||
|
||||
ResultAndClient launchNoErr(TraceRmiLaunchOffer offer, Map<String, ValStr<?>> args)
|
||||
throws Throwable {
|
||||
LaunchResult result = offer.launchProgram(new ConsoleTaskMonitor(), new Config(args));
|
||||
if (result.exception() != null) {
|
||||
throw (result.exception());
|
||||
}
|
||||
return new ResultAndClient(result, client);
|
||||
}
|
||||
|
||||
ResultAndClient launchNoErr(TraceRmiLaunchOffer offer) throws Throwable {
|
||||
return launchNoErr(offer, Map.of());
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setupOfferTest() throws PluginException {
|
||||
// BUG: Seems I shouldn't have to do this. It's in servicesRequired (transitive)
|
||||
addPlugin(tool, DebuggerTargetServicePlugin.class);
|
||||
launchPlugin = addPlugin(tool, TraceRmiLauncherServicePlugin.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTitleOnly() throws Throwable {
|
||||
createProgram();
|
||||
TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(program, """
|
||||
@title Test
|
||||
""");
|
||||
|
||||
try (ResultAndClient rac = launchNoErr(offer)) {
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("Shell", rac.mockSession())),
|
||||
rac.result.sessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTtyAlways() throws Throwable {
|
||||
TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@tty TTY_TARGET
|
||||
""");
|
||||
try (ResultAndClient rac = launchNoErr(offer)) {
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("Shell", rac.mockSession()),
|
||||
Map.entry("TTY_TARGET", rac.mockNull("null-1"))),
|
||||
rac.result.sessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTtyCondBoolFalse() throws Throwable {
|
||||
TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@env OPT_EXTRA_TTY:bool=false "Extra TTY" "Provide a separate tty."
|
||||
@tty TTY_TARGET if env:OPT_EXTRA_TTY
|
||||
""");
|
||||
try (ResultAndClient rac = launchNoErr(offer)) {
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("Shell", rac.mockSession())),
|
||||
rac.result.sessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTtyCondBoolTrue() throws Throwable {
|
||||
TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@env OPT_EXTRA_TTY:bool=false "Extra TTY" "Provide a separate tty."
|
||||
@tty TTY_TARGET if env:OPT_EXTRA_TTY
|
||||
""");
|
||||
try (ResultAndClient rac = launchNoErr(offer, Map.of(
|
||||
"env:OPT_EXTRA_TTY", ValStr.from(true)))) {
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("Shell", rac.mockSession()),
|
||||
Map.entry("TTY_TARGET", rac.mockNull("null-1"))),
|
||||
rac.result.sessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = ParseException.class)
|
||||
public void testTtyCondBoolTypeMismatch() throws Throwable {
|
||||
new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@env OPT_SOME_INT:int=0 "An integer" "Just an option for testing."
|
||||
@tty TTY_TARGET if env:OPT_SOME_INT
|
||||
""");
|
||||
}
|
||||
|
||||
@Test(expected = ParseException.class)
|
||||
public void testTtyCondBoolNoSuch() throws Throwable {
|
||||
new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@tty TTY_TARGET if env:NO_SUCH
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTtyCondStrEqFalse() throws Throwable {
|
||||
TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@env OPT_EXTRA_TTY:str="No" "Extra TTY" "Provide a separate tty."
|
||||
@tty TTY_TARGET if env:OPT_EXTRA_TTY == "Yes"
|
||||
""");
|
||||
try (ResultAndClient rac = launchNoErr(offer)) {
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("Shell", rac.mockSession())),
|
||||
rac.result.sessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTtyCondStrEqTrue() throws Throwable {
|
||||
TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@env OPT_EXTRA_TTY:str="No" "Extra TTY" "Provide a separate tty."
|
||||
@tty TTY_TARGET if env:OPT_EXTRA_TTY == "Yes"
|
||||
""");
|
||||
try (ResultAndClient rac = launchNoErr(offer, Map.of(
|
||||
"env:OPT_EXTRA_TTY", ValStr.str("Yes")))) {
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("Shell", rac.mockSession()),
|
||||
Map.entry("TTY_TARGET", rac.mockNull("null-1"))),
|
||||
rac.result.sessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = ParseException.class)
|
||||
public void testTtyCondStrEqNoSuch() throws Throwable {
|
||||
new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@tty TTY_TARGET if env:NO_SUCH == "Yes"
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTtyCondIntEqFalse() throws Throwable {
|
||||
TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@env OPT_EXTRA_TTY:int=0 "Extra TTY" "Provide a separate tty."
|
||||
@tty TTY_TARGET if env:OPT_EXTRA_TTY == 6
|
||||
""");
|
||||
try (ResultAndClient rac = launchNoErr(offer)) {
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("Shell", rac.mockSession())),
|
||||
rac.result.sessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTtyCondIntEqTrue() throws Throwable {
|
||||
TraceRmiLaunchOffer offer = new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@env OPT_EXTRA_TTY:int=0 "Extra TTY" "Provide a separate tty."
|
||||
@tty TTY_TARGET if env:OPT_EXTRA_TTY == 0b110
|
||||
""");
|
||||
try (ResultAndClient rac = launchNoErr(offer, Map.of(
|
||||
"env:OPT_EXTRA_TTY", ValStr.from(BigInteger.valueOf(6))))) {
|
||||
assertEquals(Map.ofEntries(
|
||||
Map.entry("Shell", rac.mockSession()),
|
||||
Map.entry("TTY_TARGET", rac.mockNull("null-1"))),
|
||||
rac.result.sessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = ParseException.class)
|
||||
public void testTtyCondIntEqParseErr() throws Throwable {
|
||||
new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@env OPT_SOME_INT:int=0 "An integer" "Just an option for testing."
|
||||
@tty TTY_TARGET if env:OPT_SOME_INT == "Yes"
|
||||
""");
|
||||
}
|
||||
|
||||
@Test(expected = ParseException.class)
|
||||
public void testTtyCondIntEqNoSuch() throws Throwable {
|
||||
new TestScriptTraceRmiLaunchOffer(null, """
|
||||
@title Test
|
||||
@no-image
|
||||
@tty TTY_TARGET if env:NO_SUCH == 6
|
||||
""");
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -20,9 +20,8 @@ import static org.junit.Assert.assertEquals;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.debug.api.tracermi.TerminalSession;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.*;
|
||||
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
@ -30,9 +29,9 @@ import ghidra.util.task.TaskMonitor;
|
||||
public class TestTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion {
|
||||
|
||||
public static class TestTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer {
|
||||
private static final ParameterDescription<String> PARAM_DESC_IMAGE =
|
||||
ParameterDescription.create(String.class, "image", true, "",
|
||||
PARAM_DISPLAY_IMAGE, "Image to execute");
|
||||
private static final LaunchParameter<String> PARAM_IMAGE =
|
||||
LaunchParameter.create(String.class, "image", PARAM_DISPLAY_IMAGE, "Image to execute",
|
||||
true, ValStr.str(""), str -> str);
|
||||
|
||||
public TestTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program) {
|
||||
super(plugin, program);
|
||||
@ -58,8 +57,8 @@ public class TestTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ParameterDescription<?>> getParameters() {
|
||||
return Map.ofEntries(Map.entry(PARAM_DESC_IMAGE.name, PARAM_DESC_IMAGE));
|
||||
public Map<String, LaunchParameter<?>> getParameters() {
|
||||
return LaunchParameter.mapOf(PARAM_IMAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,19 +68,19 @@ public class TestTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion {
|
||||
|
||||
@Override
|
||||
protected void launchBackEnd(TaskMonitor monitor, Map<String, TerminalSession> sessions,
|
||||
Map<String, ?> args, SocketAddress address) throws Exception {
|
||||
Map<String, ValStr<?>> args, SocketAddress address) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LaunchResult launchProgram(TaskMonitor monitor, LaunchConfigurator configurator) {
|
||||
assertEquals(PromptMode.NEVER, configurator.getPromptMode());
|
||||
Map<String, ?> args =
|
||||
Map<String, ValStr<?>> args =
|
||||
configurator.configureLauncher(this, loadLastLauncherArgs(false), RelPrompt.NONE);
|
||||
return new LaunchResult(program, null, null, null, null,
|
||||
new RuntimeException("Test launcher cannot launch " + args.get("image")));
|
||||
new RuntimeException("Test launcher cannot launch " + PARAM_IMAGE.get(args).val()));
|
||||
}
|
||||
|
||||
public void saveLauncherArgs(Map<String, ?> args) {
|
||||
public void saveLauncherArgs(Map<String, ValStr<?>> args) {
|
||||
super.saveLauncherArgs(args, getParameters());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,255 @@
|
||||
/* ###
|
||||
* 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 static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.junit.*;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
|
||||
import ghidra.app.plugin.core.debug.gui.InvocationDialogHelper;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.BaseType;
|
||||
import ghidra.async.SwingExecutorService;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.LaunchParameter;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.AutoConfigState.PathIsDir;
|
||||
import ghidra.framework.plugintool.AutoConfigState.PathIsFile;
|
||||
|
||||
public class TraceRmiLaunchDialogTest extends AbstractGhidraHeadedDebuggerTest {
|
||||
private static final LaunchParameter<String> PARAM_STRING =
|
||||
BaseType.STRING.createParameter("some_string", "A String", "A string",
|
||||
true, ValStr.str("Hello"));
|
||||
private static final LaunchParameter<BigInteger> PARAM_INT =
|
||||
BaseType.INT.createParameter("some_int", "An Int", "An integer",
|
||||
true, intVal(99));
|
||||
private static final LaunchParameter<Boolean> PARAM_BOOL =
|
||||
BaseType.BOOL.createParameter("some_bool", "A Bool", "A boolean",
|
||||
true, ValStr.from(true));
|
||||
private static final LaunchParameter<Path> PARAM_PATH =
|
||||
BaseType.PATH.createParameter("some_path", "A Path", "A path",
|
||||
true, pathVal("my_path"));
|
||||
private static final LaunchParameter<PathIsDir> PARAM_DIR =
|
||||
BaseType.DIR.createParameter("some_dir", "A Dir", "A directory",
|
||||
true, dirVal("my_dir"));
|
||||
private static final LaunchParameter<PathIsFile> PARAM_FILE =
|
||||
BaseType.FILE.createParameter("some_file", "A File", "A file",
|
||||
true, fileVal("my_file"));
|
||||
|
||||
private TraceRmiLaunchDialog dialog;
|
||||
|
||||
@Before
|
||||
public void setupRmiLaunchDialogTest() throws Exception {
|
||||
dialog = new TraceRmiLaunchDialog(tool, "Launch Test", "Launch", null);
|
||||
}
|
||||
|
||||
record PromptResult(CompletableFuture<Map<String, ValStr<?>>> args,
|
||||
InvocationDialogHelper<LaunchParameter<?>, ?> h) {}
|
||||
|
||||
protected PromptResult prompt(LaunchParameter<?>... params) {
|
||||
CompletableFuture<Map<String, ValStr<?>>> args = CompletableFuture.supplyAsync(
|
||||
() -> dialog.promptArguments(LaunchParameter.mapOf(params), Map.of(), Map.of()),
|
||||
SwingExecutorService.LATER);
|
||||
InvocationDialogHelper<LaunchParameter<?>, ?> helper =
|
||||
InvocationDialogHelper.waitFor(TraceRmiLaunchDialog.class);
|
||||
return new PromptResult(args, helper);
|
||||
}
|
||||
|
||||
static ValStr<BigInteger> intVal(long val, String str) {
|
||||
return new ValStr<>(BigInteger.valueOf(val), str);
|
||||
}
|
||||
|
||||
static ValStr<BigInteger> intVal(long val) {
|
||||
return ValStr.from(BigInteger.valueOf(val));
|
||||
}
|
||||
|
||||
static ValStr<Path> pathVal(String path) {
|
||||
return new ValStr<>(Paths.get(path), path);
|
||||
}
|
||||
|
||||
static ValStr<PathIsDir> dirVal(String path) {
|
||||
return new ValStr<>(PathIsDir.fromString(path), path);
|
||||
}
|
||||
|
||||
static ValStr<PathIsFile> fileVal(String path) {
|
||||
return new ValStr<>(PathIsFile.fromString(path), path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringDefaultValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_STRING);
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_string", ValStr.str("Hello")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringInputValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_STRING);
|
||||
result.h.setArgAsString(PARAM_STRING, "World");
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_string", ValStr.str("World")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntDefaultValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_INT);
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_int", intVal(99)), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntInputHexValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_INT);
|
||||
result.h.setArgAsString(PARAM_INT, "0x11");
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_int", intVal(17, "0x11")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntInputHexValueIncomplete() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_INT);
|
||||
try {
|
||||
result.h.setArgAsString(PARAM_INT, "0x");
|
||||
fail();
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
// pass
|
||||
}
|
||||
result.h.invoke();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntSaveHexValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_INT);
|
||||
result.h.setArgAsString(PARAM_INT, "0x11");
|
||||
result.h.invoke();
|
||||
|
||||
SaveState state = result.h.saveState();
|
||||
assertEquals("0x11", state.getString("some_int,java.math.BigInteger", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testIntLoadHexValue() throws Throwable {
|
||||
/**
|
||||
* TODO: This is a bit out of order. However, the dialog cannot load/decode from the state
|
||||
* until it has the parameters. Worse, to check that user input was valid, the dialog
|
||||
* verifies that the value it gets back matches the text in the box, because if it doesn't,
|
||||
* then the editor must have failed to parse/decode the value. Currently, loading the state
|
||||
* while the dialog box has already populated its values, does not modify the contents of
|
||||
* any editor, so the text <em>will not</em> match, causing this test to fail.
|
||||
*/
|
||||
PromptResult result = prompt(PARAM_INT);
|
||||
SaveState state = new SaveState();
|
||||
state.putString("some_int,java.math.BigInteger", "0x11");
|
||||
result.h.loadState(state);
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_int", intVal(17, "0x11")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBoolDefaultValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_BOOL);
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_bool", ValStr.from(true)), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBoolInputValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_BOOL);
|
||||
result.h.setArg(PARAM_BOOL, false);
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_bool", ValStr.from(false)), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPathDefaultValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_PATH);
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_path", pathVal("my_path")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPathInputValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_PATH);
|
||||
result.h.setArgAsString(PARAM_PATH, "your_path");
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_path", pathVal("your_path")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirDefaultValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_DIR);
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_dir", dirVal("my_dir")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirInputValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_DIR);
|
||||
result.h.setArgAsString(PARAM_DIR, "your_dir");
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_dir", dirVal("your_dir")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileDefaultValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_FILE);
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_file", fileVal("my_file")), args);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileInputValue() throws Throwable {
|
||||
PromptResult result = prompt(PARAM_FILE);
|
||||
result.h.setArgAsString(PARAM_FILE, "your_file");
|
||||
result.h.invoke();
|
||||
|
||||
Map<String, ValStr<?>> args = waitOn(result.args);
|
||||
assertEquals(Map.of("some_file", fileVal("your_file")), args);
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -28,6 +28,7 @@ import org.junit.Test;
|
||||
import db.Transaction;
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
|
||||
import ghidra.app.services.TraceRmiLauncherService;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*;
|
||||
import ghidra.framework.OperatingSystem;
|
||||
@ -57,11 +58,11 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
|
||||
protected LaunchConfigurator gdbFileOnly(String file) {
|
||||
return new LaunchConfigurator() {
|
||||
@Override
|
||||
public Map<String, ?> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ?> arguments, RelPrompt relPrompt) {
|
||||
Map<String, Object> args = new HashMap<>(arguments);
|
||||
args.put("arg:1", new PathIsFile(Paths.get(file)));
|
||||
args.put("env:OPT_START_CMD", "starti");
|
||||
public Map<String, ValStr<?>> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ValStr<?>> arguments, RelPrompt relPrompt) {
|
||||
Map<String, ValStr<?>> args = new HashMap<>(arguments);
|
||||
args.put("arg:1", new ValStr<>(new PathIsFile(Paths.get(file)), file));
|
||||
args.put("env:OPT_START_CMD", ValStr.str("starti"));
|
||||
return args;
|
||||
}
|
||||
};
|
||||
@ -93,10 +94,10 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
|
||||
protected LaunchConfigurator dbgengFileOnly(String file) {
|
||||
return new LaunchConfigurator() {
|
||||
@Override
|
||||
public Map<String, ?> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ?> arguments, RelPrompt relPrompt) {
|
||||
Map<String, Object> args = new HashMap<>(arguments);
|
||||
args.put("env:OPT_TARGET_IMG", new PathIsFile(Paths.get(file)));
|
||||
public Map<String, ValStr<?>> configureLauncher(TraceRmiLaunchOffer offer,
|
||||
Map<String, ValStr<?>> arguments, RelPrompt relPrompt) {
|
||||
Map<String, ValStr<?>> args = new HashMap<>(arguments);
|
||||
args.put("env:OPT_TARGET_IMG", new ValStr<>(new PathIsFile(Paths.get(file)), file));
|
||||
return args;
|
||||
}
|
||||
};
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -31,6 +31,7 @@ import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TestTraceRmiLaunchOpin
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.TraceRmiLauncherServicePlugin;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TestTraceRmiConnection.TestRemoteMethod;
|
||||
import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiTarget;
|
||||
import ghidra.debug.api.ValStr;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.LaunchResult;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.program.model.lang.RegisterValue;
|
||||
@ -182,7 +183,7 @@ public class FlatDebuggerRmiAPITest extends AbstractLiveFlatDebuggerAPITest<Flat
|
||||
|
||||
TestTraceRmiLaunchOffer offer =
|
||||
Unique.assertOne(filter(api.getLaunchOffers(), TestTraceRmiLaunchOffer.class));
|
||||
offer.saveLauncherArgs(Map.of("image", "/test/image"));
|
||||
offer.saveLauncherArgs(Map.of("image", ValStr.str("/test/image")));
|
||||
|
||||
assertEquals(List.of(offer), api.getSavedLaunchOffers());
|
||||
}
|
||||
@ -191,7 +192,7 @@ public class FlatDebuggerRmiAPITest extends AbstractLiveFlatDebuggerAPITest<Flat
|
||||
public void testLaunchCustomCommandLine() throws Throwable {
|
||||
TestTraceRmiLaunchOffer offer =
|
||||
Unique.assertOne(filter(api.getLaunchOffers(), TestTraceRmiLaunchOffer.class));
|
||||
offer.saveLauncherArgs(Map.of("image", "/test/image"));
|
||||
offer.saveLauncherArgs(Map.of("image", ValStr.str("/test/image")));
|
||||
|
||||
LaunchResult result = api.launch(monitor);
|
||||
assertEquals("Test launcher cannot launch /test/image", result.exception().getMessage());
|
||||
|
Loading…
Reference in New Issue
Block a user