From db9ebfd6fc34b67407c9ac04d3de156c3f21babc Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Mon, 15 May 2023 13:41:51 +0100
Subject: [PATCH] Add more wrapper states, allow wrapper to be reused.

---
 .../onionwrapper/AndroidTorWrapper.java       |  39 ++---
 .../onionwrapper/AbstractTorWrapper.java      | 161 +++++++++++++-----
 .../briarproject/onionwrapper/TorWrapper.java |  64 ++++++-
 .../onionwrapper/JavaTorWrapper.java          |   9 +-
 .../onionwrapper/WindowsTorWrapper.java       |  47 +----
 .../onionwrapper/BootstrapTest.java           |  13 +-
 6 files changed, 201 insertions(+), 132 deletions(-)

diff --git a/onionwrapper-android/src/main/java/org/briarproject/onionwrapper/AndroidTorWrapper.java b/onionwrapper-android/src/main/java/org/briarproject/onionwrapper/AndroidTorWrapper.java
index 26a6b73..7c5c570 100644
--- a/onionwrapper-android/src/main/java/org/briarproject/onionwrapper/AndroidTorWrapper.java
+++ b/onionwrapper-android/src/main/java/org/briarproject/onionwrapper/AndroidTorWrapper.java
@@ -5,7 +5,6 @@ import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
-import android.os.Build;
 
 import org.briarproject.android.dontkillmelib.wakelock.AndroidWakeLock;
 import org.briarproject.android.dontkillmelib.wakelock.AndroidWakeLockManager;
@@ -24,6 +23,9 @@ import java.util.logging.Logger;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
+import static android.os.Build.CPU_ABI;
+import static android.os.Build.CPU_ABI2;
+import static android.os.Build.SUPPORTED_ABIS;
 import static android.os.Build.VERSION.SDK_INT;
 import static java.util.Arrays.asList;
 import static java.util.logging.Level.INFO;
@@ -42,8 +44,7 @@ public class AndroidTorWrapper extends AbstractTorWrapper {
 	private static final String OBFS4_LIB_NAME = "libobfs4proxy.so";
 	private static final String SNOWFLAKE_LIB_NAME = "libsnowflake.so";
 
-	private static final Logger LOG =
-			getLogger(AndroidTorWrapper.class.getName());
+	private static final Logger LOG = getLogger(AndroidTorWrapper.class.getName());
 
 	private final Application app;
 	private final AndroidWakeLock wakeLock;
@@ -74,8 +75,7 @@ public class AndroidTorWrapper extends AbstractTorWrapper {
 			File torDirectory,
 			int torSocksPort,
 			int torControlPort) {
-		super(ioExecutor, eventExecutor, architecture, torDirectory,
-				torSocksPort, torControlPort);
+		super(ioExecutor, eventExecutor, architecture, torDirectory, torSocksPort, torControlPort);
 		this.app = app;
 		wakeLock = wakeLockManager.createWakeLock("TorPlugin");
 		String nativeLibDir = app.getApplicationInfo().nativeLibraryDir;
@@ -119,7 +119,7 @@ public class AndroidTorWrapper extends AbstractTorWrapper {
 	}
 
 	@Override
-	public void stop() throws IOException {
+	public void stop() throws IOException, InterruptedException {
 		try {
 			super.stop();
 		} finally {
@@ -139,8 +139,7 @@ public class AndroidTorWrapper extends AbstractTorWrapper {
 
 	@Override
 	protected File getSnowflakeExecutableFile() {
-		return snowflakeLib.exists() ? snowflakeLib
-				: super.getSnowflakeExecutableFile();
+		return snowflakeLib.exists() ? snowflakeLib : super.getSnowflakeExecutableFile();
 	}
 
 	@Override
@@ -150,18 +149,15 @@ public class AndroidTorWrapper extends AbstractTorWrapper {
 
 	@Override
 	protected void installObfs4Executable() throws IOException {
-		installExecutable(super.getObfs4ExecutableFile(), obfs4Lib,
-				OBFS4_LIB_NAME);
+		installExecutable(super.getObfs4ExecutableFile(), obfs4Lib, OBFS4_LIB_NAME);
 	}
 
 	@Override
 	protected void installSnowflakeExecutable() throws IOException {
-		installExecutable(super.getSnowflakeExecutableFile(), snowflakeLib,
-				SNOWFLAKE_LIB_NAME);
+		installExecutable(super.getSnowflakeExecutableFile(), snowflakeLib, SNOWFLAKE_LIB_NAME);
 	}
 
-	private void installExecutable(File extracted, File lib, String libName)
-			throws IOException {
+	private void installExecutable(File extracted, File lib, String libName) throws IOException {
 		if (lib.exists()) {
 			// If an older version left behind a binary, delete it
 			if (extracted.exists()) {
@@ -177,8 +173,7 @@ public class AndroidTorWrapper extends AbstractTorWrapper {
 		}
 	}
 
-	private void extractLibraryFromApk(String libName, File dest)
-			throws IOException {
+	private void extractLibraryFromApk(String libName, File dest) throws IOException {
 		File sourceDir = new File(app.getApplicationInfo().sourceDir);
 		if (sourceDir.isFile()) {
 			// Look for other APK files in the same directory, if we're allowed
@@ -189,12 +184,10 @@ public class AndroidTorWrapper extends AbstractTorWrapper {
 		for (File apk : findApkFiles(sourceDir)) {
 			@SuppressWarnings("IOStreamConstructor")
 			ZipInputStream zin = new ZipInputStream(new FileInputStream(apk));
-			for (ZipEntry e = zin.getNextEntry(); e != null;
-			     e = zin.getNextEntry()) {
+			for (ZipEntry e = zin.getNextEntry(); e != null; e = zin.getNextEntry()) {
 				if (libPaths.contains(e.getName())) {
 					if (LOG.isLoggable(INFO)) {
-						LOG.info("Extracting " + e.getName()
-								+ " from " + apk.getAbsolutePath());
+						LOG.info("Extracting " + e.getName() + " from " + apk.getAbsolutePath());
 					}
 					extract(zin, dest); // Zip input stream will be closed
 					return;
@@ -243,10 +236,10 @@ public class AndroidTorWrapper extends AbstractTorWrapper {
 	private Collection<String> getSupportedArchitectures() {
 		List<String> abis = new ArrayList<>();
 		if (SDK_INT >= 21) {
-			abis.addAll(asList(Build.SUPPORTED_ABIS));
+			abis.addAll(asList(SUPPORTED_ABIS));
 		} else {
-			abis.add(Build.CPU_ABI);
-			if (Build.CPU_ABI2 != null) abis.add(Build.CPU_ABI2);
+			abis.add(CPU_ABI);
+			if (CPU_ABI2 != null) abis.add(CPU_ABI2);
 		}
 		return abis;
 	}
diff --git a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
index d857f02..b2dbb22 100644
--- a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
+++ b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
@@ -19,8 +19,9 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Scanner;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -44,7 +45,11 @@ import static org.briarproject.onionwrapper.TorUtils.tryToClose;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.CONNECTED;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.CONNECTING;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.DISABLED;
-import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTING_STOPPING;
+import static org.briarproject.onionwrapper.TorWrapper.TorState.NOT_STARTED;
+import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTED;
+import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTING;
+import static org.briarproject.onionwrapper.TorWrapper.TorState.STOPPED;
+import static org.briarproject.onionwrapper.TorWrapper.TorState.STOPPING;
 
 @InterfaceNotNullByDefault
 abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
@@ -63,8 +68,7 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 	private static final String OWNER = "__OwningControllerProcess";
 	private static final int COOKIE_TIMEOUT_MS = 3000;
 	private static final int COOKIE_POLLING_INTERVAL_MS = 200;
-	private static final Pattern BOOTSTRAP_PERCENTAGE =
-			Pattern.compile(".*PROGRESS=(\\d{1,3}).*");
+	private static final Pattern BOOTSTRAP_PERCENTAGE = Pattern.compile(".*PROGRESS=(\\d{1,3}).*");
 
 	protected final Executor ioExecutor;
 	protected final Executor eventExecutor;
@@ -72,10 +76,10 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 	private final File torDirectory, configFile, doneFile, cookieFile;
 	private final int torSocksPort;
 	private final int torControlPort;
-	private final AtomicBoolean used = new AtomicBoolean(false);
 
 	protected final NetworkState state = new NetworkState();
 
+	private volatile Process torProcess = null;
 	private volatile Socket controlSocket = null;
 	private volatile TorControlConnection controlConnection = null;
 
@@ -83,8 +87,7 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 
 	protected abstract long getLastUpdateTime();
 
-	protected abstract InputStream getResourceInputStream(String name,
-			String extension);
+	protected abstract InputStream getResourceInputStream(String name, String extension);
 
 	AbstractTorWrapper(Executor ioExecutor,
 			Executor eventExecutor,
@@ -123,7 +126,7 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 
 	@Override
 	public void start() throws IOException, InterruptedException {
-		if (used.getAndSet(true)) throw new IllegalStateException();
+		state.setStarting();
 		if (!torDirectory.exists()) {
 			if (!torDirectory.mkdirs()) {
 				throw new IOException("Could not create Tor directory");
@@ -133,17 +136,16 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		if (!assetsAreUpToDate()) installAssets();
 		// Start from the default config every time
 		extract(getConfigInputStream(), configFile);
-		if (cookieFile.exists() && !cookieFile.delete())
+		if (cookieFile.exists() && !cookieFile.delete()) {
 			LOG.warning("Old auth cookie not deleted");
+		}
 		// Start a new Tor process
 		LOG.info("Starting Tor");
 		File torFile = getTorExecutableFile();
 		String torPath = torFile.getAbsolutePath();
 		String configPath = configFile.getAbsolutePath();
 		String pid = String.valueOf(getProcessId());
-		Process torProcess;
-		ProcessBuilder pb =
-				new ProcessBuilder(torPath, "-f", configPath, OWNER, pid);
+		ProcessBuilder pb = new ProcessBuilder(torPath, "-f", configPath, OWNER, pid);
 		Map<String, String> env = pb.environment();
 		env.put("HOME", torDirectory.getAbsolutePath());
 		pb.directory(torDirectory);
@@ -154,7 +156,7 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 			throw new IOException(e);
 		}
 		// Wait for the Tor process to start
-		waitForTorToStart(torProcess);
+		waitForTorToStart(requireNonNull(torProcess));
 		// Wait for the auth cookie file to be created/updated
 		long start = System.currentTimeMillis();
 		while (cookieFile.length() < 32) {
@@ -243,8 +245,7 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 
 	private InputStream getExecutableInputStream(String basename) {
 		String ext = getExecutableExtension();
-		return requireNonNull(
-				getResourceInputStream(architecture + "/" + basename, ext));
+		return requireNonNull(getResourceInputStream(architecture + "/" + basename, ext));
 	}
 
 	protected String getExecutableExtension() {
@@ -265,7 +266,6 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		append(strb, "CookieAuthentication", 1);
 		append(strb, "DataDirectory", dataDirectory.getAbsolutePath());
 		append(strb, "DisableNetwork", 1);
-		append(strb, "RunAsDaemon", 1);
 		append(strb, "SafeSocks", 1);
 		append(strb, "SocksPort", torSocksPort);
 		strb.append("GeoIPFile\n");
@@ -295,29 +295,48 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		}
 	}
 
-	protected void waitForTorToStart(Process torProcess)
-			throws InterruptedException, IOException {
-		Scanner stdout = new Scanner(torProcess.getInputStream());
-		// Log the first line of stdout (contains Tor and library versions)
-		if (stdout.hasNextLine()) LOG.info(stdout.nextLine());
-		// Read the process's stdout (and redirected stderr) until it detaches
-		while (stdout.hasNextLine()) stdout.nextLine();
-		stdout.close();
-		// Wait for the process to detach or exit
-		int exit = torProcess.waitFor();
-		if (exit != 0) throw new IOException("Tor exited with value " + exit);
+	protected void waitForTorToStart(Process torProcess) throws InterruptedException, IOException {
+		// Wait for the control port to be opened, then continue to read Tor's
+		// stdout and stderr in a background thread until it exits.
+		BlockingQueue<Boolean> success = new ArrayBlockingQueue<>(1);
+		ioExecutor.execute(() -> {
+			boolean started = false;
+			// Read the process's stdout (and redirected stderr)
+			Scanner stdout = new Scanner(torProcess.getInputStream());
+			// Log the first line of stdout (contains Tor and library versions)
+			if (stdout.hasNextLine()) LOG.info(stdout.nextLine());
+			// Startup has succeeded when the control port is open
+			while (stdout.hasNextLine()) {
+				String line = stdout.nextLine();
+				if (!started && line.contains("Opened Control listener")) {
+					success.add(true);
+					started = true;
+				}
+			}
+			stdout.close();
+			// If the control port wasn't opened, startup has failed
+			if (!started) success.add(false);
+			// Wait for the process to exit
+			try {
+				int exit = torProcess.waitFor();
+				if (LOG.isLoggable(INFO)) LOG.info("Tor exited with value " + exit);
+			} catch (InterruptedException e1) {
+				LOG.warning("Interrupted while waiting for Tor to exit");
+				Thread.currentThread().interrupt();
+			}
+		});
+		// Wait for the startup result
+		if (!success.take()) throw new IOException();
 	}
 
 	@Override
 	public HiddenServiceProperties publishHiddenService(int localPort,
 			int remotePort, @Nullable String privKey) throws IOException {
-		Map<Integer, String> portLines =
-				singletonMap(remotePort, "127.0.0.1:" + localPort);
+		Map<Integer, String> portLines = singletonMap(remotePort, "127.0.0.1:" + localPort);
 		// Use the control connection to set up the hidden service
 		Map<String, String> response;
 		if (privKey == null) {
-			response = getControlConnection().addOnion("NEW:ED25519-V3",
-					portLines, null);
+			response = getControlConnection().addOnion("NEW:ED25519-V3", portLines, null);
 		} else {
 			response = getControlConnection().addOnion(privKey, portLines);
 		}
@@ -362,14 +381,23 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 	}
 
 	@Override
-	public void stop() throws IOException {
-		state.setStopped();
-		if (controlSocket != null && controlConnection != null) {
-			LOG.info("Stopping Tor");
-			try {
+	public void stop() throws IOException, InterruptedException {
+		state.setStopping();
+		try {
+			if (controlConnection != null) {
 				controlConnection.shutdownTor("TERM");
+			}
+		} finally {
+			controlConnection = null;
+			tryToClose(controlSocket, LOG, WARNING);
+			controlSocket = null;
+			try {
+				if (torProcess != null) {
+					torProcess.waitFor();
+				}
 			} finally {
-				tryToClose(controlSocket, LOG, WARNING);
+				torProcess = null;
+				state.setStopped();
 			}
 		}
 	}
@@ -536,6 +564,10 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		return controlConnection;
 	}
 
+	private enum ProcessState {
+		NOT_STARTED, STARTING, STARTED, STOPPING, STOPPED
+	}
+
 	@ThreadSafe
 	@NotNullByDefault
 	private class NetworkState {
@@ -545,9 +577,10 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		private Observer observer = null;
 
 		@GuardedBy("this")
-		private boolean started = false,
-				stopped = false,
-				networkInitialised = false,
+		private ProcessState processState = ProcessState.NOT_STARTED;
+
+		@GuardedBy("this")
+		private boolean networkInitialised = false,
 				networkEnabled = false,
 				paddingEnabled = false,
 				ipv6Enabled = false,
@@ -566,8 +599,7 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		@Nullable
 		private TorState state = null;
 
-		private synchronized void setObserver(
-				@Nullable Observer observer) {
+		private synchronized void setObserver(@Nullable Observer observer) {
 			this.observer = observer;
 		}
 
@@ -583,18 +615,50 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 			}
 		}
 
+		private synchronized void setStarting() {
+			// It's legal to call start() if the wrapper has never been started, or has been
+			// started and then stopped
+			if (processState != ProcessState.NOT_STARTED && processState != ProcessState.STOPPED) {
+				throw new IllegalStateException();
+			}
+			processState = ProcessState.STARTING;
+			updateState();
+		}
+
 		private synchronized void setStarted() {
-			started = true;
+			// It's illegal to call start() and stop() concurrently
+			if (processState != ProcessState.STARTING) throw new IllegalStateException();
+			processState = ProcessState.STARTED;
 			updateState();
 		}
 
 		@SuppressWarnings("BooleanMethodIsAlwaysInverted")
 		private synchronized boolean isTorRunning() {
-			return started && !stopped;
+			return processState == ProcessState.STARTED;
+		}
+
+		private synchronized void setStopping() {
+			// It's legal to call stop() if start() has returned or thrown an exception
+			if (processState != ProcessState.STARTING && processState != ProcessState.STARTED) {
+				throw new IllegalStateException();
+			}
+			processState = ProcessState.STOPPING;
+			updateState();
 		}
 
 		private synchronized void setStopped() {
-			stopped = true;
+			// It's illegal to call start() and stop() concurrently
+			if (processState != ProcessState.STOPPING) throw new IllegalStateException();
+			processState = ProcessState.STOPPED;
+			// Reset all state related to the process that has stopped
+			networkInitialised = false;
+			networkEnabled = false;
+			paddingEnabled = false;
+			ipv6Enabled = false;
+			circuitBuilt = false;
+			bootstrapPercentage = 0;
+			bridges = emptyList();
+			orConnectionsConnected = 0;
 			updateState();
 		}
 
@@ -668,8 +732,11 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		}
 
 		private synchronized TorState getState() {
-			if (!started || stopped) return STARTING_STOPPING;
-			if (!networkInitialised) return CONNECTING;
+			if (processState == ProcessState.NOT_STARTED) return NOT_STARTED;
+			if (processState == ProcessState.STARTING) return STARTING;
+			if (processState == ProcessState.STOPPING) return STOPPING;
+			if (processState == ProcessState.STOPPED) return STOPPED;
+			if (!networkInitialised) return STARTED;
 			if (!networkEnabled) return DISABLED;
 			return bootstrapPercentage == 100 && circuitBuilt
 					&& orConnectionsConnected > 0 ? CONNECTED : CONNECTING;
diff --git a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/TorWrapper.java b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/TorWrapper.java
index ed04b3c..961ba8f 100644
--- a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/TorWrapper.java
+++ b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/TorWrapper.java
@@ -17,19 +17,33 @@ public interface TorWrapper {
 	Logger LOG = getLogger(TorWrapper.class.getName());
 
 	/**
-	 * Starts the Tor process, but does not yet connect to the Tor Network.
+	 * Starts the Tor process, but does not yet connect to the Tor network.
 	 * Call {@link #enableNetwork(boolean)} for this.
 	 * <p>
-	 * This method must only be called once. To restart the Tor process, stop
-	 * this wrapper instance and then create a new instance.
+	 * This method waits for the Tor process to start before returning. Methods
+	 * that modify the wrapper's configuration
+	 * ({@link #publishHiddenService(int, int, String)},
+	 * {@link #removeHiddenService(String)}, {@link #enableNetwork(boolean)},
+	 * {@link #enableBridges(List)}, {@link #enableConnectionPadding(boolean)},
+	 * {@link #enableIpv6(boolean)}) should be called after this method returns.
+	 * <p>
+	 * Do not call this method concurrently with {@link #stop()}.
+	 * <p>
+	 * If this method throws an exception, call {@link #stop()} before trying
+	 * to call this method again.
 	 */
 	void start() throws IOException, InterruptedException;
 
 	/**
-	 * Tell the Tor process to stop and returns without waiting for the
-	 * process to exit.
+	 * Tell the Tor process to stop and waits for it to stop before returning.
+	 * <p>
+	 * The wrapper's configuration is reset, so if the wrapper is reused by
+	 * calling {@link #start()} again then any configuration applied via
+	 * {@link #enableNetwork(boolean)} etc must be applied again.
+	 * <p>
+	 * Do not call this method concurrently with {@link #start()}.
 	 */
-	void stop() throws IOException;
+	void stop() throws IOException, InterruptedException;
 
 	/**
 	 * Sets an observer for observing the state of the wrapper, replacing any
@@ -111,9 +125,26 @@ public interface TorWrapper {
 	enum TorState {
 
 		/**
-		 * The Tor process is either starting or stopping.
+		 * The wrapper has been created but the {@link #start()} method has not
+		 * yet been called. This is the initial state.
+		 */
+		NOT_STARTED,
+
+		/**
+		 * The {@link #start()} method has been called and the Tor process is
+		 * starting.
+		 */
+		STARTING,
+
+		/**
+		 * The {@link #start()} method has been called and the Tor process has
+		 * started.
+		 * <p>
+		 * No connections to the Tor network will be made in this state. The
+		 * wrapper remains in this state until {@link #enableNetwork(boolean)}
+		 * is called.
 		 */
-		STARTING_STOPPING,
+		STARTED,
 
 		/**
 		 * The Tor process has started, its network connection is enabled, and
@@ -131,7 +162,22 @@ public interface TorWrapper {
 		/**
 		 * The Tor process has started but its network connection is disabled.
 		 */
-		DISABLED
+		DISABLED,
+
+		/**
+		 * The {@link #stop()} method has been called and the Tor process is
+		 * stopping.
+		 */
+		STOPPING,
+
+		/**
+		 * The {@link #stop()} method has been called and the Tor process has
+		 * stopped.
+		 * <p>
+		 * A new Tor process can be started by calling the {@link #start()}
+		 * method again.
+		 */
+		STOPPED
 	}
 
 	/**
diff --git a/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/JavaTorWrapper.java b/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/JavaTorWrapper.java
index 5c5dbce..85edffa 100644
--- a/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/JavaTorWrapper.java
+++ b/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/JavaTorWrapper.java
@@ -20,14 +20,12 @@ abstract class JavaTorWrapper extends AbstractTorWrapper {
 			File torDirectory,
 			int torSocksPort,
 			int torControlPort) {
-		super(ioExecutor, eventExecutor, architecture, torDirectory,
-				torSocksPort, torControlPort);
+		super(ioExecutor, eventExecutor, architecture, torDirectory, torSocksPort, torControlPort);
 	}
 
 	@Override
 	protected long getLastUpdateTime() {
-		CodeSource codeSource =
-				getClass().getProtectionDomain().getCodeSource();
+		CodeSource codeSource = getClass().getProtectionDomain().getCodeSource();
 		if (codeSource == null) throw new AssertionError("CodeSource null");
 		try {
 			URI path = codeSource.getLocation().toURI();
@@ -39,8 +37,7 @@ abstract class JavaTorWrapper extends AbstractTorWrapper {
 	}
 
 	@Override
-	protected InputStream getResourceInputStream(String name,
-			String extension) {
+	protected InputStream getResourceInputStream(String name, String extension) {
 		ClassLoader cl = getClass().getClassLoader();
 		return requireNonNull(cl.getResourceAsStream(name + extension));
 	}
diff --git a/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/WindowsTorWrapper.java b/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/WindowsTorWrapper.java
index 084a558..2885286 100644
--- a/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/WindowsTorWrapper.java
+++ b/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/WindowsTorWrapper.java
@@ -5,14 +5,8 @@ import com.sun.jna.platform.win32.Kernel32;
 import org.briarproject.nullsafety.NotNullByDefault;
 
 import java.io.File;
-import java.io.IOException;
-import java.util.Scanner;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Executor;
 
-import static java.util.logging.Level.INFO;
-
 /**
  * A Tor wrapper for the Windows operating system.
  */
@@ -40,8 +34,7 @@ public class WindowsTorWrapper extends JavaTorWrapper {
 			File torDirectory,
 			int torSocksPort,
 			int torControlPort) {
-		super(ioExecutor, eventExecutor, architecture, torDirectory,
-				torSocksPort, torControlPort);
+		super(ioExecutor, eventExecutor, architecture, torDirectory, torSocksPort, torControlPort);
 	}
 
 	@Override
@@ -49,44 +42,6 @@ public class WindowsTorWrapper extends JavaTorWrapper {
 		return Kernel32.INSTANCE.GetCurrentProcessId();
 	}
 
-	@Override
-	protected void waitForTorToStart(Process torProcess)
-			throws InterruptedException, IOException {
-		// On Windows the RunAsDaemon option has no effect, so Tor won't detach.
-		// Wait for the control port to be opened, then continue to read its
-		// stdout and stderr in a background thread until it exits.
-		BlockingQueue<Boolean> success = new ArrayBlockingQueue<>(1);
-		ioExecutor.execute(() -> {
-			boolean started = false;
-			// Read the process's stdout (and redirected stderr)
-			Scanner stdout = new Scanner(torProcess.getInputStream());
-			// Log the first line of stdout (contains Tor and library versions)
-			if (stdout.hasNextLine()) LOG.info(stdout.nextLine());
-			// Startup has succeeded when the control port is open
-			while (stdout.hasNextLine()) {
-				String line = stdout.nextLine();
-				if (!started && line.contains("Opened Control listener")) {
-					success.add(true);
-					started = true;
-				}
-			}
-			stdout.close();
-			// If the control port wasn't opened, startup has failed
-			if (!started) success.add(false);
-			// Wait for the process to exit
-			try {
-				int exit = torProcess.waitFor();
-				if (LOG.isLoggable(INFO))
-					LOG.info("Tor exited with value " + exit);
-			} catch (InterruptedException e1) {
-				LOG.warning("Interrupted while waiting for Tor to exit");
-				Thread.currentThread().interrupt();
-			}
-		});
-		// Wait for the startup result
-		if (!success.take()) throw new IOException();
-	}
-
 	@Override
 	protected String getExecutableExtension() {
 		return ".exe";
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
index 516b1ec..63d56d8 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
@@ -18,6 +18,9 @@ import static org.briarproject.onionwrapper.TestUtils.getTestDirectory;
 import static org.briarproject.onionwrapper.TestUtils.isLinux;
 import static org.briarproject.onionwrapper.TestUtils.isWindows;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.CONNECTED;
+import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTED;
+import static org.briarproject.onionwrapper.TorWrapper.TorState.STOPPED;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeNotNull;
 import static org.junit.Assume.assumeTrue;
@@ -28,7 +31,7 @@ public class BootstrapTest extends BaseTest {
 
 	private static final int SOCKS_PORT = 59060;
 	private static final int CONTROL_PORT = 59061;
-	private final static long TIMEOUT = MINUTES.toMillis(2);
+	private final static long TIMEOUT = MINUTES.toMillis(5);
 
 	private final ExecutorService executor = newCachedThreadPool();
 	private final File torDir = getTestDirectory();
@@ -59,9 +62,16 @@ public class BootstrapTest extends BaseTest {
 			throw new AssertionError("Running on unsupported OS");
 		}
 
+		// Bootstrap twice with the same wrapper to test wrapper reuse
+		testBootstrapping(tor);
+		testBootstrapping(tor);
+	}
+
+	private void testBootstrapping(TorWrapper tor) throws Exception {
 		boolean connected;
 		try {
 			tor.start();
+			assertEquals(STARTED, tor.getTorState());
 			tor.enableNetwork(true);
 			long start = System.currentTimeMillis();
 			while (System.currentTimeMillis() - start < TIMEOUT) {
@@ -74,6 +84,7 @@ public class BootstrapTest extends BaseTest {
 			else LOG.warning("Could not connect to Tor within timeout");
 		} finally {
 			tor.stop();
+			assertEquals(STOPPED, tor.getTorState());
 		}
 		assertTrue(connected);
 	}
-- 
GitLab