001package com.github.theholywaffle.teamspeak3; 002 003/* 004 * #%L 005 * TeamSpeak 3 Java API 006 * %% 007 * Copyright (C) 2014 Bert De Geyter 008 * %% 009 * Permission is hereby granted, free of charge, to any person obtaining a copy 010 * of this software and associated documentation files (the "Software"), to deal 011 * in the Software without restriction, including without limitation the rights 012 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 013 * copies of the Software, and to permit persons to whom the Software is 014 * furnished to do so, subject to the following conditions: 015 * 016 * The above copyright notice and this permission notice shall be included in 017 * all copies or substantial portions of the Software. 018 * 019 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 020 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 021 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 022 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 023 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 024 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 025 * THE SOFTWARE. 026 * #L% 027 */ 028 029import com.github.theholywaffle.teamspeak3.api.exception.TS3ConnectionFailedException; 030import com.github.theholywaffle.teamspeak3.api.exception.TS3QueryShutDownException; 031import com.github.theholywaffle.teamspeak3.api.reconnect.ConnectionHandler; 032import com.github.theholywaffle.teamspeak3.api.reconnect.DisconnectingConnectionHandler; 033import com.github.theholywaffle.teamspeak3.api.reconnect.ReconnectStrategy; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037import java.util.concurrent.ExecutorService; 038import java.util.concurrent.Executors; 039import java.util.concurrent.atomic.AtomicBoolean; 040 041public class TS3Query { 042 043 private static final Logger log = LoggerFactory.getLogger(TS3Query.class); 044 045 /** 046 * Artificial delay between sending commands, measured in milliseconds. 047 * <p> 048 * If the query's hostname / IP has not been added to the server's {@code query_ip_whitelist.txt}, 049 * you need to use {@link FloodRate#DEFAULT} to prevent the query from being flood-banned. 050 * </p><p> 051 * Calling {@link FloodRate#custom} allows you to use a custom command delay if neither 052 * {@link FloodRate#UNLIMITED} nor {@link FloodRate#DEFAULT} fit your needs. 053 * </p> 054 */ 055 public static class FloodRate { 056 057 /** 058 * Default delay of 350 milliseconds between commands for queries that are not whitelisted. 059 */ 060 public static final FloodRate DEFAULT = new FloodRate(350); 061 062 /** 063 * No delay between commands. If a query uses this without being whitelisted, it will likely be flood-banned. 064 */ 065 public static final FloodRate UNLIMITED = new FloodRate(0); 066 067 /** 068 * Creates a FloodRate object that represents a custom command delay. 069 * 070 * @param milliseconds 071 * the delay between sending commands in milliseconds 072 * 073 * @return a new {@code FloodRate} object representing a custom delay 074 */ 075 public static FloodRate custom(int milliseconds) { 076 if (milliseconds < 0) throw new IllegalArgumentException("Timeout must be positive"); 077 return new FloodRate(milliseconds); 078 } 079 080 private final int ms; 081 082 private FloodRate(int ms) { 083 this.ms = ms; 084 } 085 086 public int getMs() { 087 return ms; 088 } 089 } 090 091 /** 092 * The protocol used to communicate with the TeamSpeak3 server. 093 */ 094 public enum Protocol { 095 RAW, SSH 096 } 097 098 private final ConnectionHandler connectionHandler; 099 private final EventManager eventManager; 100 private final ExecutorService userThreadPool; 101 private final FileTransferHelper fileTransferHelper; 102 private final CommandQueue globalQueue; 103 private final TS3Config config; 104 105 private final AtomicBoolean connected = new AtomicBoolean(false); 106 107 private Connection connection; 108 109 /** 110 * Creates a TS3Query that connects to a TS3 server at 111 * {@code localhost:10011} using default settings. 112 */ 113 public TS3Query() { 114 this(new TS3Config()); 115 } 116 117 /** 118 * Creates a customized TS3Query that connects to a server 119 * specified by {@code config}. 120 * 121 * @param config 122 * configuration for this TS3Query 123 */ 124 public TS3Query(TS3Config config) { 125 this.config = config.freeze(); 126 this.eventManager = new EventManager(this); 127 this.userThreadPool = Executors.newCachedThreadPool(); 128 this.fileTransferHelper = new FileTransferHelper(config.getHost()); 129 this.connectionHandler = config.getReconnectStrategy().create(config.getConnectionHandler()); 130 this.globalQueue = CommandQueue.newGlobalQueue(this, connectionHandler instanceof DisconnectingConnectionHandler); 131 } 132 133 // PUBLIC 134 135 /** 136 * Tries to establish a connection to the TeamSpeak3 server. 137 * 138 * @throws IllegalStateException 139 * if this method was called from {@link ConnectionHandler#onConnect} 140 * @throws TS3ConnectionFailedException 141 * if the query can't connect to the server or the {@link ConnectionHandler} throws an exception 142 */ 143 public void connect() { 144 if (Thread.holdsLock(this)) { 145 // Check that connect is not called from onConnect 146 throw new IllegalStateException("Cannot call connect from onConnect handler"); 147 } 148 149 doConnect(); 150 } 151 152 private synchronized void doConnect() { 153 if (userThreadPool.isShutdown()) { 154 throw new IllegalStateException("The query has already been shut down"); 155 } 156 157 disconnect(); // If we're already connected 158 159 try { 160 CommandQueue queue = CommandQueue.newConnectQueue(this); 161 Connection con = new Connection(this, config, queue); 162 163 try { 164 TS3Api api = queue.getApi(); 165 if (config.getProtocol() == Protocol.RAW && config.hasLoginCredentials()) { 166 api.login(config.getUsername(), config.getPassword()); 167 } 168 connectionHandler.onConnect(api); 169 } catch (TS3QueryShutDownException e) { 170 // Disconnected during onConnect, re-throw as a TS3ConnectionFailedException 171 queue.failRemainingCommands(); 172 throw new TS3ConnectionFailedException(e); 173 } catch (Exception e) { 174 con.disconnect(); 175 queue.failRemainingCommands(); 176 throw new TS3ConnectionFailedException("ConnectionHandler threw exception in connect handler", e); 177 } 178 179 // Reject new commands and wait until the onConnect queue is empty 180 queue.shutDown(); 181 182 connection = con; 183 con.setCommandQueue(globalQueue); 184 connected.set(true); 185 } catch (TS3ConnectionFailedException conFailed) { 186 // If this is the first connection attempt, we won't run the handleDisconnect method, 187 // so we need to call shutDown from this method instead. 188 if (connection == null) shutDown(); 189 throw conFailed; 190 } 191 } 192 193 /** 194 * Disconnects the query and closes all open resources. 195 * <p> 196 * If the command queue still contains commands when this method is called, 197 * the query will first process these commands, causing this method to block. 198 * However, the query will reject any new commands as soon as this method is called. 199 * </p> 200 * 201 * @throws IllegalStateException 202 * if this method was called from {@link ConnectionHandler#onConnect} 203 */ 204 public void exit() { 205 if (Thread.holdsLock(this)) { 206 // Check that exit is not called from onConnect 207 throw new IllegalStateException("Cannot call exit from onConnect handler"); 208 } 209 210 try { 211 globalQueue.quit(); 212 } finally { 213 shutDown(); 214 } 215 } 216 217 private synchronized void shutDown() { 218 if (userThreadPool.isShutdown()) return; 219 220 disconnect(); 221 globalQueue.failRemainingCommands(); 222 userThreadPool.shutdown(); 223 } 224 225 private synchronized void disconnect() { 226 if (connection == null) return; 227 228 connection.disconnect(); 229 connected.set(false); 230 } 231 232 /** 233 * Returns {@code true} if the query is likely connected, 234 * {@code false} if the query is disconnected or currently trying to reconnect. 235 * <p> 236 * Note that the only way to really determine whether the query is connected or not 237 * is to send a command and check whether it succeeds. 238 * Thus this method could return {@code true} almost a minute after the connection 239 * has been lost, when the last keep-alive command was sent. 240 * </p><p> 241 * Please do not use this method to write your own connection handler. 242 * Instead, use the built-in classes in the {@code api.reconnect} package. 243 * </p> 244 * 245 * @return whether the query is connected or not 246 * 247 * @see TS3Config#setReconnectStrategy(ReconnectStrategy) 248 * @see TS3Config#setConnectionHandler(ConnectionHandler) 249 */ 250 public boolean isConnected() { 251 return connected.get(); 252 } 253 254 /** 255 * Gets the API object that can be used to send commands to the TS3 server. 256 * 257 * @return a {@code TS3Api} object 258 */ 259 public TS3Api getApi() { 260 return globalQueue.getApi(); 261 } 262 263 /** 264 * Gets the asynchronous API object that can be used to send commands to the TS3 server 265 * in a non-blocking manner. 266 * <p> 267 * Please only use the asynchronous API if it is really necessary and if you understand 268 * the implications of having multiple threads interact with your program. 269 * </p> 270 * 271 * @return a {@code TS3ApiAsync} object 272 */ 273 public TS3ApiAsync getAsyncApi() { 274 return globalQueue.getAsyncApi(); 275 } 276 277 // INTERNAL 278 279 void submitUserTask(final String name, final Runnable task) { 280 userThreadPool.submit(() -> { 281 try { 282 task.run(); 283 } catch (Throwable throwable) { 284 log.error(name + " threw an exception", throwable); 285 } 286 }); 287 } 288 289 EventManager getEventManager() { 290 return eventManager; 291 } 292 293 FileTransferHelper getFileTransferHelper() { 294 return fileTransferHelper; 295 } 296 297 void fireDisconnect() { 298 connected.set(false); 299 300 submitUserTask("ConnectionHandler disconnect task", this::handleDisconnect); 301 } 302 303 private void handleDisconnect() { 304 try { 305 connectionHandler.onDisconnect(this); 306 } finally { 307 synchronized (this) { 308 if (!connected.get()) { 309 shutDown(); 310 } 311 } 312 } 313 } 314}