GP-785: Prompting for connection parameters, too.

This commit is contained in:
Dan
2021-04-27 12:48:03 -04:00
parent 88c94f16a6
commit ca01260eeb
12 changed files with 222 additions and 119 deletions
@@ -20,23 +20,22 @@ import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.ModelActivatedPluginEvent;
import ghidra.app.services.DebuggerModelService;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginStatus;
@PluginInfo( //
shortDescription = "Debugger targets manager", //
description = "GUI to manage connections to external debuggers and trace recording", //
category = PluginCategoryNames.DEBUGGER, //
packageName = DebuggerPluginPackage.NAME, //
status = PluginStatus.RELEASED, //
eventsConsumed = {
ModelActivatedPluginEvent.class, //
}, //
servicesRequired = { //
DebuggerModelService.class, //
} //
shortDescription = "Debugger targets manager", //
description = "GUI to manage connections to external debuggers and trace recording", //
category = PluginCategoryNames.DEBUGGER, //
packageName = DebuggerPluginPackage.NAME, //
status = PluginStatus.RELEASED, //
eventsConsumed = {
ModelActivatedPluginEvent.class, //
}, //
servicesRequired = { //
DebuggerModelService.class, //
} //
)
public class DebuggerTargetsPlugin extends AbstractDebuggerPlugin {
@AutoServiceConsumed
@@ -68,14 +67,4 @@ public class DebuggerTargetsPlugin extends AbstractDebuggerPlugin {
provider.modelActivated(evt.getActiveModel());
}
}
@Override
public void writeConfigState(SaveState saveState) {
provider.writeConfigState(saveState);
}
@Override
public void readConfigState(SaveState saveState) {
provider.readConfigState(saveState);
}
}
@@ -35,7 +35,6 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.services.DebuggerModelService;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
@@ -106,8 +105,9 @@ public class DebuggerTargetsProvider extends ComponentProviderAdapter {
@Override
public void actionPerformed(ActionContext context) {
connectDialog.reset();
tool.showDialog(connectDialog);
// NB. Drop the future on the floor, because the UI will report issues.
// Cancellation should be ignored.
modelService.showConnectDialog();
}
@Override
@@ -163,8 +163,6 @@ public class DebuggerTargetsProvider extends ComponentProviderAdapter {
protected GTree tree;
protected DebuggerConnectionsNode rootNode;
protected DebuggerConnectDialog connectDialog = new DebuggerConnectDialog();
ConnectAction actionConnect;
DisconnectAction actionDisconnect;
DockingAction actionDisconnectAll;
@@ -267,7 +265,6 @@ public class DebuggerTargetsProvider extends ComponentProviderAdapter {
rootNode = new DebuggerConnectionsNode(modelService, this);
tree.setRootNode(rootNode);
}
connectDialog.setModelService(modelService);
}
protected void updateTree(boolean select, Object obj) {
@@ -304,12 +301,4 @@ public class DebuggerTargetsProvider extends ComponentProviderAdapter {
model.invalidateAllLocalCaches();
}
}
public void writeConfigState(SaveState saveState) {
connectDialog.writeConfigState(saveState);
}
public void readConfigState(SaveState saveState) {
connectDialog.readConfigState(saveState);
}
}
@@ -108,12 +108,17 @@ public class GdbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
}
@Override
public String getMenuTitle() {
public String getQuickTitle() {
Map<String, Property<?>> opts = factory.getOptions();
return String.format("in GDB via ssh:%s@%s",
opts.get("SSH username").getValue(),
opts.get("SSH hostname").getValue());
}
@Override
public String getMenuTitle() {
return "in GDB via ssh";
}
}
@Override
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.target;
package ghidra.app.plugin.core.debug.service.model;
import java.awt.*;
import java.awt.event.ActionEvent;
@@ -86,6 +86,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
protected JButton connectButton;
protected CompletableFuture<? extends DebuggerObjectModel> futureConnect;
protected CompletableFuture<DebuggerObjectModel> result;
protected static class FactoryEntry {
DebuggerModelFactory factory;
@@ -236,11 +237,21 @@ public class DebuggerConnectDialog extends DialogComponentProvider
synchronized (this) {
futureConnect = factory.build();
}
futureConnect.thenAcceptAsync(model -> {
modelService.addModel(model);
futureConnect.thenAcceptAsync(m -> {
modelService.addModel(m);
setStatusText("");
close();
modelService.activateModel(model);
modelService.activateModel(m);
synchronized (this) {
/**
* NB. Errors will typically be reported, the dialog stays up, and the user is given
* an opportunity to rectify the failure. Thus, errors should not be used to
* complete the result exceptionally. Only catastrophic errors and cancellation
* should affect the result.
*/
result.completeAsync(() -> m);
result = null;
}
}, SwingExecutorService.INSTANCE).exceptionally(e -> {
e = AsyncUtils.unwrapThrowable(e);
if (!(e instanceof CancellationException)) {
@@ -261,12 +272,31 @@ public class DebuggerConnectDialog extends DialogComponentProvider
if (futureConnect != null) {
futureConnect.cancel(false);
}
if (result != null) {
result.cancel(false);
}
super.cancelCallback();
}
protected void reset() {
protected synchronized CompletableFuture<DebuggerObjectModel> reset(
DebuggerModelFactory factory) {
if (factory != null) {
synchronized (factories) {
dropdownModel.setSelectedItem(factories.get(factory));
}
dropdown.setEnabled(false);
}
else {
dropdown.setEnabled(true);
}
if (result != null) {
result.cancel(false);
}
result = new CompletableFuture<>();
setStatusText("");
connectButton.setEnabled(true);
return result;
}
protected void syncOptionsEnabled() {
@@ -201,7 +201,10 @@ public class DebuggerModelServicePlugin extends Plugin
protected final ChangeListener classChangeListener = new ChangeListenerForFactoryInstances();
protected final ListenerOnRecorders listenerOnRecorders = new ListenerOnRecorders();
DebuggerSelectMappingOfferDialog offerDialog = new DebuggerSelectMappingOfferDialog();
protected final DebuggerSelectMappingOfferDialog offerDialog =
new DebuggerSelectMappingOfferDialog();
protected final DebuggerConnectDialog connectDialog = new DebuggerConnectDialog();
DockingAction actionDisconnectAll;
protected DebuggerObjectModel currentModel;
@@ -211,6 +214,7 @@ public class DebuggerModelServicePlugin extends Plugin
ClassSearcher.addChangeListener(classChangeListener);
refreshFactoryInstances();
connectDialog.setModelService(this);
}
@Override
@@ -653,6 +657,7 @@ public class DebuggerModelServicePlugin extends Plugin
factory.writeConfigState(factoryState);
saveState.putXmlElement(stateName, factoryState.saveToXml());
}
connectDialog.writeConfigState(saveState);
}
@Override
@@ -665,6 +670,7 @@ public class DebuggerModelServicePlugin extends Plugin
factory.readConfigState(factoryState);
}
}
connectDialog.readConfigState(saveState);
}
@Override
@@ -673,4 +679,16 @@ public class DebuggerModelServicePlugin extends Plugin
.stream()
.flatMap(opinion -> opinion.getOffers(program, tool, this).stream());
}
protected CompletableFuture<DebuggerObjectModel> doShowConnectDialog(PluginTool tool,
DebuggerModelFactory factory) {
CompletableFuture<DebuggerObjectModel> future = connectDialog.reset(factory);
tool.showDialog(connectDialog);
return future;
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory) {
return doShowConnectDialog(tool, factory);
}
}
@@ -18,10 +18,13 @@ package ghidra.app.plugin.core.debug.service.model;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.exception.ExceptionUtils;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.builder.MultiStateActionBuilder;
@@ -57,6 +60,7 @@ import ghidra.util.Msg;
import ghidra.util.database.UndoableTransaction;
import ghidra.util.datastruct.CollectionChangeListener;
import ghidra.util.datastruct.ListenerSet;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@PluginInfo( //
@@ -94,12 +98,17 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
@Override
public String getMenuParentTitle() {
return null;
return "";
}
@Override
public String getMenuTitle() {
return null;
return "";
}
@Override
public String getQuickTitle() {
return "";
}
@Override
@@ -256,6 +265,11 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
closeAllModels();
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory) {
return delegate.doShowConnectDialog(tool, factory);
}
@Override
public Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program) {
return orderOffers(delegate.getProgramLaunchOffers(program), program);
@@ -325,7 +339,15 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
Msg.error(this, "Trouble writing recent launches to program user data");
return null;
});
return offer.launchProgram(m, prompt);
return offer.launchProgram(m, prompt).exceptionally(ex -> {
Throwable t = AsyncUtils.unwrapThrowable(ex);
if (t instanceof CancellationException || t instanceof CancelledException) {
return null;
}
return ExceptionUtils.rethrow(ex);
}).whenCompleteAsync((v, e) -> {
updateActionDebugProgram();
}, AsyncUtils.SWING_EXECUTOR);
});
}
@@ -251,15 +251,23 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg
}
}
protected CompletableFuture<DebuggerObjectModel> connect(boolean prompt) {
DebuggerModelService service = tool.getService(DebuggerModelService.class);
DebuggerModelFactory factory = getModelFactory();
if (prompt) {
return service.showConnectDialog(factory);
}
return factory.build().thenApplyAsync(m -> {
service.addModel(m);
return m;
});
}
@Override
public CompletableFuture<Void> launchProgram(TaskMonitor monitor, boolean prompt) {
monitor.initialize(2);
monitor.setMessage("Connecting");
return getModelFactory().build().thenApplyAsync(m -> {
DebuggerModelService service = tool.getService(DebuggerModelService.class);
service.addModel(m);
return m;
}).thenComposeAsync(m -> {
return connect(prompt).thenComposeAsync(m -> {
List<String> launcherPath = getLauncherPath();
TargetObjectSchema schema = m.getRootSchema().getSuccessorSchema(launcherPath);
if (!schema.getInterfaces().contains(TargetLauncher.class)) {
@@ -85,13 +85,26 @@ public interface DebuggerProgramLaunchOffer {
*/
String getMenuTitle();
/**
* Get the text displayed if the user will not be prompted
*
* <p>
* Sometimes when "the last options" are being used without prompting, it's a good idea to
* remind the user what those options were.
*
* @return the title
*/
default String getQuickTitle() {
return getMenuTitle();
}
/**
* Get the text displayed on buttons for this offer
*
* @return the title
*/
default String getButtonTitle() {
return getMenuParentTitle() + " " + getMenuTitle();
return getMenuParentTitle() + " " + getQuickTitle();
}
/**
@@ -266,9 +266,10 @@ public interface DebuggerModelService {
* <p>
* Assuming the target object is being actively traced, find the last focused object among those
* being traced by the same recorder. Essentially, given that the target likely belongs to a
* process, find the object within that process that last had focus. This is primarily used when
* switching focus between traces. Since the user has not explicitly selected a model object,
* the UI should choose the one which had focus when the newly-activated trace was last active.
* process, find the object within that process that last had focus. This is primarily used
* wh@Override en switching focus between traces. Since the user has not explicitly selected a
* model object, the UI should choose the one which had focus when the newly-activated trace was
* last active.
*
* @param target a source model object being actively traced
* @return the last focused object being traced by the same recorder
@@ -345,4 +346,21 @@ public interface DebuggerModelService {
* @return the offers
*/
Stream<DebuggerProgramLaunchOffer> getProgramLaunchOffers(Program program);
/**
* Prompt the user to create a new connection
*
* @return a future which completes with the new connection, possibly cancelled
*/
default CompletableFuture<DebuggerObjectModel> showConnectDialog() {
return showConnectDialog(null);
}
/**
* Prompt the user to create a new connection, optionally fixing the factory
*
* @param factory the required factory, or null for user selection
* @return a future which completes with the new connection, possible cancelled
*/
CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory);
}
@@ -21,8 +21,7 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceInternal;
import ghidra.app.plugin.core.debug.service.model.DebuggerModelServiceProxyPlugin;
import ghidra.app.plugin.core.debug.service.model.*;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
@@ -18,14 +18,9 @@ package ghidra.app.plugin.core.debug.gui.target;
import static ghidra.app.plugin.core.debug.gui.target.DebuggerTargetsProviderFriend.selectNodeForObject;
import static org.junit.Assert.*;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import javax.swing.JLabel;
import javax.swing.JTextField;
import org.junit.Before;
import org.junit.Test;
@@ -33,11 +28,8 @@ import org.junit.Test;
import docking.widgets.tree.GTreeNode;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.plugin.core.debug.gui.target.DebuggerConnectDialog.FactoryEntry;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.model.TestDebuggerModelFactory;
import ghidra.app.plugin.core.debug.service.model.DebuggerConnectDialog;
import ghidra.dbg.model.TestDebuggerObjectModel;
import ghidra.util.datastruct.CollectionChangeListener;
/**
* Tests of the target provider
@@ -53,68 +45,16 @@ public class DebuggerTargetsProviderTest extends AbstractGhidraHeadedDebuggerGUI
}
@Test
public void testConnectDialogPopulates() {
public void testConnectActionShowDialog() {
modelServiceInternal.setModelFactories(List.of(mb.testFactory));
waitForSwing();
performAction(targetsProvider.actionConnect, false);
DebuggerConnectDialog dialog = waitForDialogComponent(DebuggerConnectDialog.class);
FactoryEntry fe = (FactoryEntry) dialog.dropdownModel.getSelectedItem();
assertEquals(mb.testFactory, fe.factory);
assertEquals(TestDebuggerModelFactory.FAKE_DETAILS_HTML, dialog.description.getText());
Component[] components = dialog.pairPanel.getComponents();
assertTrue(components[0] instanceof JLabel);
JLabel label = (JLabel) components[0];
assertEquals(TestDebuggerModelFactory.FAKE_OPTION_NAME, label.getText());
assertTrue(components[1] instanceof JTextField);
JTextField field = (JTextField) components[1];
assertEquals(TestDebuggerModelFactory.FAKE_DEFAULT, field.getText());
pressButtonByText(dialog, "Cancel", true);
}
@Test
public void testConnectDialogConnectsAndRegistersModelWithService() {
modelServiceInternal.setModelFactories(List.of(mb.testFactory));
CompletableFuture<DebuggerObjectModel> futureModel = new CompletableFuture<>();
CollectionChangeListener<DebuggerObjectModel> listener =
new CollectionChangeListener<DebuggerObjectModel>() {
@Override
public void elementAdded(DebuggerObjectModel element) {
futureModel.complete(element);
}
@Override
public void elementModified(DebuggerObjectModel element) {
// Don't care
}
@Override
public void elementRemoved(DebuggerObjectModel element) {
fail();
}
};
modelService.addModelsChangedListener(listener);
performAction(targetsProvider.actionConnect, false);
DebuggerConnectDialog connectDialog = waitForDialogComponent(DebuggerConnectDialog.class);
FactoryEntry fe = (FactoryEntry) connectDialog.dropdownModel.getSelectedItem();
assertEquals(mb.testFactory, fe.factory);
pressButtonByText(connectDialog, AbstractConnectAction.NAME, true);
// NOTE: testModel is null. Don't use #createTestModel(), which adds to service
TestDebuggerObjectModel model = new TestDebuggerObjectModel();
mb.testFactory.pollBuild().complete(model);
assertEquals(model, futureModel.getNow(null));
}
@Test
public void testRegisteredModelsShowInTree() throws Exception {
createTestModel();
@@ -17,16 +17,23 @@ package ghidra.app.plugin.core.debug.service.model;
import static org.junit.Assert.*;
import java.awt.Component;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.swing.JLabel;
import javax.swing.JTextField;
import org.junit.Test;
import generic.Unique;
import ghidra.app.plugin.core.debug.event.ModelObjectFocusedPluginEvent;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.AbstractConnectAction;
import ghidra.app.plugin.core.debug.service.model.DebuggerConnectDialog.FactoryEntry;
import ghidra.app.plugin.core.debug.service.model.TestDebuggerProgramLaunchOpinion.TestDebuggerProgramLaunchOffer;
import ghidra.app.plugin.core.debug.service.model.launch.DebuggerProgramLaunchOffer;
import ghidra.app.services.TraceRecorder;
@@ -34,9 +41,11 @@ import ghidra.async.AsyncPairingQueue;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.model.TestDebuggerModelFactory;
import ghidra.dbg.model.TestDebuggerObjectModel;
import ghidra.dbg.testutil.DebuggerModelTestUtils;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.util.Swing;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.CollectionChangeListener;
import mockit.Mocked;
@@ -485,4 +494,67 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes
waitOn(mb.testModel.close());
assertNull(modelService.getCurrentModel());
}
@Test
public void testConnectDialogPopulates() {
modelServiceInternal.setModelFactories(List.of(mb.testFactory));
waitForSwing();
Swing.runLater(() -> modelService.showConnectDialog());
DebuggerConnectDialog dialog = waitForDialogComponent(DebuggerConnectDialog.class);
FactoryEntry fe = (FactoryEntry) dialog.dropdownModel.getSelectedItem();
assertEquals(mb.testFactory, fe.factory);
assertEquals(TestDebuggerModelFactory.FAKE_DETAILS_HTML, dialog.description.getText());
Component[] components = dialog.pairPanel.getComponents();
assertTrue(components[0] instanceof JLabel);
JLabel label = (JLabel) components[0];
assertEquals(TestDebuggerModelFactory.FAKE_OPTION_NAME, label.getText());
assertTrue(components[1] instanceof JTextField);
JTextField field = (JTextField) components[1];
assertEquals(TestDebuggerModelFactory.FAKE_DEFAULT, field.getText());
pressButtonByText(dialog, "Cancel", true);
}
@Test
public void testConnectDialogConnectsAndRegistersModelWithService() throws Throwable {
modelServiceInternal.setModelFactories(List.of(mb.testFactory));
CompletableFuture<DebuggerObjectModel> futureModel = new CompletableFuture<>();
CollectionChangeListener<DebuggerObjectModel> listener =
new CollectionChangeListener<DebuggerObjectModel>() {
@Override
public void elementAdded(DebuggerObjectModel element) {
futureModel.complete(element);
}
@Override
public void elementModified(DebuggerObjectModel element) {
// Don't care
}
@Override
public void elementRemoved(DebuggerObjectModel element) {
fail();
}
};
modelService.addModelsChangedListener(listener);
Swing.runLater(() -> modelService.showConnectDialog());
DebuggerConnectDialog connectDialog = waitForDialogComponent(DebuggerConnectDialog.class);
FactoryEntry fe = (FactoryEntry) connectDialog.dropdownModel.getSelectedItem();
assertEquals(mb.testFactory, fe.factory);
pressButtonByText(connectDialog, AbstractConnectAction.NAME, true);
// NOTE: testModel is null. Don't use #createTestModel(), which adds to service
TestDebuggerObjectModel model = new TestDebuggerObjectModel();
mb.testFactory.pollBuild().complete(model);
assertEquals(model, waitOn(futureModel));
}
}