Commands now spool sequentially. Command ACK queues no longer collide with pings or ack_low. Getter more stable.

This commit is contained in:
Manevolent
2018-08-02 19:52:44 -04:00
parent 964936abdf
commit 1236221f6b
5 changed files with 132 additions and 112 deletions
@@ -36,12 +36,10 @@ public abstract class AbstractCommandResponse<T> implements CommandResponse<T> {
@Override @Override
public T get() public T get()
throws ExecutionException, InterruptedException { throws ExecutionException, InterruptedException {
Ts3Debugging.debug("WAITING ON " + future);
return map(future.get()); return map(future.get());
} }
protected void completeSuccess(Iterable<MultiCommand> commands) { protected void completeSuccess(Iterable<MultiCommand> commands) {
Ts3Debugging.debug("SUCCEEDING " + future);
future.complete(commands); future.complete(commands);
} }
@@ -1,10 +1,14 @@
package com.github.manevolent.ts3j.command.response; package com.github.manevolent.ts3j.command.response;
import com.github.manevolent.ts3j.command.Command;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
public interface CommandResponse<T> { public interface CommandResponse<T> {
Command getCommand();
default void complete() default void complete()
throws ExecutionException, InterruptedException { throws ExecutionException, InterruptedException {
get(); get();
@@ -53,6 +53,8 @@ public class PacketBody0Voice extends PacketBody {
@Override @Override
public void read(ByteBuffer buffer) { public void read(ByteBuffer buffer) {
Ts3Debugging.debug(buffer.array());
packetId = buffer.getShort() & 0x0000FFFF; packetId = buffer.getShort() & 0x0000FFFF;
if (getRole() == ProtocolRole.SERVER) if (getRole() == ProtocolRole.SERVER)
@@ -41,10 +41,14 @@ public abstract class AbstractTeamspeakClientSocket
0x54, 0x53, 0x33, 0x49, 0x4E, 0x49, 0x54, 0x31 0x54, 0x53, 0x33, 0x49, 0x4E, 0x49, 0x54, 0x31
}; };
private final Object connectionStateLock = new Object();
private final LinkedBlockingDeque<Packet> readQueue = new LinkedBlockingDeque<>(65536);
private final Map<PacketBodyType, Reassembly> reassemblyQueue = new ConcurrentHashMap<>();
private final Map<String, Object> clientOptions = new ConcurrentHashMap<>(); private final Map<String, Object> clientOptions = new ConcurrentHashMap<>();
private final Object connectionStateLock = new Object();
private final LinkedBlockingDeque<Packet> readQueue = new LinkedBlockingDeque<>(65536);
private final Map<PacketBodyType, Reassembly> reassemblyQueue = new ConcurrentHashMap<>();
private final Map<Integer, PacketResponse> sendQueue = new ConcurrentHashMap<>(); private final Map<Integer, PacketResponse> sendQueue = new ConcurrentHashMap<>();
private final Map<Integer, PacketResponse> sendQueueLow = new ConcurrentHashMap<>(); private final Map<Integer, PacketResponse> sendQueueLow = new ConcurrentHashMap<>();
@@ -65,8 +69,9 @@ public abstract class AbstractTeamspeakClientSocket
private long lastPing; private long lastPing;
private final NetworkReader networkReader = new NetworkReader(); private final NetworkReader networkReader = new NetworkReader();
private final NetworkHandler networkHandler = new NetworkHandler();
private Thread networkThread = null; private Thread networkThread = null;
private final NetworkHandler networkHandler = new NetworkHandler();
private Thread handlerThread = null; private Thread handlerThread = null;
private boolean reading = false; private boolean reading = false;
@@ -365,11 +370,28 @@ public abstract class AbstractTeamspeakClientSocket
); );
// Find if the packet must be acknowledged and create a promise for that action // Find if the packet must be acknowledged and create a promise for that action
boolean willAcknowledge = packet.getHeader().getType().getAcknolwedgedBy() != null; boolean willWaitForAck =
packet.getHeader().getType().getAcknolwedgedBy() != null &&
networkPacket.getHeader().getType() != PacketBodyType.PING;
final PacketResponse response; final PacketResponse response;
if (willAcknowledge) if (willWaitForAck) {
sendQueue.put( Map<Integer, PacketResponse> responsibleQueue;
switch (packet.getHeader().getType()) {
case COMMAND:
responsibleQueue = sendQueue;
break;
case COMMAND_LOW:
responsibleQueue = sendQueueLow;
break;
default:
throw new IllegalArgumentException("no ack queue exists for " +
packet.getHeader().getType().name());
}
responsibleQueue.put(
packet.getHeader().getPacketId(), packet.getHeader().getPacketId(),
response = new PacketResponse( response = new PacketResponse(
networkPacket, networkPacket,
@@ -377,7 +399,7 @@ public abstract class AbstractTeamspeakClientSocket
networkPacket.getHeader().getType().canResend() ? 10 : 0 networkPacket.getHeader().getType().canResend() ? 10 : 0
) )
); );
else } else
response = null; response = null;
Ts3Debugging.debug("[PROTOCOL] WRITE " + networkPacket.getHeader().getType().name()); Ts3Debugging.debug("[PROTOCOL] WRITE " + networkPacket.getHeader().getType().name());
@@ -385,7 +407,7 @@ public abstract class AbstractTeamspeakClientSocket
writeNetworkPacket(networkPacket); writeNetworkPacket(networkPacket);
// If necessary, fulfill promise for the packet acknowledgement or throw TimeoutException // If necessary, fulfill promise for the packet acknowledgement or throw TimeoutException
if (response != null && networkPacket.getHeader().getType() != PacketBodyType.PING) { if (response != null) {
while (response.getRetries() < response.getMaxTries()) { while (response.getRetries() < response.getMaxTries()) {
try { try {
if (getState() == ClientConnectionState.DISCONNECTED) if (getState() == ClientConnectionState.DISCONNECTED)
@@ -535,8 +557,6 @@ public abstract class AbstractTeamspeakClientSocket
if (ackType != null) { if (ackType != null) {
switch (ackType) { switch (ackType) {
case ACK: case ACK:
Ts3Debugging.debug("[PROTOCOL] Acknowledge " + packet + " " + packet.getHeader().getType().name()
+ " id=" + packet.getHeader().getPacketId());
writePacket(new PacketBody6Ack(getRole().getOut(), packet.getHeader().getPacketId())); writePacket(new PacketBody6Ack(getRole().getOut(), packet.getHeader().getPacketId()));
break; break;
case ACK_LOW: case ACK_LOW:
@@ -567,20 +587,20 @@ public abstract class AbstractTeamspeakClientSocket
if (packet == null) continue; if (packet == null) continue;
} }
// Find if the packet is itself an acknowledgement // Find if the packet is itself an acknowledgement (ACK or ACK_LOW)
boolean handle; boolean handle;
final PacketResponse response; final PacketResponse response;
switch (packet.getHeader().getType()) { switch (packet.getHeader().getType()) {
case ACK: case ACK:
response = sendQueue.get(((PacketBody6Ack)packet.getBody()).getPacketId()); response = sendQueue.get(((PacketBody6Ack)packet.getBody()).getPacketId());
if (response == null)
Ts3Debugging.debug("Unrecognized ACK: " + ((PacketBody6Ack)packet.getBody()).getPacketId());
handle = false; handle = false;
break; break;
case ACK_LOW: case ACK_LOW:
response = sendQueueLow.get(((PacketBody7AckLow)packet.getBody()).getPacketId()); response = sendQueueLow.get(((PacketBody7AckLow)packet.getBody()).getPacketId());
handle = false; if (response == null)
break; Ts3Debugging.debug("Unrecognized ACK_LOW: " + ((PacketBody6Ack)packet.getBody()).getPacketId());
case PONG:
response = null;
handle = false; handle = false;
break; break;
default: default:
@@ -590,14 +610,14 @@ public abstract class AbstractTeamspeakClientSocket
if (packet.getHeader().getType().canResend() && packet.getHeader().getType() != PacketBodyType.INIT1) { if (packet.getHeader().getType().canResend() && packet.getHeader().getType() != PacketBodyType.INIT1) {
// Find if we already acknowledged this packet // Find if we already acknowledged this packet
handle = handle & counter.put(packet.getHeader().getPacketId()); if (counter != null)
handle = handle & counter.put(packet.getHeader().getPacketId());
} }
if (response != null) { if (response != null) {
response.acknowledge(packet); response.acknowledge(packet);
} else if (handle) {
return packet;
} }
if (handle) return packet;
} }
throw new IllegalStateException("no longer reading"); throw new IllegalStateException("no longer reading");
@@ -947,7 +967,6 @@ public abstract class AbstractTeamspeakClientSocket
if (amountMoved > 0) { // should always be true if (amountMoved > 0) { // should always be true
int toMove = Math.max(0, bufferSize - amountMoved); int toMove = Math.max(0, bufferSize - amountMoved);
Ts3Debugging.debug(Arrays.toString(buffer));
if (toMove > 0 && toMove < bufferSize) if (toMove > 0 && toMove < bufferSize)
System.arraycopy(buffer, amountMoved, buffer, 0, toMove); System.arraycopy(buffer, amountMoved, buffer, 0, toMove);
@@ -955,7 +974,6 @@ public abstract class AbstractTeamspeakClientSocket
for (int i = bufferSize - toNullify; i < bufferSize; i ++) for (int i = bufferSize - toNullify; i < bufferSize; i ++)
buffer[i] = null; buffer[i] = null;
Ts3Debugging.debug(Arrays.toString(buffer));
int bufferStartGeneration = bufferStart.getKey(); int bufferStartGeneration = bufferStart.getKey();
int bufferStartPosition = bufferStart.getValue(); int bufferStartPosition = bufferStart.getValue();
@@ -992,8 +1010,6 @@ public abstract class AbstractTeamspeakClientSocket
} }
} }
public static class RemoteCounterFullModulated extends RemoteCounterFull { public static class RemoteCounterFullModulated extends RemoteCounterFull {
private final int modulationSize; private final int modulationSize;
private final int modulationsPerGeneration; private final int modulationsPerGeneration;
@@ -1035,8 +1051,10 @@ public abstract class AbstractTeamspeakClientSocket
public interface LocalCounter { public interface LocalCounter {
Pair<Integer, Integer> next(); Pair<Integer, Integer> next();
int getPacketId(); int getPacketId();
boolean setPacketId(int packetId); boolean setPacketId(int packetId);
int getGeneration(); int getGeneration();
void setGeneration(int i); void setGeneration(int i);
@@ -59,7 +59,7 @@ public class LocalTeamspeakClientSocket
private int acceptingReturnCode = 0; private int acceptingReturnCode = 0;
private int lastReturnCode = 0; private int lastReturnCode = 0;
private Map<Integer, CommandProcessor> awaitingCommands = new ConcurrentHashMap<>(); private Map<Integer, ClientCommandResponse> awaitingCommands = new ConcurrentHashMap<>();
public LocalTeamspeakClientSocket() { public LocalTeamspeakClientSocket() {
super(); super();
@@ -204,7 +204,7 @@ public class LocalTeamspeakClientSocket
processor.process(client, singleCommand); processor.process(client, singleCommand);
} else if (singleCommand.getName().startsWith("notify")) { } else if (singleCommand.getName().startsWith("notify")) {
TS3Event event = TS3Event.createEvent(singleCommand); TS3Event event = TS3Event.createEvent(singleCommand);
for (TS3Listener listener : listeners) event.fire(listener); listeners.forEach(event::fire);
} else { } else {
CommandProcessor awaitingCommandProcessor; CommandProcessor awaitingCommandProcessor;
@@ -218,49 +218,18 @@ public class LocalTeamspeakClientSocket
} }
public CommandResponse<Iterable<Channel>> getChannels() throws IOException, TimeoutException { public CommandResponse<Iterable<Channel>> getChannels() throws IOException, TimeoutException {
return executeMappedCommand( return executeCommand(
new SingleCommand("channellist", ProtocolRole.CLIENT), new SingleCommand("channellist", ProtocolRole.CLIENT),
command -> new Channel(command.toMap()) command -> new Channel(command.toMap())
); );
} }
private CommandResponse<Iterable<MultiCommand>> executeCommand(Command command) private CommandResponse<Iterable<SingleCommand>> executeCommand(Command command)
throws IOException, TimeoutException { throws IOException, TimeoutException {
return executeCommand(command, Function.identity()); return executeCommand(command, Function.identity());
} }
private <T> CommandResponse<T> executeCommand( private <T> CommandResponse<Iterable<T>> executeCommand(
Command command,
Function<Iterable<MultiCommand>, T> processor
) throws IOException, TimeoutException {
int returnCode;
final ClientCommandResponse<T> response;
// sync may not be necessary here
synchronized (commandSendLock) {
returnCode = lastReturnCode++;
awaitingCommands.put(returnCode, response = new ClientCommandResponse<>(returnCode, processor));
acceptingReturnCode = calculateAcceptingReturnCode();
}
try {
command.add(new CommandSingleParameter("return_code", Integer.toString(returnCode)));
writePacket(new PacketBody2Command(getRole().getOut(), command));
} catch (Throwable e) {
synchronized (commandSendLock) {
awaitingCommands.remove(returnCode);
acceptingReturnCode = calculateAcceptingReturnCode();
}
throw e;
}
return response;
}
private <T> CommandResponse<Iterable<T>> executeMappedCommand(
Command command, Command command,
Function<SingleCommand, T> processor Function<SingleCommand, T> processor
) throws IOException, TimeoutException { ) throws IOException, TimeoutException {
@@ -277,26 +246,15 @@ public class LocalTeamspeakClientSocket
List<SingleCommand> commands = new LinkedList<>(); List<SingleCommand> commands = new LinkedList<>();
for (MultiCommand multiCommand : multiCommands) commands.addAll(multiCommand.simplify()); for (MultiCommand multiCommand : multiCommands) commands.addAll(multiCommand.simplify());
List<T> results = new LinkedList<>(); return commands.stream()
for (SingleCommand command1 : commands) .map(processor::apply)
results.add(processor.apply(command1)); .collect(Collectors.toCollection(LinkedList::new));
}, command));
return results;
}));
acceptingReturnCode = calculateAcceptingReturnCode(); acceptingReturnCode = calculateAcceptingReturnCode();
}
try { if (acceptingReturnCode == returnCode)
writePacket(new PacketBody2Command(getRole().getOut(), command)); sendNextCommand();
} catch (Throwable e) {
synchronized (commandSendLock) {
awaitingCommands.remove(returnCode);
acceptingReturnCode = calculateAcceptingReturnCode();
}
throw e;
} }
return response; return response;
@@ -324,6 +282,14 @@ public class LocalTeamspeakClientSocket
return lowest; return lowest;
} }
private void sendNextCommand()
throws IOException, TimeoutException {
synchronized (commandSendLock) {
ClientCommandResponse currentResponse = awaitingCommands.get(acceptingReturnCode);
if (currentResponse != null) currentResponse.ensureSent();
}
}
private class MicrophoneTask implements Runnable { private class MicrophoneTask implements Runnable {
@Override @Override
public void run() { public void run() {
@@ -359,12 +325,18 @@ public class LocalTeamspeakClientSocket
extends AbstractCommandResponse<T> extends AbstractCommandResponse<T>
implements CommandProcessor { implements CommandProcessor {
private final int returnCode; private final int returnCode;
private final Command command;
private final List<MultiCommand> commands = new LinkedList<>(); private final List<MultiCommand> commands = new LinkedList<>();
private Object sendLock = new Object();
private boolean sent = false;
public ClientCommandResponse(int returnCode, public ClientCommandResponse(int returnCode,
Function<Iterable<MultiCommand>, T> processor) { Function<Iterable<MultiCommand>, T> processor,
Command command) {
super(processor); super(processor);
this.returnCode = returnCode; this.returnCode = returnCode;
this.command = command;
} }
@Override @Override
@@ -372,7 +344,11 @@ public class LocalTeamspeakClientSocket
MultiCommand multiCommand) MultiCommand multiCommand)
throws CommandProcessException { throws CommandProcessException {
if (multiCommand.getName().equalsIgnoreCase("error")) if (multiCommand.getName().equalsIgnoreCase("error"))
handleError(multiCommand.simplifyOne()); try {
handleError(multiCommand.simplifyOne());
} catch (IOException | TimeoutException e) {
throw new CommandProcessException(e);
}
else else
commands.add(multiCommand); commands.add(multiCommand);
} }
@@ -382,32 +358,58 @@ public class LocalTeamspeakClientSocket
SingleCommand command) SingleCommand command)
throws CommandProcessException { throws CommandProcessException {
if (command.getName().equalsIgnoreCase("error")) if (command.getName().equalsIgnoreCase("error"))
handleError(command); try {
handleError(command);
} catch (IOException | TimeoutException e) {
throw new CommandProcessException(e);
}
else else
commands.add(new MultiCommand(command.getName(), ProtocolRole.CLIENT, command)); commands.add(new MultiCommand(command.getName(), ProtocolRole.CLIENT, command));
} }
private void handleError(SingleCommand command) { private void handleError(SingleCommand command) throws IOException, TimeoutException {
int errorId = Integer.parseInt(command.get("id").getValue()); try {
int errorId = Integer.parseInt(command.get("id").getValue());
synchronized (awaitingCommands) { synchronized (commandSendLock) {
awaitingCommands.remove(returnCode); awaitingCommands.remove(returnCode);
acceptingReturnCode = calculateAcceptingReturnCode(); acceptingReturnCode = calculateAcceptingReturnCode();
} }
switch (errorId) { switch (errorId) {
case 0: case 0:
completeSuccess(commands); completeSuccess(commands);
break; break;
default: default:
completeFailure(new CommandException(command.get("msg").getValue(), errorId)); completeFailure(new CommandException(command.get("msg").getValue(), errorId));
break; break;
}
} finally {
sendNextCommand();
} }
} }
public int getReturnCode() { public int getReturnCode() {
return returnCode; return returnCode;
} }
@Override
public Command getCommand() {
return command;
}
public void ensureSent()
throws IOException, TimeoutException {
synchronized (sendLock) {
if (!sent) {
LocalTeamspeakClientSocket.this.writePacket(
new PacketBody2Command(ProtocolRole.CLIENT, command)
);
sent = true;
}
}
}
} }
private class ChannelListFinishedHandler implements CommandProcessor { private class ChannelListFinishedHandler implements CommandProcessor {
@@ -461,7 +463,7 @@ public class LocalTeamspeakClientSocket
if (timeInSeconds != null) banCommand.add(new CommandSingleParameter("time", timeInSeconds.toString())); if (timeInSeconds != null) banCommand.add(new CommandSingleParameter("time", timeInSeconds.toString()));
if (reason != null) banCommand.add(new CommandSingleParameter("banreason", reason)); if (reason != null) banCommand.add(new CommandSingleParameter("banreason", reason));
return executeMappedCommand(banCommand, command -> new Ban(command.toMap())).get(); return executeCommand(banCommand, command -> new Ban(command.toMap())).get();
} }
public Iterable<Ban> banDatabaseClient(int dbid, Integer timeInSeconds, String reason) public Iterable<Ban> banDatabaseClient(int dbid, Integer timeInSeconds, String reason)
@@ -472,7 +474,7 @@ public class LocalTeamspeakClientSocket
if (timeInSeconds != null) banCommand.add(new CommandSingleParameter("time", timeInSeconds.toString())); if (timeInSeconds != null) banCommand.add(new CommandSingleParameter("time", timeInSeconds.toString()));
if (reason != null) banCommand.add(new CommandSingleParameter("banreason", reason)); if (reason != null) banCommand.add(new CommandSingleParameter("banreason", reason));
return executeMappedCommand(banCommand, command -> new Ban(command.toMap())).get(); return executeCommand(banCommand, command -> new Ban(command.toMap())).get();
} }
public Iterable<Ban> banClient(Uid uid, Integer timeInSeconds, String reason) public Iterable<Ban> banClient(Uid uid, Integer timeInSeconds, String reason)
@@ -483,7 +485,7 @@ public class LocalTeamspeakClientSocket
if (timeInSeconds != null) banCommand.add(new CommandSingleParameter("time", timeInSeconds.toString())); if (timeInSeconds != null) banCommand.add(new CommandSingleParameter("time", timeInSeconds.toString()));
if (reason != null) banCommand.add(new CommandSingleParameter("banreason", reason)); if (reason != null) banCommand.add(new CommandSingleParameter("banreason", reason));
return executeMappedCommand(banCommand, command -> new Ban(command.toMap())).get(); return executeCommand(banCommand, command -> new Ban(command.toMap())).get();
} }
public Iterable<Ban> banClient(Client client, Integer timeInSeconds, String reason) public Iterable<Ban> banClient(Client client, Integer timeInSeconds, String reason)
@@ -509,7 +511,7 @@ public class LocalTeamspeakClientSocket
public Iterable<Ban> listBans() public Iterable<Ban> listBans()
throws InterruptedException, ExecutionException, TimeoutException, CommandException, IOException { throws InterruptedException, ExecutionException, TimeoutException, CommandException, IOException {
Command banCommand = new SingleCommand("banlist", ProtocolRole.CLIENT); Command banCommand = new SingleCommand("banlist", ProtocolRole.CLIENT);
return executeMappedCommand(banCommand, command -> new Ban(command.toMap())).get(); return executeCommand(banCommand, command -> new Ban(command.toMap())).get();
} }
public void deleteBan(Ban ban) public void deleteBan(Ban ban)
@@ -517,8 +519,8 @@ public class LocalTeamspeakClientSocket
deleteBan(ban.getId()); deleteBan(ban.getId());
} }
public CommandResponse<Iterable<MultiCommand>> channelAddPermission(int channelId, Permission... permission) public void channelAddPermission(int channelId, Permission... permission)
throws IOException, TimeoutException { throws IOException, TimeoutException, ExecutionException, InterruptedException {
MultiCommand command = new MultiCommand( MultiCommand command = new MultiCommand(
"channeladdperm", "channeladdperm",
ProtocolRole.CLIENT, ProtocolRole.CLIENT,
@@ -531,13 +533,11 @@ public class LocalTeamspeakClientSocket
command.add(new CommandSingleParameter("cid", Integer.toString(channelId))); command.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
return executeCommand(command); executeCommand(command).complete();
} }
public CommandResponse<Iterable<MultiCommand>> channelClientAddPermission(int channelId, public void channelClientAddPermission(int channelId, int clientDatabaseId, Permission... permission)
int clientDatabaseId, throws IOException, TimeoutException, ExecutionException, InterruptedException {
Permission... permission)
throws IOException, TimeoutException {
MultiCommand command = new MultiCommand( MultiCommand command = new MultiCommand(
"channelclientaddperm", "channelclientaddperm",
ProtocolRole.CLIENT, ProtocolRole.CLIENT,
@@ -551,12 +551,10 @@ public class LocalTeamspeakClientSocket
command.add(new CommandSingleParameter("cid", Integer.toString(channelId))); command.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
command.add(new CommandSingleParameter("cldbid", Integer.toString(channelId))); command.add(new CommandSingleParameter("cldbid", Integer.toString(channelId)));
return executeCommand(command); executeCommand(command).complete();
} }
public void channelClientDeletePermission(int channelId, public void channelClientDeletePermission(int channelId, int clientDatabaseId, Permission... permission)
int clientDatabaseId,
Permission... permission)
throws IOException, TimeoutException, ExecutionException, InterruptedException { throws IOException, TimeoutException, ExecutionException, InterruptedException {
MultiCommand command = new MultiCommand( MultiCommand command = new MultiCommand(
"channeldelperm", "channeldelperm",
@@ -596,7 +594,7 @@ public class LocalTeamspeakClientSocket
command.add(new CommandSingleParameter("cid", Integer.toString(channelId))); command.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
return executeMappedCommand(command, x -> new Client(x.toMap())).get(); return executeCommand(command, x -> new Client(x.toMap())).get();
} }
/** /**
@@ -629,7 +627,7 @@ public class LocalTeamspeakClientSocket
throws IOException, TimeoutException, ExecutionException, InterruptedException { throws IOException, TimeoutException, ExecutionException, InterruptedException {
Command command = new SingleCommand("channellist", ProtocolRole.CLIENT); Command command = new SingleCommand("channellist", ProtocolRole.CLIENT);
return executeMappedCommand(command, x -> new Channel(x.toMap())).get(); return executeCommand(command, x -> new Channel(x.toMap())).get();
} }
/** /**
@@ -641,7 +639,7 @@ public class LocalTeamspeakClientSocket
throws IOException, TimeoutException, ExecutionException, InterruptedException { throws IOException, TimeoutException, ExecutionException, InterruptedException {
Command command = new SingleCommand("clientlist", ProtocolRole.CLIENT); Command command = new SingleCommand("clientlist", ProtocolRole.CLIENT);
return executeMappedCommand(command, x -> new Client(x.toMap())).get(); return executeCommand(command, x -> new Client(x.toMap())).get();
} }
/** /**