mirror of https://github.com/jenkinsci/jenkins.git
[JENKINS-68933] Better WebSocket testing, removal of reflection (#6780)
This commit is contained in:
parent
d9d951be4b
commit
6de288f424
|
@ -29,5 +29,11 @@
|
||||||
<file url="file://$PROJECT_DIR$/war/src/main/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/war/src/main/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/war/src/main/resources" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/war/src/main/resources" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/war/src/test/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/war/src/test/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/websocket/jetty9/src/filter/resources" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/websocket/jetty9/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/websocket/jetty9/src/main/resources" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/websocket/spi/src/filter/resources" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/websocket/spi/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/websocket/spi/src/main/resources" charset="UTF-8" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -296,6 +296,11 @@ THE SOFTWARE.
|
||||||
<artifactId>j-interop</artifactId>
|
<artifactId>j-interop</artifactId>
|
||||||
<version>2.0.8-kohsuke-1</version>
|
<version>2.0.8-kohsuke-1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.kohsuke.metainf-services</groupId>
|
||||||
|
<artifactId>metainf-services</artifactId>
|
||||||
|
<version>1.9</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.kohsuke.stapler</groupId>
|
<groupId>org.kohsuke.stapler</groupId>
|
||||||
<artifactId>json-lib</artifactId>
|
<artifactId>json-lib</artifactId>
|
||||||
|
|
13
core/pom.xml
13
core/pom.xml
|
@ -328,6 +328,17 @@ THE SOFTWARE.
|
||||||
<groupId>org.jenkins-ci</groupId>
|
<groupId>org.jenkins-ci</groupId>
|
||||||
<artifactId>version-number</artifactId>
|
<artifactId>version-number</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jenkins-ci.main</groupId>
|
||||||
|
<artifactId>websocket-spi</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jfree</groupId>
|
<groupId>org.jfree</groupId>
|
||||||
<artifactId>jfreechart</artifactId>
|
<artifactId>jfreechart</artifactId>
|
||||||
|
@ -375,10 +386,8 @@ THE SOFTWARE.
|
||||||
<artifactId>j-interop</artifactId>
|
<artifactId>j-interop</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<!-- not in BOM, optional -->
|
|
||||||
<groupId>org.kohsuke.metainf-services</groupId>
|
<groupId>org.kohsuke.metainf-services</groupId>
|
||||||
<artifactId>metainf-services</artifactId>
|
<artifactId>metainf-services</artifactId>
|
||||||
<version>1.9</version>
|
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
package jenkins.websocket;
|
package jenkins.websocket;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
@ -60,48 +59,28 @@ public abstract class WebSocketSession {
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(WebSocketSession.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(WebSocketSession.class.getName());
|
||||||
|
|
||||||
private Object session;
|
Provider.Handler handler;
|
||||||
// https://www.eclipse.org/jetty/javadoc/9.4.24.v20191120/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpoint.html
|
|
||||||
private Object remoteEndpoint;
|
|
||||||
private ScheduledFuture<?> pings;
|
private ScheduledFuture<?> pings;
|
||||||
|
|
||||||
protected WebSocketSession() {}
|
protected WebSocketSession() {}
|
||||||
|
|
||||||
Object onWebSocketSomething(Object proxy, Method method, Object[] args) throws Exception {
|
void startPings() {
|
||||||
switch (method.getName()) {
|
if (PING_INTERVAL_SECONDS != 0) {
|
||||||
case "onWebSocketConnect":
|
pings = Timer.get().scheduleAtFixedRate(() -> {
|
||||||
this.session = args[0];
|
try {
|
||||||
this.remoteEndpoint = session.getClass().getMethod("getRemote").invoke(args[0]);
|
handler.sendPing(ByteBuffer.wrap(new byte[0]));
|
||||||
if (PING_INTERVAL_SECONDS != 0) {
|
} catch (Exception x) {
|
||||||
pings = Timer.get().scheduleAtFixedRate(() -> {
|
error(x);
|
||||||
try {
|
pings.cancel(true);
|
||||||
remoteEndpoint.getClass().getMethod("sendPing", ByteBuffer.class).invoke(remoteEndpoint, ByteBuffer.wrap(new byte[0]));
|
}
|
||||||
} catch (Exception x) {
|
}, PING_INTERVAL_SECONDS / 2, PING_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||||
error(x);
|
}
|
||||||
pings.cancel(true);
|
}
|
||||||
}
|
|
||||||
}, PING_INTERVAL_SECONDS / 2, PING_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
void stopPings() {
|
||||||
}
|
if (pings != null) {
|
||||||
opened();
|
pings.cancel(true);
|
||||||
return null;
|
// alternately, check Session.isOpen each time
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,38 +102,20 @@ public abstract class WebSocketSession {
|
||||||
LOGGER.warning("unexpected text frame");
|
LOGGER.warning("unexpected text frame");
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected final Future<Void> sendBinary(ByteBuffer data) throws IOException {
|
protected final Future<Void> sendBinary(ByteBuffer data) throws IOException {
|
||||||
try {
|
return handler.sendBinary(data);
|
||||||
return (Future<Void>) remoteEndpoint.getClass().getMethod("sendBytesByFuture", ByteBuffer.class).invoke(remoteEndpoint, data);
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new IOException(x);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException {
|
protected final void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException {
|
||||||
try {
|
handler.sendBinary(partialByte, isLast);
|
||||||
remoteEndpoint.getClass().getMethod("sendPartialBytes", ByteBuffer.class, boolean.class).invoke(remoteEndpoint, partialByte, isLast);
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new IOException(x);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected final Future<Void> sendText(String text) throws IOException {
|
protected final Future<Void> sendText(String text) throws IOException {
|
||||||
try {
|
return handler.sendText(text);
|
||||||
return (Future<Void>) remoteEndpoint.getClass().getMethod("sendStringByFuture", String.class).invoke(remoteEndpoint, text);
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new IOException(x);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final void close() throws IOException {
|
protected final void close() throws IOException {
|
||||||
try {
|
handler.close();
|
||||||
session.getClass().getMethod("close").invoke(session);
|
|
||||||
} catch (Exception x) {
|
|
||||||
throw new IOException(x);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,99 +24,101 @@
|
||||||
|
|
||||||
package jenkins.websocket;
|
package jenkins.websocket;
|
||||||
|
|
||||||
import hudson.Extension;
|
import java.io.IOException;
|
||||||
import hudson.ExtensionList;
|
import java.nio.channels.ClosedChannelException;
|
||||||
import java.lang.reflect.Method;
|
import java.util.Iterator;
|
||||||
import java.lang.reflect.Proxy;
|
import java.util.ServiceConfigurationError;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
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.Restricted;
|
||||||
import org.kohsuke.accmod.restrictions.Beta;
|
import org.kohsuke.accmod.restrictions.Beta;
|
||||||
import org.kohsuke.stapler.HttpResponse;
|
import org.kohsuke.stapler.HttpResponse;
|
||||||
import org.kohsuke.stapler.HttpResponses;
|
import org.kohsuke.stapler.HttpResponses;
|
||||||
import org.kohsuke.stapler.Stapler;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Support for serving WebSocket responses.
|
* Support for serving WebSocket responses.
|
||||||
* @since 2.216
|
* @since 2.216
|
||||||
*/
|
*/
|
||||||
@Restricted(Beta.class)
|
@Restricted(Beta.class)
|
||||||
@Extension
|
|
||||||
public class WebSockets {
|
public class WebSockets {
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(WebSockets.class.getName());
|
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<Provider> 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?
|
// TODO ability to handle subprotocols?
|
||||||
|
|
||||||
public static HttpResponse upgrade(WebSocketSession session) {
|
public static HttpResponse upgrade(WebSocketSession session) {
|
||||||
|
if (provider == null) {
|
||||||
|
throw HttpResponses.notFound();
|
||||||
|
}
|
||||||
return (req, rsp, node) -> {
|
return (req, rsp, node) -> {
|
||||||
try {
|
try {
|
||||||
Object factory = ExtensionList.lookupSingleton(WebSockets.class).init();
|
session.handler = provider.handle(req, rsp, new Provider.Listener() {
|
||||||
if (!((Boolean) webSocketServletFactoryClass.getMethod("isUpgradeRequest", HttpServletRequest.class, HttpServletResponse.class).invoke(factory, req, rsp))) {
|
@Override
|
||||||
throw HttpResponses.errorWithoutStack(HttpServletResponse.SC_BAD_REQUEST, "only WS connections accepted here");
|
public void onWebSocketConnect() {
|
||||||
}
|
session.startPings();
|
||||||
req.setAttribute(ATTR_SESSION, session);
|
session.opened();
|
||||||
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");
|
|
||||||
}
|
@Override
|
||||||
} catch (HttpResponses.HttpResponseException x) {
|
public void onWebSocketClose(int statusCode, String reason) {
|
||||||
throw x;
|
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) {
|
} catch (Exception x) {
|
||||||
LOGGER.log(Level.WARNING, null, x);
|
LOGGER.log(Level.WARNING, null, x);
|
||||||
throw HttpResponses.error(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() {
|
public static boolean isSupported() {
|
||||||
try {
|
return provider != null;
|
||||||
staticInit();
|
|
||||||
return true;
|
|
||||||
} catch (Exception x) {
|
|
||||||
LOGGER.log(Level.FINE, null, x);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private /*WebSocketServletFactory*/Object factory;
|
private WebSockets() {}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
4
pom.xml
4
pom.xml
|
@ -51,6 +51,8 @@ THE SOFTWARE.
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>bom</module>
|
<module>bom</module>
|
||||||
|
<module>websocket/spi</module>
|
||||||
|
<module>websocket/jetty9</module>
|
||||||
<module>core</module>
|
<module>core</module>
|
||||||
<module>war</module>
|
<module>war</module>
|
||||||
<module>test</module>
|
<module>test</module>
|
||||||
|
@ -94,7 +96,7 @@ THE SOFTWARE.
|
||||||
|
|
||||||
<spotbugs.effort>Max</spotbugs.effort>
|
<spotbugs.effort>Max</spotbugs.effort>
|
||||||
<spotbugs.threshold>Medium</spotbugs.threshold>
|
<spotbugs.threshold>Medium</spotbugs.threshold>
|
||||||
<spotbugs.excludeFilterFile>${project.basedir}/../src/spotbugs/spotbugs-excludes.xml</spotbugs.excludeFilterFile>
|
<spotbugs.excludeFilterFile>${maven.multiModuleProjectDirectory}/src/spotbugs/spotbugs-excludes.xml</spotbugs.excludeFilterFile>
|
||||||
|
|
||||||
<access-modifier.version>1.27</access-modifier.version>
|
<access-modifier.version>1.27</access-modifier.version>
|
||||||
<bridge-method-injector.version>1.23</bridge-method-injector.version>
|
<bridge-method-injector.version>1.23</bridge-method-injector.version>
|
||||||
|
|
|
@ -658,6 +658,8 @@
|
||||||
<Class name="jenkins.slaves.restarter.WinswSlaveRestarter"/>
|
<Class name="jenkins.slaves.restarter.WinswSlaveRestarter"/>
|
||||||
<Class name="jenkins.slaves.StandardOutputSwapper$ChannelSwapper"/>
|
<Class name="jenkins.slaves.StandardOutputSwapper$ChannelSwapper"/>
|
||||||
<Class name="jenkins.util.ProgressiveRendering"/>
|
<Class name="jenkins.util.ProgressiveRendering"/>
|
||||||
|
<Class name="jenkins.websocket.Jetty9Provider"/>
|
||||||
|
<Class name="jenkins.websocket.Provider"/>
|
||||||
<Class name="jenkins.websocket.WebSockets"/>
|
<Class name="jenkins.websocket.WebSockets"/>
|
||||||
<Class name="jenkins.websocket.WebSocketSession"/>
|
<Class name="jenkins.websocket.WebSocketSession"/>
|
||||||
<Class name="jenkins.widgets.RunListProgressiveRendering"/>
|
<Class name="jenkins.widgets.RunListProgressiveRendering"/>
|
||||||
|
|
|
@ -29,11 +29,21 @@ import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
import hudson.ExtensionList;
|
import hudson.ExtensionList;
|
||||||
import hudson.PluginWrapper;
|
import hudson.PluginWrapper;
|
||||||
|
import hudson.Proc;
|
||||||
import hudson.model.FreeStyleBuild;
|
import hudson.model.FreeStyleBuild;
|
||||||
import hudson.model.FreeStyleProject;
|
import hudson.model.FreeStyleProject;
|
||||||
import hudson.model.Slave;
|
import hudson.model.Slave;
|
||||||
import hudson.util.FormValidation;
|
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 jenkins.slaves.JnlpSlaveAgentProtocol4;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.tools.ant.util.JavaEnvUtils;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.Description;
|
import org.junit.runner.Description;
|
||||||
|
@ -75,4 +85,48 @@ public class JNLPLauncherRealTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,11 @@ THE SOFTWARE.
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jenkins-ci.main</groupId>
|
||||||
|
<artifactId>websocket-jetty9</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<!--
|
<!--
|
||||||
We bundle slf4j binding since we got some components (sshd for example)
|
We bundle slf4j binding since we got some components (sshd for example)
|
||||||
|
@ -174,6 +179,8 @@ THE SOFTWARE.
|
||||||
<excludes>
|
<excludes>
|
||||||
<exclude>org.jenkins-ci.main:cli</exclude>
|
<exclude>org.jenkins-ci.main:cli</exclude>
|
||||||
<exclude>org.jenkins-ci.main:jenkins-core</exclude>
|
<exclude>org.jenkins-ci.main:jenkins-core</exclude>
|
||||||
|
<exclude>org.jenkins-ci.main:websocket-jetty9</exclude>
|
||||||
|
<exclude>org.jenkins-ci.main:websocket-spi</exclude>
|
||||||
</excludes>
|
</excludes>
|
||||||
</enforceBytecodeVersion>
|
</enforceBytecodeVersion>
|
||||||
</rules>
|
</rules>
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019, 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.
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.jenkins-ci.main</groupId>
|
||||||
|
<artifactId>jenkins-parent</artifactId>
|
||||||
|
<version>${revision}${changelist}</version>
|
||||||
|
<relativePath>../..</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>websocket-jetty9</artifactId>
|
||||||
|
<name>Jetty 9 implementation for WebSocket</name>
|
||||||
|
<description>An implementation of the WebSocket handler that works with Jetty 9.</description>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jenkins-ci.main</groupId>
|
||||||
|
<artifactId>jenkins-bom</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jenkins-ci</groupId>
|
||||||
|
<artifactId>winstone</artifactId>
|
||||||
|
<version>5.25</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jenkins-ci.main</groupId>
|
||||||
|
<artifactId>websocket-spi</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.kohsuke</groupId>
|
||||||
|
<artifactId>access-modifier-annotation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.kohsuke.metainf-services</groupId>
|
||||||
|
<artifactId>metainf-services</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
|
@ -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<Listener, Session> 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<Void> 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<Void> 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019, 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.
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.jenkins-ci.main</groupId>
|
||||||
|
<artifactId>jenkins-parent</artifactId>
|
||||||
|
<version>${revision}${changelist}</version>
|
||||||
|
<relativePath>../..</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>websocket-spi</artifactId>
|
||||||
|
<name>Internal SPI for WebSocket</name>
|
||||||
|
<description>An abstraction of how Jenkins can serve a WebSocket connection from its servlet container.</description>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jenkins-ci.main</groupId>
|
||||||
|
<artifactId>jenkins-bom</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
|
@ -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<Void> sendBinary(ByteBuffer data) throws IOException;
|
||||||
|
|
||||||
|
void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException;
|
||||||
|
|
||||||
|
Future<Void> sendText(String text) throws IOException;
|
||||||
|
|
||||||
|
void sendPing(ByteBuffer applicationData) throws IOException;
|
||||||
|
|
||||||
|
void close() throws IOException;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue