[JENKINS-68933] Better WebSocket testing, removal of reflection (#6780)

This commit is contained in:
Jesse Glick 2022-07-07 11:23:47 -04:00 committed by GitHub
parent d9d951be4b
commit 6de288f424
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 553 additions and 126 deletions

View File

@ -29,5 +29,11 @@
<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/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>
</project>

View File

@ -296,6 +296,11 @@ THE SOFTWARE.
<artifactId>j-interop</artifactId>
<version>2.0.8-kohsuke-1</version>
</dependency>
<dependency>
<groupId>org.kohsuke.metainf-services</groupId>
<artifactId>metainf-services</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>json-lib</artifactId>

View File

@ -328,6 +328,17 @@ THE SOFTWARE.
<groupId>org.jenkins-ci</groupId>
<artifactId>version-number</artifactId>
</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>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
@ -375,10 +386,8 @@ THE SOFTWARE.
<artifactId>j-interop</artifactId>
</dependency>
<dependency>
<!-- not in BOM, optional -->
<groupId>org.kohsuke.metainf-services</groupId>
<artifactId>metainf-services</artifactId>
<version>1.9</version>
<optional>true</optional>
</dependency>
<dependency>

View File

@ -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,49 +59,29 @@ 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]);
void startPings() {
if (PING_INTERVAL_SECONDS != 0) {
pings = Timer.get().scheduleAtFixedRate(() -> {
try {
remoteEndpoint.getClass().getMethod("sendPing", ByteBuffer.class).invoke(remoteEndpoint, ByteBuffer.wrap(new byte[0]));
handler.sendPing(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":
}
void stopPings() {
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();
}
}
protected void opened() {
@ -123,38 +102,20 @@ public abstract class WebSocketSession {
LOGGER.warning("unexpected text frame");
}
@SuppressWarnings("unchecked")
protected final Future<Void> sendBinary(ByteBuffer data) throws IOException {
try {
return (Future<Void>) 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<Void> sendText(String text) throws IOException {
try {
return (Future<Void>) 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();
}
}

View File

@ -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<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?
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");
session.handler = provider.handle(req, rsp, new Provider.Listener() {
@Override
public void onWebSocketConnect() {
session.startPings();
session.opened();
}
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");
@Override
public void onWebSocketClose(int statusCode, String reason) {
session.stopPings();
session.closed(statusCode, reason);
}
} catch (HttpResponses.HttpResponseException x) {
throw x;
@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() {}
}

View File

@ -51,6 +51,8 @@ THE SOFTWARE.
<modules>
<module>bom</module>
<module>websocket/spi</module>
<module>websocket/jetty9</module>
<module>core</module>
<module>war</module>
<module>test</module>
@ -94,7 +96,7 @@ THE SOFTWARE.
<spotbugs.effort>Max</spotbugs.effort>
<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>
<bridge-method-injector.version>1.23</bridge-method-injector.version>

View File

@ -658,6 +658,8 @@
<Class name="jenkins.slaves.restarter.WinswSlaveRestarter"/>
<Class name="jenkins.slaves.StandardOutputSwapper$ChannelSwapper"/>
<Class name="jenkins.util.ProgressiveRendering"/>
<Class name="jenkins.websocket.Jetty9Provider"/>
<Class name="jenkins.websocket.Provider"/>
<Class name="jenkins.websocket.WebSockets"/>
<Class name="jenkins.websocket.WebSocketSession"/>
<Class name="jenkins.widgets.RunListProgressiveRendering"/>

View File

@ -29,11 +29,21 @@ import static org.hamcrest.Matchers.is;
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 @@ 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);
}
}
}
}

View File

@ -96,6 +96,11 @@ THE SOFTWARE.
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>websocket-jetty9</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<!--
We bundle slf4j binding since we got some components (sshd for example)
@ -174,6 +179,8 @@ THE SOFTWARE.
<excludes>
<exclude>org.jenkins-ci.main:cli</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>
</enforceBytecodeVersion>
</rules>

84
websocket/jetty9/pom.xml Normal file
View File

@ -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>

View File

@ -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);
}
};
}
}

70
websocket/spi/pom.xml Normal file
View File

@ -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>

View File

@ -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;
}
}