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