diff --git a/.idea/encodings.xml b/.idea/encodings.xml index c502d2d4d072..2281e7d3ce2d 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -29,5 +29,11 @@ + + + + + + \ No newline at end of file diff --git a/bom/pom.xml b/bom/pom.xml index 6192bd3f6889..6b8d1b3c6246 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -296,6 +296,11 @@ THE SOFTWARE. j-interop 2.0.8-kohsuke-1 + + org.kohsuke.metainf-services + metainf-services + 1.9 + org.kohsuke.stapler json-lib diff --git a/core/pom.xml b/core/pom.xml index 507b01b921da..fe1415c82182 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -328,6 +328,17 @@ THE SOFTWARE. org.jenkins-ci version-number + + org.jenkins-ci.main + websocket-spi + ${project.version} + + + javax.servlet + javax.servlet-api + + + org.jfree jfreechart @@ -375,10 +386,8 @@ THE SOFTWARE. j-interop - org.kohsuke.metainf-services metainf-services - 1.9 true diff --git a/core/src/main/java/jenkins/websocket/WebSocketSession.java b/core/src/main/java/jenkins/websocket/WebSocketSession.java index 341be5ea71c5..f40c083a4bc4 100644 --- a/core/src/main/java/jenkins/websocket/WebSocketSession.java +++ b/core/src/main/java/jenkins/websocket/WebSocketSession.java @@ -25,7 +25,6 @@ package jenkins.websocket; import java.io.IOException; -import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; @@ -60,48 +59,28 @@ public abstract class WebSocketSession { private static final Logger LOGGER = Logger.getLogger(WebSocketSession.class.getName()); - private Object session; - // https://www.eclipse.org/jetty/javadoc/9.4.24.v20191120/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpoint.html - private Object remoteEndpoint; + Provider.Handler handler; private ScheduledFuture pings; protected WebSocketSession() {} - Object onWebSocketSomething(Object proxy, Method method, Object[] args) throws Exception { - switch (method.getName()) { - case "onWebSocketConnect": - this.session = args[0]; - this.remoteEndpoint = session.getClass().getMethod("getRemote").invoke(args[0]); - if (PING_INTERVAL_SECONDS != 0) { - pings = Timer.get().scheduleAtFixedRate(() -> { - try { - remoteEndpoint.getClass().getMethod("sendPing", ByteBuffer.class).invoke(remoteEndpoint, ByteBuffer.wrap(new byte[0])); - } catch (Exception x) { - error(x); - pings.cancel(true); - } - }, PING_INTERVAL_SECONDS / 2, PING_INTERVAL_SECONDS, TimeUnit.SECONDS); - } - opened(); - return null; - case "onWebSocketClose": - if (pings != null) { - pings.cancel(true); - // alternately, check Session.isOpen each time - } - closed((Integer) args[0], (String) args[1]); - return null; - case "onWebSocketError": - error((Throwable) args[0]); - return null; - case "onWebSocketBinary": - binary((byte[]) args[0], (Integer) args[1], (Integer) args[2]); - return null; - case "onWebSocketText": - text((String) args[0]); - return null; - default: - throw new AssertionError(); + void startPings() { + if (PING_INTERVAL_SECONDS != 0) { + pings = Timer.get().scheduleAtFixedRate(() -> { + try { + handler.sendPing(ByteBuffer.wrap(new byte[0])); + } catch (Exception x) { + error(x); + pings.cancel(true); + } + }, PING_INTERVAL_SECONDS / 2, PING_INTERVAL_SECONDS, TimeUnit.SECONDS); + } + } + + void stopPings() { + if (pings != null) { + pings.cancel(true); + // alternately, check Session.isOpen each time } } @@ -123,38 +102,20 @@ protected void text(String message) throws IOException { LOGGER.warning("unexpected text frame"); } - @SuppressWarnings("unchecked") protected final Future sendBinary(ByteBuffer data) throws IOException { - try { - return (Future) remoteEndpoint.getClass().getMethod("sendBytesByFuture", ByteBuffer.class).invoke(remoteEndpoint, data); - } catch (Exception x) { - throw new IOException(x); - } + return handler.sendBinary(data); } protected final void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException { - try { - remoteEndpoint.getClass().getMethod("sendPartialBytes", ByteBuffer.class, boolean.class).invoke(remoteEndpoint, partialByte, isLast); - } catch (Exception x) { - throw new IOException(x); - } + handler.sendBinary(partialByte, isLast); } - @SuppressWarnings("unchecked") protected final Future sendText(String text) throws IOException { - try { - return (Future) remoteEndpoint.getClass().getMethod("sendStringByFuture", String.class).invoke(remoteEndpoint, text); - } catch (Exception x) { - throw new IOException(x); - } + return handler.sendText(text); } protected final void close() throws IOException { - try { - session.getClass().getMethod("close").invoke(session); - } catch (Exception x) { - throw new IOException(x); - } + handler.close(); } } diff --git a/core/src/main/java/jenkins/websocket/WebSockets.java b/core/src/main/java/jenkins/websocket/WebSockets.java index 91d80c8c5f93..aafabcd79c9d 100644 --- a/core/src/main/java/jenkins/websocket/WebSockets.java +++ b/core/src/main/java/jenkins/websocket/WebSockets.java @@ -24,99 +24,101 @@ package jenkins.websocket; -import hudson.Extension; -import hudson.ExtensionList; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; +import java.io.IOException; +import java.nio.channels.ClosedChannelException; +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.Beta; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; -import org.kohsuke.stapler.Stapler; /** * Support for serving WebSocket responses. * @since 2.216 */ @Restricted(Beta.class) -@Extension public class WebSockets { private static final Logger LOGGER = Logger.getLogger(WebSockets.class.getName()); - private static final String ATTR_SESSION = WebSockets.class.getName() + ".session"; + private static final Provider provider = findProvider(); + + private static Provider findProvider() { + Iterator it = ServiceLoader.load(Provider.class).iterator(); + while (it.hasNext()) { + try { + return it.next(); + } catch (ServiceConfigurationError x) { + LOGGER.log(Level.FINE, null, x); + } + } + return null; + } // TODO ability to handle subprotocols? public static HttpResponse upgrade(WebSocketSession session) { + if (provider == null) { + throw HttpResponses.notFound(); + } return (req, rsp, node) -> { try { - Object factory = ExtensionList.lookupSingleton(WebSockets.class).init(); - if (!((Boolean) webSocketServletFactoryClass.getMethod("isUpgradeRequest", HttpServletRequest.class, HttpServletResponse.class).invoke(factory, req, rsp))) { - throw HttpResponses.errorWithoutStack(HttpServletResponse.SC_BAD_REQUEST, "only WS connections accepted here"); - } - req.setAttribute(ATTR_SESSION, session); - if (!((Boolean) webSocketServletFactoryClass.getMethod("acceptWebSocket", HttpServletRequest.class, HttpServletResponse.class).invoke(factory, req, rsp))) { - throw HttpResponses.errorWithoutStack(HttpServletResponse.SC_BAD_REQUEST, "did not manage to upgrade"); - } - } catch (HttpResponses.HttpResponseException x) { - throw x; + session.handler = provider.handle(req, rsp, new Provider.Listener() { + @Override + public void onWebSocketConnect() { + session.startPings(); + session.opened(); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + session.stopPings(); + session.closed(statusCode, reason); + } + + @Override + public void onWebSocketError(Throwable cause) { + if (cause instanceof ClosedChannelException) { + onWebSocketClose(0, cause.toString()); + } else { + session.error(cause); + } + } + + @Override + public void onWebSocketBinary(byte[] payload, int offset, int length) { + try { + session.binary(payload, offset, length); + } catch (IOException x) { + session.error(x); + } + } + + @Override + public void onWebSocketText(String message) { + try { + session.text(message); + } catch (IOException x) { + session.error(x); + } + } + }); } catch (Exception x) { LOGGER.log(Level.WARNING, null, x); throw HttpResponses.error(x); } - // OK! + // OK, unless handler is null in which case we expect an error was already sent. }; } - private static ClassLoader cl; - private static Class webSocketServletFactoryClass; - - private static synchronized void staticInit() throws Exception { - if (webSocketServletFactoryClass == null) { - cl = ServletContext.class.getClassLoader(); - webSocketServletFactoryClass = cl.loadClass("org.eclipse.jetty.websocket.servlet.WebSocketServletFactory"); - } - } - public static boolean isSupported() { - try { - staticInit(); - return true; - } catch (Exception x) { - LOGGER.log(Level.FINE, null, x); - return false; - } + return provider != null; } - private /*WebSocketServletFactory*/Object factory; - - private synchronized Object init() throws Exception { - if (factory == null) { - staticInit(); - Class webSocketPolicyClass = cl.loadClass("org.eclipse.jetty.websocket.api.WebSocketPolicy"); - factory = cl.loadClass("org.eclipse.jetty.websocket.servlet.WebSocketServletFactory$Loader") - .getMethod("load", ServletContext.class, webSocketPolicyClass) - .invoke( - null, - Stapler.getCurrent().getServletContext(), - webSocketPolicyClass.getMethod("newServerPolicy").invoke(null)); - webSocketServletFactoryClass.getMethod("start").invoke(factory); - Class webSocketCreatorClass = cl.loadClass("org.eclipse.jetty.websocket.servlet.WebSocketCreator"); - webSocketServletFactoryClass.getMethod("setCreator", webSocketCreatorClass).invoke(factory, Proxy.newProxyInstance(cl, new Class[] {webSocketCreatorClass}, this::createWebSocket)); - } - return factory; - } - - private Object createWebSocket(Object proxy, Method method, Object[] args) throws Exception { - Object servletUpgradeRequest = args[0]; - WebSocketSession session = (WebSocketSession) servletUpgradeRequest.getClass().getMethod("getServletAttribute", String.class).invoke(servletUpgradeRequest, ATTR_SESSION); - return Proxy.newProxyInstance(cl, new Class[] {cl.loadClass("org.eclipse.jetty.websocket.api.WebSocketListener")}, session::onWebSocketSomething); - } + private WebSockets() {} } diff --git a/pom.xml b/pom.xml index 4679531fe3d9..ee1dafc4a6de 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,8 @@ THE SOFTWARE. bom + websocket/spi + websocket/jetty9 core war test @@ -94,7 +96,7 @@ THE SOFTWARE. Max Medium - ${project.basedir}/../src/spotbugs/spotbugs-excludes.xml + ${maven.multiModuleProjectDirectory}/src/spotbugs/spotbugs-excludes.xml 1.27 1.23 diff --git a/src/spotbugs/spotbugs-excludes.xml b/src/spotbugs/spotbugs-excludes.xml index 96dd3781fb30..50f646752f88 100644 --- a/src/spotbugs/spotbugs-excludes.xml +++ b/src/spotbugs/spotbugs-excludes.xml @@ -658,6 +658,8 @@ + + diff --git a/test/src/test/java/hudson/slaves/JNLPLauncherRealTest.java b/test/src/test/java/hudson/slaves/JNLPLauncherRealTest.java index cbaf383b7db2..ac31919de8aa 100644 --- a/test/src/test/java/hudson/slaves/JNLPLauncherRealTest.java +++ b/test/src/test/java/hudson/slaves/JNLPLauncherRealTest.java @@ -29,11 +29,21 @@ import hudson.ExtensionList; import hudson.PluginWrapper; +import hudson.Proc; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.Slave; import hudson.util.FormValidation; +import io.jenkins.lib.support_log_formatter.SupportLogFormatter; +import java.io.File; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.agents.WebSocketAgentsTest; import jenkins.slaves.JnlpSlaveAgentProtocol4; +import org.apache.commons.io.FileUtils; +import org.apache.tools.ant.util.JavaEnvUtils; import org.junit.Rule; import org.junit.Test; import org.junit.runner.Description; @@ -75,4 +85,48 @@ private static void _smokes(JenkinsRule r) throws Throwable { } + /** + * Simplified version of {@link WebSocketAgentsTest#smokes} just checking Jetty/Winstone. + */ + @Issue("JENKINS-68933") + @Test public void webSocket() throws Throwable { + rr.then(JNLPLauncherRealTest::_webSocket); + } + + private static void _webSocket(JenkinsRule r) throws Throwable { + // TODO RealJenkinsRule does not yet support LoggerRule + Handler handler = new ConsoleHandler(); + handler.setFormatter(new SupportLogFormatter()); + handler.setLevel(Level.FINE); + Logger logger = Logger.getLogger("jenkins.websocket"); + logger.setLevel(Level.FINE); + logger.addHandler(handler); + assertThat(ExtensionList.lookupSingleton(JNLPLauncher.DescriptorImpl.class).doCheckWebSocket(true, null).kind, is(FormValidation.Kind.OK)); + // TODO InboundAgentRule does not yet support WebSocket + JNLPLauncher launcher = new JNLPLauncher(true); + launcher.setWebSocket(true); + DumbSlave s = new DumbSlave("remote", new File(r.jenkins.root, "agent").getAbsolutePath(), launcher); + r.jenkins.addNode(s); + String secret = ((SlaveComputer) s.toComputer()).getJnlpMac(); + File slaveJar = new File(r.jenkins.root, "agent.jar"); + FileUtils.copyURLToFile(new Slave.JnlpJar("agent.jar").getURL(), slaveJar); + Proc proc = r.createLocalLauncher().launch().cmds( + JavaEnvUtils.getJreExecutable("java"), "-jar", slaveJar.getAbsolutePath(), + "-jnlpUrl", r.getURL() + "computer/remote/jenkins-agent.jnlp", + "-secret", secret + ).stdout(System.out).start(); + try { + FreeStyleProject p = r.createFreeStyleProject(); + p.setAssignedNode(s); + r.buildAndAssertSuccess(p); + assertThat(s.toComputer().getSystemProperties().get("java.class.path"), is(slaveJar.getAbsolutePath())); + } finally { + proc.kill(); + while (r.jenkins.getComputer("remote").isOnline()) { + System.err.println("waiting for computer to go offline"); + Thread.sleep(250); + } + } + } + } diff --git a/war/pom.xml b/war/pom.xml index 4031c7977a02..1db05dc7b875 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -96,6 +96,11 @@ THE SOFTWARE. + + org.jenkins-ci.main + websocket-jetty9 + ${project.version} + + + 4.0.0 + + + org.jenkins-ci.main + jenkins-parent + ${revision}${changelist} + ../.. + + + websocket-jetty9 + Jetty 9 implementation for WebSocket + An implementation of the WebSocket handler that works with Jetty 9. + + + + + org.jenkins-ci.main + jenkins-bom + ${project.version} + pom + import + + + + + + + org.jenkins-ci + winstone + 5.25 + true + + + org.jenkins-ci.main + websocket-spi + ${project.version} + + + org.kohsuke + access-modifier-annotation + + + org.kohsuke.metainf-services + metainf-services + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + + + + + diff --git a/websocket/jetty9/src/main/java/jenkins/websocket/Jetty9Provider.java b/websocket/jetty9/src/main/java/jenkins/websocket/Jetty9Provider.java new file mode 100644 index 000000000000..ed5156980f7b --- /dev/null +++ b/websocket/jetty9/src/main/java/jenkins/websocket/Jetty9Provider.java @@ -0,0 +1,149 @@ +/* + * The MIT License + * + * Copyright 2022 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.Future; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketListener; +import org.eclipse.jetty.websocket.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.kohsuke.MetaInfServices; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +@MetaInfServices(Provider.class) +public class Jetty9Provider implements Provider { + + private static final String ATTR_LISTENER = Jetty9Provider.class.getName() + ".listener"; + + // TODO does not seem possible to use HttpServletRequest.get/setAttribute for this + private static final Map sessions = new WeakHashMap<>(); + + private WebSocketServletFactory factory; + + public Jetty9Provider() { + WebSocketServletFactory.class.hashCode(); + } + + private synchronized void init(HttpServletRequest req) throws Exception { + if (factory == null) { + factory = WebSocketServletFactory.Loader.load(req.getServletContext(), WebSocketPolicy.newServerPolicy()); + factory.start(); + factory.setCreator(Jetty9Provider::createWebSocket); + } + } + + @Override + public Handler handle(HttpServletRequest req, HttpServletResponse rsp, Listener listener) throws Exception { + init(req); + req.setAttribute(ATTR_LISTENER, listener); + if (!factory.isUpgradeRequest(req, rsp)) { + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "only WS connections accepted here"); + return null; + } + if (!factory.acceptWebSocket(req, rsp)) { + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "did not manage to upgrade"); + return null; + } + return new Handler() { + @Override + public Future sendBinary(ByteBuffer data) throws IOException { + return session().getRemote().sendBytesByFuture(data); + } + + @Override + public void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException { + session().getRemote().sendPartialBytes(partialByte, isLast); + } + + @Override + public Future sendText(String text) throws IOException { + return session().getRemote().sendStringByFuture(text); + } + + @Override + public void sendPing(ByteBuffer applicationData) throws IOException { + session().getRemote().sendPing(applicationData); + } + + @Override + public void close() throws IOException { + session().close(); + } + + private Session session() { + Session session = sessions.get(listener); + if (session == null) { + throw new IllegalStateException("missing session"); + } + return session; + } + }; + } + + private static Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) { + Listener listener = (Listener) req.getHttpServletRequest().getAttribute(ATTR_LISTENER); + if (listener == null) { + throw new IllegalStateException("missing listener attribute"); + } + return new WebSocketListener() { + @Override + public void onWebSocketBinary(byte[] payload, int offset, int length) { + listener.onWebSocketBinary(payload, offset, length); + } + + @Override + public void onWebSocketText(String message) { + listener.onWebSocketText(message); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + listener.onWebSocketClose(statusCode, reason); + } + + @Override + public void onWebSocketConnect(Session session) { + sessions.put(listener, session); + listener.onWebSocketConnect(); + } + + @Override + public void onWebSocketError(Throwable cause) { + listener.onWebSocketError(cause); + } + }; + } + +} diff --git a/websocket/spi/pom.xml b/websocket/spi/pom.xml new file mode 100644 index 000000000000..90ec18754ae9 --- /dev/null +++ b/websocket/spi/pom.xml @@ -0,0 +1,70 @@ + + + + 4.0.0 + + + org.jenkins-ci.main + jenkins-parent + ${revision}${changelist} + ../.. + + + websocket-spi + Internal SPI for WebSocket + An abstraction of how Jenkins can serve a WebSocket connection from its servlet container. + + + + + org.jenkins-ci.main + jenkins-bom + ${project.version} + pom + import + + + + + + + javax.servlet + javax.servlet-api + 3.1.0 + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + + + + + diff --git a/websocket/spi/src/main/java/jenkins/websocket/Provider.java b/websocket/spi/src/main/java/jenkins/websocket/Provider.java new file mode 100644 index 000000000000..caef2559dfb3 --- /dev/null +++ b/websocket/spi/src/main/java/jenkins/websocket/Provider.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * + * Copyright 2022 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.Future; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Defines a way for Jenkins core to serve WebSocket connections. + * This permits core to use one or more implementations supplied by the servlet container, + * based on static compilation while not having a hard dependency on any one. + * ({@code javax.websocket.*} APIs are not suited to usage from Stapler.) + * The constructor should try to link against everything necessary so any errors are thrown up front. + */ +interface Provider { + + /** + * Handle a WebSocket server request. + * @return a handler, unless an error code was already set + */ + Handler handle(HttpServletRequest req, HttpServletResponse rsp, Listener listener) throws Exception; + + interface Listener { + + void onWebSocketConnect(); + + void onWebSocketClose(int statusCode, String reason); + + void onWebSocketError(Throwable cause); + + void onWebSocketBinary(byte[] payload, int offset, int length); + + void onWebSocketText(String message); + + } + + interface Handler { + + Future sendBinary(ByteBuffer data) throws IOException; + + void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException; + + Future sendText(String text) throws IOException; + + void sendPing(ByteBuffer applicationData) throws IOException; + + void close() throws IOException; + + } + +}