diff --git a/api/net/sf/briar/api/lifecycle/ShutdownManager.java b/api/net/sf/briar/api/lifecycle/ShutdownManager.java
new file mode 100644
index 000000000..8d8b0af40
--- /dev/null
+++ b/api/net/sf/briar/api/lifecycle/ShutdownManager.java
@@ -0,0 +1,16 @@
+package net.sf.briar.api.lifecycle;
+
+public interface ShutdownManager {
+
+ /**
+ * Registers a hook to be run when the JVM shuts down and returns a handle
+ * that can be used to remove the hook.
+ */
+ int addShutdownHook(Runnable hook);
+
+ /**
+ * Removes the shutdown hook identified by the given handle and returns
+ * true if the hook was removed.
+ */
+ boolean removeShutdownHook(int handle);
+}
diff --git a/build-common.xml b/build-common.xml
index 7d7504ff6..681fdb9a1 100644
--- a/build-common.xml
+++ b/build-common.xml
@@ -21,6 +21,7 @@
+
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index d8b2d5c8d..92420cca1 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -16,6 +16,8 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import net.sf.briar.api.Bytes;
import net.sf.briar.api.ContactId;
@@ -38,6 +40,7 @@ import net.sf.briar.api.db.event.RatingChangedEvent;
import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
import net.sf.briar.api.db.event.TransportAddedEvent;
+import net.sf.briar.api.lifecycle.ShutdownManager;
import net.sf.briar.api.protocol.Ack;
import net.sf.briar.api.protocol.AuthorId;
import net.sf.briar.api.protocol.Batch;
@@ -73,6 +76,9 @@ import com.google.inject.Inject;
class DatabaseComponentImpl implements DatabaseComponent,
DatabaseCleaner.Callback {
+ private static final Logger LOG =
+ Logger.getLogger(DatabaseComponentImpl.class.getName());
+
/*
* Locks must always be acquired in alphabetical order. See the Database
* interface to find out which calls require which locks.
@@ -97,27 +103,61 @@ DatabaseCleaner.Callback {
private final Database db;
private final DatabaseCleaner cleaner;
+ private final ShutdownManager shutdown;
private final List listeners =
new ArrayList(); // Locking: self
+
private final Object spaceLock = new Object();
private long bytesStoredSinceLastCheck = 0L; // Locking: spaceLock
private long timeOfLastCheck = 0L; // Locking: spaceLock
+ private final Object openCloseLock = new Object();
+ private boolean open = false; // Locking: openCloseLock;
+ private int shutdownHandle = -1; // Locking: openCloseLock;
+
@Inject
- DatabaseComponentImpl(Database db, DatabaseCleaner cleaner) {
+ DatabaseComponentImpl(Database db, DatabaseCleaner cleaner,
+ ShutdownManager shutdown) {
this.db = db;
this.cleaner = cleaner;
+ this.shutdown = shutdown;
}
public void open(boolean resume) throws DbException, IOException {
- db.open(resume);
- cleaner.startCleaning(this, MAX_MS_BETWEEN_SPACE_CHECKS);
+ synchronized(openCloseLock) {
+ if(open) throw new IllegalStateException();
+ open = true;
+ db.open(resume);
+ cleaner.startCleaning(this, MAX_MS_BETWEEN_SPACE_CHECKS);
+ shutdownHandle = shutdown.addShutdownHook(new Runnable() {
+ public void run() {
+ try {
+ synchronized(openCloseLock) {
+ shutdownHandle = -1;
+ close();
+ }
+ } catch(DbException e) {
+ if(LOG.isLoggable(Level.WARNING))
+ LOG.warning(e.getMessage());
+ } catch(IOException e) {
+ if(LOG.isLoggable(Level.WARNING))
+ LOG.warning(e.getMessage());
+ }
+ }
+ });
+ }
}
public void close() throws DbException, IOException {
- cleaner.stopCleaning();
- db.close();
+ synchronized(openCloseLock) {
+ if(!open) return;
+ open = false;
+ if(shutdownHandle != -1)
+ shutdown.removeShutdownHook(shutdownHandle);
+ cleaner.stopCleaning();
+ db.close();
+ }
}
public void addListener(DatabaseListener d) {
diff --git a/components/net/sf/briar/db/DatabaseModule.java b/components/net/sf/briar/db/DatabaseModule.java
index 9df6352ec..ab58a5efa 100644
--- a/components/net/sf/briar/db/DatabaseModule.java
+++ b/components/net/sf/briar/db/DatabaseModule.java
@@ -8,6 +8,7 @@ import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DatabaseDirectory;
import net.sf.briar.api.db.DatabaseMaxSize;
import net.sf.briar.api.db.DatabasePassword;
+import net.sf.briar.api.lifecycle.ShutdownManager;
import net.sf.briar.api.protocol.GroupFactory;
import net.sf.briar.api.transport.ConnectionContextFactory;
import net.sf.briar.api.transport.ConnectionWindowFactory;
@@ -35,7 +36,7 @@ public class DatabaseModule extends AbstractModule {
@Provides @Singleton
DatabaseComponent getDatabaseComponent(Database db,
- DatabaseCleaner cleaner) {
- return new DatabaseComponentImpl(db, cleaner);
+ DatabaseCleaner cleaner, ShutdownManager shutdown) {
+ return new DatabaseComponentImpl(db, cleaner, shutdown);
}
}
diff --git a/components/net/sf/briar/lifecycle/LifecycleModule.java b/components/net/sf/briar/lifecycle/LifecycleModule.java
new file mode 100644
index 000000000..241e41a09
--- /dev/null
+++ b/components/net/sf/briar/lifecycle/LifecycleModule.java
@@ -0,0 +1,16 @@
+package net.sf.briar.lifecycle;
+
+import net.sf.briar.api.lifecycle.ShutdownManager;
+import net.sf.briar.util.OsUtils;
+
+import com.google.inject.AbstractModule;
+
+public class LifecycleModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ if(OsUtils.isWindows())
+ bind(ShutdownManager.class).to(WindowsShutdownManagerImpl.class);
+ else bind(ShutdownManager.class).to(ShutdownManagerImpl.class);
+ }
+}
diff --git a/components/net/sf/briar/lifecycle/ShutdownManagerImpl.java b/components/net/sf/briar/lifecycle/ShutdownManagerImpl.java
new file mode 100644
index 000000000..001f8a3f0
--- /dev/null
+++ b/components/net/sf/briar/lifecycle/ShutdownManagerImpl.java
@@ -0,0 +1,31 @@
+package net.sf.briar.lifecycle;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import net.sf.briar.api.lifecycle.ShutdownManager;
+
+class ShutdownManagerImpl implements ShutdownManager {
+
+ protected final Map hooks; // Locking: this
+
+ private int nextHandle = 0; // Locking: this
+
+ ShutdownManagerImpl() {
+ hooks = new HashMap();
+ }
+
+ public synchronized int addShutdownHook(Runnable runnable) {
+ int handle = nextHandle++;
+ Thread hook = new Thread(runnable);
+ hooks.put(handle, hook);
+ Runtime.getRuntime().addShutdownHook(hook);
+ return handle;
+ }
+
+ public synchronized boolean removeShutdownHook(int handle) {
+ Thread hook = hooks.remove(handle);
+ if(hook == null) return false;
+ else return Runtime.getRuntime().removeShutdownHook(hook);
+ }
+}
diff --git a/components/net/sf/briar/lifecycle/WindowsShutdownManagerImpl.java b/components/net/sf/briar/lifecycle/WindowsShutdownManagerImpl.java
new file mode 100644
index 000000000..7d8939c34
--- /dev/null
+++ b/components/net/sf/briar/lifecycle/WindowsShutdownManagerImpl.java
@@ -0,0 +1,142 @@
+package net.sf.briar.lifecycle;
+
+import java.awt.HeadlessException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.swing.JFrame;
+
+import net.sf.briar.util.OsUtils;
+
+import com.sun.jna.Native;
+import com.sun.jna.platform.win32.WinDef.HWND;
+import com.sun.jna.platform.win32.WinDef.LPARAM;
+import com.sun.jna.platform.win32.WinDef.LRESULT;
+import com.sun.jna.platform.win32.WinDef.WPARAM;
+import com.sun.jna.win32.StdCallLibrary;
+import com.sun.jna.win32.StdCallLibrary.StdCallCallback;
+
+class WindowsShutdownManagerImpl extends ShutdownManagerImpl {
+
+ private static final Logger LOG =
+ Logger.getLogger(WindowsShutdownManagerImpl.class.getName());
+
+ private static final int WM_QUERYENDSESSION = 17;
+ private static final int WM_ENDSESSION = 22;
+ private static final int GWL_WNDPROC = -4;
+
+ private boolean initialised = false; // Locking: this
+
+ @Override
+ public synchronized int addShutdownHook(Runnable runnable) {
+ if(!initialised) initialise();
+ return super.addShutdownHook(new RunOnce(runnable));
+ }
+
+ // Locking: this
+ private void initialise() {
+ if(OsUtils.isWindows()) {
+ try {
+ HWND hwnd = new HWND();
+ hwnd.setPointer(Native.getComponentPointer(new JFrame()));
+ User32 u = (User32) Native.loadLibrary("user32", User32.class);
+ try {
+ // Load the 64-bit functions
+ setCallback64Bit(u, hwnd);
+ } catch(UnsatisfiedLinkError e) {
+ // Load the 32-bit functions
+ setCallback32Bit(u, hwnd);
+ }
+ } catch(HeadlessException e) {
+ if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+ } catch(UnsatisfiedLinkError e) {
+ if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+ }
+ } else {
+ if(LOG.isLoggable(Level.WARNING))
+ LOG.warning("Windows shutdown manager used on non-Windows OS");
+ }
+ initialised = true;
+ }
+
+ private void setCallback64Bit(final User32 user32, HWND hwnd) {
+ final WindowProc oldProc = user32.GetWindowLongPtrW(hwnd, GWL_WNDPROC);
+ WindowProc newProc = new WindowProc() {
+ public LRESULT callback(HWND hwnd, int msg, WPARAM wp, LPARAM lp) {
+ if(msg == WM_QUERYENDSESSION) {
+ // It's safe to delay returning from this message
+ runShutdownHooks();
+ } else if(msg == WM_ENDSESSION) {
+ // Return immediately or the JVM crashes on return
+ }
+ return user32.CallWindowProcPtrW(oldProc, hwnd, msg, wp, lp);
+ }
+ };
+ user32.SetWindowLongPtrW(hwnd, GWL_WNDPROC, newProc);
+ }
+
+ private void setCallback32Bit(final User32 user32, HWND hwnd) {
+ final WindowProc oldProc = user32.GetWindowLongW(hwnd, GWL_WNDPROC);
+ WindowProc newProc = new WindowProc() {
+ public LRESULT callback(HWND hwnd, int msg, WPARAM wp, LPARAM lp) {
+ if(msg == WM_QUERYENDSESSION) {
+ // It's safe to delay returning from this message
+ runShutdownHooks();
+ } else if(msg == WM_ENDSESSION) {
+ // Return immediately or the JVM crashes on return
+ }
+ return user32.CallWindowProcW(oldProc, hwnd, msg, wp, lp);
+ }
+ };
+ user32.SetWindowLongW(hwnd, GWL_WNDPROC, newProc);
+ }
+
+ // Package access for testing
+ synchronized void runShutdownHooks() {
+ // Start each hook in its own thread
+ for(Thread hook : hooks.values()) hook.start();
+ // Wait for all the hooks to finish
+ for(Thread hook : hooks.values()) {
+ try {
+ hook.join();
+ } catch(InterruptedException e) {
+ if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+ }
+ }
+ }
+
+ private static class RunOnce implements Runnable {
+
+ private final Runnable runnable;
+ private final AtomicBoolean called = new AtomicBoolean(false);
+
+ private RunOnce(Runnable runnable) {
+ this.runnable = runnable;
+ }
+
+ public void run() {
+ if(called.getAndSet(true)) return;
+ runnable.run();
+ }
+ }
+
+ private static interface User32 extends StdCallLibrary {
+
+ LRESULT CallWindowProcW(WindowProc oldProc, HWND hwnd, int msg,
+ WPARAM wp, LPARAM lp);
+ LRESULT CallWindowProcPtrW(WindowProc oldProc, HWND hwnd, int msg,
+ WPARAM wp, LPARAM lp);
+
+ WindowProc GetWindowLongW(HWND hwnd, int index);
+ WindowProc GetWindowLongPtrW(HWND hwnd, int index);
+
+ LRESULT SetWindowLongW(HWND hwnd, int index, WindowProc newProc);
+ LRESULT SetWindowLongPtrW(HWND hwnd, int index, WindowProc newProc);
+ }
+
+ private static interface WindowProc extends StdCallCallback {
+
+ public LRESULT callback(HWND hwnd, int msg, WPARAM wp, LPARAM lp);
+ }
+}
diff --git a/components/net/sf/briar/plugins/socket/LanSocketPlugin.java b/components/net/sf/briar/plugins/socket/LanSocketPlugin.java
index 4cd0d9efb..181a9b8ff 100644
--- a/components/net/sf/briar/plugins/socket/LanSocketPlugin.java
+++ b/components/net/sf/briar/plugins/socket/LanSocketPlugin.java
@@ -19,7 +19,7 @@ import net.sf.briar.api.transport.StreamTransportConnection;
import net.sf.briar.util.ByteUtils;
/** A socket plugin that supports exchanging invitations over a LAN. */
-public class LanSocketPlugin extends SimpleSocketPlugin {
+class LanSocketPlugin extends SimpleSocketPlugin {
private static final Logger LOG =
Logger.getLogger(LanSocketPlugin.class.getName());
diff --git a/components/net/sf/briar/transport/ConnectionDispatcherImpl.java b/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
index adbb5ad45..54cce8ab0 100644
--- a/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
+++ b/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
@@ -22,7 +22,7 @@ import net.sf.briar.api.transport.TransportConstants;
import com.google.inject.Inject;
-public class ConnectionDispatcherImpl implements ConnectionDispatcher {
+class ConnectionDispatcherImpl implements ConnectionDispatcher {
private static final Logger LOG =
Logger.getLogger(ConnectionDispatcherImpl.class.getName());
diff --git a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
index 940b37c09..34eacc868 100644
--- a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
+++ b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
@@ -28,6 +28,7 @@ import net.sf.briar.api.db.event.DatabaseEvent;
import net.sf.briar.api.db.event.DatabaseListener;
import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
import net.sf.briar.api.db.event.TransportAddedEvent;
+import net.sf.briar.api.lifecycle.ShutdownManager;
import net.sf.briar.api.protocol.Transport;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.protocol.TransportIndex;
@@ -46,6 +47,7 @@ DatabaseListener {
private final CryptoComponent crypto;
private final DatabaseComponent db;
private final Executor executor;
+ private final ShutdownManager shutdown;
private final Cipher ivCipher; // Locking: this
private final Map expected; // Locking: this
@@ -53,10 +55,11 @@ DatabaseListener {
@Inject
ConnectionRecogniserImpl(CryptoComponent crypto, DatabaseComponent db,
- Executor executor) {
+ Executor executor, ShutdownManager shutdown) {
this.crypto = crypto;
this.db = db;
this.executor = executor;
+ this.shutdown = shutdown;
ivCipher = crypto.getIvCipher();
expected = new HashMap();
db.addListener(this);
@@ -64,8 +67,8 @@ DatabaseListener {
// Locking: this
private void initialise() throws DbException {
- Runtime.getRuntime().addShutdownHook(new Thread() {
- @Override
+ assert !initialised;
+ shutdown.addShutdownHook(new Runnable() {
public void run() {
eraseSecrets();
}
diff --git a/components/net/sf/briar/transport/stream/IncomingStreamConnection.java b/components/net/sf/briar/transport/stream/IncomingStreamConnection.java
index d3da63f21..994693d0b 100644
--- a/components/net/sf/briar/transport/stream/IncomingStreamConnection.java
+++ b/components/net/sf/briar/transport/stream/IncomingStreamConnection.java
@@ -13,7 +13,7 @@ import net.sf.briar.api.transport.ConnectionWriter;
import net.sf.briar.api.transport.ConnectionWriterFactory;
import net.sf.briar.api.transport.StreamTransportConnection;
-public class IncomingStreamConnection extends StreamConnection {
+class IncomingStreamConnection extends StreamConnection {
private final ConnectionContext ctx;
private final byte[] encryptedIv;
diff --git a/components/net/sf/briar/transport/stream/OutgoingStreamConnection.java b/components/net/sf/briar/transport/stream/OutgoingStreamConnection.java
index 178190e2a..ba37e28de 100644
--- a/components/net/sf/briar/transport/stream/OutgoingStreamConnection.java
+++ b/components/net/sf/briar/transport/stream/OutgoingStreamConnection.java
@@ -15,7 +15,7 @@ import net.sf.briar.api.transport.ConnectionWriter;
import net.sf.briar.api.transport.ConnectionWriterFactory;
import net.sf.briar.api.transport.StreamTransportConnection;
-public class OutgoingStreamConnection extends StreamConnection {
+class OutgoingStreamConnection extends StreamConnection {
private final TransportIndex transportIndex;
diff --git a/components/net/sf/briar/transport/stream/StreamConnectionFactoryImpl.java b/components/net/sf/briar/transport/stream/StreamConnectionFactoryImpl.java
index cfaf8fe74..514a70230 100644
--- a/components/net/sf/briar/transport/stream/StreamConnectionFactoryImpl.java
+++ b/components/net/sf/briar/transport/stream/StreamConnectionFactoryImpl.java
@@ -13,7 +13,7 @@ import net.sf.briar.api.transport.StreamTransportConnection;
import com.google.inject.Inject;
-public class StreamConnectionFactoryImpl implements StreamConnectionFactory {
+class StreamConnectionFactoryImpl implements StreamConnectionFactory {
private final ConnectionReaderFactory connReaderFactory;
private final ConnectionWriterFactory connWriterFactory;
diff --git a/test/build.xml b/test/build.xml
index 2644843b7..b3c7132f5 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -26,6 +26,7 @@
+
diff --git a/test/net/sf/briar/ProtocolIntegrationTest.java b/test/net/sf/briar/ProtocolIntegrationTest.java
index cd0ad582f..1ed4eb0fb 100644
--- a/test/net/sf/briar/ProtocolIntegrationTest.java
+++ b/test/net/sf/briar/ProtocolIntegrationTest.java
@@ -54,6 +54,7 @@ import net.sf.briar.api.transport.ConnectionWriter;
import net.sf.briar.api.transport.ConnectionWriterFactory;
import net.sf.briar.crypto.CryptoModule;
import net.sf.briar.db.DatabaseModule;
+import net.sf.briar.lifecycle.LifecycleModule;
import net.sf.briar.protocol.ProtocolModule;
import net.sf.briar.protocol.writers.ProtocolWritersModule;
import net.sf.briar.serial.SerialModule;
@@ -102,10 +103,11 @@ public class ProtocolIntegrationTest extends TestCase {
}
};
Injector i = Guice.createInjector(testModule, new CryptoModule(),
- new DatabaseModule(), new ProtocolModule(),
- new ProtocolWritersModule(), new SerialModule(),
- new TestDatabaseModule(), new TransportBatchModule(),
- new TransportModule(), new TransportStreamModule());
+ new DatabaseModule(), new LifecycleModule(),
+ new ProtocolModule(), new ProtocolWritersModule(),
+ new SerialModule(), new TestDatabaseModule(),
+ new TransportBatchModule(), new TransportModule(),
+ new TransportStreamModule());
connectionContextFactory =
i.getInstance(ConnectionContextFactory.class);
connectionReaderFactory = i.getInstance(ConnectionReaderFactory.class);
diff --git a/test/net/sf/briar/db/DatabaseComponentImplTest.java b/test/net/sf/briar/db/DatabaseComponentImplTest.java
index da67ef8b4..0a2c3f612 100644
--- a/test/net/sf/briar/db/DatabaseComponentImplTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentImplTest.java
@@ -7,6 +7,7 @@ import java.util.Collections;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.lifecycle.ShutdownManager;
import net.sf.briar.db.DatabaseCleaner.Callback;
import org.jmock.Expectations;
@@ -25,11 +26,12 @@ public class DatabaseComponentImplTest extends DatabaseComponentTest {
@SuppressWarnings("unchecked")
final Database