Update ts3j: resend like the official client; clean up failed connects

- ts3j: native resend timing, INIT1 resends, one clientinitiv per handshake,
  non-blocking disconnect.
- Ignore errors from a connect attempt that has been superseded or cancelled.
- Show the whole cause chain in network error messages.
- -Dts3j.debug=true enables ts3j's protocol logging.
- Tests run with a throwaway user.home instead of the real profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 04:58:21 +00:00
parent c743efc3b6
commit 5fc2405d4a
4 changed files with 34 additions and 4 deletions

View File

@@ -105,6 +105,11 @@ public final class TeamspeakConnection implements TS3Listener {
private ExecutorService eventExecutor;
private volatile boolean connected;
/**
* The connect in progress. A disconnect clears it and a newer connect replaces it, which
* tells a superseded attempt that is still failing not to clean up after itself.
*/
private volatile Object connectAttempt;
private volatile int selfClientId = -1;
/** Package-private: read directly by {@link ConnectionStatsCollector}. */
volatile long connectedAtMs;
@@ -240,6 +245,8 @@ public final class TeamspeakConnection implements TS3Listener {
private void doConnect(String address, int port, String nickname, String password, LocalIdentity withIdentity,
String channel, String channelPassword) {
Object attempt = new Object();
connectAttempt = attempt;
try {
identity = withIdentity;
serverHost = address;
@@ -290,7 +297,7 @@ public final class TeamspeakConnection implements TS3Listener {
// A late packet dispatched after close() rejects on the shut-down
// command executor; harmless during teardown.
if (t instanceof java.util.concurrent.RejectedExecutionException) return;
ui.onError("Network error: " + rootMessage(t));
ui.onError("Network error: " + messageChain(t));
});
applyClientVersion();
@@ -331,6 +338,9 @@ public final class TeamspeakConnection implements TS3Listener {
// Only captures if this connection holds the microphone.
applyMicrophoneState();
} catch (Exception e) {
// Whoever took over reports on their own; cleaning up here would tear down what a newer
// connect has set up in the meantime.
if (connectAttempt != attempt) return;
connected = false;
ui.onError("Connection failed: " + rootMessage(e) + versionHint(e));
safeCleanup();
@@ -505,6 +515,7 @@ public final class TeamspeakConnection implements TS3Listener {
* close) where the JVM must not exit before the leave notification is sent.
*/
public void disconnectBlocking(String reason) {
connectAttempt = null;
leaving = true;
try {
if (client != null) client.disconnect(reason);
@@ -1667,6 +1678,18 @@ public final class TeamspeakConnection implements TS3Listener {
return s == null ? "" : s;
}
/** Every message along the cause chain, outermost first: network errors are vague without context. */
private static String messageChain(Throwable t) {
StringBuilder chain = new StringBuilder();
for (Throwable c = t; c != null; c = c.getCause() == c ? null : c.getCause()) {
String m = c.getMessage();
if (m == null || chain.indexOf(m) >= 0) continue;
if (chain.length() > 0) chain.append(": ");
chain.append(m);
}
return chain.length() > 0 ? chain.toString() : t.getClass().getSimpleName();
}
private static String rootMessage(Throwable t) {
Throwable r = t;
while (r.getCause() != null && r.getCause() != r) r = r.getCause();

View File

@@ -34,6 +34,12 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
<configuration>
<!-- The stores resolve ~/.ts3jclient once per JVM; keep every test out of the real profile. -->
<systemPropertyVariables>
<user.home>${project.build.directory}/test-home</user.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</pluginManagement>

View File

@@ -15,9 +15,10 @@ import javax.swing.SwingUtilities;
public final class Main {
public static void main(String[] args) {
// ts3j is verbose by default; keep the console quiet unless debugging.
// ts3j is verbose by default; keep the console quiet unless asked for its protocol log
// (-Dts3j.debug=true, written to stderr).
try {
com.github.manevolent.ts3j.util.Ts3Debugging.setEnabled(false);
com.github.manevolent.ts3j.util.Ts3Debugging.setEnabled(Boolean.getBoolean("ts3j.debug"));
} catch (Throwable ignored) {
}