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.CommandFuture; 030import com.github.theholywaffle.teamspeak3.api.exception.TS3CommandFailedException; 031import com.github.theholywaffle.teamspeak3.api.wrapper.QueryError; 032import com.github.theholywaffle.teamspeak3.commands.Command; 033import com.github.theholywaffle.teamspeak3.commands.response.DefaultArrayResponse; 034import com.github.theholywaffle.teamspeak3.commands.response.ResponseBuilder; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import java.io.BufferedReader; 039import java.io.IOException; 040import java.io.InputStreamReader; 041import java.net.SocketTimeoutException; 042import java.util.Collection; 043import java.util.Queue; 044 045public class SocketReader extends Thread { 046 047 private static final Logger log = LoggerFactory.getLogger(SocketReader.class); 048 049 private final TS3Query ts3; 050 private final Queue<ResponseBuilder> receiveQueue; 051 private final BufferedReader in; 052 private final SocketWriter writer; 053 private final long commandTimeout; 054 private final boolean logComms; 055 056 private String lastEvent = ""; 057 058 public SocketReader(QueryIO io, SocketWriter writer, TS3Query ts3Query, TS3Config config) throws IOException { 059 super("[TeamSpeak-3-Java-API] SocketReader"); 060 this.receiveQueue = io.getReceiveQueue(); 061 this.writer = writer; 062 this.commandTimeout = config.getCommandTimeout(); 063 this.ts3 = ts3Query; 064 this.logComms = config.getEnableCommunicationsLogging(); 065 066 // Connect 067 this.in = new BufferedReader(new InputStreamReader(io.getSocket().getInputStream(), "UTF-8")); 068 int i = 0; 069 while (i < 4 || in.ready()) { 070 String welcomeMessage = in.readLine(); 071 if (logComms) log.debug("< {}", welcomeMessage); 072 i++; 073 } 074 } 075 076 @Override 077 public void run() { 078 while (!isInterrupted()) { 079 final String line; 080 081 try { 082 // Will block until a full line of text could be read. 083 line = in.readLine(); 084 } catch (SocketTimeoutException socketTimeout) { 085 // Really disconnected or just no data transferred for <commandTimeout> milliseconds? 086 if (receiveQueue.isEmpty() || writer.getIdleTime() < commandTimeout) { 087 continue; 088 } else { 089 log.error("Connection timed out.", socketTimeout); 090 break; 091 } 092 } catch (IOException io) { 093 if (!isInterrupted()) { 094 log.error("Connection error occurred.", io); 095 } 096 break; 097 } 098 099 if (line == null) { 100 // End of stream: connection terminated by server 101 log.error("Connection closed by the server."); 102 break; 103 } else if (line.isEmpty()) { 104 continue; // The server is sending garbage 105 } 106 107 if (line.startsWith("notify")) { 108 handleEvent(line); 109 } else { 110 handleCommandResponse(line); 111 } 112 } 113 114 try { 115 in.close(); 116 } catch (IOException ignored) { 117 // Ignore 118 } 119 120 if (!isInterrupted()) { 121 ts3.fireDisconnect(); 122 } 123 } 124 125 private void handleEvent(final String event) { 126 if (logComms) log.debug("[event] < {}", event); 127 128 // Filter out duplicate events for join, quit and channel move events 129 if (!isDuplicate(event)) { 130 final String arr[] = event.split(" ", 2); 131 ts3.getEventManager().fireEvent(arr[0], arr[1]); 132 } 133 } 134 135 private void handleCommandResponse(final String response) { 136 final ResponseBuilder responseBuilder = receiveQueue.peek(); 137 if (responseBuilder == null) { 138 log.warn("[UNHANDLED] < {}", response); 139 return; 140 } 141 142 if (logComms) log.debug("[{}] < {}", responseBuilder.getCommand().getName(), response); 143 144 if (response.startsWith("error ")) { 145 handleCommandError(responseBuilder, response); 146 } else { 147 responseBuilder.appendResponse(response); 148 } 149 } 150 151 private void handleCommandError(ResponseBuilder responseBuilder, final String error) { 152 final Command command = responseBuilder.getCommand(); 153 if (command.getName().equals("quit")) { 154 // Response to a quit command received, we're done 155 interrupt(); 156 } 157 158 receiveQueue.remove(); 159 160 final QueryError queryError = DefaultArrayResponse.parseError(error); 161 final CommandFuture<DefaultArrayResponse> future = command.getFuture(); 162 163 if (queryError.isSuccessful()) { 164 final DefaultArrayResponse response = responseBuilder.buildResponse(); 165 166 ts3.submitUserTask("Future SuccessListener (" + command.getName() + ")", () -> future.set(response)); 167 } else { 168 log.debug("TS3 command error: {}", queryError); 169 170 ts3.submitUserTask("Future FailureListener (" + command.getName() + ")", 171 () -> future.fail(new TS3CommandFailedException(queryError, command.getName()))); 172 } 173 } 174 175 private boolean isDuplicate(String eventMessage) { 176 if (!(eventMessage.startsWith("notifyclientmoved") 177 || eventMessage.startsWith("notifycliententerview") 178 || eventMessage.startsWith("notifyclientleftview"))) { 179 180 // Event that will never cause duplicates 181 return false; 182 } 183 184 if (eventMessage.equals(lastEvent)) { 185 // Duplicate event! 186 lastEvent = ""; // Let's only ever filter one duplicate 187 return true; 188 } 189 190 lastEvent = eventMessage; 191 return false; 192 } 193 194 void drainCommandsTo(Collection<Command> commands) { 195 for (ResponseBuilder responseBuilder : receiveQueue) { 196 commands.add(responseBuilder.getCommand()); 197 } 198 } 199}