Repository: bisq-network/bisq2
Pull request #3483: Fix(Tor): Robust control port polling for embedded Tor startup
Author: login: hiciefte
Created: 2025-05-15T10:58:05Z | Merged: 2025-05-22T05:21:32Z

While running the Bisq 2 http-api in a docker container, I ran into Tor timing issues, that prevented the containerized Bisq 2 instance to connect via Tor.

- Implements a robust polling mechanism in ControlPortFilePoller to reliably detect the correct Tor control port. The poller repeatedly attempts to parse the port from the control port file, confirming stability before proceeding.
- Prevents race conditions where the application could connect to a stale or incorrect port, which previously led to CannotConnectWithTorException errors.
- Improves reliability of embedded Tor initialization, especially in fast or containerized environments.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
	- Improved reliability and stability when connecting to the Tor network by enhancing socket lifecycle management, retries, and resource cleanup.
	- Enhanced control port polling with scheduled checks, failure tracking, and stability verification to reduce connection issues.

- **Chores**
	- Added detailed logging for better monitoring and troubleshooting of Tor connection and polling processes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java b/network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
index 583a82bb9e..45ec33473a 100644
--- a/network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
+++ b/network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
@@ -26,7 +26,7 @@
 public class TorControlProtocol implements AutoCloseable {
     private static final int MAX_CONNECTION_ATTEMPTS = 10;
 
-    private final Socket controlSocket;
+    private Socket controlSocket;
     private final TorControlReader torControlReader;
     private Optional<OutputStream> outputStream = Optional.empty();
 
@@ -36,16 +36,30 @@ public class TorControlProtocol implements AutoCloseable {
     private volatile boolean closeInProgress;
 
     public TorControlProtocol() {
-        controlSocket = new Socket();
         torControlReader = new TorControlReader();
     }
 
     public void initialize(int port) {
         try {
-            connectToTor(port);
-            torControlReader.start(controlSocket.getInputStream());
-            outputStream = Optional.of(controlSocket.getOutputStream());
-        } catch (IOException | InterruptedException e) {
+            this.controlSocket = createAndConnectControlSocket(port);
+            this.outputStream = Optional.of(this.controlSocket.getOutputStream());
+            this.torControlReader.start(this.controlSocket.getInputStream());
+            log.info("TorControlProtocol initialized successfully for port {}", port);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("TorControlProtocol initialization for port {} was interrupted.", port, e);
+            close();
+            throw new CannotConnectWithTorException(e);
+        } catch (IOException e) {
+            log.error("TorControlProtocol failed to set up streams for port {}.", port, e);
+            close();
+            throw new CannotConnectWithTorException(e);
+        } catch (CannotConnectWithTorException e) {
+            log.error("TorControlProtocol failed to connect to Tor control port {} (as thrown by createAndConnectControlSocket).", port, e);
+            close();
+            throw e;
+        } catch (Exception e) {
+            log.error("Unexpected exception during TorControlProtocol initialization for port {}.", port, e);
             close();
             throw new CannotConnectWithTorException(e);
         }
@@ -57,16 +71,23 @@ public void close() {
             return;
         }
         closeInProgress = true;
+        log.debug("Closing TorControlProtocol resources.");
         try {
-            controlSocket.close();
+            if (controlSocket != null && !controlSocket.isClosed()) {
+                controlSocket.close();
+                log.debug("Control socket closed.");
+            }
         } catch (IOException e) {
-            throw new RuntimeException(e);
+            log.warn("IOException while closing control socket. This may be expected if connection was problematic.", e);
         }
         try {
             torControlReader.close();
+            log.debug("TorControlReader closed.");
         } catch (Exception e) {
-            throw new RuntimeException(e);
+            log.warn("Exception while closing TorControlReader.", e);
         }
+        outputStream = Optional.empty();
+        closeInProgress = false;
     }
 
     public void authenticate(PasswordDigest passwordDigest) {
@@ -157,7 +178,6 @@ public void addBootstrapEventListener(BootstrapEventListener listener) {
         Set<String> previous = getEventTypesOfBootstrapEventListeners();
         torControlReader.addBootstrapEventListener(listener);
         String newEventType = listener.getEventType().name();
-        // If our listener has a new eventType we register for that event
         if (!previous.contains(newEventType)) {
             refreshEventRegistration();
         } else {
@@ -169,12 +189,10 @@ public void removeBootstrapEventListener(BootstrapEventListener listener) {
         Set<String> previous = getEventTypesOfBootstrapEventListeners();
         String newEventType = listener.getEventType().name();
         if (!previous.contains(newEventType)) {
-            log.warn("Remove BootstrapEventListener but did not have eventType in listeners. " +
-                    "This could happen if removeBootstrapEventListener was called without addBootstrapEventListener before.");
+            log.warn("Remove BootstrapEventListener but did not have eventType in listeners. This could happen if removeBootstrapEventListener was called without addBootstrapEventListener before.");
         }
         torControlReader.removeBootstrapEventListener(listener);
         Set<String> current = getEventTypesOfBootstrapEventListeners();
-        // If your listener was the only listener with that eventType we unregister for that event
         if (!current.contains(newEventType)) {
             refreshEventRegistration();
         } else {
@@ -192,7 +210,6 @@ public void addHsDescEventListener(HsDescEventListener listener) {
         Set<String> previous = getEventTypesOfHsDescEventListeners();
         torControlReader.addHsDescEventListener(listener);
         String newEventType = listener.getEventType().name();
-        // If our listener has a new eventType we register for that event
         if (!previous.contains(newEventType)) {
             refreshEventRegistration();
         } else {
@@ -204,12 +221,10 @@ public void removeHsDescEventListener(HsDescEventListener listener) {
         Set<String> previous = getEventTypesOfHsDescEventListeners();
         String newEventType = listener.getEventType().name();
         if (!previous.contains(newEventType) && !closeInProgress) {
-            log.warn("Remove HsDescEventListener but did not have eventType in listeners. " +
-                    "This could happen if removeHsDescEventListener was called without addHsDescEventListener before.");
+            log.warn("Remove HsDescEventListener but did not have eventType in listeners. This could happen if removeHsDescEventListener was called without addHsDescEventListener before.");
         }
         torControlReader.removeHsDescEventListener(listener);
         Set<String> current = getEventTypesOfHsDescEventListeners();
-        // If your listener was the only listener with that eventType we unregister for that event
         if (!current.contains(newEventType)) {
             refreshEventRegistration();
         } else {
@@ -247,24 +262,60 @@ private void registerEvents(Set<String> events) {
         }
     }
 
-    private void connectToTor(int port) throws InterruptedException {
+    private Socket createAndConnectControlSocket(int port) throws InterruptedException, CannotConnectWithTorException {
         int connectionAttempt = 0;
+        Exception lastException = null;
+
         while (connectionAttempt < MAX_CONNECTION_ATTEMPTS) {
+            Socket attemptSocket = new Socket();
             try {
+                // The Tor Control Port communication is typically unencrypted. 
+                // This is considered safe because the connection is made to 127.0.0.1 (localhost),
+                // ensuring that the communication does not leave the local machine and is not 
+                // exposed to external networks. Authentication (via cookie or password) 
+                // is handled by the Tor control protocol itself after connection.
                 var socketAddress = new InetSocketAddress("127.0.0.1", port);
-                controlSocket.connect(socketAddress);
-                break;
+                log.debug("Attempting to connect to Tor control port {} ({}/{})", socketAddress, connectionAttempt + 1, MAX_CONNECTION_ATTEMPTS);
+                attemptSocket.connect(socketAddress);
+                log.info("Successfully connected control socket to Tor port {} after {} attempts", port, connectionAttempt + 1);
+                return attemptSocket;
             } catch (ConnectException e) {
-                connectionAttempt++;
-                Thread.sleep(200);
+                lastException = e;
+                log.warn("ConnectException on attempt {} to Tor control port {}: {}. Closing attemptSocket.",
+                        connectionAttempt + 1, port, e.getMessage());
+                try {
+                    attemptSocket.close();
+                } catch (IOException closeEx) {
+                    log.warn("Failed to close attemptSocket after ConnectException on port {}: {}", port, closeEx.getMessage(), closeEx);
+                }
             } catch (IOException e) {
-                close();
-                throw new CannotConnectWithTorException(e);
+                lastException = e;
+                log.warn("IOException on attempt {} to Tor control port {}: {}. Closing attemptSocket.",
+                        connectionAttempt + 1, port, e.getMessage());
+                try {
+                    attemptSocket.close();
+                } catch (IOException closeEx) {
+                    log.warn("Failed to close attemptSocket after IOException on port {}: {}", port, closeEx.getMessage(), closeEx);
+                }
+            }
+
+            connectionAttempt++;
+            if (connectionAttempt < MAX_CONNECTION_ATTEMPTS) {
+                log.debug("Connection attempt to Tor control port {} failed. Retrying in 200ms...", port);
+                Thread.sleep(200);
             }
         }
+
+        String errorMessage = "Failed to connect to Tor control port " + port + " after " + MAX_CONNECTION_ATTEMPTS + " attempts.";
+        IOException wrapperException = new IOException(errorMessage, lastException);
+        log.error(errorMessage, wrapperException);
+        throw new CannotConnectWithTorException(wrapperException);
     }
 
     private void sendCommand(String command) {
+        if (outputStream.isEmpty()) {
+            throw new IllegalStateException("TorControlProtocol output stream not initialized. Cannot send command.");
+        }
         try {
             String commandToLog = command.contains("AUTHENTICATE")
                     ? command.split(" ")[0] + " [authentication data hidden in logs]"
@@ -273,10 +324,10 @@ private void sendCommand(String command) {
                 commandToLog = commandToLog.substring(0, commandToLog.length() - 2);
             }
             log.info("Send Tor control command: {}", commandToLog);
-            @SuppressWarnings("resource") OutputStream outputStream = this.outputStream.orElseThrow();
+            @SuppressWarnings("resource") OutputStream os = this.outputStream.orElseThrow();
             byte[] commandBytes = command.getBytes(StandardCharsets.US_ASCII);
-            outputStream.write(commandBytes);
-            outputStream.flush();
+            os.write(commandBytes);
+            os.flush();
         } catch (IOException e) {
             throw new CannotSendCommandToTorException(e);
         }
diff --git a/network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java b/network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
index af904c51af..de3ba60067 100644
--- a/network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
+++ b/network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
@@ -17,63 +17,261 @@
 
 package bisq.network.tor.process.control_port;
 
+import bisq.common.threading.ExecutorFactory;
 import bisq.common.threading.ThreadName;
+import lombok.extern.slf4j.Slf4j;
 
 import java.nio.file.Path;
 import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+@Slf4j
 public class ControlPortFilePoller {
     private final AtomicBoolean isRunning = new AtomicBoolean();
     private final CompletableFuture<Integer> portCompletableFuture = new CompletableFuture<>();
     private final Path controlPortFilePath;
 
+    private static final int MAX_CONSECUTIVE_READ_FAILURES = 10;
+    private static final int MAX_ATTEMPTS = 300;
+    private static final int SETTLE_DELAY_MS = 50;
+    private static final int POLLING_INTERVAL_MS = 100;
+
+    private ScheduledExecutorService executorService;
+    private int attempts = 0;
+    private Optional<Integer> lastParsedPort = Optional.empty();
+    private int consecutiveReadFailures = 0;
+
     public ControlPortFilePoller(Path controlPortFilePath) {
         this.controlPortFilePath = controlPortFilePath;
     }
 
     public CompletableFuture<Integer> parsePort() {
-        boolean isSuccess = isRunning.compareAndSet(false, true);
-        if (isSuccess) {
-            startPoller();
+        if (isRunning.compareAndSet(false, true)) {
+            submitInitialPollingTask();
         }
         return portCompletableFuture;
     }
 
-    private void startPoller() {
-        Thread thread = new Thread(() -> {
-            ThreadName.setName("ControlPortFilePoller.startPoller");
-            try {
-                while (true) {
-                    Optional<Integer> optionalPort = parsePortFromFile();
-
-                    if (optionalPort.isPresent()) {
-                        portCompletableFuture.complete(optionalPort.get());
-                        break;
-                    } else {
-                        // We can't use Java's WatcherService because it misses events between event processing.
-                        // Tor writes the port to a swap file first, and renames it afterward.
-                        // The WatcherService can miss the second operation, causing a deadlock.
-                        //noinspection BusyWait
-                        Thread.sleep(100);
+    private void submitInitialPollingTask() {
+        this.executorService = ExecutorFactory.newSingleThreadScheduledExecutor("ControlPortFilePollerThread");
+        this.attempts = 0;
+        this.lastParsedPort = Optional.empty();
+        this.consecutiveReadFailures = 0;
+
+        log.debug("Starting control port file poller for: {}. Max attempts: {}, Settle delay: {}ms, Poll interval: {}ms",
+                controlPortFilePath, MAX_ATTEMPTS, SETTLE_DELAY_MS, POLLING_INTERVAL_MS);
+
+        // Schedule the first poll cycle to run immediately on the executor thread
+        if (canContinuePolling()) {
+            executorService.submit(this::executePollCycle);
+        }
+    }
+
+    private void executePollCycle() {
+        if (!canContinuePolling()) return;
+
+        this.attempts++;
+        try {
+            ThreadName.setName("ControlPortFilePoller.pollCycle-" + this.attempts);
+            log.trace("Executing poll cycle attempt {} for {}", this.attempts, controlPortFilePath);
+
+            if (this.attempts > MAX_ATTEMPTS) {
+                handleTimeout();
+                return;
+            }
+
+            Optional<Integer> optionalPort = parsePortFromFile();
+
+            if (optionalPort.isPresent()) {
+                int currentPort = optionalPort.get();
+                if (this.lastParsedPort.isEmpty() || this.lastParsedPort.get() != currentPort) {
+                    log.debug("Poller parsed new/changed port {} from {} (attempt {}). Scheduling stability check in {}ms.",
+                            currentPort, controlPortFilePath, this.attempts, SETTLE_DELAY_MS);
+                    if (canContinuePolling()) {
+                        executorService.schedule(() -> performStabilityCheck(currentPort, this.attempts),
+                                SETTLE_DELAY_MS, TimeUnit.MILLISECONDS);
                     }
+                    return;
+                } else {
+                    log.info("ControlPortFilePoller confirmed stable port (same as last): {} from file {} (attempt {})",
+                            currentPort, controlPortFilePath, this.attempts);
+                    completeSuccessfully(currentPort);
+                    return;
+                }
+            } else {
+                handleReadFailure(this.attempts, "initial parse");
+                if (portCompletableFuture.isDone()) return;
+                scheduleNextRegularPollCycle();
+            }
+        } catch (Exception e) {
+            handleUnexpectedException("pollCycle attempt " + this.attempts, e);
+        }
+    }
+
+    private void performStabilityCheck(int initialPort, int originatingAttemptNum) {
+        if (!canContinuePolling()) return;
+
+        try {
+            ThreadName.setName("ControlPortFilePoller.stabilityCheck-" + originatingAttemptNum);
+            log.trace("Performing stability check for port {} (from attempt {}) for file {}",
+                    initialPort, originatingAttemptNum, controlPortFilePath);
+
+            Optional<Integer> finalCheckOptionalPort = parsePortFromFile();
+
+            if (finalCheckOptionalPort.isPresent() && finalCheckOptionalPort.get() == initialPort) {
+                log.info("ControlPortFilePoller confirmed stable port {} post-check (from attempt {}) for file {}",
+                        initialPort, originatingAttemptNum, controlPortFilePath);
+                completeSuccessfully(initialPort);
+            } else {
+                String failureContext = "stability check (originally attempt " + originatingAttemptNum + ")";
+                if (finalCheckOptionalPort.isPresent()) {
+                    log.warn("Control port file {} changed (was {}, now {}) during {}. Re-polling.",
+                            controlPortFilePath, initialPort, finalCheckOptionalPort.get(), failureContext);
+                    this.lastParsedPort = finalCheckOptionalPort;
+                } else {
+                    log.warn("Control port file {} became unparseable (was {}) during {}. Re-polling.",
+                            controlPortFilePath, initialPort, failureContext);
+                    this.lastParsedPort = Optional.empty();
+                }
+                handleReadFailure(originatingAttemptNum, failureContext);
+                if (portCompletableFuture.isDone()) return;
+                scheduleNextRegularPollCycle();
+            }
+        } catch (Exception e) {
+            handleUnexpectedException("stabilityCheck (from attempt " + originatingAttemptNum + ")", e);
+        }
+    }
+
+    private void scheduleNextRegularPollCycle() {
+        if (!canContinuePolling()) return;
+
+        if (this.attempts < MAX_ATTEMPTS) {
+            log.trace("Scheduling next poll cycle for {} (current attempts: {}).", controlPortFilePath, this.attempts);
+            // Ensure executorService is not null before scheduling, canContinuePolling should ideally cover this.
+            if (executorService != null && !executorService.isShutdown()) {
+                executorService.schedule(this::executePollCycle, POLLING_INTERVAL_MS, TimeUnit.MILLISECONDS);
+            } else {
+                log.warn("Executor service not available for scheduling next poll cycle for {}. Polling may stop.", controlPortFilePath);
+                // If executor is gone, and future not done, this might be an issue. CanContinuePolling should ideally catch.
+                if (!portCompletableFuture.isDone()) {
+                    // This state indicates a problem or a race condition with shutdown.
+                    // Forcefully complete future if it's stuck without an executor.
+                    portCompletableFuture.completeExceptionally(new CancellationException("Polling stopped: Executor service became unavailable unexpectedly."));
+                    tryStopExecutorService(); // Attempt cleanup just in case.
                 }
+            }
+        } else {
+            log.debug("Max attempts ({}) reached for {}, not scheduling further poll cycles.", MAX_ATTEMPTS, controlPortFilePath);
+            handleTimeout();
+        }
+    }
+
+    private void completeSuccessfully(int port) {
+        if (portCompletableFuture.complete(port)) {
+            log.info("ControlPortFilePoller for {} completed successfully with port {}.", controlPortFilePath, port);
+            tryStopExecutorService();
+        }
+        this.consecutiveReadFailures = 0;
+    }
+
+    private void handleReadFailure(int attemptContext, String phase) {
+        this.consecutiveReadFailures++;
+        checkConsecutiveFailuresAndCompleteExceptionally(attemptContext, this.consecutiveReadFailures, phase);
+    }
+
+    private void checkConsecutiveFailuresAndCompleteExceptionally(int currentAttemptNum,
+                                                                  int failures,
+                                                                  String phase) {
+        if (failures >= MAX_CONSECUTIVE_READ_FAILURES) {
+            log.error("ControlPortFilePoller for {} failed after {} consecutive read failures (during {}, attempt {}). Giving up.",
+                    controlPortFilePath, failures, phase, currentAttemptNum);
+            if (portCompletableFuture.completeExceptionally(
+                    new ControlPortFileParseFailureException("Failed to parse/stabilize port file '" + controlPortFilePath
+                            + "' after " + failures + " consecutive read failures (phase: " + phase + ", attempt: " + currentAttemptNum + ")."))) {
+                tryStopExecutorService();
+            }
+        } else {
+            log.trace("Read failure for file {} (phase: {}, attempt: {}). Consecutive failures: {}/{}.",
+                    controlPortFilePath, phase, currentAttemptNum, failures, MAX_CONSECUTIVE_READ_FAILURES);
+        }
+    }
+
+    private void handleTimeout() {
+        log.error("ControlPortFilePoller timed out for file {} after {} attempts.",
+                controlPortFilePath, this.attempts);
+        if (portCompletableFuture.completeExceptionally(
+                new TimeoutException("ControlPortFilePoller timed out for file '" + controlPortFilePath + "' after "
+                        + this.attempts + " attempts."))) {
+            tryStopExecutorService();
+        }
+    }
+
+    private void handleUnexpectedException(String context, Exception e) {
+        log.error("ControlPortFilePoller for {} encountered an unexpected error during {}.", controlPortFilePath, context, e);
+        if (portCompletableFuture.completeExceptionally(e)) {
+            tryStopExecutorService();
+        }
+    }
+
+    private boolean canContinuePolling() {
+        if (Thread.currentThread().isInterrupted()) {
+            log.warn("Polling for {} was interrupted.", controlPortFilePath);
+            if (portCompletableFuture.completeExceptionally(new InterruptedException("Polling for " + controlPortFilePath + " was interrupted."))) {
+                tryStopExecutorService();
+            }
+            return false;
+        }
+        if (executorService == null || executorService.isShutdown()) {
+            log.warn("Polling for {} cannot continue: executor service not available or shutdown.", controlPortFilePath);
+            if (portCompletableFuture.completeExceptionally(new CancellationException("Polling for " + controlPortFilePath + " stopped: Executor service unavailable."))) {
+                tryStopExecutorService(); // Try to clean up if we somehow got here with a null/shutdown executor but live future
+            }
+            return false;
+        }
+        if (portCompletableFuture.isDone()) {
+            log.trace("Polling for {} stopped: future already completed.", controlPortFilePath);
+            return false;
+        }
+        return true;
+    }
 
-            } catch (ControlPortFileParseFailureException | InterruptedException e) {
-                portCompletableFuture.completeExceptionally(e);
+    private void tryStopExecutorService() {
+        this.isRunning.set(false); // Mark as not actively polling/able to start new cycles.
+        ScheduledExecutorService serviceRef = this.executorService;
+        if (serviceRef != null) {
+            log.debug("ControlPortFilePoller for {} is shutting down its executor service.", controlPortFilePath);
+            ExecutorFactory.shutdownAndAwaitTermination(serviceRef, 200, TimeUnit.MILLISECONDS); // Default was 200ms for poller
+            if (this.executorService == serviceRef) { // Avoid race condition if shutdown is called concurrently
+                this.executorService = null;
             }
-        });
+        }
+    }
 
-        thread.start();
+    public void shutdown() {
+        log.info("External shutdown requested for ControlPortFilePoller for {}", controlPortFilePath);
+        // tryStopExecutorService will set isRunning to false.
+        tryStopExecutorService();
+        // If the future wasn't completed by the poller's own logic or the shutdown process above, cancel it.
+        if (portCompletableFuture.completeExceptionally(new CancellationException("ControlPortFilePoller for " + controlPortFilePath + " was shut down by external request."))) {
+            log.warn("ControlPortFilePoller for {} future cancelled due to external shutdown request.", controlPortFilePath);
+        } else {
+            log.debug("ControlPortFilePoller for {} was already completed or shutdown internally.", controlPortFilePath);
+        }
     }
 
     private Optional<Integer> parsePortFromFile() {
-        if (!controlPortFilePath.toFile().exists()) {
+        try {
+            if (!controlPortFilePath.toFile().exists()) {
+                log.trace("Control port file does not exist yet: {}", controlPortFilePath);
+                return Optional.empty();
+            }
+            int controlPort = ControlPortFileParser.parse(controlPortFilePath);
+            return Optional.of(controlPort);
+        } catch (Exception e) {
+            log.warn("Failed to parse control port from file {} due to: {}. Treating as transient error.",
+                    controlPortFilePath, e.getMessage());
             return Optional.empty();
         }
-
-        int controlPort = ControlPortFileParser.parse(controlPortFilePath);
-        return Optional.of(controlPort);
     }
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java (1)</summary><blockquote>

`29-33`: _⚠️ Potential issue_

**Prevent `NullPointerException` when `close()` is invoked before a connection is established**

`controlSocket` is now lazily initialised, yet `close()` (lines 55-63) still calls `controlSocket.close()` unconditionally.  
If `initialize()` fails early (e.g. all retries exhausted) `controlSocket` remains `null`, leading to an NPE during cleanup.

```diff
 public void close() {
     if (closeInProgress) {
         return;
     }
     closeInProgress = true;
-    try {
-        controlSocket.close();
+    if (controlSocket != null) {
+        try {
+            controlSocket.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
     } catch (IOException e) {
         throw new RuntimeException(e);
     }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java (1)</summary><blockquote>

`100-113`: **`consecutiveReadFailures` counter isn’t reset after unstable parses**

When a port is parsed but fails the “settle” re-check (`finalCheckOptionalPort.isEmpty()`), the counter remains at zero even though the file was effectively unreadable. This can result in an infinite loop where the poller never times out despite repeated failures.

Increment the failure counter in this branch or reset it only on *confirmed* stable ports.

[Nit, but worth tightening in edge cases.]

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between cfa9969bc33a0903285cbc84b04caba8cf1ff691 and 191f13c0985ccf2f7b450bdfac058ba51895838f.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java` (3 hunks)
* `network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 ast-grep (0.31.1)</summary>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java</summary>

[info] 253-253: "Detected use of a Java socket that is not encrypted. As a result, the
                    traffic could be read by an attacker intercepting the network traffic. Use
                    an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
                    instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
    - https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (15)</summary>

* GitHub Check: Build wallet module on windows-latest / Build wallet module on windows-latest
* GitHub Check: Build tor module on macOS-latest / Build tor module on macOS-latest
* GitHub Check: Build wallet module on macOS-latest / Build wallet module on macOS-latest
* GitHub Check: Build tor module on windows-latest / Build tor module on windows-latest
* GitHub Check: Build wallet module on ubuntu-latest / Build wallet module on ubuntu-latest
* GitHub Check: Build tor module on ubuntu-latest / Build tor module on ubuntu-latest
* GitHub Check: Build network module on macOS-latest / Build network module on macOS-latest
* GitHub Check: Build network module on windows-latest / Build network module on windows-latest
* GitHub Check: Build apps module on windows-latest / Build apps module on windows-latest
* GitHub Check: Build network module on ubuntu-latest / Build network module on ubuntu-latest
* GitHub Check: Build apps module on macOS-latest / Build apps module on macOS-latest
* GitHub Check: Build apps module on ubuntu-latest / Build apps module on ubuntu-latest
* GitHub Check: Build Bisq on windows-latest / Build Bisq on windows-latest
* GitHub Check: Build Bisq on macOS-latest / Build Bisq on macOS-latest
* GitHub Check: Build Bisq on ubuntu-latest / Build Bisq on ubuntu-latest

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java (1)</summary>

`287-291`: **👍  Defensive check before sending commands looks good**

Guarding `sendCommand` with `outputStream.isEmpty()` prevents silent `NullPointerException`s and surfaces mis-sequenced API usage early. Nice addition.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T11:01:41Z
user: login: HenrikJannsen
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T05:40:19Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java (1)</summary><blockquote>

`248-261`: **Improved error handling in parsePortFromFile method**

The method now properly handles exceptions during parsing as transient errors, allowing the polling to continue instead of failing completely. This addresses the previous review comment about treating parser exceptions as transient.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 191f13c0985ccf2f7b450bdfac058ba51895838f and b80fd6ddd468465ad8586f95ef6f12036e18963f.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java` (7 hunks)
* `network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 ast-grep (0.38.1)</summary>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java</summary>

[info] 269-269: "Detected use of a Java socket that is not encrypted. As a result, the
                traffic could be read by an attacker intercepting the network traffic. Use
                an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
                instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
    - https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (15)</summary>

* GitHub Check: Build network module on macOS-latest / Build network module on macOS-latest
* GitHub Check: Build network module on windows-latest / Build network module on windows-latest
* GitHub Check: Build wallet module on macOS-latest / Build wallet module on macOS-latest
* GitHub Check: Build network module on ubuntu-latest / Build network module on ubuntu-latest
* GitHub Check: Build wallet module on windows-latest / Build wallet module on windows-latest
* GitHub Check: Build tor module on macOS-latest / Build tor module on macOS-latest
* GitHub Check: Build tor module on windows-latest / Build tor module on windows-latest
* GitHub Check: Build wallet module on ubuntu-latest / Build wallet module on ubuntu-latest
* GitHub Check: Build tor module on ubuntu-latest / Build tor module on ubuntu-latest
* GitHub Check: Build Bisq on windows-latest / Build Bisq on windows-latest
* GitHub Check: Build Bisq on macOS-latest / Build Bisq on macOS-latest
* GitHub Check: Build Bisq on ubuntu-latest / Build Bisq on ubuntu-latest
* GitHub Check: Build apps module on ubuntu-latest / Build apps module on ubuntu-latest
* GitHub Check: Build apps module on windows-latest / Build apps module on windows-latest
* GitHub Check: Build apps module on macOS-latest / Build apps module on macOS-latest

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java (7)</summary>

`20-44`: **Well-structured constants and fields for improved configuration**

The code now properly externalizes configuration parameters as static constants, which improves maintainability and readability. The use of descriptive variable names and clear separation of concerns is excellent.

---

`49-54`: **LGTM: Improved thread safety with atomic comparison**

The `compareAndSet` approach ensures that multiple calls to `parsePort()` won't spawn multiple pollers, which could lead to resource leaks and race conditions.

---

`56-69`: **Excellent executor service initialization**

The use of `ExecutorFactory` with a meaningful thread name provides better debugging and monitoring capabilities. The proper initialization of state variables before starting the polling ensures a clean initial state.

---

`71-113`: **Well-designed polling cycle with proper flow control**

The polling cycle implementation includes all necessary checks:
- Thread interruption detection
- Maximum attempts limit
- Detailed logging at appropriate levels
- Port change detection
- Stability scheduling

I particularly appreciate the early returns that ensure the method doesn't continue execution when conditions aren't met.

---

`115-149`: **Robust stability check prevents race conditions**

The stability check ensures that the port value doesn't change during the critical period between initial detection and usage, which was likely the root cause of the Docker container connection issues. The detailed logging provides excellent visibility into the polling process.

---

`164-193`: **Good error handling with incremental failure tracking**

The consecutive failure tracking with clear thresholds provides a sensible balance between retrying transient failures and failing fast on persistent issues. The detailed error messages will be invaluable for troubleshooting in production.

---

`212-246`: **Clean shutdown with proper resource management**

The shutdown implementation ensures that all resources are properly released, and any pending futures are completed exceptionally with a clear message. This prevents resource leaks and provides clear error indication to callers.

</details>
<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java (5)</summary>

`29-30`: **Proper refactoring of controlSocket field**

Changing the `controlSocket` from a final field initialized in the constructor to a mutable field assigned in `initialize()` allows for better lifecycle management and error handling during connection.

---

`42-66`: **Well-structured initialization with comprehensive error handling**

The `initialize` method now properly separates responsibilities and handles different exception types with appropriate actions:
- InterruptedException preserves the interrupt flag
- IOException from stream setup is handled separately
- CannotConnectWithTorException is properly propagated
- All paths ensure the socket is closed on failure

This enhances the robustness of the connection process.

---

`69-91`: **Improved resource cleanup in close method**

The close method now:
1. Properly checks if the socket exists and is not already closed
2. Provides detailed logging
3. Handles exceptions without throwing runtime errors
4. Clears the output stream optional

This ensures clean shutdown and prevents resource leaks.

---

`265-308`: **Robust socket connection with retry mechanism**

The new `createAndConnectControlSocket` method significantly improves reliability by:
1. Implementing retry logic with a configurable maximum number of attempts
2. Properly cleaning up resources on failed attempts
3. Providing detailed logging for each attempt
4. Capturing and propagating the last exception for better diagnostics

This addresses the core issue with Docker container connectivity.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 ast-grep (0.38.1)</summary>

[info] 269-269: "Detected use of a Java socket that is not encrypted. As a result, the
                traffic could be read by an attacker intercepting the network traffic. Use
                an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
                instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
    - https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

</details>

</details>

---

`310-329`: **Improved output stream validation in sendCommand**

The method now properly checks if the output stream is initialized before attempting to send commands, throwing an appropriate exception with a clear message if not. The use of `orElseThrow()` is a cleaner approach than the previous implementation.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T08:49:24Z
user: login: hiciefte
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T09:20:44Z
user: login: HenrikJannsen
body: utACK
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T05:21:26Z

Review comments:
diff_hunk: @@ -262,9 +269,25 @@ private void connectToTor(int port) throws InterruptedException {
                 throw new CannotConnectWithTorException(e);
             }
         }
+        if (controlSocket == null || !controlSocket.isConnected()) {
+            throw new CannotConnectWithTorException(
+                    new IOException("Failed to connect after " + MAX_CONNECTION_ATTEMPTS + " attempts."));
+        }
+    }
+
+    private void setupStreams() throws IOException {
+        if (controlSocket == null || !controlSocket.isConnected()) {
+            throw new IllegalStateException("Cannot setup streams on a null or disconnected socket.");
+        }
+        this.outputStream = Optional.of(controlSocket.getOutputStream());
+        this.torControlReader.start(controlSocket.getInputStream());
     }
path: network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Duplicate reader/stream initialisation may start `TorControlReader` twice**

`setupStreams()` already starts the reader and captures `outputStream`, but `initialize()` repeats both calls immediately afterwards (lines 45-46).  
Starting the reader twice can spawn a second thread or throw `IllegalStateException`, and re-wrapping the output stream is redundant.

Consider removing the duplicated lines from `initialize()`:

```diff
-            torControlReader.start(controlSocket.getInputStream());
-            outputStream = Optional.of(controlSocket.getOutputStream());
```

or, alternatively, move `setupStreams()` *out* of `connectToTor()` and keep the logic solely in `initialize()`. Both approaches ensure a single initialisation path.  



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
around lines 278 to 284, the setupStreams() method initializes the outputStream
and starts the TorControlReader, but the initialize() method also repeats these
calls immediately after, causing duplicate initialization and potential errors.
To fix this, remove the duplicated outputStream assignment and TorControlReader
start calls from initialize(), ensuring these actions happen only once in
setupStreams(), or alternatively, move the setupStreams() call out of
connectToTor() and keep all initialization logic exclusively in initialize() to
maintain a single initialization path.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 1910beb to b80fd6d
created_at: 2025-05-15T11:01:40Z
author_association: NONE
diff_hunk: @@ -251,8 +250,16 @@
         int connectionAttempt = 0;
         while (connectionAttempt < MAX_CONNECTION_ATTEMPTS) {
             try {
+                // Create a new socket for each attempt
+                Socket attemptSocket = new Socket();
                 var socketAddress = new InetSocketAddress("127.0.0.1", port);
-                controlSocket.connect(socketAddress);
+                attemptSocket.connect(socketAddress);
+
+                // Assign the successful socket to the class member
+                controlSocket = attemptSocket;
+                // Setup streams for the successful socket
+                setupStreams();
path: network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Close the temporary socket on each failed connection attempt to avoid FD leaks**

`attemptSocket` is created on every retry, but if `connect()` throws `ConnectException` the socket is never closed, leaking a file descriptor each cycle.

```diff
-            } catch (ConnectException e) {
+            } catch (ConnectException e) {
+                try {
+                    attemptSocket.close();           // free FD before retrying
+                } catch (IOException ioe) {
+                    log.debug("Failed to close attempt socket after ConnectException", ioe);
+                }
                 connectionAttempt++;
                 Thread.sleep(200);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
                // Create a new socket for each attempt
                Socket attemptSocket = new Socket();
                var socketAddress = new InetSocketAddress("127.0.0.1", port);
                attemptSocket.connect(socketAddress);

                // Assign the successful socket to the class member
                controlSocket = attemptSocket;
                // Setup streams for the successful socket
                setupStreams();
            } catch (ConnectException e) {
                try {
                    attemptSocket.close();           // free FD before retrying
                } catch (IOException ioe) {
                    log.debug("Failed to close attempt socket after ConnectException", ioe);
                }
                connectionAttempt++;
                Thread.sleep(200);
            }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 ast-grep (0.31.1)</summary>

[info] 253-253: "Detected use of a Java socket that is not encrypted. As a result, the
                    traffic could be read by an attacker intercepting the network traffic. Use
                    an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
                    instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
    - https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
around lines 253 to 261, the temporary socket created for each connection
attempt is not closed if the connect() call throws a ConnectException, causing a
file descriptor leak. To fix this, wrap the connect() call in a try-catch block
and ensure that if an exception occurs, the attemptSocket is properly closed
before retrying or propagating the error.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 1910beb to b80fd6d
created_at: 2025-05-15T11:01:40Z
author_association: NONE
diff_hunk: @@ -45,26 +48,94 @@ private void startPoller() {
         Thread thread = new Thread(() -> {
             ThreadName.setName("ControlPortFilePoller.startPoller");
             try {
-                while (true) {
+                int attemptLimit = 300; // e.g., try for up to 30 seconds (300 * 100ms interval)
+                int attempts = 0;
+                Optional<Integer> lastParsedPort = Optional.empty();
+                int consecutiveReadFailures = 0;
+                final int MAX_CONSECUTIVE_READ_FAILURES = 10; // Increased limit for robustness
+                final int SETTLE_DELAY_MS = 50;
+                final int POLLING_INTERVAL_MS = 100;
+
+                log.debug("Starting control port file poller for: {}. Timeout: {}s",
+                        controlPortFilePath, attemptLimit * POLLING_INTERVAL_MS / 1000);
+
+                while (attempts < attemptLimit) {
+                    attempts++;
                     Optional<Integer> optionalPort = parsePortFromFile();
 
                     if (optionalPort.isPresent()) {
-                        portCompletableFuture.complete(optionalPort.get());
-                        break;
-                    } else {
-                        // We can't use Java's WatcherService because it misses events between event processing.
-                        // Tor writes the port to a swap file first, and renames it afterward.
-                        // The WatcherService can miss the second operation, causing a deadlock.
-                        //noinspection BusyWait
-                        Thread.sleep(100);
+                        consecutiveReadFailures = 0; // Reset counter on successful parse
+                        int currentPort = optionalPort.get();
+
+                        // Optimization: if port hasn't changed and we just failed (external check
+                        // needed for this),
path: network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Treat parser exceptions as transient instead of fatal**

`parsePortFromFile()` may throw when the file exists but is partially written (common during Tor start-up).  
Because the call is outside a try/catch, any parsing error propagates to the outer `catch (Exception)` block and kills the poller, even though the next read would likely succeed.

Wrap the call and convert parse errors into an empty `Optional` so the retry loop continues:

```diff
-    Optional<Integer> optionalPort = parsePortFromFile();
+    Optional<Integer> optionalPort;
+    try {
+        optionalPort = parsePortFromFile();
+    } catch (Exception ex) {          // include NumberFormatException, IO issues …
+        log.trace("Port file present but unparseable (attempt {}). Will retry.", attempts, ex);
+        optionalPort = Optional.empty();
+    }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
                while (attempts < attemptLimit) {
                    attempts++;
-                   Optional<Integer> optionalPort = parsePortFromFile();
+                   Optional<Integer> optionalPort;
+                   try {
+                       optionalPort = parsePortFromFile();
+                   } catch (Exception ex) {          // include NumberFormatException, IO issues …
+                       log.trace("Port file present but unparseable (attempt {}). Will retry.", attempts, ex);
+                       optionalPort = Optional.empty();
+                   }
                 
                    if (optionalPort.isPresent()) {
                        consecutiveReadFailures = 0; // Reset counter on successful parse
                        int currentPort = optionalPort.get();
    
                        // Optimization: if port hasn't changed and we just failed (external check
                        // needed for this),
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
around lines 62 to 71, the call to parsePortFromFile() is not wrapped in a
try/catch, causing parse exceptions to terminate the poller prematurely. To fix
this, wrap the parsePortFromFile() call in a try/catch block that catches
parsing exceptions and returns an empty Optional instead, allowing the retry
loop to continue on transient parse errors.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 1910beb to b80fd6d
created_at: 2025-05-15T11:01:40Z
author_association: NONE
diff_hunk: @@ -45,26 +48,94 @@ private void startPoller() {
         Thread thread = new Thread(() -> {
             ThreadName.setName("ControlPortFilePoller.startPoller");
             try {
-                while (true) {
+                int attemptLimit = 300; // e.g., try for up to 30 seconds (300 * 100ms interval)
+                int attempts = 0;
+                Optional<Integer> lastParsedPort = Optional.empty();
+                int consecutiveReadFailures = 0;
+                final int MAX_CONSECUTIVE_READ_FAILURES = 10; // Increased limit for robustness
+                final int SETTLE_DELAY_MS = 50;
+                final int POLLING_INTERVAL_MS = 100;
+
+                log.debug("Starting control port file poller for: {}. Timeout: {}s",
+                        controlPortFilePath, attemptLimit * POLLING_INTERVAL_MS / 1000);
+
+                while (attempts < attemptLimit) {
+                    attempts++;
                     Optional<Integer> optionalPort = parsePortFromFile();
 
                     if (optionalPort.isPresent()) {
-                        portCompletableFuture.complete(optionalPort.get());
-                        break;
-                    } else {
-                        // We can't use Java's WatcherService because it misses events between event processing.
-                        // Tor writes the port to a swap file first, and renames it afterward.
-                        // The WatcherService can miss the second operation, causing a deadlock.
-                        //noinspection BusyWait
-                        Thread.sleep(100);
+                        consecutiveReadFailures = 0; // Reset counter on successful parse
+                        int currentPort = optionalPort.get();
+
+                        // Optimization: if port hasn't changed and we just failed (external check
path: network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
user: login: HenrikJannsen
body: Shouldn't that be "if port has changed"
created_at: 2025-05-16T05:23:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,26 +48,94 @@ private void startPoller() {
         Thread thread = new Thread(() -> {
             ThreadName.setName("ControlPortFilePoller.startPoller");
             try {
-                while (true) {
+                int attemptLimit = 300; // e.g., try for up to 30 seconds (300 * 100ms interval)
path: network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
user: login: HenrikJannsen
body: Maybe for consistency rename to MAX_ATTEMPTS and make it final static at class level
created_at: 2025-05-16T05:24:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,26 +48,94 @@ private void startPoller() {
         Thread thread = new Thread(() -> {
             ThreadName.setName("ControlPortFilePoller.startPoller");
             try {
-                while (true) {
+                int attemptLimit = 300; // e.g., try for up to 30 seconds (300 * 100ms interval)
+                int attempts = 0;
+                Optional<Integer> lastParsedPort = Optional.empty();
+                int consecutiveReadFailures = 0;
+                final int MAX_CONSECUTIVE_READ_FAILURES = 10; // Increased limit for robustness
path: network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
user: login: HenrikJannsen
body: Maybe better move at class level and make it static.
created_at: 2025-05-16T05:25:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,26 +48,94 @@ private void startPoller() {
         Thread thread = new Thread(() -> {
             ThreadName.setName("ControlPortFilePoller.startPoller");
             try {
-                while (true) {
+                int attemptLimit = 300; // e.g., try for up to 30 seconds (300 * 100ms interval)
+                int attempts = 0;
+                Optional<Integer> lastParsedPort = Optional.empty();
+                int consecutiveReadFailures = 0;
+                final int MAX_CONSECUTIVE_READ_FAILURES = 10; // Increased limit for robustness
+                final int SETTLE_DELAY_MS = 50;
+                final int POLLING_INTERVAL_MS = 100;
+
+                log.debug("Starting control port file poller for: {}. Timeout: {}s",
+                        controlPortFilePath, attemptLimit * POLLING_INTERVAL_MS / 1000);
+
+                while (attempts < attemptLimit) {
+                    attempts++;
                     Optional<Integer> optionalPort = parsePortFromFile();
 
                     if (optionalPort.isPresent()) {
-                        portCompletableFuture.complete(optionalPort.get());
-                        break;
-                    } else {
-                        // We can't use Java's WatcherService because it misses events between event processing.
-                        // Tor writes the port to a swap file first, and renames it afterward.
-                        // The WatcherService can miss the second operation, causing a deadlock.
-                        //noinspection BusyWait
-                        Thread.sleep(100);
+                        consecutiveReadFailures = 0; // Reset counter on successful parse
+                        int currentPort = optionalPort.get();
+
+                        // Optimization: if port hasn't changed and we just failed (external check
+                        // needed for this),
+                        // or just to ensure stability, wait briefly after initial parse.
+                        if (lastParsedPort.isEmpty() || lastParsedPort.get() != currentPort) {
+                            log.debug("Poller parsed port {} (attempt {}). Waiting {}ms to check for stability.",
+                                    currentPort, attempts, SETTLE_DELAY_MS);
+                            Thread.sleep(SETTLE_DELAY_MS);
+                            Optional<Integer> finalCheckOptionalPort = parsePortFromFile();
+
+                            if (finalCheckOptionalPort.isPresent() && finalCheckOptionalPort.get() == currentPort) {
+                                log.info("ControlPortFilePoller confirmed stable port: {}", currentPort);
+                                portCompletableFuture.complete(currentPort);
+                                break;
+                            } else if (finalCheckOptionalPort.isPresent()) {
+                                log.warn(
+                                        "Control port file changed rapidly after initial parse ({} -> {}). Re-polling.",
+                                        currentPort, finalCheckOptionalPort.get());
+                                lastParsedPort = finalCheckOptionalPort; // Update to the newest read
+                            } else {
+                                log.warn("Control port file became unparseable after initial parse. Re-polling.");
+                                lastParsedPort = Optional.empty(); // Reset last known good port
+                            }
+                        } else {
+                            // Port parsed is the same as the last attempt, assume stable
+                            log.info("ControlPortFilePoller confirmed stable port (same as last): {}", currentPort);
+                            portCompletableFuture.complete(currentPort);
+                            break;
+                        }
+
+                    } else { // parsePortFromFile returned empty (e.g. file not found or not ready)
+                        consecutiveReadFailures++;
+                        log.trace("Control port file not found or not ready, attempt {}. Consecutive failures: {}.",
+                                attempts, consecutiveReadFailures);
+                        if (consecutiveReadFailures >= MAX_CONSECUTIVE_READ_FAILURES) {
+                            log.error(
+                                    "ControlPortFilePoller failed to parse port file after {} consecutive attempts. Giving up.",
+                                    MAX_CONSECUTIVE_READ_FAILURES);
+                            portCompletableFuture.completeExceptionally(
+                                    new ControlPortFileParseFailureException("Failed to parse port file after "
+                                            + MAX_CONSECUTIVE_READ_FAILURES + " attempts."));
+                            break;
+                        }
+                        lastParsedPort = Optional.empty(); // Reset if file unparseable
                     }
+
+                    // Main polling interval wait
+                    Thread.sleep(POLLING_INTERVAL_MS);
+
                 }
 
-            } catch (ControlPortFileParseFailureException | InterruptedException e) {
+                if (!portCompletableFuture.isDone()) {
path: network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
user: login: HenrikJannsen
body: Maybe just set a portCompletableFuture.orTimeout initially?
created_at: 2025-05-16T05:28:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,26 +48,94 @@ private void startPoller() {
         Thread thread = new Thread(() -> {
             ThreadName.setName("ControlPortFilePoller.startPoller");
             try {
-                while (true) {
+                int attemptLimit = 300; // e.g., try for up to 30 seconds (300 * 100ms interval)
+                int attempts = 0;
+                Optional<Integer> lastParsedPort = Optional.empty();
+                int consecutiveReadFailures = 0;
+                final int MAX_CONSECUTIVE_READ_FAILURES = 10; // Increased limit for robustness
+                final int SETTLE_DELAY_MS = 50;
+                final int POLLING_INTERVAL_MS = 100;
+
+                log.debug("Starting control port file poller for: {}. Timeout: {}s",
+                        controlPortFilePath, attemptLimit * POLLING_INTERVAL_MS / 1000);
+
+                while (attempts < attemptLimit) {
+                    attempts++;
                     Optional<Integer> optionalPort = parsePortFromFile();
 
                     if (optionalPort.isPresent()) {
-                        portCompletableFuture.complete(optionalPort.get());
-                        break;
-                    } else {
-                        // We can't use Java's WatcherService because it misses events between event processing.
-                        // Tor writes the port to a swap file first, and renames it afterward.
-                        // The WatcherService can miss the second operation, causing a deadlock.
-                        //noinspection BusyWait
-                        Thread.sleep(100);
+                        consecutiveReadFailures = 0; // Reset counter on successful parse
+                        int currentPort = optionalPort.get();
+
+                        // Optimization: if port hasn't changed and we just failed (external check
+                        // needed for this),
+                        // or just to ensure stability, wait briefly after initial parse.
+                        if (lastParsedPort.isEmpty() || lastParsedPort.get() != currentPort) {
+                            log.debug("Poller parsed port {} (attempt {}). Waiting {}ms to check for stability.",
+                                    currentPort, attempts, SETTLE_DELAY_MS);
+                            Thread.sleep(SETTLE_DELAY_MS);
+                            Optional<Integer> finalCheckOptionalPort = parsePortFromFile();
+
+                            if (finalCheckOptionalPort.isPresent() && finalCheckOptionalPort.get() == currentPort) {
+                                log.info("ControlPortFilePoller confirmed stable port: {}", currentPort);
+                                portCompletableFuture.complete(currentPort);
+                                break;
+                            } else if (finalCheckOptionalPort.isPresent()) {
+                                log.warn(
+                                        "Control port file changed rapidly after initial parse ({} -> {}). Re-polling.",
+                                        currentPort, finalCheckOptionalPort.get());
+                                lastParsedPort = finalCheckOptionalPort; // Update to the newest read
+                            } else {
+                                log.warn("Control port file became unparseable after initial parse. Re-polling.");
+                                lastParsedPort = Optional.empty(); // Reset last known good port
+                            }
+                        } else {
+                            // Port parsed is the same as the last attempt, assume stable
+                            log.info("ControlPortFilePoller confirmed stable port (same as last): {}", currentPort);
+                            portCompletableFuture.complete(currentPort);
+                            break;
+                        }
+
+                    } else { // parsePortFromFile returned empty (e.g. file not found or not ready)
+                        consecutiveReadFailures++;
+                        log.trace("Control port file not found or not ready, attempt {}. Consecutive failures: {}.",
+                                attempts, consecutiveReadFailures);
+                        if (consecutiveReadFailures >= MAX_CONSECUTIVE_READ_FAILURES) {
+                            log.error(
+                                    "ControlPortFilePoller failed to parse port file after {} consecutive attempts. Giving up.",
+                                    MAX_CONSECUTIVE_READ_FAILURES);
+                            portCompletableFuture.completeExceptionally(
+                                    new ControlPortFileParseFailureException("Failed to parse port file after "
+                                            + MAX_CONSECUTIVE_READ_FAILURES + " attempts."));
+                            break;
+                        }
+                        lastParsedPort = Optional.empty(); // Reset if file unparseable
                     }
+
+                    // Main polling interval wait
+                    Thread.sleep(POLLING_INTERVAL_MS);
+
                 }
 
-            } catch (ControlPortFileParseFailureException | InterruptedException e) {
+                if (!portCompletableFuture.isDone()) {
+                    log.error("ControlPortFilePoller timed out after {} attempts ({} seconds) for file: {}",
+                            attempts, attemptLimit * POLLING_INTERVAL_MS / 1000, controlPortFilePath);
+                    portCompletableFuture.completeExceptionally(
+                            new TimeoutException("ControlPortFilePoller timed out after "
+                                    + (attemptLimit * POLLING_INTERVAL_MS / 1000) + " seconds."));
+                }
+
+            } catch (InterruptedException e) {
+                log.warn("ControlPortFilePoller interrupted.", e);
+                portCompletableFuture.completeExceptionally(e);
+                Thread.currentThread().interrupt();
+            } catch (Exception e) { // Catch unexpected errors during polling
+                log.error("ControlPortFilePoller encountered an unexpected error.", e);
                 portCompletableFuture.completeExceptionally(e);
             }
-        });
+        }, "ControlPortFilePollerThread");
path: network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
user: login: HenrikJannsen
body: Thread name already set with ThreadName.setName("ControlPortFilePoller.startPoller");
created_at: 2025-05-16T05:30:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,26 +48,94 @@ private void startPoller() {
         Thread thread = new Thread(() -> {
path: network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
user: login: HenrikJannsen
body: Consider using `ExecutorFactory.newSingleThreadScheduledExecutor("ControlPortFilePollerThread")`, keeping a class reference and add a shutdown method to stop the executorService. 
created_at: 2025-05-16T05:31:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -247,24 +262,55 @@ private void registerEvents(Set<String> events) {
         }
     }
 
-    private void connectToTor(int port) throws InterruptedException {
+    private Socket createAndConnectControlSocket(int port) throws InterruptedException, CannotConnectWithTorException {
         int connectionAttempt = 0;
+        Exception lastException = null;
+
         while (connectionAttempt < MAX_CONNECTION_ATTEMPTS) {
+            Socket attemptSocket = new Socket();
             try {
                 var socketAddress = new InetSocketAddress("127.0.0.1", port);
-                controlSocket.connect(socketAddress);
-                break;
+                log.debug("Attempting to connect to Tor control port {} ({}/{})", socketAddress, connectionAttempt + 1, MAX_CONNECTION_ATTEMPTS);
+                attemptSocket.connect(socketAddress);
+                log.info("Successfully connected control socket to Tor port {} after {} attempts", port, connectionAttempt + 1);
+                return attemptSocket;
             } catch (ConnectException e) {
-                connectionAttempt++;
-                Thread.sleep(200);
+                lastException = e;
+                log.warn("ConnectException on attempt {} to Tor control port {}: {}. Closing attemptSocket.",
+                        connectionAttempt + 1, port, e.getMessage());
+                try {
+                    attemptSocket.close();
+                } catch (IOException closeEx) {
+                    log.warn("Failed to close attemptSocket after ConnectException on port {}: {}", port, closeEx.getMessage(), closeEx);
+                }
             } catch (IOException e) {
-                close();
-                throw new CannotConnectWithTorException(e);
+                lastException = e;
+                log.warn("IOException on attempt {} to Tor control port {}: {}. Closing attemptSocket.",
+                        connectionAttempt + 1, port, e.getMessage());
+                try {
+                    attemptSocket.close();
+                } catch (IOException closeEx) {
+                    log.warn("Failed to close attemptSocket after IOException on port {}: {}", port, closeEx.getMessage(), closeEx);
path: network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Security consideration with unencrypted socket**

The static analysis tool flagged the use of an unencrypted socket. While this is acceptable for local Tor control communication (which only listens on localhost), consider adding a comment explaining this security decision to prevent future confusion.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the Tor control connection is only used for localhost communication
grep -A 10 "socketAddress = new InetSocketAddress" network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
```

Length of output: 902

---



</details>

**Add clarification for unencrypted localhost‐only Tor control socket**

To prevent future confusion from static analysis tools, document that this unencrypted socket is safe because it’s strictly bound to localhost for Tor control communication.

• File: `network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java`  
  Location: around line 269, where the socket is created.

Suggested diff:
```diff
     // Connect to Tor control port on localhost only (unencrypted by design)
-    var socketAddress = new InetSocketAddress("127.0.0.1", port);
+    // Using unencrypted socket is safe here because Tor control listens only on 127.0.0.1
+    var socketAddress = new InetSocketAddress("127.0.0.1", port);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        while (connectionAttempt < MAX_CONNECTION_ATTEMPTS) {
            Socket attemptSocket = new Socket();
            try {
                // Using unencrypted socket is safe here because Tor control listens only on 127.0.0.1
                var socketAddress = new InetSocketAddress("127.0.0.1", port);
                log.debug("Attempting to connect to Tor control port {} ({}/{})", socketAddress, connectionAttempt + 1, MAX_CONNECTION_ATTEMPTS);
                attemptSocket.connect(socketAddress);
                log.info("Successfully connected control socket to Tor port {} after {} attempts", port, connectionAttempt + 1);
                return attemptSocket;
            } catch (ConnectException e) {
                lastException = e;
                log.warn("ConnectException on attempt {} to Tor control port {}: {}. Closing attemptSocket.",
                        connectionAttempt + 1, port, e.getMessage());
                try {
                    attemptSocket.close();
                } catch (IOException closeEx) {
                    log.warn("Failed to close attemptSocket after ConnectException on port {}: {}", port, closeEx.getMessage(), closeEx);
                }
            } catch (IOException e) {
                lastException = e;
                log.warn("IOException on attempt {} to Tor control port {}: {}. Closing attemptSocket.",
                        connectionAttempt + 1, port, e.getMessage());
                try {
                    attemptSocket.close();
                } catch (IOException closeEx) {
                    log.warn("Failed to close attemptSocket after IOException on port {}: {}", port, closeEx.getMessage(), closeEx);
                }
            }
            connectionAttempt++;
            Thread.sleep(RETRY_DELAY_MS);
        }
        throw lastException;
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 ast-grep (0.38.1)</summary>

[info] 269-269: "Detected use of a Java socket that is not encrypted. As a result, the
                traffic could be read by an attacker intercepting the network traffic. Use
                an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
                instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
    - https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
around lines 269 to 293, add a comment explaining that the unencrypted socket
connection to the Tor control port is safe because it is strictly bound to
localhost (127.0.0.1). This clarifies the security decision and prevents
confusion from static analysis tools by explicitly stating that the socket
communication is local-only and does not expose unencrypted data externally.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 5460b88
created_at: 2025-05-21T08:49:23Z
author_association: NONE
diff_hunk: @@ -45,26 +48,94 @@ private void startPoller() {
         Thread thread = new Thread(() -> {
             ThreadName.setName("ControlPortFilePoller.startPoller");
             try {
-                while (true) {
+                int attemptLimit = 300; // e.g., try for up to 30 seconds (300 * 100ms interval)
+                int attempts = 0;
+                Optional<Integer> lastParsedPort = Optional.empty();
+                int consecutiveReadFailures = 0;
+                final int MAX_CONSECUTIVE_READ_FAILURES = 10; // Increased limit for robustness
+                final int SETTLE_DELAY_MS = 50;
+                final int POLLING_INTERVAL_MS = 100;
+
+                log.debug("Starting control port file poller for: {}. Timeout: {}s",
+                        controlPortFilePath, attemptLimit * POLLING_INTERVAL_MS / 1000);
+
+                while (attempts < attemptLimit) {
+                    attempts++;
                     Optional<Integer> optionalPort = parsePortFromFile();
 
                     if (optionalPort.isPresent()) {
-                        portCompletableFuture.complete(optionalPort.get());
-                        break;
-                    } else {
-                        // We can't use Java's WatcherService because it misses events between event processing.
-                        // Tor writes the port to a swap file first, and renames it afterward.
-                        // The WatcherService can miss the second operation, causing a deadlock.
-                        //noinspection BusyWait
-                        Thread.sleep(100);
+                        consecutiveReadFailures = 0; // Reset counter on successful parse
+                        int currentPort = optionalPort.get();
+
+                        // Optimization: if port hasn't changed and we just failed (external check
+                        // needed for this),
+                        // or just to ensure stability, wait briefly after initial parse.
+                        if (lastParsedPort.isEmpty() || lastParsedPort.get() != currentPort) {
+                            log.debug("Poller parsed port {} (attempt {}). Waiting {}ms to check for stability.",
+                                    currentPort, attempts, SETTLE_DELAY_MS);
+                            Thread.sleep(SETTLE_DELAY_MS);
+                            Optional<Integer> finalCheckOptionalPort = parsePortFromFile();
+
+                            if (finalCheckOptionalPort.isPresent() && finalCheckOptionalPort.get() == currentPort) {
+                                log.info("ControlPortFilePoller confirmed stable port: {}", currentPort);
+                                portCompletableFuture.complete(currentPort);
+                                break;
+                            } else if (finalCheckOptionalPort.isPresent()) {
+                                log.warn(
+                                        "Control port file changed rapidly after initial parse ({} -> {}). Re-polling.",
+                                        currentPort, finalCheckOptionalPort.get());
+                                lastParsedPort = finalCheckOptionalPort; // Update to the newest read
+                            } else {
+                                log.warn("Control port file became unparseable after initial parse. Re-polling.");
+                                lastParsedPort = Optional.empty(); // Reset last known good port
+                            }
+                        } else {
+                            // Port parsed is the same as the last attempt, assume stable
+                            log.info("ControlPortFilePoller confirmed stable port (same as last): {}", currentPort);
+                            portCompletableFuture.complete(currentPort);
+                            break;
+                        }
+
+                    } else { // parsePortFromFile returned empty (e.g. file not found or not ready)
+                        consecutiveReadFailures++;
+                        log.trace("Control port file not found or not ready, attempt {}. Consecutive failures: {}.",
+                                attempts, consecutiveReadFailures);
+                        if (consecutiveReadFailures >= MAX_CONSECUTIVE_READ_FAILURES) {
+                            log.error(
+                                    "ControlPortFilePoller failed to parse port file after {} consecutive attempts. Giving up.",
+                                    MAX_CONSECUTIVE_READ_FAILURES);
+                            portCompletableFuture.completeExceptionally(
+                                    new ControlPortFileParseFailureException("Failed to parse port file after "
+                                            + MAX_CONSECUTIVE_READ_FAILURES + " attempts."));
+                            break;
+                        }
+                        lastParsedPort = Optional.empty(); // Reset if file unparseable
                     }
+
+                    // Main polling interval wait
+                    Thread.sleep(POLLING_INTERVAL_MS);
+
                 }
 
-            } catch (ControlPortFileParseFailureException | InterruptedException e) {
+                if (!portCompletableFuture.isDone()) {
+                    log.error("ControlPortFilePoller timed out after {} attempts ({} seconds) for file: {}",
+                            attempts, attemptLimit * POLLING_INTERVAL_MS / 1000, controlPortFilePath);
+                    portCompletableFuture.completeExceptionally(
+                            new TimeoutException("ControlPortFilePoller timed out after "
+                                    + (attemptLimit * POLLING_INTERVAL_MS / 1000) + " seconds."));
+                }
+
+            } catch (InterruptedException e) {
+                log.warn("ControlPortFilePoller interrupted.", e);
+                portCompletableFuture.completeExceptionally(e);
+                Thread.currentThread().interrupt();
+            } catch (Exception e) { // Catch unexpected errors during polling
+                log.error("ControlPortFilePoller encountered an unexpected error.", e);
                 portCompletableFuture.completeExceptionally(e);
             }
-        });
+        }, "ControlPortFilePollerThread");
path: network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java
user: login: hiciefte
body: I still have a thread naming in place although it is now handled by the ExecutorFactory to make it easier for bug finding in case of Tor issues. see ControPortFilePoller line 76 now.
created_at: 2025-05-21T09:20:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-15T10:58:12Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The Tor control socket management was enhanced with retry logic, improved resource cleanup, and defensive checks. The ControlPortFilePoller was refactored to use a scheduled executor for polling with stability checks, detailed logging, failure handling, and a shutdown mechanism.

## Changes

| Files                                                                                  | Change Summary                                                                                                                  |
|----------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java       | Made `controlSocket` mutable; added `createAndConnectControlSocket` with retry logic; improved `initialize` and `close` methods with detailed exception handling and logging; added defensive checks in `sendCommand`. Minor logging and cleanup. |
| network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java | Replaced infinite loop polling with `ScheduledExecutorService`-based polling; added stability checks for port reads; detailed logging for all events; added failure tracking and abort on excessive failures; introduced `shutdown()` method for clean termination. |

## Error: Could not generate a valid Mermaid diagram after multiple attempts.

## Poem

> 🐇  
> In tunnels deep where shadows play,  
> The socket wakes, then tries to stay.  
> Ten times it knocks, then finds a way,  
> To speak with Tor without delay.  
>  
> A poller watches, steady, bright,  
> Ensuring ports are just right.  
> With hops and skips, we code the night! 🌙✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b80fd6ddd468465ad8586f95ef6f12036e18963f and 5460b880f5be04f39abbba8c0ece9476f1d636e9.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java` (7 hunks)
* `network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 ast-grep (0.38.1)</summary>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java</summary>

[info] 269-269: "Detected use of a Java socket that is not encrypted. As a result, the
                traffic could be read by an attacker intercepting the network traffic. Use
                an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
                instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
    - https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (15)</summary>

* GitHub Check: Build network module on windows-latest / Build network module on windows-latest
* GitHub Check: Build wallet module on ubuntu-latest / Build wallet module on ubuntu-latest
* GitHub Check: Build Bisq on ubuntu-latest / Build Bisq on ubuntu-latest
* GitHub Check: Build network module on ubuntu-latest / Build network module on ubuntu-latest
* GitHub Check: Build wallet module on macOS-latest / Build wallet module on macOS-latest
* GitHub Check: Build tor module on macOS-latest / Build tor module on macOS-latest
* GitHub Check: Build tor module on ubuntu-latest / Build tor module on ubuntu-latest
* GitHub Check: Build wallet module on windows-latest / Build wallet module on windows-latest
* GitHub Check: Build network module on macOS-latest / Build network module on macOS-latest
* GitHub Check: Build apps module on ubuntu-latest / Build apps module on ubuntu-latest
* GitHub Check: Build tor module on windows-latest / Build tor module on windows-latest
* GitHub Check: Build apps module on windows-latest / Build apps module on windows-latest
* GitHub Check: Build apps module on macOS-latest / Build apps module on macOS-latest
* GitHub Check: Build Bisq on macOS-latest / Build Bisq on macOS-latest
* GitHub Check: Build Bisq on windows-latest / Build Bisq on windows-latest

</details>

<details>
<summary>🔇 Additional comments (23)</summary><blockquote>

<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/process/control_port/ControlPortFilePoller.java (16)</summary>

`20-27`: **Good use of ExecutorFactory for thread management.**

The imports and Lombok annotations are properly organized. Using the ExecutorFactory is a good practice for managing thread lifecycle in this context.

---

`35-39`: **Well-defined constants improve configurability.**

The static constants with descriptive names improve code readability and make the polling behavior configurable. This follows good practice of externalizing magic numbers into named constants.

---

`40-44`: **Proper instance variables for state tracking.**

These instance variables effectively track the state of the polling process, with a clear separation of concerns. The optional lastParsedPort is a good choice for representing a value that may not be present.

---

`49-54`: **Thread-safe initialization with atomic boolean.**

The use of `compareAndSet` on the atomic boolean ensures thread-safe initialization of the polling process, preventing potential race conditions if multiple threads call `parsePort()`.

---

`56-69`: **Clean executor service initialization and first task submission.**

The method properly initializes the executor service and state variables before submitting the first polling task. The debug logging provides good visibility into the polling configuration.

---

`71-110`: **Comprehensive polling cycle implementation with proper error handling.**

The polling cycle is well-structured with:
- Early exit if polling should stop
- Attempt tracking
- Timeout handling
- Port parsing with appropriate handling of present/absent values
- Stability check scheduling for new/changed ports
- Immediate completion for stable ports
- Proper error handling

The code demonstrates good separation of concerns and defensive programming.

---

`112-144`: **Robust stability check implementation.**

The stability check mechanism addresses potential race conditions by confirming port values remain stable before proceeding. This is essential for environments like Docker containers where file I/O timing can be unpredictable.

The error handling and logging provide good visibility into the stability verification process.

---

`146-168`: **Well-designed scheduling mechanism with safety checks.**

The method correctly handles various edge cases:
- Checks if polling should continue before scheduling
- Verifies the executor service is available
- Handles the case where the executor has been shut down
- Provides detailed logging for diagnostic purposes
- Properly handles the max attempts case

The additional null and shutdown checks on line 152 provide an extra safety layer beyond what `canContinuePolling()` offers.

---

`170-176`: **Clean success completion handling.**

This method completes the future, logs success, and cleans up resources properly. Resetting the consecutive failures counter is important for maintaining an accurate state if the method is reused.

---

`178-198`: **Graduated failure handling with appropriate thresholds.**

The failure handling mechanism appropriately:
1. Tracks consecutive failures
2. Checks against a threshold
3. Provides detailed error logging
4. Completes the future exceptionally when the threshold is reached
5. Cleans up resources as needed

This approach prevents premature failure while still failing fast after a reasonable number of errors.

---

`200-208`: **Clear timeout handling with proper resource cleanup.**

The timeout handling is well-implemented with appropriate error logging and resource cleanup. It properly completes the future exceptionally with a meaningful TimeoutException.

---

`210-215`: **Good unexpected exception handling.**

This method properly handles unexpected exceptions with detailed logging and completes the future exceptionally to signal the failure. The resource cleanup ensures no leaks occur.

---

`217-237`: **Comprehensive polling continuation check.**

The method thoroughly checks all conditions that should prevent further polling:
- Thread interruption
- Executor service unavailability or shutdown
- Future completion

Each case is handled with appropriate logging and resource cleanup, ensuring a clean shutdown in all scenarios.

---

`239-249`: **Safe executor service shutdown implementation.**

The method securely handles executor service shutdown by:
1. Marking the poller as not running
2. Using a local reference to avoid race conditions
3. Using ExecutorFactory's shutdown helper
4. Nulling the reference safely with a check against potential concurrent modifications

This prevents resource leaks and ensures clean termination.

---

`251-261`: **External shutdown capability improves resource management.**

Adding an external shutdown method is excellent for resource management, particularly in containerized environments. The method properly:
1. Logs the shutdown request
2. Stops the executor service
3. Completes the future exceptionally if still pending
4. Provides detailed logging on the shutdown outcome

This enables clean application shutdown when the Tor control is no longer needed.

---

`263-276`: **Improved error handling in parsePortFromFile method.**

The method now properly catches exceptions when parsing the control port file and treats them as transient errors rather than fatal ones. This matches the previous review comment suggestion and allows the polling to continue despite temporary file access or parsing issues.

</details>
<details>
<summary>network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java (7)</summary>

`29-29`: **Removed 'final' modifier to allow socket reassignment.**

The removal of the 'final' modifier from controlSocket is necessary to support the new connection pattern where the socket is created in a separate method and assigned in initialize().

---

`42-66`: **Improved initialization with explicit connection steps and error handling.**

The refactored initialize method now:
1. Creates and connects the socket using the new dedicated method
2. Sets up streams in a clear, sequential manner
3. Handles different exception types specifically with appropriate logging
4. Ensures resources are cleaned up on failure

This approach improves clarity and ensures proper resource handling in error cases.

---

`69-91`: **Enhanced resource cleanup in close method.**

The improved close method now:
1. Checks if the socket is non-null and open before closing
2. Provides better logging for debugging
3. Handles exceptions without throwing runtime exceptions
4. Properly resets the outputStream reference

These changes prevent potential NullPointerExceptions and improve diagnostics during shutdown.

---

`188-193`: **Improved warning message for removal of event listeners.**

The updated warning message explains the potential cause of the condition, making it clearer to developers why this situation might occur.

---

`220-225`: **Improved warning message for removal of HsDescEventListener.**

Similar to the previous change, this provides better context for why this warning might occur, improving diagnostic value.

---

`265-313`: **Well-implemented socket connection with retry logic and proper resource handling.**

The new createAndConnectControlSocket method:
1. Implements retry logic with configurable max attempts
2. Creates a new socket for each attempt to avoid issues with partially initialized sockets
3. Properly closes sockets on connection failure to prevent resource leaks
4. Provides detailed logging for connection attempts and failures
5. Throws a clear exception with the root cause when all attempts fail

The added security comments on lines 272-276 address the static analysis hint about using unencrypted sockets by clearly explaining why this is safe in this context (localhost-only connection).

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 ast-grep (0.38.1)</summary>

[info] 269-269: "Detected use of a Java socket that is not encrypted. As a result, the
                traffic could be read by an attacker intercepting the network traffic. Use
                an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
                instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
    - https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

</details>

</details>

---

`315-334`: **Better error handling in sendCommand method.**

The method now checks if the output stream is initialized before attempting to send commands, throwing an IllegalStateException with a clear message if the stream is not available. This prevents potential NullPointerExceptions and provides a more meaningful error message.

The suppression of the resource warning is appropriate since the stream is managed by the class lifecycle.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=bisq-network/bisq2&utm_content=3483):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: HenrikJannsen
created_at: 2025-05-16T05:42:08Z
body: A method returning the socket might provide a more clear assignment.
Something like in that patch:

```
Subject: [PATCH] Suggestions
---
Index: network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java b/network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java
--- a/network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java	(revision 191f13c0985ccf2f7b450bdfac058ba51895838f)
+++ b/network/tor/tor/src/main/java/bisq/network/tor/controller/TorControlProtocol.java	(date 1747373967007)
@@ -41,9 +41,9 @@
 
     public void initialize(int port) {
         try {
-            connectToTor(port);
-            torControlReader.start(controlSocket.getInputStream());
+            controlSocket = createAndConnectControlSocket(port);
             outputStream = Optional.of(controlSocket.getOutputStream());
+            torControlReader.start(controlSocket.getInputStream());
         } catch (IOException | InterruptedException e) {
             close();
             throw new CannotConnectWithTorException(e);
@@ -246,21 +246,15 @@
         }
     }
 
-    private void connectToTor(int port) throws InterruptedException {
+    private Socket createAndConnectControlSocket(int port) throws InterruptedException {
         int connectionAttempt = 0;
         while (connectionAttempt < MAX_CONNECTION_ATTEMPTS) {
             try {
                 // Create a new socket for each attempt
-                Socket attemptSocket = new Socket();
+                Socket socket = new Socket();
                 var socketAddress = new InetSocketAddress("127.0.0.1", port);
-                attemptSocket.connect(socketAddress);
-
-                // Assign the successful socket to the class member
-                controlSocket = attemptSocket;
-                // Setup streams for the successful socket
-                setupStreams();
-
-                break;
+                socket.connect(socketAddress);
+                return socket;
             } catch (ConnectException e) {
                 connectionAttempt++;
                 Thread.sleep(200);
@@ -269,18 +263,8 @@
                 throw new CannotConnectWithTorException(e);
             }
         }
-        if (controlSocket == null || !controlSocket.isConnected()) {
-            throw new CannotConnectWithTorException(
-                    new IOException("Failed to connect after " + MAX_CONNECTION_ATTEMPTS + " attempts."));
-        }
-    }
-
-    private void setupStreams() throws IOException {
-        if (controlSocket == null || !controlSocket.isConnected()) {
-            throw new IllegalStateException("Cannot setup streams on a null or disconnected socket.");
-        }
-        this.outputStream = Optional.of(controlSocket.getOutputStream());
-        this.torControlReader.start(controlSocket.getInputStream());
+        throw new CannotConnectWithTorException(
+                new IOException("Failed to create a socket to port " + port + " after " + connectionAttempt + " attempts."));
     }
 
     private void sendCommand(String command) {

```
author_association: CONTRIBUTOR
user: login: hiciefte
created_at: 2025-05-21T09:28:08Z
body: @HenrikJannsen I did some more code restructuring and clean ups to make the code more robust. I did run it on my cloud instance and it still works and also running Bisq 2 live with Tor on my own machine works as well
author_association: CONTRIBUTOR