args, Locale locale, InputStream stdin, PrintStream
if (!(this instanceof HelpCommand || this instanceof WhoAmICommand))
Jenkins.get().checkPermission(Jenkins.READ);
p.parseArgument(args.toArray(new String[0]));
- if (!(this instanceof HelpCommand || this instanceof WhoAmICommand))
- Jenkins.get().checkPermission(Jenkins.READ);
LOGGER.log(Level.FINE, "Invoking CLI command {0}, with {1} arguments, as user {2}.",
new Object[] {getName(), args.size(), auth.getName()});
int res = run();
diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java
index 29856dbcfa98..75f88ea92eed 100644
--- a/core/src/main/java/hudson/model/Slave.java
+++ b/core/src/main/java/hudson/model/Slave.java
@@ -407,23 +407,19 @@ public URL getURL() throws IOException {
if (!ALLOWED_JNLPJARS_FILES.contains(name)) {
throw new MalformedURLException("The specified file path " + fileName + " is not allowed due to security reasons");
}
-
+
+ Class> owner = null;
if (name.equals("hudson-cli.jar") || name.equals("jenkins-cli.jar")) {
- File cliJar = Which.jarFile(CLI.class);
- if (cliJar.isFile()) {
- name = "jenkins-cli.jar";
- } else {
- URL res = findExecutableJar(cliJar, CLI.class);
- if (res != null) {
- return res;
- }
- }
+ owner = CLI.class;
} else if (name.equals("agent.jar") || name.equals("slave.jar") || name.equals("remoting.jar")) {
- File remotingJar = Which.jarFile(hudson.remoting.Launcher.class);
- if (remotingJar.isFile()) {
- name = "lib/" + remotingJar.getName();
+ owner = hudson.remoting.Launcher.class;
+ }
+ if (owner != null) {
+ File jar = Which.jarFile(owner);
+ if (jar.isFile()) {
+ name = "lib/" + jar.getName();
} else {
- URL res = findExecutableJar(remotingJar, hudson.remoting.Launcher.class);
+ URL res = findExecutableJar(jar, owner);
if (res != null) {
return res;
}
diff --git a/core/src/main/java/hudson/slaves/JNLPLauncher.java b/core/src/main/java/hudson/slaves/JNLPLauncher.java
index 86f38d6b948d..db006ce8bd75 100644
--- a/core/src/main/java/hudson/slaves/JNLPLauncher.java
+++ b/core/src/main/java/hudson/slaves/JNLPLauncher.java
@@ -27,19 +27,21 @@
import hudson.Util;
import hudson.model.Computer;
import hudson.model.Descriptor;
-import hudson.model.DescriptorVisibilityFilter;
import hudson.model.TaskListener;
+import hudson.util.FormValidation;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import jenkins.slaves.RemotingWorkDirSettings;
import jenkins.util.java.JavaUtils;
+import jenkins.websocket.WebSockets;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
+import org.kohsuke.stapler.QueryParameter;
/**
* {@link ComputerLauncher} via inbound connections.
@@ -72,6 +74,8 @@ public class JNLPLauncher extends ComputerLauncher {
@Nonnull
private RemotingWorkDirSettings workDirSettings = RemotingWorkDirSettings.getEnabledDefaults();
+ private boolean webSocket;
+
/**
* Constructor.
* @param tunnel Tunnel settings
@@ -143,6 +147,21 @@ public boolean isLaunchSupported() {
return false;
}
+ /**
+ * @since TODO
+ */
+ public boolean isWebSocket() {
+ return webSocket;
+ }
+
+ /**
+ * @since TODO
+ */
+ @DataBoundSetter
+ public void setWebSocket(boolean webSocket) {
+ this.webSocket = webSocket;
+ }
+
@Override
public void launch(SlaveComputer computer, TaskListener listener) {
// do nothing as we cannot self start
@@ -196,25 +215,23 @@ public boolean isWorkDirSupported() {
// Causes JENKINS-45895 in the case of includes otherwise
return DescriptorImpl.class.equals(getClass());
}
- }
-
- /**
- * Hides the JNLP launcher when the JNLP agent port is not enabled.
- *
- * @since 2.16
- */
- @Extension
- public static class DescriptorVisibilityFilterImpl extends DescriptorVisibilityFilter {
- @Override
- public boolean filter(@CheckForNull Object context, @Nonnull Descriptor descriptor) {
- return descriptor.clazz != JNLPLauncher.class || Jenkins.get().getTcpSlaveAgentListener() != null;
+ public FormValidation doCheckWebSocket(@QueryParameter boolean webSocket, @QueryParameter String tunnel) {
+ if (webSocket) {
+ if (!WebSockets.isSupported()) {
+ return FormValidation.error("WebSocket support is not enabled in this Jenkins installation");
+ }
+ if (Util.fixEmptyAndTrim(tunnel) != null) {
+ return FormValidation.error("Tunneling is not supported in WebSocket mode");
+ }
+ } else {
+ if (Jenkins.get().getTcpSlaveAgentListener() == null) {
+ return FormValidation.error("Either WebSocket mode is selected, or the TCP port for inbound agents must be enabled");
+ }
+ }
+ return FormValidation.ok();
}
- @Override
- public boolean filterType(@Nonnull Class> contextClass, @Nonnull Descriptor descriptor) {
- return descriptor.clazz != JNLPLauncher.class || Jenkins.get().getTcpSlaveAgentListener() != null;
- }
}
/**
diff --git a/core/src/main/java/hudson/triggers/SafeTimerTask.java b/core/src/main/java/hudson/triggers/SafeTimerTask.java
index 47654b2550db..73409e4ae444 100644
--- a/core/src/main/java/hudson/triggers/SafeTimerTask.java
+++ b/core/src/main/java/hudson/triggers/SafeTimerTask.java
@@ -50,6 +50,27 @@
*/
public abstract class SafeTimerTask extends TimerTask {
+ /**
+ * Lambda-friendly means of creating a task.
+ * @since TODO
+ */
+ public static SafeTimerTask of(ExceptionRunnable r) {
+ return new SafeTimerTask() {
+ @Override
+ protected void doRun() throws Exception {
+ r.run();
+ }
+ };
+ }
+ /**
+ * @see #of
+ * @since TODO
+ */
+ @FunctionalInterface
+ public interface ExceptionRunnable {
+ void run() throws Exception;
+ }
+
/**
* System property to change the location where (tasks) logging should be sent.
* Beware: changing it while Jenkins is running gives no guarantee logs will be sent to the new location
diff --git a/core/src/main/java/jenkins/agents/WebSocketAgents.java b/core/src/main/java/jenkins/agents/WebSocketAgents.java
new file mode 100644
index 000000000000..965c3b67eb8e
--- /dev/null
+++ b/core/src/main/java/jenkins/agents/WebSocketAgents.java
@@ -0,0 +1,191 @@
+/*
+ * The MIT License
+ *
+ * Copyright 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.
+ */
+
+package jenkins.agents;
+
+import com.google.common.collect.ImmutableMap;
+import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.model.Computer;
+import hudson.model.InvisibleAction;
+import hudson.model.UnprotectedRootAction;
+import hudson.remoting.AbstractByteArrayCommandTransport;
+import hudson.remoting.Capability;
+import hudson.remoting.Channel;
+import hudson.remoting.ChannelBuilder;
+import hudson.remoting.Engine;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.slaves.JnlpAgentReceiver;
+import jenkins.slaves.RemotingVersionInfo;
+import jenkins.websocket.WebSocketSession;
+import jenkins.websocket.WebSockets;
+import org.jenkinsci.remoting.engine.JnlpConnectionState;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.HttpResponse;
+import org.kohsuke.stapler.HttpResponses;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+
+@Extension
+@Restricted(NoExternalUse.class)
+public final class WebSocketAgents extends InvisibleAction implements UnprotectedRootAction {
+
+ private static final Logger LOGGER = Logger.getLogger(WebSocketAgents.class.getName());
+
+ @Override
+ public String getUrlName() {
+ return WebSockets.isSupported() ? "wsagents" : null;
+ }
+
+ public HttpResponse doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException {
+ String agent = req.getHeader(JnlpConnectionState.CLIENT_NAME_KEY);
+ String secret = req.getHeader(JnlpConnectionState.SECRET_KEY);
+ String remoteCapabilityStr = req.getHeader(Capability.KEY);
+ if (agent == null || secret == null || remoteCapabilityStr == null) {
+ LOGGER.warning(() -> "incomplete headers: " + Collections.list(req.getHeaderNames()));
+ throw HttpResponses.errorWithoutStack(400, "This endpoint is only for use from agent.jar in WebSocket mode");
+ }
+ LOGGER.fine(() -> "receiving headers: " + Collections.list(req.getHeaderNames()));
+ if (!JnlpAgentReceiver.DATABASE.exists(agent)) {
+ LOGGER.warning(() -> "no such agent " + agent);
+ throw HttpResponses.errorWithoutStack(400, "no such agent");
+ }
+ if (!MessageDigest.isEqual(secret.getBytes(StandardCharsets.US_ASCII), JnlpAgentReceiver.DATABASE.getSecretOf(agent).getBytes(StandardCharsets.US_ASCII))) {
+ LOGGER.warning(() -> "incorrect secret for " + agent);
+ throw HttpResponses.forbidden();
+ }
+ JnlpConnectionState state = new JnlpConnectionState(null, ExtensionList.lookup(JnlpAgentReceiver.class));
+ state.setRemoteEndpointDescription(req.getRemoteAddr());
+ state.fireBeforeProperties();
+ LOGGER.fine(() -> "connecting " + agent);
+ state.fireAfterProperties(ImmutableMap.of(
+ // TODO or just pass all request headers?
+ JnlpConnectionState.CLIENT_NAME_KEY, agent,
+ JnlpConnectionState.SECRET_KEY, secret
+ ));
+ Capability remoteCapability = Capability.fromASCII(remoteCapabilityStr);
+ LOGGER.fine(() -> "received " + remoteCapability);
+ rsp.setHeader(Capability.KEY, new Capability().toASCII());
+ rsp.setHeader(Engine.REMOTING_MINIMUM_VERSION_HEADER, RemotingVersionInfo.getMinimumSupportedVersion().toString());
+ rsp.setHeader(JnlpConnectionState.COOKIE_KEY, JnlpAgentReceiver.generateCookie()); // TODO figure out what this is for, if anything
+ return WebSockets.upgrade(new Session(state, agent, remoteCapability));
+ }
+
+ private static class Session extends WebSocketSession {
+
+ private final JnlpConnectionState state;
+ private final String agent;
+ private final Capability remoteCapability;
+ private AbstractByteArrayCommandTransport.ByteArrayReceiver receiver;
+
+ Session(JnlpConnectionState state, String agent, Capability remoteCapability) {
+ this.state = state;
+ this.agent = agent;
+ this.remoteCapability = remoteCapability;
+ }
+
+ @Override
+ protected void opened() {
+ Computer.threadPoolForRemoting.submit(() -> {
+ LOGGER.fine(() -> "setting up channel for " + agent);
+ state.fireBeforeChannel(new ChannelBuilder(agent, Computer.threadPoolForRemoting));
+ state.fireAfterChannel(state.getChannelBuilder().build(new Transport()));
+ LOGGER.fine(() -> "set up channel for " + agent);
+ return null;
+ });
+ }
+
+ @Override
+ protected void binary(byte[] payload, int offset, int len) {
+ LOGGER.finest(() -> "reading block of length " + len + " from " + agent);
+ if (offset == 0 && len == payload.length) {
+ receiver.handle(payload);
+ } else {
+ receiver.handle(Arrays.copyOfRange(payload, offset, offset + len));
+ }
+ }
+
+ @Override
+ protected void closed(int statusCode, String reason) {
+ LOGGER.finest(() -> "closed " + statusCode + " " + reason);
+ IOException x = new ClosedChannelException();
+ receiver.terminate(x);
+ state.fireChannelClosed(x);
+ state.fireAfterDisconnect();
+ }
+
+ @Override
+ protected void error(Throwable cause) {
+ LOGGER.log(Level.WARNING, null, cause);
+ }
+
+ class Transport extends AbstractByteArrayCommandTransport {
+
+ @Override
+ public void setup(AbstractByteArrayCommandTransport.ByteArrayReceiver bar) {
+ receiver = bar;
+ }
+
+ @Override
+ public void writeBlock(Channel chnl, byte[] bytes) throws IOException {
+ LOGGER.finest(() -> "writing block of length " + bytes.length + " to " + agent);
+ try {
+ sendBinary(ByteBuffer.wrap(bytes)).get();
+ } catch (Exception x) {
+ x.printStackTrace();
+ throw new IOException(x);
+ }
+ }
+
+ @Override
+ public Capability getRemoteCapability() throws IOException {
+ return remoteCapability;
+ }
+
+ @Override
+ public void closeWrite() throws IOException {
+ LOGGER.finest(() -> "closeWrite");
+ close();
+ }
+
+ @Override
+ public void closeRead() throws IOException {
+ LOGGER.finest(() -> "closeRead");
+ close();
+ }
+
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java b/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java
index 140f466a726d..62cebc92d650 100644
--- a/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java
+++ b/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java
@@ -113,7 +113,7 @@ public void afterProperties(@Nonnull JnlpConnectionState event) {
+ "Set system property "
+ "jenkins.slaves.DefaultJnlpSlaveReceiver.disableStrictVerification=true to allow"
+ "connections until the plugin has been fixed.",
- new Object[]{clientName, event.getSocket().getRemoteSocketAddress(), computer.getLauncher().getClass()});
+ new Object[]{clientName, event.getRemoteEndpointDescription(), computer.getLauncher().getClass()});
event.reject(new ConnectionRefusalException(String.format("%s is not an inbound agent", clientName)));
return;
}
@@ -149,7 +149,7 @@ public void beforeChannel(@Nonnull JnlpConnectionState event) {
final OutputStream log = computer.openLogFile();
state.setLog(log);
PrintWriter logw = new PrintWriter(log, true);
- logw.println("Inbound agent connected from " + event.getSocket().getInetAddress());
+ logw.println("Inbound agent connected from " + event.getRemoteEndpointDescription());
for (ChannelConfigurator cc : ChannelConfigurator.all()) {
cc.onChannelBuilding(event.getChannelBuilder(), computer);
}
diff --git a/core/src/main/java/jenkins/slaves/JnlpAgentReceiver.java b/core/src/main/java/jenkins/slaves/JnlpAgentReceiver.java
index 5464056fdbbe..b738b3316a56 100644
--- a/core/src/main/java/jenkins/slaves/JnlpAgentReceiver.java
+++ b/core/src/main/java/jenkins/slaves/JnlpAgentReceiver.java
@@ -7,12 +7,13 @@
import java.security.SecureRandom;
import javax.annotation.Nonnull;
+import jenkins.agents.WebSocketAgents;
import jenkins.security.HMACConfidentialKey;
import org.jenkinsci.remoting.engine.JnlpClientDatabase;
import org.jenkinsci.remoting.engine.JnlpConnectionStateListener;
/**
- * Receives incoming agents connecting through {@link JnlpSlaveAgentProtocol4}.
+ * Receives incoming agents connecting through the likes of {@link JnlpSlaveAgentProtocol4} or {@link WebSocketAgents}.
*
*
* This is useful to establish the communication with other JVMs and use them
diff --git a/core/src/main/java/jenkins/websocket/WebSocketEcho.java b/core/src/main/java/jenkins/websocket/WebSocketEcho.java
new file mode 100644
index 000000000000..5aa8b432c2f8
--- /dev/null
+++ b/core/src/main/java/jenkins/websocket/WebSocketEcho.java
@@ -0,0 +1,67 @@
+/*
+ * The MIT License
+ *
+ * Copyright 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.
+ */
+
+package jenkins.websocket;
+
+import hudson.Extension;
+import hudson.model.InvisibleAction;
+import hudson.model.RootAction;
+import java.nio.ByteBuffer;
+import jenkins.model.Jenkins;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.HttpResponse;
+
+@Extension
+@Restricted(NoExternalUse.class)
+public class WebSocketEcho extends InvisibleAction implements RootAction {
+
+ @Override
+ public String getUrlName() {
+ return "wsecho";
+ }
+
+ public HttpResponse doIndex() {
+ Jenkins.get().checkPermission(Jenkins.ADMINISTER);
+ return WebSockets.upgrade(new WebSocketSession() {
+ @Override
+ protected void text(String message) {
+ sendText("hello " + message);
+ }
+ @Override
+ protected void binary(byte[] payload, int offset, int len) {
+ ByteBuffer data = ByteBuffer.allocate(len);
+ for (int i = 0; i < len; i++) {
+ byte b = payload[offset + i];
+ if (b >= 'a' && b <= 'z') {
+ b += 'A' - 'a';
+ }
+ data.put(i, b);
+ }
+ sendBinary(data);
+ }
+ });
+ }
+
+}
diff --git a/core/src/main/java/jenkins/websocket/WebSocketSession.java b/core/src/main/java/jenkins/websocket/WebSocketSession.java
new file mode 100644
index 000000000000..95a7ed76094c
--- /dev/null
+++ b/core/src/main/java/jenkins/websocket/WebSocketSession.java
@@ -0,0 +1,150 @@
+/*
+ * The MIT License
+ *
+ * Copyright 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.
+ */
+
+package jenkins.websocket;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.util.SystemProperties;
+import jenkins.util.Timer;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.Beta;
+
+/**
+ * One WebSocket connection.
+ * @see WebSockets
+ * @since TODO
+ */
+@Restricted(Beta.class)
+public abstract class WebSocketSession {
+
+ /**
+ * Number of seconds between server-sent pings.
+ * Zero to disable.
+ *
nginx docs claim 60s timeout and this seems to match experiments.
+ * GKE docs says 30s
+ * but this is a total timeout, not inactivity, so you need to set {@code BackendConfigSpec.timeoutSec} anyway.
+ *
This is set for the whole Jenkins session rather than a particular service,
+ * since it has more to do with the environment than anything else.
+ * Certain services may have their own “keep alive” semantics,
+ * but for example {@link hudson.remoting.PingThread} may be too infrequent.
+ */
+ private static long PING_INTERVAL_SECONDS = SystemProperties.getLong("jenkins.websocket.pingInterval", 30L);
+
+ private static final Logger LOGGER = Logger.getLogger(WebSocketSession.class.getName());
+
+ private Object session;
+ private Object remoteEndpoint;
+ private ScheduledFuture> pings;
+
+ protected WebSocketSession() {}
+
+ Object onWebSocketSomething(Object proxy, Method method, Object[] args) throws Exception {
+ switch (method.getName()) {
+ case "onWebSocketConnect":
+ this.session = args[0];
+ this.remoteEndpoint = session.getClass().getMethod("getRemote").invoke(args[0]);
+ if (PING_INTERVAL_SECONDS != 0) {
+ pings = Timer.get().scheduleAtFixedRate(() -> {
+ try {
+ remoteEndpoint.getClass().getMethod("sendPing", ByteBuffer.class).invoke(remoteEndpoint, ByteBuffer.wrap(new byte[0]));
+ } catch (Exception x) {
+ error(x);
+ pings.cancel(true);
+ }
+ }, PING_INTERVAL_SECONDS / 2, PING_INTERVAL_SECONDS, TimeUnit.SECONDS);
+ }
+ opened();
+ return null;
+ case "onWebSocketClose":
+ if (pings != null) {
+ pings.cancel(true);
+ // alternately, check Session.isOpen each time
+ }
+ closed((Integer) args[0], (String) args[1]);
+ return null;
+ case "onWebSocketError":
+ error((Throwable) args[0]);
+ return null;
+ case "onWebSocketBinary":
+ binary((byte[]) args[0], (Integer) args[1], (Integer) args[2]);
+ return null;
+ case "onWebSocketText":
+ text((String) args[0]);
+ return null;
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ protected void opened() {
+ }
+
+ protected void closed(int statusCode, String reason) {
+ }
+
+ protected void error(Throwable cause) {
+ LOGGER.log(Level.WARNING, "unhandled WebSocket service error", cause);
+ }
+
+ protected void binary(byte[] payload, int offset, int len) {
+ LOGGER.warning("unexpected binary frame");
+ }
+
+ protected void text(String message) {
+ LOGGER.warning("unexpected text frame");
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final Future sendBinary(ByteBuffer data) {
+ try {
+ return (Future) remoteEndpoint.getClass().getMethod("sendBytesByFuture", ByteBuffer.class).invoke(remoteEndpoint, data);
+ } catch (Exception x) {
+ throw new RuntimeException(x);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final Future sendText(String text) {
+ try {
+ return (Future) remoteEndpoint.getClass().getMethod("sendStringByFuture", String.class).invoke(remoteEndpoint, text);
+ } catch (Exception x) {
+ throw new RuntimeException(x);
+ }
+ }
+
+ protected final void close() {
+ try {
+ session.getClass().getMethod("close").invoke(session);
+ } catch (Exception x) {
+ throw new RuntimeException(x);
+ }
+ }
+
+}
diff --git a/core/src/main/java/jenkins/websocket/WebSockets.java b/core/src/main/java/jenkins/websocket/WebSockets.java
new file mode 100644
index 000000000000..2d856d5069cb
--- /dev/null
+++ b/core/src/main/java/jenkins/websocket/WebSockets.java
@@ -0,0 +1,117 @@
+/*
+ * The MIT License
+ *
+ * Copyright 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.
+ */
+
+package jenkins.websocket;
+
+import hudson.Extension;
+import hudson.ExtensionList;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+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 TODO
+ */
+@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";
+
+ // TODO ability to handle subprotocols?
+
+ public static HttpResponse upgrade(WebSocketSession session) {
+ return (req, rsp, node) -> {
+ try {
+ Object factory = ExtensionList.lookupSingleton(WebSockets.class).init();
+ if (!((Boolean) webSocketServletFactoryClass.getMethod("isUpgradeRequest", HttpServletRequest.class, HttpServletResponse.class).invoke(factory, req, rsp))) {
+ throw HttpResponses.errorWithoutStack(HttpServletResponse.SC_BAD_REQUEST, "only WS connections accepted here");
+ }
+ req.setAttribute(ATTR_SESSION, session);
+ if (!((Boolean) webSocketServletFactoryClass.getMethod("acceptWebSocket", HttpServletRequest.class, HttpServletResponse.class).invoke(factory, req, rsp))) {
+ throw HttpResponses.errorWithoutStack(HttpServletResponse.SC_BAD_REQUEST, "did not manage to upgrade");
+ }
+ } catch (HttpResponses.HttpResponseException x) {
+ throw x;
+ } catch (Exception x) {
+ LOGGER.log(Level.WARNING, null, x);
+ throw HttpResponses.error(x);
+ }
+ // OK!
+ };
+ }
+
+ 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;
+ }
+ }
+
+ 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);
+ }
+
+}
diff --git a/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly b/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly
index b837be4264e7..1b04d3c2c9ae 100644
--- a/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly
+++ b/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly
@@ -40,7 +40,8 @@ THE SOFTWARE.
-
+
+
Jenkins
diff --git a/core/src/main/resources/hudson/cli/CLIAction/example.jelly b/core/src/main/resources/hudson/cli/CLIAction/example.jelly
index a7e6e2cc10b8..5a4c4cef320f 100644
--- a/core/src/main/resources/hudson/cli/CLIAction/example.jelly
+++ b/core/src/main/resources/hudson/cli/CLIAction/example.jelly
@@ -34,6 +34,6 @@ THE SOFTWARE.
}
- java -jar jenkins-cli.jar -s ${h.inferHudsonURL(request)} ${commandArgs}
+ java -jar jenkins-cli.jar -s ${h.inferHudsonURL(request)} -webSocket ${commandArgs}
diff --git a/core/src/main/resources/hudson/slaves/JNLPLauncher/config.jelly b/core/src/main/resources/hudson/slaves/JNLPLauncher/config.jelly
index d0765ddb5b5e..6b33de6b8068 100644
--- a/core/src/main/resources/hudson/slaves/JNLPLauncher/config.jelly
+++ b/core/src/main/resources/hudson/slaves/JNLPLauncher/config.jelly
@@ -28,6 +28,9 @@ THE SOFTWARE.
+
+
+
diff --git a/core/src/main/resources/hudson/slaves/JNLPLauncher/help-webSocket.html b/core/src/main/resources/hudson/slaves/JNLPLauncher/help-webSocket.html
new file mode 100644
index 000000000000..90f5c28f9077
--- /dev/null
+++ b/core/src/main/resources/hudson/slaves/JNLPLauncher/help-webSocket.html
@@ -0,0 +1,4 @@
+
+ Use WebSocket to connect to the Jenkins master rather than the TCP port.
+ See
JEP-222 for background.
+
diff --git a/core/src/main/resources/hudson/slaves/JNLPLauncher/main.jelly b/core/src/main/resources/hudson/slaves/JNLPLauncher/main.jelly
index 134837393300..404e30b9f0f0 100644
--- a/core/src/main/resources/hudson/slaves/JNLPLauncher/main.jelly
+++ b/core/src/main/resources/hudson/slaves/JNLPLauncher/main.jelly
@@ -25,7 +25,7 @@ THE SOFTWARE.
-
+
${%slaveAgentPort.disabled}
${%configure.link.text}.
diff --git a/core/src/main/resources/hudson/slaves/SlaveComputer/slave-agent.jnlp.jelly b/core/src/main/resources/hudson/slaves/SlaveComputer/slave-agent.jnlp.jelly
index e07b78974141..173a897e9c2d 100644
--- a/core/src/main/resources/hudson/slaves/SlaveComputer/slave-agent.jnlp.jelly
+++ b/core/src/main/resources/hudson/slaves/SlaveComputer/slave-agent.jnlp.jelly
@@ -63,6 +63,9 @@ THE SOFTWARE.
${it.jnlpMac}
${it.node.nodeName}
+
+ -webSocket
+
-tunnel
${launcher.tunnel}
diff --git a/core/src/main/resources/hudson/slaves/SlaveComputer/systemInfo.jelly b/core/src/main/resources/hudson/slaves/SlaveComputer/systemInfo.jelly
index 279fc74807f6..8fa1b2ba2f29 100644
--- a/core/src/main/resources/hudson/slaves/SlaveComputer/systemInfo.jelly
+++ b/core/src/main/resources/hudson/slaves/SlaveComputer/systemInfo.jelly
@@ -45,7 +45,7 @@ THE SOFTWARE.
- ${it.oSDescription} slave, version ${it.slaveVersion}
+ ${it.oSDescription} agent, version ${it.slaveVersion}
${instance.displayName}
diff --git a/pom.xml b/pom.xml
index 392fbbee82dc..567a6e4fff63 100755
--- a/pom.xml
+++ b/pom.xml
@@ -101,7 +101,7 @@ THE SOFTWARE.
3.2.3
- 3.40
+ 4.0
3.14
diff --git a/test/pom.xml b/test/pom.xml
index a6a278179200..18326007370d 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -70,7 +70,7 @@ THE SOFTWARE.
${project.groupId}
jenkins-test-harness
- 2.58
+ 2.59
test
diff --git a/test/src/test/java/hudson/cli/CLIActionTest.java b/test/src/test/java/hudson/cli/CLIActionTest.java
index 4e7e2a7acc85..f3840459b815 100644
--- a/test/src/test/java/hudson/cli/CLIActionTest.java
+++ b/test/src/test/java/hudson/cli/CLIActionTest.java
@@ -20,7 +20,6 @@
import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.List;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import jenkins.model.Jenkins;
@@ -53,8 +52,6 @@ public class CLIActionTest {
@Rule
public LoggerRule logging = new LoggerRule();
- private ExecutorService pool;
-
@Test
@PresetData(DataSet.NO_ANONYMOUS_READACCESS)
@Issue("SECURITY-192")
@@ -87,7 +84,6 @@ public void authentication() throws Exception {
// @CLIMethod:
assertExitCode(6, false, jar, "disable-job", "p"); // AccessDeniedException from CLIRegisterer?
assertExitCode(0, true, jar, "disable-job", "p");
- // If we have anonymous read access, then the situation is simpler.
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to(ADMIN).grant(Jenkins.READ, Item.READ).everywhere().toEveryone());
assertExitCode(6, false, jar, "get-job", "p"); // AccessDeniedException from AbstractItem.writeConfigDotXml
assertExitCode(0, true, jar, "get-job", "p"); // works with API tokens
@@ -98,7 +94,7 @@ public void authentication() throws Exception {
private static final String ADMIN = "admin@mycorp.com";
private void assertExitCode(int code, boolean useApiToken, File jar, String... args) throws IOException, InterruptedException {
- List commands = Lists.newArrayList("java", "-jar", jar.getAbsolutePath(), "-s", j.getURL().toString(), /* not covering SSH keys in this test */ "-noKeyAuth");
+ List commands = Lists.newArrayList("java", "-jar", jar.getAbsolutePath(), "-s", j.getURL().toString(), /* TODO until it is the default */ "-webSocket");
if (useApiToken) {
commands.add("-auth");
commands.add(ADMIN + ":" + User.get(ADMIN).getProperty(ApiTokenProperty.class).getApiToken());
@@ -137,7 +133,8 @@ public void encodingAndLocale() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
assertEquals(0, new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(
"java", "-Dfile.encoding=ISO-8859-2", "-Duser.language=cs", "-Duser.country=CZ", "-jar", jar.getAbsolutePath(),
- "-s", j.getURL().toString()./* just checking */replaceFirst("/$", ""), "-noKeyAuth", "test-diagnostic").
+ "-webSocket", // TODO as above
+ "-s", j.getURL().toString()./* just checking */replaceFirst("/$", ""), "test-diagnostic").
stdout(baos).stderr(System.err).join());
assertEquals("encoding=ISO-8859-2 locale=cs_CZ", baos.toString().trim());
// TODO test that stdout/stderr are in expected encoding (not true of -remoting mode!)
@@ -155,7 +152,9 @@ public void interleavedStdio() throws Exception {
PipedOutputStream pos = new PipedOutputStream(pis);
PrintWriter pw = new PrintWriter(new TeeOutputStream(pos, System.err), true);
Proc proc = new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds(
- "java", "-jar", jar.getAbsolutePath(), "-s", j.getURL().toString(), "-noKeyAuth", "groovysh").
+ "java", "-jar", jar.getAbsolutePath(), "-s", j.getURL().toString(),
+ "-webSocket", // TODO as above
+ "groovysh").
stdout(new TeeOutputStream(baos, System.out)).stderr(System.err).stdin(pis).start();
while (!baos.toString().contains("000")) { // cannot just search for, say, "groovy:000> " since there are ANSI escapes there (cf. StringEscapeUtils.escapeJava)
Thread.sleep(100);
@@ -164,7 +163,6 @@ public void interleavedStdio() throws Exception {
while (!baos.toString().contains("121")) { // ditto not "===> 121"
Thread.sleep(100);
}
- Thread.sleep(31_000); // aggravate org.eclipse.jetty.io.IdleTimeout (cf. AbstractConnector._idleTimeout)
pw.println("11 * 11 * 11");
while (!baos.toString().contains("1331")) {
Thread.sleep(100);
diff --git a/test/src/test/java/hudson/cli/CLITest.java b/test/src/test/java/hudson/cli/CLITest.java
index 9f62ebb996ee..ca541bc95007 100644
--- a/test/src/test/java/hudson/cli/CLITest.java
+++ b/test/src/test/java/hudson/cli/CLITest.java
@@ -157,6 +157,7 @@ public void interrupt() throws Exception {
p.getBuildersList().add(new SleepBuilder(TimeUnit.MINUTES.toMillis(5)));
doInterrupt(p, "-ssh", "-user", "admin", "-i", privkey.getAbsolutePath());
doInterrupt(p, "-http", "-auth", "admin:admin");
+ doInterrupt(p, "-webSocket", "-auth", "admin:admin");
}
private void doInterrupt(FreeStyleProject p, String... modeArgs) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -191,6 +192,7 @@ public void reportNotJenkins() throws Exception {
assertThat(baos.toString(), containsString("There's no Jenkins running at"));
assertNotEquals(0, ret);
}
+ // TODO -webSocket currently produces a stack trace
}
@TestExtension("reportNotJenkins")
public static final class NoJenkinsAction extends CrumbExclusion implements UnprotectedRootAction, StaplerProxy {
@@ -244,7 +246,7 @@ public void redirectToEndpointShouldBeFollowed() throws Exception {
assertNull(rsp.getContentAsString(), rsp.getResponseHeaderValue("X-Jenkins-CLI-Port"));
assertNull(rsp.getContentAsString(), rsp.getResponseHeaderValue("X-SSH-Endpoint"));
- for (String transport: Arrays.asList("-http", "-ssh")) {
+ for (String transport: Arrays.asList("-http", "-ssh", "-webSocket")) {
String url = r.getURL().toString() + "cli-proxy/";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
diff --git a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java
index 31110c12adac..a60c4dc68786 100644
--- a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java
+++ b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java
@@ -56,9 +56,11 @@
import static org.junit.Assert.fail;
import java.awt.*;
+import java.util.logging.Level;
import static org.hamcrest.Matchers.instanceOf;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.LoggerRule;
import org.jvnet.hudson.test.recipes.LocalData;
/**
@@ -69,7 +71,9 @@
public class JNLPLauncherTest {
@Rule public JenkinsRule j = new JenkinsRule();
- @Rule public TemporaryFolder tmpDir = new TemporaryFolder();
+ @Rule public TemporaryFolder tmpDir = new TemporaryFolder();
+
+ @Rule public LoggerRule logging = new LoggerRule().record(Slave.class, Level.FINE);
/**
* Starts a JNLP agent and makes sure it successfully connects to Jenkins.
diff --git a/test/src/test/java/jenkins/agents/WebSocketAgentsTest.java b/test/src/test/java/jenkins/agents/WebSocketAgentsTest.java
new file mode 100644
index 000000000000..37c47dcfd388
--- /dev/null
+++ b/test/src/test/java/jenkins/agents/WebSocketAgentsTest.java
@@ -0,0 +1,125 @@
+/*
+ * The MIT License
+ *
+ * Copyright 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.
+ */
+
+package jenkins.agents;
+
+import hudson.Functions;
+import hudson.Proc;
+import hudson.model.FreeStyleProject;
+import hudson.model.Slave;
+import hudson.remoting.Engine;
+import hudson.slaves.DumbSlave;
+import hudson.slaves.JNLPLauncher;
+import hudson.slaves.SlaveComputer;
+import hudson.tasks.BatchFile;
+import hudson.tasks.Shell;
+import java.io.File;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.security.SlaveToMasterCallable;
+import org.apache.commons.io.FileUtils;
+import org.apache.tools.ant.util.JavaEnvUtils;
+import org.junit.ClassRule;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.jvnet.hudson.test.BuildWatcher;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.LoggerRule;
+
+@Issue("JEP-222")
+public class WebSocketAgentsTest {
+
+ private static final Logger LOGGER = Logger.getLogger(WebSocketAgentsTest.class.getName());
+
+ @ClassRule
+ public static BuildWatcher buildWatcher = new BuildWatcher();
+
+ @Rule
+ public JenkinsRule r = new JenkinsRule();
+
+ @Rule
+ public LoggerRule logging = new LoggerRule().
+ record(Slave.class, Level.FINE).
+ record(SlaveComputer.class, Level.FINEST).
+ record(WebSocketAgents.class, Level.FINEST).
+ record(Engine.class, Level.FINEST);
+
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ /**
+ * Verify basic functionality of an agent in {@code -webSocket} mode.
+ * Requires {@code remoting} to have been {@code mvn install}ed.
+ * Does not show {@code FINE} or lower agent logs ({@link JenkinsRule#showAgentLogs(Slave, LoggerRule)} cannot be used here).
+ * Unlike {@link hudson.slaves.JNLPLauncherTest} this does not use {@code javaws};
+ * closer to {@link hudson.bugs.JnlpAccessWithSecuredHudsonTest}.
+ * @see hudson.remoting.Launcher
+ */
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ @Test
+ public void smokes() throws Exception {
+ AtomicReference proc = new AtomicReference<>();
+ try {
+ JNLPLauncher launcher = new JNLPLauncher(true);
+ launcher.setWebSocket(true);
+ DumbSlave s = new DumbSlave("remote", tmp.newFolder("agent").getAbsolutePath(), launcher);
+ r.jenkins.addNode(s);
+ String secret = ((SlaveComputer) s.toComputer()).getJnlpMac();
+ File slaveJar = tmp.newFile();
+ FileUtils.copyURLToFile(new Slave.JnlpJar("slave.jar").getURL(), slaveJar);
+ proc.set(r.createLocalLauncher().launch().cmds(
+ JavaEnvUtils.getJreExecutable("java"), "-jar", slaveJar.getAbsolutePath(),
+ "-jnlpUrl", r.getURL() + "computer/remote/slave-agent.jnlp",
+ "-secret", secret
+ ).stdout(System.out).start());
+ r.waitOnline(s);
+ assertEquals("response", s.getChannel().call(new DummyTask()));
+ FreeStyleProject p = r.createFreeStyleProject();
+ p.setAssignedNode(s);
+ p.getBuildersList().add(Functions.isWindows() ? new BatchFile("echo hello") : new Shell("echo hello"));
+ r.buildAndAssertSuccess(p);
+ s.toComputer().getLogText().writeLogTo(0, System.out);
+ } finally {
+ if (proc.get() != null) {
+ proc.get().kill();
+ while (r.jenkins.getComputer("remote").isOnline()) {
+ LOGGER.info("waiting for computer to go offline");
+ Thread.sleep(250);
+ }
+ }
+ }
+ }
+
+ private static class DummyTask extends SlaveToMasterCallable {
+ @Override
+ public String call() {
+ return "response";
+ }
+ }
+
+}
diff --git a/war/pom.xml b/war/pom.xml
index 934be6cf4df2..da3705dd924c 100644
--- a/war/pom.xml
+++ b/war/pom.xml
@@ -91,9 +91,7 @@ THE SOFTWARE.
${project.groupId}
cli
- jar-with-dependencies
${project.version}
- provided
org.jenkins-ci
winstone
- 5.4
+ 5.6
test
@@ -262,13 +260,6 @@ THE SOFTWARE.
-
- ${project.groupId}
- cli
- jar-with-dependencies
- ${project.build.directory}/${project.build.finalName}/WEB-INF
- jenkins-cli.jar
-
org.jenkins-ci
winstone