diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c6cd2c431..f362e4cfb9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,8 @@ This page provides information about contributing code to the Jenkins core codeb If you hit a new issue, please report it with a `java11-devtools-compatibility` label in our issue tracker. * Maven 3.5.4 or above. You can [download maven]. * Any IDE which supports importing Maven projects. + * Install [NodeJS](https://nodejs.org/en/). **Note:** only needed to work on the frontend assets found on the `war` module. + * Frontend tasks are ran using [yarn](https://yarnpkg.com/lang/en/). Run `npm install -g yarn` to install it. 4. Setup your development environment as described in [Preparing for Plugin Development] If you want to contribute to Jenkins or just learn about the project, @@ -49,6 +51,20 @@ mvn -pl war jetty:run (Beware that `maven-plugin` builds will not work in this mode due to class loading conflicts.) +### Building frontend assets + +To work on the `war` module frontend assets two processes are needed at the same time: + +On one terminal, start a development server that will not process frontend assets: +```sh +mvn -pl war jetty:run -Dskip.yarn +``` + +On another terminal, move to the war folder and start a [webpack](https://webpack.js.org/) dev server: +```sh +cd war; yarn start +``` + ## Testing changes Jenkins core includes unit and functional tests as a part of the repository. @@ -67,6 +83,13 @@ In addition to the included tests, you can also find extra integration and UI tests in the [Acceptance Test Harness (ATH)] repository. If you propose complex UI changes, you should create new ATH tests for them. +### JavaScript unit tests + +In case there's only need to run the JS tests: +```sh +cd war; yarn test +``` + ## Proposing Changes The Jenkins project source code repositories are hosted at GitHub. diff --git a/cli/pom.xml b/cli/pom.xml index c7d2a78397..77375bbe7a 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -53,14 +53,17 @@ org.kohsuke access-modifier-annotation + provided org.jenkins-ci annotation-indexer + true commons-io commons-io + true junit @@ -71,32 +74,41 @@ org.jvnet.localizer localizer 1.26 + true org.apache.sshd sshd-core 1.7.0 - true + true + + + + net.i2p.crypto + eddsa + 0.3.0 + true - - - net.i2p.crypto - eddsa - 0.3.0 - org.slf4j slf4j-jdk14 - true + true + + + org.glassfish.tyrus.bundles + tyrus-standalone-client-jdk + 1.12 + true com.github.spotbugs spotbugs-annotations - true + provided commons-lang commons-lang + true @@ -107,29 +119,43 @@ 2.22.2 - maven-assembly-plugin - - - - - single - - package - - - jar-with-dependencies - - - - hudson.cli.CLI - - - ${project.version} - - - - - + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + false + + + javax.websocket + io.jenkins.cli.shaded.javax.websocket + + + org + io.jenkins.cli.shaded.org + + + net + io.jenkins.cli.shaded.net + + + + + hudson.cli.CLI + + ${project.version} + + + + + + + org.jvnet.localizer diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java index b66f092448..1b9b4856ad 100644 --- a/cli/src/main/java/hudson/cli/CLI.java +++ b/cli/src/main/java/hudson/cli/CLI.java @@ -24,6 +24,7 @@ package hudson.cli; import hudson.cli.client.Messages; +import java.io.DataInputStream; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; @@ -33,22 +34,33 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URI; import java.net.URL; import java.net.URLConnection; +import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.SecureRandom; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Properties; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; import static java.util.logging.Level.*; +import javax.websocket.ClientEndpointConfig; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.Session; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; +import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.client.ClientProperties; +import org.glassfish.tyrus.container.jdk.client.JdkClientContainer; /** * CLI entry point to Jenkins. @@ -91,7 +103,7 @@ public class CLI { } } - private enum Mode {HTTP, SSH} + private enum Mode {HTTP, SSH, WEB_SOCKET} public static int _main(String[] _args) throws Exception { List args = Arrays.asList(_args); PrivateKeyProvider provider = new PrivateKeyProvider(); @@ -101,8 +113,9 @@ public class CLI { if (url==null) url = System.getenv("HUDSON_URL"); - boolean tryLoadPKey = true; + boolean noKeyAuth = false; + // TODO perhaps allow mode to be defined by environment variable too (assuming $JENKINS_USER_ID can be used for -user) Mode mode = null; String user = null; @@ -137,6 +150,15 @@ public class CLI { args = args.subList(1, args.size()); continue; } + if (head.equals("-webSocket")) { + if (mode != null) { + printUsage("-webSocket clashes with previously defined mode " + mode); + return -1; + } + mode = Mode.WEB_SOCKET; + args = args.subList(1, args.size()); + continue; + } if (head.equals("-remoting")) { printUsage("-remoting mode is no longer supported"); return -1; @@ -161,7 +183,7 @@ public class CLI { continue; } if (head.equals("-noKeyAuth")) { - tryLoadPKey = false; + noKeyAuth = true; args = args.subList(1,args.size()); continue; } @@ -229,9 +251,6 @@ public class CLI { if(args.isEmpty()) args = Arrays.asList("help"); // default to help - if (tryLoadPKey && !provider.hasKeys()) - provider.readFromDefaultLocations(); - if (mode == null) { mode = Mode.HTTP; } @@ -248,6 +267,9 @@ public class CLI { LOGGER.warning("-user required when using -ssh"); return -1; } + if (!noKeyAuth && !provider.hasKeys()) { + provider.readFromDefaultLocations(); + } return SSHCLI.sshConnection(url, user, args, provider, strictHostKey); } @@ -255,6 +277,10 @@ public class CLI { LOGGER.warning("-strictHostKey meaningful only with -ssh"); } + if (noKeyAuth) { + LOGGER.warning("-noKeyAuth meaningful only with -ssh"); + } + if (user != null) { LOGGER.warning("Warning: -user ignored unless using -ssh"); } @@ -271,66 +297,63 @@ public class CLI { return plainHttpConnection(url, args, factory); } + if (mode == Mode.WEB_SOCKET) { + return webSocketConnection(url, args, factory); + } + throw new AssertionError(); } + private static int webSocketConnection(String url, List args, CLIConnectionFactory factory) throws Exception { + LOGGER.fine(() -> "Trying to connect to " + url + " via plain protocol over WebSocket"); + class CLIEndpoint extends Endpoint { + @Override + public void onOpen(Session session, EndpointConfig config) {} + } + class Authenticator extends ClientEndpointConfig.Configurator { + @Override + public void beforeRequest(Map> headers) { + if (factory.authorization != null) { + headers.put("Authorization", Collections.singletonList(factory.authorization)); + } + } + } + ClientManager client = ClientManager.createClient(JdkClientContainer.class.getName()); // ~ ContainerProvider.getWebSocketContainer() + client.getProperties().put(ClientProperties.REDIRECT_ENABLED, true); // https://tyrus-project.github.io/documentation/1.13.1/index/tyrus-proprietary-config.html#d0e1775 + Session session = client.connectToServer(new CLIEndpoint(), ClientEndpointConfig.Builder.create().configurator(new Authenticator()).build(), URI.create(url.replaceFirst("^http", "ws") + "cli/ws")); + PlainCLIProtocol.Output out = new PlainCLIProtocol.Output() { + @Override + public void send(byte[] data) throws IOException { + session.getBasicRemote().sendBinary(ByteBuffer.wrap(data)); + } + @Override + public void close() throws IOException { + session.close(); + } + }; + try (ClientSideImpl connection = new ClientSideImpl(out)) { + session.addMessageHandler(InputStream.class, is -> { + try { + connection.handle(new DataInputStream(is)); + } catch (IOException x) { + LOGGER.log(Level.WARNING, null, x); + } + }); + connection.start(args); + return connection.exit(); + } + } + private static int plainHttpConnection(String url, List args, CLIConnectionFactory factory) throws IOException, InterruptedException { LOGGER.log(FINE, "Trying to connect to {0} via plain protocol over HTTP", url); FullDuplexHttpStream streams = new FullDuplexHttpStream(new URL(url), "cli?remoting=false", factory.authorization); - class ClientSideImpl extends PlainCLIProtocol.ClientSide { - boolean complete; - int exit = -1; - ClientSideImpl(InputStream is, OutputStream os) throws IOException { - super(is, os); - if (is.read() != 0) { // cf. FullDuplexHttpService - throw new IOException("expected to see initial zero byte; perhaps you are connecting to an old server which does not support -http?"); - } + try (final ClientSideImpl connection = new ClientSideImpl(new PlainCLIProtocol.FramedOutput(streams.getOutputStream()))) { + connection.start(args); + InputStream is = streams.getInputStream(); + if (is.read() != 0) { // cf. FullDuplexHttpService + throw new IOException("expected to see initial zero byte; perhaps you are connecting to an old server which does not support -http?"); } - @Override - protected void onExit(int code) { - this.exit = code; - finished(); - } - @Override - protected void onStdout(byte[] chunk) throws IOException { - System.out.write(chunk); - } - @Override - protected void onStderr(byte[] chunk) throws IOException { - System.err.write(chunk); - } - @Override - protected void handleClose() { - finished(); - } - private synchronized void finished() { - complete = true; - notifyAll(); - } - } - try (final ClientSideImpl connection = new ClientSideImpl(streams.getInputStream(), streams.getOutputStream())) { - for (String arg : args) { - connection.sendArg(arg); - } - connection.sendEncoding(Charset.defaultCharset().name()); - connection.sendLocale(Locale.getDefault().toString()); - connection.sendStart(); - connection.begin(); - new Thread("input reader") { - @Override - public void run() { - try { - final OutputStream stdin = connection.streamStdin(); - int c; - while (!connection.complete && (c = System.in.read()) != -1) { - stdin.write(c); - } - connection.sendEndStdin(); - } catch (IOException x) { - LOGGER.log(Level.WARNING, null, x); - } - } - }.start(); + new PlainCLIProtocol.FramedReader(connection, is).start(); new Thread("ping") { // JENKINS-46659 @Override public void run() { @@ -347,15 +370,79 @@ public class CLI { } }.start(); - synchronized (connection) { - while (!connection.complete) { - connection.wait(); - } - } - return connection.exit; + return connection.exit(); } } + private static final class ClientSideImpl extends PlainCLIProtocol.ClientSide { + + volatile boolean complete; + private int exit = -1; + + ClientSideImpl(PlainCLIProtocol.Output out) { + super(out); + } + + void start(List args) throws IOException { + for (String arg : args) { + sendArg(arg); + } + sendEncoding(Charset.defaultCharset().name()); + sendLocale(Locale.getDefault().toString()); + sendStart(); + new Thread("input reader") { + @Override + public void run() { + try { + final OutputStream stdin = streamStdin(); + int c; + // TODO check available to avoid sending lots of one-byte frames + while (!complete && (c = System.in.read()) != -1) { + stdin.write(c); + } + sendEndStdin(); + } catch (IOException x) { + LOGGER.log(Level.WARNING, null, x); + } + } + }.start(); + } + + @Override + protected synchronized void onExit(int code) { + this.exit = code; + finished(); + } + + @Override + protected void onStdout(byte[] chunk) throws IOException { + System.out.write(chunk); + } + + @Override + protected void onStderr(byte[] chunk) throws IOException { + System.err.write(chunk); + } + + @Override + protected void handleClose() { + finished(); + } + + private synchronized void finished() { + complete = true; + notifyAll(); + } + + synchronized int exit() throws InterruptedException { + while (!complete) { + wait(); + } + return exit; + } + + } + private static String computeVersion() { Properties props = new Properties(); try { diff --git a/cli/src/main/java/hudson/cli/PlainCLIProtocol.java b/cli/src/main/java/hudson/cli/PlainCLIProtocol.java index ed5c453360..5359b7c738 100644 --- a/cli/src/main/java/hudson/cli/PlainCLIProtocol.java +++ b/cli/src/main/java/hudson/cli/PlainCLIProtocol.java @@ -37,6 +37,7 @@ import java.nio.channels.ReadPendingException; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BoundedInputStream; import org.apache.commons.io.input.CountingInputStream; /** @@ -76,111 +77,143 @@ class PlainCLIProtocol { } } - static abstract class EitherSide implements Closeable { + interface Output extends Closeable { + void send(byte[] data) throws IOException; + } - private final CountingInputStream cis; - private final FlightRecorderInputStream flightRecorder; - final DataInputStream dis; - final DataOutputStream dos; + static final class FramedOutput implements Output { - protected EitherSide(InputStream is, OutputStream os) { - cis = new CountingInputStream(is); - flightRecorder = new FlightRecorderInputStream(cis); - dis = new DataInputStream(flightRecorder); + private final DataOutputStream dos; + + FramedOutput(OutputStream os) { dos = new DataOutputStream(os); } - final void begin() { - new Reader().start(); + @Override + public void send(byte[] data) throws IOException { + dos.writeInt(data.length - 1); // not counting the opcode + dos.write(data); + dos.flush(); } - private class Reader extends Thread { + @Override + public void close() throws IOException { + dos.close(); + } - Reader() { - super("PlainCLIProtocol"); // TODO set distinctive Thread.name - } + } - @Override - public void run() { - try { - while (true) { - LOGGER.finest("reading frame"); - int framelen; - try { - framelen = dis.readInt(); - } catch (EOFException x) { - handleClose(); - break; // TODO verify that we hit EOF immediately, not partway into framelen - } - if (framelen < 0) { - throw new IOException("corrupt stream: negative frame length"); - } - byte b = dis.readByte(); - if (b < 0) { // i.e., >127 - throw new IOException("corrupt stream: negative operation code"); - } - if (b >= Op.values().length) { - LOGGER.log(Level.WARNING, "unknown operation #{0}: {1}", new Object[] {b, HexDump.toHex(flightRecorder.getRecord())}); - IOUtils.skipFully(dis, framelen); - continue; - } - Op op = Op.values()[b]; - long start = cis.getByteCount(); - LOGGER.log(Level.FINEST, "handling frame with {0} of length {1}", new Object[] {op, framelen}); - boolean handled = handle(op, framelen); - if (handled) { - long actuallyRead = cis.getByteCount() - start; - if (actuallyRead != framelen) { - throw new IOException("corrupt stream: expected to read " + framelen + " bytes from " + op + " but read " + actuallyRead); - } - } else { - LOGGER.log(Level.WARNING, "unexpected {0}: {1}", new Object[] {op, HexDump.toHex(flightRecorder.getRecord())}); - IOUtils.skipFully(dis, framelen); + static final class FramedReader extends Thread { + + private final EitherSide side; + private final CountingInputStream cis; + private final FlightRecorderInputStream flightRecorder; + private final DataInputStream dis; + + FramedReader(EitherSide side, InputStream is) { + super("PlainCLIProtocol"); // TODO set distinctive Thread.name + this.side = side; + cis = new CountingInputStream(is); + flightRecorder = new FlightRecorderInputStream(cis); + dis = new DataInputStream(flightRecorder); + } + + @Override + public void run() { + try { + while (true) { + LOGGER.finest("reading frame"); + int framelen; + try { + framelen = dis.readInt(); + } catch (EOFException x) { + side.handleClose(); + break; // TODO verify that we hit EOF immediately, not partway into framelen + } + if (framelen < 0) { + throw new IOException("corrupt stream: negative frame length"); + } + LOGGER.finest("read frame length " + framelen); + long start = cis.getByteCount(); + try { + side.handle(new DataInputStream(new BoundedInputStream(dis, /* op byte not counted */framelen + 1))); + } catch (ProtocolException x) { + LOGGER.log(Level.WARNING, null, x); + // but read another frame + } finally { + long actuallyRead = cis.getByteCount() - start; + long unread = framelen + 1 - actuallyRead; + if (unread > 0) { + LOGGER.warning(() -> "Did not read " + unread + " bytes"); + IOUtils.skipFully(dis, unread); } } - } catch (ClosedChannelException x) { - LOGGER.log(Level.FINE, null, x); - handleClose(); - } catch (IOException x) { - LOGGER.log(Level.WARNING, null, flightRecorder.analyzeCrash(x, "broken stream")); - } catch (ReadPendingException x) { - // in case trick in CLIAction does not work - LOGGER.log(Level.FINE, null, x); - handleClose(); - } catch (RuntimeException x) { - LOGGER.log(Level.WARNING, null, x); - handleClose(); } + } catch (ClosedChannelException x) { + LOGGER.log(Level.FINE, null, x); + side.handleClose(); + } catch (IOException x) { + LOGGER.log(Level.WARNING, null, flightRecorder.analyzeCrash(x, "broken stream")); + } catch (ReadPendingException x) { + // in case trick in CLIAction does not work + LOGGER.log(Level.FINE, null, x); + side.handleClose(); + } catch (RuntimeException x) { + LOGGER.log(Level.WARNING, null, x); + side.handleClose(); } + } + } + + private static final class ProtocolException extends IOException { + ProtocolException(String message) { + super(message); + } + } + + static abstract class EitherSide implements Closeable { + + private final Output out; + + protected EitherSide(Output out) { + this.out = out; } protected abstract void handleClose(); - protected abstract boolean handle(Op op, int framelen) throws IOException; - - private void writeOp(Op op) throws IOException { - dos.writeByte((byte) op.ordinal()); + final void handle(DataInputStream dis) throws IOException { + byte b = dis.readByte(); + if (b < 0) { // i.e., >127 + throw new IOException("corrupt stream: negative operation code"); + } + if (b >= Op.values().length) { + throw new ProtocolException("unknown operation #" + b); + } + Op op = Op.values()[b]; + LOGGER.finest(() -> "handling frame with " + op); + if (!handle(op, dis)) { + throw new ProtocolException("unhandled: " + op); + } } + protected abstract boolean handle(Op op, DataInputStream dis) throws IOException; + protected final synchronized void send(Op op) throws IOException { - dos.writeInt(0); - writeOp(op); - dos.flush(); + send(op, new byte[0], 0, 0); } - protected final synchronized void send(Op op, int number) throws IOException { - dos.writeInt(4); - writeOp(op); - dos.writeInt(number); - dos.flush(); + protected final synchronized void send(Op op, int v) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4); + new DataOutputStream(baos).writeInt(v); + send(op, baos.toByteArray()); } protected final synchronized void send(Op op, byte[] chunk, int off, int len) throws IOException { - dos.writeInt(len); - writeOp(op); - dos.write(chunk, off, len); - dos.flush(); + byte[] data = new byte[len + 1]; + data[0] = (byte) op.ordinal(); + System.arraycopy(chunk, off, data, 1, len); + out.send(data); } protected final void send(Op op, byte[] chunk) throws IOException { @@ -193,13 +226,6 @@ class PlainCLIProtocol { send(op, buf.toByteArray()); } - protected final byte[] readChunk(int framelen) throws IOException { - assert Thread.currentThread() instanceof EitherSide.Reader; - byte[] buf = new byte[framelen]; - dis.readFully(buf); - return buf; - } - protected final OutputStream stream(final Op op) { return new OutputStream() { @Override @@ -219,20 +245,19 @@ class PlainCLIProtocol { @Override public synchronized void close() throws IOException { - dos.close(); + out.close(); } } static abstract class ServerSide extends EitherSide { - ServerSide(InputStream is, OutputStream os) { - super(is, os); + ServerSide(Output out) { + super(out); } @Override - protected final boolean handle(Op op, int framelen) throws IOException { - assert Thread.currentThread() instanceof EitherSide.Reader; + protected final boolean handle(Op op, DataInputStream dis) throws IOException { assert op.clientSide; switch (op) { case ARG: @@ -248,7 +273,7 @@ class PlainCLIProtocol { onStart(); return true; case STDIN: - onStdin(readChunk(framelen)); + onStdin(IOUtils.toByteArray(dis)); return true; case END_STDIN: onEndStdin(); @@ -286,23 +311,22 @@ class PlainCLIProtocol { static abstract class ClientSide extends EitherSide { - ClientSide(InputStream is, OutputStream os) { - super(is, os); + ClientSide(Output out) { + super(out); } @Override - protected boolean handle(Op op, int framelen) throws IOException { - assert Thread.currentThread() instanceof EitherSide.Reader; + protected boolean handle(Op op, DataInputStream dis) throws IOException { assert !op.clientSide; switch (op) { case EXIT: onExit(dis.readInt()); return true; case STDOUT: - onStdout(readChunk(framelen)); + onStdout(IOUtils.toByteArray(dis)); return true; case STDERR: - onStderr(readChunk(framelen)); + onStderr(IOUtils.toByteArray(dis)); return true; default: return false; @@ -311,6 +335,7 @@ class PlainCLIProtocol { protected abstract void onExit(int code); + // TODO more efficient to change signature to InputStream, then use IOUtils.copy protected abstract void onStdout(byte[] chunk) throws IOException; protected abstract void onStderr(byte[] chunk) throws IOException; diff --git a/cli/src/main/resources/hudson/cli/client/Messages.properties b/cli/src/main/resources/hudson/cli/client/Messages.properties index c789036505..de4cac2174 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages.properties @@ -3,6 +3,7 @@ CLI.Usage=Jenkins CLI\n\ Options:\n\ \ -s URL : the server URL (defaults to the JENKINS_URL env var)\n\ \ -http : use a plain CLI protocol over HTTP(S) (the default; mutually exclusive with -ssh)\n\ + \ -webSocket : like -http but using WebSocket (works better with most reverse proxies)\n\ \ -ssh : use SSH protocol (requires -user; SSH port must be open on server, and user must have registered a public key)\n\ \ -i KEY : SSH private key file used for authentication (for use with -ssh)\n\ \ -noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution\n\ diff --git a/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java b/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java index e8381f452b..953b01d02c 100644 --- a/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java +++ b/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java @@ -27,6 +27,7 @@ package hudson.cli; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; @@ -44,7 +45,7 @@ public class PlainCLIProtocolTest { int code = -1; final ByteArrayOutputStream stdout = new ByteArrayOutputStream(); Client() throws IOException { - super(new PipedInputStream(download), upload); + super(new PlainCLIProtocol.FramedOutput(upload)); } @Override protected synchronized void onExit(int code) { @@ -65,6 +66,7 @@ public class PlainCLIProtocolTest { streamStdin().write("hello".getBytes()); } void newop() throws IOException { + DataOutputStream dos = new DataOutputStream(upload); dos.writeInt(0); dos.writeByte(99); dos.flush(); @@ -75,7 +77,7 @@ public class PlainCLIProtocolTest { boolean started; final ByteArrayOutputStream stdin = new ByteArrayOutputStream(); Server() throws IOException { - super(new PipedInputStream(upload), download); + super(new PlainCLIProtocol.FramedOutput(download)); } @Override protected void onArg(String text) { @@ -110,6 +112,7 @@ public class PlainCLIProtocolTest { sendExit(2); } void newop() throws IOException { + DataOutputStream dos = new DataOutputStream(download); dos.writeInt(0); dos.writeByte(99); dos.flush(); @@ -117,8 +120,8 @@ public class PlainCLIProtocolTest { } Client client = new Client(); Server server = new Server(); - client.begin(); - server.begin(); + new PlainCLIProtocol.FramedReader(client, new PipedInputStream(download)).start(); + new PlainCLIProtocol.FramedReader(server, new PipedInputStream(upload)).start(); client.send(); client.newop(); synchronized (server) { diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java index ef5cfe1237..bd8fc132d0 100644 --- a/core/src/main/java/hudson/ExtensionFinder.java +++ b/core/src/main/java/hudson/ExtensionFinder.java @@ -497,7 +497,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { m.setAccessible(true); m.invoke(ecl, c); } - for (Class cc = c; cc != Object.class; cc = cc.getSuperclass()) { + for (Class cc = c; cc != Object.class && cc != null; cc = cc.getSuperclass()) { /** * See {@link com.google.inject.spi.InjectionPoint#getInjectionPoints(TypeLiteral, boolean, Errors)} */ diff --git a/core/src/main/java/hudson/cli/CLIAction.java b/core/src/main/java/hudson/cli/CLIAction.java index c3858b6dd1..102dbbc00b 100644 --- a/core/src/main/java/hudson/cli/CLIAction.java +++ b/core/src/main/java/hudson/cli/CLIAction.java @@ -43,20 +43,26 @@ import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import hudson.Extension; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.PrintStream; +import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.util.FullDuplexHttpService; +import jenkins.websocket.WebSocketSession; +import jenkins.websocket.WebSockets; +import org.acegisecurity.Authentication; +import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; /** @@ -100,6 +106,74 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy { req.getView(this, "command.jelly").forward(req, rsp); } + /** for Jelly */ + public boolean isWebSocketSupported() { + return WebSockets.isSupported(); + } + + /** + * WebSocket endpoint. + */ + public HttpResponse doWs() { + if (!WebSockets.isSupported()) { + return HttpResponses.notFound(); + } + Authentication authentication = Jenkins.getAuthentication(); + return WebSockets.upgrade(new WebSocketSession() { + ServerSideImpl connection; + class OutputImpl implements PlainCLIProtocol.Output { + @Override + public void send(byte[] data) throws IOException { + sendBinary(ByteBuffer.wrap(data)); + } + @Override + public void close() throws IOException { + doClose(); + } + } + private void doClose() { + close(); + } + @Override + protected void opened() { + try { + connection = new ServerSideImpl(new OutputImpl(), authentication); + } catch (IOException x) { + error(x); + return; + } + new Thread(() -> { + try { + try { + connection.run(); + } finally { + connection.close(); + } + } catch (Exception x) { + error(x); + } + }, "CLI handler for " + authentication.getName()).start(); + } + @Override + protected void binary(byte[] payload, int offset, int len) { + try { + connection.handle(new DataInputStream(new ByteArrayInputStream(payload, offset, len))); + } catch (IOException x) { + error(x); + } + } + @Override + protected void error(Throwable cause) { + LOGGER.log(Level.WARNING, null, cause); + } + @Override + protected void closed(int statusCode, String reason) { + LOGGER.fine(() -> "closed: " + statusCode + ": " + reason); + connection.handleClose(); + } + }); + } + @Override public Object getTarget() { StaplerRequest req = Stapler.getCurrentRequest(); @@ -116,6 +190,105 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy { } } + class ServerSideImpl extends PlainCLIProtocol.ServerSide { + private Thread runningThread; + private boolean ready; + private final List args = new ArrayList<>(); + private Locale locale = Locale.getDefault(); + private Charset encoding = Charset.defaultCharset(); + private final PipedInputStream stdin = new PipedInputStream(); + private final PipedOutputStream stdinMatch = new PipedOutputStream(); + private final Authentication authentication; + ServerSideImpl(PlainCLIProtocol.Output out, Authentication authentication) throws IOException { + super(out); + stdinMatch.connect(stdin); + this.authentication = authentication; + } + @Override + protected void onArg(String text) { + args.add(text); + } + @Override + protected void onLocale(String text) { + for (Locale _locale : Locale.getAvailableLocales()) { + if (_locale.toString().equals(text)) { + locale = _locale; + return; + } + } + LOGGER.log(Level.WARNING, "unknown client locale {0}", text); + } + @Override + protected void onEncoding(String text) { + try { + encoding = Charset.forName(text); + } catch (UnsupportedCharsetException x) { + LOGGER.log(Level.WARNING, "unknown client charset {0}", text); + } + } + @Override + protected void onStart() { + ready(); + } + @Override + protected void onStdin(byte[] chunk) throws IOException { + stdinMatch.write(chunk); + } + @Override + protected void onEndStdin() throws IOException { + stdinMatch.close(); + } + @Override + protected void handleClose() { + ready(); + if (runningThread != null) { + runningThread.interrupt(); + } + } + private synchronized void ready() { + ready = true; + notifyAll(); + } + void run() throws IOException, InterruptedException { + synchronized (this) { + while (!ready) { + wait(); + } + } + PrintStream stdout = new PrintStream(streamStdout(), false, encoding.name()); + PrintStream stderr = new PrintStream(streamStderr(), true, encoding.name()); + if (args.isEmpty()) { + stderr.println("Connection closed before arguments received"); + sendExit(2); + return; + } + String commandName = args.get(0); + CLICommand command = CLICommand.clone(commandName); + if (command == null) { + stderr.println("No such command " + commandName); + sendExit(2); + return; + } + command.setTransportAuth(authentication); + command.setClientCharset(encoding); + CLICommand orig = CLICommand.setCurrent(command); + try { + runningThread = Thread.currentThread(); + int exit = command.main(args.subList(1, args.size()), locale, stdin, stdout, stderr); + stdout.flush(); + sendExit(exit); + try { // seems to avoid ReadPendingException from Jetty + Thread.sleep(1000); + } catch (InterruptedException x) { + // expected; ignore + } + } finally { + CLICommand.setCurrent(orig); + runningThread = null; + } + } + } + /** * Serves {@link PlainCLIProtocol} response. */ @@ -130,103 +303,9 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy { return new FullDuplexHttpService(uuid) { @Override protected void run(InputStream upload, OutputStream download) throws IOException, InterruptedException { - final AtomicReference runningThread = new AtomicReference<>(); - class ServerSideImpl extends PlainCLIProtocol.ServerSide { - boolean ready; - List args = new ArrayList<>(); - Locale locale = Locale.getDefault(); - Charset encoding = Charset.defaultCharset(); - final PipedInputStream stdin = new PipedInputStream(); - final PipedOutputStream stdinMatch = new PipedOutputStream(); - ServerSideImpl(InputStream is, OutputStream os) throws IOException { - super(is, os); - stdinMatch.connect(stdin); - } - @Override - protected void onArg(String text) { - args.add(text); - } - @Override - protected void onLocale(String text) { - for (Locale _locale : Locale.getAvailableLocales()) { - if (_locale.toString().equals(text)) { - locale = _locale; - return; - } - } - LOGGER.log(Level.WARNING, "unknown client locale {0}", text); - } - @Override - protected void onEncoding(String text) { - try { - encoding = Charset.forName(text); - } catch (UnsupportedCharsetException x) { - LOGGER.log(Level.WARNING, "unknown client charset {0}", text); - } - } - @Override - protected void onStart() { - ready(); - } - @Override - protected void onStdin(byte[] chunk) throws IOException { - stdinMatch.write(chunk); - } - @Override - protected void onEndStdin() throws IOException { - stdinMatch.close(); - } - @Override - protected void handleClose() { - ready(); - Thread t = runningThread.get(); - if (t != null) { - t.interrupt(); - } - } - private synchronized void ready() { - ready = true; - notifyAll(); - } - } - try (ServerSideImpl connection = new ServerSideImpl(upload, download)) { - connection.begin(); - synchronized (connection) { - while (!connection.ready) { - connection.wait(); - } - } - PrintStream stdout = new PrintStream(connection.streamStdout(), false, connection.encoding.name()); - PrintStream stderr = new PrintStream(connection.streamStderr(), true, connection.encoding.name()); - if (connection.args.isEmpty()) { - stderr.println("Connection closed before arguments received"); - connection.sendExit(2); - return; - } - String commandName = connection.args.get(0); - CLICommand command = CLICommand.clone(commandName); - if (command == null) { - stderr.println("No such command " + commandName); - connection.sendExit(2); - return; - } - command.setTransportAuth(Jenkins.getAuthentication()); - command.setClientCharset(connection.encoding); - CLICommand orig = CLICommand.setCurrent(command); - try { - runningThread.set(Thread.currentThread()); - int exit = command.main(connection.args.subList(1, connection.args.size()), connection.locale, connection.stdin, stdout, stderr); - stdout.flush(); - connection.sendExit(exit); - try { // seems to avoid ReadPendingException from Jetty - Thread.sleep(1000); - } catch (InterruptedException x) { - // expected; ignore - } - } finally { - CLICommand.setCurrent(orig); - runningThread.set(null); - } + try (ServerSideImpl connection = new ServerSideImpl(new PlainCLIProtocol.FramedOutput(download), Jenkins.getAuthentication())) { + new PlainCLIProtocol.FramedReader(connection, upload).start(); + connection.run(); } } }; diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index 22237a7e47..68747a6a8c 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -244,8 +244,6 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { 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/cli/ConnectNodeCommand.java b/core/src/main/java/hudson/cli/ConnectNodeCommand.java index 16fc8f9bf5..0a9a152e22 100644 --- a/core/src/main/java/hudson/cli/ConnectNodeCommand.java +++ b/core/src/main/java/hudson/cli/ConnectNodeCommand.java @@ -26,9 +26,6 @@ package hudson.cli; import hudson.AbortException; import hudson.Extension; import hudson.model.Computer; -import hudson.model.ComputerSet; -import hudson.util.EditDistance; -import jenkins.model.Jenkins; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; diff --git a/core/src/main/java/hudson/cli/OnlineNodeCommand.java b/core/src/main/java/hudson/cli/OnlineNodeCommand.java index 4777afcd37..e4a56ef8ed 100644 --- a/core/src/main/java/hudson/cli/OnlineNodeCommand.java +++ b/core/src/main/java/hudson/cli/OnlineNodeCommand.java @@ -27,9 +27,6 @@ package hudson.cli; import hudson.AbortException; import hudson.Extension; import hudson.model.Computer; -import hudson.model.ComputerSet; -import hudson.util.EditDistance; -import jenkins.model.Jenkins; import org.kohsuke.args4j.Argument; diff --git a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java index c45eebd711..856cbb0cec 100644 --- a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java +++ b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java @@ -57,8 +57,6 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import jenkins.model.Jenkins; -import org.acegisecurity.context.SecurityContext; -import org.acegisecurity.context.SecurityContextHolder; import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 52c327e350..00a82dbf69 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -665,6 +665,7 @@ public abstract class AbstractProject

,R extends A } @Override + @Exported public boolean isDisabled() { return disabled; } diff --git a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java index fb45e0a5a6..0f02f1c2a2 100644 --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java @@ -24,7 +24,6 @@ package hudson.model; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import hudson.ExtensionList; import hudson.FilePath; import hudson.Util; import java.io.IOException; diff --git a/core/src/main/java/hudson/model/ListView.java b/core/src/main/java/hudson/model/ListView.java index b9b0c6b680..5ddd52ecda 100644 --- a/core/src/main/java/hudson/model/ListView.java +++ b/core/src/main/java/hudson/model/ListView.java @@ -29,7 +29,6 @@ import hudson.Util; import hudson.diagnosis.OldDataMonitor; import hudson.model.Descriptor.FormException; import hudson.model.listeners.ItemListener; -import hudson.search.CollectionSearchIndex; import hudson.search.SearchIndexBuilder; import hudson.security.ACL; import hudson.security.ACLContext; diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java index 29856dbcfa..75f88ea92e 100644 --- a/core/src/main/java/hudson/model/Slave.java +++ b/core/src/main/java/hudson/model/Slave.java @@ -407,23 +407,19 @@ public abstract class Slave extends Node implements Serializable { 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/model/User.java b/core/src/main/java/hudson/model/User.java index 54f9db6c6a..c49c795ed2 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -31,7 +31,6 @@ import hudson.CopyOnWrite; import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; -import hudson.FeedAdapter; import hudson.Util; import hudson.XmlFile; import hudson.init.InitMilestone; diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java index d310c5295c..f51a3dd958 100644 --- a/core/src/main/java/hudson/model/View.java +++ b/core/src/main/java/hudson/model/View.java @@ -33,7 +33,6 @@ import hudson.Functions; import hudson.Indenter; import hudson.Util; import hudson.model.Descriptor.FormException; -import hudson.model.labels.LabelAtomPropertyDescriptor; import hudson.model.listeners.ItemListener; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; diff --git a/core/src/main/java/hudson/slaves/ComputerLauncher.java b/core/src/main/java/hudson/slaves/ComputerLauncher.java index 7efb9f4a41..38ee6d70be 100644 --- a/core/src/main/java/hudson/slaves/ComputerLauncher.java +++ b/core/src/main/java/hudson/slaves/ComputerLauncher.java @@ -180,7 +180,7 @@ public abstract class ComputerLauncher extends AbstractDescribableImpl 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 47654b2550..73409e4ae4 100644 --- a/core/src/main/java/hudson/triggers/SafeTimerTask.java +++ b/core/src/main/java/hudson/triggers/SafeTimerTask.java @@ -50,6 +50,27 @@ import jenkins.util.Timer; */ 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/hudson/util/FormValidation.java b/core/src/main/java/hudson/util/FormValidation.java index e632e7bb1b..94a0c4b2ac 100644 --- a/core/src/main/java/hudson/util/FormValidation.java +++ b/core/src/main/java/hudson/util/FormValidation.java @@ -23,7 +23,6 @@ */ package hudson.util; -import hudson.EnvVars; import hudson.Functions; import hudson.Launcher; import hudson.ProxyConfiguration; 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 0000000000..965c3b67eb --- /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/model/JenkinsLocationConfiguration.java b/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java index 3a99bd199e..4f526e9774 100644 --- a/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java +++ b/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java @@ -7,7 +7,6 @@ import hudson.XmlFile; import hudson.model.PersistentDescriptor; import hudson.util.FormValidation; import hudson.util.XStream2; -import jenkins.security.ApiTokenProperty; import jenkins.util.SystemProperties; import jenkins.util.UrlHelper; import org.jenkinsci.Symbol; diff --git a/core/src/main/java/jenkins/mvn/FilePathGlobalSettingsProvider.java b/core/src/main/java/jenkins/mvn/FilePathGlobalSettingsProvider.java index ee7b81cae7..3adc364c1e 100644 --- a/core/src/main/java/jenkins/mvn/FilePathGlobalSettingsProvider.java +++ b/core/src/main/java/jenkins/mvn/FilePathGlobalSettingsProvider.java @@ -1,14 +1,9 @@ package jenkins.mvn; -import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; -import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.TaskListener; -import hudson.util.IOUtils; - -import java.io.File; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; diff --git a/core/src/main/java/jenkins/mvn/FilePathSettingsProvider.java b/core/src/main/java/jenkins/mvn/FilePathSettingsProvider.java index bbc95005fb..faaec2da0b 100644 --- a/core/src/main/java/jenkins/mvn/FilePathSettingsProvider.java +++ b/core/src/main/java/jenkins/mvn/FilePathSettingsProvider.java @@ -1,14 +1,9 @@ package jenkins.mvn; -import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; -import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.TaskListener; -import hudson.util.IOUtils; - -import java.io.File; import org.apache.commons.lang.StringUtils; import org.jenkinsci.Symbol; diff --git a/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java b/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java index 7f42c3836e..1d052cff3d 100644 --- a/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java +++ b/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java @@ -115,7 +115,7 @@ public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver { + "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; } @@ -151,7 +151,7 @@ public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver { 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 5464056fdb..b738b3316a 100644 --- a/core/src/main/java/jenkins/slaves/JnlpAgentReceiver.java +++ b/core/src/main/java/jenkins/slaves/JnlpAgentReceiver.java @@ -7,12 +7,13 @@ import hudson.model.Slave; 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/telemetry/impl/java11/MissingClassTelemetry.java b/core/src/main/java/jenkins/telemetry/impl/java11/MissingClassTelemetry.java index 5eeaece2fd..2cd8aa6c9c 100644 --- a/core/src/main/java/jenkins/telemetry/impl/java11/MissingClassTelemetry.java +++ b/core/src/main/java/jenkins/telemetry/impl/java11/MissingClassTelemetry.java @@ -104,7 +104,9 @@ public class MissingClassTelemetry extends Telemetry { {"java.util.ResourceBundle$Control", "newBundle"}, //hundreds when a job is created {"org.codehaus.groovy.control.ClassNodeResolver", "tryAsLoaderClassOrScript"}, - {"org.kohsuke.stapler.RequestImpl$TypePair", "convertJSON"} + {"org.kohsuke.stapler.RequestImpl$TypePair", "convertJSON"}, + {"net.bull.javamelody.FilterContext", "isMojarraAvailable"} // JENKINS-60725 + }; @Nonnull diff --git a/core/src/main/java/jenkins/util/AntClassLoader.java b/core/src/main/java/jenkins/util/AntClassLoader.java index 36588141fb..6a2b5f8765 100644 --- a/core/src/main/java/jenkins/util/AntClassLoader.java +++ b/core/src/main/java/jenkins/util/AntClassLoader.java @@ -49,7 +49,6 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; -import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.StringTokenizer; diff --git a/core/src/main/java/jenkins/util/AntWithFindResourceClassLoader.java b/core/src/main/java/jenkins/util/AntWithFindResourceClassLoader.java index d0cfc53911..7b70413a48 100644 --- a/core/src/main/java/jenkins/util/AntWithFindResourceClassLoader.java +++ b/core/src/main/java/jenkins/util/AntWithFindResourceClassLoader.java @@ -1,7 +1,5 @@ package jenkins.util; -import org.apache.tools.ant.Project; - import java.io.Closeable; import java.io.File; import java.io.IOException; 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 0000000000..5aa8b432c2 --- /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 0000000000..95a7ed7609 --- /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 0000000000..2d856d5069 --- /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 b837be4264..1b04d3c2c9 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 a7e6e2cc10..5a4c4cef32 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/model/UpdateCenter/CoreUpdateMonitor/message_bg.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_bg.properties index 1893ad5e2f..5c76d4878e 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_bg.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_bg.properties @@ -29,7 +29,7 @@ UpgradeComplete=\ UpgradeFailed=\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 \u043a\u044a\u043c Jenkins {0}: {1}. NewVersionAvailable=\ \u041d\u043e\u0432\u0430\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Jenkins ({0}) \u0435 \u043d\u0430\u043b\u0438\u0447\u043d\u0430 \u0437\u0430 \u0438\u0437\u0442\u0435\u0433\u043b\u044f\u043d\u0435\ - (\u0441\u043f\u0438\u0441\u044a\u043a \u0441 \u043f\u0440\u043e\u043c\u0435\u043d\u0438\u0442\u0435). + (\u0441\u043f\u0438\u0441\u044a\u043a \u0441 \u043f\u0440\u043e\u043c\u0435\u043d\u0438\u0442\u0435). Or\ Upgrade\ Automatically=\ \u0418\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 UpgradeCompleteRestartNotSupported=\ diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_cs.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_cs.properties index 691b46b7f0..2fbdd8a13f 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_cs.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_cs.properties @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=Nov\u00E1 verze Jenkins-u ({0}) je k dispozici ke sta\u017Een\u00ED (changelog). +NewVersionAvailable=Nov\u00E1 verze Jenkins-u ({0}) je k dispozici ke sta\u017Een\u00ED (changelog). Or\ Upgrade\ Automatically=Nebo Upgradovat Automaticky UpgradeComplete=Upgrade na Jenkinse {0} je hotov, \u010Dek\u00E1 na restart. UpgradeCompleteRestartNotSupported=Upgrade na Jenkinse {0} je hotov, \u010Dek\u00E1 na restart. diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_da.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_da.properties index 8b5776e534..d23b600857 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_da.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_da.properties @@ -25,4 +25,4 @@ UpgradeCompleteRestartNotSupported=Opdatering til Jenkins {0} er fuldf\u00f8rt, UpgradeProgress=Opdatering til Jenkins {0} er i gang eller er fejlet. Or\ Upgrade\ Automatically=Eller opdater automatisk NewVersionAvailable=En ny version af Jenkins ({0}) er tilg\u00e6ngelig til hentning \ - (changelog). + (changelog). diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_de.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_de.properties index 344e457118..98b368fc36 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_de.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_de.properties @@ -23,5 +23,5 @@ UpgradeComplete=Aktualisierung auf Jenkins {0} abgeschlossen. Neustart von Jenkins erforderlich. UpgradeCompleteRestartNotSupported=Aktualisierung auf Jenkins {0} abgeschlossen. Neustart von Jenkins erforderlich. UpgradeProgress=Aktualisierung auf Jenkins {0} in Arbeit oder fehlgeschlagen. -NewVersionAvailable=Eine neue Version von Jenkins ({0}) kann hier heruntergeladen werden (Was ist neu?). +NewVersionAvailable=Eine neue Version von Jenkins ({0}) kann hier heruntergeladen werden (Was ist neu?). Or\ Upgrade\ Automatically=Automatisch aktualisieren diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_es.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_es.properties index 6016d4d17c..77a67a8252 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_es.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_es.properties @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=Hay una nueva versi\u00f3n de Jenkins disponible ({0}). descargar (listado de cambios). +NewVersionAvailable=Hay una nueva versi\u00f3n de Jenkins disponible ({0}). descargar (listado de cambios). Or\ Upgrade\ Automatically=O actualizar autom\u00e1ticamente UpgradeComplete=La actualizaci\u00f3n a Jenkins {0} se ha completado, esperando para reiniciar. UpgradeCompleteRestartNotSupported=La actualizaci\u00f3n a Jenkins {0} se ha completado, esperando para reiniciar. diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_et.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_et.properties index 1df6054b14..eee3e46823 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_et.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_et.properties @@ -1,3 +1,3 @@ # This file is under the MIT License by authors -NewVersionAvailable=Jenkinsi uus versioon ({0}) on saadaval allalaadimiseks (muudatuste nimekiri). +NewVersionAvailable=Jenkinsi uus versioon ({0}) on saadaval allalaadimiseks (muudatuste nimekiri). diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_fi.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_fi.properties index 8243a6f685..162f2da6f5 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_fi.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_fi.properties @@ -1,3 +1,3 @@ # This file is under the MIT License by authors -NewVersionAvailable=Uusi Jenkinsin versio ({0}) on saatavana (muutosloki). +NewVersionAvailable=Uusi Jenkinsin versio ({0}) on saatavana (muutosloki). diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_fr.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_fr.properties index 0dbdae69d7..df31af25d9 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_fr.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_fr.properties @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=Une nouvelle version de Jenkins ({0}) est disponible (changelog). +NewVersionAvailable=Une nouvelle version de Jenkins ({0}) est disponible (changelog). Or\ Upgrade\ Automatically=Ou mettre \u00E0 jour automatiquement UpgradeProgress=Mise \u00E0 jour vers Jenkins {0} est en cours ou en \u00E9chec. UpgradeComplete=La mise \u00E0 jour de Jenkins en {0} est termin\u00E9e, en attente de red\u00E9marrage. diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_hu.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_hu.properties index 19587285ca..1f781852b2 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_hu.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_hu.properties @@ -20,5 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=A Jenkins \u00FAj verzi\u00F3ja ({0}) el\u00E9rhet\u0151 let\u00F6lt\u00E9sre (v\u00E1ltoz\u00E1sok). +NewVersionAvailable=A Jenkins \u00FAj verzi\u00F3ja ({0}) el\u00E9rhet\u0151 let\u00F6lt\u00E9sre (v\u00E1ltoz\u00E1sok). Or\ Upgrade\ Automatically=Vagy friss\u00EDtsen automatikusan diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties index 47ffb48755..4102ffeced 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties @@ -21,7 +21,7 @@ # THE SOFTWARE. NewVersionAvailable=Una nuova versione di Jenkins ({0}) \u00e8 disponibile per il download \ - (log delle modifiche). + (log delle modifiche). Retry=Riprova UpgradeCompleteRestartNotSupported=Aggiornamento a Jenkins {0} completato, in attesa del riavvio. UpgradeProgress=Aggiornamento a Jenkins {0} in corso. diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ja.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ja.properties index 5738b71b76..3f32d0544b 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ja.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ja.properties @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=Jenkins\u306E\u65B0\u3057\u3044\u30D0\u30FC\u30B8\u30E7\u30F3({0})\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9\u3067\u304D\u307E\u3059 (\u5909\u66F4\u5C65\u6B74)\u3002 +NewVersionAvailable=Jenkins\u306E\u65B0\u3057\u3044\u30D0\u30FC\u30B8\u30E7\u30F3({0})\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9\u3067\u304D\u307E\u3059 (\u5909\u66F4\u5C65\u6B74)\u3002 Or\ Upgrade\ Automatically=\u81EA\u52D5\u3067\u30A2\u30C3\u30D7\u30B0\u30EC\u30FC\u30C9 UpgradeComplete=Jenkins {0} \u3078\u306E\u30A2\u30C3\u30D7\u30B0\u30EC\u30FC\u30C9\u306F\u5B8C\u4E86\u3057\u307E\u3057\u305F\u3002\u518D\u8D77\u52D5\u5F85\u3061\u3067\u3059\u3002 UpgradeProgress=Jenkins {0}\u3078\u306E\u30A2\u30C3\u30D7\u30B0\u30EC\u30FC\u30C9\u306F\u5B9F\u884C\u4E2D\u304B\u3001\u5931\u6557\u3057\u307E\u3057\u305F\u3002 diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ko.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ko.properties index f5f6a77276..d7d6899922 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ko.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ko.properties @@ -20,5 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=Jenkins \uC2E0\uADDC\uBC84\uC804({0})\uC744 \uC5EC\uAE30\uC11C \uBC1B\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.(\uBCC0\uACBD\uC0AC\uD56D). +NewVersionAvailable=Jenkins \uC2E0\uADDC\uBC84\uC804({0})\uC744 \uC5EC\uAE30\uC11C \uBC1B\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.(\uBCC0\uACBD\uC0AC\uD56D). Or\ Upgrade\ Automatically=\uB610\uB294 \uC790\uB3D9 \uC5C5\uADF8\uB808\uC774\uB4DC diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_lt.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_lt.properties index 2a16d979f6..09a40375ad 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_lt.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_lt.properties @@ -1,4 +1,4 @@ # This file is under the MIT License by authors -NewVersionAvailable=Galima parsisi\u0173sti nauj\u0105 ({0}) Jenkins versij\u0105. (pakeitimai). +NewVersionAvailable=Galima parsisi\u0173sti nauj\u0105 ({0}) Jenkins versij\u0105. (pakeitimai). Or\ Upgrade\ Automatically=Parsi\u0173sti automati\u0161kai diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_lv.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_lv.properties index 9193a28385..77c8207456 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_lv.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_lv.properties @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=Jauna Jenkins ({0}) versija ir pieejama lejupl\u0101dei (izmai\u0146as). +NewVersionAvailable=Jauna Jenkins ({0}) versija ir pieejama lejupl\u0101dei (izmai\u0146as). Or\ Upgrade\ Automatically=Vai atjaunin\u0101t autom\u0101tiski UpgradeComplete=Atjaunin\u0101s\u0101na uz Jenkins {0} ir pabeigta; gaidu p\u0101rstart\u0113\u0161anos. UpgradeCompleteRestartNotSupported=Atjaunin\u0101s\u0101na uz Jenkins {0} ir pabeigta; gaidu p\u0101rstart\u0113\u0161anos. diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_nb_NO.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_nb_NO.properties index b15161b442..c5cac2d1bf 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_nb_NO.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_nb_NO.properties @@ -20,4 +20,4 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=En ny versjon av Jenkins ({0}) er n\u00E5 tilgjengelig (Endringer). +NewVersionAvailable=En ny versjon av Jenkins ({0}) er n\u00E5 tilgjengelig (Endringer). diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_nl.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_nl.properties index 9c291b0fdd..d9a5180a47 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_nl.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_nl.properties @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=Er is een nieuwere versie van Jenkins ({0}) beschikbaar. downloaden (changelog). +NewVersionAvailable=Er is een nieuwere versie van Jenkins ({0}) beschikbaar. downloaden (changelog). Or\ Upgrade\ Automatically=Of werk automatisch bij UpgradeComplete=Upgrade naar Jenkins {0} is volledig; aan het wachten op herstart. UpgradeCompleteRestartNotSupported=Upgrade naar Jenkins {0} is volledig; aan het wachten op herstart. diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pl.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pl.properties index 18a2cbe3c3..5793e7f5da 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pl.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pl.properties @@ -19,7 +19,7 @@ # 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. -NewVersionAvailable=Nowa wersja Jenkinsa ({0}) jest dost\u0119pna do pobrania (historia zmian). +NewVersionAvailable=Nowa wersja Jenkinsa ({0}) jest dost\u0119pna do pobrania (historia zmian). Or\ Upgrade\ Automatically=Uaktualnij automatycznie UpgradeCompleteRestartNotSupported=Aktualizacja Jenkinsa do wersji {0} zako\u0144czy\u0142a si\u0119 pomy\u015Blnie, oczekiwanie na ponowne uruchomienie. UpgradeProgress=Aktualizacja Jenkinsa do wersji {0} trwa lub nie powiod\u0142a si\u0119. diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pt_BR.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pt_BR.properties index 216eee44d9..f604966589 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pt_BR.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pt_BR.properties @@ -27,5 +27,5 @@ UpgradeCompleteRestartNotSupported=Atualiza\u00e7\u00e3o do Jenkins {0} completa UpgradeProgress=Atualiza\u00e7\u00e3o para Jenkins {0} est\u00e1 em andamento ou falhou. Or\ Upgrade\ Automatically=Ou fazer a atualiza\u00e7\u00e3o automaticamente. # New version of Jenkins ({0}) is available for download \ -# (changelog). +# (changelog). NewVersionAvailable=Nova vers\u00e3o do Jenkins ({0}) est\u00e1 dispon\u00edvel em download diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pt_PT.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pt_PT.properties index dd2d81456d..f927f4cf83 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pt_PT.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_pt_PT.properties @@ -1,4 +1,4 @@ # This file is under the MIT License by authors -NewVersionAvailable=Uma nova vers\u00E3o do Jenkins ({0}) est\u00E1 dispon\u00EDvel para download (registo de altera\u00E7\u00F5es). +NewVersionAvailable=Uma nova vers\u00E3o do Jenkins ({0}) est\u00E1 dispon\u00EDvel para download (registo de altera\u00E7\u00F5es). Or\ Upgrade\ Automatically=Ou atualizar automaticamente diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ru.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ru.properties index 7c3dc2351e..18d52aa12a 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ru.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_ru.properties @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=\u041d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Jenkins ({0}) \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0434\u043b\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 (\u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439). +NewVersionAvailable=\u041d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Jenkins ({0}) \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0434\u043b\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 (\u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439). Retry=\u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c UpgradeComplete=\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u043e Jenkins {0} \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e, \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438. UpgradeFailed=\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_sk.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_sk.properties index 96543df586..e5f4de3439 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_sk.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_sk.properties @@ -20,4 +20,4 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=Nov\u00E1 verzia Jenkins ({0}) je dostupn\u00E1 na stiahnutie (zoznam zmien). +NewVersionAvailable=Nov\u00E1 verzia Jenkins ({0}) je dostupn\u00E1 na stiahnutie (zoznam zmien). diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_sl.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_sl.properties index 649104bcf5..fa461eb53a 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_sl.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_sl.properties @@ -1,4 +1,4 @@ # This file is under the MIT License by authors -NewVersionAvailable=Nova razli\u010Dica Jenkins-a ({0}) je na voljo za prenos (seznam sprememb). +NewVersionAvailable=Nova razli\u010Dica Jenkins-a ({0}) je na voljo za prenos (seznam sprememb). Or\ Upgrade\ Automatically=ali posodobi samodejno diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_uk.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_uk.properties index 625195c351..2c3fdbb332 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_uk.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_uk.properties @@ -20,5 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -NewVersionAvailable=\u041D\u043E\u0432\u0430 \u0432\u0435\u0440\u0441\u0456\u044F \u0414\u0436\u0435\u043D\u043A\u0456\u043D\u0441 ({0}) \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u0434\u043B\u044F \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F (\u0437\u043C\u0456\u043D\u0438). +NewVersionAvailable=\u041D\u043E\u0432\u0430 \u0432\u0435\u0440\u0441\u0456\u044F \u0414\u0436\u0435\u043D\u043A\u0456\u043D\u0441 ({0}) \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u0434\u043B\u044F \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F (\u0437\u043C\u0456\u043D\u0438). Or\ Upgrade\ Automatically=\u0410\u0431\u043E \u043E\u043D\u043E\u0432\u0456\u0442\u044C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_TW.properties b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_TW.properties index 76967e8ea1..e6305137c8 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_TW.properties +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_TW.properties @@ -24,5 +24,5 @@ UpgradeComplete=Jenkins {0} \u5347\u7248\u5b8c\u6210\uff0c\u7b49\u5019\u91cd\u65b0\u555f\u52d5\u3002 UpgradeCompleteRestartNotSupported=Jenkins {0} \u5347\u7248\u5b8c\u6210\uff0c\u7b49\u5019\u91cd\u65b0\u555f\u52d5\u3002 UpgradeProgress=Jenkins {0} \u5347\u7d1a\u4f5c\u696d\u6b63\u5728\u9032\u884c\u4e2d\u6216\u662f\u5df2\u7d93\u5931\u6557\u3002 -NewVersionAvailable=\u65b0\u7248\u7684 Jenkins ({0}) \u5df2\u7d93\u53ef\u4ee5\u4e0b\u8f09 (\u6539\u7248\u8a18\u9304)\u3002 +NewVersionAvailable=\u65b0\u7248\u7684 Jenkins ({0}) \u5df2\u7d93\u53ef\u4ee5\u4e0b\u8f09 (\u6539\u7248\u8a18\u9304)\u3002 Or\ Upgrade\ Automatically=\u6216\u662f\u81ea\u52d5\u5347\u7248 diff --git a/core/src/main/resources/hudson/slaves/JNLPLauncher/config.jelly b/core/src/main/resources/hudson/slaves/JNLPLauncher/config.jelly index d0765ddb5b..6b33de6b80 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 0000000000..90f5c28f90 --- /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 1348373933..404e30b9f0 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 e07b789741..173a897e9c 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 279fc74807..8fa1b2ba2f 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/core/src/main/resources/lib/layout/html.jelly b/core/src/main/resources/lib/layout/html.jelly index 9a957defe8..0a56c939ad 100644 --- a/core/src/main/resources/lib/layout/html.jelly +++ b/core/src/main/resources/lib/layout/html.jelly @@ -161,6 +161,8 @@ THE SOFTWARE.