diff --git a/.classpath b/.classpath
index 6c5b256cb74c52fb9f102b370d4a2439648a1466..c9078a1e45aef4e78ebf61648ea5fab3090e441a 100644
--- a/.classpath
+++ b/.classpath
@@ -20,7 +20,7 @@
 	<classpathentry kind="lib" path="lib/platform.jar"/>
 	<classpathentry kind="lib" path="lib/jnotify-0.93.jar"/>
 	<classpathentry kind="lib" path="lib/bluecove-gpl-2.1.0.jar" sourcepath="lib/source/bluecove-gpl-2.1.0-sources.jar"/>
-	<classpathentry kind="lib" path="lib/h2small-snapshot-2011-10-25.jar"/>
 	<classpathentry kind="lib" path="lib/bluecove-2.1.0-briar.jar" sourcepath="lib/source/bluecove-2.1.0-briar-sources.jar"/>
+	<classpathentry kind="lib" path="lib/h2small-1.3.161.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/components/net/sf/briar/lifecycle/ShutdownManagerImpl.java b/components/net/sf/briar/lifecycle/ShutdownManagerImpl.java
index 001f8a3f0a4a2cbf03770e1360d99e59ff7fc7b7..fa6eb7833fdb04f31be15f71bcc75be30ad7275c 100644
--- a/components/net/sf/briar/lifecycle/ShutdownManagerImpl.java
+++ b/components/net/sf/briar/lifecycle/ShutdownManagerImpl.java
@@ -15,14 +15,18 @@ class ShutdownManagerImpl implements ShutdownManager {
 		hooks = new HashMap<Integer, Thread>();
 	}
 
-	public synchronized int addShutdownHook(Runnable runnable) {
+	public synchronized int addShutdownHook(Runnable r) {
 		int handle = nextHandle++;
-		Thread hook = new Thread(runnable);
+		Thread hook = createThread(r);
 		hooks.put(handle, hook);
 		Runtime.getRuntime().addShutdownHook(hook);
 		return handle;
 	}
 
+	protected Thread createThread(Runnable r) {
+		return new Thread(r);
+	}
+
 	public synchronized boolean removeShutdownHook(int handle) {
 		Thread hook = hooks.remove(handle);
 		if(hook == null) return false;
diff --git a/components/net/sf/briar/lifecycle/WindowsShutdownManagerImpl.java b/components/net/sf/briar/lifecycle/WindowsShutdownManagerImpl.java
index 7d8939c340365038da773a0f0a5a3113c4153754..7fd9ec3914b1f611f7815e5fd8247cbcecd414f5 100644
--- a/components/net/sf/briar/lifecycle/WindowsShutdownManagerImpl.java
+++ b/components/net/sf/briar/lifecycle/WindowsShutdownManagerImpl.java
@@ -1,21 +1,27 @@
 package net.sf.briar.lifecycle;
 
-import java.awt.HeadlessException;
+import java.util.Map;
+import java.util.TreeMap;
 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.Library;
 import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.platform.win32.WinDef.HINSTANCE;
+import com.sun.jna.platform.win32.WinDef.HMENU;
 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.platform.win32.WinUser.MSG;
 import com.sun.jna.win32.StdCallLibrary;
 import com.sun.jna.win32.StdCallLibrary.StdCallCallback;
+import com.sun.jna.win32.W32APIFunctionMapper;
+import com.sun.jna.win32.W32APITypeMapper;
 
 class WindowsShutdownManagerImpl extends ShutdownManagerImpl {
 
@@ -23,36 +29,36 @@ class WindowsShutdownManagerImpl extends ShutdownManagerImpl {
 		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 static final int WS_MINIMIZE = 0x20000000;
+
+	private final Map<String, Object> options;
 
 	private boolean initialised = false; // Locking: this
 
+	WindowsShutdownManagerImpl() {
+		// Use the Unicode versions of Win32 API calls
+		options = new TreeMap<String, Object>();
+		options.put(Library.OPTION_TYPE_MAPPER, W32APITypeMapper.UNICODE);
+		options.put(Library.OPTION_FUNCTION_MAPPER,
+				W32APIFunctionMapper.UNICODE);
+	}
+
 	@Override
-	public synchronized int addShutdownHook(Runnable runnable) {
+	public synchronized int addShutdownHook(Runnable r) {
 		if(!initialised) initialise();
-		return super.addShutdownHook(new RunOnce(runnable));
+		return super.addShutdownHook(r);
+	}
+
+	@Override
+	protected Thread createThread(Runnable r) {
+		return new StartOnce(r);
 	}
 
 	// 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());
-			}
+			new EventLoop().start();
 		} else {
 			if(LOG.isLoggable(Level.WARNING))
 				LOG.warning("Windows shutdown manager used on non-Windows OS");
@@ -60,38 +66,6 @@ class WindowsShutdownManagerImpl extends ShutdownManagerImpl {
 		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
@@ -106,33 +80,81 @@ class WindowsShutdownManagerImpl extends ShutdownManagerImpl {
 		}
 	}
 
-	private static class RunOnce implements Runnable {
+	private class EventLoop extends Thread {
+
+		@Override
+		public void run() {			
+			try {
+				// Load user32.dll
+				final User32 user32 = (User32) Native.loadLibrary("user32",
+						User32.class, options);
+				// Create a callback to handle the WM_QUERYENDSESSION message
+				WindowProc proc = 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();
+						}
+						// Pass the message to the default window procedure
+						return user32.DefWindowProc(hwnd, msg, wp, lp);
+					}
+				};
+				// Create a native window
+				HWND hwnd = user32.CreateWindowEx(0, "STATIC", "", WS_MINIMIZE,
+						0, 0, 0, 0, null, null, null, null);
+				// Register the callback
+				try {
+					// Use SetWindowLongPtr if available (64-bit safe)
+					user32.SetWindowLongPtr(hwnd, GWL_WNDPROC, proc);
+					if(LOG.isLoggable(Level.INFO))
+						LOG.info("Registered 64-bit callback");
+				} catch(UnsatisfiedLinkError e) {
+					// Use SetWindowLong if SetWindowLongPtr isn't available
+					user32.SetWindowLong(hwnd, GWL_WNDPROC, proc);
+					if(LOG.isLoggable(Level.INFO))
+						LOG.info("Registered 32-bit callback");
+				}
+				// Handle events until the window is destroyed
+				MSG msg = new MSG();
+				while(user32.GetMessage(msg, null, 0, 0) > 0) {
+					user32.TranslateMessage(msg);
+					user32.DispatchMessage(msg);
+				}
+			} catch(UnsatisfiedLinkError e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+			}
+		}
+	}
+
+	private static class StartOnce extends Thread {
 
-		private final Runnable runnable;
 		private final AtomicBoolean called = new AtomicBoolean(false);
 
-		private RunOnce(Runnable runnable) {
-			this.runnable = runnable;
+		private StartOnce(Runnable r) {
+			super(r);
 		}
 
-		public void run() {
-			if(called.getAndSet(true)) return;
-			runnable.run();
+		@Override
+		public void start() {
+			// Ensure the thread is only started once
+			if(!called.getAndSet(true)) super.start();
 		}
 	}
 
 	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);
+		HWND CreateWindowEx(int styleEx, String className, String windowName,
+				int style, int x, int y, int width, int height, HWND parent,
+				HMENU menu, HINSTANCE instance, Pointer param);
 
-		WindowProc GetWindowLongW(HWND hwnd, int index);
-		WindowProc GetWindowLongPtrW(HWND hwnd, int index);
+		LRESULT DefWindowProc(HWND hwnd, int msg, WPARAM wp, LPARAM lp);
+		LRESULT SetWindowLong(HWND hwnd, int index, WindowProc newProc);
+		LRESULT SetWindowLongPtr(HWND hwnd, int index, WindowProc newProc);
 
-		LRESULT SetWindowLongW(HWND hwnd, int index, WindowProc newProc);
-		LRESULT SetWindowLongPtrW(HWND hwnd, int index, WindowProc newProc);
+		int GetMessage(MSG msg, HWND hwnd, int filterMin, int filterMax);
+		boolean TranslateMessage(MSG msg);
+		LRESULT DispatchMessage(MSG msg);
 	}
 
 	private static interface WindowProc extends StdCallCallback {
diff --git a/components/net/sf/briar/plugins/file/RemovableDrivePluginFactory.java b/components/net/sf/briar/plugins/file/RemovableDrivePluginFactory.java
index f42d322c7753e51a1f40e69ba57584e6c03b2d00..92850a4e03a89e22ad96fb1b6fb99e5b341d6e25 100644
--- a/components/net/sf/briar/plugins/file/RemovableDrivePluginFactory.java
+++ b/components/net/sf/briar/plugins/file/RemovableDrivePluginFactory.java
@@ -18,9 +18,14 @@ public class RemovableDrivePluginFactory implements BatchPluginFactory {
 		if(OsUtils.isLinux()) {
 			finder = new LinuxRemovableDriveFinder();
 			monitor = new LinuxRemovableDriveMonitor();
-		} else if(OsUtils.isMac()) {
+		} else if(OsUtils.isMacLeopardOrNewer()) {
 			finder = new MacRemovableDriveFinder();
 			monitor = new MacRemovableDriveMonitor();
+		} else if(OsUtils.isMac()) {
+			// JNotify requires OS X 10.5 or newer, so we have to poll
+			finder = new MacRemovableDriveFinder();
+			monitor = new PollingRemovableDriveMonitor(finder,
+					POLLING_INTERVAL);
 		} else if(OsUtils.isWindows()) {
 			finder = new WindowsRemovableDriveFinder();
 			monitor = new PollingRemovableDriveMonitor(finder,
diff --git a/lib/h2small-1.3.161.jar b/lib/h2small-1.3.161.jar
new file mode 100644
index 0000000000000000000000000000000000000000..b209bed31c7ecf658d4d930b8552d72e660f9c79
Binary files /dev/null and b/lib/h2small-1.3.161.jar differ
diff --git a/lib/h2small-snapshot-2011-10-25.jar b/lib/h2small-snapshot-2011-10-25.jar
deleted file mode 100644
index 85821fc58c0394144a60c60ecd8c738d49012595..0000000000000000000000000000000000000000
Binary files a/lib/h2small-snapshot-2011-10-25.jar and /dev/null differ
diff --git a/test/build.xml b/test/build.xml
index b3c7132f56dd40102bc7cb3a674430df9239ba7e..e7df24043d059788a956d140e7a82852ad623db9 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -27,6 +27,7 @@
 			<test name='net.sf.briar.i18n.I18nTest'/>
 			<test name='net.sf.briar.invitation.InvitationWorkerTest'/>
 			<test name='net.sf.briar.lifecycle.ShutdownManagerImplTest'/>
+			<test name='net.sf.briar.lifecycle.WindowsShutdownManagerImplTest'/>
 			<test name='net.sf.briar.plugins.PluginManagerImplTest'/>
 			<test name='net.sf.briar.plugins.file.LinuxRemovableDriveFinderTest'/>
 			<test name='net.sf.briar.plugins.file.MacRemovableDriveFinderTest'/>
diff --git a/test/net/sf/briar/lifecycle/ShutdownManagerImplTest.java b/test/net/sf/briar/lifecycle/ShutdownManagerImplTest.java
index fc2482c9fa300e2118bca22a6fca1660e3391b40..a7be414c9611dc1d21fb6a64bbd7d0f885be1e25 100644
--- a/test/net/sf/briar/lifecycle/ShutdownManagerImplTest.java
+++ b/test/net/sf/briar/lifecycle/ShutdownManagerImplTest.java
@@ -12,7 +12,7 @@ public class ShutdownManagerImplTest extends TestCase {
 
 	@Test
 	public void testAddAndRemove() {
-		ShutdownManager s = new ShutdownManagerImpl();
+		ShutdownManager s = createShutdownManager();
 		Set<Integer> handles = new HashSet<Integer>();
 		for(int i = 0; i < 100; i++) {
 			int handle = s.addShutdownHook(new Runnable() {
@@ -26,4 +26,8 @@ public class ShutdownManagerImplTest extends TestCase {
 		// The hooks should no longer be removable
 		for(int handle : handles) assertFalse(s.removeShutdownHook(handle));
 	}
+
+	protected ShutdownManager createShutdownManager() {
+		return new ShutdownManagerImpl();
+	}
 }
diff --git a/test/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java b/test/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..42795b4ca2b80ab81f5cb5cf361e05a57012a5a0
--- /dev/null
+++ b/test/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java
@@ -0,0 +1,39 @@
+package net.sf.briar.lifecycle;
+
+import net.sf.briar.api.lifecycle.ShutdownManager;
+
+import org.junit.Test;
+
+public class WindowsShutdownManagerImplTest extends ShutdownManagerImplTest {
+
+	@Override
+	protected ShutdownManager createShutdownManager() {
+		return new WindowsShutdownManagerImpl();
+	}
+
+	@Test
+	public void testManagerWaitsForHooksToRun() {
+		WindowsShutdownManagerImpl s = new WindowsShutdownManagerImpl();
+		SlowHook[] hooks = new SlowHook[10];
+		for(int i = 0; i < hooks.length; i++) {
+			hooks[i] = new SlowHook();
+			s.addShutdownHook(hooks[i]);
+		}
+		s.runShutdownHooks();
+		for(int i = 0; i < hooks.length; i++) assertTrue(hooks[i].finished);
+	}
+
+	private static class SlowHook implements Runnable {
+
+		private volatile boolean finished = false;
+
+		public void run() {
+			try {
+				Thread.sleep(100);
+				finished = true;
+			} catch(InterruptedException e) {
+				// Don't finish
+			}
+		}
+	}
+}
diff --git a/test/net/sf/briar/plugins/PluginManagerImplTest.java b/test/net/sf/briar/plugins/PluginManagerImplTest.java
index 0a1b4693ff88dd152693fcdc11a458c67541c17d..276f16236a9019253c39538350a8d0dcf64c3822 100644
--- a/test/net/sf/briar/plugins/PluginManagerImplTest.java
+++ b/test/net/sf/briar/plugins/PluginManagerImplTest.java
@@ -41,9 +41,12 @@ public class PluginManagerImplTest extends TestCase {
 		Poller poller = new PollerImpl();
 		PluginManagerImpl p = new PluginManagerImpl(db, executor, poller,
 				dispatcher, uiCallback);
-		// The Bluetooth plugin will not start without a Bluetooth device, so
-		// we expect two plugins to be started
-		assertEquals(2, p.startPlugins());
-		assertEquals(2, p.stopPlugins());
+		// We expect either 2 or 3 plugins to be started, depending on whether
+		// the test machine has a Bluetooth device
+		int started = p.startPlugins();
+		int stopped = p.stopPlugins();
+		assertEquals(started, stopped);
+		assertTrue(started >= 2);
+		assertTrue(started <= 3);
 	}
 }
diff --git a/test/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java b/test/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java
index 1ed814d038dfb03eccebcb07ddac128616d96b44..c320968d0ed687067f30bd52f26d2afebe478192 100644
--- a/test/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java
+++ b/test/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java
@@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit;
 import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
 import net.sf.briar.plugins.file.RemovableDriveMonitor.Callback;
+import net.sf.briar.util.OsUtils;
 
 import org.junit.After;
 import org.junit.Before;
@@ -25,6 +26,10 @@ public class UnixRemovableDriveMonitorTest extends TestCase {
 
 	@Test
 	public void testNonexistentDir() throws Exception {
+		if(!OsUtils.isLinux() || OsUtils.isMacLeopardOrNewer()) {
+			System.err.println("Warning: Skipping test");
+			return;
+		}
 		File doesNotExist = new File(testDir, "doesNotExist");
 		RemovableDriveMonitor monitor = createMonitor(doesNotExist);
 		monitor.start(null);
@@ -33,6 +38,10 @@ public class UnixRemovableDriveMonitorTest extends TestCase {
 
 	@Test
 	public void testOneCallbackPerFile() throws Exception {
+		if(!OsUtils.isLinux() || OsUtils.isMacLeopardOrNewer()) {
+			System.err.println("Warning: Skipping test");
+			return;
+		}
 		// Create a callback that will wait for two files before stopping
 		final List<File> detected = new ArrayList<File>();
 		final CountDownLatch latch = new CountDownLatch(2);
diff --git a/util/net/sf/briar/util/OsUtils.java b/util/net/sf/briar/util/OsUtils.java
index f8e8553da5ca15ee90872b8eb8a12a191b8b680d..dd32ad897fbf4314b446fe49c16ae8c63aa8dece 100644
--- a/util/net/sf/briar/util/OsUtils.java
+++ b/util/net/sf/briar/util/OsUtils.java
@@ -3,6 +3,7 @@ package net.sf.briar.util;
 public class OsUtils {
 
 	private static final String os = System.getProperty("os.name");
+	private static final String version = System.getProperty("os.version");
 
 	public static boolean isWindows() {
 		return os.indexOf("Windows") != -1;
@@ -12,6 +13,19 @@ public class OsUtils {
 		return os.indexOf("Mac OS") != -1;
 	}
 
+	public static boolean isMacLeopardOrNewer() {
+		if(!isMac() || version == null) return false;
+		try {
+			String[] v = version.split("\\.");
+			if(v.length != 3) return false;
+			int major = Integer.parseInt(v[0]);
+			int minor = Integer.parseInt(v[1]);
+			return major >= 10 && minor >= 5;
+		} catch(NumberFormatException e) {
+			return false;
+		}
+	}
+
 	public static boolean isLinux() {
 		return os.indexOf("Linux") != -1;
 	}