Merge remote-tracking branch 'origin/GP-6392_Dan_fixSaveOnCloseDeadlock-take4--SQUASHED' into patch

This commit is contained in:
Ryan Kurtz
2026-02-20 13:29:00 -05:00
12 changed files with 113 additions and 87 deletions
@@ -634,7 +634,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
} }
@Override @Override
public synchronized Collection<Trace> getOpenTraces() { public Collection<Trace> getOpenTraces() {
synchronized (listenersByTrace) { synchronized (listenersByTrace) {
return Set.copyOf(tracesView); return Set.copyOf(tracesView);
} }
@@ -15,7 +15,6 @@
*/ */
package ghidra.trace.database; package ghidra.trace.database;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -876,16 +875,10 @@ public class DBTrace extends DBCachedDomainObjectAdapter implements Trace, Trace
} }
@Override @Override
public void save(String comment, TaskMonitor monitor) throws IOException, CancelledException { protected void prepareToSave() {
objectManager.flushWbCaches(); try (Transaction tx = openForcedTransaction("flush for save")) {
super.save(comment, monitor); objectManager.flushWbCaches();
} }
@Override
public void saveToPackedFile(File outputFile, TaskMonitor monitor)
throws IOException, CancelledException {
objectManager.flushWbCaches();
super.saveToPackedFile(outputFile, monitor);
} }
public boolean isClosing() { public boolean isClosing() {
@@ -895,9 +888,9 @@ public class DBTrace extends DBCachedDomainObjectAdapter implements Trace, Trace
@Override @Override
protected void close() { protected void close() {
closing = true; closing = true;
objectManager.flushWbCaches(); // NOTE: Any unsaved changes in the write-back cache are unrecoverable
super.close();
objectManager.waitWbWorkers(); objectManager.waitWbWorkers();
super.close();
} }
@Override @Override
@@ -41,16 +41,16 @@ public class DBTraceContentHandler extends DBWithUserDataContentHandler<DBTrace>
static final Class<DBTrace> TRACE_DOMAIN_OBJECT_CLASS = DBTrace.class; static final Class<DBTrace> TRACE_DOMAIN_OBJECT_CLASS = DBTrace.class;
static final String TRACE_CONTENT_DEFAULT_TOOL = "Debugger"; static final String TRACE_CONTENT_DEFAULT_TOOL = "Debugger";
private static final DBTraceLinkContentHandler linkHandler = new DBTraceLinkContentHandler(); private static final DBTraceLinkContentHandler LINK_HANDLER = new DBTraceLinkContentHandler();
@Override @Override
public long createFile(FileSystem fs, FileSystem userfs, String path, String name, public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
DomainObject obj, TaskMonitor monitor) DomainObject obj, TaskMonitor monitor)
throws IOException, InvalidNameException, CancelledException { throws IOException, InvalidNameException, CancelledException {
if (!(obj instanceof DBTrace)) { if (!(obj instanceof DBTrace trace)) {
throw new IOException("Unsupported domain object: " + obj.getClass().getName()); throw new IOException("Unsupported domain object: " + obj.getClass().getName());
} }
return createFile((DBTrace) obj, TRACE_CONTENT_TYPE, fs, path, name, monitor); return createFile(trace, TRACE_CONTENT_TYPE, fs, path, name, monitor);
} }
@Override @Override
@@ -340,6 +340,6 @@ public class DBTraceContentHandler extends DBWithUserDataContentHandler<DBTrace>
@Override @Override
public DBTraceLinkContentHandler getLinkHandler() { public DBTraceLinkContentHandler getLinkHandler() {
return linkHandler; return LINK_HANDLER;
} }
} }
@@ -40,8 +40,9 @@ class DBTraceObjectValueWriteBehindCache {
private final AsyncReference<Boolean, Void> busy = new AsyncReference<>(false); private final AsyncReference<Boolean, Void> busy = new AsyncReference<>(false);
private volatile boolean flushing = false; private volatile boolean flushing = false;
private final Map<DBTraceObject, Map<String, NavigableMap<Long, DBTraceObjectValueBehind>>> cachedValues = private final Map<DBTraceObject,
new HashMap<>(); Map<String, NavigableMap<Long, DBTraceObjectValueBehind>>> cachedValues =
new HashMap<>();
public DBTraceObjectValueWriteBehindCache(DBTraceObjectManager manager) { public DBTraceObjectValueWriteBehindCache(DBTraceObjectManager manager) {
this.manager = manager; this.manager = manager;
@@ -51,7 +52,7 @@ class DBTraceObjectValueWriteBehindCache {
} }
private void workLoop() { private void workLoop() {
while (!manager.trace.isClosed()) { while (!manager.trace.isClosing()) {
try { try {
synchronized (cachedValues) { synchronized (cachedValues) {
if (cachedValues.isEmpty()) { if (cachedValues.isEmpty()) {
@@ -69,7 +70,7 @@ class DBTraceObjectValueWriteBehindCache {
cachedValues.wait(left); cachedValues.wait(left);
} }
} }
if (manager.trace.isClosed()) { if (manager.trace.isClosing()) {
break; break;
} }
writeBatch(); writeBatch();
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -55,31 +55,36 @@ abstract class AbstractTransactionManager {
checkLockingTask(); checkLockingTask();
boolean doPrepare = false;
synchronized (this) { synchronized (this) {
if (getCurrentTransactionInfo() != null && !transactionTerminated) { if (getCurrentTransactionInfo() != null && !transactionTerminated) {
return false; return false;
} }
if (lockCount == 0) { if (lockCount == 0) {
for (DomainObjectAdapterDB domainObj : getDomainObjects()) { doPrepare = true;
if (domainObj.isChanged()) {
domainObj.prepareToSave();
}
}
} }
lockReason = reason; lockReason = reason;
++lockCount; ++lockCount;
return true;
} }
if (doPrepare) {
for (DomainObjectAdapterDB domainObj : getDomainObjects()) {
if (domainObj.isChanged()) {
domainObj.prepareToSave();
}
}
}
return true;
} }
/** /**
* Attempt to obtain a modification lock on the domain object when generating a * Attempt to obtain a modification lock on the domain object when generating a background
* background snapshot. * snapshot.
*
* @param domainObj domain object corresponding to snapshot * @param domainObj domain object corresponding to snapshot
* @param hasProgress true if monitor has progress indicator * @param hasProgress true if monitor has progress indicator
* @param title title to be used for monitor * @param title title to be used for monitor
* @return monitor object if lock obtained successfully, else null which indicates that a * @return monitor object if lock obtained successfully, else null which indicates that a
* modification is in process. * modification is in process.
*/ */
final synchronized LockingTaskMonitor lockForSnapshot(DomainObjectAdapterDB domainObj, final synchronized LockingTaskMonitor lockForSnapshot(DomainObjectAdapterDB domainObj,
boolean hasProgress, String title) { boolean hasProgress, String title) {
@@ -106,9 +111,10 @@ abstract class AbstractTransactionManager {
/** /**
* Force transaction lock and terminate current transaction. * Force transaction lock and terminate current transaction.
*
* @param rollback true if rollback of non-commited changes should occurs, false if commit * @param rollback true if rollback of non-commited changes should occurs, false if commit
* should be done. NOTE: it can be potentially detrimental to commit an incomplete transaction * should be done. NOTE: it can be potentially detrimental to commit an incomplete
* and should be avoided. * transaction and should be avoided.
* @param reason very short reason for requesting lock * @param reason very short reason for requesting lock
*/ */
final void forceLock(boolean rollback, String reason) { final void forceLock(boolean rollback, String reason) {
@@ -131,9 +137,10 @@ abstract class AbstractTransactionManager {
/** /**
* Terminate current transaction. * Terminate current transaction.
*
* @param rollback true if rollback of non-commited changes should occurs, false if commit * @param rollback true if rollback of non-commited changes should occurs, false if commit
* should be done. NOTE: it can be potentially detrimental to commit an incomplete transaction * should be done. NOTE: it can be potentially detrimental to commit an incomplete
* and should be avoided. * transaction and should be avoided.
* @param notify true for listeners to be notified else false * @param notify true for listeners to be notified else false
*/ */
abstract void terminateTransaction(boolean rollback, boolean notify); abstract void terminateTransaction(boolean rollback, boolean notify);
@@ -159,8 +166,7 @@ abstract class AbstractTransactionManager {
} }
/** /**
* Block on active locking task. * Block on active locking task. Do not invoke this method from within a synchronized block.
* Do not invoke this method from within a synchronized block.
*/ */
final void checkLockingTask() { final void checkLockingTask() {
synchronized (this) { synchronized (this) {
@@ -173,6 +179,7 @@ abstract class AbstractTransactionManager {
/** /**
* Throw lock exception if currently locked * Throw lock exception if currently locked
*
* @throws DomainObjectLockedException if currently locked * @throws DomainObjectLockedException if currently locked
*/ */
final void verifyNoLock() throws DomainObjectLockedException { final void verifyNoLock() throws DomainObjectLockedException {
@@ -194,7 +201,7 @@ abstract class AbstractTransactionManager {
} }
} }
final int startTransaction(DomainObjectAdapterDB object, String description, final int startTransactionChecked(DomainObjectAdapterDB object, String description,
AbortedTransactionListener listener, boolean notify) AbortedTransactionListener listener, boolean notify)
throws TerminatedTransactionException { throws TerminatedTransactionException {
@@ -222,47 +229,53 @@ abstract class AbstractTransactionManager {
boolean commit, boolean notify) throws IllegalStateException; boolean commit, boolean notify) throws IllegalStateException;
/** /**
* Returns the undo stack depth. * Returns the undo stack depth. (The number of items on the undo stack) This method is for
* (The number of items on the undo stack) * JUnits.
* This method is for JUnits. *
* @return the undo stack depth * @return the undo stack depth
*/ */
abstract int getUndoStackDepth(); abstract int getUndoStackDepth();
/** /**
* Returns true if there is at least one redo transaction to be redone. * Returns true if there is at least one redo transaction to be redone.
*
* @return true if there is at least one redo transaction to be redone * @return true if there is at least one redo transaction to be redone
*/ */
abstract boolean canRedo(); abstract boolean canRedo();
/** /**
* Returns true if there is at least one undo transaction to be undone. * Returns true if there is at least one undo transaction to be undone.
*
* @return true if there is at least one undo transaction to be undone * @return true if there is at least one undo transaction to be undone
*/ */
abstract boolean canUndo(); abstract boolean canUndo();
/** /**
* Returns the name of the next undo transaction (The most recent change). * Returns the name of the next undo transaction (The most recent change).
*
* @return the name of the next undo transaction (The most recent change) * @return the name of the next undo transaction (The most recent change)
*/ */
abstract String getRedoName(); abstract String getRedoName();
/** /**
* Returns the name of the next redo transaction (The most recent undo). * Returns the name of the next redo transaction (The most recent undo).
*
* @return the name of the next redo transaction (The most recent undo) * @return the name of the next redo transaction (The most recent undo)
*/ */
abstract String getUndoName(); abstract String getUndoName();
/** /**
* Returns the names of all undoable transactions in reverse chronological order. In other * Returns the names of all undoable transactions in reverse chronological order. In other words
* words the transaction at the top of the list must be undone first. * the transaction at the top of the list must be undone first.
*
* @return the names of all undoable transactions in reverse chronological order * @return the names of all undoable transactions in reverse chronological order
*/ */
abstract List<String> getAllUndoNames(); abstract List<String> getAllUndoNames();
/** /**
* Returns the names of all redoable transactions in chronological order. In other words * Returns the names of all redoable transactions in chronological order. In other words the
* the transaction at the top of the list must be redone first. * transaction at the top of the list must be redone first.
*
* @return the names of all redoable transactions in chronological order * @return the names of all redoable transactions in chronological order
*/ */
abstract List<String> getAllRedoNames(); abstract List<String> getAllRedoNames();
@@ -321,7 +334,7 @@ abstract class AbstractTransactionManager {
abstract void doClose(DomainObjectAdapterDB object); abstract void doClose(DomainObjectAdapterDB object);
/** /**
* Set instance as immutable by disabling use of transactions. Attempts to start a transaction * Set instance as immutable by disabling use of transactions. Attempts to start a transaction
* will result in a {@link TerminatedTransactionException}. * will result in a {@link TerminatedTransactionException}.
*/ */
public void setImmutable() { public void setImmutable() {
@@ -133,7 +133,7 @@ public abstract class DomainObjectAdapterDB extends DomainObjectAdapter implemen
* using a shared transaction manager. If either or both is already shared, * using a shared transaction manager. If either or both is already shared,
* a transition to a single shared transaction manager will be * a transition to a single shared transaction manager will be
* performed. * performed.
* @param domainObj * @param domainObj the domain object to synchronize with
* @throws LockException if lock or open transaction is active on either * @throws LockException if lock or open transaction is active on either
* this or the specified domain object * this or the specified domain object
*/ */
@@ -184,7 +184,7 @@ public abstract class DomainObjectAdapterDB extends DomainObjectAdapter implemen
} }
/** /**
* Returns the open handle to the underlying database. * {@return the open handle to the underlying database}
*/ */
public DBHandle getDBHandle() { public DBHandle getDBHandle() {
return dbh; return dbh;
@@ -259,17 +259,17 @@ public abstract class DomainObjectAdapterDB extends DomainObjectAdapter implemen
return transactionMgr.lock(reason); return transactionMgr.lock(reason);
} }
void prepareToSave() { /**
int txId = transactionMgr.startTransaction(this, "Update Metadata", null, true, true); * Prepare to save and store any last minute DB data. The default behavior is to invoke
try { * {@link #updateMetadata()}.
*/
protected void prepareToSave() {
try (Transaction tx = openForcedTransaction("Update Metadata")) {
updateMetadata(); updateMetadata();
} }
catch (IOException e) { catch (IOException e) {
dbError(e); dbError(e);
} }
finally {
transactionMgr.endTransaction(this, txId, true, true);
}
} }
/** /**
@@ -310,24 +310,40 @@ public abstract class DomainObjectAdapterDB extends DomainObjectAdapter implemen
transactionMgr.unlock(handler); transactionMgr.unlock(handler);
} }
private class DomainObjectTransaction extends Transaction {
private final int txId;
DomainObjectTransaction(int txId) {
this.txId = txId;
}
@Override
protected boolean endTransaction(boolean commit) {
DomainObjectAdapterDB.this.endTransaction(txId, commit);
return commit;
}
@Override
public boolean isSubTransaction() {
return true;
}
}
/**
* Open forced transaction which bypasses any lock checking, although method should only be
* used in very controlled situations where a save lock is in place.
* @param description transaction description
* @return {@link AutoCloseable} transaction object
*/
protected Transaction openForcedTransaction(String description) {
int txId = transactionMgr.startTransaction(this, description, null, true, true);
return new DomainObjectTransaction(txId);
}
@Override @Override
public Transaction openTransaction(String description) public Transaction openTransaction(String description)
throws TerminatedTransactionException, IllegalStateException { throws TerminatedTransactionException, IllegalStateException {
return new Transaction() { return new DomainObjectTransaction(startTransaction(description));
int txId = startTransaction(description);
@Override
protected boolean endTransaction(boolean commit) {
DomainObjectAdapterDB.this.endTransaction(txId, commit);
return commit;
}
@Override
public boolean isSubTransaction() {
return true;
}
};
} }
@Override @Override
@@ -348,7 +364,8 @@ public abstract class DomainObjectAdapterDB extends DomainObjectAdapter implemen
while (true) { while (true) {
try { try {
return transactionMgr.startTransaction(this, description, listener, true); return transactionMgr.startTransactionChecked(this, description, listener,
true);
} }
catch (DomainObjectLockedException e) { catch (DomainObjectLockedException e) {
if (!exceptionHandler.apply(e)) { if (!exceptionHandler.apply(e)) {
@@ -103,11 +103,12 @@ class DomainObjectTransactionManager extends AbstractTransactionManager {
throw new IllegalArgumentException("invalid domain object"); throw new IllegalArgumentException("invalid domain object");
} }
if (!force) {
verifyNoLock();
}
if (transaction == null) { if (transaction == null) {
if (!force) {
verifyNoLock();
}
transactionTerminated = false; transactionTerminated = false;
transaction = transaction =
new DomainObjectDBTransaction(domainObj.dbh.startTransaction(), domainObj); new DomainObjectDBTransaction(domainObj.dbh.startTransaction(), domainObj);
@@ -1037,7 +1037,7 @@ class GhidraFolderData {
if (doa.isClosed()) { if (doa.isClosed()) {
throw new ClosedException(); throw new ClosedException();
} }
if (!doa.lock(null)) { if (!doa.lock("create file")) {
throw new IOException("Object is busy and can not be saved"); throw new IOException("Object is busy and can not be saved");
} }
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -152,11 +152,12 @@ class SynchronizedTransactionManager extends AbstractTransactionManager {
synchronized int startTransaction(DomainObjectAdapterDB object, String description, synchronized int startTransaction(DomainObjectAdapterDB object, String description,
AbortedTransactionListener listener, boolean force, boolean notify) { AbortedTransactionListener listener, boolean force, boolean notify) {
if (!force) {
verifyNoLock();
}
if (transaction == null) { if (transaction == null) {
if (!force) {
verifyNoLock();
}
transactionTerminated = false; transactionTerminated = false;
transaction = new SynchronizedTransaction(domainObjectTransactionManagers); transaction = new SynchronizedTransaction(domainObjectTransactionManagers);
int txId = transaction.addEntry(object, description, listener); int txId = transaction.addEntry(object, description, listener);
@@ -311,7 +311,7 @@ class FileActionManager {
locked = false; locked = false;
break; break;
} }
if (!domainObjects[lastIndex].lock(null)) { if (!domainObjects[lastIndex].lock("save changes")) {
String title = "Exit Ghidra"; String title = "Exit Ghidra";
StringBuffer buf = new StringBuffer(); StringBuffer buf = new StringBuffer();
DomainObject d = domainObjects[lastIndex]; DomainObject d = domainObjects[lastIndex];
@@ -400,7 +400,7 @@ public interface DomainObject {
* </pre> * </pre>
* *
* @param description a short description of the changes to be made. * @param description a short description of the changes to be made.
* @return transaction object * @return {@link AutoCloseable} transaction object
* @throws IllegalStateException if this {@link DomainObject} has already been closed. * @throws IllegalStateException if this {@link DomainObject} has already been closed.
*/ */
public Transaction openTransaction(String description) throws IllegalStateException; public Transaction openTransaction(String description) throws IllegalStateException;