Compare commits

...

6 Commits

Author SHA1 Message Date
Stefan Meinecke
ab2344598d Add missing unidentified keep-alive methods to test stub
Implement addUnidentifiedKeepAlive and removeUnidentifiedKeepAlive in SseInitialFlushTest's Manager stub to match the updated interface.
2026-06-01 20:30:12 +00:00
Stefan Meinecke
896d6d7ff6 Tie unauthenticated WebSocket keep-alive to active client connections
Keep the unidentified socket alive while a JSON-RPC connection is open (including stdio mode) and while a D-Bus object is exported, instead of for the lifetime of the receive loop. The receive loop does not use the unauthenticated socket, so keeping it alive there was semantically wrong.

This also covers --receive-mode=manual, where no receive loop runs butclients still send messages.
2026-06-01 20:30:12 +00:00
Stefan Meinecke
9299b95401 Keep unauthenticated WebSocket alive during daemon receive loop
The unauthenticated (sealed sender) socket had no keep-alive token
registered, causing SignalWebSocket's DelayedDisconnectThread to tear
down the connection ~10s after each send. Every subsequent group message
then had to re-establish a fresh TLS connection (~6s delay).

The authenticated socket avoids this by registering a "receive" keep-alive
token for the lifetime of the receive loop. Apply the same pattern to the
unauthenticated socket: register the token alongside the authenticated one
and remove it in the same finally block.

This keeps the unidentified connection alive in daemon mode, matching the
behaviour of Signal mobile clients.
2026-06-01 20:30:12 +00:00
AsamK
de678596c6 Update test script 2026-05-25 17:39:53 +02:00
AsamK
29e65a5c8e Add missing graalvm metadata
Fixes #2050
2026-05-25 11:51:08 +02:00
AsamK
60779c91c6 Add missing graalvm metadata
Fixes #2051
2026-05-25 11:40:46 +02:00
9 changed files with 141 additions and 47 deletions

View File

@ -407,6 +407,10 @@ public interface Manager extends Closeable {
void addClosedListener(Runnable listener);
void addUnidentifiedKeepAlive(String token);
void removeUnidentifiedKeepAlive(String token);
InputStream retrieveAttachment(final String id) throws IOException;
InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;

View File

@ -1709,6 +1709,16 @@ public class ManagerImpl implements Manager {
}
}
@Override
public void addUnidentifiedKeepAlive(final String token) {
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken(token);
}
@Override
public void removeUnidentifiedKeepAlive(final String token) {
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken(token);
}
@Override
public void addCallEventListener(final CallEventListener listener) {
context.getCallManager().addCallEventListener(listener);

View File

@ -38,6 +38,13 @@ else
SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli"
fi
# Prefer line-buffered output for external commands when available
if command -v stdbuf >/dev/null 2>&1; then
STD_BUF="stdbuf -oL -eL --"
else
STD_BUF=""
fi
run() {
# To update graalvm config, set GRAALVM_HOME, e.g:
# export GRAALVM_HOME=/usr/lib/jvm/java-25-graalvm
@ -48,11 +55,23 @@ run() {
set -x
if [ "$JSON_RPC" -eq 1 ]; then
"$SIGNAL_CLI" $@
if [ -n "$STD_BUF" ]; then
$STD_BUF "$SIGNAL_CLI" $@
else
"$SIGNAL_CLI" $@
fi
elif [ "$DBUS" -eq 1 ]; then
"$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org'
if [ -n "$STD_BUF" ]; then
$STD_BUF "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org'
else
"$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org'
fi
else
"$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org'
if [ -n "$STD_BUF" ]; then
$STD_BUF "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org'
else
"$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org'
fi
fi
set +x
}
@ -98,9 +117,10 @@ link() {
rm -f "$LINK_CODE_FILE"
mkfifo "$LINK_CODE_FILE"
run_linked link -n "test-device" >"$LINK_CODE_FILE" &
read LINK_CODE <"$LINK_CODE_FILE"
LINK_PID=$!
read -r LINK_CODE <"$LINK_CODE_FILE"
run_main -a "$NUMBER" addDevice --uri "$LINK_CODE"
wait
wait $LINK_PID
run_linked -a "$NUMBER" send --note-to-self -m hi
run_main -a "$NUMBER" receive
run_linked -a "$NUMBER" receive
@ -180,6 +200,7 @@ run_main -a "$NUMBER_2" updateContact "$NUMBER_1" -n NUMBER_1 -e 10
run_main -a "$NUMBER_2" block "$NUMBER_1"
run_main -a "$NUMBER_2" unblock "$NUMBER_1"
run_main -a "$NUMBER_2" listContacts
run_main -a "$NUMBER_2" listContacts "$NUMBER_1"
run_main -a "$NUMBER_1" send "$NUMBER_2" -m hi
run_main -a "$NUMBER_2" receive
@ -207,6 +228,7 @@ run_main -a "$NUMBER_1" updateGroup -g "$GROUP_ID" -m "$NUMBER_2"
run_main -a "$NUMBER_1" listGroups -d
run_main -a "$NUMBER_1" --output=json listGroups -d
run_main -a "$NUMBER_2" receive
run_main -a "$NUMBER_2" listGroups -g "$GROUP_ID"
run_main -a "$NUMBER_2" quitGroup -g "$GROUP_ID"
run_main -a "$NUMBER_2" listGroups -d
run_main -a "$NUMBER_2" --output=json listGroups -d
@ -228,6 +250,7 @@ for OUTPUT in "plain-text" "json"; do
run_main -a "$NUMBER_2" --output="$OUTPUT" receive
run_main -a "$NUMBER_1" --output="$OUTPUT" receive
run_main -a "$NUMBER_1" --output="$OUTPUT" send -e "$NUMBER_2"
run_main -a "$NUMBER_1" --output="$OUTPUT" send "$NUMBER_2" -m test
run_main -a "$NUMBER_2" --output="$OUTPUT" receive
done
@ -235,8 +258,8 @@ done
run_main -a "$NUMBER_1" updateProfile --given-name=GIVEN --family-name=FAMILY --about=ABOUT --about-emoji=EMOJI --avatar=LICENSE --mobile-coin-address="YWJjCg=="
## Provisioning
link "$NUMBER_1"
link "$NUMBER_2"
link "$NUMBER_1" || true
link "$NUMBER_2" || true
run_main -a "$NUMBER_1" listDevices
run_linked -a "$NUMBER_1" sendSyncRequest
run_main -a "$NUMBER_1" sendContacts

View File

@ -915,6 +915,14 @@ public class DbusManagerImpl implements Manager {
}
}
@Override
public void addUnidentifiedKeepAlive(final String token) {
}
@Override
public void removeUnidentifiedKeepAlive(final String token) {
}
@Override
public void addCallEventListener(final CallEventListener listener) {
// Not supported over DBus

View File

@ -100,6 +100,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
public void initObjects() {
exportObjects();
m.addUnidentifiedKeepAlive("dbus");
if (!noReceiveOnStart) {
subscribeReceive();
}
@ -116,6 +117,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
@Override
public void close() {
m.removeUnidentifiedKeepAlive("dbus");
if (dbusMessageHandler != null) {
m.removeReceiveHandler(dbusMessageHandler);
dbusMessageHandler = null;

View File

@ -30,6 +30,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -43,8 +44,11 @@ public class SignalJsonRpcDispatcherHandler {
private final JsonRpcReader jsonRpcReader;
private final boolean noReceiveOnStart;
private final Map<Integer, ArrayList<Pair<Manager, Manager.ReceiveMessageHandler>>> receiveHandlers = new HashMap<>();
private final Map<Integer, ArrayList<Pair<Manager, Manager.CallEventListener>>> callEventHandlers = new HashMap<>();
private final Map<Integer, List<Pair<Manager, Manager.ReceiveMessageHandler>>> receiveHandlers = new HashMap<>();
private final Map<Integer, List<Pair<Manager, Manager.CallEventListener>>> callEventHandlers = new HashMap<>();
private final String connectionKeepAliveToken = "jsonrpc-" + UUID.randomUUID();
private final List<Manager> keepAliveManagers = new ArrayList<>();
private boolean connectionActive = true;
private SignalJsonRpcCommandHandler commandHandler;
public SignalJsonRpcDispatcherHandler(
@ -71,6 +75,10 @@ public class SignalJsonRpcDispatcherHandler {
c.addOnManagerAddedHandler(m -> callEventHandlers.forEach((subscriptionId, handlers) -> handlers.add(
createCallEventHandler(m, subscriptionId))));
c.getManagers().forEach(this::registerKeepAlive);
c.addOnManagerAddedHandler(this::registerKeepAlive);
c.addOnManagerRemovedHandler(this::unregisterKeepAlive);
handleConnection();
}
@ -84,6 +92,8 @@ public class SignalJsonRpcDispatcherHandler {
final var currentThread = Thread.currentThread();
m.addClosedListener(currentThread::interrupt);
registerKeepAlive(m);
handleConnection();
}
@ -204,14 +214,29 @@ public class SignalJsonRpcDispatcherHandler {
subscriptionId.ifPresent(this::unsubscribeReceive);
}
private void registerKeepAlive(final Manager m) {
if (!connectionActive) return;
m.addUnidentifiedKeepAlive(connectionKeepAliveToken);
keepAliveManagers.add(m);
}
private void unregisterKeepAlive(final Manager m) {
if (!connectionActive) return;
m.removeUnidentifiedKeepAlive(connectionKeepAliveToken);
keepAliveManagers.remove(m);
}
private void handleConnection() {
try {
jsonRpcReader.readMessages((method, params) -> commandHandler.handleRequest(objectMapper, method, params),
response -> logger.debug("Received unexpected response for id {}", response.getId()));
} finally {
connectionActive = false;
receiveHandlers.forEach((_subscriptionId, handlers) -> handlers.forEach(this::unsubscribeReceiveHandler));
receiveHandlers.clear();
unsubscribeAllCallEvents();
keepAliveManagers.forEach(m -> m.removeUnidentifiedKeepAlive(connectionKeepAliveToken));
keepAliveManagers.clear();
}
}

View File

@ -1586,42 +1586,13 @@
},
{
"type": "kotlin.reflect.jvm.internal.impl.resolve.scopes.DescriptorKindFilter",
"allPublicFields": true,
"fields": [
{
"name": "ALL"
},
{
"name": "CALLABLES"
},
{
"name": "CLASSIFIERS"
},
{
"name": "Companion"
},
{
"name": "FUNCTIONS"
},
{
"name": "NON_SINGLETON_CLASSIFIERS"
},
{
"name": "PACKAGES"
},
{
"name": "SINGLETON_CLASSIFIERS"
},
{
"name": "TYPE_ALIASES"
},
{
"name": "VALUES"
},
{
"name": "VARIABLES"
}
]
"allPublicFields": true
},
{
"type": "kotlinx.coroutines.AwaitAll"
},
{
"type": "kotlinx.coroutines.AwaitAll$AwaitAllNode"
},
{
"type": "kotlinx.coroutines.CancellableContinuationImpl",
@ -6570,6 +6541,41 @@
{
"type": "org.signal.network.api.SenderCertificate$ByteArraySerializer"
},
{
"type": "org.signal.network.api.SubmitRecaptchaChallengePayload",
"methods": [
{
"name": "getCaptcha",
"parameterTypes": []
},
{
"name": "getToken",
"parameterTypes": []
},
{
"name": "getType",
"parameterTypes": []
}
]
},
{
"type": "org.signal.network.util.JsonUtil$IdentityKeyDeserializer",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
},
{
"type": "org.signal.network.util.JsonUtil$ServiceIdDeserializer",
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
},
{
"type": "org.signal.storageservice.storage.protos.groups.AccessControl",
"fields": [

View File

@ -447,6 +447,14 @@ class SseInitialFlushTest {
public void addClosedListener(Runnable listener) {
}
@Override
public void addUnidentifiedKeepAlive(String token) {
}
@Override
public void removeUnidentifiedKeepAlive(String token) {
}
@Override
public InputStream retrieveAttachment(String id) {
return null;

View File

@ -495,6 +495,14 @@ class SubscribeCallEventsTest {
public void addClosedListener(Runnable l) {
}
@Override
public void addUnidentifiedKeepAlive(String token) {
}
@Override
public void removeUnidentifiedKeepAlive(String token) {
}
@Override
public InputStream retrieveAttachment(String id) {
return null;
@ -740,8 +748,8 @@ class SubscribeCallEventsTest {
assertEquals(1, manager1.addCount.get(), "manager1 should have one listener");
assertEquals(1, manager2.addCount.get(), "manager2 should have one listener");
// Also registers an onManagerAdded handler for receive and one for call events
assertEquals(2, multi.addedHandlers.size(), "should register onManagerAdded handlers");
// Registers onManagerAdded handlers for receive, call events, and keep-alive
assertEquals(3, multi.addedHandlers.size(), "should register onManagerAdded handlers");
}
@Test