Release a previous connection's resources on connect

A connect on an instance that was still connected, or still
connecting, replaced the socket and audio pipelines without closing
them. The fields shared between the connect, event, action and UI
threads are now volatile, and read once where they are used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 10:30:34 +00:00
parent f350b19d31
commit d28bb57598

View File

@@ -90,19 +90,23 @@ public final class TeamspeakConnection implements TS3Listener {
final BanAdmin bans = new BanAdmin(this); final BanAdmin bans = new BanAdmin(this);
private final AvatarAdmin avatarAdmin = new AvatarAdmin(this); private final AvatarAdmin avatarAdmin = new AvatarAdmin(this);
/*
* Set up by the connect thread and torn down by whichever thread notices the end; read from
* the UI, event and action threads, so each reader copies the field before using it.
*/
/** Package-private: read directly by {@link ConnectionStatsCollector}. */ /** Package-private: read directly by {@link ConnectionStatsCollector}. */
LocalTeamspeakClientSocket client; volatile LocalTeamspeakClientSocket client;
private VoiceInput microphone; private volatile VoiceInput microphone;
private VoiceOutput playback; private volatile VoiceOutput playback;
private LocalIdentity identity; private volatile LocalIdentity identity;
private FileTransferManager fileTransfers; private volatile FileTransferManager fileTransfers;
private ChatLogStore chatLogs; private volatile ChatLogStore chatLogs;
/** /**
* Runs ts3j's event callbacks. Supplying it ourselves (rather than leaving ts3j to make * Runs ts3j's event callbacks. Supplying it ourselves (rather than leaving ts3j to make
* its own) is what lets {@link #awaitEventsProcessed()} tell when everything the server * its own) is what lets {@link #awaitEventsProcessed()} tell when everything the server
* has already sent has been handled. Closed together with the socket. * has already sent has been handled. Closed together with the socket.
*/ */
private ExecutorService eventExecutor; private volatile ExecutorService eventExecutor;
/** Runs this connection's requests to the server; created on connect, shut down with the socket. */ /** Runs this connection's requests to the server; created on connect, shut down with the socket. */
private volatile ExecutorService actions; private volatile ExecutorService actions;
@@ -248,6 +252,9 @@ public final class TeamspeakConnection implements TS3Listener {
String channel, String channelPassword) { String channel, String channelPassword) {
Object attempt = new Object(); Object attempt = new Object();
connectAttempt = attempt; connectAttempt = attempt;
// A socket or audio line an earlier connect left behind must not outlive it.
connected = false;
safeCleanup();
try { try {
identity = withIdentity; identity = withIdentity;
serverHost = address; serverHost = address;
@@ -570,7 +577,8 @@ public final class TeamspeakConnection implements TS3Listener {
// ---- self actions ---- // ---- self actions ----
public void setMicMuted(boolean muted) { public void setMicMuted(boolean muted) {
if (microphone != null) microphone.setMuted(muted); VoiceInput mic = microphone;
if (mic != null) mic.setMuted(muted);
sound(muted ? SoundEvent.SOUND_CAPTURE_MUTED : SoundEvent.SOUND_CAPTURE_UNMUTED); sound(muted ? SoundEvent.SOUND_CAPTURE_MUTED : SoundEvent.SOUND_CAPTURE_UNMUTED);
pushSelfFlags(); pushSelfFlags();
} }
@@ -580,22 +588,26 @@ public final class TeamspeakConnection implements TS3Listener {
* status the server (and other clients) see, and without a mute/unmute sound. * status the server (and other clients) see, and without a mute/unmute sound.
*/ */
public void setMicLocalMuted(boolean muted) { public void setMicLocalMuted(boolean muted) {
if (microphone != null) microphone.setLocalMuted(muted); VoiceInput mic = microphone;
if (mic != null) mic.setLocalMuted(muted);
} }
public void setDeafened(boolean deaf) { public void setDeafened(boolean deaf) {
// Announce muting while we can still be heard, and unmuting once we can again. // Announce muting while we can still be heard, and unmuting once we can again.
if (deaf) sound(SoundEvent.SOUND_PLAYBACK_MUTED); if (deaf) sound(SoundEvent.SOUND_PLAYBACK_MUTED);
deafened = deaf; deafened = deaf;
if (playback != null) playback.setDeafened(deaf); VoiceOutput out = playback;
if (out != null) out.setDeafened(deaf);
if (!deaf) sound(SoundEvent.SOUND_PLAYBACK_UNMUTED); if (!deaf) sound(SoundEvent.SOUND_PLAYBACK_UNMUTED);
pushSelfFlags(); pushSelfFlags();
} }
private void pushSelfFlags() { private void pushSelfFlags() {
if (microphone == null || playback == null) return; VoiceInput mic = microphone;
boolean micMuted = microphone.isMuted(); VoiceOutput out = playback;
boolean deaf = playback.isDeafened(); if (mic == null || out == null) return;
boolean micMuted = mic.isMuted();
boolean deaf = out.isDeafened();
updateSelf(self -> { updateSelf(self -> {
self.inputMuted = micMuted; self.inputMuted = micMuted;
self.outputMuted = deaf; self.outputMuted = deaf;
@@ -815,7 +827,8 @@ public final class TeamspeakConnection implements TS3Listener {
public double getPingMillis() { public double getPingMillis() {
try { try {
return client != null ? client.getPing().getKey() * 1000.0 : -1; LocalTeamspeakClientSocket sock = client;
return sock != null ? sock.getPing().getKey() * 1000.0 : -1;
} catch (Exception e) { } catch (Exception e) {
return -1; return -1;
} }
@@ -972,9 +985,10 @@ public final class TeamspeakConnection implements TS3Listener {
private Contact applyContactPlayback(ClientEntry client) { private Contact applyContactPlayback(ClientEntry client) {
if (client == null || client.self) return null; if (client == null || client.self) return null;
Contact contact = contacts.find(client.uniqueId); Contact contact = contacts.find(client.uniqueId);
if (contact != null && playback != null) { VoiceOutput out = playback;
if (contact.automute) playback.setClientMuted(client.id, true); if (contact != null && out != null) {
playback.setClientVolume(client.id, contact.volumeGain()); if (contact.automute) out.setClientMuted(client.id, true);
out.setClientVolume(client.id, contact.volumeGain());
} }
return contact; return contact;
} }
@@ -991,7 +1005,8 @@ public final class TeamspeakConnection implements TS3Listener {
/** Remembers a per-client volume, in the contact entry when the client has one. */ /** Remembers a per-client volume, in the contact entry when the client has one. */
public void setClientVolumeModifier(ClientEntry client, double decibels) { public void setClientVolumeModifier(ClientEntry client, double decibels) {
if (playback != null) playback.setClientVolume(client.id, Math.pow(10.0, decibels / 20.0)); VoiceOutput out = playback;
if (out != null) out.setClientVolume(client.id, Math.pow(10.0, decibels / 20.0));
Contact contact = contacts.find(client.uniqueId); Contact contact = contacts.find(client.uniqueId);
if (contact == null) return; if (contact == null) return;
contact.hasVolumeModifier = decibels != 0; contact.hasVolumeModifier = decibels != 0;
@@ -1000,8 +1015,9 @@ public final class TeamspeakConnection implements TS3Listener {
} }
public double getClientVolumeModifier(ClientEntry client) { public double getClientVolumeModifier(ClientEntry client) {
if (playback == null) return 0; VoiceOutput out = playback;
double gain = playback.getClientVolume(client.id); if (out == null) return 0;
double gain = out.getClientVolume(client.id);
return gain <= 0 ? -60 : 20.0 * Math.log10(gain); return gain <= 0 ? -60 : 20.0 * Math.log10(gain);
} }