Run a connection's requests on its own pool
Each action started a thread that read the socket field once it ran; a disconnect in between cleared it, and the user saw "Could not join channel: NullPointerException". Actions now capture the socket when they are issued and run on a pool that lives as long as the connection. What fails because the connection went away is no longer reported as an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -102,6 +103,8 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* has already sent has been handled. Closed together with the socket.
|
||||
*/
|
||||
private ExecutorService eventExecutor;
|
||||
/** Runs this connection's requests to the server; created on connect, shut down with the socket. */
|
||||
private volatile ExecutorService actions;
|
||||
|
||||
private volatile boolean connected;
|
||||
/**
|
||||
@@ -211,7 +214,6 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
private void pushInputHardware() {
|
||||
boolean hardware = microphoneActive;
|
||||
updateSelf(self -> self.inputHardware = hardware);
|
||||
if (client == null || !connected) return;
|
||||
selfUpdate(cmd ->
|
||||
cmd.add(new CommandSingleParameter("client_input_hardware", hardware ? "1" : "0")),
|
||||
"Microphone hardware status update failed");
|
||||
@@ -283,6 +285,11 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
return t;
|
||||
});
|
||||
client.setCommandExecutorService(eventExecutor);
|
||||
actions = Executors.newCachedThreadPool(r -> {
|
||||
Thread t = new Thread(r, "ts3j-action");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
client.setIdentity(identity);
|
||||
client.setNickname(nickname);
|
||||
client.setHWID("ts3jswing-" + Integer.toHexString(nickname.hashCode()));
|
||||
@@ -520,7 +527,9 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
LocalTeamspeakClientSocket sock = client;
|
||||
FileTransferManager ft = fileTransfers;
|
||||
ExecutorService events = eventExecutor;
|
||||
ExecutorService pool = actions;
|
||||
eventExecutor = null;
|
||||
actions = null;
|
||||
microphone = null;
|
||||
playback = null;
|
||||
client = null;
|
||||
@@ -555,6 +564,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
}
|
||||
}
|
||||
if (events != null) events.shutdownNow();
|
||||
if (pool != null) pool.shutdownNow();
|
||||
}
|
||||
|
||||
// ---- self actions ----
|
||||
@@ -594,7 +604,6 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
// Publish the input/output muted flags to the server so others see them. These
|
||||
// are runtime status, not editable client-database properties, so they go out
|
||||
// through clientupdate (like nickname/away), not clientedit.
|
||||
if (client == null || !connected) return;
|
||||
selfUpdate(cmd -> {
|
||||
cmd.add(new CommandSingleParameter("client_input_muted", micMuted ? "1" : "0"));
|
||||
cmd.add(new CommandSingleParameter("client_output_muted", deaf ? "1" : "0"));
|
||||
@@ -602,25 +611,14 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
}
|
||||
|
||||
public void joinChannel(int channelId, String password) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.joinChannel(channelId, (password == null || password.isEmpty()) ? null : password);
|
||||
} catch (Exception e) {
|
||||
error("Could not join channel: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-join").start();
|
||||
act("Could not join channel: ", sock ->
|
||||
sock.joinChannel(channelId, (password == null || password.isEmpty()) ? null : password));
|
||||
}
|
||||
|
||||
/** Moves another client (or ourselves) into a channel. */
|
||||
public void moveClient(int clientId, int channelId, String password) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.clientMove(clientId, channelId,
|
||||
(password == null || password.isEmpty()) ? null : password);
|
||||
} catch (Exception e) {
|
||||
error("Could not move client: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-move-client").start();
|
||||
act("Could not move client: ", sock -> sock.clientMove(clientId, channelId,
|
||||
(password == null || password.isEmpty()) ? null : password));
|
||||
}
|
||||
|
||||
/** Kicks a client out of its channel, back into the server's default one. */
|
||||
@@ -634,19 +632,15 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
}
|
||||
|
||||
private void kick(int clientId, int reasonId, String reason, String errorPrefix) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
SingleCommand cmd = new SingleCommand("clientkick", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId)));
|
||||
cmd.add(new CommandSingleParameter("reasonid", Integer.toString(reasonId)));
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
cmd.add(new CommandSingleParameter("reasonmsg", reason));
|
||||
}
|
||||
client.executeCommand(cmd).complete();
|
||||
} catch (Exception e) {
|
||||
error(errorPrefix + rootMessage(e));
|
||||
act(errorPrefix, sock -> {
|
||||
SingleCommand cmd = new SingleCommand("clientkick", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId)));
|
||||
cmd.add(new CommandSingleParameter("reasonid", Integer.toString(reasonId)));
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
cmd.add(new CommandSingleParameter("reasonmsg", reason));
|
||||
}
|
||||
}, "ts3j-kick").start();
|
||||
sock.executeCommand(cmd).complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -655,14 +649,9 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* @param seconds how long the ban lasts, or 0 for a permanent one
|
||||
*/
|
||||
public void banClient(int clientId, long seconds, String reason) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.banClient(clientId, seconds <= 0 ? null : (int) Math.min(seconds, Integer.MAX_VALUE),
|
||||
reason == null || reason.isEmpty() ? null : reason);
|
||||
} catch (Exception e) {
|
||||
error("Could not ban client: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-ban").start();
|
||||
act("Could not ban client: ", sock -> sock.banClient(clientId,
|
||||
seconds <= 0 ? null : (int) Math.min(seconds, Integer.MAX_VALUE),
|
||||
reason == null || reason.isEmpty() ? null : reason));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -676,24 +665,20 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
// The server refuses channelmove within the same parent ("already member of
|
||||
// channel"); sorting among the same siblings is an edit of channel_order.
|
||||
boolean reorder = channel != null && channel.parentId == newParentId;
|
||||
new Thread(() -> {
|
||||
try {
|
||||
SingleCommand cmd;
|
||||
if (reorder) {
|
||||
cmd = new SingleCommand("channeledit", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||
cmd.add(new CommandSingleParameter("channel_order", Integer.toString(orderPredecessorId)));
|
||||
} else {
|
||||
cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||
cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId)));
|
||||
cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId)));
|
||||
}
|
||||
client.executeCommand(cmd).complete();
|
||||
} catch (Exception e) {
|
||||
error("Could not move channel: " + rootMessage(e));
|
||||
act("Could not move channel: ", sock -> {
|
||||
SingleCommand cmd;
|
||||
if (reorder) {
|
||||
cmd = new SingleCommand("channeledit", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||
cmd.add(new CommandSingleParameter("channel_order", Integer.toString(orderPredecessorId)));
|
||||
} else {
|
||||
cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||
cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId)));
|
||||
cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId)));
|
||||
}
|
||||
}, "ts3j-move-channel").start();
|
||||
sock.executeCommand(cmd).complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -705,40 +690,27 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* won't be known yet.
|
||||
*/
|
||||
private void requestPermissionNames() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.executeCommand(new SingleCommand("permissionlist", ProtocolRole.CLIENT)).complete();
|
||||
} catch (Exception ignored) {
|
||||
// Best-effort: the menus fall back to treating unresolved permissions as 0.
|
||||
}
|
||||
}, "ts3j-permission-list").start();
|
||||
// Best-effort: the menus fall back to treating unresolved permissions as 0.
|
||||
attempt(sock -> sock.executeCommand(new SingleCommand("permissionlist", ProtocolRole.CLIENT)).complete());
|
||||
}
|
||||
|
||||
/** Assigns or removes a server group for a client (by database id, as {@code servergroupaddclient} needs). */
|
||||
public void setClientServerGroup(int clientDatabaseId, int groupId, boolean assign) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (assign) client.serverGroupAddClient(groupId, clientDatabaseId);
|
||||
else client.serverGroupRemoveClient(groupId, clientDatabaseId);
|
||||
} catch (Exception e) {
|
||||
error("Could not " + (assign ? "assign" : "remove") + " server group: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-server-group").start();
|
||||
act("Could not " + (assign ? "assign" : "remove") + " server group: ", sock -> {
|
||||
if (assign) sock.serverGroupAddClient(groupId, clientDatabaseId);
|
||||
else sock.serverGroupRemoveClient(groupId, clientDatabaseId);
|
||||
});
|
||||
}
|
||||
|
||||
/** Assigns a channel group for a client in the channel it currently sits in. */
|
||||
public void setClientChannelGroup(int clientDatabaseId, int channelId, int groupId) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
SingleCommand cmd = new SingleCommand("setclientchannelgroup", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("cgid", Integer.toString(groupId)));
|
||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||
cmd.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId)));
|
||||
client.executeCommand(cmd).complete();
|
||||
} catch (Exception e) {
|
||||
error("Could not set channel group: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-channel-group").start();
|
||||
act("Could not set channel group: ", sock -> {
|
||||
SingleCommand cmd = new SingleCommand("setclientchannelgroup", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("cgid", Integer.toString(groupId)));
|
||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||
cmd.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId)));
|
||||
sock.executeCommand(cmd).complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -749,76 +721,56 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (channelIds.isEmpty()) return;
|
||||
List<Integer> ids = List.copyOf(channelIds);
|
||||
String name = subscribed ? "channelsubscribe" : "channelunsubscribe";
|
||||
new Thread(() -> {
|
||||
try {
|
||||
List<SingleCommand> parts = new ArrayList<>(ids.size());
|
||||
for (int id : ids) {
|
||||
parts.add(new SingleCommand(name, ProtocolRole.CLIENT,
|
||||
new CommandSingleParameter("cid", Integer.toString(id))));
|
||||
}
|
||||
client.executeCommand(new MultiCommand(name, ProtocolRole.CLIENT, parts)).complete();
|
||||
} catch (Exception e) {
|
||||
error((subscribed ? "Could not subscribe: " : "Could not unsubscribe: ") + rootMessage(e));
|
||||
act(subscribed ? "Could not subscribe: " : "Could not unsubscribe: ", sock -> {
|
||||
List<SingleCommand> parts = new ArrayList<>(ids.size());
|
||||
for (int id : ids) {
|
||||
parts.add(new SingleCommand(name, ProtocolRole.CLIENT,
|
||||
new CommandSingleParameter("cid", Integer.toString(id))));
|
||||
}
|
||||
}, "ts3j-channel-subscribe").start();
|
||||
sock.executeCommand(new MultiCommand(name, ProtocolRole.CLIENT, parts)).complete();
|
||||
});
|
||||
}
|
||||
|
||||
public void sendChannelMessage(String text) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
ClientEntry self = model.getClient(selfClientId);
|
||||
int cid = self != null ? self.channelId : 0;
|
||||
client.sendChannelMessage(cid, text);
|
||||
persistOutgoingChat(ConnectionListener.ChatScope.CHANNEL, 0, "", text);
|
||||
sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null));
|
||||
} catch (Exception e) {
|
||||
error("Message failed: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-chan-msg").start();
|
||||
act("Message failed: ", sock -> {
|
||||
ClientEntry self = model.getClient(selfClientId);
|
||||
int cid = self != null ? self.channelId : 0;
|
||||
sock.sendChannelMessage(cid, text);
|
||||
persistOutgoingChat(ConnectionListener.ChatScope.CHANNEL, 0, "", text);
|
||||
sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null));
|
||||
});
|
||||
}
|
||||
|
||||
public void sendServerMessage(String text) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.sendServerMessage(text);
|
||||
persistOutgoingChat(ConnectionListener.ChatScope.SERVER, 0, "", text);
|
||||
sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars());
|
||||
} catch (Exception e) {
|
||||
error("Message failed: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-srv-msg").start();
|
||||
act("Message failed: ", sock -> {
|
||||
sock.sendServerMessage(text);
|
||||
persistOutgoingChat(ConnectionListener.ChatScope.SERVER, 0, "", text);
|
||||
sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars());
|
||||
});
|
||||
}
|
||||
|
||||
public void sendPrivateMessage(int clientId, String text) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.sendPrivateMessage(clientId, text);
|
||||
ClientEntry peer = model.getClient(clientId);
|
||||
String peerUid = peer != null ? peer.uniqueId : "";
|
||||
if (peerUid == null || peerUid.isBlank()) {
|
||||
try {
|
||||
Client info = client.getClientInfo(clientId);
|
||||
if (info != null) peerUid = info.getUniqueIdentifier();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
act("Message failed: ", sock -> {
|
||||
sock.sendPrivateMessage(clientId, text);
|
||||
ClientEntry peer = model.getClient(clientId);
|
||||
String peerUid = peer != null ? peer.uniqueId : "";
|
||||
if (peerUid == null || peerUid.isBlank()) {
|
||||
try {
|
||||
Client info = sock.getClientInfo(clientId);
|
||||
if (info != null) peerUid = info.getUniqueIdentifier();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
persistOutgoingChat(ConnectionListener.ChatScope.PRIVATE, clientId, peerUid, text);
|
||||
sound(SoundEvent.CHAT_SENT_MESSAGE_CLIENT, clientVars(clientId, null));
|
||||
} catch (Exception e) {
|
||||
error("Message failed: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-pm").start();
|
||||
persistOutgoingChat(ConnectionListener.ChatScope.PRIVATE, clientId, peerUid, text);
|
||||
sound(SoundEvent.CHAT_SENT_MESSAGE_CLIENT, clientVars(clientId, null));
|
||||
});
|
||||
}
|
||||
|
||||
public void poke(int clientId, String message) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.clientPoke(clientId, message);
|
||||
sound(SoundEvent.OTHER_SENT_POKE, clientVars(clientId, null));
|
||||
} catch (Exception e) {
|
||||
error("Poke failed: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-poke").start();
|
||||
act("Poke failed: ", sock -> {
|
||||
sock.clientPoke(clientId, message);
|
||||
sound(SoundEvent.OTHER_SENT_POKE, clientVars(clientId, null));
|
||||
});
|
||||
}
|
||||
|
||||
public void setAway(boolean away, String message) {
|
||||
@@ -852,17 +804,13 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
ui.onModelChanged();
|
||||
}
|
||||
|
||||
/** Sends a {@code clientupdate} for the local client on a background thread. */
|
||||
/** Sends a {@code clientupdate} for the local client in the background. */
|
||||
private void selfUpdate(java.util.function.Consumer<SingleCommand> fill, String errorLabel) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT);
|
||||
fill.accept(cmd);
|
||||
client.executeCommand(cmd).complete();
|
||||
} catch (Exception e) {
|
||||
error(errorLabel + ": " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-selfupdate").start();
|
||||
act(errorLabel + ": ", sock -> {
|
||||
SingleCommand cmd = new SingleCommand("clientupdate", ProtocolRole.CLIENT);
|
||||
fill.accept(cmd);
|
||||
sock.executeCommand(cmd).complete();
|
||||
});
|
||||
}
|
||||
|
||||
public double getPingMillis() {
|
||||
@@ -874,13 +822,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
}
|
||||
|
||||
public void setNickname(String nickname) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.setNickname(nickname);
|
||||
} catch (Exception e) {
|
||||
error("Rename failed: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-rename").start();
|
||||
act("Rename failed: ", sock -> sock.setNickname(nickname));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -894,7 +836,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* {@link ConnectionEventHandler#onChannelEdit}.
|
||||
*/
|
||||
public void requestChannelInfo(int channelId) {
|
||||
requestInfo("channelgetdescription", "cid", channelId, "ts3j-channelinfo");
|
||||
requestInfo("channelgetdescription", "cid", channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -902,18 +844,13 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* lacks. The answer arrives as a client update, see {@link ConnectionEventHandler#onClientChanged}.
|
||||
*/
|
||||
public void requestClientInfo(int clientId) {
|
||||
requestInfo("clientgetvariables", "clid", clientId, "ts3j-clientinfo");
|
||||
requestInfo("clientgetvariables", "clid", clientId);
|
||||
}
|
||||
|
||||
private void requestInfo(String command, String idParameter, int id, String threadName) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.executeCommand(new SingleCommand(command, ProtocolRole.CLIENT,
|
||||
new CommandSingleParameter(idParameter, Integer.toString(id)))).complete();
|
||||
} catch (Exception ignored) {
|
||||
// may be permission-restricted; leave as-is
|
||||
}
|
||||
}, threadName).start();
|
||||
/** May be refused for lack of permission; the details then stay as they are. */
|
||||
private void requestInfo(String command, String idParameter, int id) {
|
||||
attempt(sock -> sock.executeCommand(new SingleCommand(command, ProtocolRole.CLIENT,
|
||||
new CommandSingleParameter(idParameter, Integer.toString(id)))).complete());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -921,8 +858,8 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* live text instead of whatever an earlier info request cached.
|
||||
*/
|
||||
public void requestClientDescription(int clientId, BiConsumer<String, String> callback) {
|
||||
run("ts3j-client-description", callback, () -> {
|
||||
Client c = client.getClientInfo(clientId);
|
||||
run(callback, () -> {
|
||||
Client c = socket().getClientInfo(clientId);
|
||||
String description = c == null ? "" : orEmpty(c.get("client_description"));
|
||||
ClientEntry e = model.getClient(clientId);
|
||||
if (e != null) e.description = description;
|
||||
@@ -939,11 +876,11 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
*/
|
||||
public void setClientDescription(int clientId, String description, Consumer<String> callback) {
|
||||
boolean self = clientId == selfClientId;
|
||||
run("ts3j-client-description-set", (ignored, error) -> callback.accept(error), () -> {
|
||||
run((ignored, error) -> callback.accept(error), () -> {
|
||||
SingleCommand cmd = new SingleCommand("clientedit", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId)));
|
||||
cmd.add(new CommandSingleParameter("client_description", description));
|
||||
client.executeCommand(cmd).complete();
|
||||
socket().executeCommand(cmd).complete();
|
||||
// Only the other clients are told about our own update, so the local entry
|
||||
// (and the info panel showing it) has to be caught up here.
|
||||
if (self) {
|
||||
@@ -959,12 +896,12 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
|
||||
/** Reads the server's ban list, delivering it (or a failure message) off the UI thread. */
|
||||
public void requestBanList(BiConsumer<List<BanEntry>, String> callback) {
|
||||
run("ts3j-ban-list", callback, bans::list);
|
||||
run(callback, bans::list);
|
||||
}
|
||||
|
||||
/** @param callback given {@code null} on success, or the failure message */
|
||||
public void addBan(BanRule rule, Consumer<String> callback) {
|
||||
run("ts3j-ban-add", (ignored, error) -> callback.accept(error), () -> {
|
||||
run((ignored, error) -> callback.accept(error), () -> {
|
||||
bans.add(rule);
|
||||
return null;
|
||||
});
|
||||
@@ -976,7 +913,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* @param callback given {@code null} on success, or the failure message
|
||||
*/
|
||||
public void replaceBan(int banId, BanRule rule, Consumer<String> callback) {
|
||||
run("ts3j-ban-edit", (ignored, error) -> callback.accept(error), () -> {
|
||||
run((ignored, error) -> callback.accept(error), () -> {
|
||||
bans.replace(banId, rule);
|
||||
return null;
|
||||
});
|
||||
@@ -988,7 +925,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* @param callback given {@code null} on success, or the failure message
|
||||
*/
|
||||
public void deleteBans(Collection<Integer> banIds, Consumer<String> callback) {
|
||||
run("ts3j-ban-delete", (ignored, error) -> callback.accept(error), () -> {
|
||||
run((ignored, error) -> callback.accept(error), () -> {
|
||||
for (int id : banIds) bans.delete(id);
|
||||
return null;
|
||||
});
|
||||
@@ -1082,7 +1019,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* {@code callback} off the UI thread.
|
||||
*/
|
||||
public void requestChannelSettings(int channelId, BiConsumer<ChannelSettings, String> callback) {
|
||||
run("ts3j-channel-read", callback, () -> channels.readSettings(channelId));
|
||||
run(callback, () -> channels.readSettings(channelId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1091,7 +1028,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
*/
|
||||
public void requestChannelPermissions(int channelId,
|
||||
BiConsumer<Map<String, Integer>, String> callback) {
|
||||
run("ts3j-channel-perms", callback, () -> channels.readPermissions(channelId));
|
||||
run(callback, () -> channels.readPermissions(channelId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1104,7 +1041,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
Map<String, Integer> setPermissions,
|
||||
Collection<String> removePermissions,
|
||||
Consumer<String> callback) {
|
||||
run("ts3j-channel-edit", (ignored, error) -> callback.accept(error), () -> {
|
||||
run((ignored, error) -> callback.accept(error), () -> {
|
||||
channels.edit(channelId, changes);
|
||||
channels.writePermissions(channelId, setPermissions, removePermissions);
|
||||
if (changes.containsKey("channel_description")) {
|
||||
@@ -1130,7 +1067,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
public void createChannel(int parentId, Map<String, String> properties,
|
||||
Map<String, Integer> permissions,
|
||||
BiConsumer<Integer, String> callback) {
|
||||
run("ts3j-channel-create", callback, () -> {
|
||||
run(callback, () -> {
|
||||
int channelId = channels.create(parentId, properties);
|
||||
try {
|
||||
channels.writePermissions(channelId, permissions, List.of());
|
||||
@@ -1144,43 +1081,82 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
|
||||
/** The ids of the icons uploaded to this virtual server. */
|
||||
public void requestServerIcons(BiConsumer<List<Long>, String> callback) {
|
||||
run("ts3j-icon-list", callback, channels::listIcons);
|
||||
run(callback, channels::listIcons);
|
||||
}
|
||||
|
||||
/** Uploads an image as a server icon and reports the new icon's id. */
|
||||
public void uploadIcon(File source, BiConsumer<Long, String> callback) {
|
||||
run("ts3j-icon-upload", callback, () -> channels.uploadIcon(source));
|
||||
run(callback, () -> channels.uploadIcon(source));
|
||||
}
|
||||
|
||||
/** Deletes a server icon; {@code callback} is given {@code null} on success. */
|
||||
public void deleteIcon(long iconId, Consumer<String> callback) {
|
||||
run("ts3j-icon-delete", (ignored, error) -> callback.accept(error), () -> {
|
||||
run((ignored, error) -> callback.accept(error), () -> {
|
||||
channels.deleteIcon(iconId);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/** Work that produces a value or an error message, run on its own thread. */
|
||||
/** A request made on the socket of the connection it was issued on. */
|
||||
private interface Action {
|
||||
void run(LocalTeamspeakClientSocket sock) throws Exception;
|
||||
}
|
||||
|
||||
/** Work that produces a value or an error message. */
|
||||
private interface Job<T> {
|
||||
T call() throws Exception;
|
||||
}
|
||||
|
||||
/** Sends a request in the background, reporting a failure as {@code errorPrefix} and the reason. */
|
||||
private void act(String errorPrefix, Action action) {
|
||||
submit(action, e -> error(errorPrefix + rootMessage(e)));
|
||||
}
|
||||
|
||||
/** Sends a request in the background whose failure nobody needs to hear about. */
|
||||
private void attempt(Action action) {
|
||||
submit(action, e -> {
|
||||
});
|
||||
}
|
||||
|
||||
private void submit(Action action, Consumer<Exception> onFailure) {
|
||||
LocalTeamspeakClientSocket sock = client;
|
||||
ExecutorService pool = actions;
|
||||
if (sock == null || pool == null || !connected) return;
|
||||
try {
|
||||
pool.execute(() -> {
|
||||
try {
|
||||
action.run(sock);
|
||||
} catch (Exception e) {
|
||||
// Leaving fails whatever was still in flight; that is no error to report.
|
||||
if (client == sock && connected) onFailure.accept(e);
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException ignored) {
|
||||
// disconnected in the meantime
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a blocking server request in the background and hands the result — or the
|
||||
* failure message — to {@code callback}, which therefore never runs on the UI thread.
|
||||
*/
|
||||
private <T> void run(String threadName, BiConsumer<T, String> callback, Job<T> job) {
|
||||
new Thread(() -> {
|
||||
if (!connected || client == null) {
|
||||
callback.accept(null, "Not connected");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
callback.accept(job.call(), null);
|
||||
} catch (Exception e) {
|
||||
callback.accept(null, rootMessage(e));
|
||||
}
|
||||
}, threadName).start();
|
||||
private <T> void run(BiConsumer<T, String> callback, Job<T> job) {
|
||||
ExecutorService pool = actions;
|
||||
if (pool == null || client == null || !connected) {
|
||||
callback.accept(null, "Not connected");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
pool.execute(() -> {
|
||||
try {
|
||||
callback.accept(job.call(), null);
|
||||
} catch (Exception e) {
|
||||
callback.accept(null, connected ? rootMessage(e) : "Not connected");
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException e) {
|
||||
callback.accept(null, "Not connected");
|
||||
}
|
||||
}
|
||||
|
||||
/** The live socket. Package-private for {@link ChannelAdmin}. */
|
||||
@@ -1248,7 +1224,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
* @param callback given {@code null} on success, or the failure message
|
||||
*/
|
||||
public void setAvatar(File source, Consumer<String> callback) {
|
||||
run("ts3j-avatar-upload", (flag, error) -> {
|
||||
run((flag, error) -> {
|
||||
if (error == null) updateSelf(self -> self.avatarFlag = flag);
|
||||
callback.accept(error);
|
||||
}, () -> avatarAdmin.upload(source));
|
||||
@@ -1256,7 +1232,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
|
||||
/** Withdraws our avatar; {@code callback} is given {@code null} on success. */
|
||||
public void removeAvatar(Consumer<String> callback) {
|
||||
run("ts3j-avatar-remove", (ignored, error) -> {
|
||||
run((ignored, error) -> {
|
||||
if (error == null) updateSelf(self -> self.avatarFlag = "");
|
||||
callback.accept(error);
|
||||
}, () -> {
|
||||
|
||||
Reference in New Issue
Block a user