mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-31 09:09:37 +08:00
GP-6782: Add 'Save Trace As' action.
This commit is contained in:
+15
-5
@@ -17,6 +17,7 @@ package ghidra.app.services;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import ghidra.async.AsyncReference;
|
import ghidra.async.AsyncReference;
|
||||||
import ghidra.debug.api.target.Target;
|
import ghidra.debug.api.target.Target;
|
||||||
@@ -237,17 +238,26 @@ public interface DebuggerTraceManagerService {
|
|||||||
* <p>
|
* <p>
|
||||||
* If a different domain file of the trace's name already exists, an incrementing integer is
|
* 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
|
* 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
|
* need to invoke {@link CompletableFuture#exceptionally(Function)} on the returned future. The
|
||||||
* returned future. The future is returned as a means of registering follow-up actions.
|
* future is returned as a means of registering follow-up actions.
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* TODO: Support save-as, prompting to overwrite, etc?
|
|
||||||
*
|
*
|
||||||
* @param trace the trace to save
|
* @param trace the trace to save
|
||||||
* @return a future which completes when the save is finished
|
* @return a future which completes when the save is finished
|
||||||
*/
|
*/
|
||||||
CompletableFuture<Void> saveTrace(Trace trace);
|
CompletableFuture<Void> saveTrace(Trace trace);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt the user and save the trace to a chosen path in the project
|
||||||
|
* <p>
|
||||||
|
* 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<Void> saveTraceAs(Trace trace);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the given trace
|
* Close the given trace
|
||||||
*
|
*
|
||||||
|
|||||||
+17
@@ -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 {
|
abstract class AbstractConnectAction extends DockingAction {
|
||||||
public static final String NAME = "Connect";
|
public static final String NAME = "Connect";
|
||||||
public static final Icon ICON = ICON_CONNECTION;
|
public static final Icon ICON = ICON_CONNECTION;
|
||||||
|
|||||||
+21
-1
@@ -41,7 +41,7 @@ import utilities.util.SuppressableCallback;
|
|||||||
import utilities.util.SuppressableCallback.Suppression;
|
import utilities.util.SuppressableCallback.Suppression;
|
||||||
|
|
||||||
public class DebuggerTraceTabPanel extends GTabPanel<Trace>
|
public class DebuggerTraceTabPanel extends GTabPanel<Trace>
|
||||||
implements PluginEventListener, DomainObjectListener {
|
implements PluginEventListener, DomainObjectListener, DomainFolderChangeListener {
|
||||||
|
|
||||||
private class TargetsChangeListener implements TargetPublicationListener {
|
private class TargetsChangeListener implements TargetPublicationListener {
|
||||||
@Override
|
@Override
|
||||||
@@ -79,6 +79,7 @@ public class DebuggerTraceTabPanel extends GTabPanel<Trace>
|
|||||||
tool.addEventListener(TraceOpenedPluginEvent.class, this);
|
tool.addEventListener(TraceOpenedPluginEvent.class, this);
|
||||||
tool.addEventListener(TraceActivatedPluginEvent.class, this);
|
tool.addEventListener(TraceActivatedPluginEvent.class, this);
|
||||||
tool.addEventListener(TraceClosedPluginEvent.class, this);
|
tool.addEventListener(TraceClosedPluginEvent.class, this);
|
||||||
|
tool.getProject().getProjectData().addDomainFolderChangeListener(this);
|
||||||
|
|
||||||
setNameFunction(this::getNameForTrace);
|
setNameFunction(this::getNameForTrace);
|
||||||
setIconFunction(this::getIconForTrace);
|
setIconFunction(this::getIconForTrace);
|
||||||
@@ -212,6 +213,25 @@ public class DebuggerTraceTabPanel extends GTabPanel<Trace>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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) {
|
private void traceTabSelected(Trace newTrace) {
|
||||||
cbCoordinateActivation.invoke(() -> {
|
cbCoordinateActivation.invoke(() -> {
|
||||||
if (traceManager == null) {
|
if (traceManager == null) {
|
||||||
|
|||||||
+90
@@ -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<Void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
-118
@@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.debug.service.tracemgr;
|
package ghidra.app.plugin.core.debug.service.tracemgr;
|
||||||
|
|
||||||
import static ghidra.framework.main.DataTreeDialogType.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
@@ -46,7 +44,9 @@ import ghidra.debug.api.tracemgr.DebuggerCoordinates;
|
|||||||
import ghidra.framework.client.ClientUtil;
|
import ghidra.framework.client.ClientUtil;
|
||||||
import ghidra.framework.client.NotConnectedException;
|
import ghidra.framework.client.NotConnectedException;
|
||||||
import ghidra.framework.data.DomainObjectAdapterDB;
|
import ghidra.framework.data.DomainObjectAdapterDB;
|
||||||
|
import ghidra.framework.data.DomainObjectFileListener;
|
||||||
import ghidra.framework.main.DataTreeDialog;
|
import ghidra.framework.main.DataTreeDialog;
|
||||||
|
import ghidra.framework.main.DataTreeDialogType;
|
||||||
import ghidra.framework.model.*;
|
import ghidra.framework.model.*;
|
||||||
import ghidra.framework.options.SaveState;
|
import ghidra.framework.options.SaveState;
|
||||||
import ghidra.framework.plugintool.*;
|
import ghidra.framework.plugintool.*;
|
||||||
@@ -66,7 +66,6 @@ import ghidra.trace.model.time.TraceSnapshot;
|
|||||||
import ghidra.trace.model.time.schedule.TraceSchedule;
|
import ghidra.trace.model.time.schedule.TraceSchedule;
|
||||||
import ghidra.trace.util.TraceEvents;
|
import ghidra.trace.util.TraceEvents;
|
||||||
import ghidra.util.*;
|
import ghidra.util.*;
|
||||||
import ghidra.util.database.DomainObjectLockHold;
|
|
||||||
import ghidra.util.exception.*;
|
import ghidra.util.exception.*;
|
||||||
import ghidra.util.task.*;
|
import ghidra.util.task.*;
|
||||||
|
|
||||||
@@ -96,16 +95,17 @@ import ghidra.util.task.*;
|
|||||||
public class DebuggerTraceManagerServicePlugin extends Plugin
|
public class DebuggerTraceManagerServicePlugin extends Plugin
|
||||||
implements DebuggerTraceManagerService {
|
implements DebuggerTraceManagerService {
|
||||||
|
|
||||||
private static final AutoConfigState.ClassHandler<DebuggerTraceManagerServicePlugin> //
|
private static final AutoConfigState.ClassHandler<
|
||||||
CONFIG_STATE_HANDLER = AutoConfigState.wireHandler(DebuggerTraceManagerServicePlugin.class,
|
DebuggerTraceManagerServicePlugin> CONFIG_STATE_HANDLER = AutoConfigState
|
||||||
MethodHandles.lookup());
|
.wireHandler(DebuggerTraceManagerServicePlugin.class, MethodHandles.lookup());
|
||||||
|
|
||||||
private static final String KEY_TRACE_COUNT = "NUM_TRACES";
|
private static final String KEY_TRACE_COUNT = "NUM_TRACES";
|
||||||
private static final String PREFIX_OPEN_TRACE = "OPEN_TRACE_";
|
private static final String PREFIX_OPEN_TRACE = "OPEN_TRACE_";
|
||||||
private static final String KEY_CURRENT_COORDS = "CURRENT_COORDS";
|
private static final String KEY_CURRENT_COORDS = "CURRENT_COORDS";
|
||||||
public static final String NEW_TRACES_FOLDER_NAME = "New Traces";
|
public static final String NEW_TRACES_FOLDER_NAME = "New Traces";
|
||||||
|
|
||||||
class ListenerForTraceChanges extends TraceDomainObjectListener {
|
class ListenerForTraceChanges extends TraceDomainObjectListener
|
||||||
|
implements DomainObjectFileListener {
|
||||||
private final Trace trace;
|
private final Trace trace;
|
||||||
|
|
||||||
public ListenerForTraceChanges(Trace trace) {
|
public ListenerForTraceChanges(Trace trace) {
|
||||||
@@ -162,6 +162,14 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
}
|
}
|
||||||
activate(current.object(object), ActivationCause.SYNC_MODEL);
|
activate(current.object(object), ActivationCause.SYNC_MODEL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void domainFileChanged(DomainObject domainObject) {
|
||||||
|
if (current.getTrace() != trace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contextChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class TransactionEndFuture extends CompletableFuture<Void>
|
static class TransactionEndFuture extends CompletableFuture<Void>
|
||||||
@@ -310,6 +318,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
DockingAction actionCloseOtherTraces;
|
DockingAction actionCloseOtherTraces;
|
||||||
DockingAction actionCloseDeadTraces;
|
DockingAction actionCloseDeadTraces;
|
||||||
DockingAction actionSaveTrace;
|
DockingAction actionSaveTrace;
|
||||||
|
DockingAction actionSaveTraceAs;
|
||||||
DockingAction actionOpenTrace;
|
DockingAction actionOpenTrace;
|
||||||
ToggleDockingAction actionSaveByDefault;
|
ToggleDockingAction actionSaveByDefault;
|
||||||
ToggleDockingAction actionCloseOnTerminate;
|
ToggleDockingAction actionCloseOnTerminate;
|
||||||
@@ -337,6 +346,10 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
.enabledWhen(c -> current.getTrace() != null)
|
.enabledWhen(c -> current.getTrace() != null)
|
||||||
.onAction(this::activatedSaveTrace)
|
.onAction(this::activatedSaveTrace)
|
||||||
.buildAndInstall(tool);
|
.buildAndInstall(tool);
|
||||||
|
actionSaveTraceAs = SaveTraceAsAction.builder(this)
|
||||||
|
.enabledWhen(c -> current.getTrace() != null)
|
||||||
|
.onAction(this::activatedSaveTraceAs)
|
||||||
|
.buildAndInstall(tool);
|
||||||
actionOpenTrace = OpenTraceAction.builder(this)
|
actionOpenTrace = OpenTraceAction.builder(this)
|
||||||
.enabledWhen(ctx -> true)
|
.enabledWhen(ctx -> true)
|
||||||
.onAction(this::activatedOpenTrace)
|
.onAction(this::activatedOpenTrace)
|
||||||
@@ -381,12 +394,26 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
saveTrace(trace);
|
saveTrace(trace);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void activatedOpenTrace(ActionContext ctx) {
|
private void activatedSaveTraceAs(ActionContext ctx) {
|
||||||
DomainFile df = askTrace(current.getTrace());
|
Trace trace = current.getTrace();
|
||||||
if (df != null) {
|
if (trace == null) {
|
||||||
Trace trace = openTrace(df, DomainFile.DEFAULT_VERSION); // TODO: Permit opening a previous revision?
|
return;
|
||||||
activateTrace(trace);
|
|
||||||
}
|
}
|
||||||
|
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) {
|
private void activatedCloseTrace(ActionContext ctx) {
|
||||||
@@ -413,18 +440,55 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
closeDeadTraces();
|
closeDeadTraces();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DataTreeDialog getTraceChooserDialog() {
|
protected static DataTreeDialog getTraceChooserDialog(String title, DataTreeDialogType type) {
|
||||||
DomainFileFilter filter = new DefaultDomainFileFilter(Trace.class, false);
|
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) {
|
public record AskTraceResult(DomainFile file, DomainFolder parent, String name) {}
|
||||||
DataTreeDialog dialog = getTraceChooserDialog();
|
|
||||||
if (trace != null) {
|
public static AskTraceResult askTrace(PluginTool tool, String title, DataTreeDialogType type,
|
||||||
dialog.selectDomainFile(trace.getDomainFile());
|
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);
|
tool.showDialog(dialog);
|
||||||
return dialog.getDomainFile();
|
if (dialog.wasCancelled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new AskTraceResult(dialog.getDomainFile(), dialog.getDomainFolder(),
|
||||||
|
dialog.getNameText());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -553,9 +617,18 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
return newCurrent;
|
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() {
|
protected void contextChanged() {
|
||||||
Trace trace = current.getTrace();
|
Trace trace = current.getTrace();
|
||||||
String itemName = trace == null ? "..." : trace.getName();
|
String itemName = trace == null ? "..." : getTraceDisplayName(trace);
|
||||||
actionCloseTrace.getMenuBarData().setMenuItemName(CloseTraceAction.NAME_PREFIX + itemName);
|
actionCloseTrace.getMenuBarData().setMenuItemName(CloseTraceAction.NAME_PREFIX + itemName);
|
||||||
actionSaveTrace.getMenuBarData().setMenuItemName(SaveTraceAction.NAME_PREFIX + itemName);
|
actionSaveTrace.getMenuBarData().setMenuItemName(SaveTraceAction.NAME_PREFIX + itemName);
|
||||||
tool.contextChanged(null);
|
tool.contextChanged(null);
|
||||||
@@ -793,6 +866,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
ListenerForTraceChanges listener = new ListenerForTraceChanges(trace);
|
ListenerForTraceChanges listener = new ListenerForTraceChanges(trace);
|
||||||
listenersByTrace.put(trace, listener);
|
listenersByTrace.put(trace, listener);
|
||||||
trace.addListener(listener);
|
trace.addListener(listener);
|
||||||
|
trace.addDomainFileListener(listener);
|
||||||
}
|
}
|
||||||
contextChanged();
|
contextChanged();
|
||||||
firePluginEvent(new TraceOpenedPluginEvent(getName(), trace));
|
firePluginEvent(new TraceOpenedPluginEvent(getName(), trace));
|
||||||
@@ -867,7 +941,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static DomainFolder createOrGetFolder(PluginTool tool, String operation,
|
public static DomainFolder createOrGetFolder(PluginTool tool, String operation,
|
||||||
DomainFolder parent, String name) throws InvalidNameException {
|
DomainFolder parent, String name) {
|
||||||
try {
|
try {
|
||||||
return parent.createFolder(name);
|
return parent.createFolder(name);
|
||||||
}
|
}
|
||||||
@@ -883,118 +957,70 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
tool.getToolFrame());
|
tool.getToolFrame());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
catch (InvalidNameException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
protected static DomainObjectLockHold maybeLock(Trace trace, boolean lock) {
|
|
||||||
if (!lock) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return DomainObjectLockHold.forceLock(trace, false, "Auto Save");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Void> 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<Void> 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);
|
tool.prepareToSave(trace);
|
||||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
// LATER: Get all the nuances for this correct: Locking, tx flushing, etc.
|
||||||
// TODO: Get all the nuances for this correct...
|
|
||||||
// "Save As" action, Locking, transaction flushing, etc....
|
final AbstractSaveTraceTask task;
|
||||||
if (trace.getDomainFile().getParent() != null) {
|
if (promptPath) {
|
||||||
new TaskLauncher(new Task("Save Trace", true, true, true) {
|
if (Objects.equals(trace.getDomainFile(), asked.parent().getFile(asked.name()))) {
|
||||||
@Override
|
task = new SaveTraceTask(tool, trace, asked, force);
|
||||||
public void run(TaskMonitor monitor) throws CancelledException {
|
}
|
||||||
try (DomainObjectLockHold hold = maybeLock(trace, force)) {
|
else {
|
||||||
trace.getDomainFile().save(monitor);
|
task = new SaveTraceAsTask(tool, trace, asked, force);
|
||||||
future.complete(null);
|
}
|
||||||
}
|
}
|
||||||
catch (CancelledException e) {
|
else if (trace.getDomainFile().getParent() == null) {
|
||||||
// Done
|
task = new SaveNewTraceTask(tool, trace, asked, force);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
DomainFolder root = tool.getProject().getProjectData().getRootFolder();
|
task = new SaveTraceTask(tool, trace, asked, force);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return future;
|
|
||||||
|
new TaskLauncher(task);
|
||||||
|
return task.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> saveTrace(Trace trace, boolean force) {
|
public CompletableFuture<Void> saveTrace(Trace trace, boolean promptPath, boolean force) {
|
||||||
if (isDisposed()) {
|
if (isDisposed()) {
|
||||||
Msg.error(this, "Cannot save trace after manager disposal! Data may have been lost.");
|
Msg.error(this, "Cannot save trace after manager disposal! Data may have been lost.");
|
||||||
return AsyncUtils.nil();
|
return AsyncUtils.nil();
|
||||||
}
|
}
|
||||||
return saveTrace(tool, trace, force);
|
return saveTrace(tool, trace, promptPath, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> saveTrace(Trace trace) {
|
public CompletableFuture<Void> saveTrace(Trace trace) {
|
||||||
return saveTrace(trace, false);
|
return saveTrace(trace, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> saveTraceAs(Trace trace) {
|
||||||
|
return saveTrace(trace, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void doTraceClosed(Trace trace) {
|
protected void doTraceClosed(Trace trace) {
|
||||||
@@ -1003,7 +1029,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
}
|
}
|
||||||
synchronized (listenersByTrace) {
|
synchronized (listenersByTrace) {
|
||||||
lastCoordsByTrace.remove(trace);
|
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());
|
//Msg.debug(this, "Remaining Consumers of " + trace + ": " + trace.getConsumerList());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1089,7 +1117,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
|||||||
Trace trace = it.next();
|
Trace trace = it.next();
|
||||||
trace.release(this);
|
trace.release(this);
|
||||||
lastCoordsByTrace.remove(trace);
|
lastCoordsByTrace.remove(trace);
|
||||||
trace.removeListener(listenersByTrace.get(trace));
|
ListenerForTraceChanges listener = listenersByTrace.get(trace);
|
||||||
|
trace.removeListener(listener);
|
||||||
|
trace.removeDomainFileListener(listener);
|
||||||
it.remove();
|
it.remove();
|
||||||
}
|
}
|
||||||
// Be certain
|
// Be certain
|
||||||
|
|||||||
+48
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -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.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user