diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java
index b9476ba045..cdac1ab3b7 100644
--- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java
+++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/app/services/DebuggerTraceManagerService.java
@@ -17,6 +17,7 @@ package ghidra.app.services;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
import ghidra.async.AsyncReference;
import ghidra.debug.api.target.Target;
@@ -237,17 +238,26 @@ public interface DebuggerTraceManagerService {
*
* If a different domain file of the trace's name already exists, an incrementing integer is
* appended. Errors are handled in the same fashion as saving a program, so there is little/no
- * need to invoke {@link CompletableFuture#exceptionally(java.util.function.Function)} on the
- * returned future. The future is returned as a means of registering follow-up actions.
- *
- *
- * TODO: Support save-as, prompting to overwrite, etc?
+ * need to invoke {@link CompletableFuture#exceptionally(Function)} on the returned future. The
+ * future is returned as a means of registering follow-up actions.
*
* @param trace the trace to save
* @return a future which completes when the save is finished
*/
CompletableFuture saveTrace(Trace trace);
+ /**
+ * Prompt the user and save the trace to a chosen path in the project
+ *
+ * Errors are handled in the same fashion as saving a program, so there is little/no need to
+ * invoke {@link CompletableFuture#exceptionally(Function)} on the returned future. The future
+ * is returned as a means of registering follow-up actions.
+ *
+ * @param trace the trace to (rename and) save
+ * @return a future which completes when the save is finished
+ */
+ CompletableFuture saveTraceAs(Trace trace);
+
/**
* Close the given trace
*
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
index 1755ee9c86..f66818dbe2 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java
@@ -355,6 +355,23 @@ public interface DebuggerResources {
}
}
+ interface SaveTraceAsAction {
+ String NAME = "Save Trace As";
+ String DESCRIPTION = "Rename and save the selected trace";
+ Icon ICON = DebuggerResources.ICON_SAVE;
+ String GROUP = DebuggerResources.GROUP_TRACE;
+ String HELP_ANCHOR = "save_trace_as";
+
+ static ActionBuilder builder(Plugin owner) {
+ String ownerName = owner.getName();
+ return new ActionBuilder(NAME, ownerName).description(DESCRIPTION)
+ .menuPath(DebuggerPluginPackage.NAME, NAME)
+ .menuIcon(ICON)
+ .menuGroup(GROUP)
+ .helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
+ }
+ }
+
abstract class AbstractConnectAction extends DockingAction {
public static final String NAME = "Connect";
public static final Icon ICON = ICON_CONNECTION;
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanel.java
index 18eda4023e..97a78b183c 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanel.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanel.java
@@ -41,7 +41,7 @@ import utilities.util.SuppressableCallback;
import utilities.util.SuppressableCallback.Suppression;
public class DebuggerTraceTabPanel extends GTabPanel
- implements PluginEventListener, DomainObjectListener {
+ implements PluginEventListener, DomainObjectListener, DomainFolderChangeListener {
private class TargetsChangeListener implements TargetPublicationListener {
@Override
@@ -79,6 +79,7 @@ public class DebuggerTraceTabPanel extends GTabPanel
tool.addEventListener(TraceOpenedPluginEvent.class, this);
tool.addEventListener(TraceActivatedPluginEvent.class, this);
tool.addEventListener(TraceClosedPluginEvent.class, this);
+ tool.getProject().getProjectData().addDomainFolderChangeListener(this);
setNameFunction(this::getNameForTrace);
setIconFunction(this::getIconForTrace);
@@ -212,6 +213,25 @@ public class DebuggerTraceTabPanel extends GTabPanel
}
}
+ @Override
+ public void domainFileRenamed(DomainFile file, String oldName) {
+ if (file.getOpenedDomainObject(this) instanceof Trace trace) {
+ try {
+ refreshTab(trace);
+ }
+ finally {
+ trace.release(this);
+ }
+ }
+ }
+
+ @Override
+ public void domainFileObjectOpenedForUpdate(DomainFile file, DomainObject object) {
+ if (object instanceof Trace trace) {
+ refreshTab(trace);
+ }
+ }
+
private void traceTabSelected(Trace newTrace) {
cbCoordinateActivation.invoke(() -> {
if (traceManager == null) {
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/AbstractSaveTraceTask.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/AbstractSaveTraceTask.java
new file mode 100644
index 0000000000..9e4e007e56
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/AbstractSaveTraceTask.java
@@ -0,0 +1,90 @@
+/* ###
+ * 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.service.tracemgr;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.util.concurrent.CompletableFuture;
+
+import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin.AskTraceResult;
+import ghidra.framework.client.ClientUtil;
+import ghidra.framework.client.NotConnectedException;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.trace.model.Trace;
+import ghidra.util.InvalidNameException;
+import ghidra.util.Msg;
+import ghidra.util.database.DomainObjectLockHold;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.Task;
+import ghidra.util.task.TaskMonitor;
+
+public abstract class AbstractSaveTraceTask extends Task {
+ protected final Trace trace;
+ protected final AskTraceResult asked;
+ protected final boolean force;
+ protected final PluginTool tool;
+
+ protected final CompletableFuture future = new CompletableFuture<>();
+
+ public AbstractSaveTraceTask(String title, PluginTool tool, Trace trace, AskTraceResult asked,
+ boolean force) {
+ super(title, true, true, true);
+ this.tool = tool;
+ this.trace = trace;
+ this.asked = asked;
+ this.force = force;
+ }
+
+ protected DomainObjectLockHold maybeLock(Trace trace, boolean lock) {
+ if (!lock) {
+ return null;
+ }
+ return DomainObjectLockHold.forceLock(trace, false, getTaskTitle());
+ }
+
+ protected abstract void saveTrace(TaskMonitor monitor)
+ throws CancelledException, InvalidNameException, IOException;
+
+ @Override
+ public void run(TaskMonitor monitor) throws CancelledException {
+ try (DomainObjectLockHold hold = maybeLock(trace, force)) {
+ saveTrace(monitor);
+ }
+ catch (CancelledException e) {
+ // Done
+ future.completeExceptionally(e);
+ }
+ catch (NotConnectedException | ConnectException e) {
+ ClientUtil.promptForReconnect(tool.getProject().getRepository(), tool.getToolFrame());
+ future.completeExceptionally(e);
+ }
+ catch (IOException e) {
+ ClientUtil.handleException(tool.getProject().getRepository(), e, getTaskTitle(),
+ tool.getToolFrame());
+ future.completeExceptionally(e);
+ }
+ catch (InvalidNameException e) {
+ Msg.showError(DebuggerTraceManagerServicePlugin.class, null, getTaskTitle(),
+ e.getMessage());
+ future.completeExceptionally(e);
+ }
+ catch (Throwable e) {
+ Msg.showError(DebuggerTraceManagerServicePlugin.class, null, getTaskTitle(),
+ e.getMessage(), e);
+ future.completeExceptionally(e);
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java
index 0825d1328d..baad273a8d 100644
--- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java
@@ -15,8 +15,6 @@
*/
package ghidra.app.plugin.core.debug.service.tracemgr;
-import static ghidra.framework.main.DataTreeDialogType.*;
-
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.ConnectException;
@@ -46,7 +44,9 @@ import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.client.ClientUtil;
import ghidra.framework.client.NotConnectedException;
import ghidra.framework.data.DomainObjectAdapterDB;
+import ghidra.framework.data.DomainObjectFileListener;
import ghidra.framework.main.DataTreeDialog;
+import ghidra.framework.main.DataTreeDialogType;
import ghidra.framework.model.*;
import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
@@ -66,7 +66,6 @@ import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.util.TraceEvents;
import ghidra.util.*;
-import ghidra.util.database.DomainObjectLockHold;
import ghidra.util.exception.*;
import ghidra.util.task.*;
@@ -96,16 +95,17 @@ import ghidra.util.task.*;
public class DebuggerTraceManagerServicePlugin extends Plugin
implements DebuggerTraceManagerService {
- private static final AutoConfigState.ClassHandler //
- CONFIG_STATE_HANDLER = AutoConfigState.wireHandler(DebuggerTraceManagerServicePlugin.class,
- MethodHandles.lookup());
+ private static final AutoConfigState.ClassHandler<
+ DebuggerTraceManagerServicePlugin> CONFIG_STATE_HANDLER = AutoConfigState
+ .wireHandler(DebuggerTraceManagerServicePlugin.class, MethodHandles.lookup());
private static final String KEY_TRACE_COUNT = "NUM_TRACES";
private static final String PREFIX_OPEN_TRACE = "OPEN_TRACE_";
private static final String KEY_CURRENT_COORDS = "CURRENT_COORDS";
public static final String NEW_TRACES_FOLDER_NAME = "New Traces";
- class ListenerForTraceChanges extends TraceDomainObjectListener {
+ class ListenerForTraceChanges extends TraceDomainObjectListener
+ implements DomainObjectFileListener {
private final Trace trace;
public ListenerForTraceChanges(Trace trace) {
@@ -162,6 +162,14 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
}
activate(current.object(object), ActivationCause.SYNC_MODEL);
}
+
+ @Override
+ public void domainFileChanged(DomainObject domainObject) {
+ if (current.getTrace() != trace) {
+ return;
+ }
+ contextChanged();
+ }
}
static class TransactionEndFuture extends CompletableFuture
@@ -310,6 +318,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
DockingAction actionCloseOtherTraces;
DockingAction actionCloseDeadTraces;
DockingAction actionSaveTrace;
+ DockingAction actionSaveTraceAs;
DockingAction actionOpenTrace;
ToggleDockingAction actionSaveByDefault;
ToggleDockingAction actionCloseOnTerminate;
@@ -337,6 +346,10 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
.enabledWhen(c -> current.getTrace() != null)
.onAction(this::activatedSaveTrace)
.buildAndInstall(tool);
+ actionSaveTraceAs = SaveTraceAsAction.builder(this)
+ .enabledWhen(c -> current.getTrace() != null)
+ .onAction(this::activatedSaveTraceAs)
+ .buildAndInstall(tool);
actionOpenTrace = OpenTraceAction.builder(this)
.enabledWhen(ctx -> true)
.onAction(this::activatedOpenTrace)
@@ -381,12 +394,26 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
saveTrace(trace);
}
- private void activatedOpenTrace(ActionContext ctx) {
- DomainFile df = askTrace(current.getTrace());
- if (df != null) {
- Trace trace = openTrace(df, DomainFile.DEFAULT_VERSION); // TODO: Permit opening a previous revision?
- activateTrace(trace);
+ private void activatedSaveTraceAs(ActionContext ctx) {
+ Trace trace = current.getTrace();
+ if (trace == null) {
+ return;
}
+ saveTraceAs(trace);
+ }
+
+ private void activatedOpenTrace(ActionContext ctx) {
+ AskTraceResult asked =
+ askTrace(tool, OpenTraceAction.NAME, DataTreeDialogType.OPEN, current.getTrace());
+ if (asked == null) {
+ return;
+ }
+ if (asked.file() == null) {
+ return;
+ }
+ // LATER: Permit opening a previous revision?
+ Trace trace = openTrace(asked.file(), DomainFile.DEFAULT_VERSION);
+ activateTrace(trace);
}
private void activatedCloseTrace(ActionContext ctx) {
@@ -413,18 +440,55 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
closeDeadTraces();
}
- protected DataTreeDialog getTraceChooserDialog() {
+ protected static DataTreeDialog getTraceChooserDialog(String title, DataTreeDialogType type) {
DomainFileFilter filter = new DefaultDomainFileFilter(Trace.class, false);
- return new DataTreeDialog(null, OpenTraceAction.NAME, OPEN, filter);
+ return new DataTreeDialog(null, title, type, filter);
}
- public DomainFile askTrace(Trace trace) {
- DataTreeDialog dialog = getTraceChooserDialog();
- if (trace != null) {
- dialog.selectDomainFile(trace.getDomainFile());
+ public record AskTraceResult(DomainFile file, DomainFolder parent, String name) {}
+
+ public static AskTraceResult askTrace(PluginTool tool, String title, DataTreeDialogType type,
+ Trace defaultTrace) {
+ DataTreeDialog dialog = getTraceChooserDialog(title, type);
+ if (defaultTrace != null) {
+ dialog.selectDomainFile(defaultTrace.getDomainFile());
}
+
+ dialog.addOkActionListener(ev -> {
+ dialog.setStatusText("");
+ String name = dialog.getNameText();
+ if (name.isBlank()) {
+ dialog.setStatusText("Please enter a name");
+ return;
+ }
+ DomainFolder parent = dialog.getDomainFolder();
+ if (parent == null) {
+ dialog.setStatusText("Please select a folder");
+ return;
+ }
+ DomainFile file = parent.getFile(name);
+ if (type == DataTreeDialogType.SAVE && file != null && file.isReadOnly()) {
+ dialog.setStatusText("Specified file is read-only");
+ return;
+ }
+ if (type == DataTreeDialogType.SAVE && file != null) {
+ String msg = """
+ Domain file %s already exists with type %s. Do you want to overwrite it?\
+ """.formatted(name, file.getContentType());
+ int overwrite = OptionDialog.showYesNoDialog(tool.getToolFrame(), "Overwrite", msg);
+ if (overwrite != OptionDialog.YES_OPTION) {
+ return;
+ }
+ }
+ dialog.close();
+ });
+
tool.showDialog(dialog);
- return dialog.getDomainFile();
+ if (dialog.wasCancelled()) {
+ return null;
+ }
+ return new AskTraceResult(dialog.getDomainFile(), dialog.getDomainFolder(),
+ dialog.getNameText());
}
@Override
@@ -553,9 +617,18 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
return newCurrent;
}
+ protected String getTraceDisplayName(Trace trace) {
+ DomainFile file = trace.getDomainFile();
+ String name = file.getName();
+ if (!name.isBlank()) {
+ return name;
+ }
+ return trace.getName();
+ }
+
protected void contextChanged() {
Trace trace = current.getTrace();
- String itemName = trace == null ? "..." : trace.getName();
+ String itemName = trace == null ? "..." : getTraceDisplayName(trace);
actionCloseTrace.getMenuBarData().setMenuItemName(CloseTraceAction.NAME_PREFIX + itemName);
actionSaveTrace.getMenuBarData().setMenuItemName(SaveTraceAction.NAME_PREFIX + itemName);
tool.contextChanged(null);
@@ -793,6 +866,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
ListenerForTraceChanges listener = new ListenerForTraceChanges(trace);
listenersByTrace.put(trace, listener);
trace.addListener(listener);
+ trace.addDomainFileListener(listener);
}
contextChanged();
firePluginEvent(new TraceOpenedPluginEvent(getName(), trace));
@@ -867,7 +941,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
}
public static DomainFolder createOrGetFolder(PluginTool tool, String operation,
- DomainFolder parent, String name) throws InvalidNameException {
+ DomainFolder parent, String name) {
try {
return parent.createFolder(name);
}
@@ -883,118 +957,70 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
tool.getToolFrame());
return null;
}
- }
-
- protected static DomainObjectLockHold maybeLock(Trace trace, boolean lock) {
- if (!lock) {
- return null;
+ catch (InvalidNameException e) {
+ throw new AssertionError(e);
}
- return DomainObjectLockHold.forceLock(trace, false, "Auto Save");
}
- public static CompletableFuture saveTrace(PluginTool tool, Trace trace, boolean force) {
+ public static AskTraceResult deriveDefaults(PluginTool tool, Trace trace) {
+ DomainFile file = trace.getDomainFile();
+ DomainFolder folder = file.getParent();
+ if (folder != null) {
+ return new AskTraceResult(file, folder, file.getName());
+ }
+ DomainFolder root = tool.getProject().getProjectData().getRootFolder();
+ return new AskTraceResult(file,
+ createOrGetFolder(tool, "Save New Trace", root, NEW_TRACES_FOLDER_NAME),
+ trace.getName());
+ }
+
+ public static CompletableFuture saveTrace(PluginTool tool, Trace trace,
+ boolean promptPath, boolean force) {
+ AskTraceResult asked = promptPath
+ ? askTrace(tool, SaveTraceAsAction.NAME, DataTreeDialogType.SAVE, trace)
+ : deriveDefaults(tool, trace);
+ if (asked == null) {
+ return CompletableFuture.failedFuture(new CancelledException("User cancelled"));
+ }
tool.prepareToSave(trace);
- CompletableFuture future = new CompletableFuture<>();
- // TODO: Get all the nuances for this correct...
- // "Save As" action, Locking, transaction flushing, etc....
- if (trace.getDomainFile().getParent() != null) {
- new TaskLauncher(new Task("Save Trace", true, true, true) {
- @Override
- public void run(TaskMonitor monitor) throws CancelledException {
- try (DomainObjectLockHold hold = maybeLock(trace, force)) {
- trace.getDomainFile().save(monitor);
- future.complete(null);
- }
- catch (CancelledException e) {
- // Done
- future.completeExceptionally(e);
- }
- catch (NotConnectedException | ConnectException e) {
- ClientUtil.promptForReconnect(tool.getProject().getRepository(),
- tool.getToolFrame());
- future.completeExceptionally(e);
- }
- catch (IOException e) {
- ClientUtil.handleException(tool.getProject().getRepository(), e,
- "Save Trace", tool.getToolFrame());
- future.completeExceptionally(e);
- }
- catch (Throwable e) {
- future.completeExceptionally(e);
- }
- }
- });
+ // LATER: Get all the nuances for this correct: Locking, tx flushing, etc.
+
+ final AbstractSaveTraceTask task;
+ if (promptPath) {
+ if (Objects.equals(trace.getDomainFile(), asked.parent().getFile(asked.name()))) {
+ task = new SaveTraceTask(tool, trace, asked, force);
+ }
+ else {
+ task = new SaveTraceAsTask(tool, trace, asked, force);
+ }
+ }
+ else if (trace.getDomainFile().getParent() == null) {
+ task = new SaveNewTraceTask(tool, trace, asked, force);
}
else {
- DomainFolder root = tool.getProject().getProjectData().getRootFolder();
- DomainFolder traces;
- try {
- traces = createOrGetFolder(tool, "Save New Trace", root, NEW_TRACES_FOLDER_NAME);
- }
- catch (InvalidNameException e) {
- throw new AssertionError(e);
- }
-
- new TaskLauncher(new Task("Save New Trace", true, true, true) {
-
- @Override
- public void run(TaskMonitor monitor) throws CancelledException {
- String filename = trace.getName();
- try (DomainObjectLockHold hold = maybeLock(trace, force)) {
- for (int i = 1;; i++) {
- try {
- traces.createFile(filename, trace, monitor);
- break;
- }
- catch (DuplicateFileException e) {
- filename = trace.getName() + "." + i;
- }
- }
- trace.save("Initial save", monitor);
- future.complete(null);
- }
- catch (CancelledException e) {
- // Done
- future.completeExceptionally(e);
- }
- catch (NotConnectedException | ConnectException e) {
- ClientUtil.promptForReconnect(tool.getProject().getRepository(),
- tool.getToolFrame());
- future.completeExceptionally(e);
- }
- catch (IOException e) {
- ClientUtil.handleException(tool.getProject().getRepository(), e,
- "Save New Trace", tool.getToolFrame());
- future.completeExceptionally(e);
- }
- catch (InvalidNameException e) {
- Msg.showError(DebuggerTraceManagerServicePlugin.class, null,
- "Save New Trace Error", e.getMessage());
- future.completeExceptionally(e);
- }
- catch (Throwable e) {
- Msg.showError(DebuggerTraceManagerServicePlugin.class, null,
- "Save New Trace Error", e.getMessage(), e);
- future.completeExceptionally(e);
- }
- }
-
- });
+ task = new SaveTraceTask(tool, trace, asked, force);
}
- return future;
+
+ new TaskLauncher(task);
+ return task.future;
}
- public CompletableFuture saveTrace(Trace trace, boolean force) {
+ public CompletableFuture saveTrace(Trace trace, boolean promptPath, boolean force) {
if (isDisposed()) {
Msg.error(this, "Cannot save trace after manager disposal! Data may have been lost.");
return AsyncUtils.nil();
}
- return saveTrace(tool, trace, force);
+ return saveTrace(tool, trace, promptPath, force);
}
@Override
public CompletableFuture saveTrace(Trace trace) {
- return saveTrace(trace, false);
+ return saveTrace(trace, false, false);
+ }
+
+ @Override
+ public CompletableFuture saveTraceAs(Trace trace) {
+ return saveTrace(trace, true, false);
}
protected void doTraceClosed(Trace trace) {
@@ -1003,7 +1029,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
}
synchronized (listenersByTrace) {
lastCoordsByTrace.remove(trace);
- trace.removeListener(listenersByTrace.remove(trace));
+ ListenerForTraceChanges listener = listenersByTrace.remove(trace);
+ trace.removeListener(listener);
+ trace.removeDomainFileListener(listener);
//Msg.debug(this, "Remaining Consumers of " + trace + ": " + trace.getConsumerList());
}
try {
@@ -1089,7 +1117,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
Trace trace = it.next();
trace.release(this);
lastCoordsByTrace.remove(trace);
- trace.removeListener(listenersByTrace.get(trace));
+ ListenerForTraceChanges listener = listenersByTrace.get(trace);
+ trace.removeListener(listener);
+ trace.removeDomainFileListener(listener);
it.remove();
}
// Be certain
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveNewTraceTask.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveNewTraceTask.java
new file mode 100644
index 0000000000..0a8058e01f
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveNewTraceTask.java
@@ -0,0 +1,48 @@
+/* ###
+ * 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.service.tracemgr;
+
+import java.io.IOException;
+
+import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin.AskTraceResult;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.trace.model.Trace;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.exception.DuplicateFileException;
+import ghidra.util.task.TaskMonitor;
+
+public class SaveNewTraceTask extends AbstractSaveTraceTask {
+ public SaveNewTraceTask(PluginTool tool, Trace trace, AskTraceResult asked, boolean force) {
+ super("Save trace " + trace.getName(), tool, trace, asked, force);
+ }
+
+ @Override
+ protected void saveTrace(TaskMonitor monitor)
+ throws CancelledException, InvalidNameException, IOException {
+ String filename = asked.name();
+ for (int i = 1;; i++) {
+ try {
+ asked.parent().createFile(filename, trace, monitor);
+ break; // success, so fall through
+ }
+ catch (DuplicateFileException e) {
+ filename = "%s.%d".formatted(asked.name(), i);
+ }
+ }
+ trace.save("Initial save", monitor);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveTraceAsTask.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveTraceAsTask.java
new file mode 100644
index 0000000000..6e828204b7
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveTraceAsTask.java
@@ -0,0 +1,49 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.debug.service.tracemgr;
+
+import java.io.IOException;
+
+import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin.AskTraceResult;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.trace.model.Trace;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class SaveTraceAsTask extends AbstractSaveTraceTask {
+ public SaveTraceAsTask(PluginTool tool, Trace trace, AskTraceResult asked,
+ boolean force) {
+ super("Save %s as %s".formatted(trace, asked.name()), tool, trace, asked, force);
+ }
+
+ @Override
+ protected void saveTrace(TaskMonitor monitor)
+ throws CancelledException, InvalidNameException, IOException {
+ DomainFile exists = asked.parent().getFile(asked.name());
+ if (exists != null) {
+ exists.delete();
+ }
+ asked.parent().createFile(asked.name(), trace, monitor);
+ trace.setName(asked.name());
+ trace.save("Save As", monitor);
+ /**
+ * NOTE: Refrain from modifying "Trace Information", since that better indicates the
+ * *original* trace name, as reported by the back end debugger.
+ */
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveTraceTask.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveTraceTask.java
new file mode 100644
index 0000000000..8f6781fdef
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/SaveTraceTask.java
@@ -0,0 +1,37 @@
+/* ###
+ * 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.service.tracemgr;
+
+import java.io.IOException;
+
+import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServicePlugin.AskTraceResult;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.trace.model.Trace;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class SaveTraceTask extends AbstractSaveTraceTask {
+ public SaveTraceTask(PluginTool tool, Trace trace, AskTraceResult asked, boolean force) {
+ super("Save trace " + trace.getDomainFile().getName(), tool, trace, asked, force);
+ }
+
+ @Override
+ protected void saveTrace(TaskMonitor monitor)
+ throws CancelledException, InvalidNameException, IOException {
+ trace.getDomainFile().save(monitor);
+ }
+}