GUACAMOLE-267: Implement FailoverGuacamoleSocket.

Project: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/commit/798f06ee
Tree: 
http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/798f06ee
Diff: 
http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/798f06ee

Branch: refs/heads/master
Commit: 798f06ee0a4e3e72ba303f970d9dded7aa5e29a8
Parents: 79c66b0
Author: Michael Jumper <[email protected]>
Authored: Tue Apr 4 12:28:05 2017 -0700
Committer: Michael Jumper <[email protected]>
Committed: Thu Apr 20 22:37:08 2017 -0700

----------------------------------------------------------------------
 .../protocol/FailoverGuacamoleSocket.java       | 347 +++++++++++++++++++
 1 file changed, 347 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/798f06ee/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
----------------------------------------------------------------------
diff --git 
a/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
 
b/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
new file mode 100644
index 0000000..fcef9a5
--- /dev/null
+++ 
b/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.protocol;
+
+import java.util.List;
+import org.apache.guacamole.GuacamoleConnectionClosedException;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.io.GuacamoleReader;
+import org.apache.guacamole.io.GuacamoleWriter;
+import org.apache.guacamole.net.GuacamoleSocket;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GuacamoleSocket implementation which transparently switches between an
+ * underlying queue of available sockets when errors are encountered during
+ * the initial part of a connection (prior to first "sync").
+ */
+public class FailoverGuacamoleSocket implements GuacamoleSocket {
+
+    /**
+     * Logger for this class.
+     */
+    private Logger logger = 
LoggerFactory.getLogger(FailoverGuacamoleSocket.class);
+
+    /**
+     * A queue of available sockets. The full set of available sockets need not
+     * be known ahead of time.
+     */
+    public static interface SocketQueue {
+
+        /**
+         * Returns the next available socket in the queue, or null if no
+         * further sockets are available. This function will be invoked when an
+         * error occurs within the current socket, and will be invoked again if
+         * a GuacamoleException is thrown. Such repeated calls to this function
+         * will occur until errors cease or null is returned.
+         *
+         * @return
+         *     The next available socket in the queue, or null if no further
+         *     sockets are available.
+         *
+         * @throws GuacamoleException
+         *     If an error occurs preventing the next available socket from
+         *     being used.
+         */
+        GuacamoleSocket nextSocket() throws GuacamoleException;
+
+    }
+
+    /**
+     * The queue of available sockets provided when this 
FailoverGuacamoleSocket
+     * was created.
+     */
+    private final SocketQueue sockets;
+
+    /**
+     * The current socket being used, or null if no socket is available.
+     */
+    private GuacamoleSocket socket;
+
+    /**
+     * Creates a new FailoverGuacamoleSocket which pulls its sockets from the
+     * given SocketQueue. If an error occurs during the Guacamole connection,
+     * other sockets from the SocketQueue are tried, in order, until no error
+     * occurs.
+     *
+     * @param sockets
+     *     A SocketQueue which returns the sockets which should be used, in
+     *     order.
+     *
+     * @throws GuacamoleException
+     *     If errors prevent use of the sockets defined by the SocketQueue, and
+     *     no further sockets remain to be tried.
+     */
+    public FailoverGuacamoleSocket(SocketQueue sockets)
+            throws GuacamoleException {
+        this.sockets = sockets;
+        selectNextSocket();
+    }
+
+    private GuacamoleException tryNextSocket() {
+
+        try {
+            if (socket != null)
+                socket.close();
+        }
+        catch (GuacamoleException e) {
+            if (socket.isOpen())
+                logger.debug("Previous failed socket could not be closed.", e);
+        }
+
+        try {
+
+            socket = sockets.nextSocket();
+            if (socket == null)
+                return new GuacamoleServerException("No remaining sockets to 
try.");
+
+        }
+
+        catch (GuacamoleException e) {
+            if (tryNextSocket() != null)
+                return e;
+        }
+
+        return null;
+
+    }
+
+    private void selectNextSocket() throws GuacamoleException {
+        synchronized (sockets) {
+            GuacamoleException failure = tryNextSocket();
+            if (failure != null)
+                throw failure;
+        }
+    }
+
+    private class ErrorFilter implements GuacamoleFilter {
+
+        /**
+         * Whether a "sync" instruction has been read.
+         */
+        private boolean receivedSync = false;
+
+        @Override
+        public GuacamoleInstruction filter(GuacamoleInstruction instruction)
+                throws GuacamoleException {
+
+            // Ignore instructions after first sync is received
+            if (receivedSync)
+                return instruction;
+
+            String opcode = instruction.getOpcode();
+
+            if (opcode.equals("sync")) {
+                receivedSync = true;
+                return instruction;
+            }
+
+            if (opcode.equals("error"))
+                return handleError(instruction);
+
+            return instruction;
+
+        }
+
+        private GuacamoleInstruction handleError(GuacamoleInstruction 
instruction) {
+
+            // Ignore error instructions which are missing the status code
+            List<String> args = instruction.getArgs();
+            if (args.size() < 2) {
+                logger.debug("Ignoring \"error\" instruction without status 
code.");
+                return instruction;
+            }
+
+            int statusCode;
+            try {
+                statusCode = Integer.parseInt(args.get(1));
+            }
+            catch (NumberFormatException e) {
+                logger.debug("Ignoring \"error\" instruction with non-numeric 
status code.", e);
+                return instruction;
+            }
+
+            // Invalid status code
+            GuacamoleStatus status = 
GuacamoleStatus.fromGuacamoleStatusCode(statusCode);
+            if (status == null) {
+                logger.debug("Ignoring \"error\" instruction with 
unknown/invalid status code: {}", statusCode);
+                return instruction;
+            }
+
+            // Only handle error instructions related to the upstream remote 
desktop
+            switch (status) {
+
+                // Transparently connect to a different connection if upstream 
fails
+                case UPSTREAM_ERROR:
+                case UPSTREAM_NOT_FOUND:
+                case UPSTREAM_TIMEOUT:
+                case UPSTREAM_UNAVAILABLE:
+                    break;
+
+                // Allow error through otherwise
+                default:
+                    return instruction;
+
+            }
+
+            logger.debug("Overriding {} \"error\" instruction. Failing over to 
next connection...", status);
+
+            // Advance through remaining sockets until another functional 
socket
+            // is retrieved, or no more sockets remain
+            try {
+                selectNextSocket();
+                return null;
+            }
+
+            catch (GuacamoleException e) {
+                logger.debug("No sockets remain to be tried - giving up on 
failover.");
+            }
+
+            // Allow error through if not intercepting
+            return instruction;
+
+        }
+
+    }
+
+    /**
+     * GuacamoleReader which filters "error" instructions, transparently 
failing
+     * over to the next socket in the queue if an error is encountered. Read
+     * attempts are delegated to the GuacamoleReader of the current socket.
+     */
+    private final GuacamoleReader reader = new FilteredGuacamoleReader(new 
GuacamoleReader() {
+
+        @Override
+        public boolean available() throws GuacamoleException {
+            synchronized (sockets) {
+
+                if (socket == null)
+                    return false;
+
+                return socket.getReader().available();
+
+            }
+        }
+
+        @Override
+        public char[] read() throws GuacamoleException {
+            synchronized (sockets) {
+
+                if (socket == null)
+                    return null;
+
+                return socket.getReader().read();
+
+            }
+        }
+
+        @Override
+        public GuacamoleInstruction readInstruction()
+                throws GuacamoleException {
+            synchronized (sockets) {
+
+                if (socket == null)
+                    return null;
+
+                return socket.getReader().readInstruction();
+
+            }
+        }
+
+    }, new ErrorFilter());
+
+    /**
+     * GuacamoleWriter which delegates all write attempts to the 
GuacamoleWriter
+     * of the current socket.
+     */
+    private final GuacamoleWriter writer = new GuacamoleWriter() {
+
+        @Override
+        public void write(char[] chunk, int off, int len)
+                throws GuacamoleException {
+            synchronized (sockets) {
+
+                if (socket == null)
+                    throw new GuacamoleConnectionClosedException("No further 
sockets remaining in SocketQueue.");
+
+                socket.getWriter().write(chunk, off, len);
+
+            }
+        }
+
+        @Override
+        public void write(char[] chunk) throws GuacamoleException {
+            synchronized (sockets) {
+
+                if (socket == null)
+                    throw new GuacamoleConnectionClosedException("No further 
sockets remaining in SocketQueue.");
+
+                socket.getWriter().write(chunk);
+
+            }
+        }
+
+        @Override
+        public void writeInstruction(GuacamoleInstruction instruction)
+                throws GuacamoleException {
+            synchronized (sockets) {
+
+                if (socket == null)
+                    throw new GuacamoleConnectionClosedException("No further 
sockets remaining in SocketQueue.");
+
+                socket.getWriter().writeInstruction(instruction);
+
+            }
+        }
+
+    };
+
+    @Override
+    public GuacamoleReader getReader() {
+        return reader;
+    }
+
+    @Override
+    public GuacamoleWriter getWriter() {
+        return writer;
+    }
+
+    @Override
+    public void close() throws GuacamoleException {
+        synchronized (sockets) {
+            socket.close();
+        }
+    }
+
+    @Override
+    public boolean isOpen() {
+        synchronized (sockets) {
+
+            if (socket == null)
+                return false;
+
+            return socket.isOpen();
+
+        }
+    }
+    
+}

Reply via email to