diff --git a/environment/LogSlurp/ClientLogger/Program.cs b/environment/LogSlurp/ClientLogger/Program.cs
index 24aa03df..ccb97fe5 100644
--- a/environment/LogSlurp/ClientLogger/Program.cs
+++ b/environment/LogSlurp/ClientLogger/Program.cs
@@ -9,6 +9,8 @@
using var httpClient = new HttpClient();
var log_id = Guid.NewGuid().ToString();
var response = await httpClient.PostAsync($"http://localhost:{Port}/startNewLog", JsonContent.Create(new { log_id }));
+Console.WriteLine();
+Console.WriteLine("Session ID: " + log_id);
var ws = new ClientWebSocket();
ws.Options.SetRequestHeader("CBL-Log-ID", log_id);
@@ -31,4 +33,4 @@
var logString = await httpClient.GetStringAsync($"http://localhost:{Port}/retrieveLog");
Console.WriteLine();
Console.WriteLine("==== Retrieved ====");
-Console.WriteLine(logString);
\ No newline at end of file
+Console.WriteLine(logString);
diff --git a/environment/LogSlurp/README.md b/environment/LogSlurp/README.md
index ca290913..9d5e2dad 100644
--- a/environment/LogSlurp/README.md
+++ b/environment/LogSlurp/README.md
@@ -106,3 +106,13 @@ quit
==== Retrieved ====
ClientLogger: 2024-08-12 23:33:34,147 hello
+
+The client logger can be used to test the implementation of a logger. Do this:
+- Start the log slurper
+- Start the client logger. It will print the id of the session it started with the slurper
+- When the client logger loops asking for log messages let it sit
+- Run your logger implementation. Use the session id from the client logger and an arbitrary "tag"
+- Log a few things from your implementation. Maybe type a couple of log messages at the client logger as well
+- When you have enough logs to validate your implementation, type "quit" at the client logger
+- The client logger will display the contents of the session, including the log messages you typed at it, along with the ones sent from your logger implementation, interleaved approprately.
+
diff --git a/jenkins/pipelines/android/Jenkinsfile b/jenkins/pipelines/android/Jenkinsfile
index f2fe08b5..d14c7216 100644
--- a/jenkins/pipelines/android/Jenkinsfile
+++ b/jenkins/pipelines/android/Jenkinsfile
@@ -6,7 +6,7 @@ pipeline {
string(name: 'CBL_BUILD', defaultValue: '', description: 'Couchbase Lite Build Number')
string(name: 'SGW_URL', defaultValue: '', description: "The url of Sync Gateway to download")
}
- options { timeout(time: 30, unit: 'MINUTES') }
+ options { timeout(time: 60, unit: 'MINUTES') }
stages {
stage('Init') {
steps {
diff --git a/jenkins/pipelines/java/Jenkinsfile b/jenkins/pipelines/java/Jenkinsfile
index 7e983860..e99c499a 100644
--- a/jenkins/pipelines/java/Jenkinsfile
+++ b/jenkins/pipelines/java/Jenkinsfile
@@ -6,7 +6,7 @@ pipeline {
string(name: 'CBL_BUILD', defaultValue: '', description: 'Couchbase Lite Build Number')
string(name: 'SGW_URL', defaultValue: '', description: "The url of Sync Gateway to download")
}
- options { timeout(time: 60, unit: 'MINUTES') }
+ options { timeout(time: 120, unit: 'MINUTES') }
stages {
stage('Init') {
steps {
diff --git a/jenkins/pipelines/java/desktop/win_tests.ps1 b/jenkins/pipelines/java/desktop/win_tests.ps1
index c1240d29..1839253a 100644
--- a/jenkins/pipelines/java/desktop/win_tests.ps1
+++ b/jenkins/pipelines/java/desktop/win_tests.ps1
@@ -3,7 +3,7 @@ param (
[string]$version,
[Parameter(Mandatory=$true)]
- [string]$buildNumber
+ [string]$buildNumber,
[Parameter(Mandatory=$false)]
[string]$sgUrl,
diff --git a/servers/jak/shared/common/main/java/com/couchbase/lite/mobiletest/services/RemoteLogger.java b/servers/jak/shared/common/main/java/com/couchbase/lite/mobiletest/services/RemoteLogger.java
index 265faade..648a937d 100644
--- a/servers/jak/shared/common/main/java/com/couchbase/lite/mobiletest/services/RemoteLogger.java
+++ b/servers/jak/shared/common/main/java/com/couchbase/lite/mobiletest/services/RemoteLogger.java
@@ -15,13 +15,15 @@
//
package com.couchbase.lite.mobiletest.services;
-import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -37,7 +39,46 @@
@SuppressWarnings({"PMD.UnusedPrivateField", "PMD.SingularField"})
public class RemoteLogger extends Log.TestLogger {
private static final String TAG = "REMLOG";
+ private static final long TIMEOUT_SECS = 30;
+ @NonNull
+ private final WebSocketListener listener = new WebSocketListener() {
+ @Override
+ public void onOpen(@NonNull WebSocket socket, @NonNull Response resp) {
+ if (!(resp.isSuccessful() || (resp.code() == 101))) { fail("Failed starting new log: " + resp.code()); }
+ final WebSocket oSocket = webSocket.getAndSet(socket);
+ startLatch.countDown();
+ if (oSocket != null) { fail("Unexpected WebSocket open"); }
+ }
+
+ @Override
+ public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
+ Log.p(TAG, "Unexpected message from LogSlurper: " + text);
+ }
+
+ @Override
+ public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable t, @Nullable Response resp) {
+ stopLatch.countDown();
+ startLatch.countDown();
+ fail("WebSocket error", t);
+ }
+
+ @Override
+ public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
+ stopLatch.countDown();
+ startLatch.countDown();
+ close(1000, "Closed");
+ }
+ };
+
+ @NonNull
+ private final CountDownLatch startLatch = new CountDownLatch(1);
+ @NonNull
+ private final CountDownLatch stopLatch = new CountDownLatch(1);
+ @NonNull
+ private final AtomicReference webSocket = new AtomicReference<>();
+ @NonNull
+ private final AtomicBoolean connected = new AtomicBoolean();
@NonNull
private final String url;
@@ -46,9 +87,6 @@ public class RemoteLogger extends Log.TestLogger {
@NonNull
private final String tag;
- @Nullable
- @GuardedBy("url")
- private WebSocket webSocket;
public RemoteLogger(@NonNull String url, @NonNull String sessionId, @NonNull String tag) {
this.url = url;
@@ -56,40 +94,27 @@ public RemoteLogger(@NonNull String url, @NonNull String sessionId, @NonNull Str
this.tag = tag;
}
+ // Synchronously open a connection to the remote log server.
public void connect() {
+ if (connected.getAndSet(true)) { throw new ServerError("Attempt to reused a RemoteLogger"); }
+
new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.build()
.newWebSocket(
new Request.Builder()
- .url("\"ws://" + url + "/openLogStream")
+ .url("http://" + url + "/openLogStream")
.header("CBL-Log-ID", sessionId)
.header("CBL-Log-Tag", tag)
.get()
.build(),
- new WebSocketListener() {
- @Override
- public void onOpen(@NonNull WebSocket socket, @NonNull Response resp) {
- if (!resp.isSuccessful()) {
- fail("Failed starting new log response: " + resp.code());
- }
- synchronized (url) { webSocket = socket; }
- }
-
- @Override
- public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
- fail("Unexpected message from LogSlurper: " + text);
- }
-
- @Override
- public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable t, @Nullable Response resp) {
- fail("WebSocket error: " + t.getMessage(), t);
- close();
- }
-
- @Override
- public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) { close(); }
- });
+ listener);
+
+ try {
+ if (startLatch.await(10, TimeUnit.SECONDS)) { return; }
+ }
+ catch (InterruptedException ignore) { }
+ fail("Failed opening LogSlurper websocket");
}
@Override
@@ -99,37 +124,45 @@ public void log(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull Str
@Override
public void log(LogLevel level, String tag, String msg, Exception err) {
- final WebSocket socket;
- synchronized (url) { socket = webSocket; }
-
+ final WebSocket socket = webSocket.get();
if (socket == null) {
Log.p(TAG, "RemoteLogger is not connected");
return;
}
- final StringBuilder logMsg = new StringBuilder();
- logMsg.append(tag).append('/').append(level.toString()).append(' ').append(msg);
+ sendLogMessage(socket, new StringBuilder(tag).append('/').append(level).append(' ').append(msg).toString());
if (err != null) {
final StringWriter sw = new StringWriter();
err.printStackTrace(new PrintWriter(sw));
- logMsg.append(System.lineSeparator()).append(sw);
+ sendLogMessage(socket, sw.toString());
}
-
- socket.send(logMsg.toString());
}
@Override
- public void close() {
- final WebSocket socket;
- synchronized (url) { socket = webSocket; }
- if (socket != null) { socket.close(1000, null); }
+ public void close() { close(1001, "Closed by client"); }
+
+ private void sendLogMessage(@NonNull WebSocket socket, @NonNull String message) {
+ if (!socket.send(message)) { Log.p(TAG, "Failed to send log message"); }
}
private void fail(String message) { fail(message, null); }
private void fail(String message, Throwable e) {
- close();
+ close(1011, message);
throw new ServerError(message, e);
}
+
+ // Synchronously close the connection to the remote log server.
+ private void close(int code, @NonNull String reason) {
+ final WebSocket socket = webSocket.getAndSet(null);
+ if (socket == null) { return; }
+
+ socket.close(code, reason);
+ try {
+ if (stopLatch.await(TIMEOUT_SECS, TimeUnit.SECONDS)) { return; }
+ }
+ catch (InterruptedException ignore) { }
+ Log.p(TAG, "Failed closing LogSlurper websocket");
+ }
}