You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
713 lines
23 KiB
713 lines
23 KiB
package com.jotuntech.sketcher.server;
|
|
|
|
import java.awt.AlphaComposite;
|
|
import java.awt.Color;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.Image;
|
|
import java.awt.Rectangle;
|
|
import java.awt.image.BufferedImage;
|
|
import java.awt.image.DataBufferInt;
|
|
import java.awt.image.ImageObserver;
|
|
import java.io.DataOutputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.channels.ClosedByInterruptException;
|
|
import java.nio.channels.ServerSocketChannel;
|
|
import java.nio.channels.SocketChannel;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.IdentityHashMap;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ArrayBlockingQueue;
|
|
import java.util.zip.DeflaterOutputStream;
|
|
|
|
import javax.swing.event.ChangeEvent;
|
|
import javax.swing.event.ChangeListener;
|
|
|
|
import com.jotuntech.sketcher.common.BitmapLayer;
|
|
import com.jotuntech.sketcher.common.Canvas;
|
|
import com.jotuntech.sketcher.common.Layer;
|
|
import com.jotuntech.sketcher.common.Log;
|
|
import com.jotuntech.sketcher.common.StreamableUtils;
|
|
import com.jotuntech.sketcher.common.TwoWayHashMap;
|
|
import com.jotuntech.sketcher.common.User;
|
|
import com.jotuntech.sketcher.server.command.KickCommand;
|
|
import com.jotuntech.sketcher.server.command.ServerMessageCommand;
|
|
import com.jotuntech.sketcher.server.command.SignOutCommand;
|
|
|
|
public class Server extends Thread implements ImageObserver {
|
|
/** Map of client connections */
|
|
private TwoWayHashMap<Integer, Connection> connectionMap;
|
|
|
|
/** Buffer for encoding commands before transmission */
|
|
private ByteBuffer commandBuffer;
|
|
|
|
/** Array of active users - always replace instead of mutating */
|
|
private User[] users;
|
|
|
|
/** Map of connections - always replace instead of muting */
|
|
private TwoWayHashMap<Integer, Connection> safeConnectionMap;
|
|
|
|
/** Layers and pixel information */
|
|
private Canvas canvas;
|
|
|
|
/** Custom attributes for the host */
|
|
private Map<String, Object> attributes = new HashMap<String, Object>();
|
|
|
|
/** TCP port number */
|
|
private int port;
|
|
|
|
/** Flattened image of canvas */
|
|
private BufferedImage image;
|
|
|
|
/** Pixel array for image */
|
|
private int[] pixels;
|
|
|
|
/** Dirty rectangle since last merge */
|
|
private Rectangle mergeDirty;
|
|
|
|
/** Data output stream for animation data */
|
|
private DataOutputStream animationDOS;
|
|
|
|
/** Optional access controller for callback to host */
|
|
private AccessController accessController;
|
|
|
|
/** Socket channel that the server listens on */
|
|
private ServerSocketChannel serverChannel;
|
|
|
|
/** Set to inject command into server loop for broadcasting to clients */
|
|
private CommandEntry injectCommand;
|
|
|
|
/** Millisecond time of last layer cleanup */
|
|
private long lastLayerClean;
|
|
|
|
/** Voice chat server */
|
|
private VoiceServer voiceServer;
|
|
|
|
/** MOTD text */
|
|
private String motd = null;
|
|
|
|
public Server(String name, int port, int width, int height) throws IOException {
|
|
super(name);
|
|
|
|
this.port = port;
|
|
this.users = new User[0];
|
|
this.safeConnectionMap = new TwoWayHashMap<Integer, Connection>();
|
|
|
|
connectionMap = new TwoWayHashMap<Integer, Connection>();
|
|
connectionMap.addChangeListener(new ChangeListener() {
|
|
public void stateChanged(ChangeEvent e) {
|
|
/** Find number of connections with signed-in users */
|
|
int numUsers = 0;
|
|
for(Connection c : connectionMap.values()) {
|
|
if(c.getUser() == null) {
|
|
continue;
|
|
}
|
|
|
|
++numUsers;
|
|
}
|
|
|
|
/** Allocate array for new user list */
|
|
User[] newUsers = new User[numUsers];
|
|
|
|
/** Populate array with users */
|
|
int index = 0;
|
|
for(Connection c : connectionMap.values()) {
|
|
if(c.getUser() == null) {
|
|
continue;
|
|
}
|
|
|
|
newUsers[index++] = c.getUser();
|
|
}
|
|
|
|
/** Replace array atomically */
|
|
users = newUsers;
|
|
|
|
/** Replicate connection map */
|
|
TwoWayHashMap<Integer, Connection> newMap = connectionMap.copy();
|
|
safeConnectionMap = newMap;
|
|
}
|
|
});
|
|
|
|
canvas = new Canvas(width, height);
|
|
|
|
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
|
|
|
|
/** Initialize flattened canvas image by filling it with white */
|
|
Graphics2D g2 = (Graphics2D)image.getGraphics();
|
|
g2.setColor(Color.WHITE);
|
|
g2.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
|
g2.dispose();
|
|
|
|
/** Create default layers */
|
|
TwoWayHashMap<Integer, Layer> layerMap = getCanvas().getLayerMap();
|
|
layerMap.put(new BitmapLayer("Sketch"));
|
|
layerMap.put(new BitmapLayer("Color"));
|
|
layerMap.put(new BitmapLayer("Shadow"));
|
|
layerMap.put(new BitmapLayer("Light"));
|
|
layerMap.put(new BitmapLayer("Ink"));
|
|
layerMap.put(new BitmapLayer("Doodle"));
|
|
|
|
/** Receive image updates from canvas */
|
|
canvas.addImageObserver(this);
|
|
|
|
/** Allocate buffer to hold command length and data */
|
|
commandBuffer = ByteBuffer.allocate(65538);
|
|
|
|
/** Start layer cleanup cycle at a random time for smooth operation of multiple servers */
|
|
lastLayerClean = System.currentTimeMillis() - Math.round(Math.random() * 30000);
|
|
|
|
/** Open listening socket */
|
|
serverChannel = ServerSocketChannel.open();
|
|
serverChannel.configureBlocking(false);
|
|
serverChannel.socket().bind(new java.net.InetSocketAddress(port));
|
|
Log.info("Sketcher server running on port " + port);
|
|
|
|
/** Start voice chat server */
|
|
voiceServer = new VoiceServer(port + 100);
|
|
voiceServer.start();
|
|
}
|
|
|
|
public Canvas getCanvas() {
|
|
return canvas;
|
|
}
|
|
|
|
public TwoWayHashMap<Integer, Connection> getConnectionMap() {
|
|
return connectionMap;
|
|
}
|
|
|
|
public TwoWayHashMap<Integer, Connection> getSafeConnectionMap() {
|
|
return safeConnectionMap;
|
|
}
|
|
|
|
public User[] getUsers() {
|
|
return users;
|
|
}
|
|
|
|
public void setAttribute(String key, Object value) {
|
|
attributes.put(key, value);
|
|
}
|
|
|
|
public Object getAttribute(String key) {
|
|
return attributes.get(key);
|
|
}
|
|
|
|
public int getPort() {
|
|
return port;
|
|
}
|
|
|
|
public BufferedImage getImage() {
|
|
return image;
|
|
}
|
|
|
|
public void announce(String message) {
|
|
Log.info("Announce: " + message);
|
|
injectCommand = new CommandEntry(0, new ServerMessageCommand(message));
|
|
}
|
|
|
|
public void kick(String username, String reason) {
|
|
Log.info("Kick: " + username + " (" + reason + ")");
|
|
/** Kick command was initialized with a name, and not a key, so we must look it up */
|
|
for(Map.Entry<Integer, Connection> e : getSafeConnectionMap().entrySet()) {
|
|
User u = e.getValue().getUser();
|
|
if(u == null) {
|
|
continue;
|
|
}
|
|
if(username.equalsIgnoreCase(u.getName())) {
|
|
Command c = new KickCommand(e.getKey(), reason);
|
|
c.perform(this, null);
|
|
injectCommand = new CommandEntry(0, c);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean imageUpdate(Image img, int infoflags, int left, int top, int width, int height) {
|
|
if(infoflags == ImageObserver.ALLBITS) {
|
|
/** Entire image needs an update */
|
|
left = 0;
|
|
top = 0;
|
|
width = getCanvas().getWidth();
|
|
height = getCanvas().getHeight();
|
|
} else {
|
|
left = Math.max(0, Math.min(getCanvas().getWidth() - 1, left));
|
|
top = Math.max(0, Math.min(getCanvas().getHeight() - 1, top));
|
|
width = Math.max(0, Math.min(getCanvas().getWidth(), left + width) - left);
|
|
height = Math.max(0, Math.min(getCanvas().getHeight(), top + height) - top);
|
|
}
|
|
|
|
Graphics2D g2 = (Graphics2D)image.getGraphics();
|
|
|
|
/** Set clipping rectangle */
|
|
g2.setClip(left, top, width, height);
|
|
|
|
/** Composite source over destination */
|
|
AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1);
|
|
g2.setComposite(ac);
|
|
|
|
/** Clear with white */
|
|
g2.setColor(Color.WHITE);
|
|
g2.fillRect(left, top, width, height);
|
|
|
|
/** Create a map of layers, showing which of them have phantom layers */
|
|
IdentityHashMap<Layer, Set<Layer>> selectedLayers = new IdentityHashMap<Layer, Set<Layer>>();
|
|
for(Connection c : connectionMap.values()) {
|
|
User u = c.getUser();
|
|
|
|
if(u == null) {
|
|
continue;
|
|
}
|
|
|
|
Layer l = u.getLayer();
|
|
|
|
if(l == null) {
|
|
continue;
|
|
}
|
|
|
|
Set<Layer> us = selectedLayers.get(l);
|
|
|
|
if(us == null) {
|
|
us = new HashSet<Layer>();
|
|
selectedLayers.put(l, us);
|
|
}
|
|
|
|
us.add(u.getPhantomLayer());
|
|
}
|
|
|
|
/** Paint layers over it */
|
|
for(Layer l : canvas.getLayerMap().values()) {
|
|
Set<Layer> ls = selectedLayers.get(l);
|
|
if(l instanceof BitmapLayer && ls != null) {
|
|
((BitmapLayer) l).draw(g2, ls);
|
|
} else {
|
|
l.draw(g2);
|
|
}
|
|
}
|
|
|
|
g2.dispose();
|
|
|
|
if(mergeDirty == null) {
|
|
mergeDirty = new Rectangle(left, top, width, height);
|
|
} else {
|
|
mergeDirty.union(new Rectangle(left, top, width, height));
|
|
}
|
|
|
|
if(animationDOS != null) {
|
|
synchronized(animationDOS) {
|
|
try {
|
|
animationDOS.writeLong(System.currentTimeMillis());
|
|
animationDOS.writeInt(left);
|
|
animationDOS.writeInt(top);
|
|
animationDOS.writeInt(width);
|
|
animationDOS.writeInt(height);
|
|
for(int y = top; y < top + height; y++) {
|
|
for(int x = left; x < left + width; x++) {
|
|
animationDOS.writeInt(pixels[y * image.getWidth() + x]);
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
try { animationDOS.close(); } catch (IOException e1) { }
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void setAccessController(AccessController accessController) {
|
|
this.accessController = accessController;
|
|
}
|
|
|
|
public AccessController getAccessController() {
|
|
return accessController;
|
|
}
|
|
|
|
|
|
public void setAnimationFile(String name) throws IOException {
|
|
if(animationDOS != null) {
|
|
synchronized(animationDOS) {
|
|
animationDOS.close();
|
|
}
|
|
}
|
|
|
|
animationDOS = new DataOutputStream(new DeflaterOutputStream(new FileOutputStream(name, true)));
|
|
}
|
|
|
|
public void run() {
|
|
try {
|
|
while(!interrupted()) {
|
|
/** Conserve CPU time when idle */
|
|
sleep(20);
|
|
|
|
/** Run layer clean-up every 30 seconds */
|
|
if(lastLayerClean + 30000 < System.currentTimeMillis()) {
|
|
/** Cleanup now happens internally and separately on client and server */
|
|
for(Layer l : canvas.getLayerMap().values()) {
|
|
/** Clean up layer */
|
|
l.clean();
|
|
}
|
|
|
|
/** Time of last layer cleanup is now */
|
|
lastLayerClean = System.currentTimeMillis();
|
|
|
|
//Log.info("Cleaned layers.");
|
|
}
|
|
|
|
/** Accept and initialize client connections */
|
|
for(SocketChannel channel = serverChannel.accept(); channel != null; channel = serverChannel.accept()) {
|
|
channel.configureBlocking(false);
|
|
Connection connection = new Connection(channel);
|
|
Integer connectionKey = connectionMap.put(connection);
|
|
Log.debug("Accepting connection " + connectionKey + " from " + channel.socket().getRemoteSocketAddress());
|
|
}
|
|
|
|
/** Broadcast any injected command */
|
|
if(injectCommand != null) {
|
|
for(Connection c : connectionMap.values()) {
|
|
c.getSendQueue().offer(injectCommand);
|
|
}
|
|
injectCommand = null;
|
|
}
|
|
|
|
/** Loop through all client connections */
|
|
for(Map.Entry<Integer, Connection> entry : connectionMap.entrySet()) {
|
|
/** Fetch basic information about each connection */
|
|
Integer connectionKey = entry.getKey();
|
|
Connection connection = entry.getValue();
|
|
SocketChannel channel = connection.getChannel();
|
|
|
|
/** All connection specific errors are trapped by this 'try' statement */
|
|
try {
|
|
if(!channel.isConnected() && !connection.hasTimeOfDeath()) {
|
|
/** Remove closed connection from map */
|
|
connectionMap.remove(connectionKey);
|
|
|
|
/** Remove voice chat client if any */
|
|
voiceServer.removeClient(connectionKey);
|
|
|
|
if(connection.getUser() != null) {
|
|
/** Connection has closed unceremoniously. Broadcast to other clients. */
|
|
CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was closed"));
|
|
for(Connection c : connectionMap.values()) {
|
|
c.getSendQueue().offer(commandEntry);
|
|
}
|
|
Log.debug("Connection " + connectionKey + " had no time of death, sent explicit sign-out.");
|
|
}
|
|
|
|
|
|
Log.debug("Connection " + connectionKey + " was closed.");
|
|
|
|
/** The loop must be broken here to avoid a ConcurrentModificationException */
|
|
break;
|
|
} else if(connection.isTimeOfDeath()) {
|
|
/** Client signed out earlier. Close the channel. */
|
|
try { channel.close(); } catch(IOException e) { }
|
|
|
|
/** Remove from connection map */
|
|
connectionMap.remove(connectionKey);
|
|
|
|
/** Remove voice chat client if any */
|
|
voiceServer.removeClient(connectionKey);
|
|
|
|
Log.debug("Connection " + connectionKey + " has reached time of death and was closed.");
|
|
|
|
/** The loop must be broken here to avoid a ConcurrentModificationException */
|
|
break;
|
|
} else if(!connection.hasTimeOfDeath() && System.currentTimeMillis() - connection.getLastPing() > Connection.MAX_PING) {
|
|
connection.setTimeOfDeath(System.currentTimeMillis() + 5000);
|
|
|
|
CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Ping timeout"));
|
|
for(Connection c : connectionMap.values()) {
|
|
c.getSendQueue().offer(commandEntry);
|
|
}
|
|
|
|
if(connection.getUser() != null) {
|
|
Log.info(connection.getUser().getName() + " has signed out (Ping timeout).");
|
|
}
|
|
}
|
|
|
|
/** Get buffers for networking */
|
|
ByteBuffer inputBuffer = connection.getInputBuffer();
|
|
ByteBuffer outputBuffer = connection.getOutputBuffer();
|
|
|
|
ArrayBlockingQueue<CommandEntry> sendQueue = connection.getSendQueue();
|
|
|
|
//Random sendRandom = connection.getSendRandom();
|
|
//Random recvRandom = connection.getRecvRandom();
|
|
|
|
//int startPosition = outputBuffer.position();
|
|
|
|
/** Only check send queue if there is room for a minimal command in the output buffer. */
|
|
while(outputBuffer.remaining() >= 2 + 4 + 1 + 2 && sendQueue.size() > 0) {
|
|
/** Peek at the head of the queue */
|
|
CommandEntry e = sendQueue.peek();
|
|
|
|
/** Get command entry fields */
|
|
Integer k = e.getSourceKey();
|
|
Command c = e.getCommand();
|
|
|
|
/** Clear command buffer */
|
|
commandBuffer.clear();
|
|
|
|
/** Write peer key */
|
|
commandBuffer.putInt(k);
|
|
|
|
/** Write command name */
|
|
String commandName = c.getClass().getSimpleName();
|
|
commandBuffer.put((byte) (commandName.length() - 1));
|
|
for(int i = 0; i < commandName.length(); i++) {
|
|
commandBuffer.putChar(commandName.charAt(i));
|
|
}
|
|
|
|
/** Encode and write command data */
|
|
c.encode(commandBuffer);
|
|
|
|
/** Flip command buffer before reading */
|
|
commandBuffer.flip();
|
|
|
|
/** Discard command if there is no room for it in the output buffer */
|
|
if(commandBuffer.remaining() + 2 > outputBuffer.remaining()) {
|
|
break;
|
|
}
|
|
|
|
/** Append command length and command data to output buffer */
|
|
outputBuffer.putShort((short) (commandBuffer.remaining() - 1));
|
|
outputBuffer.put(commandBuffer);
|
|
|
|
/** Remove command from queue */
|
|
sendQueue.remove();
|
|
}
|
|
|
|
//for(int i = startPosition; i < outputBuffer.position(); i++) {
|
|
// outputBuffer.put(i, (byte) (outputBuffer.get(i) ^ sendRandom.nextInt(256)));
|
|
//}
|
|
|
|
/** Flip output buffer */
|
|
outputBuffer.flip();
|
|
|
|
if(outputBuffer.remaining() > 0) {
|
|
try {
|
|
/** Flush buffer */
|
|
channel.write(outputBuffer);
|
|
} catch (IOException e) {
|
|
Log.info("Connection" + connectionKey + " was broken (" + e.getMessage() + "). Sending sign-out.");
|
|
try { channel.close(); } catch(IOException e2) { }
|
|
connectionMap.remove(connectionKey);
|
|
CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was broken: " + e.getMessage()));
|
|
for(Connection c : connectionMap.values()) {
|
|
c.getSendQueue().offer(commandEntry);
|
|
}
|
|
|
|
/** Remove voice chat client if any */
|
|
voiceServer.removeClient(connectionKey);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
/** Prepare output buffer for append */
|
|
outputBuffer.compact();
|
|
|
|
/** Mortal connections are mute and have no voice chat audio */
|
|
if(connection.hasTimeOfDeath()) {
|
|
continue;
|
|
}
|
|
|
|
/** Append socket data to input buffer */
|
|
//startPosition = inputBuffer.position();
|
|
int readResult = channel.read(inputBuffer);
|
|
if(readResult == -1) {
|
|
/** Close the channel */
|
|
try { channel.close(); } catch(IOException e) { }
|
|
|
|
/** Remove from connection map */
|
|
connectionMap.remove(connectionKey);
|
|
|
|
/** Broadcast sign-out to other clients */
|
|
CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was closed"));
|
|
for(Connection c : connectionMap.values()) {
|
|
c.getSendQueue().offer(commandEntry);
|
|
}
|
|
|
|
/** Remove voice chat client if any */
|
|
voiceServer.removeClient(connectionKey);
|
|
|
|
Log.info("Connection " + connectionKey + " reached end of stream. Sending sign-out.");
|
|
|
|
/** The loop must be broken here to avoid a ConcurrentModificationException */
|
|
break;
|
|
} else if(readResult > 0) {
|
|
//for(int i = startPosition; i < inputBuffer.position(); i++) {
|
|
// inputBuffer.put(i, (byte) (inputBuffer.get(i) ^ recvRandom.nextInt(256)));
|
|
//}
|
|
|
|
/** Prepare input buffer for reading */
|
|
inputBuffer.flip();
|
|
|
|
/** Only check input buffer if long enough to hold a minimal command. */
|
|
while(inputBuffer.remaining() >= 2 + 4 + 1 + 2) {
|
|
/** Peek at length */
|
|
int commandLength = (inputBuffer.getShort(inputBuffer.position()) & 0xFFFF) + 1;
|
|
|
|
/** Only read input buffer if long enough to read entire command. */
|
|
if(inputBuffer.remaining() < commandLength + 2) {
|
|
break;
|
|
}
|
|
|
|
/** We already have the command length */
|
|
inputBuffer.getShort();
|
|
|
|
/** Map data into command buffer */
|
|
ByteBuffer commandBuffer = inputBuffer.slice();
|
|
commandBuffer.limit(commandLength);
|
|
|
|
/** Skip command data in input buffer */
|
|
inputBuffer.position(inputBuffer.position() + commandLength);
|
|
|
|
/** Ignore peer key from client */
|
|
commandBuffer.getInt();
|
|
|
|
/** Read command name */
|
|
int commandNameLength = (commandBuffer.get() & 0xFF) + 1;
|
|
StringBuffer commandNameBuffer = new StringBuffer();
|
|
for(int i = 0; i < commandNameLength; i++) {
|
|
commandNameBuffer.append(commandBuffer.getChar());
|
|
}
|
|
String commandName = commandNameBuffer.toString();
|
|
|
|
/** Attempt to instanciate command */
|
|
Command command = (Command) StreamableUtils.create("com.jotuntech.sketcher.server.command." + commandName, commandBuffer);
|
|
|
|
/** Wrap it in a CommandEntry in case of a broadcast */
|
|
CommandEntry commandEntry = new CommandEntry(connectionKey, command);
|
|
|
|
/** Perform the command and check the result */
|
|
switch(command.perform(this, connection)) {
|
|
/** Command wishes to be broadcast to all connections */
|
|
case Connection.SEND_ALL:
|
|
for(Connection c : connectionMap.values()) {
|
|
c.getSendQueue().offer(commandEntry);
|
|
}
|
|
break;
|
|
|
|
/** Command wishes to be broadcast to all but its own connection */
|
|
case Connection.SEND_OTHERS:
|
|
for(Connection c : connectionMap.values()) {
|
|
if(c != connection) {
|
|
c.getSendQueue().offer(commandEntry);
|
|
}
|
|
}
|
|
break;
|
|
|
|
/** Command wishes to loop back to the client */
|
|
case Connection.SEND_SELF:
|
|
connection.getSendQueue().offer(commandEntry);
|
|
break;
|
|
|
|
/** Command wishes to be anonymous */
|
|
case Connection.SEND_NONE:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/** Prepare input buffer for appending */
|
|
inputBuffer.compact();
|
|
}
|
|
} catch (IOException e) {
|
|
/** Remove from connection map */
|
|
connectionMap.remove(connectionKey);
|
|
|
|
/** Close the channel */
|
|
try { channel.close(); } catch(IOException e2) { }
|
|
|
|
if(connection.getUser() != null) {
|
|
/** Broadcast sign-out to clients */
|
|
CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was broken: " + e.getMessage()));
|
|
for(Connection c : connectionMap.values()) {
|
|
c.getSendQueue().offer(commandEntry);
|
|
}
|
|
}
|
|
|
|
/** Remove voice chat client if any */
|
|
voiceServer.removeClient(connectionKey);
|
|
|
|
Log.info("Connection " + connectionKey + " was broken (" + e.getMessage() + "). Sending sign-out.");
|
|
|
|
/** The loop must be broken here to avoid a ConcurrentModificationException */
|
|
break;
|
|
} catch(Throwable t) {
|
|
/** We have crashed and must let the client know, so we let connection linger a bit */
|
|
connection.setTimeOfDeath(System.currentTimeMillis() + 5000);
|
|
|
|
if(connection.getUser() != null) {
|
|
/** Broadcast sign-out to clients */
|
|
CommandEntry commandEntry = new CommandEntry(connectionKey, new SignOutCommand("Connection was crashed: " + t.getClass().getSimpleName()));
|
|
for(Connection c : connectionMap.values()) {
|
|
c.getSendQueue().offer(commandEntry);
|
|
}
|
|
}
|
|
|
|
Log.error("Connection " + connectionKey + " crashed!");
|
|
Log.error(t);
|
|
|
|
/** Remove voice chat client if any */
|
|
voiceServer.removeClient(connectionKey);
|
|
|
|
/** The loop must be broken here to avoid a ConcurrentModificationException */
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
} catch(ClosedByInterruptException e) {
|
|
Log.info("Server interrupted.");
|
|
} catch(InterruptedException e) {
|
|
Log.info("Server interrupted.");
|
|
} catch(Throwable e) {
|
|
Log.error("Server crashed.");
|
|
Log.error(e);
|
|
}
|
|
|
|
/** Shut down the voice server */
|
|
voiceServer.interrupt();
|
|
|
|
/** Close all client connections */
|
|
for(Connection c : connectionMap.values()) {
|
|
try { c.getChannel().close(); } catch(IOException e) { }
|
|
}
|
|
|
|
/** Close server socket channel */
|
|
try { serverChannel.close(); } catch(IOException e) { }
|
|
|
|
/** Empty the user list */
|
|
users = new User[0];
|
|
|
|
/** Clear the connection map */
|
|
connectionMap.clear();
|
|
|
|
}
|
|
|
|
public VoiceServer getVoiceServer() {
|
|
return voiceServer;
|
|
}
|
|
|
|
public String getMOTD() {
|
|
return motd;
|
|
}
|
|
|
|
public void setMOTD(String motd) {
|
|
this.motd = motd;
|
|
}
|
|
|
|
public void setMergeDirty(Rectangle mergeDirty) {
|
|
this.mergeDirty = mergeDirty;
|
|
}
|
|
|
|
public Rectangle getMergeDirty() {
|
|
return mergeDirty;
|
|
}
|
|
} |