diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPlugin.java index 311cf1ad82..b97927b5a1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPlugin.java @@ -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); - } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProvider.java index 9a89c7a38c..fc4bd1a0e3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProvider.java @@ -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); - } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbDebuggerProgramLaunchOpinion.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbDebuggerProgramLaunchOpinion.java index f4dc976503..1a53ec23fd 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbDebuggerProgramLaunchOpinion.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/platform/GdbDebuggerProgramLaunchOpinion.java @@ -108,12 +108,17 @@ public class GdbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi } @Override - public String getMenuTitle() { + public String getQuickTitle() { Map> 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 diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerConnectDialog.java similarity index 90% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerConnectDialog.java index b631b30bc5..1086ab625b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/target/DebuggerConnectDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerConnectDialog.java @@ -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 futureConnect; + protected CompletableFuture 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 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() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java index 7c5ceef1d9..4243d8b94c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServicePlugin.java @@ -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 doShowConnectDialog(PluginTool tool, + DebuggerModelFactory factory) { + CompletableFuture future = connectDialog.reset(factory); + tool.showDialog(connectDialog); + return future; + } + + @Override + public CompletableFuture showConnectDialog(DebuggerModelFactory factory) { + return doShowConnectDialog(tool, factory); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java index da631181eb..851c91e95d 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceProxyPlugin.java @@ -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 showConnectDialog(DebuggerModelFactory factory) { + return delegate.doShowConnectDialog(tool, factory); + } + @Override public Stream 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); }); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java index a5de54c911..042df07fda 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/AbstractDebuggerProgramLaunchOffer.java @@ -251,15 +251,23 @@ public abstract class AbstractDebuggerProgramLaunchOffer implements DebuggerProg } } + protected CompletableFuture 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 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 launcherPath = getLauncherPath(); TargetObjectSchema schema = m.getRootSchema().getSuccessorSchema(launcherPath); if (!schema.getInterfaces().contains(TargetLauncher.class)) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java index f4d6996bc2..7df6b583fc 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/launch/DebuggerProgramLaunchOffer.java @@ -85,13 +85,26 @@ public interface DebuggerProgramLaunchOffer { */ String getMenuTitle(); + /** + * Get the text displayed if the user will not be prompted + * + *

+ * 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(); } /** diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java index 8015b9c27f..e40d8e81c2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/services/DebuggerModelService.java @@ -266,9 +266,10 @@ public interface DebuggerModelService { *

* 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 getProgramLaunchOffers(Program program); + + /** + * Prompt the user to create a new connection + * + * @return a future which completes with the new connection, possibly cancelled + */ + default CompletableFuture 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 showConnectDialog(DebuggerModelFactory factory); } diff --git a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPluginScreenShots.java b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPluginScreenShots.java index 4ac6c5df48..49c056e0e2 100644 --- a/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPluginScreenShots.java +++ b/Ghidra/Debug/Debugger/src/screen/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsPluginScreenShots.java @@ -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; diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProviderTest.java index 847fdfbe7e..57d52b2165 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/target/DebuggerTargetsProviderTest.java @@ -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 futureModel = new CompletableFuture<>(); - CollectionChangeListener listener = - new CollectionChangeListener() { - @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(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java index b1ef828057..4725f34e88 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/model/DebuggerModelServiceTest.java @@ -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 futureModel = new CompletableFuture<>(); + CollectionChangeListener listener = + new CollectionChangeListener() { + @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)); + } }