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/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>
|
|
@ -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>
|
||||
|
|
13
core/pom.xml
13
core/pom.xml
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {}
|
||||
|
||||
}
|
||||
|
|
4
pom.xml
4
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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