From 59ebaf43a6a5526d134353ace7d17eedcfbe77d0 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 6 Nov 2013 10:16:36 -0500 Subject: [PATCH 0001/2380] Follow suggestion from JDK-6507809 [1] to print stack traces in a readable order. Would still need to handle some corner cases; write unit tests; and replace existing calls to Throwable.printStackTrace. [1] https://bugs.openjdk.java.net/browse/JDK-6507809 --- core/src/main/java/hudson/Functions.java | 39 +++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index eb4330bc76..53f53f2c52 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -82,7 +82,6 @@ import hudson.widgets.RenderOnDemandClosure; import java.io.File; import java.io.IOException; -import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.lang.management.LockInfo; @@ -1356,9 +1355,41 @@ public class Functions { } public static String printThrowable(Throwable t) { - StringWriter sw = new StringWriter(); - t.printStackTrace(new PrintWriter(sw)); - return sw.toString(); + StringBuilder s = new StringBuilder(); + doPrintStackTrace(s, t, null); + return s.toString(); + } + private static void doPrintStackTrace(StringBuilder s, Throwable t, Throwable higher) { + // TODO check if t overrides printStackTrace + // TODO handle suppressed exceptions + Throwable lower = t.getCause(); + if (lower != null) { + doPrintStackTrace(s, lower, t); + s.append("Caused: "); + } + String summary = t.toString(); + if (lower != null) { + String suffix = ": " + lower; + if (summary.endsWith(suffix)) { + summary = summary.substring(0, summary.length() - suffix.length()); + } + } + s.append(summary).append('\n'); + StackTraceElement[] trace = t.getStackTrace(); + int end = trace.length; + if (higher != null) { + StackTraceElement[] higherTrace = higher.getStackTrace(); + while (end > 0) { + int higherEnd = end + higherTrace.length - trace.length; + if (higherEnd <= 0 || !higherTrace[higherEnd - 1].equals(trace[end - 1])) { + break; + } + end--; + } + } + for (int i = 0; i < end; i++) { + s.append("\tat ").append(trace[i]).append('\n'); + } } /** -- GitLab From 358b9cee87c154e7ed9b689acfc35bf58b481614 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 27 Jun 2014 15:25:40 -0700 Subject: [PATCH 0002/2380] Adding a module to allow organizations to track usage of Jenkins within their domain. There is a privacy concern in having this information reported, but OTOH in a corporate network this seems like a reasonable compromise, and people really often do not have any clues where they are running. --- war/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/war/pom.xml b/war/pom.xml index d4d10cd1b0..6179ef6247 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -106,6 +106,11 @@ THE SOFTWARE. 2.3 test + + org.jenkins-ci.modules + domain-discovery + 1.0 + org.jenkins-ci.modules instance-identity -- GitLab From 9bb35a78b8298930adf978046e9f6c53c6853e27 Mon Sep 17 00:00:00 2001 From: Akshay Dayal Date: Fri, 17 Apr 2015 08:11:43 -0700 Subject: [PATCH 0003/2380] [JENKINS-26580] Initial implementation of JNLP3-connect protocol --- .../slaves/JnlpSlaveAgentProtocol3.java | 221 ++++++++++++++++++ .../jenkins/slaves/JnlpSlaveHandshake.java | 2 +- pom.xml | 2 +- 3 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java new file mode 100644 index 0000000000..5c4d348b00 --- /dev/null +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java @@ -0,0 +1,221 @@ +package jenkins.slaves; + +import hudson.AbortException; +import hudson.Extension; +import hudson.TcpSlaveAgentListener; +import hudson.Util; +import hudson.remoting.Channel; +import hudson.remoting.ChannelBuilder; +import hudson.remoting.SocketChannelStream; +import hudson.slaves.SlaveComputer; +import jenkins.AgentProtocol; +import jenkins.model.Jenkins; +import org.jenkinsci.remoting.engine.JnlpProtocol; +import org.jenkinsci.remoting.engine.JnlpProtocol3; +import org.jenkinsci.remoting.engine.jnlp3.ChannelCiphers; +import org.jenkinsci.remoting.engine.jnlp3.CipherUtils; +import org.jenkinsci.remoting.engine.jnlp3.HandshakeCiphers; +import org.jenkinsci.remoting.nio.NioChannelHub; + +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.inject.Inject; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.Socket; +import java.security.SecureRandom; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; + +/** + * Master-side implementation for JNLP3-connect protocol. + * + *

@see {@link JnlpProtocol3} for more details. + */ +@Extension +public class JnlpSlaveAgentProtocol3 extends AgentProtocol { + @Inject + NioChannelSelector hub; + + @Override + public String getName() { + return "JNLP3-connect"; + } + + @Override + public void handle(Socket socket) throws IOException, InterruptedException { + new Handler(hub.getHub(), socket).run(); + } + + static class Handler extends JnlpSlaveHandshake { + + public Handler(NioChannelHub hub, Socket socket) throws IOException { + super(hub,socket, + new DataInputStream(socket.getInputStream()), + new PrintWriter(new BufferedWriter( + new OutputStreamWriter(socket.getOutputStream(), "UTF-8")), true)); + } + + protected void run() throws IOException, InterruptedException { + request.load(new ByteArrayInputStream(in.readUTF().getBytes("UTF-8"))); + String nodeName = request.getProperty(JnlpProtocol3.SLAVE_NAME_KEY); + String encryptedChallenge = request.getProperty(JnlpProtocol3.CHALLENGE_KEY); + byte[] handshakeSpecKey = CipherUtils.keyFromString( + request.getProperty(JnlpProtocol3.HANDSHAKE_SPEC_KEY)); + String cookie = request.getProperty(JnlpProtocol3.COOKIE_KEY); + + SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName); + if(computer == null) { + error("Slave trying to register for invalid node: " + nodeName); + return; + } + String slaveSecret = computer.getJnlpMac(); + + HandshakeCiphers handshakeCiphers = null; + try { + handshakeCiphers = HandshakeCiphers.create(nodeName, slaveSecret, handshakeSpecKey); + } catch (Exception e) { + error("Failed to create handshake ciphers for node: " + nodeName); + return; + } + + String challenge = null; + try { + challenge = handshakeCiphers.decrypt(encryptedChallenge); + } catch (Exception e) { + throw new IOException("Unable to decrypt challenge", e); + } + if (!challenge.startsWith(JnlpProtocol3.CHALLENGE_PREFIX)) { + error("Received invalid challenge"); + return; + } + + // At this point the slave looks legit, check if we think they are already connected. + Channel oldChannel = computer.getChannel(); + if(oldChannel != null) { + if (cookie != null && cookie.equals(oldChannel.getProperty(COOKIE_NAME))) { + // We think we are currently connected, but this request proves that it's from + // the party we are supposed to be communicating to. so let the current one get + // disconnected + LOGGER.info("Disconnecting " + nodeName + + " as we are reconnected from the current peer"); + try { + computer.disconnect(new TcpSlaveAgentListener.ConnectionFromCurrentPeer()) + .get(15, TimeUnit.SECONDS); + } catch (ExecutionException e) { + throw new IOException("Failed to disconnect the current client",e); + } catch (TimeoutException e) { + throw new IOException("Failed to disconnect the current client",e); + } + } else { + error(nodeName + + " is already connected to this master. Rejecting this connection."); + return; + } + } + + // Send challenge response. + String challengeReverse = new StringBuilder( + challenge.substring(JnlpProtocol3.CHALLENGE_PREFIX.length())) + .reverse().toString(); + String challengeResponse = JnlpProtocol3.CHALLENGE_PREFIX + challengeReverse; + String encryptedChallengeResponse = null; + try { + encryptedChallengeResponse = handshakeCiphers.encrypt(challengeResponse); + } catch (Exception e) { + throw new IOException("Error encrypting challenge response", e); + } + out.println(encryptedChallengeResponse.getBytes("UTF-8").length); + out.print(encryptedChallengeResponse); + out.flush(); + + // If the slave accepted our challenge response it will send channel cipher keys. + String challengeVerificationMessage = in.readUTF(); + if (!challengeVerificationMessage.equals(JnlpProtocol.GREETING_SUCCESS)) { + error("Slave did not accept our challenge response"); + return; + } + String encryptedAesKeyString = in.readUTF(); + String encryptedSpecKeyString = in.readUTF(); + ChannelCiphers channelCiphers = null; + try { + String aesKeyString = handshakeCiphers.decrypt(encryptedAesKeyString); + String specKeyString = handshakeCiphers.decrypt(encryptedSpecKeyString); + channelCiphers = ChannelCiphers.create( + CipherUtils.keyFromString(aesKeyString), + CipherUtils.keyFromString(specKeyString)); + } catch (Exception e) { + error("Failed to decrypt channel cipher keys"); + return; + } + + String newCookie = generateCookie(); + try { + out.println(handshakeCiphers.encrypt(newCookie)); + } catch (Exception e) { + throw new IOException("Error encrypting cookie", e); + } + + Channel establishedChannel = jnlpConnect(computer, channelCiphers); + establishedChannel.setProperty(COOKIE_NAME, newCookie); + } + + protected Channel jnlpConnect( + SlaveComputer computer, ChannelCiphers channelCiphers) + throws InterruptedException, IOException { + final String nodeName = computer.getName(); + final OutputStream log = computer.openLogFile(); + PrintWriter logw = new PrintWriter(log,true); + logw.println("JNLP agent connected from "+ socket.getInetAddress()); + + try { + ChannelBuilder cb = createChannelBuilder(nodeName); + Channel channel = cb.withHeaderStream(log) + .build(new CipherInputStream(SocketChannelStream.in(socket), + channelCiphers.getDecryptCipher()), + new CipherOutputStream(SocketChannelStream.out(socket), + channelCiphers.getEncryptCipher())); + + computer.setChannel(channel, log, + new Channel.Listener() { + @Override + public void onClosed(Channel channel, IOException cause) { + if(cause != null) + LOGGER.log(Level.WARNING, + Thread.currentThread().getName() + " for + " + + nodeName + " terminated", cause); + try { + socket.close(); + } catch (IOException e) { + // Do nothing. + } + } + }); + return computer.getChannel(); + } catch (AbortException e) { + logw.println(e.getMessage()); + logw.println("Failed to establish the connection with the slave"); + throw e; + } catch (IOException e) { + logw.println("Failed to establish the connection with the slave " + nodeName); + e.printStackTrace(logw); + throw e; + } + } + + private String generateCookie() { + byte[] cookie = new byte[32]; + new SecureRandom().nextBytes(cookie); + return Util.toHexString(cookie); + } + } + + static final String COOKIE_NAME = JnlpSlaveAgentProtocol3.class.getName() + ".cookie"; +} diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveHandshake.java b/core/src/main/java/jenkins/slaves/JnlpSlaveHandshake.java index b7c01a239e..eb9499e1c3 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveHandshake.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveHandshake.java @@ -114,5 +114,5 @@ public class JnlpSlaveHandshake { } - private static final Logger LOGGER = Logger.getLogger(JnlpSlaveHandshake.class.getName()); + static final Logger LOGGER = Logger.getLogger(JnlpSlaveHandshake.class.getName()); } diff --git a/pom.xml b/pom.xml index bb2e89b46a..4a525155a5 100644 --- a/pom.xml +++ b/pom.xml @@ -176,7 +176,7 @@ THE SOFTWARE. org.jenkins-ci.main remoting - 2.50 + 2.51-SNAPSHOT -- GitLab From e9f5caa13fa1a3d1bf602ec9d67dac75f3310889 Mon Sep 17 00:00:00 2001 From: Akshay Dayal Date: Thu, 23 Apr 2015 02:24:40 -0700 Subject: [PATCH 0004/2380] [JENKINS-26580] Updated implementation of Jnlp3 protocol --- .../slaves/JnlpSlaveAgentProtocol3.java | 119 ++++++++++-------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java index 5c4d348b00..ce0a59beef 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java @@ -13,8 +13,8 @@ import jenkins.model.Jenkins; import org.jenkinsci.remoting.engine.JnlpProtocol; import org.jenkinsci.remoting.engine.JnlpProtocol3; import org.jenkinsci.remoting.engine.jnlp3.ChannelCiphers; -import org.jenkinsci.remoting.engine.jnlp3.CipherUtils; import org.jenkinsci.remoting.engine.jnlp3.HandshakeCiphers; +import org.jenkinsci.remoting.engine.jnlp3.Jnlp3Util; import org.jenkinsci.remoting.nio.NioChannelHub; import javax.crypto.CipherInputStream; @@ -28,6 +28,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.Socket; +import java.nio.charset.Charset; import java.security.SecureRandom; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -38,6 +39,8 @@ import java.util.logging.Level; * Master-side implementation for JNLP3-connect protocol. * *

@see {@link JnlpProtocol3} for more details. + * + * @author Akshay Dayal */ @Extension public class JnlpSlaveAgentProtocol3 extends AgentProtocol { @@ -59,45 +62,41 @@ public class JnlpSlaveAgentProtocol3 extends AgentProtocol { public Handler(NioChannelHub hub, Socket socket) throws IOException { super(hub,socket, new DataInputStream(socket.getInputStream()), - new PrintWriter(new BufferedWriter( - new OutputStreamWriter(socket.getOutputStream(), "UTF-8")), true)); + new PrintWriter(new BufferedWriter(new OutputStreamWriter( + socket.getOutputStream(), Charset.forName("UTF-8"))), true)); } protected void run() throws IOException, InterruptedException { - request.load(new ByteArrayInputStream(in.readUTF().getBytes("UTF-8"))); + // Get initiation information from slave. + request.load(new ByteArrayInputStream(in.readUTF().getBytes(Charset.forName("UTF-8")))); String nodeName = request.getProperty(JnlpProtocol3.SLAVE_NAME_KEY); - String encryptedChallenge = request.getProperty(JnlpProtocol3.CHALLENGE_KEY); - byte[] handshakeSpecKey = CipherUtils.keyFromString( - request.getProperty(JnlpProtocol3.HANDSHAKE_SPEC_KEY)); - String cookie = request.getProperty(JnlpProtocol3.COOKIE_KEY); + // Create handshake ciphers. SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName); if(computer == null) { error("Slave trying to register for invalid node: " + nodeName); return; } String slaveSecret = computer.getJnlpMac(); + HandshakeCiphers handshakeCiphers = HandshakeCiphers.create(nodeName, slaveSecret); - HandshakeCiphers handshakeCiphers = null; - try { - handshakeCiphers = HandshakeCiphers.create(nodeName, slaveSecret, handshakeSpecKey); - } catch (Exception e) { - error("Failed to create handshake ciphers for node: " + nodeName); + // Authenticate to the slave. + if (!authenticateToSlave(handshakeCiphers)) { return; } - String challenge = null; - try { - challenge = handshakeCiphers.decrypt(encryptedChallenge); - } catch (Exception e) { - throw new IOException("Unable to decrypt challenge", e); + // If there is a cookie decrypt it. + String cookie = null; + if (request.getProperty(JnlpProtocol3.COOKIE_KEY) != null) { + cookie = handshakeCiphers.decrypt(request.getProperty(JnlpProtocol3.COOKIE_KEY)); } - if (!challenge.startsWith(JnlpProtocol3.CHALLENGE_PREFIX)) { - error("Received invalid challenge"); + + // Validate the slave. + if (!validateSlave(handshakeCiphers)) { return; } - // At this point the slave looks legit, check if we think they are already connected. + // The slave is authenticated, see if its already connected. Channel oldChannel = computer.getChannel(); if(oldChannel != null) { if (cookie != null && cookie.equals(oldChannel.getProperty(COOKIE_NAME))) { @@ -121,50 +120,60 @@ public class JnlpSlaveAgentProtocol3 extends AgentProtocol { } } - // Send challenge response. - String challengeReverse = new StringBuilder( - challenge.substring(JnlpProtocol3.CHALLENGE_PREFIX.length())) - .reverse().toString(); - String challengeResponse = JnlpProtocol3.CHALLENGE_PREFIX + challengeReverse; - String encryptedChallengeResponse = null; - try { - encryptedChallengeResponse = handshakeCiphers.encrypt(challengeResponse); - } catch (Exception e) { - throw new IOException("Error encrypting challenge response", e); - } - out.println(encryptedChallengeResponse.getBytes("UTF-8").length); + // Send greeting and new cookie. + out.println(JnlpProtocol.GREETING_SUCCESS); + String newCookie = generateCookie(); + out.println(handshakeCiphers.encrypt(newCookie)); + + // Now get the channel cipher information. + String aesKeyString = handshakeCiphers.decrypt(in.readUTF()); + String specKeyString = handshakeCiphers.decrypt(in.readUTF()); + ChannelCiphers channelCiphers = ChannelCiphers.create( + Jnlp3Util.keyFromString(aesKeyString), + Jnlp3Util.keyFromString(specKeyString)); + + Channel establishedChannel = jnlpConnect(computer, channelCiphers); + establishedChannel.setProperty(COOKIE_NAME, newCookie); + } + + private boolean authenticateToSlave(HandshakeCiphers handshakeCiphers) throws IOException { + String challenge = handshakeCiphers.decrypt( + request.getProperty(JnlpProtocol3.CHALLENGE_KEY)); + + // Send slave challenge response. + String challengeResponse = Jnlp3Util.createChallengeResponse(challenge); + String encryptedChallengeResponse = handshakeCiphers.encrypt(challengeResponse); + out.println(encryptedChallengeResponse.getBytes(Charset.forName("UTF-8")).length); out.print(encryptedChallengeResponse); out.flush(); - // If the slave accepted our challenge response it will send channel cipher keys. + // If the slave accepted our challenge response send our challenge. String challengeVerificationMessage = in.readUTF(); if (!challengeVerificationMessage.equals(JnlpProtocol.GREETING_SUCCESS)) { error("Slave did not accept our challenge response"); - return; - } - String encryptedAesKeyString = in.readUTF(); - String encryptedSpecKeyString = in.readUTF(); - ChannelCiphers channelCiphers = null; - try { - String aesKeyString = handshakeCiphers.decrypt(encryptedAesKeyString); - String specKeyString = handshakeCiphers.decrypt(encryptedSpecKeyString); - channelCiphers = ChannelCiphers.create( - CipherUtils.keyFromString(aesKeyString), - CipherUtils.keyFromString(specKeyString)); - } catch (Exception e) { - error("Failed to decrypt channel cipher keys"); - return; + return false; } - String newCookie = generateCookie(); - try { - out.println(handshakeCiphers.encrypt(newCookie)); - } catch (Exception e) { - throw new IOException("Error encrypting cookie", e); + return true; + } + + private boolean validateSlave(HandshakeCiphers handshakeCiphers) throws IOException { + String masterChallenge = Jnlp3Util.generateChallenge(); + String encryptedMasterChallenge = handshakeCiphers.encrypt(masterChallenge); + out.println(encryptedMasterChallenge.getBytes(Charset.forName("UTF-8")).length); + out.print(encryptedMasterChallenge); + out.flush(); + + // Verify the challenge response from the slave. + String encryptedMasterChallengeResponse = in.readUTF(); + String masterChallengeResponse = handshakeCiphers.decrypt( + encryptedMasterChallengeResponse); + if (!Jnlp3Util.validateChallengeResponse(masterChallenge, masterChallengeResponse)) { + error("Incorrect master challenge response from slave"); + return false; } - Channel establishedChannel = jnlpConnect(computer, channelCiphers); - establishedChannel.setProperty(COOKIE_NAME, newCookie); + return true; } protected Channel jnlpConnect( -- GitLab From f73255968f1570ada189c28afd74466ccbfe377f Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 29 May 2015 07:54:36 -0400 Subject: [PATCH 0005/2380] Use system line separator. --- core/src/main/java/hudson/Functions.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 970e3b0f62..5fbe79bb91 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -151,6 +151,7 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import hudson.util.RunList; import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.io.IOUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -1411,7 +1412,7 @@ public class Functions { summary = summary.substring(0, summary.length() - suffix.length()); } } - s.append(summary).append('\n'); + s.append(summary).append(IOUtils.LINE_SEPARATOR); StackTraceElement[] trace = t.getStackTrace(); int end = trace.length; if (higher != null) { @@ -1425,7 +1426,7 @@ public class Functions { } } for (int i = 0; i < end; i++) { - s.append("\tat ").append(trace[i]).append('\n'); + s.append("\tat ").append(trace[i]).append(IOUtils.LINE_SEPARATOR); } } -- GitLab From 47cb028ac308b5a2a12d1859408a331154d2c77e Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 29 May 2015 08:02:59 -0400 Subject: [PATCH 0006/2380] Added Javadoc. --- core/src/main/java/hudson/Functions.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 5fbe79bb91..a285476b7a 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -150,7 +150,10 @@ import org.kohsuke.stapler.jelly.InternationalizedStringExpression.RawHtmlArgume import com.google.common.base.Predicate; import com.google.common.base.Predicates; import hudson.util.RunList; +import java.io.PrintWriter; import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import org.apache.commons.io.IOUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -1392,12 +1395,19 @@ public class Functions { return value!=null ? value : defaultValue; } - public static String printThrowable(Throwable t) { + /** + * Prints a stack trace from an exception into a readable form. + * Unlike {@link Throwable#printStackTrace(PrintWriter)}, this implementation follows the suggestion of JDK-6507809 + * to produce a linear trace even when {@link Throwable#getCause} is used. + * @param t any throwable + * @return generally a multiline string ending in a (platform-specific) newline + */ + public static @Nonnull String printThrowable(@Nonnull Throwable t) { StringBuilder s = new StringBuilder(); doPrintStackTrace(s, t, null); return s.toString(); } - private static void doPrintStackTrace(StringBuilder s, Throwable t, Throwable higher) { + private static void doPrintStackTrace(@Nonnull StringBuilder s, @Nonnull Throwable t, @CheckForNull Throwable higher) { // TODO check if t overrides printStackTrace // TODO handle suppressed exceptions Throwable lower = t.getCause(); -- GitLab From 6bca813768e763a75a51f8f4f031574f2e60b764 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 29 May 2015 08:42:30 -0400 Subject: [PATCH 0007/2380] Starting to write tests for printThrowable. --- core/src/test/java/hudson/FunctionsTest.java | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/core/src/test/java/hudson/FunctionsTest.java b/core/src/test/java/hudson/FunctionsTest.java index 45f845f181..26bcfcddfe 100644 --- a/core/src/test/java/hudson/FunctionsTest.java +++ b/core/src/test/java/hudson/FunctionsTest.java @@ -35,7 +35,10 @@ import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import jenkins.model.Jenkins; +import org.apache.commons.io.IOUtils; import static org.junit.Assert.*; import org.junit.Ignore; import org.junit.Test; @@ -339,4 +342,44 @@ public class FunctionsTest { assertEquals("Bad input <xml/>\n", Functions.printLogRecordHtml(lr, null)[3]); } + @Issue("JDK-6507809") + @Test public void printThrowable() throws Exception { + Throwable x; + try { + method1(); + throw new AssertionError(); + } catch (IllegalStateException _x) { + x = _x; + } + String stack = Functions.printThrowable(x).replace(IOUtils.LINE_SEPARATOR, "\n"); + Matcher m = Pattern.compile( + "java\\.lang\\.NullPointerException: oops\n" + + "\tat hudson\\.FunctionsTest\\.method2\\(FunctionsTest\\.java:(\\d+)\\)\n" + + "\tat hudson\\.FunctionsTest\\.method1\\(FunctionsTest\\.java:(\\d+)\\)\n" + + "Caused: java\\.lang\\.IllegalStateException\n" + + "\tat hudson\\.FunctionsTest\\.method1\\(FunctionsTest\\.java:(\\d+)\\)\n" + + "\tat hudson\\.FunctionsTest\\.printThrowable\\(FunctionsTest\\.java:\\d+\\)\n" + + "(\tat .+\n)+").matcher(stack); + assertTrue(stack, m.matches()); + int throwNPE = Integer.parseInt(m.group(1)); + int callToMethod2 = Integer.parseInt(m.group(2)); + int throwISE = Integer.parseInt(m.group(3)); + assertEquals(callToMethod2 + 2, throwISE); + assertEquals(callToMethod2 + 6, throwNPE); + // TODO assert display of new WrapperException("more info", wrapped) + // TODO assert display of new WrapperException("more info: " + wrapped, wrapped) + // TODO assert that full stack is preserved if wrapped does not share a common stack (e.g., comes from an executor service thread) + } + // Do not change line spacing of these: + private static void method1() { + try { + method2(); + } catch (Exception x) { + throw new IllegalStateException(x); + } + } + private static void method2() { + throw new NullPointerException("oops"); + } + } -- GitLab From d69ae563eb16e236410fca464862a6e768c1ac8d Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 29 Oct 2015 18:57:03 -0400 Subject: [PATCH 0008/2380] Call hpi:record-core-location. --- core/pom.xml | 1 + pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/pom.xml b/core/pom.xml index 556990ce9a..fe1af5e25f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -640,6 +640,7 @@ THE SOFTWARE. generate-taglib-interface + record-core-location diff --git a/pom.xml b/pom.xml index 01c8df0f93..5090c699a9 100644 --- a/pom.xml +++ b/pom.xml @@ -511,7 +511,7 @@ THE SOFTWARE. org.jenkins-ci.tools maven-hpi-plugin - 1.114 + 1.116-SNAPSHOT org.apache.maven.plugins -- GitLab From 709fe5b9aa154b7e994dc90987ccb8d17dfffcc6 Mon Sep 17 00:00:00 2001 From: Johannes Ernst Date: Tue, 17 Nov 2015 01:38:51 +0000 Subject: [PATCH 0009/2380] Allow setting of system properties from context.xml in addition to setting from command-line. Accomplished by centralizing calls to System.getProperty(String) and related into new file SystemProperties.java. There, we first check for existence of system property; if not, we look for property in context.xml This is done for "application" properties (like hudson.DNSMultiCast.disabled) but not for java properties (like user.name) --- .../java/hudson/ClassicPluginStrategy.java | 4 +- core/src/main/java/hudson/DNSMultiCast.java | 2 +- core/src/main/java/hudson/FilePath.java | 2 +- core/src/main/java/hudson/Functions.java | 14 +- .../main/java/hudson/LocalPluginManager.java | 2 +- core/src/main/java/hudson/Main.java | 4 +- core/src/main/java/hudson/PluginManager.java | 4 +- .../main/java/hudson/SystemProperties.java | 220 ++++++++++++++++++ .../java/hudson/TcpSlaveAgentListener.java | 4 +- .../main/java/hudson/UDPBroadcastThread.java | 2 +- core/src/main/java/hudson/Util.java | 4 +- core/src/main/java/hudson/WebAppMain.java | 6 +- core/src/main/java/hudson/cli/CLICommand.java | 3 +- .../main/java/hudson/init/InitStrategy.java | 3 +- .../main/java/hudson/lifecycle/Lifecycle.java | 5 +- .../lifecycle/WindowsInstallerLink.java | 5 +- .../main/java/hudson/model/AbstractBuild.java | 3 +- core/src/main/java/hudson/model/Computer.java | 5 +- .../java/hudson/model/DownloadService.java | 5 +- .../hudson/model/FullDuplexHttpChannel.java | 3 +- core/src/main/java/hudson/model/Item.java | 3 +- .../java/hudson/model/LoadStatistics.java | 5 +- core/src/main/java/hudson/model/Queue.java | 3 +- core/src/main/java/hudson/model/Run.java | 7 +- core/src/main/java/hudson/model/Slave.java | 3 +- core/src/main/java/hudson/model/TreeView.java | 3 +- .../main/java/hudson/model/UpdateCenter.java | 5 +- .../main/java/hudson/model/UpdateSite.java | 3 +- .../java/hudson/model/UsageStatistics.java | 3 +- core/src/main/java/hudson/model/User.java | 2 +- core/src/main/java/hudson/model/ViewJob.java | 3 +- .../hudson/model/WorkspaceCleanupThread.java | 7 +- .../java/hudson/model/queue/BackFiller.java | 3 +- .../java/hudson/os/solaris/ZFSInstaller.java | 5 +- .../java/hudson/scheduler/BaseParser.java | 3 +- .../security/csrf/DefaultCrumbIssuer.java | 3 +- .../java/hudson/slaves/ChannelPinger.java | 3 +- .../hudson/slaves/CloudRetentionStrategy.java | 3 +- .../slaves/ConnectionActivityMonitor.java | 3 +- .../java/hudson/slaves/NodeProvisioner.java | 9 +- .../java/hudson/slaves/WorkspaceList.java | 3 +- .../java/hudson/tasks/ArtifactArchiver.java | 3 +- .../main/java/hudson/tasks/Fingerprinter.java | 3 +- .../hudson/util/CharacterEncodingFilter.java | 5 +- .../main/java/hudson/util/ProcessTree.java | 5 +- .../hudson/util/RingBufferLogHandler.java | 3 +- core/src/main/java/hudson/util/Secret.java | 3 +- .../java/hudson/widgets/HistoryWidget.java | 3 +- .../main/java/jenkins/InitReactorRunner.java | 3 +- .../java/jenkins/model/Configuration.java | 5 +- core/src/main/java/jenkins/model/Jenkins.java | 3 +- .../jenkins/model/lazy/BuildReference.java | 3 +- .../jenkins/security/ApiTokenProperty.java | 3 +- .../BasicHeaderRealPasswordAuthenticator.java | 3 +- .../security/FrameOptionsPageDecorator.java | 3 +- .../jenkins/security/SecureRequester.java | 3 +- .../s2m/CallableDirectionChecker.java | 3 +- .../security/s2m/DefaultFilePathFilter.java | 3 +- .../jenkins/slaves/NioChannelSelector.java | 3 +- .../jenkins/slaves/StandardOutputSwapper.java | 3 +- .../main/java/jenkins/util/xml/XMLUtils.java | 3 +- 61 files changed, 358 insertions(+), 90 deletions(-) create mode 100644 core/src/main/java/hudson/SystemProperties.java diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index b90e6fcfda..5cd2e4857a 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -802,7 +802,7 @@ public class ClassicPluginStrategy implements PluginStrategy { } } - public static boolean useAntClassLoader = Boolean.getBoolean(ClassicPluginStrategy.class.getName()+".useAntClassLoader"); + public static boolean useAntClassLoader = SystemProperties.getBoolean(ClassicPluginStrategy.class.getName()+".useAntClassLoader"); private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName()); - public static boolean DISABLE_TRANSFORMER = Boolean.getBoolean(ClassicPluginStrategy.class.getName()+".noBytecodeTransformer"); + public static boolean DISABLE_TRANSFORMER = SystemProperties.getBoolean(ClassicPluginStrategy.class.getName()+".noBytecodeTransformer"); } diff --git a/core/src/main/java/hudson/DNSMultiCast.java b/core/src/main/java/hudson/DNSMultiCast.java index 8d029b6ae3..4d2eb1816d 100644 --- a/core/src/main/java/hudson/DNSMultiCast.java +++ b/core/src/main/java/hudson/DNSMultiCast.java @@ -86,5 +86,5 @@ public class DNSMultiCast implements Closeable { private static final Logger LOGGER = Logger.getLogger(DNSMultiCast.class.getName()); - public static boolean disabled = Boolean.getBoolean(DNSMultiCast.class.getName()+".disabled"); + public static boolean disabled = SystemProperties.getBoolean(DNSMultiCast.class.getName()+".disabled"); } diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 9690c39cd0..536cfe679b 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -2354,7 +2354,7 @@ public final class FilePath implements Serializable { * Default bound for {@link #validateAntFileMask(String, int, boolean)}. * @since 1.592 */ - public static int VALIDATE_ANT_FILE_MASK_BOUND = Integer.getInteger(FilePath.class.getName() + ".VALIDATE_ANT_FILE_MASK_BOUND", 10000); + public static int VALIDATE_ANT_FILE_MASK_BOUND = SystemProperties.getInteger(FilePath.class.getName() + ".VALIDATE_ANT_FILE_MASK_BOUND", 10000); /** * Like {@link #validateAntFileMask(String)} but performing only a bounded number of operations. diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 9f74f42b20..ca26fb0d6f 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -561,7 +561,7 @@ public class Functions { /** * Set to true if you need to use the debug version of YUI. */ - public static boolean DEBUG_YUI = Boolean.getBoolean("debug.YUI"); + public static boolean DEBUG_YUI = SystemProperties.getBoolean("debug.YUI"); /** * Creates a sub map by using the given range (both ends inclusive). @@ -616,7 +616,7 @@ public class Functions { response.addCookie(c); } if (refresh) { - response.addHeader("Refresh", System.getProperty("hudson.Functions.autoRefreshSeconds", "10")); + response.addHeader("Refresh", SystemProperties.getProperty("hudson.Functions.autoRefreshSeconds", "10")); } } @@ -838,7 +838,7 @@ public class Functions { */ public static String getFooterURL() { if(footerURL == null) { - footerURL = System.getProperty("hudson.footerURL"); + footerURL = SystemProperties.getProperty("hudson.footerURL"); if(StringUtils.isBlank(footerURL)) { footerURL = "http://jenkins-ci.org/"; } @@ -1567,10 +1567,6 @@ public class Functions { return projectName; } - public String getSystemProperty(String key) { - return System.getProperty(key); - } - /** * Obtains the host name of the Hudson server that clients can use to talk back to. *

@@ -1772,7 +1768,7 @@ public class Functions { * the permission can't be configured in the security screen). Got it?

*/ public static boolean isArtifactsPermissionEnabled() { - return Boolean.getBoolean("hudson.security.ArtifactsPermission"); + return SystemProperties.getBoolean("hudson.security.ArtifactsPermission"); } /** @@ -1787,7 +1783,7 @@ public class Functions { * control on the "Wipe Out Workspace" action.

*/ public static boolean isWipeOutPermissionEnabled() { - return Boolean.getBoolean("hudson.security.WipeOutPermission"); + return SystemProperties.getBoolean("hudson.security.WipeOutPermission"); } public static String createRenderOnDemandProxy(JellyContext context, String attributesToCapture) { diff --git a/core/src/main/java/hudson/LocalPluginManager.java b/core/src/main/java/hudson/LocalPluginManager.java index 4cfbf6ad93..62aee91afa 100644 --- a/core/src/main/java/hudson/LocalPluginManager.java +++ b/core/src/main/java/hudson/LocalPluginManager.java @@ -60,7 +60,7 @@ public class LocalPluginManager extends PluginManager { @Override protected Collection loadBundledPlugins() { // this is used in tests, when we want to override the default bundled plugins with .jpl (or .hpl) versions - if (System.getProperty("hudson.bundled.plugins") != null) { + if (SystemProperties.getProperty("hudson.bundled.plugins") != null) { return Collections.emptySet(); } diff --git a/core/src/main/java/hudson/Main.java b/core/src/main/java/hudson/Main.java index cf8a168ba1..c1aaab9786 100644 --- a/core/src/main/java/hudson/Main.java +++ b/core/src/main/java/hudson/Main.java @@ -218,10 +218,10 @@ public class Main { /** * Set to true if we are running inside "mvn hpi:run" or "mvn hudson-dev:run" */ - public static boolean isDevelopmentMode = Boolean.getBoolean(Main.class.getName()+".development"); + public static boolean isDevelopmentMode = SystemProperties.getBoolean(Main.class.getName()+".development"); /** * Time out for socket connection to Hudson. */ - public static final int TIMEOUT = Integer.getInteger(Main.class.getName()+".timeout",15000); + public static final int TIMEOUT = SystemProperties.getInteger(Main.class.getName()+".timeout",15000); } diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 2af02aa4c5..d96c68dc20 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -633,7 +633,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * Creates a hudson.PluginStrategy, looking at the corresponding system property. */ protected PluginStrategy createPluginStrategy() { - String strategyName = System.getProperty(PluginStrategy.class.getName()); + String strategyName = SystemProperties.getProperty(PluginStrategy.class.getName()); if (strategyName != null) { try { Class klazz = getClass().getClassLoader().loadClass(strategyName); @@ -1249,7 +1249,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName()); - public static boolean FAST_LOOKUP = !Boolean.getBoolean(PluginManager.class.getName()+".noFastLookup"); + public static boolean FAST_LOOKUP = !SystemProperties.getBoolean(PluginManager.class.getName()+".noFastLookup"); public static final Permission UPLOAD_PLUGINS = new Permission(Jenkins.PERMISSIONS, "UploadPlugins", Messages._PluginManager_UploadPluginsPermission_Description(),Jenkins.ADMINISTER,PermissionScope.JENKINS); public static final Permission CONFIGURE_UPDATECENTER = new Permission(Jenkins.PERMISSIONS, "ConfigureUpdateCenter", Messages._PluginManager_ConfigureUpdateCenterPermission_Description(),Jenkins.ADMINISTER,PermissionScope.JENKINS); diff --git a/core/src/main/java/hudson/SystemProperties.java b/core/src/main/java/hudson/SystemProperties.java new file mode 100644 index 0000000000..88fa83163f --- /dev/null +++ b/core/src/main/java/hudson/SystemProperties.java @@ -0,0 +1,220 @@ +/* + * The MIT License + * + * Copyright 2015 Johannes Ernst, http://upon2020.com/. + * + * 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 hudson; + +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.servlet.ServletContext; + +/** + * Centralizes calls to System.getProperty() and related calls. + * This allows us to get values not just from environment variables but also from + * the ServletContext, so things like hudson.DNSMultiCast.disabled can be set + * in context.xml and the app server's boot script does not have to be changed. + * + * While it looks like it on first glamce, this cannot be mapped to EnvVars.java + * because EnvVars.java is only for build variables, not Jenkins itself variables. + * + * This should be invoked for hudson parameters (e.g. hudson.DNSMultiCast.disabled), + * but not for system parameters (e.g. os.name). + * + * @author Johannes Ernst + */ +public class SystemProperties { + /** + * The ServletContext to get the init parameters from. + */ + private static ServletContext theContext; + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(SystemProperties.class.getName()); + + /** + * Gets the system property indicated by the specified key. + * This behaves just like System.getProperty(String), except that it + * also consults the ServletContext's init parameters. + * + * @param key the name of the system property. + * @return the string value of the system property, + * or null if there is no property with that key. + * + * @exception SecurityException if a security manager exists and its + * checkPropertyAccess method doesn't allow + * access to the specified system property. + * @exception NullPointerException if key is + * null. + * @exception IllegalArgumentException if key is empty. + */ + public static String getProperty(String key) { + String value = System.getProperty(key); // keep passing on any exceptions + if( value != null ) { + if( LOGGER.isLoggable(Level.INFO )) { + LOGGER.log(Level.INFO, "Property (system): {0} => {1}", new Object[]{ key, value }); + } + } else if( theContext != null ) { + value = theContext.getInitParameter(key); + if( value != null ) { + if( LOGGER.isLoggable(Level.INFO )) { + LOGGER.log(Level.INFO, "Property (context): {0} => {1}", new Object[]{ key, value }); + } + } + } else { + if( LOGGER.isLoggable(Level.INFO )) { + LOGGER.log(Level.INFO, "Property (not found): {0} => {1}", new Object[]{ key, value }); + } + } + + return value; + } + + /** + * Gets the system property indicated by the specified key. + * This behaves just like System.getProperty(String), except that it + * also consults the ServletContext's init parameters. + * + * @param key the name of the system property. + * @param def a default value. + * @return the string value of the system property, + * or null if there is no property with that key. + * + * @exception SecurityException if a security manager exists and its + * checkPropertyAccess method doesn't allow + * access to the specified system property. + * @exception NullPointerException if key is + * null. + * @exception IllegalArgumentException if key is empty. + */ + public static String getProperty(String key, String def) { + String value = System.getProperty(key); // keep passing on any exceptions + if( value != null ) { + if( LOGGER.isLoggable(Level.INFO )) { + LOGGER.log(Level.INFO, "Property (system): {0} => {1}", new Object[]{ key, value }); + } + } else if( theContext != null ) { + value = theContext.getInitParameter(key); + if( value != null ) { + if( LOGGER.isLoggable(Level.INFO )) { + LOGGER.log(Level.INFO, "Property (context): {0} => {1}", new Object[]{ key, value }); + } + } + } + if( value == null ) { + value = def; + if( LOGGER.isLoggable(Level.INFO )) { + LOGGER.log(Level.INFO, "Property (default): {0} => {1}", new Object[]{ key, value }); + } + } + return value; + } + + /** + * Returns {@code true} if the system property + * named by the argument exists and is equal to the string + * {@code "true"}. If the system property does not exist, return + * {@code "true"} if a property by this name exists in the servletcontext + * and is equal to the string {@code "true"}. + * + * This behaves just like Boolean.getBoolean(String), except that it + * also consults the ServletContext's init parameters. + * + * @param name the system property name. + * @return the {@code boolean} value of the system property. + */ + public static boolean getBoolean(String name) { + return getBoolean(name, false); + } + + /** + * Returns {@code true} if the system property + * named by the argument exists and is equal to the string + * {@code "true"}. If the system property does not exist, return + * {@code "true"} if a property by this name exists in the servletcontext + * and is equal to the string {@code "true"}. If that property does not + * exist either, return the default value. + * + * This behaves just like Boolean.getBoolean(String), except that it + * also consults the ServletContext's init parameters. + * + * @param name the system property name. + * @param def a default value. + * @return the {@code boolean} value of the system property. + */ + public static boolean getBoolean(String name, boolean def) { + String v = getProperty(name); + + if (v != null) { + return Boolean.parseBoolean(v); + } + return def; + } + + /** + * Determines the integer value of the system property with the + * specified name. + * + * This behaves just like Integer.getInteger(String), except that it + * also consults the ServletContext's init parameters. + * + * @param name property name. + * @return the {@code Integer} value of the property. + */ + public static Integer getInteger(String name) { + return getInteger( name, null ); + } + + /** + * Determines the integer value of the system property with the + * specified name. + * + * This behaves just like Integer.getInteger(String), except that it + * also consults the ServletContext's init parameters. If neither exist, + * return the default value. + * + * @param name property name. + * @param def a default value. + * @return the {@code Integer} value of the property. + */ + + public static Integer getInteger(String name, Integer def) { + String v = getProperty(name); + + if (v != null) { + try { + return Integer.decode(v); + } catch (NumberFormatException e) { + } + } + return def; + } + /** + * Invoked by WebAppMain, tells us where to get the init parameters from. + * + * @param context the ServletContext obtained from contextInitialized + */ + public static void initialize(ServletContext context) { + theContext = context; + } +} diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java index 46d99076ae..7fe14e0496 100644 --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java @@ -203,7 +203,7 @@ public final class TcpSlaveAgentListener extends Thread { * * TODO: think about how to expose this (including whether this needs to be exposed at all.) */ - public static String CLI_HOST_NAME = System.getProperty(TcpSlaveAgentListener.class.getName()+".hostName"); + public static String CLI_HOST_NAME = SystemProperties.getProperty(TcpSlaveAgentListener.class.getName()+".hostName"); /** * Port number that we advertise the CLI client to connect to. @@ -215,7 +215,7 @@ public final class TcpSlaveAgentListener extends Thread { * * @since 1.611 */ - public static Integer CLI_PORT = Integer.getInteger(TcpSlaveAgentListener.class.getName()+".port"); + public static Integer CLI_PORT = SystemProperties.getInteger(TcpSlaveAgentListener.class.getName()+".port"); } /* diff --git a/core/src/main/java/hudson/UDPBroadcastThread.java b/core/src/main/java/hudson/UDPBroadcastThread.java index ebc6eb9e1b..bb9b5aa708 100644 --- a/core/src/main/java/hudson/UDPBroadcastThread.java +++ b/core/src/main/java/hudson/UDPBroadcastThread.java @@ -125,7 +125,7 @@ public class UDPBroadcastThread extends Thread { interrupt(); } - public static final int PORT = Integer.getInteger("hudson.udp",33848); + public static final int PORT = SystemProperties.getInteger("hudson.udp",33848); private static final Logger LOGGER = Logger.getLogger(UDPBroadcastThread.class.getName()); diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index e9297a83eb..c3c649dc48 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -1525,7 +1525,7 @@ public class Util { /** * On Unix environment that cannot run "ln", set this to true. */ - public static boolean NO_SYMLINK = Boolean.getBoolean(Util.class.getName()+".noSymLink"); + public static boolean NO_SYMLINK = SystemProperties.getBoolean(Util.class.getName()+".noSymLink"); - public static boolean SYMLINK_ESCAPEHATCH = Boolean.getBoolean(Util.class.getName()+".symlinkEscapeHatch"); + public static boolean SYMLINK_ESCAPEHATCH = SystemProperties.getBoolean(Util.class.getName()+".symlinkEscapeHatch"); } diff --git a/core/src/main/java/hudson/WebAppMain.java b/core/src/main/java/hudson/WebAppMain.java index 11d438d5ab..99bdef5641 100644 --- a/core/src/main/java/hudson/WebAppMain.java +++ b/core/src/main/java/hudson/WebAppMain.java @@ -117,6 +117,8 @@ public class WebAppMain implements ServletContextListener { installLogger(); + SystemProperties.initialize( event.getServletContext() ); + markCookieAsHttpOnly(context); final FileAndDescription describedHomeDir = getHomeDir(event); @@ -357,9 +359,9 @@ public class WebAppMain implements ServletContextListener { // next the system property for (String name : HOME_NAMES) { - String sysProp = System.getProperty(name); + String sysProp = SystemProperties.getProperty(name); if(sysProp!=null) - return new FileAndDescription(new File(sysProp.trim()),"System.getProperty(\""+name+"\")"); + return new FileAndDescription(new File(sysProp.trim()),"SystemProperties.getProperty(\""+name+"\")"); } // look at the env var next diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index 2c68b0f6ff..d18a66464c 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -29,6 +29,7 @@ import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.cli.declarative.CLIMethod; import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson; +import hudson.SystemProperties; import hudson.cli.declarative.OptionHandlerExtension; import jenkins.model.Jenkins; import hudson.remoting.Callable; @@ -418,7 +419,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { } public String call() throws IOException { - return System.getProperty(name); + return SystemProperties.getProperty(name); } private static final long serialVersionUID = 1L; diff --git a/core/src/main/java/hudson/init/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java index 7b8497405d..1ba014597d 100644 --- a/core/src/main/java/hudson/init/InitStrategy.java +++ b/core/src/main/java/hudson/init/InitStrategy.java @@ -15,6 +15,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import hudson.PluginManager; +import hudson.SystemProperties; import hudson.util.DirScanner; import hudson.util.FileVisitor; import hudson.util.Service; @@ -84,7 +85,7 @@ public class InitStrategy { * TODO: maven-hpi-plugin should inject its own InitStrategy instead of having this in the core. */ protected void getBundledPluginsFromProperty(final List r) { - String hplProperty = System.getProperty("hudson.bundled.plugins"); + String hplProperty = SystemProperties.getProperty("hudson.bundled.plugins"); if (hplProperty != null) { for (String hplLocation : hplProperty.split(",")) { File hpl = new File(hplLocation.trim()); diff --git a/core/src/main/java/hudson/lifecycle/Lifecycle.java b/core/src/main/java/hudson/lifecycle/Lifecycle.java index d314742f85..b768cc545c 100644 --- a/core/src/main/java/hudson/lifecycle/Lifecycle.java +++ b/core/src/main/java/hudson/lifecycle/Lifecycle.java @@ -25,6 +25,7 @@ package hudson.lifecycle; import hudson.ExtensionPoint; import hudson.Functions; +import hudson.SystemProperties; import hudson.Util; import jenkins.model.Jenkins; @@ -57,7 +58,7 @@ public abstract class Lifecycle implements ExtensionPoint { public synchronized static Lifecycle get() { if(INSTANCE==null) { Lifecycle instance; - String p = System.getProperty("hudson.lifecycle"); + String p = SystemProperties.getProperty("hudson.lifecycle"); if(p!=null) { try { ClassLoader cl = Jenkins.getInstance().getPluginManager().uberClassLoader; @@ -119,7 +120,7 @@ public abstract class Lifecycle implements ExtensionPoint { * to a newer version. */ public File getHudsonWar() { - String war = System.getProperty("executable-war"); + String war = SystemProperties.getProperty("executable-war"); if(war!=null && new File(war).exists()) return new File(war); return null; diff --git a/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java b/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java index 4401b43649..afef84e84e 100644 --- a/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java +++ b/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java @@ -34,6 +34,7 @@ import hudson.util.jna.Shell32; import jenkins.model.Jenkins; import hudson.AbortException; import hudson.Extension; +import hudson.SystemProperties; import hudson.util.StreamTaskListener; import hudson.util.jna.DotNet; import org.apache.commons.io.IOUtils; @@ -257,14 +258,14 @@ public class WindowsInstallerLink extends ManagementLink { // this system property is set by the launcher when we run "java -jar jenkins.war" // and this is how we know where is jenkins.war. - String war = System.getProperty("executable-war"); + String war = SystemProperties.getProperty("executable-war"); if(war!=null && new File(war).exists()) { WindowsInstallerLink link = new WindowsInstallerLink(new File(war)); // in certain situations where we know the user is just trying Jenkins (like when Jenkins is launched // from JNLP), also put this link on the navigation bar to increase // visibility - if(System.getProperty(WindowsInstallerLink.class.getName()+".prominent")!=null) + if(SystemProperties.getProperty(WindowsInstallerLink.class.getName()+".prominent")!=null) Jenkins.getInstance().getActions().add(link); return link; diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java index e9c4626706..bb55d86198 100644 --- a/core/src/main/java/hudson/model/AbstractBuild.java +++ b/core/src/main/java/hudson/model/AbstractBuild.java @@ -30,6 +30,7 @@ import hudson.EnvVars; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; +import hudson.SystemProperties; import hudson.console.ModelHyperlinkNote; import hudson.model.Fingerprint.BuildPtr; import hudson.model.Fingerprint.RangeSet; @@ -106,7 +107,7 @@ public abstract class AbstractBuild

,R extends Abs /** * Set if we want the blame information to flow from upstream to downstream build. */ - private static final boolean upstreamCulprits = Boolean.getBoolean("hudson.upstreamCulprits"); + private static final boolean upstreamCulprits = SystemProperties.getBoolean("hudson.upstreamCulprits"); /** * Name of the slave this project was built on. diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 547e51c753..f221a396d1 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -29,6 +29,7 @@ import edu.umd.cs.findbugs.annotations.When; import hudson.EnvVars; import hudson.Extension; import hudson.Launcher.ProcStarter; +import hudson.SystemProperties; import hudson.Util; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; @@ -1272,7 +1273,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces private static class GetFallbackName extends MasterToSlaveCallable { public String call() throws IOException { - return System.getProperty("host.name"); + return SystemProperties.getProperty("host.name"); } private static final long serialVersionUID = 1L; } @@ -1693,7 +1694,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces /** * @since 1.532 */ - public static final Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._Computer_ExtendedReadPermission_Description(), CONFIGURE, Boolean.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.COMPUTER}); + public static final Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._Computer_ExtendedReadPermission_Description(), CONFIGURE, SystemProperties.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.COMPUTER}); public static final Permission DELETE = new Permission(PERMISSIONS,"Delete", Messages._Computer_DeletePermission_Description(), Permission.DELETE, PermissionScope.COMPUTER); public static final Permission CREATE = new Permission(PERMISSIONS,"Create", Messages._Computer_CreatePermission_Description(), Permission.CREATE, PermissionScope.COMPUTER); public static final Permission DISCONNECT = new Permission(PERMISSIONS,"Disconnect", Messages._Computer_DisconnectPermission_Description(), Jenkins.ADMINISTER, PermissionScope.COMPUTER); diff --git a/core/src/main/java/hudson/model/DownloadService.java b/core/src/main/java/hudson/model/DownloadService.java index 905199581c..359ff186bf 100644 --- a/core/src/main/java/hudson/model/DownloadService.java +++ b/core/src/main/java/hudson/model/DownloadService.java @@ -28,6 +28,7 @@ import hudson.ExtensionList; import hudson.ExtensionListListener; import hudson.ExtensionPoint; import hudson.ProxyConfiguration; +import hudson.SystemProperties; import hudson.init.InitMilestone; import hudson.init.Initializer; import hudson.util.FormValidation; @@ -408,7 +409,7 @@ public class DownloadService extends PageDecorator { Long.getLong(Downloadable.class.getName()+".defaultInterval", DAYS.toMillis(1)); } - public static boolean neverUpdate = Boolean.getBoolean(DownloadService.class.getName()+".never"); + public static boolean neverUpdate = SystemProperties.getBoolean(DownloadService.class.getName()+".never"); /** * May be used to temporarily disable signature checking on {@link DownloadService} and {@link UpdateCenter}. @@ -416,6 +417,6 @@ public class DownloadService extends PageDecorator { * Should only be used when {@link DownloadSettings#isUseBrowser}; * disabling signature checks for in-browser downloads is very dangerous as unprivileged users could submit spoofed metadata! */ - public static boolean signatureCheck = !Boolean.getBoolean(DownloadService.class.getName()+".noSignatureCheck"); + public static boolean signatureCheck = !SystemProperties.getBoolean(DownloadService.class.getName()+".noSignatureCheck"); } diff --git a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java index e967188901..872c6f9dc3 100644 --- a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java +++ b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java @@ -23,6 +23,7 @@ */ package hudson.model; +import hudson.SystemProperties; import hudson.remoting.Channel; import hudson.remoting.PingThread; import hudson.remoting.Channel.Mode; @@ -153,7 +154,7 @@ abstract public class FullDuplexHttpChannel { * Set to true if the servlet container doesn't support chunked encoding. */ @Restricted(NoExternalUse.class) - public static boolean DIY_CHUNKING = Boolean.getBoolean("hudson.diyChunking"); + public static boolean DIY_CHUNKING = SystemProperties.getBoolean("hudson.diyChunking"); /** * Controls the time out of waiting for the 2nd HTTP request to arrive. diff --git a/core/src/main/java/hudson/model/Item.java b/core/src/main/java/hudson/model/Item.java index 94ee4181de..50a8b23b38 100644 --- a/core/src/main/java/hudson/model/Item.java +++ b/core/src/main/java/hudson/model/Item.java @@ -25,6 +25,7 @@ package hudson.model; import hudson.Functions; +import hudson.SystemProperties; import hudson.security.PermissionScope; import org.kohsuke.stapler.StaplerRequest; @@ -226,7 +227,7 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont Permission CONFIGURE = new Permission(PERMISSIONS, "Configure", Messages._Item_CONFIGURE_description(), Permission.CONFIGURE, PermissionScope.ITEM); Permission READ = new Permission(PERMISSIONS, "Read", Messages._Item_READ_description(), Permission.READ, PermissionScope.ITEM); Permission DISCOVER = new Permission(PERMISSIONS, "Discover", Messages._AbstractProject_DiscoverPermission_Description(), READ, PermissionScope.ITEM); - Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._AbstractProject_ExtendedReadPermission_Description(), CONFIGURE, Boolean.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.ITEM}); + Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._AbstractProject_ExtendedReadPermission_Description(), CONFIGURE, SystemProperties.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.ITEM}); // TODO the following really belong in Job, not Item, but too late to move since the owner.name is encoded in the ID: Permission BUILD = new Permission(PERMISSIONS, "Build", Messages._AbstractProject_BuildPermission_Description(), Permission.UPDATE, PermissionScope.ITEM); Permission WORKSPACE = new Permission(PERMISSIONS, "Workspace", Messages._AbstractProject_WorkspacePermission_Description(), Permission.READ, PermissionScope.ITEM); diff --git a/core/src/main/java/hudson/model/LoadStatistics.java b/core/src/main/java/hudson/model/LoadStatistics.java index cd72f87b7a..c905c4ab4e 100644 --- a/core/src/main/java/hudson/model/LoadStatistics.java +++ b/core/src/main/java/hudson/model/LoadStatistics.java @@ -24,6 +24,7 @@ package hudson.model; import hudson.Extension; +import hudson.SystemProperties; import hudson.model.MultiStageTimeSeries.TimeScale; import hudson.model.MultiStageTimeSeries.TrendChart; import hudson.model.queue.SubTask; @@ -371,11 +372,11 @@ public abstract class LoadStatistics { * * Put differently, the half reduction time is {@code CLOCK*log(0.5)/log(DECAY)} */ - public static final float DECAY = Float.parseFloat(System.getProperty(LoadStatistics.class.getName()+".decay","0.9")); + public static final float DECAY = Float.parseFloat(SystemProperties.getProperty(LoadStatistics.class.getName()+".decay","0.9")); /** * Load statistics clock cycle in milliseconds. Specify a small value for quickly debugging this feature and node provisioning through cloud. */ - public static int CLOCK = Integer.getInteger(LoadStatistics.class.getName() + ".clock", 10 * 1000); + public static int CLOCK = SystemProperties.getInteger(LoadStatistics.class.getName() + ".clock", 10 * 1000); /** * Periodically update the load statistics average. diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 3152d08006..df5c002c8d 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -123,6 +123,7 @@ import org.kohsuke.accmod.restrictions.DoNotUse; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter; +import hudson.SystemProperties; import javax.annotation.CheckForNull; import javax.annotation.Nonnegative; import jenkins.model.queue.AsynchronousExecution; @@ -174,7 +175,7 @@ public class Queue extends ResourceController implements Saveable { * Data should be defined in milliseconds, default value - 1000; * @since 1.577 */ - private static int CACHE_REFRESH_PERIOD = Integer.getInteger(Queue.class.getName() + ".cacheRefreshPeriod", 1000); + private static int CACHE_REFRESH_PERIOD = SystemProperties.getInteger(Queue.class.getName() + ".cacheRefreshPeriod", 1000); /** * Items that are waiting for its quiet period to pass. diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index c4f64ccade..bf77d9fc52 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -36,6 +36,7 @@ import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.FeedAdapter; import hudson.Functions; +import hudson.SystemProperties; import hudson.Util; import hudson.XmlFile; import hudson.cli.declarative.CLIMethod; @@ -45,6 +46,7 @@ import hudson.model.Run.RunExecution; import hudson.model.listeners.RunListener; import hudson.model.listeners.SaveableListener; import hudson.model.queue.Executables; +import hudson.model.queue.SubTask; import hudson.search.SearchIndexBuilder; import hudson.security.ACL; import hudson.security.AccessControlled; @@ -103,6 +105,7 @@ import jenkins.model.PeepholePermalink; import jenkins.model.RunAction2; import jenkins.model.StandardArtifactManager; import jenkins.model.lazy.BuildReference; +import jenkins.model.lazy.LazyBuildMixIn; import jenkins.util.VirtualFile; import jenkins.util.io.OnMaster; import net.sf.json.JSONObject; @@ -1137,12 +1140,12 @@ public abstract class Run ,RunT extends Run, RunT extends Run< * when explicitly requested. * */ - public static boolean reloadPeriodically = Boolean.getBoolean(ViewJob.class.getName()+".reloadPeriodically"); + public static boolean reloadPeriodically = SystemProperties.getBoolean(ViewJob.class.getName()+".reloadPeriodically"); } diff --git a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java index 9e83e143e5..875bbfb8f1 100644 --- a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java +++ b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java @@ -26,6 +26,7 @@ package hudson.model; import hudson.Extension; import hudson.ExtensionList; import hudson.FilePath; +import hudson.SystemProperties; import hudson.Util; import java.io.IOException; import java.util.ArrayList; @@ -143,15 +144,15 @@ public class WorkspaceCleanupThread extends AsyncPeriodicWork { /** * Can be used to disable workspace clean up. */ - public static boolean disabled = Boolean.getBoolean(WorkspaceCleanupThread.class.getName()+".disabled"); + public static boolean disabled = SystemProperties.getBoolean(WorkspaceCleanupThread.class.getName()+".disabled"); /** * How often the clean up should run. This is final as Jenkins will not reflect changes anyway. */ - public static final int recurrencePeriodHours = Integer.getInteger(WorkspaceCleanupThread.class.getName()+".recurrencePeriodHours", 24); + public static final int recurrencePeriodHours = SystemProperties.getInteger(WorkspaceCleanupThread.class.getName()+".recurrencePeriodHours", 24); /** * Number of days workspaces should be retained. */ - public static int retainForDays = Integer.getInteger(WorkspaceCleanupThread.class.getName()+".retainForDays", 30); + public static int retainForDays = SystemProperties.getInteger(WorkspaceCleanupThread.class.getName()+".retainForDays", 30); } diff --git a/core/src/main/java/hudson/model/queue/BackFiller.java b/core/src/main/java/hudson/model/queue/BackFiller.java index 0eeea61670..2a456e099e 100644 --- a/core/src/main/java/hudson/model/queue/BackFiller.java +++ b/core/src/main/java/hudson/model/queue/BackFiller.java @@ -2,6 +2,7 @@ package hudson.model.queue; import com.google.common.collect.Iterables; import hudson.Extension; +import hudson.SystemProperties; import hudson.model.Computer; import hudson.model.Executor; import jenkins.model.Jenkins; @@ -204,7 +205,7 @@ public class BackFiller extends LoadPredictor { */ @Extension public static BackFiller newInstance() { - if (Boolean.getBoolean(BackFiller.class.getName())) + if (SystemProperties.getBoolean(BackFiller.class.getName())) return new BackFiller(); return null; } diff --git a/core/src/main/java/hudson/os/solaris/ZFSInstaller.java b/core/src/main/java/hudson/os/solaris/ZFSInstaller.java index 94ff9638d8..13bea8be2c 100644 --- a/core/src/main/java/hudson/os/solaris/ZFSInstaller.java +++ b/core/src/main/java/hudson/os/solaris/ZFSInstaller.java @@ -28,6 +28,7 @@ import com.sun.akuma.JavaVMArguments; import hudson.Launcher.LocalLauncher; import hudson.Util; import hudson.Extension; +import hudson.SystemProperties; import hudson.os.SU; import hudson.model.AdministrativeMonitor; import jenkins.model.Jenkins; @@ -282,7 +283,7 @@ public class ZFSInstaller extends AdministrativeMonitor implements Serializable @Extension public static AdministrativeMonitor init() { - String migrationTarget = System.getProperty(ZFSInstaller.class.getName() + ".migrate"); + String migrationTarget = SystemProperties.getProperty(ZFSInstaller.class.getName() + ".migrate"); if(migrationTarget!=null) { ByteArrayOutputStream out = new ByteArrayOutputStream(); StreamTaskListener listener = new StreamTaskListener(new ForkOutputStream(System.out, out)); @@ -436,5 +437,5 @@ public class ZFSInstaller extends AdministrativeMonitor implements Serializable /** * Escape hatch in case JNI calls fatally crash, like in HUDSON-3733. */ - public static boolean disabled = Boolean.getBoolean(ZFSInstaller.class.getName()+".disabled"); + public static boolean disabled = SystemProperties.getBoolean(ZFSInstaller.class.getName()+".disabled"); } diff --git a/core/src/main/java/hudson/scheduler/BaseParser.java b/core/src/main/java/hudson/scheduler/BaseParser.java index 476bc41e5e..b89f7ca7de 100644 --- a/core/src/main/java/hudson/scheduler/BaseParser.java +++ b/core/src/main/java/hudson/scheduler/BaseParser.java @@ -31,6 +31,7 @@ import antlr.Token; import antlr.TokenBuffer; import antlr.TokenStream; import antlr.TokenStreamException; +import hudson.SystemProperties; /** * @author Kohsuke Kawaguchi @@ -145,7 +146,7 @@ abstract class BaseParser extends LLkParser { /** * This property hashes tokens in the cron tab tokens like @daily so that they spread evenly. */ - public static boolean HASH_TOKENS = !"false".equals(System.getProperty(BaseParser.class.getName()+".hash")); + public static boolean HASH_TOKENS = !"false".equals(SystemProperties.getProperty(BaseParser.class.getName()+".hash")); /** * Constant that indicates no step value. diff --git a/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java b/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java index b801866fa6..145d6fc632 100644 --- a/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java +++ b/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java @@ -11,6 +11,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import hudson.Extension; +import hudson.SystemProperties; import hudson.Util; import jenkins.model.Jenkins; import hudson.model.ModelObject; @@ -121,7 +122,7 @@ public class DefaultCrumbIssuer extends CrumbIssuer { private final static HexStringConfidentialKey CRUMB_SALT = new HexStringConfidentialKey(Jenkins.class,"crumbSalt",16); public DescriptorImpl() { - super(CRUMB_SALT.get(), System.getProperty("hudson.security.csrf.requestfield", ".crumb")); + super(CRUMB_SALT.get(), SystemProperties.getProperty("hudson.security.csrf.requestfield", ".crumb")); load(); } diff --git a/core/src/main/java/hudson/slaves/ChannelPinger.java b/core/src/main/java/hudson/slaves/ChannelPinger.java index 49ae7c1cf9..f8f1faa36c 100644 --- a/core/src/main/java/hudson/slaves/ChannelPinger.java +++ b/core/src/main/java/hudson/slaves/ChannelPinger.java @@ -25,6 +25,7 @@ package hudson.slaves; import hudson.Extension; import hudson.FilePath; +import hudson.SystemProperties; import hudson.model.Computer; import hudson.model.Slave; import hudson.model.TaskListener; @@ -57,7 +58,7 @@ public class ChannelPinger extends ComputerListener { private int pingInterval = 5; public ChannelPinger() { - String interval = System.getProperty(SYS_PROPERTY_NAME); + String interval = SystemProperties.getProperty(SYS_PROPERTY_NAME); if (interval != null) { try { pingInterval = Integer.valueOf(interval); diff --git a/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java b/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java index 054a13b6ac..48cfed1f7b 100644 --- a/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java +++ b/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java @@ -24,6 +24,7 @@ package hudson.slaves; +import hudson.SystemProperties; import javax.annotation.concurrent.GuardedBy; import java.io.IOException; import java.util.logging.Logger; @@ -76,5 +77,5 @@ public class CloudRetentionStrategy extends RetentionStrategy { diff --git a/core/src/main/java/hudson/slaves/NodeProvisioner.java b/core/src/main/java/hudson/slaves/NodeProvisioner.java index 216c8b3ef1..91ca1ee49d 100644 --- a/core/src/main/java/hudson/slaves/NodeProvisioner.java +++ b/core/src/main/java/hudson/slaves/NodeProvisioner.java @@ -30,6 +30,7 @@ import jenkins.model.Jenkins; import static hudson.model.LoadStatistics.DECAY; import hudson.model.MultiStageTimeSeries.TimeScale; import hudson.Extension; +import hudson.SystemProperties; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; @@ -778,8 +779,8 @@ public class NodeProvisioner { * Give some initial warm up time so that statically connected slaves * can be brought online before we start allocating more. */ - public static int INITIALDELAY = Integer.getInteger(NodeProvisioner.class.getName()+".initialDelay",LoadStatistics.CLOCK*10); - public static int RECURRENCEPERIOD = Integer.getInteger(NodeProvisioner.class.getName()+".recurrencePeriod",LoadStatistics.CLOCK); + public static int INITIALDELAY = SystemProperties.getInteger(NodeProvisioner.class.getName()+".initialDelay",LoadStatistics.CLOCK*10); + public static int RECURRENCEPERIOD = SystemProperties.getInteger(NodeProvisioner.class.getName()+".recurrencePeriod",LoadStatistics.CLOCK); @Override public long getInitialDelay() { @@ -800,7 +801,7 @@ public class NodeProvisioner { } private static final Logger LOGGER = Logger.getLogger(NodeProvisioner.class.getName()); - private static final float MARGIN = Integer.getInteger(NodeProvisioner.class.getName()+".MARGIN",10)/100f; + private static final float MARGIN = SystemProperties.getInteger(NodeProvisioner.class.getName()+".MARGIN",10)/100f; private static final float MARGIN0 = Math.max(MARGIN, getFloatSystemProperty(NodeProvisioner.class.getName()+".MARGIN0",0.5f)); private static final float MARGIN_DECAY = getFloatSystemProperty(NodeProvisioner.class.getName()+".MARGIN_DECAY",0.5f); @@ -808,7 +809,7 @@ public class NodeProvisioner { private static final TimeScale TIME_SCALE = TimeScale.SEC10; private static float getFloatSystemProperty(String propName, float defaultValue) { - String v = System.getProperty(propName); + String v = SystemProperties.getProperty(propName); if (v!=null) try { return Float.parseFloat(v); diff --git a/core/src/main/java/hudson/slaves/WorkspaceList.java b/core/src/main/java/hudson/slaves/WorkspaceList.java index a344b45aac..632d8e4598 100644 --- a/core/src/main/java/hudson/slaves/WorkspaceList.java +++ b/core/src/main/java/hudson/slaves/WorkspaceList.java @@ -25,6 +25,7 @@ package hudson.slaves; import hudson.FilePath; import hudson.Functions; +import hudson.SystemProperties; import hudson.model.Computer; import java.io.Closeable; @@ -288,5 +289,5 @@ public final class WorkspaceList { /** * The token that combines the project name and unique number to create unique workspace directory. */ - private static final String COMBINATOR = System.getProperty(WorkspaceList.class.getName(),"@"); + private static final String COMBINATOR = SystemProperties.getProperty(WorkspaceList.class.getName(),"@"); } diff --git a/core/src/main/java/hudson/tasks/ArtifactArchiver.java b/core/src/main/java/hudson/tasks/ArtifactArchiver.java index 297300b525..87631e86b9 100644 --- a/core/src/main/java/hudson/tasks/ArtifactArchiver.java +++ b/core/src/main/java/hudson/tasks/ArtifactArchiver.java @@ -28,6 +28,7 @@ import jenkins.MasterToSlaveFileCallable; import hudson.Launcher; import hudson.Util; import hudson.Extension; +import hudson.SystemProperties; import hudson.model.AbstractProject; import hudson.model.Result; import hudson.model.Run; @@ -137,7 +138,7 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep { // Backwards compatibility for older builds public Object readResolve() { if (allowEmptyArchive == null) { - this.allowEmptyArchive = Boolean.getBoolean(ArtifactArchiver.class.getName()+".warnOnEmpty"); + this.allowEmptyArchive = SystemProperties.getBoolean(ArtifactArchiver.class.getName()+".warnOnEmpty"); } if (defaultExcludes == null){ defaultExcludes = true; diff --git a/core/src/main/java/hudson/tasks/Fingerprinter.java b/core/src/main/java/hudson/tasks/Fingerprinter.java index 051809dfca..9dc89d7cee 100644 --- a/core/src/main/java/hudson/tasks/Fingerprinter.java +++ b/core/src/main/java/hudson/tasks/Fingerprinter.java @@ -29,6 +29,7 @@ import hudson.Extension; import hudson.FilePath; import jenkins.MasterToSlaveFileCallable; import hudson.Launcher; +import hudson.SystemProperties; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; @@ -82,7 +83,7 @@ import jenkins.tasks.SimpleBuildStep; * @author Kohsuke Kawaguchi */ public class Fingerprinter extends Recorder implements Serializable, DependencyDeclarer, SimpleBuildStep { - public static boolean enableFingerprintsInDependencyGraph = Boolean.getBoolean(Fingerprinter.class.getName() + ".enableFingerprintsInDependencyGraph"); + public static boolean enableFingerprintsInDependencyGraph = SystemProperties.getBoolean(Fingerprinter.class.getName() + ".enableFingerprintsInDependencyGraph"); /** * Comma-separated list of files/directories to be fingerprinted. diff --git a/core/src/main/java/hudson/util/CharacterEncodingFilter.java b/core/src/main/java/hudson/util/CharacterEncodingFilter.java index f9ddffc2b3..6d0def96f7 100644 --- a/core/src/main/java/hudson/util/CharacterEncodingFilter.java +++ b/core/src/main/java/hudson/util/CharacterEncodingFilter.java @@ -23,6 +23,7 @@ */ package hudson.util; +import hudson.SystemProperties; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -48,13 +49,13 @@ public class CharacterEncodingFilter implements Filter { private static final String ENCODING = "UTF-8"; private static final Boolean DISABLE_FILTER - = Boolean.getBoolean(CharacterEncodingFilter.class.getName() + ".disableFilter"); + = SystemProperties.getBoolean(CharacterEncodingFilter.class.getName() + ".disableFilter"); /** * The character encoding sets forcibly? */ private static final Boolean FORCE_ENCODING - = Boolean.getBoolean(CharacterEncodingFilter.class.getName() + ".forceEncoding"); + = SystemProperties.getBoolean(CharacterEncodingFilter.class.getName() + ".forceEncoding"); public void init(FilterConfig filterConfig) throws ServletException { LOGGER.log(Level.FINE, diff --git a/core/src/main/java/hudson/util/ProcessTree.java b/core/src/main/java/hudson/util/ProcessTree.java index dd5a3d756b..575ec1fb4a 100644 --- a/core/src/main/java/hudson/util/ProcessTree.java +++ b/core/src/main/java/hudson/util/ProcessTree.java @@ -59,6 +59,7 @@ import java.util.logging.Logger; import javax.annotation.CheckForNull; import static com.sun.jna.Pointer.NULL; +import hudson.SystemProperties; import static hudson.util.jna.GNUCLibrary.LIBC; import static java.util.logging.Level.FINE; import static java.util.logging.Level.FINER; @@ -1284,6 +1285,6 @@ public abstract class ProcessTree implements Iterable, IProcessTree, *

* This property supports two names for a compatibility reason. */ - public static boolean enabled = !Boolean.getBoolean(ProcessTreeKiller.class.getName()+".disable") - && !Boolean.getBoolean(ProcessTree.class.getName()+".disable"); + public static boolean enabled = !SystemProperties.getBoolean(ProcessTreeKiller.class.getName()+".disable") + && !SystemProperties.getBoolean(ProcessTree.class.getName()+".disable"); } diff --git a/core/src/main/java/hudson/util/RingBufferLogHandler.java b/core/src/main/java/hudson/util/RingBufferLogHandler.java index 469cdbba4b..d292123fad 100644 --- a/core/src/main/java/hudson/util/RingBufferLogHandler.java +++ b/core/src/main/java/hudson/util/RingBufferLogHandler.java @@ -23,6 +23,7 @@ */ package hudson.util; +import hudson.SystemProperties; import java.util.AbstractList; import java.util.List; import java.util.logging.Handler; @@ -35,7 +36,7 @@ import java.util.logging.LogRecord; */ public class RingBufferLogHandler extends Handler { - private static final int DEFAULT_RING_BUFFER_SIZE = Integer.getInteger(RingBufferLogHandler.class.getName() + ".defaultSize", 256); + private static final int DEFAULT_RING_BUFFER_SIZE = SystemProperties.getInteger(RingBufferLogHandler.class.getName() + ".defaultSize", 256); private int start = 0; private final LogRecord[] records; diff --git a/core/src/main/java/hudson/util/Secret.java b/core/src/main/java/hudson/util/Secret.java index 80be75b8ff..e0819149fc 100644 --- a/core/src/main/java/hudson/util/Secret.java +++ b/core/src/main/java/hudson/util/Secret.java @@ -29,6 +29,7 @@ import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.trilead.ssh2.crypto.Base64; +import hudson.SystemProperties; import jenkins.model.Jenkins; import hudson.Util; import jenkins.security.CryptoConfidentialKey; @@ -223,7 +224,7 @@ public final class Secret implements Serializable { * Workaround for JENKINS-6459 / http://java.net/jira/browse/GLASSFISH-11862 * @see #getCipher(String) */ - private static final String PROVIDER = System.getProperty(Secret.class.getName()+".provider"); + private static final String PROVIDER = SystemProperties.getProperty(Secret.class.getName()+".provider"); /** * For testing only. Override the secret key so that we can test this class without {@link Jenkins}. diff --git a/core/src/main/java/hudson/widgets/HistoryWidget.java b/core/src/main/java/hudson/widgets/HistoryWidget.java index 0059680ef5..695240a8d6 100644 --- a/core/src/main/java/hudson/widgets/HistoryWidget.java +++ b/core/src/main/java/hudson/widgets/HistoryWidget.java @@ -24,6 +24,7 @@ package hudson.widgets; import hudson.Functions; +import hudson.SystemProperties; import hudson.model.ModelObject; import hudson.model.Run; @@ -242,7 +243,7 @@ public class HistoryWidget extends Widget { req.getView(page,"ajaxBuildHistory.jelly").forward(req,rsp); } - static final int THRESHOLD = Integer.getInteger(HistoryWidget.class.getName()+".threshold",30); + static final int THRESHOLD = SystemProperties.getInteger(HistoryWidget.class.getName()+".threshold",30); public String getNextBuildNumberToFetch() { return nextBuildNumberToFetch; diff --git a/core/src/main/java/jenkins/InitReactorRunner.java b/core/src/main/java/jenkins/InitReactorRunner.java index f3b8711e32..364f5950d3 100644 --- a/core/src/main/java/jenkins/InitReactorRunner.java +++ b/core/src/main/java/jenkins/InitReactorRunner.java @@ -1,5 +1,6 @@ package jenkins; +import hudson.SystemProperties; import hudson.init.InitMilestone; import hudson.init.InitReactorListener; import hudson.util.DaemonThreadFactory; @@ -91,7 +92,7 @@ public class InitReactorRunner { protected void onInitMilestoneAttained(InitMilestone milestone) { } - private static final int TWICE_CPU_NUM = Integer.getInteger( + private static final int TWICE_CPU_NUM = SystemProperties.getInteger( InitReactorRunner.class.getName()+".concurrency", Runtime.getRuntime().availableProcessors() * 2); diff --git a/core/src/main/java/jenkins/model/Configuration.java b/core/src/main/java/jenkins/model/Configuration.java index 9de56f968c..97d6756165 100644 --- a/core/src/main/java/jenkins/model/Configuration.java +++ b/core/src/main/java/jenkins/model/Configuration.java @@ -23,6 +23,7 @@ */ package jenkins.model; +import hudson.SystemProperties; import hudson.model.Hudson; @@ -34,9 +35,9 @@ public class Configuration { } public static String getStringConfigParameter(String name, String defaultValue) { - String value = System.getProperty(Jenkins.class.getName()+"." + name); + String value = SystemProperties.getProperty(Jenkins.class.getName()+"." + name); if( value == null ) - value = System.getProperty(Hudson.class.getName()+"." + name); + value = SystemProperties.getProperty(Hudson.class.getName()+"." + name); return (value==null)?defaultValue:value; } } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 942088b1c2..d375a13d67 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -50,6 +50,7 @@ import hudson.Plugin; import hudson.PluginManager; import hudson.PluginWrapper; import hudson.ProxyConfiguration; +import hudson.SystemProperties; import hudson.TcpSlaveAgentListener; import hudson.UDPBroadcastThread; import hudson.Util; @@ -4158,7 +4159,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve VERSION_HASH = Util.getDigestOf(ver).substring(0, 8); SESSION_HASH = Util.getDigestOf(ver+System.currentTimeMillis()).substring(0, 8); - if(ver.equals("?") || Boolean.getBoolean("hudson.script.noCache")) + if(ver.equals("?") || SystemProperties.getBoolean("hudson.script.noCache")) RESOURCE_PATH = ""; else RESOURCE_PATH = "/static/"+SESSION_HASH; diff --git a/core/src/main/java/jenkins/model/lazy/BuildReference.java b/core/src/main/java/jenkins/model/lazy/BuildReference.java index 93546d46e1..83a0d976b7 100644 --- a/core/src/main/java/jenkins/model/lazy/BuildReference.java +++ b/core/src/main/java/jenkins/model/lazy/BuildReference.java @@ -3,6 +3,7 @@ package jenkins.model.lazy; import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; +import hudson.SystemProperties; import hudson.model.Run; import java.lang.ref.Reference; import java.lang.ref.SoftReference; @@ -144,7 +145,7 @@ public final class BuildReference { @Extension(ordinal=Double.NEGATIVE_INFINITY) public static final class DefaultHolderFactory implements HolderFactory { public static final String MODE_PROPERTY = "jenkins.model.lazy.BuildReference.MODE"; - private static final String mode = System.getProperty(MODE_PROPERTY); + private static final String mode = SystemProperties.getProperty(MODE_PROPERTY); @Override public Holder make(R referent) { if (mode == null || mode.equals("soft")) { diff --git a/core/src/main/java/jenkins/security/ApiTokenProperty.java b/core/src/main/java/jenkins/security/ApiTokenProperty.java index 8aef388cbd..4e87c8b664 100644 --- a/core/src/main/java/jenkins/security/ApiTokenProperty.java +++ b/core/src/main/java/jenkins/security/ApiTokenProperty.java @@ -24,6 +24,7 @@ package jenkins.security; import hudson.Extension; +import hudson.SystemProperties; import hudson.Util; import hudson.model.Descriptor.FormException; import hudson.model.User; @@ -65,7 +66,7 @@ public class ApiTokenProperty extends UserProperty { * @since TODO */ private static final boolean SHOW_TOKEN_TO_ADMINS = - Boolean.getBoolean(ApiTokenProperty.class.getName() + ".showTokenToAdmins"); + SystemProperties.getBoolean(ApiTokenProperty.class.getName() + ".showTokenToAdmins"); @DataBoundConstructor diff --git a/core/src/main/java/jenkins/security/BasicHeaderRealPasswordAuthenticator.java b/core/src/main/java/jenkins/security/BasicHeaderRealPasswordAuthenticator.java index 3b41c006dd..a0feec091d 100644 --- a/core/src/main/java/jenkins/security/BasicHeaderRealPasswordAuthenticator.java +++ b/core/src/main/java/jenkins/security/BasicHeaderRealPasswordAuthenticator.java @@ -15,6 +15,7 @@ package jenkins.security; import hudson.Extension; +import hudson.SystemProperties; import jenkins.ExtensionFilter; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; @@ -69,5 +70,5 @@ public class BasicHeaderRealPasswordAuthenticator extends BasicHeaderAuthenticat * Legacy property to disable the real password support. * Now that this is an extension, {@link ExtensionFilter} is a better way to control this. */ - public static boolean DISABLE = Boolean.getBoolean("jenkins.security.ignoreBasicAuth"); + public static boolean DISABLE = SystemProperties.getBoolean("jenkins.security.ignoreBasicAuth"); } diff --git a/core/src/main/java/jenkins/security/FrameOptionsPageDecorator.java b/core/src/main/java/jenkins/security/FrameOptionsPageDecorator.java index bc47e231b5..9155717606 100644 --- a/core/src/main/java/jenkins/security/FrameOptionsPageDecorator.java +++ b/core/src/main/java/jenkins/security/FrameOptionsPageDecorator.java @@ -1,6 +1,7 @@ package jenkins.security; import hudson.Extension; +import hudson.SystemProperties; import hudson.model.PageDecorator; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -13,5 +14,5 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; @Extension(ordinal = 1000) public class FrameOptionsPageDecorator extends PageDecorator { @Restricted(NoExternalUse.class) - public static boolean enabled = Boolean.valueOf(System.getProperty(FrameOptionsPageDecorator.class.getName() + ".enabled", "true")); + public static boolean enabled = Boolean.valueOf(SystemProperties.getProperty(FrameOptionsPageDecorator.class.getName() + ".enabled", "true")); } diff --git a/core/src/main/java/jenkins/security/SecureRequester.java b/core/src/main/java/jenkins/security/SecureRequester.java index 42c6ac1dbb..5443898928 100644 --- a/core/src/main/java/jenkins/security/SecureRequester.java +++ b/core/src/main/java/jenkins/security/SecureRequester.java @@ -2,6 +2,7 @@ package jenkins.security; import hudson.Extension; import hudson.ExtensionPoint; +import hudson.SystemProperties; import hudson.model.Api; import java.util.logging.Logger; import jenkins.model.Jenkins; @@ -34,7 +35,7 @@ public interface SecureRequester extends ExtensionPoint { @Extension class Default implements SecureRequester { private static final String PROP = "hudson.model.Api.INSECURE"; - private static final boolean INSECURE = Boolean.getBoolean(PROP); + private static final boolean INSECURE = SystemProperties.getBoolean(PROP); static { if (INSECURE) { Logger.getLogger(SecureRequester.class.getName()).warning(PROP + " system property is deprecated; implement SecureRequester instead"); diff --git a/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java b/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java index 706d17c0be..ab03d4ea16 100644 --- a/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java +++ b/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java @@ -1,6 +1,7 @@ package jenkins.security.s2m; import hudson.Extension; +import hudson.SystemProperties; import hudson.remoting.Callable; import hudson.remoting.ChannelBuilder; import jenkins.security.ChannelConfigurator; @@ -38,7 +39,7 @@ public class CallableDirectionChecker extends RoleChecker { * This is an escape hatch in case the fix breaks something critical, to allow the user * to keep operation. */ - public static boolean BYPASS = Boolean.getBoolean(BYPASS_PROP); + public static boolean BYPASS = SystemProperties.getBoolean(BYPASS_PROP); private CallableDirectionChecker(Object context) { this.context = context; diff --git a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java index 563d19269e..c4a810fe9b 100644 --- a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java +++ b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java @@ -25,6 +25,7 @@ package jenkins.security.s2m; import hudson.Extension; +import hudson.SystemProperties; import hudson.remoting.ChannelBuilder; import jenkins.ReflectiveFilePathFilter; import jenkins.security.ChannelConfigurator; @@ -44,7 +45,7 @@ import java.util.logging.Logger; /** * Escape hatch to disable this check completely. */ - public static boolean BYPASS = Boolean.getBoolean(DefaultFilePathFilter.class.getName()+".allow"); + public static boolean BYPASS = SystemProperties.getBoolean(DefaultFilePathFilter.class.getName()+".allow"); private static final Logger LOGGER = Logger.getLogger(DefaultFilePathFilter.class.getName()); diff --git a/core/src/main/java/jenkins/slaves/NioChannelSelector.java b/core/src/main/java/jenkins/slaves/NioChannelSelector.java index 3aea28a1e3..97d97b4fbd 100644 --- a/core/src/main/java/jenkins/slaves/NioChannelSelector.java +++ b/core/src/main/java/jenkins/slaves/NioChannelSelector.java @@ -1,6 +1,7 @@ package jenkins.slaves; import hudson.Extension; +import hudson.SystemProperties; import hudson.model.Computer; import org.jenkinsci.remoting.nio.NioChannelHub; @@ -37,7 +38,7 @@ public class NioChannelSelector { /** * Escape hatch to disable use of NIO. */ - public static boolean DISABLED = Boolean.getBoolean(NioChannelSelector.class.getName()+".disabled"); + public static boolean DISABLED = SystemProperties.getBoolean(NioChannelSelector.class.getName()+".disabled"); private static final Logger LOGGER = Logger.getLogger(NioChannelSelector.class.getName()); } diff --git a/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java b/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java index 794fc8626d..82efe25384 100644 --- a/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java +++ b/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java @@ -2,6 +2,7 @@ package jenkins.slaves; import hudson.Extension; import hudson.FilePath; +import hudson.SystemProperties; import hudson.model.Computer; import hudson.model.TaskListener; import hudson.remoting.Channel; @@ -74,5 +75,5 @@ public class StandardOutputSwapper extends ComputerListener { } private static final Logger LOGGER = Logger.getLogger(StandardOutputSwapper.class.getName()); - public static boolean disabled = Boolean.getBoolean(StandardOutputSwapper.class.getName()+".disabled"); + public static boolean disabled = SystemProperties.getBoolean(StandardOutputSwapper.class.getName()+".disabled"); } diff --git a/core/src/main/java/jenkins/util/xml/XMLUtils.java b/core/src/main/java/jenkins/util/xml/XMLUtils.java index 84e6f6151b..afe3bc844c 100644 --- a/core/src/main/java/jenkins/util/xml/XMLUtils.java +++ b/core/src/main/java/jenkins/util/xml/XMLUtils.java @@ -1,5 +1,6 @@ package jenkins.util.xml; +import hudson.SystemProperties; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.xml.sax.InputSource; @@ -66,7 +67,7 @@ public final class XMLUtils { // for some reason we could not convert source // this applies to DOMSource and StAXSource - and possibly 3rd party implementations... // a DOMSource can already be compromised as it is parsed by the time it gets to us. - if (Boolean.getBoolean(DISABLED_PROPERTY_NAME)) { + if (SystemProperties.getBoolean(DISABLED_PROPERTY_NAME)) { LOGGER.log(Level.WARNING, "XML external entity (XXE) prevention has been disabled by the system " + "property {0}=true Your system may be vulnerable to XXE attacks.", DISABLED_PROPERTY_NAME); if (LOGGER.isLoggable(Level.FINE)) { -- GitLab From 9303136c9d4e5f8fedfac2cd3cf78c10b677298d Mon Sep 17 00:00:00 2001 From: Johannes Ernst Date: Wed, 18 Nov 2015 18:26:41 +0000 Subject: [PATCH 0010/2380] Incorporated pull request feedback: * moved to jenkins.util.SystemProperties * consistent naming getString/getInteger/getBoolean * improved code formatting Improved JavaDoc --- .../java/hudson/ClassicPluginStrategy.java | 1 + core/src/main/java/hudson/DNSMultiCast.java | 1 + core/src/main/java/hudson/FilePath.java | 1 + core/src/main/java/hudson/Functions.java | 5 +- .../main/java/hudson/LocalPluginManager.java | 3 +- core/src/main/java/hudson/Main.java | 1 + core/src/main/java/hudson/PluginManager.java | 3 +- .../java/hudson/TcpSlaveAgentListener.java | 3 +- .../main/java/hudson/UDPBroadcastThread.java | 1 + core/src/main/java/hudson/Util.java | 1 + core/src/main/java/hudson/WebAppMain.java | 3 +- core/src/main/java/hudson/cli/CLICommand.java | 4 +- .../main/java/hudson/init/InitStrategy.java | 4 +- .../main/java/hudson/lifecycle/Lifecycle.java | 6 +- .../lifecycle/WindowsInstallerLink.java | 6 +- .../main/java/hudson/model/AbstractBuild.java | 2 +- core/src/main/java/hudson/model/Computer.java | 4 +- .../java/hudson/model/DownloadService.java | 2 +- .../hudson/model/FullDuplexHttpChannel.java | 2 +- core/src/main/java/hudson/model/Item.java | 2 +- .../java/hudson/model/LoadStatistics.java | 4 +- core/src/main/java/hudson/model/Queue.java | 2 +- core/src/main/java/hudson/model/Run.java | 6 +- core/src/main/java/hudson/model/Slave.java | 4 +- core/src/main/java/hudson/model/TreeView.java | 2 +- .../main/java/hudson/model/UpdateCenter.java | 4 +- .../main/java/hudson/model/UpdateSite.java | 2 +- .../java/hudson/model/UsageStatistics.java | 2 +- core/src/main/java/hudson/model/User.java | 1 + core/src/main/java/hudson/model/ViewJob.java | 2 +- .../hudson/model/WorkspaceCleanupThread.java | 2 +- .../java/hudson/model/queue/BackFiller.java | 2 +- .../java/hudson/os/solaris/ZFSInstaller.java | 4 +- .../java/hudson/scheduler/BaseParser.java | 4 +- .../security/csrf/DefaultCrumbIssuer.java | 4 +- .../java/hudson/slaves/ChannelPinger.java | 4 +- .../hudson/slaves/CloudRetentionStrategy.java | 2 +- .../slaves/ConnectionActivityMonitor.java | 2 +- .../java/hudson/slaves/NodeProvisioner.java | 4 +- .../java/hudson/slaves/WorkspaceList.java | 4 +- .../java/hudson/tasks/ArtifactArchiver.java | 2 +- .../main/java/hudson/tasks/Fingerprinter.java | 2 +- .../hudson/util/CharacterEncodingFilter.java | 2 +- .../main/java/hudson/util/ProcessTree.java | 2 +- .../hudson/util/RingBufferLogHandler.java | 2 +- core/src/main/java/hudson/util/Secret.java | 4 +- .../java/hudson/widgets/HistoryWidget.java | 2 +- .../main/java/jenkins/InitReactorRunner.java | 2 +- .../java/jenkins/model/Configuration.java | 6 +- core/src/main/java/jenkins/model/Jenkins.java | 2 +- .../jenkins/model/lazy/BuildReference.java | 4 +- .../jenkins/security/ApiTokenProperty.java | 2 +- .../BasicHeaderRealPasswordAuthenticator.java | 2 +- .../security/FrameOptionsPageDecorator.java | 4 +- .../jenkins/security/SecureRequester.java | 2 +- .../s2m/CallableDirectionChecker.java | 2 +- .../security/s2m/DefaultFilePathFilter.java | 2 +- .../jenkins/slaves/NioChannelSelector.java | 2 +- .../jenkins/slaves/StandardOutputSwapper.java | 2 +- .../util}/SystemProperties.java | 117 ++++++++++-------- .../main/java/jenkins/util/xml/XMLUtils.java | 2 +- 61 files changed, 151 insertions(+), 132 deletions(-) rename core/src/main/java/{hudson => jenkins/util}/SystemProperties.java (63%) diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index 5cd2e4857a..fda8c3e054 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -23,6 +23,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import com.google.common.collect.Lists; import hudson.Plugin.DummyImpl; import hudson.PluginWrapper.Dependency; diff --git a/core/src/main/java/hudson/DNSMultiCast.java b/core/src/main/java/hudson/DNSMultiCast.java index 4d2eb1816d..2ed4f28746 100644 --- a/core/src/main/java/hudson/DNSMultiCast.java +++ b/core/src/main/java/hudson/DNSMultiCast.java @@ -1,5 +1,6 @@ package hudson; +import jenkins.util.SystemProperties; import jenkins.model.Jenkins; import jenkins.model.Jenkins.MasterComputer; diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 536cfe679b..61817e9dde 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -25,6 +25,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import com.jcraft.jzlib.GZIPInputStream; import com.jcraft.jzlib.GZIPOutputStream; import hudson.Launcher.LocalLauncher; diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index ca26fb0d6f..1cd644ebfd 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -25,6 +25,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import hudson.cli.CLICommand; import hudson.console.ConsoleAnnotationDescriptor; import hudson.console.ConsoleAnnotatorFactory; @@ -616,7 +617,7 @@ public class Functions { response.addCookie(c); } if (refresh) { - response.addHeader("Refresh", SystemProperties.getProperty("hudson.Functions.autoRefreshSeconds", "10")); + response.addHeader("Refresh", SystemProperties.getString("hudson.Functions.autoRefreshSeconds", "10")); } } @@ -838,7 +839,7 @@ public class Functions { */ public static String getFooterURL() { if(footerURL == null) { - footerURL = SystemProperties.getProperty("hudson.footerURL"); + footerURL = SystemProperties.getString("hudson.footerURL"); if(StringUtils.isBlank(footerURL)) { footerURL = "http://jenkins-ci.org/"; } diff --git a/core/src/main/java/hudson/LocalPluginManager.java b/core/src/main/java/hudson/LocalPluginManager.java index 62aee91afa..acbac8e412 100644 --- a/core/src/main/java/hudson/LocalPluginManager.java +++ b/core/src/main/java/hudson/LocalPluginManager.java @@ -24,6 +24,7 @@ package hudson; +import jenkins.util.SystemProperties; import jenkins.model.Jenkins; import javax.servlet.ServletContext; @@ -60,7 +61,7 @@ public class LocalPluginManager extends PluginManager { @Override protected Collection loadBundledPlugins() { // this is used in tests, when we want to override the default bundled plugins with .jpl (or .hpl) versions - if (SystemProperties.getProperty("hudson.bundled.plugins") != null) { + if (SystemProperties.getString("hudson.bundled.plugins") != null) { return Collections.emptySet(); } diff --git a/core/src/main/java/hudson/Main.java b/core/src/main/java/hudson/Main.java index c1aaab9786..8e8ec46dd4 100644 --- a/core/src/main/java/hudson/Main.java +++ b/core/src/main/java/hudson/Main.java @@ -23,6 +23,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import hudson.util.DualOutputStream; import hudson.util.EncodingStream; import com.thoughtworks.xstream.core.util.Base64Encoder; diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index d96c68dc20..802bbf180f 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -23,6 +23,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import hudson.PluginWrapper.Dependency; import hudson.init.InitMilestone; import hudson.init.InitStrategy; @@ -633,7 +634,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * Creates a hudson.PluginStrategy, looking at the corresponding system property. */ protected PluginStrategy createPluginStrategy() { - String strategyName = SystemProperties.getProperty(PluginStrategy.class.getName()); + String strategyName = SystemProperties.getString(PluginStrategy.class.getName()); if (strategyName != null) { try { Class klazz = getClass().getClassLoader().loadClass(strategyName); diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java index 7fe14e0496..8e4b075344 100644 --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java @@ -23,6 +23,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import hudson.slaves.OfflineCause; import jenkins.AgentProtocol; @@ -203,7 +204,7 @@ public final class TcpSlaveAgentListener extends Thread { * * TODO: think about how to expose this (including whether this needs to be exposed at all.) */ - public static String CLI_HOST_NAME = SystemProperties.getProperty(TcpSlaveAgentListener.class.getName()+".hostName"); + public static String CLI_HOST_NAME = SystemProperties.getString(TcpSlaveAgentListener.class.getName()+".hostName"); /** * Port number that we advertise the CLI client to connect to. diff --git a/core/src/main/java/hudson/UDPBroadcastThread.java b/core/src/main/java/hudson/UDPBroadcastThread.java index bb9b5aa708..9824f6c027 100644 --- a/core/src/main/java/hudson/UDPBroadcastThread.java +++ b/core/src/main/java/hudson/UDPBroadcastThread.java @@ -23,6 +23,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import edu.umd.cs.findbugs.annotations.SuppressWarnings; import hudson.model.Hudson; import jenkins.model.Jenkins; diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index c3c649dc48..f4d9ae5177 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -23,6 +23,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import com.sun.jna.Memory; import com.sun.jna.Native; import com.sun.jna.NativeLong; diff --git a/core/src/main/java/hudson/WebAppMain.java b/core/src/main/java/hudson/WebAppMain.java index 99bdef5641..b7b3d0ed8e 100644 --- a/core/src/main/java/hudson/WebAppMain.java +++ b/core/src/main/java/hudson/WebAppMain.java @@ -23,6 +23,7 @@ */ package hudson; +import jenkins.util.SystemProperties; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; import com.thoughtworks.xstream.core.JVM; import com.trilead.ssh2.util.IOUtils; @@ -359,7 +360,7 @@ public class WebAppMain implements ServletContextListener { // next the system property for (String name : HOME_NAMES) { - String sysProp = SystemProperties.getProperty(name); + String sysProp = SystemProperties.getString(name); if(sysProp!=null) return new FileAndDescription(new File(sysProp.trim()),"SystemProperties.getProperty(\""+name+"\")"); } diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index d18a66464c..4995a38c82 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -29,7 +29,7 @@ import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.cli.declarative.CLIMethod; import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.cli.declarative.OptionHandlerExtension; import jenkins.model.Jenkins; import hudson.remoting.Callable; @@ -419,7 +419,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { } public String call() throws IOException { - return SystemProperties.getProperty(name); + return SystemProperties.getString(name); } private static final long serialVersionUID = 1L; diff --git a/core/src/main/java/hudson/init/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java index 1ba014597d..69227afb61 100644 --- a/core/src/main/java/hudson/init/InitStrategy.java +++ b/core/src/main/java/hudson/init/InitStrategy.java @@ -15,7 +15,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import hudson.PluginManager; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.util.DirScanner; import hudson.util.FileVisitor; import hudson.util.Service; @@ -85,7 +85,7 @@ public class InitStrategy { * TODO: maven-hpi-plugin should inject its own InitStrategy instead of having this in the core. */ protected void getBundledPluginsFromProperty(final List r) { - String hplProperty = SystemProperties.getProperty("hudson.bundled.plugins"); + String hplProperty = SystemProperties.getString("hudson.bundled.plugins"); if (hplProperty != null) { for (String hplLocation : hplProperty.split(",")) { File hpl = new File(hplLocation.trim()); diff --git a/core/src/main/java/hudson/lifecycle/Lifecycle.java b/core/src/main/java/hudson/lifecycle/Lifecycle.java index b768cc545c..34915da0de 100644 --- a/core/src/main/java/hudson/lifecycle/Lifecycle.java +++ b/core/src/main/java/hudson/lifecycle/Lifecycle.java @@ -25,7 +25,7 @@ package hudson.lifecycle; import hudson.ExtensionPoint; import hudson.Functions; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.Util; import jenkins.model.Jenkins; @@ -58,7 +58,7 @@ public abstract class Lifecycle implements ExtensionPoint { public synchronized static Lifecycle get() { if(INSTANCE==null) { Lifecycle instance; - String p = SystemProperties.getProperty("hudson.lifecycle"); + String p = SystemProperties.getString("hudson.lifecycle"); if(p!=null) { try { ClassLoader cl = Jenkins.getInstance().getPluginManager().uberClassLoader; @@ -120,7 +120,7 @@ public abstract class Lifecycle implements ExtensionPoint { * to a newer version. */ public File getHudsonWar() { - String war = SystemProperties.getProperty("executable-war"); + String war = SystemProperties.getString("executable-war"); if(war!=null && new File(war).exists()) return new File(war); return null; diff --git a/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java b/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java index afef84e84e..978b3c7965 100644 --- a/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java +++ b/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java @@ -34,7 +34,7 @@ import hudson.util.jna.Shell32; import jenkins.model.Jenkins; import hudson.AbortException; import hudson.Extension; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.util.StreamTaskListener; import hudson.util.jna.DotNet; import org.apache.commons.io.IOUtils; @@ -258,14 +258,14 @@ public class WindowsInstallerLink extends ManagementLink { // this system property is set by the launcher when we run "java -jar jenkins.war" // and this is how we know where is jenkins.war. - String war = SystemProperties.getProperty("executable-war"); + String war = SystemProperties.getString("executable-war"); if(war!=null && new File(war).exists()) { WindowsInstallerLink link = new WindowsInstallerLink(new File(war)); // in certain situations where we know the user is just trying Jenkins (like when Jenkins is launched // from JNLP), also put this link on the navigation bar to increase // visibility - if(SystemProperties.getProperty(WindowsInstallerLink.class.getName()+".prominent")!=null) + if(SystemProperties.getString(WindowsInstallerLink.class.getName()+".prominent")!=null) Jenkins.getInstance().getActions().add(link); return link; diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java index bb55d86198..e37e7a7347 100644 --- a/core/src/main/java/hudson/model/AbstractBuild.java +++ b/core/src/main/java/hudson/model/AbstractBuild.java @@ -30,7 +30,7 @@ import hudson.EnvVars; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.console.ModelHyperlinkNote; import hudson.model.Fingerprint.BuildPtr; import hudson.model.Fingerprint.RangeSet; diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index f221a396d1..7f0f8673ec 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -29,7 +29,7 @@ import edu.umd.cs.findbugs.annotations.When; import hudson.EnvVars; import hudson.Extension; import hudson.Launcher.ProcStarter; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.Util; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; @@ -1273,7 +1273,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces private static class GetFallbackName extends MasterToSlaveCallable { public String call() throws IOException { - return SystemProperties.getProperty("host.name"); + return SystemProperties.getString("host.name"); } private static final long serialVersionUID = 1L; } diff --git a/core/src/main/java/hudson/model/DownloadService.java b/core/src/main/java/hudson/model/DownloadService.java index 359ff186bf..2af1515145 100644 --- a/core/src/main/java/hudson/model/DownloadService.java +++ b/core/src/main/java/hudson/model/DownloadService.java @@ -28,7 +28,7 @@ import hudson.ExtensionList; import hudson.ExtensionListListener; import hudson.ExtensionPoint; import hudson.ProxyConfiguration; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.init.InitMilestone; import hudson.init.Initializer; import hudson.util.FormValidation; diff --git a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java index 872c6f9dc3..c0fdafb56f 100644 --- a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java +++ b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java @@ -23,7 +23,7 @@ */ package hudson.model; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.remoting.Channel; import hudson.remoting.PingThread; import hudson.remoting.Channel.Mode; diff --git a/core/src/main/java/hudson/model/Item.java b/core/src/main/java/hudson/model/Item.java index 50a8b23b38..9d43714841 100644 --- a/core/src/main/java/hudson/model/Item.java +++ b/core/src/main/java/hudson/model/Item.java @@ -25,7 +25,7 @@ package hudson.model; import hudson.Functions; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.security.PermissionScope; import org.kohsuke.stapler.StaplerRequest; diff --git a/core/src/main/java/hudson/model/LoadStatistics.java b/core/src/main/java/hudson/model/LoadStatistics.java index c905c4ab4e..728d79ddbb 100644 --- a/core/src/main/java/hudson/model/LoadStatistics.java +++ b/core/src/main/java/hudson/model/LoadStatistics.java @@ -24,7 +24,7 @@ package hudson.model; import hudson.Extension; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.model.MultiStageTimeSeries.TimeScale; import hudson.model.MultiStageTimeSeries.TrendChart; import hudson.model.queue.SubTask; @@ -372,7 +372,7 @@ public abstract class LoadStatistics { * * Put differently, the half reduction time is {@code CLOCK*log(0.5)/log(DECAY)} */ - public static final float DECAY = Float.parseFloat(SystemProperties.getProperty(LoadStatistics.class.getName()+".decay","0.9")); + public static final float DECAY = Float.parseFloat(SystemProperties.getString(LoadStatistics.class.getName()+".decay","0.9")); /** * Load statistics clock cycle in milliseconds. Specify a small value for quickly debugging this feature and node provisioning through cloud. */ diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index df5c002c8d..f66303efa5 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -123,7 +123,7 @@ import org.kohsuke.accmod.restrictions.DoNotUse; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import javax.annotation.CheckForNull; import javax.annotation.Nonnegative; import jenkins.model.queue.AsynchronousExecution; diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index bf77d9fc52..8138bb4745 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -36,7 +36,7 @@ import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.FeedAdapter; import hudson.Functions; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.Util; import hudson.XmlFile; import hudson.cli.declarative.CLIMethod; @@ -1140,12 +1140,12 @@ public abstract class Run ,RunT extends Run { @Extension(ordinal=Double.NEGATIVE_INFINITY) public static final class DefaultHolderFactory implements HolderFactory { public static final String MODE_PROPERTY = "jenkins.model.lazy.BuildReference.MODE"; - private static final String mode = SystemProperties.getProperty(MODE_PROPERTY); + private static final String mode = SystemProperties.getString(MODE_PROPERTY); @Override public Holder make(R referent) { if (mode == null || mode.equals("soft")) { diff --git a/core/src/main/java/jenkins/security/ApiTokenProperty.java b/core/src/main/java/jenkins/security/ApiTokenProperty.java index 4e87c8b664..c478ab16f3 100644 --- a/core/src/main/java/jenkins/security/ApiTokenProperty.java +++ b/core/src/main/java/jenkins/security/ApiTokenProperty.java @@ -24,7 +24,7 @@ package jenkins.security; import hudson.Extension; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.Util; import hudson.model.Descriptor.FormException; import hudson.model.User; diff --git a/core/src/main/java/jenkins/security/BasicHeaderRealPasswordAuthenticator.java b/core/src/main/java/jenkins/security/BasicHeaderRealPasswordAuthenticator.java index a0feec091d..4e3d60f7bd 100644 --- a/core/src/main/java/jenkins/security/BasicHeaderRealPasswordAuthenticator.java +++ b/core/src/main/java/jenkins/security/BasicHeaderRealPasswordAuthenticator.java @@ -15,7 +15,7 @@ package jenkins.security; import hudson.Extension; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import jenkins.ExtensionFilter; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; diff --git a/core/src/main/java/jenkins/security/FrameOptionsPageDecorator.java b/core/src/main/java/jenkins/security/FrameOptionsPageDecorator.java index 9155717606..7bf162a422 100644 --- a/core/src/main/java/jenkins/security/FrameOptionsPageDecorator.java +++ b/core/src/main/java/jenkins/security/FrameOptionsPageDecorator.java @@ -1,7 +1,7 @@ package jenkins.security; import hudson.Extension; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.model.PageDecorator; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -14,5 +14,5 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; @Extension(ordinal = 1000) public class FrameOptionsPageDecorator extends PageDecorator { @Restricted(NoExternalUse.class) - public static boolean enabled = Boolean.valueOf(SystemProperties.getProperty(FrameOptionsPageDecorator.class.getName() + ".enabled", "true")); + public static boolean enabled = Boolean.valueOf(SystemProperties.getString(FrameOptionsPageDecorator.class.getName() + ".enabled", "true")); } diff --git a/core/src/main/java/jenkins/security/SecureRequester.java b/core/src/main/java/jenkins/security/SecureRequester.java index 5443898928..f13385cca7 100644 --- a/core/src/main/java/jenkins/security/SecureRequester.java +++ b/core/src/main/java/jenkins/security/SecureRequester.java @@ -2,7 +2,7 @@ package jenkins.security; import hudson.Extension; import hudson.ExtensionPoint; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.model.Api; import java.util.logging.Logger; import jenkins.model.Jenkins; diff --git a/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java b/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java index ab03d4ea16..1a63573576 100644 --- a/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java +++ b/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java @@ -1,7 +1,7 @@ package jenkins.security.s2m; import hudson.Extension; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.remoting.Callable; import hudson.remoting.ChannelBuilder; import jenkins.security.ChannelConfigurator; diff --git a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java index c4a810fe9b..562cb808fc 100644 --- a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java +++ b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java @@ -25,7 +25,7 @@ package jenkins.security.s2m; import hudson.Extension; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.remoting.ChannelBuilder; import jenkins.ReflectiveFilePathFilter; import jenkins.security.ChannelConfigurator; diff --git a/core/src/main/java/jenkins/slaves/NioChannelSelector.java b/core/src/main/java/jenkins/slaves/NioChannelSelector.java index 97d97b4fbd..9401e7686d 100644 --- a/core/src/main/java/jenkins/slaves/NioChannelSelector.java +++ b/core/src/main/java/jenkins/slaves/NioChannelSelector.java @@ -1,7 +1,7 @@ package jenkins.slaves; import hudson.Extension; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.model.Computer; import org.jenkinsci.remoting.nio.NioChannelHub; diff --git a/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java b/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java index 82efe25384..89fedfd04f 100644 --- a/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java +++ b/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java @@ -2,7 +2,7 @@ package jenkins.slaves; import hudson.Extension; import hudson.FilePath; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import hudson.model.Computer; import hudson.model.TaskListener; import hudson.remoting.Channel; diff --git a/core/src/main/java/hudson/SystemProperties.java b/core/src/main/java/jenkins/util/SystemProperties.java similarity index 63% rename from core/src/main/java/hudson/SystemProperties.java rename to core/src/main/java/jenkins/util/SystemProperties.java index 88fa83163f..04561ff4a5 100644 --- a/core/src/main/java/hudson/SystemProperties.java +++ b/core/src/main/java/jenkins/util/SystemProperties.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright 2015 Johannes Ernst, http://upon2020.com/. + * Copyright 2015 Johannes Ernst http://upon2020.com/ * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,29 +21,32 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson; +package jenkins.util; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletContext; /** - * Centralizes calls to System.getProperty() and related calls. + * Centralizes calls to System.getProperty() and related calls. * This allows us to get values not just from environment variables but also from - * the ServletContext, so things like hudson.DNSMultiCast.disabled can be set - * in context.xml and the app server's boot script does not have to be changed. + * the ServletContext, so properties like hudson.DNSMultiCast.disabled + * can be set in context.xml and the app server's boot script does not + * have to be changed. * - * While it looks like it on first glamce, this cannot be mapped to EnvVars.java - * because EnvVars.java is only for build variables, not Jenkins itself variables. + *

While it looks like it on first glance, this cannot be mapped to EnvVars.java + * because EnvVars.java is only for build variables, not Jenkins itself variables. * - * This should be invoked for hudson parameters (e.g. hudson.DNSMultiCast.disabled), - * but not for system parameters (e.g. os.name). + *

This should be used to obtain hudson/jenkins "app"-level parameters + * (e.g. hudson.DNSMultiCast.disabled), but not for system parameters + * (e.g. os.name). * * @author Johannes Ernst + * @since 1.639 */ public class SystemProperties { /** - * The ServletContext to get the init parameters from. + * The ServletContext to get the "init" parameters from. */ private static ServletContext theContext; @@ -52,10 +55,15 @@ public class SystemProperties { */ private static final Logger LOGGER = Logger.getLogger(SystemProperties.class.getName()); + /** + * This class should never be instantiated. + */ + private SystemProperties() {} + /** * Gets the system property indicated by the specified key. - * This behaves just like System.getProperty(String), except that it - * also consults the ServletContext's init parameters. + * This behaves just like System.getProperty(String), except that it + * also consults the ServletContext's "init" parameters. * * @param key the name of the system property. * @return the string value of the system property, @@ -68,32 +76,31 @@ public class SystemProperties { * null. * @exception IllegalArgumentException if key is empty. */ - public static String getProperty(String key) { + public static String getString(String key) { String value = System.getProperty(key); // keep passing on any exceptions - if( value != null ) { - if( LOGGER.isLoggable(Level.INFO )) { - LOGGER.log(Level.INFO, "Property (system): {0} => {1}", new Object[]{ key, value }); + if (value != null) { + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.log(Level.INFO, "Property (system): {0} => {1}", new Object[] {key, value}); } - } else if( theContext != null ) { + } else if (theContext != null) { value = theContext.getInitParameter(key); - if( value != null ) { - if( LOGGER.isLoggable(Level.INFO )) { - LOGGER.log(Level.INFO, "Property (context): {0} => {1}", new Object[]{ key, value }); + if (value != null) { + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.log(Level.INFO, "Property (context): {0} => {1}", new Object[] {key, value}); } } } else { - if( LOGGER.isLoggable(Level.INFO )) { - LOGGER.log(Level.INFO, "Property (not found): {0} => {1}", new Object[]{ key, value }); + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.log(Level.INFO, "Property (not found): {0} => {1}", new Object[] {key, value}); } } - return value; } /** - * Gets the system property indicated by the specified key. - * This behaves just like System.getProperty(String), except that it - * also consults the ServletContext's init parameters. + * Gets the system property indicated by the specified key, or a default value. + * This behaves just like System.getProperty(String,String), except + * that it also consults the ServletContext's "init" parameters. * * @param key the name of the system property. * @param def a default value. @@ -107,24 +114,24 @@ public class SystemProperties { * null. * @exception IllegalArgumentException if key is empty. */ - public static String getProperty(String key, String def) { + public static String getString(String key, String def) { String value = System.getProperty(key); // keep passing on any exceptions - if( value != null ) { - if( LOGGER.isLoggable(Level.INFO )) { - LOGGER.log(Level.INFO, "Property (system): {0} => {1}", new Object[]{ key, value }); + if (value != null) { + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.log(Level.INFO, "Property (system): {0} => {1}", new Object[] {key, value}); } - } else if( theContext != null ) { + } else if (theContext != null) { value = theContext.getInitParameter(key); - if( value != null ) { - if( LOGGER.isLoggable(Level.INFO )) { - LOGGER.log(Level.INFO, "Property (context): {0} => {1}", new Object[]{ key, value }); + if (value != null) { + if(LOGGER.isLoggable(Level.INFO)) { + LOGGER.log(Level.INFO, "Property (context): {0} => {1}", new Object[] {key, value}); } } } - if( value == null ) { + if (value == null) { value = def; - if( LOGGER.isLoggable(Level.INFO )) { - LOGGER.log(Level.INFO, "Property (default): {0} => {1}", new Object[]{ key, value }); + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.log(Level.INFO, "Property (default): {0} => {1}", new Object[] {key, value}); } } return value; @@ -134,11 +141,11 @@ public class SystemProperties { * Returns {@code true} if the system property * named by the argument exists and is equal to the string * {@code "true"}. If the system property does not exist, return - * {@code "true"} if a property by this name exists in the servletcontext + * {@code "true"} if a property by this name exists in the ServletContext * and is equal to the string {@code "true"}. * - * This behaves just like Boolean.getBoolean(String), except that it - * also consults the ServletContext's init parameters. + * This behaves just like Boolean.getBoolean(String), except that it + * also consults the ServletContext's "init" parameters. * * @param name the system property name. * @return the {@code boolean} value of the system property. @@ -150,20 +157,20 @@ public class SystemProperties { /** * Returns {@code true} if the system property * named by the argument exists and is equal to the string - * {@code "true"}. If the system property does not exist, return - * {@code "true"} if a property by this name exists in the servletcontext + * {@code "true"}, or a default value. If the system property does not exist, return + * {@code "true"} if a property by this name exists in the ServletContext * and is equal to the string {@code "true"}. If that property does not * exist either, return the default value. * - * This behaves just like Boolean.getBoolean(String), except that it - * also consults the ServletContext's init parameters. + * This behaves just like Boolean.getBoolean(String) with a default + * value, except that it also consults the ServletContext's "init" parameters. * * @param name the system property name. * @param def a default value. * @return the {@code boolean} value of the system property. */ public static boolean getBoolean(String name, boolean def) { - String v = getProperty(name); + String v = getString(name); if (v != null) { return Boolean.parseBoolean(v); @@ -175,31 +182,30 @@ public class SystemProperties { * Determines the integer value of the system property with the * specified name. * - * This behaves just like Integer.getInteger(String), except that it - * also consults the ServletContext's init parameters. + * This behaves just like Integer.getInteger(String), except that it + * also consults the ServletContext's "init" parameters. * * @param name property name. * @return the {@code Integer} value of the property. */ public static Integer getInteger(String name) { - return getInteger( name, null ); + return getInteger(name, null); } /** * Determines the integer value of the system property with the - * specified name. + * specified name, or a default value. * - * This behaves just like Integer.getInteger(String), except that it - * also consults the ServletContext's init parameters. If neither exist, + * This behaves just like Integer.getInteger(String,Integer), except that it + * also consults the ServletContext's "init" parameters. If neither exist, * return the default value. * * @param name property name. * @param def a default value. * @return the {@code Integer} value of the property. */ - public static Integer getInteger(String name, Integer def) { - String v = getProperty(name); + String v = getString(name); if (v != null) { try { @@ -209,10 +215,11 @@ public class SystemProperties { } return def; } + /** - * Invoked by WebAppMain, tells us where to get the init parameters from. + * Invoked by WebAppMain, tells us where to get the "init" parameters from. * - * @param context the ServletContext obtained from contextInitialized + * @param context the ServletContext obtained from contextInitialized */ public static void initialize(ServletContext context) { theContext = context; diff --git a/core/src/main/java/jenkins/util/xml/XMLUtils.java b/core/src/main/java/jenkins/util/xml/XMLUtils.java index afe3bc844c..811adf308f 100644 --- a/core/src/main/java/jenkins/util/xml/XMLUtils.java +++ b/core/src/main/java/jenkins/util/xml/XMLUtils.java @@ -1,6 +1,6 @@ package jenkins.util.xml; -import hudson.SystemProperties; +import jenkins.util.SystemProperties; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.xml.sax.InputSource; -- GitLab From 647d3e9a39a682212e6dd0f91f940d1670dc97f8 Mon Sep 17 00:00:00 2001 From: Johannes Ernst Date: Wed, 25 Nov 2015 20:42:36 +0000 Subject: [PATCH 0011/2380] System property access now logged on Level.CONFIG Added clarifying comments into JavaDoc --- .../java/jenkins/util/SystemProperties.java | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/jenkins/util/SystemProperties.java b/core/src/main/java/jenkins/util/SystemProperties.java index 04561ff4a5..948c7cd231 100644 --- a/core/src/main/java/jenkins/util/SystemProperties.java +++ b/core/src/main/java/jenkins/util/SystemProperties.java @@ -33,14 +33,24 @@ import javax.servlet.ServletContext; * the ServletContext, so properties like hudson.DNSMultiCast.disabled * can be set in context.xml and the app server's boot script does not * have to be changed. - * - *

While it looks like it on first glance, this cannot be mapped to EnvVars.java - * because EnvVars.java is only for build variables, not Jenkins itself variables. - * + * *

This should be used to obtain hudson/jenkins "app"-level parameters * (e.g. hudson.DNSMultiCast.disabled), but not for system parameters * (e.g. os.name). - * + * + *

If you run multiple instances of Jenkins in the same virtual machine and wish + * to obtain properties from context.xml, make sure these Jenkins instances use + * different ClassLoaders. Tomcat, for example, does this automatically. If you do + * not use different ClassLoaders, the values of properties specified in + * context.xml is undefined. + * + *

Property access is logged on Level.CONFIG. Note that some properties + * may be accessed by Jenkins before logging is configured properly, so early access to + * some properties may not be logged. + * + *

While it looks like it on first glance, this cannot be mapped to EnvVars.java + * because EnvVars.java is only for build variables, not Jenkins itself variables. + * * @author Johannes Ernst * @since 1.639 */ @@ -79,19 +89,19 @@ public class SystemProperties { public static String getString(String key) { String value = System.getProperty(key); // keep passing on any exceptions if (value != null) { - if (LOGGER.isLoggable(Level.INFO)) { - LOGGER.log(Level.INFO, "Property (system): {0} => {1}", new Object[] {key, value}); + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, "Property (system): {0} => {1}", new Object[] {key, value}); } } else if (theContext != null) { value = theContext.getInitParameter(key); if (value != null) { - if (LOGGER.isLoggable(Level.INFO)) { - LOGGER.log(Level.INFO, "Property (context): {0} => {1}", new Object[] {key, value}); + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, "Property (context): {0} => {1}", new Object[] {key, value}); } } } else { - if (LOGGER.isLoggable(Level.INFO)) { - LOGGER.log(Level.INFO, "Property (not found): {0} => {1}", new Object[] {key, value}); + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, "Property (not found): {0} => {1}", new Object[] {key, value}); } } return value; @@ -117,21 +127,21 @@ public class SystemProperties { public static String getString(String key, String def) { String value = System.getProperty(key); // keep passing on any exceptions if (value != null) { - if (LOGGER.isLoggable(Level.INFO)) { - LOGGER.log(Level.INFO, "Property (system): {0} => {1}", new Object[] {key, value}); + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, "Property (system): {0} => {1}", new Object[] {key, value}); } } else if (theContext != null) { value = theContext.getInitParameter(key); if (value != null) { - if(LOGGER.isLoggable(Level.INFO)) { - LOGGER.log(Level.INFO, "Property (context): {0} => {1}", new Object[] {key, value}); + if(LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, "Property (context): {0} => {1}", new Object[] {key, value}); } } } if (value == null) { value = def; - if (LOGGER.isLoggable(Level.INFO)) { - LOGGER.log(Level.INFO, "Property (default): {0} => {1}", new Object[] {key, value}); + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.log(Level.CONFIG, "Property (default): {0} => {1}", new Object[] {key, value}); } } return value; -- GitLab From c7fd90a8796fe0ad80bfe37a9837af4515fbf5cb Mon Sep 17 00:00:00 2001 From: Ted Date: Sat, 28 Nov 2015 16:23:59 +0800 Subject: [PATCH 0012/2380] fix JENKINS-31768 dead lock while removing computer --- core/src/main/java/hudson/model/Computer.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 547e51c753..3815571fd2 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -828,7 +828,19 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces protected void onRemoved(){ } - private synchronized void setNumExecutors(int n) { + /** + * Calling path, *means protected by Queue.withLock + * + * Computer.doConfigSubmit -> Computer.replaceBy ->Jenkins.setNodes* ->Computer.setNode + * AbstractCIBase.updateComputerList->Computer.inflictMortalWound* + * AbstractCIBase.updateComputerList->AbstractCIBase.updateComputer* ->Computer.setNode + * AbstractCIBase.updateComputerList->AbstractCIBase.killComputer->Computer.kill + * Computer.constructor->Computer.setNode + * Computer.kill is called after numExecutors set to zero(Computer.inflictMortalWound) so not need the Queue.lock + * + * @param number of executors + */ + private void setNumExecutors(int n) { this.numExecutors = n; final int diff = executors.size()-n; -- GitLab From a0d390a9f65e3f045679f7fdceab3e560f06c84c Mon Sep 17 00:00:00 2001 From: Allan Burdajewicz Date: Mon, 21 Dec 2015 13:20:03 +1000 Subject: [PATCH 0013/2380] [JENKINS-32134]: Improved the Build Other Projects help message for projects inside Folder. --- war/src/main/webapp/help/project-config/downstream.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/war/src/main/webapp/help/project-config/downstream.html b/war/src/main/webapp/help/project-config/downstream.html index 80fb27b920..ee74952072 100644 --- a/war/src/main/webapp/help/project-config/downstream.html +++ b/war/src/main/webapp/help/project-config/downstream.html @@ -6,4 +6,8 @@ Other than the obvious use case where you'd like to build other projects that have a dependency on the current project, this can also be useful to split a long build process in to multiple stages (such as the build portion and the test portion). + +

+ Projects located inside folders can be specified using an absolute path starting with "/" or a + relative path from the current project location. \ No newline at end of file -- GitLab From 384f73b49b09d4d7f67fede1c948c79c2247842d Mon Sep 17 00:00:00 2001 From: Allan Burdajewicz Date: Tue, 22 Dec 2015 21:56:05 +1000 Subject: [PATCH 0014/2380] [JENKINS-32134]: Referenced CloudBees Folder Plugin and Multi-Branch Project Plugin --- war/src/main/webapp/help/project-config/downstream.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/war/src/main/webapp/help/project-config/downstream.html b/war/src/main/webapp/help/project-config/downstream.html index ee74952072..51eb77d2f5 100644 --- a/war/src/main/webapp/help/project-config/downstream.html +++ b/war/src/main/webapp/help/project-config/downstream.html @@ -6,8 +6,11 @@ Other than the obvious use case where you'd like to build other projects that have a dependency on the current project, this can also be useful to split a long build process in to multiple stages (such as the build portion and the test portion). -

- Projects located inside folders can be specified using an absolute path starting with "/" or a + When using + CloudBees Folder Plugin + or other plugins depending on it and organizing jobs into folders such as + Multi-Branch Project Plugin, + projects located inside folders can be specified using an absolute path starting with "/" or a relative path from the current project location. \ No newline at end of file -- GitLab From bcba404dca6cb7239ba78381d88e927dda2c1c18 Mon Sep 17 00:00:00 2001 From: Allan Burdajewicz Date: Thu, 31 Dec 2015 16:11:09 +1000 Subject: [PATCH 0015/2380] [JENKINS-32134]: Used the core terminology "ItemGroup". Simplified and added examples. Added the closing paragraph tags. --- .../help/project-config/downstream.html | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/war/src/main/webapp/help/project-config/downstream.html b/war/src/main/webapp/help/project-config/downstream.html index 51eb77d2f5..58055de756 100644 --- a/war/src/main/webapp/help/project-config/downstream.html +++ b/war/src/main/webapp/help/project-config/downstream.html @@ -1,16 +1,21 @@

- Trigger builds of the other projects once a build is successfully completed. - Multiple projects can be specified by using comma, like "abc, def". + Trigger builds of the other projects once a build is successfully completed. + Multiple projects can be specified by using comma, like "abc, def". -

- Other than the obvious use case where you'd like to build other projects that have a dependency - on the current project, this can also be useful to split a long build process in to multiple - stages (such as the build portion and the test portion). -

- When using - CloudBees Folder Plugin - or other plugins depending on it and organizing jobs into folders such as - Multi-Branch Project Plugin, - projects located inside folders can be specified using an absolute path starting with "/" or a - relative path from the current project location. +

+ Other than the obvious use case where you'd like to build other projects that have a dependency + on the current project, this can also be useful to split a long build process in to multiple + stages (such as the build portion and the test portion). +

+ +

+ A project located inside an ItemGroup need to be specified by using a path. It can be an absolute + path starting from Jenkins root, like "/grp/abc" where "grp" is an ItemGroup located at the root + of Jenkins. It can also be a relative path from the current project location, like "grp/abc" where + "grp" is an ItemGroup located at the same level as the current project. + Examples of plugins that provide an ItemGroup implementation include but are not limited to the + CloudBees Folder Plugin + and the + Multi-Branch Project Plugin +

\ No newline at end of file -- GitLab From e0919f1a46eac6628eed0241bd68c3e1c4ee44a6 Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Tue, 19 Jan 2016 02:14:53 +0100 Subject: [PATCH 0016/2380] Set version to 2.0-alpha-1-SNAPSHOT --- cli/pom.xml | 2 +- core/pom.xml | 2 +- plugins/pom.xml | 6 +++--- pom.xml | 2 +- test/pom.xml | 2 +- war/pom.xml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 794b248f16..48c52c5743 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 1.645-SNAPSHOT + 2.0-alpha-1-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index 49a73d4c4d..a46a4f655b 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 1.645-SNAPSHOT + 2.0-alpha-1-SNAPSHOT jenkins-core diff --git a/plugins/pom.xml b/plugins/pom.xml index 01e2171292..7301dcd75e 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -12,7 +12,7 @@ org.jenkins-ci.plugins plugin Jenkins plugin POM - 1.645-SNAPSHOT + 2.0-alpha-1-SNAPSHOT pom + \ No newline at end of file diff --git a/test/pom.xml b/test/pom.xml index c0b9e272d4..43ad1f68ef 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -63,7 +63,7 @@ THE SOFTWARE. ${project.groupId} jenkins-test-harness - 2.0 + 2.1-beta-1 test diff --git a/test/src/test/java/hudson/ClassicPluginStrategyTest.java b/test/src/test/java/hudson/ClassicPluginStrategyTest.java index a0cfe146de..24a3a3e788 100644 --- a/test/src/test/java/hudson/ClassicPluginStrategyTest.java +++ b/test/src/test/java/hudson/ClassicPluginStrategyTest.java @@ -24,12 +24,18 @@ */ package hudson; +import hudson.model.Hudson; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.recipes.LocalData; +import org.jvnet.hudson.test.recipes.Recipe; +import java.io.File; import java.net.URL; +import java.util.Collection; import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Set; /** * @author Alan Harder @@ -42,6 +48,26 @@ public class ClassicPluginStrategyTest extends HudsonTestCase { super.setUp(); } + @Override + protected Hudson newHudson() throws Exception { + File home = homeLoader.allocate(); + + for (Recipe.Runner r : recipes) { + r.decorateHome(this,home); + } + LocalPluginManager pluginManager = new LocalPluginManager(home) { + @Override + protected Collection loadBundledPlugins() { + // Overriding so we can force loading of the detached plugins for testing + Set names = new LinkedHashSet<>(); + names.addAll(loadPluginsFromWar("/WEB-INF/plugins")); + names.addAll(loadPluginsFromWar("/WEB-INF/detached-plugins")); + return names; + } + }; + return new Hudson(home, createWebServer(), pluginManager); + } + /** * Test finding resources via DependencyClassLoader. */ diff --git a/test/src/test/java/hudson/PluginManagerInstalledGUITest.java b/test/src/test/java/hudson/PluginManagerInstalledGUITest.java index b4a23ad6bc..ae95099f8a 100644 --- a/test/src/test/java/hudson/PluginManagerInstalledGUITest.java +++ b/test/src/test/java/hudson/PluginManagerInstalledGUITest.java @@ -33,12 +33,13 @@ import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.recipes.WithPlugin; +import org.jvnet.hudson.test.TestPluginManager; import org.xml.sax.SAXException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -49,10 +50,28 @@ import java.util.Set; public class PluginManagerInstalledGUITest { @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); + public JenkinsRule jenkinsRule = new JenkinsRule() { + @Override + public PluginManager getPluginManager() { + try { + return new TestPluginManager() { + @Override + protected Collection loadBundledPlugins() throws Exception { + try { + return super.loadBundledPlugins(); + } finally { + installResourcePlugin("tasks.jpi"); + } + } + }; + } catch (IOException e) { + Assert.fail(e.getMessage()); + return null; + } + } + }; @Test - @WithPlugin("tasks.jpi") public void test_enable_disable_uninstall() throws IOException, SAXException { InstalledPlugins installedPlugins = new InstalledPlugins(); diff --git a/test/src/test/java/hudson/PluginManagerTest.java b/test/src/test/java/hudson/PluginManagerTest.java index 282bc9f779..af61364e5b 100644 --- a/test/src/test/java/hudson/PluginManagerTest.java +++ b/test/src/test/java/hudson/PluginManagerTest.java @@ -42,9 +42,13 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.Future; import jenkins.RestartRequiredException; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; import org.apache.commons.io.FileUtils; import org.apache.tools.ant.filters.StringInputStream; import static org.junit.Assert.*; + +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -355,6 +359,23 @@ public class PluginManagerTest { assertEquals("should not have tried to delete & unpack", lastMod, timestamp.lastModified()); } + @WithPlugin("tasks.jpi") + @Test public void pluginListJSONApi() throws IOException { + JSONObject response = r.getJSON("pluginManager/plugins").getJSONObject(); + + // Check that the basic API endpoint invocation works. + Assert.assertEquals("ok", response.getString("status")); + JSONArray data = response.getJSONArray("data"); + Assert.assertTrue(data.size() > 0); + + // Check that there was some data in the response and that the first entry + // at least had some of the expected fields. + JSONObject pluginInfo = data.getJSONObject(0); + Assert.assertTrue(pluginInfo.getString("name") != null); + Assert.assertTrue(pluginInfo.getString("title") != null); + Assert.assertTrue(pluginInfo.getString("dependencies") != null); + } + private void dynamicLoad(String plugin) throws IOException, InterruptedException, RestartRequiredException { PluginManagerUtil.dynamicLoad(plugin, r.jenkins); } diff --git a/test/src/test/java/hudson/PluginManagerTest2.java b/test/src/test/java/hudson/PluginManagerTest2.java index e51e4484db..f889130cb4 100644 --- a/test/src/test/java/hudson/PluginManagerTest2.java +++ b/test/src/test/java/hudson/PluginManagerTest2.java @@ -7,7 +7,6 @@ import java.net.URL; import javax.servlet.ServletContext; import org.jvnet.hudson.test.HudsonTestCase; -import org.jvnet.hudson.test.recipes.WithPlugin; import org.xml.sax.SAXException; import com.gargoylesoftware.htmlunit.html.HtmlForm; @@ -29,21 +28,6 @@ public class PluginManagerTest2 extends HudsonTestCase { return servletContext; } - @WithPlugin("tasks.jpi") - public void testPinned() throws Exception { - PluginWrapper tasks = jenkins.getPluginManager().getPlugin("tasks"); - assertFalse("tasks shouldn't be bundled",tasks.isBundled()); - assertFalse("tasks shouldn't be pinned before update",tasks.isPinned()); - uploadPlugin("tasks.jpi", false); - assertFalse("tasks shouldn't be pinned after update",tasks.isPinned()); - - PluginWrapper cvs = jenkins.getPluginManager().getPlugin("cvs"); - assertTrue("cvs should be bundled",cvs.isBundled()); - assertFalse("cvs shouldn't be pinned before update",cvs.isPinned()); - uploadPlugin("cvs.hpi", true); - assertTrue("cvs should be pinned after update",cvs.isPinned()); - } - private void uploadPlugin(String pluginName, boolean useServerRoot) throws IOException, SAXException, Exception { HtmlPage page = new WebClient().goTo("pluginManager/advanced"); HtmlForm f = page.getFormByName("uploadPlugin"); diff --git a/test/src/test/java/hudson/model/UpdateCenterConnectionStatusTest.java b/test/src/test/java/hudson/model/UpdateCenterConnectionStatusTest.java new file mode 100644 index 0000000000..20966be962 --- /dev/null +++ b/test/src/test/java/hudson/model/UpdateCenterConnectionStatusTest.java @@ -0,0 +1,161 @@ +/* + * The MIT License + * + * Copyright (c) 2015, 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 hudson.model; + +import net.sf.json.JSONObject; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.net.UnknownHostException; + +import static hudson.model.UpdateCenter.ConnectionStatus; + +/** + * @author tom.fennelly@gmail.com + */ +public class UpdateCenterConnectionStatusTest { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + + @Test + public void doConnectionStatus_default_site() throws IOException, SAXException { + JSONObject response = jenkinsRule.getJSON("updateCenter/connectionStatus").getJSONObject(); + + Assert.assertEquals("ok", response.getString("status")); + JSONObject statusObj = response.getJSONObject("data"); + Assert.assertTrue(statusObj.has("updatesite")); + Assert.assertTrue(statusObj.has("internet")); + + // The following is equivalent to the above + response = jenkinsRule.getJSON("updateCenter/connectionStatus?siteId=default").getJSONObject(); + + Assert.assertEquals("ok", response.getString("status")); + statusObj = response.getJSONObject("data"); + Assert.assertTrue(statusObj.has("updatesite")); + Assert.assertTrue(statusObj.has("internet")); + } + + @Test + public void doConnectionStatus_unknown_site() throws IOException, SAXException { + JSONObject response = jenkinsRule.getJSON("updateCenter/connectionStatus?siteId=blahblah").getJSONObject(); + + Assert.assertEquals("error", response.getString("status")); + Assert.assertEquals("Unknown site 'blahblah'.", response.getString("message")); + } + + private UpdateSite updateSite = new UpdateSite(UpdateCenter.ID_DEFAULT, "http://xyz") { + @Override + public String getConnectionCheckUrl() { + return "http://xyz"; + } + }; + + @Test + public void test_states_allok() { + UpdateCenter updateCenter = new UpdateCenter(new TestConfig()); + UpdateCenter.ConnectionCheckJob job = updateCenter.newConnectionCheckJob(updateSite); + + Assert.assertEquals(ConnectionStatus.PRECHECK, job.connectionStates.get(ConnectionStatus.INTERNET)); + Assert.assertEquals(ConnectionStatus.PRECHECK, job.connectionStates.get(ConnectionStatus.UPDATE_SITE)); + + job.run(); + + Assert.assertEquals(ConnectionStatus.OK, job.connectionStates.get(ConnectionStatus.INTERNET)); + Assert.assertEquals(ConnectionStatus.OK, job.connectionStates.get(ConnectionStatus.UPDATE_SITE)); + } + + @Test + public void test_states_internet_failed() { + UpdateCenter updateCenter = new UpdateCenter(new TestConfig().failInternet()); + UpdateCenter.ConnectionCheckJob job = updateCenter.newConnectionCheckJob(updateSite); + + job.run(); + + Assert.assertEquals(ConnectionStatus.FAILED, job.connectionStates.get(ConnectionStatus.INTERNET)); + Assert.assertEquals(ConnectionStatus.UNCHECKED, job.connectionStates.get(ConnectionStatus.UPDATE_SITE)); + } + + @Test + public void test_states_uc_failed_timeout() { + UpdateCenter updateCenter = new UpdateCenter(new TestConfig().failUCConnect()); + UpdateCenter.ConnectionCheckJob job = updateCenter.newConnectionCheckJob(updateSite); + + job.run(); + + Assert.assertEquals(ConnectionStatus.OK, job.connectionStates.get(ConnectionStatus.INTERNET)); + Assert.assertEquals(ConnectionStatus.FAILED, job.connectionStates.get(ConnectionStatus.UPDATE_SITE)); + } + + @Test + public void test_states_uc_failed_UnknownHost() { + UpdateCenter updateCenter = new UpdateCenter(new TestConfig().failUCResolve()); + UpdateCenter.ConnectionCheckJob job = updateCenter.newConnectionCheckJob(updateSite); + + job.run(); + + Assert.assertEquals(ConnectionStatus.OK, job.connectionStates.get(ConnectionStatus.INTERNET)); + Assert.assertEquals(ConnectionStatus.FAILED, job.connectionStates.get(ConnectionStatus.UPDATE_SITE)); + } + + private class TestConfig extends UpdateCenter.UpdateCenterConfiguration { + + private IOException checkConnectionException; + private IOException checkUpdateCenterException; + + private TestConfig failInternet() { + checkConnectionException = new IOException("Connection timed out"); + return this; + } + + private TestConfig failUCResolve() { + checkUpdateCenterException = new UnknownHostException("Unable to resolve UpdateCenter host address."); + return this; + } + + private TestConfig failUCConnect() { + checkUpdateCenterException = new IOException("Connection timed out"); + return this; + } + + @Override + public void checkConnection(UpdateCenter.ConnectionCheckJob job, String connectionCheckUrl) throws IOException { + if (checkConnectionException != null) { + throw checkConnectionException; + } + } + + @Override + public void checkUpdateCenter(UpdateCenter.ConnectionCheckJob job, String updateCenterUrl) throws IOException { + if (checkUpdateCenterException != null) { + throw checkUpdateCenterException; + } + } + } +} diff --git a/test/src/test/java/hudson/model/UpdateCenterPluginInstallTest.java b/test/src/test/java/hudson/model/UpdateCenterPluginInstallTest.java new file mode 100644 index 0000000000..a9b9901304 --- /dev/null +++ b/test/src/test/java/hudson/model/UpdateCenterPluginInstallTest.java @@ -0,0 +1,93 @@ +/* + * The MIT License + * + * Copyright (c) 2015, 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 hudson.model; + +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Arrays; + +/** + * @author tom.fennelly@gmail.com + */ +public class UpdateCenterPluginInstallTest { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + public void setup() throws IOException { + jenkinsRule.jenkins.getUpdateCenter().getSite(UpdateCenter.ID_DEFAULT).updateDirectlyNow(false); + InetSocketAddress address = new InetSocketAddress("updates.jenkins-ci.org", 80); + Assume.assumeFalse("Unable to resolve updates.jenkins-ci.org. Skip test.", address.isUnresolved()); + } + + @Test + public void test_installUnknownPlugin() throws IOException, SAXException { + setup(); + JenkinsRule.JSONWebResponse response = jenkinsRule.postJSON("pluginManager/installPlugins", buildInstallPayload("unknown_plugin_xyz")); + JSONObject json = response.getJSONObject(); + + Assert.assertEquals("error", json.get("status")); + Assert.assertEquals("No such plugin: unknown_plugin_xyz", json.get("message")); + Assert.assertEquals("error", json.get("status")); + Assert.assertEquals("No such plugin: unknown_plugin_xyz", json.get("message")); + } + + @Test + public void test_installKnownPlugins() throws IOException, SAXException { + setup(); + JenkinsRule.JSONWebResponse installResponse = jenkinsRule.postJSON("pluginManager/installPlugins", buildInstallPayload("changelog-history", "git")); + JSONObject json = installResponse.getJSONObject(); + + Assert.assertEquals("ok", json.get("status")); + JSONObject data = json.getJSONObject("data"); + Assert.assertTrue(data.has("correlationId")); + + String correlationId = data.getString("correlationId"); + JSONObject installStatus = jenkinsRule.getJSON("updateCenter/installStatus?correlationId=" + correlationId).getJSONObject(); + Assert.assertEquals("ok", json.get("status")); + JSONArray states = installStatus.getJSONArray("data"); + Assert.assertEquals(2, states.size()); + + JSONObject pluginInstallState = states.getJSONObject(0); + Assert.assertEquals("changelog-history", pluginInstallState.get("name")); + pluginInstallState = states.getJSONObject(1); + Assert.assertEquals("git", pluginInstallState.get("name")); + } + + private JSONObject buildInstallPayload(String... plugins) { + JSONObject payload = new JSONObject(); + payload.put("dynamicLoad", true); + payload.put("plugins", JSONArray.fromObject(Arrays.asList(plugins))); + return payload; + } +} diff --git a/test/src/test/java/jenkins/I18nTest.java b/test/src/test/java/jenkins/I18nTest.java new file mode 100644 index 0000000000..a934422532 --- /dev/null +++ b/test/src/test/java/jenkins/I18nTest.java @@ -0,0 +1,64 @@ +/* + * The MIT License + * + * Copyright (c) 2015, 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; + +import net.sf.json.JSONObject; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.xml.sax.SAXException; + +import java.io.IOException; + +/** + * @author tom.fennelly@gmail.com + */ +public class I18nTest { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + @Test + public void test_baseName_unspecified() throws IOException, SAXException { + JSONObject response = jenkinsRule.getJSON("i18n/resourceBundle").getJSONObject(); + Assert.assertEquals("error", response.getString("status")); + Assert.assertEquals("Mandatory parameter 'baseName' not specified.", response.getString("message")); + } + + @Test + public void test_baseName_unknown() throws IOException, SAXException { + JSONObject response = jenkinsRule.getJSON("i18n/resourceBundle?baseName=com.acme.XyzWhatever").getJSONObject(); + Assert.assertEquals("error", response.getString("status")); + Assert.assertTrue(response.getString("message").contains("com.acme.XyzWhatever")); + } + + @Test + public void test_valid() throws IOException, SAXException { + JSONObject response = jenkinsRule.getJSON("i18n/resourceBundle?baseName=hudson.logging.Messages&language=de").getJSONObject(); + Assert.assertEquals("ok", response.getString("status")); + JSONObject data = response.getJSONObject("data"); + Assert.assertEquals("Initialisiere Log-Rekorder", data.getString("LogRecorderManager.init")); + } +} diff --git a/test/src/test/java/jenkins/install/InstallUtilTest.java b/test/src/test/java/jenkins/install/InstallUtilTest.java new file mode 100644 index 0000000000..e890054931 --- /dev/null +++ b/test/src/test/java/jenkins/install/InstallUtilTest.java @@ -0,0 +1,201 @@ +/* + * The MIT License + * + * Copyright (c) 2015, 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.install; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.Mockito; + +import hudson.Main; +import hudson.model.UpdateCenter; +import hudson.model.UpdateCenter.DownloadJob.Failure; +import hudson.model.UpdateCenter.DownloadJob.InstallationStatus; +import hudson.model.UpdateCenter.DownloadJob.Installing; +import hudson.model.UpdateCenter.DownloadJob.Pending; +import hudson.model.UpdateCenter.DownloadJob.Success; +import hudson.model.UpdateCenter.UpdateCenterJob; +import hudson.model.UpdateSite; +import jenkins.model.Jenkins; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; + +/** + * Test + * @author tom.fennelly@gmail.com + */ +public class InstallUtilTest { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + @Before + public void setup() { + // JenkinsRule will have created the last exec file (indirectly), + // so remove it so we can fake the tests. + InstallUtil.getLastExecVersionFile().delete(); + // Disable the unit test flag. + Main.isUnitTest = false; + } + + @After + public void tearDown() { + // Reset the unit test flag back to its default. + Main.isUnitTest = true; + } + + /** + * Test jenkins startup sequences and the changes to the startup type.. + */ + @Test + public void test_typeTransitions() { + // A new test instance + Assert.assertEquals(InstallState.NEW, InstallUtil.getInstallState()); + + // Save the last exec version. This will only be done by Jenkins after one of: + // 1. A successful run of the install wizard. + // 2. A success upgrade. + // 3. A successful restart. + InstallUtil.saveLastExecVersion(); + + // Fudge things a little now, pretending there's a restart... + + // Now if we ask what is the InstallState, we should be told it's a RESTART because + // the install wizard is complete and the version matches the currently executing + // Jenkins version. + Assert.assertEquals(InstallState.RESTART, InstallUtil.getInstallState()); + + // Fudge things again, changing the stored version to something old, faking an upgrade... + InstallUtil.saveLastExecVersion("1.584"); + Assert.assertEquals(InstallState.UPGRADE, InstallUtil.getInstallState()); + + // Fudge things yet again, changing the stored version to something very very new, faking a downgrade... + InstallUtil.saveLastExecVersion("1000.0"); + Assert.assertEquals(InstallState.DOWNGRADE, InstallUtil.getInstallState()); + } + + + /** + * Test jenkins startup sequences and the changes to the startup type.. + */ + @Test + public void test_getLastExecVersion() throws Exception { + // Delete the config file, forcing getLastExecVersion to return + // the default/unset version value. + InstallUtil.getConfigFile().delete(); + Assert.assertEquals("1.0", InstallUtil.getLastExecVersion()); + + // Set the version to some stupid value and check again. This time, + // getLastExecVersion should read it from the file. + setStoredVersion("9.123"); + Assert.assertEquals("9.123", InstallUtil.getLastExecVersion()); + } + + private void setStoredVersion(String version) throws Exception { + Field versionField = Jenkins.class.getDeclaredField("version"); + versionField.setAccessible(true); + versionField.set(jenkinsRule.jenkins, version); + Assert.assertEquals(version, Jenkins.getStoredVersion().toString()); + // Force a save of the config.xml + jenkinsRule.jenkins.save(); + } + + /** + * Validate proper statuses are persisted and install status is cleared when invoking appropriate methods on {@link InstallUtil} + */ + @Test + public void testSaveAndRestoreInstallingPlugins() throws Exception { + final List updates = new ArrayList<>(); + + final Map nameMap = new HashMap<>(); + + new UpdateCenter() { // inner classes... + { + new UpdateSite("foo", "http://omg.org") { + { + for(String name : Arrays.asList("pending-plug:Pending", "installing-plug:Installing", "failure-plug:Failure", "success-plug:Success")) { + String statusType = name.split(":")[1]; + name = name.split(":")[0]; + + InstallationStatus status; + if("Success".equals(statusType)) { + status = Mockito.mock(Success.class); + } + else if("Failure".equals(statusType)) { + status = Mockito.mock(Failure.class); + } + else if("Installing".equals(statusType)) { + status = Mockito.mock(Installing.class); + } + else { + status = Mockito.mock(Pending.class); + } + + nameMap.put(statusType, status.getClass().getSimpleName()); + + JSONObject json = new JSONObject(); + json.put("name", name); + json.put("version", "1.1"); + json.put("url", "http://google.com"); + json.put("dependencies", new JSONArray()); + Plugin p = new Plugin(getId(), json); + + InstallationJob job = new InstallationJob(p, null, null, false); + job.status = status; + job.setCorrelationId(UUID.randomUUID()); // this indicates the plugin was 'directly selected' + updates.add(job); + } + } + }; + } + }; + + InstallUtil.persistInstallStatus(updates); + + Map persisted = InstallUtil.getPersistedInstallStatus(); + + Assert.assertEquals(nameMap.get("Pending"), persisted.get("pending-plug")); + Assert.assertEquals("Pending", persisted.get("installing-plug")); // only marked as success/fail after successful install + Assert.assertEquals(nameMap.get("Failure"), persisted.get("failure-plug")); + Assert.assertEquals(nameMap.get("Success"), persisted.get("success-plug")); + + InstallUtil.clearInstallStatus(); + + persisted = InstallUtil.getPersistedInstallStatus(); + + Assert.assertNull(persisted); // should be deleted + } +} diff --git a/war/.gitignore b/war/.gitignore index e500657ff9..7ea4dd8431 100644 --- a/war/.gitignore +++ b/war/.gitignore @@ -1,2 +1,10 @@ -work +work /rebel.xml + +# Node +node/ +node_modules/ +npm-debug.log + +# Generated JavaScript Bundles +jsbundles diff --git a/war/Gruntfile.js b/war/Gruntfile.js deleted file mode 100644 index 63507e430c..0000000000 --- a/war/Gruntfile.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = function (grunt) { - grunt.initConfig({ - - }); - - grunt.registerTask('build', [ - ]); - - grunt.registerTask('default', ['build']); -}; diff --git a/war/gulpfile.js b/war/gulpfile.js new file mode 100644 index 0000000000..6e137a91de --- /dev/null +++ b/war/gulpfile.js @@ -0,0 +1,15 @@ +// +// See https://github.com/tfennelly/jenkins-js-builder +// +var builder = require('jenkins-js-builder'); + +// +// Bundle the Install Wizard. +// See https://github.com/jenkinsci/js-builder#bundling +// +builder.bundle('src/main/js/pluginSetupWizard.js') + .withExternalModuleMapping('jquery-detached', 'core-assets/jquery-detached:jquery2') + .withExternalModuleMapping('bootstrap', 'core-assets/bootstrap:bootstrap3', {addDefaultCSS: true}) + .withExternalModuleMapping('handlebars', 'core-assets/handlebars:handlebars3') + .less('src/main/less/pluginSetupWizard.less') + .inDir('src/main/webapp/jsbundles'); diff --git a/war/package.json b/war/package.json index c35e61d1f8..37149ebe74 100644 --- a/war/package.json +++ b/war/package.json @@ -1,11 +1,20 @@ { - "name": "jenkins", - "version": "0.0.0", - "dependencies": {}, - "devDependencies": { - "grunt": "~0.4.1" - }, - "engines": { - "node": ">=0.8.0" - } -} \ No newline at end of file + "name": "jenkins-ui", + "version": "1.0.0", + "description": "Jenkins User Interface", + "license": "MIT", + "devDependencies": { + "gulp": "^3.9.0", + "handlebars": "^3.0.3", + "hbsfy": "^2.4.1", + "jenkins-handlebars-rt": "^1.0.1", + "jenkins-js-builder": "0.0.37", + "jenkins-js-test": "0.0.16" + }, + "dependencies": { + "bootstrap-detached": "^3.3.5-v1", + "jenkins-js-modules": "^1.4.0", + "jquery-detached": "^2.1.4-v2", + "window-handle": "0.0.6" + } +} diff --git a/war/pom.xml b/war/pom.xml index b37264fffa..d4706dd08e 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -41,6 +41,8 @@ THE SOFTWARE. ${basedir}/work /jenkins 8080 + 4.0.0 + 2.13.1 @@ -134,6 +136,24 @@ THE SOFTWARE. sshd 1.6 + + org.jenkins-ci.ui + jquery-detached + 1.2 + core-assets + + + org.jenkins-ci.ui + bootstrap + 1.3.1 + core-assets + + + org.jenkins-ci.ui + handlebars + 1.1 + core-assets + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/plugins + true + true + false + true + + + + detached-plugins + generate-resources + + copy + + + + ${project.groupId} maven-plugin @@ -368,7 +406,7 @@ THE SOFTWARE. hpi - ${project.build.directory}/${project.build.finalName}/WEB-INF/plugins + ${project.build.directory}/${project.build.finalName}/WEB-INF/detached-plugins true true false @@ -532,42 +570,217 @@ THE SOFTWARE. + + + + + + + node-classifier-linux + Linuxamd64 + + node-v${node.version}-linux-x64.tar.gz + + + + + node-classifier-mac + mac + + node-v${node.version}-darwin-x64.tar.gz + + + + + node-classifier-windows + windowsx64 + + win-x64/node.exe + /x64 + + + + node-classifier-windows-amd64 + windowsamd64 + + win-x64/node.exe + /x64 + + + + node-classifier-windows-x86 + windowsx86 + + win-x86/node.exe + + + + + node-download + package.json + + + + com.googlecode.maven-download-plugin + download-maven-plugin + 1.2.1 + + + get-node + initialize + + wget + + + https://nodejs.org/dist/v${node.version}/${node.download.file} + false + ${project.build.directory}/frontend/v${node.version}${node.download.classifier} + + + + get-npm + initialize + + wget + + + http://registry.npmjs.org/npm/-/npm-${npm.version}.tgz + false + ${project.build.directory}/frontend/ + npm-${npm.version}.tgz + + + + + + + - - grunt + gulp-execution + + + gulpfile.js + + - org.codehaus.mojo - exec-maven-plugin + org.apache.maven.plugins + maven-enforcer-plugin + 1.3.1 - install - validate + enforce-versions - exec + enforce - npm - - install - + + + 3.1.0 + + + + + + com.github.eirslett + frontend-maven-plugin + 0.0.23 + + + - grunt - prepare-package + initialize + install node and npm - exec + install-node-and-npm - grunt + v${node.version} + ${npm.version} + + ${project.baseUri}target/frontend/ + + + initialize + npm install + + npm + + + + install + + + + + generate-sources + gulp bundle + + gulp + + + bundle + + + + + test + gulp test + + gulp + + bundle test + + - + + + clean-node + + + package.json + + + cleanNode + + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + node + false + + + node_modules + false + + + + + + + + diff --git a/war/src/main/js/api/pluginManager.js b/war/src/main/js/api/pluginManager.js new file mode 100644 index 0000000000..6ef1c281c5 --- /dev/null +++ b/war/src/main/js/api/pluginManager.js @@ -0,0 +1,188 @@ +/** + * Provides a wrapper to interact with the plugin manager & update center + */ + +var jenkins = require('../util/jenkins'); + +// TODO: Get plugin info (plugins + recommended plugin list) from update center. +// For now, we statically store them in the wizard. + +var plugins = require('./plugins.js'); +plugins.names =[]; +for (var i = 0; i < plugins.availablePlugins.length; i++) { + var pluginCategory = plugins.availablePlugins[i]; + var categoryPlugins = pluginCategory.plugins; + for (var ii = 0; ii < categoryPlugins.length; ii++) { + var pluginName = categoryPlugins[ii].name; + if (plugins.names.indexOf(pluginName) === -1) { + plugins.names.push(pluginName); + } + } +} + +// default 10 seconds for AJAX responses to return before triggering an error condition +var pluginManagerErrorTimeoutMillis = 10 * 1000; + +/** + * Get the curated list of plugins to be offered in the wizard. + * @returns The curated list of plugins to be offered in the wizard. + */ +exports.plugins = function() { + return plugins.availablePlugins; +}; + +/** + * Get the curated list of plugins to be offered in the wizard by name only. + * @returns The curated list of plugins to be offered in the wizard by name only. + */ +exports.pluginNames = function() { + return plugins.names; +}; + +/** + * Get the subset of plugins (subset of the plugin list) that are recommended by default. + *

+ * The user can easily change this selection. + * @returns The subset of plugins (subset of the plugin list) that are recommended by default. + */ +exports.recommendedPluginNames = function() { + return plugins.recommendedPlugins; +}; + +/** + * Call this function to install plugins, will pass a correlationId to the complete callback which + * may be used to restrict further calls getting plugin lists. Note: do not use the correlation id. + * If handler is called with this.isError, there will be a corresponding this.errorMessage indicating + * the failure reason + */ +exports.installPlugins = function(plugins, handler) { + jenkins.post('/pluginManager/installPlugins', { dynamicLoad: true, plugins: plugins }, function(response) { + if(response.status !== 'ok') { + handler.call({ isError: true, errorMessage: response.message }); + return; + } + + handler.call({ isError: false }, response.data.correlationId); + }, { + timeout: pluginManagerErrorTimeoutMillis, + error: function(xhr, textStatus, errorThrown) { + handler.call({ isError: true, errorMessage: errorThrown }); + } + }); +}; + +/** + * Accepts 1 or 2 arguments, if argument 2 is not provided all installing plugins will be passed + * to the handler function. If argument 2 is non-null, it will be treated as a correlationId, which + * must be retrieved from a prior installPlugins call. + */ +exports.installStatus = function(handler, correlationId) { + var url = '/updateCenter/installStatus'; + if(correlationId !== undefined) { + url += '?correlationId=' + correlationId; + } + jenkins.get(url, function(response) { + if(response.status !== 'ok') { + handler.call({ isError: true, errorMessage: response.message }); + return; + } + + handler.call({ isError: false }, response.data); + }, { + timeout: pluginManagerErrorTimeoutMillis, + error: function(xhr, textStatus, errorThrown) { + handler.call({ isError: true, errorMessage: errorThrown }); + } + }); +}; + +/** + * Provides a list of the available plugins, some useful properties is: + * [ + * { name, title, excerpt, dependencies[], ... }, + * ... + * ] + */ +exports.availablePlugins = function(handler) { + jenkins.get('/pluginManager/plugins', function(response) { + if(response.status !== 'ok') { + handler.call({ isError: true, errorMessage: response.message }); + return; + } + + handler.call({ isError: false }, response.data); + }, { + timeout: pluginManagerErrorTimeoutMillis, + error: function(xhr, textStatus, errorThrown) { + handler.call({ isError: true, errorMessage: errorThrown }); + } + }); +}; + + +/** + * Accepts 1 or 2 arguments, if argument 2 is not provided all installing plugins will be passed + * to the handler function. If argument 2 is non-null, it will be treated as a correlationId, which + * must be retrieved from a prior installPlugins call. + */ +exports.incompleteInstallStatus = function(handler, correlationId) { + var url = '/updateCenter/incompleteInstallStatus'; + if(correlationId !== undefined) { + url += '?correlationId=' + correlationId; + } + jenkins.get(url, function(response) { + if(response.status !== 'ok') { + handler.call({ isError: true, errorMessage: response.message }); + return; + } + + handler.call({ isError: false }, response.data); + }, { + timeout: pluginManagerErrorTimeoutMillis, + error: function(xhr, textStatus, errorThrown) { + handler.call({ isError: true, errorMessage: errorThrown }); + } + }); +}; + +/** + * Call this to complete the installation without installing anything + */ +exports.completeInstall = function(handler) { + jenkins.get('/updateCenter/completeInstall', function() { + handler.call({ isError: false }); + }, { + timeout: pluginManagerErrorTimeoutMillis, + error: function(xhr, textStatus, errorThrown) { + handler.call({ isError: true, message: errorThrown }); + } + }); +}; + +/** + * Indicates there is a restart required to complete plugin installations + */ +exports.isRestartRequired = function(handler) { + jenkins.get('/updateCenter/api/json?tree=restartRequiredForCompletion', function(response) { + handler.call({ isError: false }, response.data); + }, { + timeout: pluginManagerErrorTimeoutMillis, + error: function(xhr, textStatus, errorThrown) { + handler.call({ isError: true, message: errorThrown }); + } + }); +}; + +/** + * Restart Jenkins + */ +exports.restartJenkins = function(handler) { + jenkins.get('/updateCenter/safeRestart', function() { + handler.call({ isError: false }); + }, { + timeout: pluginManagerErrorTimeoutMillis, + error: function(xhr, textStatus, errorThrown) { + handler.call({ isError: true, message: errorThrown }); + } + }); +}; diff --git a/war/src/main/js/api/plugins.js b/war/src/main/js/api/plugins.js new file mode 100644 index 0000000000..99eb62e132 --- /dev/null +++ b/war/src/main/js/api/plugins.js @@ -0,0 +1,86 @@ +// +// TODO: Get all of this information from the Update Center via a REST API. +// + +// +// TODO: Decide on what the real "recommended" plugin set is. This is just a 1st stab. +// Also remember, the user ultimately has full control as they can easily customize +// away from these. +// +exports.recommendedPlugins = [ + "antisamy-markup-formatter", + "credentials", + "junit", + "mailer", + "matrix-auth", + "script-security", + "subversion", + "translation" +]; + +// +// A Categorized list of the plugins offered for install in the wizard. +// This is a community curated list. +// +exports.availablePlugins = [ + { + "category": "General", + "description": "(a collection of things I cannot think of a better name for)", + "plugins": [ + { "name": "external-monitor-job" }, + { "name": "translation" } + ] + }, + { + "category":"Organization and Administration", + "plugins": [ + { "name": "antisamy-markup-formatter" } + ] + }, + { + "category":"Build Tools", + "plugins": [ + { "name": "ant" }, + { "name": "maven-plugin" } + ] + }, + { + "category":"Build Analysis and Reporting", + "plugins": [ + { "name": "javadoc" }, + { "name": "junit" } + ] + }, + { + "category":"SCM", + "plugins": [ + { "name": "cvs" }, + { "name": "subversion" } + ] + }, + { + "category":"Distributed Builds and Containers", + "plugins": [ + { "name": "matrix-project" }, + { "name": "ssh-slaves" }, + { "name": "windows-slaves" } + ] + }, + { + "category":"User Management and Security", + "plugins": [ + { "name": "credentials" }, + { "name": "ldap" }, + { "name": "matrix-auth" }, + { "name": "pam-auth" }, + { "name": "script-security" }, + { "name": "ssh-credentials" } + ] + }, + { + "category":"Notifications and Publishing", + "plugins": [ + { "name": "mailer" } + ] + } +]; \ No newline at end of file diff --git a/war/src/main/js/pluginSetupWizard.js b/war/src/main/js/pluginSetupWizard.js new file mode 100644 index 0000000000..38e577c3c8 --- /dev/null +++ b/war/src/main/js/pluginSetupWizard.js @@ -0,0 +1,10 @@ +// Initialize all modules by requiring them. Also makes sure they get bundled (see gulpfile.js). +var $ = require('jquery-detached').getJQuery(); + +// This is the main module +var pluginSetupWizard = require('./pluginSetupWizardGui'); + +// This entry point for the bundle only bootstraps the main module in a browser +$(function() { + pluginSetupWizard.init(); +}); diff --git a/war/src/main/js/pluginSetupWizardGui.js b/war/src/main/js/pluginSetupWizardGui.js new file mode 100644 index 0000000000..a8b304146b --- /dev/null +++ b/war/src/main/js/pluginSetupWizardGui.js @@ -0,0 +1,790 @@ +/** + * Jenkins first-run install wizard + */ + +// Require modules here, make sure they get browserify'd/bundled +var jquery = require('jquery-detached'); +var bootstrap = require('bootstrap-detached'); +var jenkins = require('./util/jenkins'); +var pluginManager = require('./api/pluginManager'); + +// Setup the dialog, exported +var createPluginSetupWizard = function() { + // call getJQuery / getBootstrap within the main function so it will work with tests -- if getJQuery etc is called in the main + var $ = jquery.getJQuery(); + var $bs = bootstrap.getBootstrap(); + + var Handlebars = jenkins.initHandlebars(); + + // Necessary handlebars helpers: + // returns the plugin count string per category selected vs. available e.g. (5/44) + Handlebars.registerHelper('pluginCountForCategory', function(cat) { + var plugs = categorizedPlugins[cat]; + var tot = 0; + var cnt = 0; + for(var i = 0; i < plugs.length; i++) { + var plug = plugs[i]; + if(plug.category === cat) { + tot++; + if(selectedPluginNames.indexOf(plug.plugin.name) >= 0) { + cnt++; + } + } + } + return '(' + cnt + '/' + tot + ')'; + }); + + // returns the total plugin count string selected vs. total e.g. (5/44) + Handlebars.registerHelper('totalPluginCount', function() { + var tot = 0; + var cnt = 0; + for(var i = 0; i < pluginList.length; i++) { + var a = pluginList[i]; + for(var c = 0; c < a.plugins.length; c++) { + var plug = a.plugins[c]; + tot++; + if(selectedPluginNames.indexOf(plug.name) >= 0) { + cnt++; + } + } + } + return '(' + cnt + '/' + tot + ')'; + }); + + // determines if the provided plugin is in the list currently selected + Handlebars.registerHelper('inSelectedPlugins', function(val, options) { + if(selectedPluginNames.indexOf(val) >= 0) { + return options.fn(); + } + }); + + // executes a block if there are dependencies + Handlebars.registerHelper('hasDependencies', function(plugName, options) { + var plug = availablePlugins[plugName]; + if(plug && plug.allDependencies && plug.allDependencies.length > 1) { // includes self + return options.fn(); + } + }); + + // gets user friendly dependency text + Handlebars.registerHelper('dependencyText', function(plugName) { + var plug = availablePlugins[plugName]; + if(!plug) { + return ''; + } + var deps = $.grep(plug.allDependencies, function(value) { // remove self + return value !== plugName; + }); + var out = ''; + for(var i = 0; i < deps.length; i++) { + if(i > 0) { + out += ', '; + } + var depName = deps[i]; + var dep = availablePlugins[depName]; + if(dep) { + out += dep.title; + } + } + return out; + }); + + // Include handlebars templates here - explicitly require them and they'll be available by hbsfy as part of the bundle process + var errorPanel = require('./templates/errorPanel.hbs'); + var loadingPanel = require('./templates/loadingPanel.hbs'); + var welcomePanel = require('./templates/welcomePanel.hbs'); + var progressPanel = require('./templates/progressPanel.hbs'); + var pluginSelectionPanel = require('./templates/pluginSelectionPanel.hbs'); + var successPanel = require('./templates/successPanel.hbs'); + var offlinePanel = require('./templates/offlinePanel.hbs'); + var pluginSetupWizard = require('./templates/pluginSetupWizard.hbs'); + var incompleteInstallationPanel = require('./templates/incompleteInstallationPanel.hbs'); + + // wrap calls with this method to handle generic errors returned by the plugin manager + var handleGenericError = function(success) { + return function() { + if(this.isError) { + var errorMessage = this.errorMessage; + if(!errorMessage || this.errorMessage === 'timeout') { + errorMessage = translations.installWizard_error_connection; + } + else { + errorMessage = translations.installWizard_error_message + " " + errorMessage; + } + setPanel(errorPanel, { errorMessage: errorMessage }); + return; + } + success.apply(this, arguments); + }; + }; + + // state variables for plugin data, selected plugins, etc.: + var pluginList = pluginManager.plugins(); + var allPluginNames = pluginManager.pluginNames(); + var selectedPluginNames = pluginManager.recommendedPluginNames(); + var categories = []; + var availablePlugins = {}; + var categorizedPlugins = {}; + + // Instantiate the wizard panel + var $wizard = $(pluginSetupWizard()); + $wizard.appendTo('body'); + var $container = $wizard.find('.modal-content'); + var currentPanel; + + // show tooltips; this is done here to work around a bootstrap/prototype incompatibility + $(document).on('mouseenter', '*[data-tooltip]', function() { + var $tip = $bs(this); + var text = $tip.attr('data-tooltip'); + if(!text) { + return; + } + // prototype/bootstrap tooltip incompatibility - triggering main element to be hidden + this.hide = undefined; + $tip.tooltip({ + html: true, + title: text + }).tooltip('show'); + }); + + // localized messages + var translations = {}; + + var decorations = [ + function() { + // any decorations after DOM replacement go here + } + ]; + + // call this to set the panel in the app, this performs some additional things & adds common transitions + var setPanel = function(panel, data, oncomplete) { + var decorate = function($base) { + for(var i = 0; i < decorations.length; i++) { + decorations[i]($base); + } + }; + var html = panel($.extend({translations: translations}, data)); + if(panel === currentPanel) { // just replace id-marked elements + var $upd = $(html); + $upd.find('*[id]').each(function() { + var $el = $(this); + var $existing = $('#'+$el.attr('id')); + if($existing.length > 0) { + if($el[0].outerHTML !== $existing[0].outerHTML) { + $existing.replaceWith($el); + decorate($el); + } + } + }); + + if(oncomplete) { + oncomplete(); + } + } + else { + var append = function() { + currentPanel = panel; + $container.append(html); + decorate($container); + + if(oncomplete) { + oncomplete(); + } + }; + var $modalBody = $container.find('.modal-body'); + if($modalBody.length > 0) { + $modalBody.stop(true).fadeOut(250, function() { + $container.children().remove(); + append(); + }); + } + else { + $container.children().remove(); + append(); + } + } + }; + + // plugin data for the progress panel + var installingPlugins = []; + + // recursively get all the dependencies for a particular plugin, this is used to show 'installing' status + // when only dependencies are being installed + var getAllDependencies = function(pluginName, deps) { + if(!deps) { // don't get stuck + deps = []; + getAllDependencies(pluginName, deps); + return deps; + } + if(deps.indexOf(pluginName) >= 0) { + return; + } + deps.push(pluginName); + + var plug = availablePlugins[pluginName]; + if(plug) { + if(plug.dependencies) { + // plug.dependencies is { "some-plug": "1.2.99", ... } + for(var k in plug.dependencies) { + getAllDependencies(k, deps); + } + } + if(plug.neededDependencies) { + // plug.neededDependencies is [ { name: "some-plug", ... }, ... ] + for(var i = 0; i < plug.neededDependencies.length; i++) { + getAllDependencies(plug.neededDependencies[i].name, deps); + } + } + } + }; + + // Initializes the set of installing plugins with pending statuses + var initInstallingPluginList = function() { + installingPlugins = []; + installingPlugins.names = []; + for (var i = 0; i < selectedPluginNames.length; i++) { + var pluginName = selectedPluginNames[i]; + var p = availablePlugins[ pluginName]; + if (p) { + var plug = $.extend({ + installStatus : 'pending' + }, p); + installingPlugins.push(plug); + installingPlugins[plug.name] = plug; + installingPlugins.names.push(pluginName); + } + } + }; + + // call this to go install the selected set of plugins + var installPlugins = function(plugins) { + pluginManager.installPlugins(plugins, handleGenericError(function() { + showInstallProgress(); + })); + + setPanel(progressPanel, { installingPlugins : installingPlugins }); + }; + + // install the default plugins + var installDefaultPlugins = function() { + loadPluginData(function() { + installPlugins(pluginManager.recommendedPluginNames()); + }); + }; + + // Define actions + var showInstallProgress = function() { + initInstallingPluginList(); + setPanel(progressPanel, { installingPlugins : installingPlugins }); + + // call to the installStatus, update progress bar & plugin details; transition on complete + var updateStatus = function() { + pluginManager.installStatus(handleGenericError(function(jobs) { + var i, j; + var complete = 0; + var total = 0; + var restartRequired = false; + + for(i = 0; i < jobs.length; i++) { + j = jobs[i]; + total++; + if(/.*Success.*/.test(j.installStatus) || /.*Fail.*/.test(j.installStatus)) { + complete++; + } + } + + if(total === 0) { // don't end while there are actual pending plugins + total = installingPlugins.length; + } + + // update progress bar + $('.progress-bar').css({width: ((100.0 * complete)/total) + '%'}); + + // update details + var $c = $('.install-text'); + $c.children().remove(); + + for(i = 0; i < jobs.length; i++) { + j = jobs[i]; + var txt = false; + var state = false; + if('true' === j.requiresRestart) { + restartRequired = true; + } + + if(/.*Success.*/.test(j.installStatus)) { + txt = j.title; + state = 'success'; + } + else if(/.*Install.*/.test(j.installStatus)) { + txt = j.title; + state = 'installing'; + } + else if(/.*Fail.*/.test(j.installStatus)) { + txt = j.title; + state = 'fail'; + } + + if(txt && state) { + for(var installingIdx = 0; installingIdx < installingPlugins.length; installingIdx++) { + var installing = installingPlugins[installingIdx]; + if(installing.name === j.name) { + installing.installStatus = state; + } + else if(installing.installStatus === 'pending' && // if no progress + installing.allDependencies.indexOf(j.name) >= 0 && // and we have a dependency + ('installing' === state || 'success' === state)) { // installing or successful + installing.installStatus = 'installing'; // show this is installing + } + } + + var isSelected = selectedPluginNames.indexOf(j.name) < 0 ? false : true; + var $div = $('

'+txt+'
'); + if(isSelected) { + $div.addClass('selected'); + } + else { + $div.addClass('dependent'); + } + $c.append($div); + + var $itemProgress = $('.selected-plugin[id="installing-' + jenkins.idIfy(j.name) + '"]'); + if($itemProgress.length > 0 && !$itemProgress.is('.'+state)) { + $itemProgress.addClass(state); + } + } + } + + $c = $('.install-console'); + if($c.is(':visible')) { + $c.scrollTop($c[0].scrollHeight); + } + + // keep polling while install is running + if(complete < total) { + setPanel(progressPanel, { installingPlugins : installingPlugins }); + // wait a sec + setTimeout(updateStatus, 250); + } + else { + // mark complete + $('.progress-bar').css({width: '100%'}); + setPanel(successPanel, { + installingPlugins : installingPlugins, + restartRequired: restartRequired + }); + } + })); + }; + + // kick it off + setTimeout(updateStatus, 250); + }; + + // Called to complete the installation + var finishInstallation = function() { + jenkins.goTo('/'); + }; + + // load the plugin data, callback + var loadPluginData = function(oncomplete) { + pluginManager.availablePlugins(handleGenericError(function(availables) { + var i, plug; + for(i = 0; i < availables.length; i++) { + plug = availables[i]; + availablePlugins[plug.name] = plug; + } + for(i = 0; i < availables.length; i++) { + plug = availables[i]; + plug.allDependencies = getAllDependencies(plug.name); + } + oncomplete(); + })); + }; + + // load the custom plugin panel, will result in an AJAX call to get the plugin data + var loadCustomPluginPanel = function() { + loadPluginData(function() { + categories = []; + for(var i = 0; i < pluginList.length; i++) { + var a = pluginList[i]; + categories.push(a.category); + var plugs = categorizedPlugins[a.category] = []; + for(var c = 0; c < a.plugins.length; c++) { + var plugInfo = a.plugins[c]; + var plug = availablePlugins[plugInfo.name]; + if(!plug) { + plug = { + name: plugInfo.name, + title: plugInfo.name + }; + } + plugs.push({ + category: a.category, + plugin: $.extend({}, plug, { + usage: plugInfo.usage, + title: plugInfo.title ? plugInfo.title : plug.title, + excerpt: plugInfo.excerpt ? plugInfo.excerpt : plug.excerpt, + updated: new Date(plug.buildDate) + }) + }); + } + } + setPanel(pluginSelectionPanel, pluginSelectionPanelData(), function() { + $bs('.plugin-selector .plugin-list').scrollspy({ target: '.plugin-selector .categories' }); + }); + }); + }; + + // get plugin selection panel data object + var pluginSelectionPanelData = function() { + return { + categories: categories, + categorizedPlugins: categorizedPlugins, + selectedPlugins: selectedPluginNames + }; + }; + + // remove a plugin from the selected list + var removePlugin = function(arr, item) { + for (var i = arr.length; i--;) { + if (arr[i] === item) { + arr.splice(i, 1); + } + } + }; + + // add a plugin to the selected list + var addPlugin = function(arr, item) { + arr.push(item); + }; + + // refreshes the plugin selection panel; call this if anything changes to ensure everything is kept in sync + var refreshPluginSelectionPanel = function() { + setPanel(pluginSelectionPanel, pluginSelectionPanelData()); + if(lastSearch !== '') { + searchForPlugins(lastSearch, false); + } + }; + + // handle clicking an item in the plugin list + $wizard.on('change', '.plugin-list input[type=checkbox]', function() { + var $input = $(this); + if($input.is(':checked')) { + addPlugin(selectedPluginNames, $input.attr('name')); + } + else { + removePlugin(selectedPluginNames, $input.attr('name')); + } + + refreshPluginSelectionPanel(); + }); + + // walk the elements and search for the text + var walk = function(elements, element, text, xform) { + var i, child, n= element.childNodes.length; + for (i = 0; i 0) { + if(lastSearch !== term) { + findIndex = 0; + } + else { + findIndex = (findIndex+1) % matches.length; + } + var $el = $(matches[findIndex]); + $el = $el.parents('label:first'); // scroll to the block + if($el && $el.length > 0) { + var pos = $pl.scrollTop() + $el.position().top; + $pl.stop(true).animate({ + scrollTop: pos + }, 100); + setTimeout(function() { // wait for css transitions to finish + var pos = $pl.scrollTop() + $el.position().top; + $pl.stop(true).animate({ + scrollTop: pos + }, 50); + }, 50); + } + } + }; + + // search for given text, optionally scroll to the next match, set classes on appropriate elements from the search match + var searchForPlugins = function(text, scroll) { + var $pl = $('.plugin-list'); + var $containers = $pl.find('label'); + + // must always do this, as it's called after refreshing the panel (e.g. check/uncheck plugs) + $containers.removeClass('match'); + $pl.find('h2').removeClass('match'); + + if(text.length > 1) { + if(text === 'show:selected') { + $('.plugin-list .selected').addClass('match'); + } + else { + var matches = findElementsWithText($pl[0], text.toLowerCase(), function(d) { return d.toLowerCase(); }); + $(matches).parents('label').addClass('match'); + if(lastSearch !== text && scroll) { + scrollPlugin($pl, matches, text); + } + } + $('.match').parent().prev('h2').addClass('match'); + $pl.addClass('searching'); + } + else { + findIndex = 0; + $pl.removeClass('searching'); + } + lastSearch = text; + }; + + // handle input for the search here + $wizard.on('keyup change', '.plugin-select-controls input[name=searchbox]', function() { + var val = $(this).val(); + searchForPlugins(val, true); + }); + + // handle clearing the search + $wizard.on('click', '.clear-search', function() { + $('input[name=searchbox]').val(''); + searchForPlugins('', false); + }); + + // toggles showing the selected items as a simple search + var toggleSelectedSearch = function() { + var $srch = $('input[name=searchbox]'); + var val = 'show:selected'; + if($srch.val() === val) { + val = ''; + } + $srch.val(val); + searchForPlugins(val, false); + }; + + // handle clicking on the category + var selectCategory = function() { + $('input[name=searchbox]').val(''); + searchForPlugins('', false); + var $el = $($(this).attr('href')); + var $pl = $('.plugin-list'); + var top = $pl.scrollTop() + $el.position().top; + $pl.stop(true).animate({ + scrollTop: top + }, 250, function() { + var top = $pl.scrollTop() + $el.position().top; + $pl.stop(true).scrollTop(top); + }); + }; + + // handle show/hide details during the installation progress panel + var toggleInstallDetails = function() { + var $c = $('.install-console'); + if($c.is(':visible')) { + $c.slideUp(); + } + else { + $c.slideDown(); + } + }; + + // Call this to resume an installation after restart + var resumeInstallation = function() { + // don't re-initialize installing plugins + initInstallingPluginList = function() { }; + selectedPluginNames = []; + for(var i = 0; i < installingPlugins.length; i++) { + var plug = installingPlugins[i]; + if(plug.installStatus === 'pending') { + selectedPluginNames.push(plug.name); + } + } + installPlugins(selectedPluginNames); + }; + + // restart jenkins + var restartJenkins = function() { + pluginManager.restartJenkins(function() { + setPanel(loadingPanel); + + console.log('-------------------'); + console.log('Waiting for Jenkins to come back online...'); + console.log('-------------------'); + var pingUntilRestarted = function() { + pluginManager.isRestartRequired(function(isRequired) { + if(this.isError || isRequired) { + console.log('Waiting...'); + setTimeout(pingUntilRestarted, 1000); + } + else { + jenkins.goTo('/'); + } + }); + }; + + pingUntilRestarted(); + }); + }; + + // close the installer, mark not to show again + var closeInstaller = function() { + pluginManager.completeInstall(handleGenericError(function() { + jenkins.goTo('/'); + })); + }; + + // scoped click handler, prevents default actions automatically + var bindClickHandler = function(cls, action) { + $wizard.on('click', cls, function(e) { + action.apply(this, arguments); + e.preventDefault(); + }); + }; + + // click action mappings + var actions = { + '.install-recommended': installDefaultPlugins, + '.install-custom': loadCustomPluginPanel, + '.install-home': function() { setPanel(welcomePanel); }, + '.install-selected': function() { installPlugins(selectedPluginNames); }, + '.toggle-install-details': toggleInstallDetails, + '.install-done': finishInstallation, + '.plugin-select-all': function() { selectedPluginNames = allPluginNames; refreshPluginSelectionPanel(); }, + '.plugin-select-none': function() { selectedPluginNames = []; refreshPluginSelectionPanel(); }, + '.plugin-select-recommended': function() { selectedPluginNames = pluginManager.recommendedPluginNames(); refreshPluginSelectionPanel(); }, + '.plugin-show-selected': toggleSelectedSearch, + '.select-category': selectCategory, + '.close': closeInstaller, + '.resume-installation': resumeInstallation, + '.install-done-restart': restartJenkins + }; + for(var cls in actions) { + bindClickHandler(cls, actions[cls]); + } + + // do this so the page isn't blank while doing connectivity checks and other downloads + setPanel(loadingPanel); + + // kick off to get resource bundle + jenkins.loadTranslations('jenkins.install.pluginSetupWizard', handleGenericError(function(localizations) { + translations = localizations; + + // check for connectivity + jenkins.testConnectivity(handleGenericError(function(isConnected) { + if(!isConnected) { + setPanel(offlinePanel); + return; + } + + // check for updates when first loaded... + pluginManager.installStatus(handleGenericError(function(jobs) { + if(jobs.length > 0) { + if (installingPlugins.length === 0) { + // This can happen on a page reload if we are in the middle of + // an install. So, lets get a list of plugins being installed at the + // moment and use that as the "selectedPlugins" list. + selectedPluginNames = []; + loadPluginData(handleGenericError(function() { + for (var i = 0; i < jobs.length; i++) { + // If the job does not have a 'correlationId', then it was not selected + // by the user for install i.e. it's probably a dependency plugin. + if (jobs[i].correlationId) { + selectedPluginNames.push(jobs[i].name); + } + } + showInstallProgress(); + })); + } else { + showInstallProgress(); + } + return; + } + + // check for crash/restart with uninstalled initial plugins + pluginManager.incompleteInstallStatus(handleGenericError(function(incompleteStatus) { + var incompletePluginNames = []; + for(var plugName in incompleteStatus) { + incompletePluginNames.push(plugName); + } + + if(incompletePluginNames.length > 0) { + selectedPluginNames = incompletePluginNames; + loadPluginData(handleGenericError(function() { + initInstallingPluginList(); + + for(var plugName in incompleteStatus) { + var j = installingPlugins[plugName]; + + if (!j) { + console.warn('Plugin "' + plugName + '" not found in the list of installing plugins.'); + console.warn('\tInstalling plugins: ' + installingPlugins.names); + continue; + } + + var txt = false; + var state = false; + var status = incompleteStatus[plugName]; + + if(/.*Success.*/.test(status)) { + txt = j.title; + state = 'success'; + } + else if(/.*Install.*/.test(status)) { + txt = j.title; + state = 'pending'; + } + else if(/.*Fail.*/.test(status)) { + txt = j.title; + state = 'fail'; + } + + if(state) { + j.installStatus = state; + } + } + setPanel(incompleteInstallationPanel, { installingPlugins : installingPlugins }); + })); + return; + } + + // finally, show the installer + // If no active install, by default, we'll show the welcome screen + setPanel(welcomePanel); + + // focus on default + $('.install-recommended').focus(); + + })); + })); + })); + })); +}; + +// export wizard creation method +exports.init = createPluginSetupWizard; diff --git a/war/src/main/js/templates/errorPanel.hbs b/war/src/main/js/templates/errorPanel.hbs new file mode 100644 index 0000000000..f72fe12786 --- /dev/null +++ b/war/src/main/js/templates/errorPanel.hbs @@ -0,0 +1,11 @@ + + diff --git a/war/src/main/js/templates/incompleteInstallationPanel.hbs b/war/src/main/js/templates/incompleteInstallationPanel.hbs new file mode 100644 index 0000000000..36d7daadd1 --- /dev/null +++ b/war/src/main/js/templates/incompleteInstallationPanel.hbs @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/war/src/main/js/templates/loadingPanel.hbs b/war/src/main/js/templates/loadingPanel.hbs new file mode 100644 index 0000000000..dfed339ea9 --- /dev/null +++ b/war/src/main/js/templates/loadingPanel.hbs @@ -0,0 +1 @@ +
diff --git a/war/src/main/js/templates/offlinePanel.hbs b/war/src/main/js/templates/offlinePanel.hbs new file mode 100644 index 0000000000..1a38c81117 --- /dev/null +++ b/war/src/main/js/templates/offlinePanel.hbs @@ -0,0 +1,10 @@ + + diff --git a/war/src/main/js/templates/pluginSelectionPanel.hbs b/war/src/main/js/templates/pluginSelectionPanel.hbs new file mode 100644 index 0000000000..ef511786cc --- /dev/null +++ b/war/src/main/js/templates/pluginSelectionPanel.hbs @@ -0,0 +1,57 @@ + + + diff --git a/war/src/main/js/templates/pluginSetupWizard.hbs b/war/src/main/js/templates/pluginSetupWizard.hbs new file mode 100644 index 0000000000..0eca05b4e3 --- /dev/null +++ b/war/src/main/js/templates/pluginSetupWizard.hbs @@ -0,0 +1,7 @@ +
+ +
\ No newline at end of file diff --git a/war/src/main/js/templates/progressPanel.hbs b/war/src/main/js/templates/progressPanel.hbs new file mode 100644 index 0000000000..5d7535ac2f --- /dev/null +++ b/war/src/main/js/templates/progressPanel.hbs @@ -0,0 +1,21 @@ + + diff --git a/war/src/main/js/templates/successPanel.hbs b/war/src/main/js/templates/successPanel.hbs new file mode 100644 index 0000000000..1889ffc2b4 --- /dev/null +++ b/war/src/main/js/templates/successPanel.hbs @@ -0,0 +1,27 @@ + + diff --git a/war/src/main/js/templates/welcomePanel.hbs b/war/src/main/js/templates/welcomePanel.hbs new file mode 100644 index 0000000000..91e1995224 --- /dev/null +++ b/war/src/main/js/templates/welcomePanel.hbs @@ -0,0 +1,31 @@ + + diff --git a/war/src/main/js/util/jenkins.js b/war/src/main/js/util/jenkins.js new file mode 100644 index 0000000000..ffc5ab8be9 --- /dev/null +++ b/war/src/main/js/util/jenkins.js @@ -0,0 +1,188 @@ +/** + * Jenkins JS Modules common utility functions + */ + +// Get the modules + +var jquery = require('jquery-detached'); +var wh = require('window-handle'); + +var debug = false; + +// gets the base Jenkins URL including context path +exports.baseUrl = function() { + var $ = jquery.getJQuery(); + var u = $('head').attr('data-rooturl'); + if(!u) { + u = ''; + } + return u; +}; + +// awful hack to get around JSONifying things with Prototype taking over wrong. ugh. Prototype is the worst. +exports.stringify = function(o) { + if(Array.prototype.toJSON) { // Prototype f's this up something bad + var protoJSON = { + a: Array.prototype.toJSON, + o: Object.prototype.toJSON, + h: Hash.prototype.toJSON, + s: String.prototype.toJSON + }; + try { + delete Array.prototype.toJSON; + delete Object.prototype.toJSON; + delete Hash.prototype.toJSON; + delete String.prototype.toJSON; + + return JSON.stringify(o); + } + finally { + if(protoJSON.a) { + Array.prototype.toJSON = protoJSON.a; + } + if(protoJSON.o) { + Object.prototype.toJSON = protoJSON.o; + } + if(protoJSON.h) { + Hash.prototype.toJSON = protoJSON.h; + } + if(protoJSON.s) { + String.prototype.toJSON = protoJSON.s; + } + } + } + else { + return JSON.stringify(o); + } +}; + +/** + * Take a string and replace non-id characters to make it a friendly-ish XML id + */ +exports.idIfy = function(str) { + return (''+str).replace(/\W+/g, '_'); +}; + +/** + * redirect + */ +exports.goTo = function(url) { + wh.getWindow().location.replace(exports.baseUrl() + url); +}; + +/** + * Jenkins AJAX GET callback. + * If last parameter is an object, will be extended to jQuery options (e.g. pass { error: function() ... } to handle errors) + */ +exports.get = function(url, success, options) { + if(debug) { + console.log('get: ' + url); + } + var $ = jquery.getJQuery(); + var args = { + url: exports.baseUrl() + url, + type: 'GET', + cache: false, + dataType: 'json', + success: success + }; + if(options instanceof Object) { + $.extend(args, options); + } + $.ajax(args); +}; + +/** + * Jenkins AJAX POST callback, formats data as a JSON object post (note: works around prototype.js ugliness using stringify() above) + * If last parameter is an object, will be extended to jQuery options (e.g. pass { error: function() ... } to handle errors) + */ +exports.post = function(url, data, success, options) { + if(debug) { + console.log('post: ' + url); + } + var $ = jquery.getJQuery(); + var args = { + url: exports.baseUrl() + url, + type: 'POST', + cache: false, + dataType: 'json', + data: exports.stringify(data), + contentType: "application/json", + success: success + }; + if(options instanceof Object) { + $.extend(args, options); + } + $.ajax(args); +}; + +/** + * handlebars setup, this does not seem to actually work or get called by the require() of this file, so have to explicitly call it + */ +exports.initHandlebars = function() { + var Handlebars = require('handlebars'); + + Handlebars.registerHelper('ifeq', function(o1, o2, options) { + if(o1 === o2) { + return options.fn(); + } + }); + + Handlebars.registerHelper('ifneq', function(o1, o2, options) { + if(o1 !== o2) { + return options.fn(); + } + }); + + Handlebars.registerHelper('in-array', function(arr, val, options) { + if(arr.indexOf(val) >= 0) { + return options.fn(); + } + }); + + Handlebars.registerHelper('id', exports.idIfy); + + return Handlebars; +}; + +/** + * Load translations for the given bundle ID, provide the message object to the handler. + * Optional error handler as the last argument. + */ +exports.loadTranslations = function(bundleName, handler, onError) { + exports.get('/i18n/resourceBundle?baseName=' +bundleName, function(res) { + if(res.status !== 'ok') { + if(onError) { + onError(res.message); + } + throw 'Unable to load localization data: ' + res.message; + } + + handler(res.data); + }); +}; + +/** + * Runs a connectivity test, calls handler with a boolean whether there is sufficient connectivity to the internet + */ +exports.testConnectivity = function(handler) { + // check the connectivity api + var testConnectivity = function() { + exports.get('/updateCenter/connectionStatus?siteId=default', function(response) { + var uncheckedStatuses = ['PRECHECK', 'CHECKING', 'UNCHECKED']; + if(uncheckedStatuses.indexOf(response.data.updatesite) >= 0 || uncheckedStatuses.indexOf(response.data.internet) >= 0) { + setTimeout(testConnectivity, 100); + } + else { + if(response.status !== 'ok' || response.data.updatesite !== 'OK' || response.data.internet !== 'OK') { + // no connectivity + handler(false); + } + else { + handler(true); + } + } + }); + }; + testConnectivity(); +}; diff --git a/war/src/main/less/pluginSetupWizard.less b/war/src/main/less/pluginSetupWizard.less new file mode 100644 index 0000000000..46ba9919ad --- /dev/null +++ b/war/src/main/less/pluginSetupWizard.less @@ -0,0 +1,748 @@ +@import (css) 'https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css'; +@import (css) '../css/icons/icomoon.css'; +@import url(https://fonts.googleapis.com/css?family=Roboto:400,300,500,900,700); + + +.plugin-setup-wizard() { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1000; + background: rgba(0,0,0,.2); + text-align: initial; + font-family: sans-serif; + + + table, form, input, td, th, p, textarea, select,h1,h2,h3,h4,h5{ + font-family:'roboto',sans-serif; + } + + + input,password { // get rid of the ms X (it doesn't fire a change event!), we provide our own + &::-ms-clear { width : 0; height: 0; } + &::-ms-reveal { width : 0; height: 0; } + } + + a, button { + &:focus, &:active { outline: 0; } + } + + .spin(@duration: 2s) { + -webkit-animation-name: spin; + -webkit-animation-duration: @duration; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + -moz-animation-name: spin; + -moz-animation-duration: @duration; + -moz-animation-iteration-count: infinite; + -moz-animation-timing-function: linear; + -ms-animation-name: spin; + -ms-animation-duration: @duration; + -ms-animation-iteration-count: infinite; + -ms-animation-timing-function: linear; + animation-name: spin; + animation-duration: @duration; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + + .no-spin() { + -webkit-animation-name: none; + -moz-animation-name: none; + -ms-animation-name: none; + animation-name: none; + } + + .modal-dialog { + width: 90%; + height: 90%; + padding: 0; + position: relative; + margin: 3% auto; + max-width:992px; + font-family:'roboto',sans-serif; + } + + .modal-content { + height: 100%; + position: relative; + box-sizing: border-box; + padding: 4.2em 0 4.8em 0; + } + + .container.error-container { + padding: 4em 3em; + + h1 { + color: #a94442; + } + } + + .modal-body { + background: linear-gradient(175deg, #f8f8f8 0%, rgba(255, 255, 255, 1) 22%, rgba(255, 255, 255, 0) 100%); + overflow-y: auto; + height: 100%; + padding:0; + border-bottom:1px solid #ccc; + + .water-mark{ + font-size: 392px; + position: absolute; + display: block; + bottom: 0px; + right: 50px; + color: rgba(0,0,0,.05); + } + + .installing-panel{ + border-bottom:1px solid #ccc; + margin:0; + padding:7.5% 10%; + height:33.3%; + position:relative; + z-index:9; + box-shadow:rgba(0,0,0,.5) 0 1px 5px; + + @media screen and (max-width: 992px) { + padding:5%; + h1{ + font-size:24px; + } + } + + @media screen and (max-width: 768px) { + padding:5%; + height:auto; + h1{ + display:none; + } + } + + .progress{ + margin:0; + } + + .selected-plugin-progress{ + width:75%; + } + } + + } + + .modal-header { + position: absolute; + top: 0; + right: 0; + left: 0; + height: 4.2em; + z-index: 2; + background: #fff; + border-radius: 5px 5px 0 0; + + .close { + line-height: 1.42857143; + margin-right: 5px; + } + } + + .modal-footer { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 4.8em; + padding: 1em; + z-index: 2; + background: #fff; + border-radius: 0 0 5px 5px; + } + + .plugin-selector { + overflow-y: hidden; + + @media screen and (min-width: 768px) { + height: 100%; + } + + .plugins { + padding-top: 2.75em; + position: relative; + border-left:1px solid rgba(0,0,0,.2); + + @media screen and (min-width: 768px) { + height: 100%; + } + + h2 {padding-top:10px; margin-top:0} + + .plugin-select-actions, .plugin-selected-info { + position: absolute; + top: 0; + left: 10px; + line-height: 34px; + + &.plugin-selected-info { + right: 10px; + left: auto; + } + + a + a:before { + content: '|'; + font-size: 11px; + text-decoration: none !important; + display: inline-block; + margin: 0 5px 0 2px; + } + } + + .plugin-search-controls { + position: relative; + display: block; + margin: 0 -17px; + + @media screen and (min-width: 768px) { + width: 100%; + max-width: 32em; + margin: 0; + + input[name=searchbox] { + display: inline-block; + } + } + + .clear-search { + display: inline-block; + position: absolute; + top: 50%; + right: 8px; + margin-top: -12px; + font-size: 23px; + line-height: 30px; + background: transparent; + color: #ddd; + text-decoration: none; + font-weight: bold; + + &:hover, &:focus { + color: #444; + } + } + } + + .plugin-select-controls { + @media screen and (min-width: 768px) { + position: absolute; + padding: 0 9em 0 14em; + } + + top: 0; + right: 0; + left: 0; + padding: 0 10px; + height: 2.75em; + z-index: 100; + background:rgba(0,0,0,.03); + border-bottom:1px solid rgba(0,0,0,.2); + + + .form-control{ + font-size:13px; + padding:2px 5px; + height:30px; + top:2px; + position:relative; + } + } + + .plugin-list-description { + margin: -10px -10px 0px -10px; + padding: 10px 20px; + opacity: 0.7; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + } + + .plugin-list { + position: relative; + padding: 10px; + margin: 0 -15px; + clear: both; + @media screen and (min-width: 768px) { + max-height: 100%; + } + overflow-y: auto; + + h2 { + font-size:28px; + transition: all .15s; + &:first-child { + margin-top: 0; + } + } + + &.searching-with-transition { + label { + display: none; + transition: all .25s; + + + &.match { + display: block; + background: #d9edf7; + margin: 4px -7px; + padding: 6px 0 6px 26px; + border-radius: 2px; + border: 1px solid #bce8f1; + color: #31708f; + + &.selected { + background: #dff0d8; + border: 1px solid #c2e0a9; + color: #3c763d; + } + } + } + h2 { + color: #ddd; + transition: all .5s; + font-size: 22px; + margin: 6px 0 6px -5px; + } + } + + &.searching { + label { + display: none; + transition: all .25s; + + &.match { + display: block; + } + } + h2 { + display: none; + &.match { + color: #ddd; + transition: all .5s; + font-size: 22px; + margin: 6px 0 6px -5px; + display: block; + } + } + } + + label { + font-weight: normal; + padding: 7px 2px 7px 27px; + position: relative; + display: block; + cursor: pointer; + margin: 4px -7px; + + &:hover{ + background: rgba(100, 200, 255, 0.1); + box-shadow:inset 0 200px 200px -200px rgba(100, 200, 255, 0.33),inset 0 0 0 1px rgba(100, 200, 255, 0.33); + } + + &.selected { + background: #dff0d8; + margin: 4px -7px; + padding: 6px 0 6px 26px; + border-radius: 2px; + border: 1px solid #C2E0A9; + color: #3c763d; + } + + .title { + display: block; + font-weight: bold; + white-space: nowrap; + position: relative; + } + + .description { + display: block; + margin-top: .5em; + } + + &.selected .dependencies { + color: #777; + } + + input[type=checkbox] { + float: left; + margin: 2px -18px; + vertical-align: middle; + } + + + .tooltip { + left: 2px !important; + margin-right: 16px; + + .tooltip-arrow { + left: 4em !important; + } + + .tooltip-inner { + max-width: none; + padding: .5em 1em; + text-align: left; + } + } + } + } + } + + .categories { + position: relative; + display: none; + overflow-y: auto; + + @media screen and (min-width: 768px) { + display: block; + max-height: 100%; + } + + .nav{ + margin:5px -5px; + } + + li { + a { + position: relative; + padding: .5em; + display: block; + color: #444; + text-decoration: none; + background: transparent; + border: none; + font-weight:500; + border-radius:none; + + &:after { + content: ''; + display: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 2px; + background: #337ab7; + } + + &:hover { + background:rgba(100,200,255,.1); + box-shadow:inset -200px 0 200px -200px rgba(100,200,255,.33); + color: #337ab7; + + &:after { + display: block; + } + } + } + + &.active a { + color: #337ab7; + + &:after { + display: block; + width: 4px; + } + } + } + } + } + + .install-console { + border-left: 1px solid #ccc; + background: #f8f8f8; + position: absolute; + right: 0; + bottom: 0; + display: block; + z-index: 5; + top: 33.3%; + width: 25%; + padding: 6px 0; + overflow:auto; + + @media screen and (max-width: 992px) { + width: 100%; + position:static; + max-height:33%; + } + + .selected { + font: 12px monospace; + overflow: auto; + height: 100%; + display:block; + padding:1px 10px; + + &:nth-child(odd){ + background:rgba(0,0,0,.05); + } + } + + .dependent { + color: #aaa; + padding-left: .4em; + + &:before { + content: ' ** '; + } + } + } + + .selected-plugin-progress { + position: absolute; + bottom: 0; + overflow: hidden; + overflow-y: auto; + top: 33.3%; + margin: 0; + padding: 0; + width: 75%; + font-size: 0; + background:rgba(0,0,0,.1); + border-bottom:1px solid #ccc; + border-top:1px solid rgba(0,0,0,.1); + + @media screen and (max-width: 992px) { + width: 100%; + position:static; + max-height:33%; + } + &.success-panel { + bottom: 0; + width:100%; + } + + .selected-plugin { + display: inline-block; + vertical-align: top; + padding: 5px 5px 5px 30px; + position: relative; + width: 25%; + overflow:hidden; + font-size: 13px; + height: 45px; + border-right: 1px solid rgba(0,0,0,.1); + border-bottom: 1px solid rgba(0,0,0,.1); + box-shadow:0 1px 3px rgba(0,0,0,.2); + background:rgba(255,255,255,.75); + + &:nth-child(odd){ + box-shadow:0 1px 3px rgba(0,0,0,.2),inset 0 0 0 999px rgba(0,0,0,.05); + } + &:before { // pending + font-family: FontAwesome; + font-size: 17px; + content: '\f10c'; + color: rgba(0,0,0,.1); + position: absolute; + left:5px; + } + + &.installing { + &:before { + content: '\f021'; + color: rgba(0,60,100,.33); + .spin(); + } + } + &.success { + background:#e5f9e5; + &:before { + content: '\f00c'; + color: #3c763d; + + .no-spin(); + } + } + &.fail { + background:#fdd; + &:before { + content: '\f00d'; + color: #a94442; + .no-spin(); + } + } + } + } + + .welcome-panel { + position: relative; + background: transparent; + padding: 0 10%; + + h1{font-size:48px; margin-top:5px;} + + @media screen and (min-width: 768px) { + padding-top: 5%; + } + + .button-set { + margin-top: 40px; + + .btn { + text-align: left; + } + } + + .btn-huge { + font-weight:200; + display: inline-block; + text-align: initial; + width: 22em; + white-space: normal; + vertical-align: top; + min-height: 12.5em; + margin-right: 1.3%; + margin-top: 1em; + padding:20px 25px; + box-shadow:inset 0 -200px 200px -100px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.2); + border:1px solid rgba(0,60,100,.5); + text-shadow:#fff 0 1px 3px; + position:relative; + background:rgba(255,255,255,.75); + + @media screen and (min-width: 768px) { + max-width: 41%; + min-width: 14em; + margin-top: 0; + } + + &:hover{ + box-shadow:inset 0 1px 3px -1px rgba(0,0,0,.2); + background:rgba(100,200,255,.1); + } + &:focus{ + background:#fff; + box-shadow:inset 0 -200px 200px -100px rgba(0,0,0,.1),0 0 5px 3px rgba(00,180,250,.2); + } + &:hover:focus{ + background:rgba(100,200,255,.1); + } + &.btn-primary{ + background:rgba(200,240,255,.2); + + box-shadow:inset 0 -200px 200px -100px rgba(0,120,160,.2), 0 1px 3px rgba(0,0,0,.2); + color:rgba(0,80,120,1); + + &:hover{ + box-shadow:inset 0 1px 3px -1px rgba(0,0,0,.2); + background:rgba(70,200,255,.2); + } + &:focus{ + box-shadow:inset 0 -200px 200px -100px rgba(0,120,160,.2),0 0 5px 3px rgba(00,180,250,.2); } + } + .icon { + position: absolute; + bottom: 20px; + right: 10px; + font-size: 144px; + opacity: .0; + } + b { + display: block; + font-size: 24px; + line-height:100%; + margin-bottom:5px; + min-height:81px; + font-weight:600; + width:80%; + } + sub{ + font-size:21px; + } + span { + display: block; + } + } + } +} + +@-ms-keyframes spin { + from { -ms-transform: rotate(0deg); } + to { -ms-transform: rotate(360deg); } +} +@-moz-keyframes spin { + from { -moz-transform: rotate(0deg); } + to { -moz-transform: rotate(360deg); } +} +@-webkit-keyframes spin { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} +@keyframes spin { + from { transform:rotate(0deg); } + to { transform:rotate(360deg); } +} + +body .plugin-setup-wizard { // need specificity, revisit the CSS inclusion order + .plugin-setup-wizard(); +} + +@fb-loader-color: #E6E6E6; + +.loader, +.loader:before, +.loader:after { + background: @fb-loader-color; + -webkit-animation: fb-loader 1s infinite ease-in-out; + animation: fb-loader 1s infinite ease-in-out; + width: 7px; + height: 8px; +} +.loader:before, +.loader:after { + position: absolute; + top: 0; + content: ''; +} +.loader:before { + left: -10px; + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; +} +.loader { + text-indent: -9999em; + top: 50%; + margin: -4px auto 0 auto; + position: relative; + font-size: 6px; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; +} +.loader:after { + left: 10px; +} +@-webkit-keyframes fb-loader { + 0%, + 80%, + 100% { + box-shadow: 0 0 @fb-loader-color; + height: 4em; + } + 40% { + box-shadow: 0 -2em @fb-loader-color; + height: 5em; + } +} +@keyframes fb-loader { + 0%, + 80%, + 100% { + box-shadow: 0 0 @fb-loader-color; + height: 4em; + } + 40% { + box-shadow: 0 -2em @fb-loader-color; + height: 5em; + } +} diff --git a/war/src/main/webapp/WEB-INF/web.xml b/war/src/main/webapp/WEB-INF/web.xml index bb63b161a9..b34fc209b0 100644 --- a/war/src/main/webapp/WEB-INF/web.xml +++ b/war/src/main/webapp/WEB-INF/web.xml @@ -43,6 +43,11 @@ THE SOFTWARE. + + install-wizard-path + jsbundles/pluginSetupWizard + + Stapler /* diff --git a/war/src/main/webapp/css/icons/icomoon.css b/war/src/main/webapp/css/icons/icomoon.css new file mode 100644 index 0000000000..95e30c4955 --- /dev/null +++ b/war/src/main/webapp/css/icons/icomoon.css @@ -0,0 +1,418 @@ +@font-face { + font-family: 'icomoon'; + src:url('icomoon/icomoon.eot?-itxuas'); + src:url('icomoon/icomoon.eot?#iefix-itxuas') format('embedded-opentype'), + url('icomoon/icomoon.ttf?-itxuas') format('truetype'), + url('icomoon/icomoon.woff?-itxuas') format('woff'), + url('icomoon/icomoon.svg?-itxuas#icomoon') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="icon-"], [class*=" icon-"] { + font-family: 'icomoon'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-jira:before { + content: "\e677"; +} +.icon-hipchat:before { + content: "\e67e"; +} +.icon-AWS_Elastic_BeanStalk:before { + content: "\e67b"; +} +.icon-mercurial:before { + content: "\e673"; +} +.icon-bitbucket:before { + content: "\e67f"; +} +.icon-git-logo:before { + content: "\e674"; +} +.icon-github:before { + content: "\e675"; +} +.icon-subversion:before { + content: "\e676"; +} +.icon-pivotal:before { + content: "\e660"; +} +.icon-vmware:before { + content: "\e65e"; +} +.icon-ec2:before { + content: "\e65f"; +} +.icon-siren:before { + content: "\e656"; +} +.icon-cloudbees-logo:before { + content: "\e64a"; +} +.icon-code:before { + content: "\e647"; +} +.icon-docker:before { + content: "\e640"; +} +.icon-fingerprint:before { + content: "\e63a"; +} +.icon-groovy:before { + content: "\e638"; +} +.icon-grails:before { + content: "\e637"; +} +.icon-git:before { + content: "\e635"; +} +.icon-sbt:before { + content: "\e636"; +} +.icon-gradle:before { + content: "\e634"; +} +.icon-ant:before { + content: "\e632"; +} +.icon-service:before { + content: "\e62a"; +} +.icon-service-o:before { + content: "\e62b"; +} +.icon-menu:before { + content: "\e620"; +} +.icon-build-hand:before { + content: "\e61b"; +} +.icon-menu2:before { + content: "\e66b"; +} +.icon-content-paste:before { + content: "\e666"; +} +.icon-format-color-fill:before { + content: "\e658"; +} +.icon-puzzle:before { + content: "\e64f"; +} +.icon-flow-merge:before { + content: "\e671"; +} +.icon-flow-switch:before { + content: "\e672"; +} +.icon-thermometer:before { + content: "\e659"; +} +.icon-arrow-shuffle:before { + content: "\e630"; +} +.icon-arrow-right:before { + content: "\e631"; +} +.icon-chat-3:before { + content: "\e680"; +} +.icon-health:before { + content: "\e657"; +} +.icon-screwdriver:before { + content: "\e668"; +} +.icon-clipboard:before { + content: "\e667"; +} +.icon-calendar:before { + content: "\e663"; +} +.icon-maven:before { + content: "\e633"; +} +.icon-brick-menu-o:before { + content: "\e62c"; +} +.icon-brick-o:before { + content: "\e62d"; +} +.icon-gear-menu-o:before { + content: "\e627"; +} +.icon-gear-menu:before { + content: "\e626"; +} +.icon-brick-menu:before { + content: "\e621"; +} +.icon-folder-menu:before { + content: "\e622"; +} +.icon-jenkins-head:before { + content: "\e617"; +} +.icon-jenkins:before { + content: "\e618"; +} +.icon-jenkins-o:before { + content: "\e619"; +} +.icon-brick:before { + content: "\e61a"; +} +.icon-checkbox-checked:before { + content: "\e623"; +} +.icon-phone:before { + content: "\e642"; +} +.icon-checkmark:before { + content: "\e681"; +} +.icon-map-pin-fill:before { + content: "\e678"; +} +.icon-box-add:before { + content: "\e67d"; +} +.icon-box-remove:before { + content: "\e67c"; +} +.icon-clock:before { + content: "\e661"; +} +.icon-plug:before { + content: "\e64e"; +} +.icon-refresh:before { + content: "\e679"; +} +.icon-search:before { + content: "\e63f"; +} +.icon-move:before { + content: "\e615"; +} +.icon-bug:before { + content: "\e611"; +} +.icon-tools:before { + content: "\e669"; +} +.icon-pulse:before { + content: "\e655"; +} +.icon-key:before { + content: "\e653"; +} +.icon-lock:before { + content: "\e64b"; +} +.icon-gear:before { + content: "\e624"; +} +.icon-repo-forked:before { + content: "\e610"; +} +.icon-issue-closed:before { + content: "\e608"; +} +.icon-lightning:before { + content: "\e603"; +} +.icon-rainy:before { + content: "\e604"; +} +.icon-cloud:before { + content: "\e605"; +} +.icon-cloudy:before { + content: "\e606"; +} +.icon-sun:before { + content: "\e607"; +} +.icon-office:before { + content: "\e600"; +} +.icon-quill:before { + content: "\e62e"; +} +.icon-book:before { + content: "\e63c"; +} +.icon-copy:before { + content: "\e66e"; +} +.icon-copy2:before { + content: "\e66f"; +} +.icon-tag:before { + content: "\e61c"; +} +.icon-support:before { + content: "\e66c"; +} +.icon-phone2:before { + content: "\e66d"; +} +.icon-map:before { + content: "\e651"; +} +.icon-stopwatch:before { + content: "\e648"; +} +.icon-mobile:before { + content: "\e643"; +} +.icon-users:before { + content: "\e641"; +} +.icon-wrench:before { + content: "\e66a"; +} +.icon-settings:before { + content: "\e614"; +} +.icon-hammer:before { + content: "\e601"; +} +.icon-aid:before { + content: "\e654"; +} +.icon-pie:before { + content: "\e65b"; +} +.icon-stats:before { + content: "\e639"; +} +.icon-bars:before { + content: "\e65c"; +} +.icon-rocket:before { + content: "\e649"; +} +.icon-remove:before { + content: "\e664"; +} +.icon-signup:before { + content: "\e682"; +} +.icon-download:before { + content: "\e670"; +} +.icon-globe:before { + content: "\e652"; +} +.icon-flag:before { + content: "\e644"; +} +.icon-cancel-circle:before { + content: "\e665"; +} +.icon-loop:before { + content: "\e65d"; +} +.icon-insert-template:before { + content: "\e64d"; +} +.icon-console:before { + content: "\e628"; +} +.icon-file-css:before { + content: "\e629"; +} +.icon-screen:before { + content: "\e60b"; +} +.icon-help:before { + content: "\e67a"; +} +.icon-feather-o:before { + content: "\e62f"; +} +.icon-compass:before { + content: "\e61d"; +} +.icon-graduation:before { + content: "\e65a"; +} +.icon-box:before { + content: "\e63b"; +} +.icon-flow-cascade:before { + content: "\e61e"; +} +.icon-flow-branch:before { + content: "\e60c"; +} +.icon-flow-tree:before { + content: "\e60d"; +} +.icon-calendar-o:before { + content: "\e662"; +} +.icon-ban:before { + content: "\e64c"; +} +.icon-flag-checkered:before { + content: "\e645"; +} +.icon-envelope-o:before { + content: "\e63d"; +} +.icon-envelope:before { + content: "\e63e"; +} +.icon-cog:before { + content: "\e625"; +} +.icon-puzzle-piece:before { + content: "\e61f"; +} +.icon-folder-open-o:before { + content: "\e60e"; +} +.icon-folder-o:before { + content: "\e60f"; +} +.icon-thumbs-o-down:before { + content: "\e609"; +} +.icon-thumbs-o-up:before { + content: "\e60a"; +} +.icon-enlarge:before { + content: "\e650"; +} +.icon-shield:before { + content: "\e646"; +} +.icon-factory:before { + content: "\e616"; +} +.icon-unlocked:before { + content: "\e612"; +} +.icon-locked:before { + content: "\e613"; +} +.icon-uniE602:before { + content: "\e602"; +} diff --git a/war/src/main/webapp/css/icons/icomoon/icomoon.eot b/war/src/main/webapp/css/icons/icomoon/icomoon.eot new file mode 100644 index 0000000000000000000000000000000000000000..06ac51bcaff074a7ae3d6e1bba765fe6f6abc933 GIT binary patch literal 51280 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 U3d;-ZWRtYR)VEjH`aACaA78Jhz5oCK literal 0 HcmV?d00001 diff --git a/war/src/main/webapp/css/icons/icomoon/icomoon.svg b/war/src/main/webapp/css/icons/icomoon/icomoon.svg new file mode 100644 index 0000000000..896baa7f44 --- /dev/null +++ b/war/src/main/webapp/css/icons/icomoon/icomoon.svg @@ -0,0 +1,141 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/war/src/main/webapp/css/icons/icomoon/icomoon.ttf b/war/src/main/webapp/css/icons/icomoon/icomoon.ttf new file mode 100644 index 0000000000000000000000000000000000000000..80185a1e56d03a1f5cdacc00a3811185dd181703 GIT binary patch literal 51116 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�a?=E5WJlkrHf9M+eq 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 0) { + return responses[counter-1]; + } + return { status: 'fail' }; + }; +}; + +// common mocks for jQuery $.ajax +var ajaxMocks = function(responseMappings) { + var defaultMappings = { + '/jenkins/i18n/resourceBundle?baseName=jenkins.install.pluginSetupWizard': { + status: 'ok', + data: { + 'installWizard_offline_title': 'Offline', + 'installWizard_error_header': 'Error', + 'installWizard_installIncomplete_title': 'Resume Installation' + } + }, + '/jenkins/updateCenter/incompleteInstallStatus': { + status: 'ok', + data: [] + }, + '/jenkins/updateCenter/installStatus': new LastResponse([{ + status: 'ok', + data: [] // first, return nothing by default, no ongoing install + }, + { + status: 'ok', + data: [ + { + name: 'subversion', + type: 'InstallJob', + installStatus: 'Success' + } + ] + }]), + '/jenkins/pluginManager/plugins': { + status: 'ok', + data: [ + { + name: 'junit', + title: 'JUnit plugin', + dependencies: {} + }, + { + name: 'subversion', + title: 'Subversion plugin' + }, + { + name: 'other', + title: 'Other thing', + dependencies: { 'subversion': '1' } + }, + { + name: 'mailer', + title: 'Mailer plugin', + dependencies: { 'other': '1' } + } + ] + }, + '/jenkins/updateCenter/connectionStatus?siteId=default': { + status: 'ok', + data: { + updatesite: 'OK', + internet: 'OK' + } + }, + '/jenkins/pluginManager/installPlugins': { + status: 'ok', + data: 'RANDOM_UUID_1234' + } + }; + + if (!responseMappings) { + responseMappings = {}; + } + + return function(call) { + if(debug) { + console.log('AJAX call: ' + call.url); + } + + var response = responseMappings[call.url]; + if (!response) { + response = defaultMappings[call.url]; + } + if (!response) { + throw 'No data mapping provided for AJAX call: ' + call.url; + } + if(response instanceof LastResponse) { + response = response.next(); + } + call.success(response); + }; +}; + +// call this for each test, it will provide a new wizard, jquery to the caller +var test = function(test, ajaxMappings) { + jsTest.onPage(function() { + // deps + var $ = getJQuery(); + + // Respond to status request + $.ajax = ajaxMocks(ajaxMappings); + + // load the module + var pluginSetupWizard = jsTest.requireSrcModule('pluginSetupWizardGui'); + + // exported init + pluginSetupWizard.init(); + + test($, pluginSetupWizard); + }); +}; + +// helper to validate the appropriate plugins were installed +var validatePlugins = function(plugins, complete) { + var jenkins = jsTest.requireSrcModule('util/jenkins'); + if(!jenkins.originalPost) { + jenkins.originalPost = jenkins.post; + } + jenkins.post = function(url, data, cb) { + expect(url).toBe('/pluginManager/installPlugins'); + var allMatch = true; + for(var i = 0; i < plugins.length; i++) { + if(data.plugins.indexOf(plugins[i]) < 0) { + allMatch = false; + break; + } + } + if(!allMatch) { + expect(JSON.stringify(plugins)).toBe('In: ' + JSON.stringify(data.plugins)); + } + // return status + cb({status:'ok',data:{correlationId:1}}); + if(complete) { + complete(); + } + }; +}; + +describe("pluginSetupWizard.js", function () { + it("wizard shows", function (done) { + test(function($) { + // Make sure the dialog was shown + var $wizard = $('.plugin-setup-wizard'); + expect($wizard.size()).toBe(1); + + done(); + }); + }); + + it("offline shows", function (done) { + jsTest.onPage(function() { + // deps + var jenkins = jsTest.requireSrcModule('./util/jenkins'); + + var $ = getJQuery(); + $.ajax = ajaxMocks(); + + var get = jenkins.get; + try { + // Respond with failure + jenkins.get = function(url, cb) { + if (debug) { + console.log('Jenkins.GET: ' + url); + } + + if(url === '/updateCenter/connectionStatus?siteId=default') { + cb({ + status: 'ok', + data: { + updatesite: 'ERROR', + internet: 'ERROR' + } + }); + } + else { + get(url, cb); + } + }; + + // load the module + var pluginSetupWizard = jsTest.requireSrcModule('pluginSetupWizardGui'); + + // exported init + pluginSetupWizard.init(); + + expect($('.welcome-panel h1').text()).toBe('Offline'); + + done(); + } finally { + jenkins.get = get; + } + }); + }); + + it("install defaults", function (done) { + test(function($) { + // Make sure the dialog was shown + var wizard = $('.plugin-setup-wizard'); + expect(wizard.size()).toBe(1); + + var goButton = $('.install-recommended'); + expect(goButton.size()).toBe(1); + + // validate a call to installPlugins with our defaults + validatePlugins(['subversion'], function() { + done(); + }); + + goButton.click(); + }); + }); + + var doit = function($, sel, trigger) { + var $el = $(sel); + if($el.length !== 1) { + console.log('Not found! ' + sel); + console.log(new Error().stack); + } + if(trigger === 'check') { + $el.prop('checked', true); + trigger = 'change'; + } + $el.trigger(trigger); + }; + + it("install custom", function (done) { + test(function($) { + $('.install-custom').click(); + + // validate a call to installPlugins with our defaults + validatePlugins(['junit','mailer'], function() { + // install a specific, other 'set' of plugins + $('input[name=searchbox]').val('junit'); + + done(); + }); + + doit($, 'input[name=searchbox]', 'blur'); + + doit($, '.plugin-select-none', 'click'); + + doit($, 'input[name="junit"]', 'check'); + doit($, 'input[name="mailer"]', 'check'); + + doit($, '.install-selected', 'click'); + }); + }); + + it("resume install", function (done) { + var ajaxMappings = { + '/jenkins/updateCenter/incompleteInstallStatus': { + status: 'ok', + data: { + 'junit': 'Success', + 'subversion': 'Pending', + 'mailer': 'Success', + 'other': 'Failed' + } + } + }; + test(function($) { + expect($('.modal-title').text()).toBe('Resume Installation'); + expect($('*[data-name="junit"]').is('.success')).toBe(true); + done(); + }, ajaxMappings); + }); + + it("error conditions", function (done) { + var ajaxMappings = { + '/jenkins/updateCenter/incompleteInstallStatus': { + status: 'error', + data: { + 'junit': 'Success', + 'subversion': 'Pending', + 'other': 'Failed', + 'mailer': 'Success' + } + } + }; + test(function($) { + expect($('.error-container h1').html()).toBe('Error'); + done(); + }, ajaxMappings); + }); + + it("restart required", function (done) { + var jenkins = jsTest.requireSrcModule('util/jenkins'); + if(jenkins.originalPost) { + jenkins.post = jenkins.originalPost; + } + + var ajaxMappings = { + '/jenkins/updateCenter/installStatus': new LastResponse([{ + status: 'ok', + data: [] // first, return nothing by default, no ongoing install + }, + { + status: 'ok', + data: [ + { + name: 'subversion', + type: 'InstallJob', + installStatus: 'Success', + requiresRestart: 'true' // a string... + } + ] + }]) + }; + test(function($) { + var goButton = $('.install-recommended'); + expect(goButton.size()).toBe(1); + + // validate a call to installPlugins with our defaults + setTimeout(function() { + expect($('.install-done').is(':visible')).toBe(false); + expect($('.install-done-restart').is(':visible')).toBe(true); + + done(); + }, 500); + + goButton.click(); + }, ajaxMappings); + }); + +}); \ No newline at end of file -- GitLab From 60b9e7e30bc1c354359884429a8a8a9b80dd838c Mon Sep 17 00:00:00 2001 From: tfennelly Date: Wed, 20 Jan 2016 18:05:57 +0000 Subject: [PATCH 0023/2380] Setting XMLUtil DocumentBuilderFactory features to try close XXE vulnerabilities --- .../main/java/jenkins/util/xml/XMLUtils.java | 46 +++++++++++++++++-- .../test/java/jenkins/xml/XMLUtilsTest.java | 15 ++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/jenkins/util/xml/XMLUtils.java b/core/src/main/java/jenkins/util/xml/XMLUtils.java index 8427ea7a62..99cced1aab 100644 --- a/core/src/main/java/jenkins/util/xml/XMLUtils.java +++ b/core/src/main/java/jenkins/util/xml/XMLUtils.java @@ -11,7 +11,6 @@ import org.xml.sax.helpers.XMLReaderFactory; import java.io.File; import java.io.FileInputStream; -import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; @@ -44,6 +43,31 @@ public final class XMLUtils { private final static Logger LOGGER = LogManager.getLogManager().getLogger(XMLUtils.class.getName()); private final static String DISABLED_PROPERTY_NAME = XMLUtils.class.getName() + ".disableXXEPrevention"; + public static final String FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; + public static final String FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; + + private static final DocumentBuilderFactory documentBuilderFactory; + + static { + documentBuilderFactory = DocumentBuilderFactory.newInstance(); + // Set parser features to prevent against XXE etc. + // Note: setting only the external entity features on DocumentBuilderFactory instance + // (ala how safeTransform does it for SAXTransformerFactory) does seem to work (was still + // processing the entities - tried Oracle JDK 7 and 8 on OSX). Setting seems a bit extreme, + // but looks like there's no other choice. + documentBuilderFactory.setXIncludeAware(false); + documentBuilderFactory.setExpandEntityReferences(false); + setDocumentBuilderFactoryFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + setDocumentBuilderFactoryFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES, false); + setDocumentBuilderFactoryFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES, true); + setDocumentBuilderFactoryFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + } + private static void setDocumentBuilderFactoryFeature(String feature, boolean state) { + try { + documentBuilderFactory.setFeature(feature, state); + } catch (Exception e) {} + } + /** * Transform the source to the output in a manner that is protected against XXE attacks. * If the transform can not be completed safely then an IOException is thrown. @@ -62,11 +86,11 @@ public final class XMLUtils { XMLReader xmlReader = XMLReaderFactory.createXMLReader(); try { - xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false); + xmlReader.setFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES, false); } catch (SAXException ignored) { /* ignored */ } try { - xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + xmlReader.setFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES, false); } catch (SAXException ignored) { /* ignored */ } // defend against XXE @@ -111,7 +135,8 @@ public final class XMLUtils { DocumentBuilder docBuilder; try { - docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + docBuilder = documentBuilderFactory.newDocumentBuilder(); + docBuilder.setEntityResolver(RestrictiveEntityResolver.INSTANCE); } catch (ParserConfigurationException e) { throw new IllegalStateException("Unexpected error creating DocumentBuilder.", e); } @@ -178,6 +203,19 @@ public final class XMLUtils { */ public static @Nonnull String getValue(@Nonnull String xpath, @Nonnull File file, @Nonnull String fileDataEncoding) throws IOException, SAXException, XPathExpressionException { Document document = parse(file, fileDataEncoding); + return getValue(xpath, document); + } + + /** + * The a "value" from an XML file using XPath. + * @param xpath The XPath expression to select the value. + * @param document The document from which the value is to be extracted. + * @return The data value. An empty {@link String} is returned when the expression does not evaluate + * to anything in the document. + * @throws XPathExpressionException Invalid XPath expression. + * @since FIXME + */ + public static String getValue(String xpath, Document document) throws XPathExpressionException { XPath xPathProcessor = XPathFactory.newInstance().newXPath(); return xPathProcessor.compile(xpath).evaluate(document); } diff --git a/core/src/test/java/jenkins/xml/XMLUtilsTest.java b/core/src/test/java/jenkins/xml/XMLUtilsTest.java index 365aa8fb12..0211653cf3 100644 --- a/core/src/test/java/jenkins/xml/XMLUtilsTest.java +++ b/core/src/test/java/jenkins/xml/XMLUtilsTest.java @@ -42,6 +42,7 @@ import javax.xml.xpath.XPathExpressionException; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.assertThat; import org.jvnet.hudson.test.Issue; +import org.w3c.dom.Document; import org.xml.sax.SAXException; public class XMLUtilsTest { @@ -115,4 +116,18 @@ public class XMLUtilsTest { Assert.assertEquals("1.480.1", XMLUtils.getValue("/hudson/version", configFile)); Assert.assertEquals("", XMLUtils.getValue("/hudson/unknown-element", configFile)); } + + @Test + public void testParse_with_XXE() throws IOException, XPathExpressionException { + try { + Document doc = XMLUtils.parse(new StringReader("\n" + + "\n" + + " ]> " + + "&xxe;")); + Assert.fail("Expecting SAXException for XXE."); + } catch (SAXException e) { + assertThat(e.getMessage(), containsString("DOCTYPE is disallowed")); + } + } } -- GitLab From 66a560d71743a41667d09d8632973db4ca2c0541 Mon Sep 17 00:00:00 2001 From: tfennelly Date: Wed, 20 Jan 2016 18:15:59 +0000 Subject: [PATCH 0024/2380] Setting XMLUtil DocumentBuilderFactory features to try close XXE vulnerabilities - fixed state for one of the features Still didn't work without rejecting doctype completely --- core/src/main/java/jenkins/util/xml/XMLUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/jenkins/util/xml/XMLUtils.java b/core/src/main/java/jenkins/util/xml/XMLUtils.java index 99cced1aab..eb744dc777 100644 --- a/core/src/main/java/jenkins/util/xml/XMLUtils.java +++ b/core/src/main/java/jenkins/util/xml/XMLUtils.java @@ -59,7 +59,7 @@ public final class XMLUtils { documentBuilderFactory.setExpandEntityReferences(false); setDocumentBuilderFactoryFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); setDocumentBuilderFactoryFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES, false); - setDocumentBuilderFactoryFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES, true); + setDocumentBuilderFactoryFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES, false); setDocumentBuilderFactoryFeature("http://apache.org/xml/features/disallow-doctype-decl", true); } private static void setDocumentBuilderFactoryFeature(String feature, boolean state) { -- GitLab From 84a2117772f2b639fe82eff652c349ff8eae999f Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Wed, 20 Jan 2016 22:24:17 -0800 Subject: [PATCH 0025/2380] [maven-release-plugin] prepare for next development iteration --- cli/pom.xml | 2 +- core/pom.xml | 2 +- plugins/pom.xml | 10 +++++----- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 51fcde1aef..e1779b0f88 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 1.642.1 + 1.642.2-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index d07b8fde49..5913b1d331 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 1.642.1 + 1.642.2-SNAPSHOT jenkins-core diff --git a/plugins/pom.xml b/plugins/pom.xml index ae70e60b03..825f83a485 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -12,7 +12,7 @@ org.jenkins-ci.plugins plugin Jenkins plugin POM - 1.642.1 + 1.642.2-SNAPSHOT pom - + diff --git a/core/src/main/resources/hudson/model/Job/configure.properties b/core/src/main/resources/hudson/model/Job/configure.properties index f6f01a21a9..1eef961fb3 100644 --- a/core/src/main/resources/hudson/model/Job/configure.properties +++ b/core/src/main/resources/hudson/model/Job/configure.properties @@ -20,4 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -name={0} name \ No newline at end of file +name={0} name +Save=Save All +Apply=Apply All \ No newline at end of file diff --git a/core/src/main/resources/lib/form/apply.jelly b/core/src/main/resources/lib/form/apply.jelly index 0551d1d115..86455202ec 100644 --- a/core/src/main/resources/lib/form/apply.jelly +++ b/core/src/main/resources/lib/form/apply.jelly @@ -32,8 +32,11 @@ THE SOFTWARE. When this button is pressed, the FORM element fires the "jenkins:apply" event that allows interested parties to write back whatever states back into the INPUT elements. + + The text of the apply button. + - + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/checkbox.jelly b/core/src/main/resources/lib/form/checkbox.jelly index eda309a14e..4c626aed64 100644 --- a/core/src/main/resources/lib/form/checkbox.jelly +++ b/core/src/main/resources/lib/form/checkbox.jelly @@ -63,7 +63,7 @@ THE SOFTWARE. name="${name}" value="${attrs.value}" title="${attrs.tooltip}" - onclick="${attrs.onclick}" id="${attrs.id}" class="${attrs.negative!=null ? 'negative' : null} ${attrs.checkUrl!=null?'validated':''}" + onclick="${attrs.onclick}" id="${attrs.id}" class="${attrs.class} ${attrs.negative!=null ? 'negative' : null} ${attrs.checkUrl!=null?'validated':''}" checkUrl="${attrs.checkUrl}" checkDependsOn="${attrs.checkDependsOn}" json="${attrs.json}" checked="${value ? 'true' : null}"/> diff --git a/core/src/main/resources/lib/form/optionalBlock.jelly b/core/src/main/resources/lib/form/optionalBlock.jelly index 08408527d7..13425038e3 100644 --- a/core/src/main/resources/lib/form/optionalBlock.jelly +++ b/core/src/main/resources/lib/form/optionalBlock.jelly @@ -68,7 +68,7 @@ THE SOFTWARE. - diff --git a/core/src/main/resources/lib/form/radioBlock.jelly b/core/src/main/resources/lib/form/radioBlock.jelly index 2a6135a917..197036a2cf 100644 --- a/core/src/main/resources/lib/form/radioBlock.jelly +++ b/core/src/main/resources/lib/form/radioBlock.jelly @@ -58,7 +58,7 @@ THE SOFTWARE. diff --git a/core/src/main/resources/lib/layout/css.jelly b/core/src/main/resources/lib/layout/css.jelly new file mode 100644 index 0000000000..a15d383023 --- /dev/null +++ b/core/src/main/resources/lib/layout/css.jelly @@ -0,0 +1,37 @@ + + + + + + Client-side CSS loading tag. Similar to adjunct, but driven from the client. See page-init.js. + + @since 2.0 + + CSS source path (relative to Jenkins). + + + +
+ \ No newline at end of file diff --git a/core/src/main/resources/lib/layout/js.jelly b/core/src/main/resources/lib/layout/js.jelly new file mode 100644 index 0000000000..88edf8dafd --- /dev/null +++ b/core/src/main/resources/lib/layout/js.jelly @@ -0,0 +1,37 @@ + + + + + + Client-side JavaScript loading tag. Similar to adjunct, but driven from the client. See page-init.js. + + @since 2.0 + + JavaScript source path (relative to Jenkins). + + + +
+ \ No newline at end of file diff --git a/core/src/main/resources/lib/layout/layout.jelly b/core/src/main/resources/lib/layout/layout.jelly index 2caba28563..632f576388 100644 --- a/core/src/main/resources/lib/layout/layout.jelly +++ b/core/src/main/resources/lib/layout/layout.jelly @@ -166,6 +166,8 @@ ${h.initPageVariables(context)} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
Quiet period
Number of seconds
SCM checkout retry count
+ + +
+ +
+ + +
Directory
Display Name
+ +
+ + + + + + +
#Build Triggers
+ + + + + + + + + + +
#Build
+ + + + +
+
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/war/src/test/js/widgets/config/tabbar-spec.js b/war/src/test/js/widgets/config/tabbar-spec.js index e3c64eac5c..73edbe4d1b 100644 --- a/war/src/test/js/widgets/config/tabbar-spec.js +++ b/war/src/test/js/widgets/config/tabbar-spec.js @@ -10,15 +10,15 @@ describe("tabbar-spec tests", function () { var jQD = require('jquery-detached'); var $ = jQD.getJQuery(); - expect($('.section-header-row', firstTableMetadata.configTable).size()).toBe(6); + expect($('.section-header-row', firstTableMetadata.configTable).size()).toBe(4); expect(firstTableMetadata.sectionCount()).toBe(4); - expect($('.tabBar .tab').size()).toBe(8); + expect($('.tabBar .tab').size()).toBe(4); expect(firstTableMetadata.sectionIds().toString()) - .toBe('config_general,config__build_triggers,config__advanced_project_options,config__workflow'); + .toBe('config_general,config__advanced_project_options,config__build_triggers,config__build'); done(); - }, 'widgets/config/workflow-config.html'); + }, 'widgets/config/freestyle-config.html'); }); it("- test section activation", function (done) { @@ -31,39 +31,45 @@ describe("tabbar-spec tests", function () { expect(firstTableMetadata.activeSectionCount()).toBe(1); firstTableMetadata.onShowSection(function() { - expect(this.id).toBe('config__workflow'); + expect(this.id).toBe('config__build'); expect(firstTableMetadata.activeSectionCount()).toBe(1); var activeSection = firstTableMetadata.activeSection(); - expect(activeSection.id).toBe('config__workflow'); - expect(activeSection.activeRowCount()).toBe(4); - expect(firstTableMetadata.getTopRows().filter('.active').size()).toBe(3); // should be the same as activeSection.activeRowCount() + expect(activeSection.id).toBe('config__build'); + expect(activeSection.activeRowCount()).toBe(2); + expect(firstTableMetadata.getTopRows().filter('.active').size()).toBe(1); // should be activeSection.activeRowCount() - 1 done(); }); // Mimic the user clicking on one of the tabs. Should make that section active, // with all of the rows in that section having an "active" class. - firstTableMetadata.activateSection('config__workflow'); + firstTableMetadata.activateSection('config__build'); // above 'firstTableMetadata.onShowSection' handler should get called now - }, 'widgets/config/workflow-config.html'); + }, 'widgets/config/freestyle-config.html'); }); - it("- test row-set activation", function (done) { + it("- test row-group modeling", function (done) { jsTest.onPage(function() { var configTabBar = jsTest.requireSrcModule('widgets/config/tabbar'); var firstTableMetadata = configTabBar.addTabsOnFirst(); var generalSection = firstTableMetadata.activeSection(); expect(generalSection.id).toBe('config_general'); - expect(generalSection.rowGroups.length).toBe(3); - expect(generalSection.getRowGroupLabels().toString()).toBe('Discard Old Builds,This build is parameterized,Execute concurrent builds if necessary,Quiet period'); - expect(generalSection.rowGroups[0].getRowCount()).toBe(6); - expect(generalSection.rowGroups[1].getRowCount()).toBe(1); + + var sectionRowGroups = generalSection.rowGroups; + + expect(sectionRowGroups.length).toBe(1); + expect(sectionRowGroups[0].getRowCount(false)).toBe(0); // zero because it does not have any non row-group rows nested immediately inside i.e. does not have any "normal" rows + expect(sectionRowGroups[0].getRowCount(true)).toBe(4); // there are some nested down in the children. see below + expect(sectionRowGroups[0].rowGroups.length).toBe(1); + expect(sectionRowGroups[0].rowGroups[0].getRowCount(false)).toBe(4); // The inner grouping has rows + expect(sectionRowGroups[0].rowGroups[0].getRowCount()).toBe(4); // Same as above ... just making sure they're direct child rows and not nested below + expect(generalSection.getRowGroupLabels().toString()).toBe('Discard Old Builds'); done(); - }, 'widgets/config/workflow-config.html'); + }, 'widgets/config/freestyle-config.html'); }); }); diff --git a/war/src/test/js/widgets/config/workflow-config.html b/war/src/test/js/widgets/config/workflow-config.html deleted file mode 100644 index ee76983409..0000000000 --- a/war/src/test/js/widgets/config/workflow-config.html +++ /dev/null @@ -1,1436 +0,0 @@ -
-
- -
-
-
General
-
Build Triggers
-
Advanced Project Options
-
Workflow
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Workflow name - - -
 Description - - -
-
- [Plain text] Preview  - - -
-
 Strategy
Help for feature: This build is parameterized
Help for feature: Quiet period
-
Loading...
-
 Quiet period
Number of seconds
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file -- GitLab From cdbbc8ccc578d02af8b30c674ce8683f45ca033f Mon Sep 17 00:00:00 2001 From: tfennelly Date: Wed, 17 Feb 2016 19:01:59 +0000 Subject: [PATCH 0236/2380] Added some finder tests --- .../js/widgets/config/model/ConfigSection.js | 36 ++++++- .../config/model/ConfigTableMetaData.js | 22 +---- war/src/main/js/widgets/jenkins-widgets.less | 7 +- .../js/widgets/config/freestyle-config.html | 3 +- war/src/test/js/widgets/config/tabbar-spec.js | 96 +++++++++++++++++++ 5 files changed, 140 insertions(+), 24 deletions(-) diff --git a/war/src/main/js/widgets/config/model/ConfigSection.js b/war/src/main/js/widgets/config/model/ConfigSection.js index 827c8678e8..295181af82 100644 --- a/war/src/main/js/widgets/config/model/ConfigSection.js +++ b/war/src/main/js/widgets/config/model/ConfigSection.js @@ -102,6 +102,29 @@ ConfigSection.prototype.markRowsAsActive = function() { this.updateRowGroupVisibility(); }; +ConfigSection.prototype.hasText = function(text) { + var $ = jQD.getJQuery(); + var selector = ":containsci('" + text + "')"; + var sectionRows = this.getRows(); + + for (var i1 = 0; i1 < sectionRows.length; i1++) { + var row = sectionRows[i1]; + var elementsWithText = $(selector, row); + + if (elementsWithText.size() > 0) { + return true; + } + } + + for (var i2 = 0; i2 < this.subSections.length; i2++) { + if (this.subSections[i2].hasText(text)) { + return true; + } + } + + return false; +}; + ConfigSection.prototype.activeRowCount = function() { var activeRowCount = 0; var rows = this.getRows(); @@ -190,8 +213,8 @@ ConfigSection.prototype.highlightText = function(text) { var selector = ":containsci('" + text + "')"; var rows = this.getRows(); - for (var i = 0; i < rows.length; i++) { - var row = rows[i]; + for (var i1 = 0; i1 < rows.length; i1++) { + var row = rows[i1]; /*jshint loopfunc: true */ $('span.highlight-split', row).each(function() { // jshint ignore:line @@ -209,11 +232,16 @@ ConfigSection.prototype.highlightText = function(text) { $this.contents().each(function () { // We specifically only mess with text nodes if (this.nodeType === 3) { - var highlightedMarkup = this.wholeText.replace(regex, '$1'); - $(this).replaceWith('' + highlightedMarkup + ''); + var $textNode = $(this); + var highlightedMarkup = $textNode.text().replace(regex, '$1'); + $textNode.replaceWith('' + highlightedMarkup + ''); } }); }); } } + + for (var i2 = 0; i2 < this.subSections.length; i2++) { + this.subSections[i2].highlightText(text); + } }; diff --git a/war/src/main/js/widgets/config/model/ConfigTableMetaData.js b/war/src/main/js/widgets/config/model/ConfigTableMetaData.js index a3940eb684..6ee7905a80 100644 --- a/war/src/main/js/widgets/config/model/ConfigTableMetaData.js +++ b/war/src/main/js/widgets/config/model/ConfigTableMetaData.js @@ -262,7 +262,7 @@ ConfigTableMetaData.prototype.showSections = function(withText) { if (withText === '') { if (this.hasSections()) { for (var i1 = 0; i1 < this.sections.length; i1++) { - this.sections[i1].activator.show(); + this.sections[i1].activator.removeClass('hidden'); } var activeSection = this.activeSection(); if (!activeSection) { @@ -273,30 +273,16 @@ ConfigTableMetaData.prototype.showSections = function(withText) { } } else { if (this.hasSections()) { - var $ = jQD.getJQuery(); - var selector = ":containsci('" + withText + "')"; var sectionsWithText = []; for (var i2 = 0; i2 < this.sections.length; i2++) { var section = this.sections[i2]; - var containsText = false; - var sectionRows = section.getRows(); - - for (var i3 = 0; i3 < sectionRows.length; i3++) { - var row = sectionRows[i3]; - var elementsWithText = $(selector, row); - - if (elementsWithText.size() > 0) { - containsText = true; - break; - } - } - if (containsText) { - section.activator.show(); + if (section.hasText(withText)) { + section.activator.removeClass('hidden'); sectionsWithText.push(section); } else { - section.activator.hide(); + section.activator.addClass('hidden'); } } diff --git a/war/src/main/js/widgets/jenkins-widgets.less b/war/src/main/js/widgets/jenkins-widgets.less index 49fc0a6bf2..c2a9e87f6b 100644 --- a/war/src/main/js/widgets/jenkins-widgets.less +++ b/war/src/main/js/widgets/jenkins-widgets.less @@ -10,4 +10,9 @@ /* * Widget styles */ -@import "config/tabbar"; \ No newline at end of file +@import "config/tabbar"; + + +.hidden { + display: none; +} \ No newline at end of file diff --git a/war/src/test/js/widgets/config/freestyle-config.html b/war/src/test/js/widgets/config/freestyle-config.html index c01cf97fd6..a236ee0f45 100644 --- a/war/src/test/js/widgets/config/freestyle-config.html +++ b/war/src/test/js/widgets/config/freestyle-config.html @@ -129,7 +129,8 @@ - + + diff --git a/war/src/test/js/widgets/config/tabbar-spec.js b/war/src/test/js/widgets/config/tabbar-spec.js index 73edbe4d1b..071dda22cc 100644 --- a/war/src/test/js/widgets/config/tabbar-spec.js +++ b/war/src/test/js/widgets/config/tabbar-spec.js @@ -71,6 +71,102 @@ describe("tabbar-spec tests", function () { done(); }, 'widgets/config/freestyle-config.html'); }); + + it("- test finder - via handler triggering", function (done) { + jsTest.onPage(function() { + var configTabBarWidget = jsTest.requireSrcModule('widgets/config/tabbar'); + var configTabBar = configTabBarWidget.addTabsOnFirst(); + var jQD = require('jquery-detached'); + + var $ = jQD.getJQuery(); + + var tabBar = $('.tabBar'); + + // All tabs should be visible... + expect($('.tab', tabBar).size()).toBe(4); + expect($('.tab.hidden', tabBar).size()).toBe(0); + + var finder = configTabBar.findInput; + expect(finder.size()).toBe(1); + + // Find sections that have the text "trigger" in them... + keydowns('trigger', finder); + + // Need to wait for the change to happen ... there's a 300ms delay. + // We could just call configTabBar.showSections(), but ... + setTimeout(function() { + expect($('.tab.hidden', tabBar).size()).toBe(3); + expect(textCleanup($('.tab.hidden', tabBar).text())).toBe('General|#Advanced Project Options|#Build'); + + var activeSection = configTabBar.activeSection(); + expect(textCleanup(activeSection.title)).toBe('#Build Triggers'); + + expect($('.highlight-split .highlight').text()).toBe('Trigger'); + + done(); + }, 600); + }, 'widgets/config/freestyle-config.html'); + }); + + it("- test finder - via showSections()", function (done) { + jsTest.onPage(function() { + var configTabBarWidget = jsTest.requireSrcModule('widgets/config/tabbar'); + var configTabBar = configTabBarWidget.addTabsOnFirst(); + var jQD = require('jquery-detached'); + + var $ = jQD.getJQuery(); + + var tabBar = $('.tabBar'); + + configTabBar.showSections('quiet period'); + expect($('.tab.hidden', tabBar).size()).toBe(3); + expect(textCleanup($('.tab.hidden', tabBar).text())).toBe('General|#Build Triggers|#Build'); + + var activeSection = configTabBar.activeSection(); + expect(textCleanup(activeSection.title)).toBe('#Advanced Project Options'); + + done(); + }, 'widgets/config/freestyle-config.html'); + }); + + it("- test finder - via showSections() - in inner row-group", function (done) { + jsTest.onPage(function() { + var configTabBarWidget = jsTest.requireSrcModule('widgets/config/tabbar'); + var configTabBar = configTabBarWidget.addTabsOnFirst(); + var jQD = require('jquery-detached'); + + var $ = jQD.getJQuery(); + + var tabBar = $('.tabBar'); + + configTabBar.showSections('Strategy'); + expect($('.tab.hidden', tabBar).size()).toBe(3); + expect(textCleanup($('.tab.hidden', tabBar).text())).toBe('#Advanced Project Options|#Build Triggers|#Build'); + + var activeSection = configTabBar.activeSection(); + expect(textCleanup(activeSection.title)).toBe('General'); + + done(); + }, 'widgets/config/freestyle-config.html'); + }); + + function keydowns(text, onInput) { + var jQD = require('jquery-detached'); + var $ = jQD.getJQuery(); + + // hmmm, for some reason, the key events do not result in the text being + // set in the input, so setting it manually. + onInput.val(text); + + // Now fire a keydown event to trigger the handler + var e = $.Event("keydown"); + e.which = 116; + onInput.trigger(e); + } + + function textCleanup(text) { + return text.trim().replace(/(\r\n|\n|\r)/gm, "").replace(/ +/g, "|"); + } }); // TODO: lots more tests !!! \ No newline at end of file -- GitLab From 9fe3362f012bc16076e9ea4883462612562d3466 Mon Sep 17 00:00:00 2001 From: tfennelly Date: Wed, 17 Feb 2016 19:12:07 +0000 Subject: [PATCH 0237/2380] Test section adoption As well as finding in sections after adoption. --- war/src/test/js/widgets/config/tabbar-spec.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/war/src/test/js/widgets/config/tabbar-spec.js b/war/src/test/js/widgets/config/tabbar-spec.js index 071dda22cc..46af54fa09 100644 --- a/war/src/test/js/widgets/config/tabbar-spec.js +++ b/war/src/test/js/widgets/config/tabbar-spec.js @@ -150,6 +150,39 @@ describe("tabbar-spec tests", function () { }, 'widgets/config/freestyle-config.html'); }); + it("- test adopt sections ", function (done) { + jsTest.onPage(function() { + var configTabBarWidget = jsTest.requireSrcModule('widgets/config/tabbar'); + var configTabBar = configTabBarWidget.addTabsOnFirst(); + var jQD = require('jquery-detached'); + + var $ = jQD.getJQuery(); + + var tabBar = $('.tabBar'); + + // Move the advanced stuff into the general section + var general = configTabBar.getSection('config_general'); + general.adoptSection('config__advanced_project_options'); + + // Only 3 tabs should be visible + // (used to be 4 before the merge/adopt)... + expect($('.tab', tabBar).size()).toBe(3); + expect(textCleanup($('.tab', tabBar).text())).toBe('General|#Build Triggers|#Build'); + + // And if we try to use the finder now to find something + // that was in the advanced section, it should now appear in the + // General section ... + configTabBar.showSections('quiet period'); + expect($('.tab.hidden', tabBar).size()).toBe(2); + expect(textCleanup($('.tab.hidden', tabBar).text())).toBe('#Build Triggers|#Build'); + + var activeSection = configTabBar.activeSection(); + expect(textCleanup(activeSection.title)).toBe('General'); + + done(); + }, 'widgets/config/freestyle-config.html'); + }); + function keydowns(text, onInput) { var jQD = require('jquery-detached'); var $ = jQD.getJQuery(); -- GitLab From 2a61f30250f930b579424dea9682a09e03e7d9db Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Fri, 4 Mar 2016 10:08:13 +0000 Subject: [PATCH 0238/2380] [FIXED JENKINS-33319] Premtively wake the acceptor thread to let it close cleanly - Also adds a Ping Agent protocol that could be used by nagios monitoring, etc. to verify that the slave agent listener is alive - We use the ping agent protocol to ensure that the acceptor thread wakes up, loops and sees that the shutdown is started that prevents the socket close exception from being thrown --- .../java/hudson/TcpSlaveAgentListener.java | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java index 059e2af965..02f17946e6 100644 --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java @@ -24,6 +24,12 @@ package hudson; import hudson.slaves.OfflineCause; +import java.io.DataOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.SocketAddress; +import java.util.Arrays; import jenkins.AgentProtocol; import java.io.BufferedWriter; @@ -37,6 +43,7 @@ import java.net.Socket; import java.nio.channels.ServerSocketChannel; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.commons.io.IOUtils; /** * Listens to incoming TCP connections from JNLP slave agents and CLI. @@ -91,7 +98,7 @@ public final class TcpSlaveAgentListener extends Thread { public void run() { try { // the loop eventually terminates when the socket is closed. - while (true) { + while (!shuttingDown) { Socket s = serverSocket.accept().socket(); // this prevents a connection from silently terminated by the router in between or the other peer @@ -115,6 +122,16 @@ public final class TcpSlaveAgentListener extends Thread { */ public void shutdown() { shuttingDown = true; + try { + SocketAddress localAddress = serverSocket.getLocalAddress(); + if (localAddress instanceof InetSocketAddress) { + InetSocketAddress address = (InetSocketAddress) localAddress; + Socket client = new Socket(address.getHostName(), address.getPort()); + new PingAgentProtocol().connect(client); + } + } catch (IOException e) { + LOGGER.log(Level.FINE, "Failed to send Ping to wake acceptor loop", e); + } try { serverSocket.close(); } catch (IOException e) { @@ -140,7 +157,7 @@ public final class TcpSlaveAgentListener extends Thread { @Override public void run() { try { - LOGGER.info("Accepted connection #"+id+" from "+s.getRemoteSocketAddress()); + LOGGER.log(Level.INFO, "Accepted connection #{0} from {1}", new Object[]{id,s.getRemoteSocketAddress()}); DataInputStream in = new DataInputStream(s.getInputStream()); PrintWriter out = new PrintWriter( @@ -183,6 +200,78 @@ public final class TcpSlaveAgentListener extends Thread { } } + /** + * This extension provides a Ping protocol that allows people to verify that the TcpSlaveAgentListener is alive. + * We also use this + * @since + */ + @Extension + public static class PingAgentProtocol extends AgentProtocol { + + private final byte[] ping; + + public PingAgentProtocol() { + try { + ping = "Ping\n".getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("JLS mandates support for UTF-8 charset", e); + } + } + + @Override + public String getName() { + return "Ping"; + } + + @Override + public void handle(Socket socket) throws IOException, InterruptedException { + try { + OutputStream stream = socket.getOutputStream(); + try { + LOGGER.log(Level.FINE, "Received ping request from {0}", socket.getRemoteSocketAddress()); + stream.write(ping); + stream.flush(); + LOGGER.log(Level.FINE, "Sent ping response to {0}", socket.getRemoteSocketAddress()); + } finally { + stream.close(); + } + } finally { + socket.close(); + } + } + + public boolean connect(Socket socket) throws IOException { + try { + DataOutputStream out = null; + InputStream in = null; + try { + LOGGER.log(Level.FINE, "Requesting ping from {0}", socket.getRemoteSocketAddress()); + out = new DataOutputStream(socket.getOutputStream()); + out.writeUTF("Protocol:Ping"); + in = socket.getInputStream(); + byte[] response = new byte[ping.length]; + int responseLength = in.read(response); + if (responseLength == ping.length && Arrays.equals(response, ping)) { + LOGGER.log(Level.FINE, "Received ping response from {0}", socket.getRemoteSocketAddress()); + return true; + } else { + LOGGER.log(Level.FINE, "Expected ping response from {0} of {1} got {2}", new Object[]{ + socket.getRemoteSocketAddress(), + new String(ping, "UTF-8"), + new String(response, 0, responseLength, "UTF-8") + }); + return false; + } + } finally { + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(in); + } + } finally { + socket.close(); + } + } + } + /** * Connection terminated because we are reconnected from the current peer. */ @@ -238,4 +327,4 @@ the application gets it from the dynamic JNLP. Just write it so that it can't do anything useful without going through a protected path or doing something to present credentials that could only have come from a valid user. -*/ \ No newline at end of file +*/ -- GitLab From 06d936f695598576d58865ab2a355d982d1a38e5 Mon Sep 17 00:00:00 2001 From: James Nord Date: Fri, 4 Mar 2016 16:16:35 +0000 Subject: [PATCH 0239/2380] Excluded war from test from the test harness. Whilst this is not used in the dependencies, the enforcer plugin downloads the i1.580.1 jar so it can scan it. Most likely a bug in the extra-enforcer-rules but we obviously don;t need it as we have an explicit dependency on the latest version! --- test/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/pom.xml b/test/pom.xml index 7767a50fa6..e494f99052 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -65,6 +65,12 @@ THE SOFTWARE. jenkins-test-harness 2.5 test + + + ${project.groupId} + jenkins-war + + ${project.groupId} -- GitLab From 920f20fd060f0d6fed9296ad391b724c8abf6b12 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 4 Mar 2016 13:11:53 -0800 Subject: [PATCH 0240/2380] Fixing a test Previously, when the 2nd `e.click()` is called, the page has already transitioned to another page, so the anchor objects have become invalid. HtmlUnit somehow still manages to follow a link, except when there's some JavaScript attached to the onclick() handler it's going to execute in a strange environment. Some management link requires a POST, and that was done via JavaScript. So that broke the test. --- .../java/hudson/model/ManagementLinkTest.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/src/test/java/hudson/model/ManagementLinkTest.java b/test/src/test/java/hudson/model/ManagementLinkTest.java index 4b19f2c8bb..5de5dd3d51 100644 --- a/test/src/test/java/hudson/model/ManagementLinkTest.java +++ b/test/src/test/java/hudson/model/ManagementLinkTest.java @@ -31,6 +31,7 @@ import com.gargoylesoftware.htmlunit.html.HtmlPage; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.WebClient; import java.util.List; @@ -47,11 +48,15 @@ public class ManagementLinkTest { */ @Test public void links() throws Exception { - HtmlPage page = j.createWebClient().goTo("manage"); - List anchors = DomNodeUtil.selectNodes(page, "id('management-links')//*[@class='link']/a[not(@onclick)]"); - assertTrue(anchors.size()>=8); - for(HtmlAnchor e : (List) anchors) { - e.click(); + WebClient wc = j.createWebClient(); + + for (int i=0; ; i++) { + HtmlPage page = wc.goTo("manage"); + List anchors = DomNodeUtil.selectNodes(page, "id('management-links')//*[@class='link']/a[not(@onclick)]"); + assertTrue(anchors.size()>=8); + if (i==anchors.size()) return; // done + + ((HtmlAnchor)anchors.get(i)).click(); } } } -- GitLab From b1e3f6fdd2095d17293ce2c9b26e40ea98380d73 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 4 Mar 2016 13:45:54 -0800 Subject: [PATCH 0241/2380] A/B test this feature in production JNLP3 is activated now for 10% of users. Let's keep it like this for a while and if no major issue occurs we should expose it to everyone. --- .../slaves/JnlpSlaveAgentProtocol3.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java index 0187a8b066..8b6b3df129 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java @@ -2,6 +2,7 @@ package jenkins.slaves; import hudson.AbortException; import hudson.Extension; +import hudson.Util; import hudson.model.Computer; import hudson.remoting.Channel; import hudson.remoting.ChannelBuilder; @@ -11,6 +12,8 @@ import jenkins.model.Jenkins; import jenkins.security.ChannelConfigurator; import org.jenkinsci.remoting.engine.JnlpServer3Handshake; import org.jenkinsci.remoting.nio.NioChannelHub; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import javax.inject.Inject; import java.io.IOException; @@ -35,7 +38,8 @@ public class JnlpSlaveAgentProtocol3 extends AgentProtocol { @Override public String getName() { - return "JNLP3-connect"; + if (ENABLED) return "JNLP3-connect"; + else return "JNLP3-disabled"; } @Override @@ -109,4 +113,25 @@ public class JnlpSlaveAgentProtocol3 extends AgentProtocol { } private static final Logger LOGGER = Logger.getLogger(JnlpSlaveAgentProtocol3.class.getName()); + + /** + * Flag to control the activation of JNLP3 protocol. + * This feature is being A/B tested right now. + * + *

+ * Once this will be on by default, the flag and this field will disappear. The system property is + * an escape hatch for those who hit any issues and those who are trying this out. + */ + @Restricted(NoExternalUse.class) + public static boolean ENABLED; + + static { + String propName = JnlpSlaveAgentProtocol3.class.getName() + ".enabled"; + if (System.getProperties().containsKey(propName)) + ENABLED = Boolean.getBoolean(propName); + else { + byte hash = Util.fromHexString(Jenkins.getActiveInstance().getLegacyInstanceId())[0]; + ENABLED = (hash%10)==0; + } + } } -- GitLab From d0d39705105179f7d3b8cdc7a73ccc2056fbf2e5 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 4 Mar 2016 14:31:29 -0800 Subject: [PATCH 0242/2380] Pick up a new version of remoting See https://github.com/jenkinsci/remoting/pull/78 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b4bcc0f5d0..bf317f7b61 100644 --- a/pom.xml +++ b/pom.xml @@ -179,7 +179,7 @@ THE SOFTWARE. org.jenkins-ci.main remoting - 2.55 + 2.56-20160304.223037-1 -- GitLab From 5368c96404d415451bb657aea8073834c8bd815b Mon Sep 17 00:00:00 2001 From: kzantow Date: Sat, 5 Mar 2016 18:51:02 -0500 Subject: [PATCH 0243/2380] JENKINS-30749 - make Jenkins secure out of the box: * create initial admin user with difficult password (based on UUID) * force login with password as security token * force initial admin user creation --- core/src/main/java/hudson/PluginManager.java | 2 +- .../main/java/hudson/model/UpdateCenter.java | 90 +- ...trolOnceLoggedInAuthorizationStrategy.java | 36 +- .../security/HudsonPrivateSecurityRealm.java | 27 +- .../java/jenkins/install/InstallState.java | 49 +- .../java/jenkins/install/InstallUtil.java | 11 +- .../java/jenkins/install/SetupWizard.java | 180 +++ core/src/main/java/jenkins/model/Jenkins.java | 57 +- .../config.jelly | 7 + .../_entryForm.jelly | 97 +- .../_entryFormPage.jelly | 50 + .../HudsonPrivateSecurityRealm/addUser.jelly | 2 +- .../firstUser.jelly | 2 +- .../setupWizardFirstUser.jelly | 53 + .../HudsonPrivateSecurityRealm/signup.jelly | 2 +- .../signupWithFederatedIdentity.jelly | 2 +- .../authenticate-security-token.jelly | 44 + .../jenkins/install/SetupWizard/index.jelly | 8 + .../SetupWizard/proxy-configuration.jelly | 13 + .../install/pluginSetupWizard.properties | 8 +- .../jenkins/model/Jenkins/login.jelly | 5 + .../jenkins/model/Jenkins/loginError.jelly | 4 + core/src/main/resources/lib/layout/html.jelly | 177 +++ .../main/resources/lib/layout/layout.jelly | 4 - .../model/UpdateCenterPluginInstallTest.java | 3 +- war/src/main/js/api/securityConfig.js | 33 + war/src/main/js/pluginSetupWizard.js | 7 +- war/src/main/js/pluginSetupWizardGui.js | 215 ++- war/src/main/js/templates/firstUserPanel.hbs | 13 + .../templates/incompleteInstallationPanel.hbs | 1 - war/src/main/js/templates/offlinePanel.hbs | 15 +- .../js/templates/pluginSelectionPanel.hbs | 1 - .../main/js/templates/proxyConfigPanel.hbs | 16 + .../main/js/templates/setupCompletePanel.hbs | 20 + war/src/main/js/templates/successPanel.hbs | 5 +- war/src/main/js/templates/welcomePanel.hbs | 1 - war/src/main/js/util/jenkins.js | 136 +- war/src/main/less/pluginSetupWizard.less | 1297 +++++++++-------- war/src/test/js/pluginSetupWizard-spec.js | 233 +-- 39 files changed, 1908 insertions(+), 1018 deletions(-) create mode 100644 core/src/main/java/jenkins/install/SetupWizard.java create mode 100644 core/src/main/resources/hudson/security/FullControlOnceLoggedInAuthorizationStrategy/config.jelly create mode 100644 core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryFormPage.jelly create mode 100644 core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/setupWizardFirstUser.jelly create mode 100644 core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly create mode 100644 core/src/main/resources/jenkins/install/SetupWizard/index.jelly create mode 100644 core/src/main/resources/jenkins/install/SetupWizard/proxy-configuration.jelly create mode 100644 core/src/main/resources/lib/layout/html.jelly create mode 100644 war/src/main/js/api/securityConfig.js create mode 100644 war/src/main/js/templates/firstUserPanel.hbs create mode 100644 war/src/main/js/templates/proxyConfigPanel.hbs create mode 100644 war/src/main/js/templates/setupCompletePanel.hbs diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 798a51f9ab..1c104ae410 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -1189,7 +1189,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas break; } updateCenter.persistInstallStatus(); - jenkins.setInstallState(InstallState.INITIAL_PLUGINS_INSTALLED); + jenkins.setInstallState(InstallState.INITIAL_PLUGINS_INSTALLING.getNextState()); InstallUtil.saveLastExecVersion(); } }.start(); diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index a8e9122e7c..e595f0a93d 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -57,7 +57,8 @@ import jenkins.install.InstallState; import jenkins.install.InstallUtil; import jenkins.model.Jenkins; import jenkins.util.io.OnMaster; -import net.sf.json.JSONArray; +import net.sf.json.JSONObject; + import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContext; import org.apache.commons.codec.binary.Base64; @@ -73,7 +74,6 @@ import javax.annotation.Nonnull; import javax.net.ssl.SSLHandshakeException; import javax.servlet.ServletException; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -81,12 +81,12 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; -import java.security.DigestInputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -131,7 +131,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST; */ @ExportedBean public class UpdateCenter extends AbstractModelObject implements Saveable, OnMaster { - + private static final String UPDATE_CENTER_URL = System.getProperty(UpdateCenter.class.getName()+".updateCenterUrl","http://updates.jenkins-ci.org/"); /** @@ -142,7 +142,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas @Restricted(NoExternalUse.class) public static final String ID_UPLOAD = "_upload"; - + /** * {@link ExecutorService} that performs installation. * @since 1.501 @@ -155,7 +155,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas */ protected final ExecutorService updateService = Executors.newCachedThreadPool( new NamingThreadFactory(new DaemonThreadFactory(), "Update site data downloader")); - + /** * List of created {@link UpdateCenterJob}s. Access needs to be synchronized. */ @@ -311,29 +311,16 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } } - /** - * Called to bypass install wizard - */ - @Restricted(DoNotUse.class) // WebOnly - public HttpResponse doCompleteInstall() { - if(isRestartRequiredForCompletion()) { - Jenkins.getActiveInstance().setInstallState(InstallState.RESTART); - } - InstallUtil.saveLastExecVersion(); - Jenkins.getActiveInstance().setInstallState(InstallState.INITIAL_PLUGINS_INSTALLED); - return HttpResponses.okJSON(); - } - /** * Called to determine if there was an incomplete installation, what the statuses of the plugins are */ @Restricted(DoNotUse.class) // WebOnly public HttpResponse doIncompleteInstallStatus() { try { - Map jobs = InstallUtil.getPersistedInstallStatus(); - if(jobs == null) { - jobs = Collections.emptyMap(); - } + Map jobs = InstallUtil.getPersistedInstallStatus(); + if(jobs == null) { + jobs = Collections.emptyMap(); + } return HttpResponses.okJSON(jobs); } catch (Exception e) { return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage())); @@ -353,16 +340,16 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas if (job instanceof InstallationJob) { InstallationJob installationJob = (InstallationJob) job; if(!installationJob.status.isSuccess()) { - activeInstalls = true; + activeInstalls = true; } } } if(activeInstalls) { - InstallUtil.persistInstallStatus(jobs); // save this info + InstallUtil.persistInstallStatus(jobs); // save this info } else { - InstallUtil.clearInstallStatus(); // clear this info + InstallUtil.clearInstallStatus(); // clear this info } } @@ -379,7 +366,10 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public HttpResponse doInstallStatus(StaplerRequest request) { try { String correlationId = request.getParameter("correlationId"); + Map response = new HashMap<>(); + response.put("state", Jenkins.getInstance().getInstallState().name()); List> installStates = new ArrayList<>(); + response.put("jobs", installStates); List jobCopy = getJobs(); for (UpdateCenterJob job : jobCopy) { @@ -400,7 +390,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } } } - return HttpResponses.okJSON(JSONArray.fromObject(installStates)); + return HttpResponses.okJSON(JSONObject.fromObject(response)); } catch (Exception e) { return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage())); } @@ -558,7 +548,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } response.sendRedirect2("."); } - + /** * Cancel all scheduled jenkins restarts */ @@ -845,16 +835,16 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas return new ArrayList(pluginMap.values()); } - + /** * Ensure that all UpdateSites are up to date, without requiring a user to * browse to the instance. - * + * * @return a list of {@link FormValidation} for each updated Update Site - * @throws ExecutionException - * @throws InterruptedException + * @throws ExecutionException + * @throws InterruptedException * @since 1.501 - * + * */ public List updateAllSites() throws InterruptedException, ExecutionException { List > futures = new ArrayList>(); @@ -864,8 +854,8 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas futures.add(future); } } - - List results = new ArrayList(); + + List results = new ArrayList(); for (Future f : futures) { results.add(f.get()); } @@ -1211,7 +1201,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public String getErrorMessage() { return error != null ? error.getMessage() : null; } - + public Throwable getError() { return error; } @@ -1226,10 +1216,10 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas */ @Exported(inline=true) public volatile RestartJenkinsJobStatus status = new Pending(); - + /** * Cancel job - */ + */ public synchronized boolean cancel() { if (status instanceof Pending) { status = new Canceled(); @@ -1237,7 +1227,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } return false; } - + public RestartJenkinsJob(UpdateSite site) { super(site); } @@ -1260,26 +1250,26 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public abstract class RestartJenkinsJobStatus { @Exported public final int id = iota.incrementAndGet(); - + } - + public class Pending extends RestartJenkinsJobStatus { @Exported public String getType() { return getClass().getSimpleName(); } } - + public class Running extends RestartJenkinsJobStatus { - + } - + public class Failure extends RestartJenkinsJobStatus { - + } - + public class Canceled extends RestartJenkinsJobStatus { - + } } @@ -1603,7 +1593,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas File baseDir = pm.rootDir; return new File(baseDir, plugin.name + ".jpi"); } - + private File getLegacyDestination() { File baseDir = pm.rootDir; return new File(baseDir, plugin.name + ".hpi"); @@ -1649,7 +1639,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public String toString() { return super.toString()+"[plugin="+plugin.title+"]"; } - + /** * Called when the download is completed to overwrite * the old file with the new file. @@ -1704,7 +1694,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas File baseDir = pm.rootDir; final File legacy = new File(baseDir, plugin.name + ".hpi"); if(legacy.exists()){ - return legacy; + return legacy; } return new File(baseDir, plugin.name + ".jpi"); } diff --git a/core/src/main/java/hudson/security/FullControlOnceLoggedInAuthorizationStrategy.java b/core/src/main/java/hudson/security/FullControlOnceLoggedInAuthorizationStrategy.java index 8b2239f2f4..d34bb3059f 100644 --- a/core/src/main/java/hudson/security/FullControlOnceLoggedInAuthorizationStrategy.java +++ b/core/src/main/java/hudson/security/FullControlOnceLoggedInAuthorizationStrategy.java @@ -29,6 +29,7 @@ import jenkins.model.Jenkins; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import javax.inject.Inject; import java.util.Collections; @@ -36,30 +37,53 @@ import java.util.List; /** * {@link AuthorizationStrategy} that grants full-control to authenticated user - * (other than anonymous users.) + * and optionally read access to anonymous users * * @author Kohsuke Kawaguchi */ public class FullControlOnceLoggedInAuthorizationStrategy extends AuthorizationStrategy { + /** + * Whether to allow read access, default behavior + * previously was true + */ + private boolean allowAnonymousRead = true; + @DataBoundConstructor public FullControlOnceLoggedInAuthorizationStrategy() { } @Override public ACL getRootACL() { - return THE_ACL; + return allowAnonymousRead ? ANONYMOUS_READ : AUTHENTICATED_READ; } public List getGroups() { return Collections.emptyList(); } + + /** + * If true, anonymous read access will be allowed + */ + public boolean isAllowAnonymousRead() { + return allowAnonymousRead; + } + + @DataBoundSetter + public void setAllowAnonymousRead(boolean allowAnonymousRead) { + this.allowAnonymousRead = allowAnonymousRead; + } - private static final SparseACL THE_ACL = new SparseACL(null); + private static final SparseACL AUTHENTICATED_READ = new SparseACL(null); + private static final SparseACL ANONYMOUS_READ = new SparseACL(null); static { - THE_ACL.add(ACL.EVERYONE, Jenkins.ADMINISTER,true); - THE_ACL.add(ACL.ANONYMOUS, Jenkins.ADMINISTER,false); - THE_ACL.add(ACL.ANONYMOUS,Permission.READ,true); + ANONYMOUS_READ.add(ACL.EVERYONE, Jenkins.ADMINISTER,true); + ANONYMOUS_READ.add(ACL.ANONYMOUS, Jenkins.ADMINISTER,false); + ANONYMOUS_READ.add(ACL.ANONYMOUS, Permission.READ,true); + + AUTHENTICATED_READ.add(ACL.EVERYONE, Jenkins.ADMINISTER, true); + AUTHENTICATED_READ.add(ACL.ANONYMOUS, Jenkins.ADMINISTER, false); + AUTHENTICATED_READ.add(ACL.ANONYMOUS, Permission.READ, false); } /** diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java index ffaf8311d7..63404b636e 100644 --- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java +++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java @@ -1,18 +1,18 @@ /* * The MIT License - * + * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, David Calavera, Seiji Sogabe - * + * * 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 @@ -29,6 +29,8 @@ import hudson.ExtensionList; import hudson.Util; import hudson.diagnosis.OldDataMonitor; import hudson.model.Descriptor; +import jenkins.install.InstallState; +import jenkins.install.SetupWizard; import jenkins.model.Jenkins; import hudson.model.ManagementLink; import hudson.model.ModelObject; @@ -97,7 +99,7 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea /** * If true, sign up is not allowed. *

- * This is a negative switch so that the default value 'false' remains compatible with older installations. + * This is a negative switch so that the default value 'false' remains compatible with older installations. */ private final boolean disableSignup; @@ -279,13 +281,24 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea * This can be run by anyone, but only to create the very first user account. */ public void doCreateFirstAccount(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - if(hasSomeUser()) { + boolean inSetup = !Jenkins.getInstance().getInstallState().isSetupComplete(); + if(!inSetup && hasSomeUser()) { rsp.sendError(SC_UNAUTHORIZED,"First user was already created"); return; } - User u = createAccount(req, rsp, false, "firstUser.jelly"); + String view = "firstUser.jelly"; + User admin = null; + if(inSetup) { + admin = getUser(SetupWizard.initialSetupAdminUserName); + view = "setupWizardFirstUser.jelly"; + } + User u = createAccount(req, rsp, false, view); if (u!=null) { tryToMakeAdmin(u); + if(admin != null) { + admin.delete(); + } + Jenkins.getInstance().setInstallState(InstallState.CREATE_ADMIN_USER.getNextState()); loginAndTakeBack(req, rsp, u); } } diff --git a/core/src/main/java/jenkins/install/InstallState.java b/core/src/main/java/jenkins/install/InstallState.java index 510feb6c78..c634180bc8 100644 --- a/core/src/main/java/jenkins/install/InstallState.java +++ b/core/src/main/java/jenkins/install/InstallState.java @@ -34,32 +34,63 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; @Restricted(NoExternalUse.class) public enum InstallState { /** - * New Jenkins install. + * The initial set up has been completed + */ + INITIAL_SETUP_COMPLETED(true, null), + /** + * Creating an admin user for an initial Jenkins install. */ - NEW, + CREATE_ADMIN_USER(false, INITIAL_SETUP_COMPLETED), /** * New Jenkins install. The user has kicked off the process of installing an * initial set of plugins (via the install wizard). */ - INITIAL_PLUGINS_INSTALLING, + INITIAL_PLUGINS_INSTALLING(false, CREATE_ADMIN_USER), /** - * New Jenkins install. The initial set of plugins are now installed. + * New Jenkins install. */ - INITIAL_PLUGINS_INSTALLED, + NEW(false, INITIAL_PLUGINS_INSTALLING), /** * Restart of an existing Jenkins install. */ - RESTART, + RESTART(true, INITIAL_SETUP_COMPLETED), /** * Upgrade of an existing Jenkins install. */ - UPGRADE, + UPGRADE(true, INITIAL_SETUP_COMPLETED), /** * Downgrade of an existing Jenkins install. */ - DOWNGRADE, + DOWNGRADE(true, INITIAL_SETUP_COMPLETED), /** * Jenkins started in test mode (JenkinsRule). */ - TEST + TEST(true, INITIAL_SETUP_COMPLETED), + /** + * Jenkins started in development mode: Bolean.getBoolean("hudson.Main.development"). + * Can be run normally with the -Djenkins.install.runSetupWizard=true + */ + DEVELOPMENT(true, INITIAL_SETUP_COMPLETED); + + private final boolean isSetupComplete; + private final InstallState nextState; + + private InstallState(boolean isSetupComplete, InstallState nextState) { + this.isSetupComplete = isSetupComplete; + this.nextState = nextState; + } + + /** + * Indicates the initial setup is complete + */ + public boolean isSetupComplete() { + return isSetupComplete; + } + + /** + * Gets the next state + */ + public InstallState getNextState() { + return nextState; + } } diff --git a/core/src/main/java/jenkins/install/InstallUtil.java b/core/src/main/java/jenkins/install/InstallUtil.java index e9622b1a9a..6fff100057 100644 --- a/core/src/main/java/jenkins/install/InstallUtil.java +++ b/core/src/main/java/jenkins/install/InstallUtil.java @@ -68,8 +68,15 @@ public class InstallUtil { * @return The type of "startup" currently under way in Jenkins. */ public static InstallState getInstallState() { - if (Functions.getIsUnitTest()) { - return InstallState.TEST; + // install wizard will always run if environment specified + if (!Boolean.getBoolean("jenkins.install.runSetupWizard")) { + if (Functions.getIsUnitTest()) { + return InstallState.TEST; + } + + if (Boolean.getBoolean("hudson.Main.development")) { + return InstallState.DEVELOPMENT; + } } VersionNumber lastRunVersion = new VersionNumber(getLastExecVersion()); diff --git a/core/src/main/java/jenkins/install/SetupWizard.java b/core/src/main/java/jenkins/install/SetupWizard.java new file mode 100644 index 0000000000..a6bde8d883 --- /dev/null +++ b/core/src/main/java/jenkins/install/SetupWizard.java @@ -0,0 +1,180 @@ +package jenkins.install; + +import java.io.IOException; +import java.util.Locale; +import java.util.UUID; +import java.util.logging.Logger; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +import hudson.BulkChange; +import hudson.ExtensionList; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.security.AuthorizationStrategy; +import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; +import hudson.security.HudsonPrivateSecurityRealm; +import hudson.security.PermissionAdder; +import hudson.security.SecurityRealm; +import hudson.security.csrf.DefaultCrumbIssuer; +import hudson.util.PluginServletFilter; +import jenkins.model.Jenkins; +import jenkins.security.s2m.AdminWhitelistRule; + +/** + * A Jenkins instance used during first-run to provide a limited set of services while + * initial installation is in progress + */ +public class SetupWizard { + /** + * The security token parameter name + */ + public static String initialSetupAdminUserName = "initial-setup-admin-user"; + + private final Logger LOGGER = Logger.getLogger(SetupWizard.class.getName()); + + public SetupWizard(Jenkins j) throws IOException { + User admin; + // Create an admin user by default with a + // difficult password + if(j.getSecurityRealm() == null || j.getSecurityRealm() == SecurityRealm.NO_AUTHENTICATION) { // this seems very fragile + BulkChange bc = new BulkChange(j); + + HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null); + j.setSecurityRealm(securityRealm); + String randomUUID = UUID.randomUUID().toString().replace("-", "").toLowerCase(Locale.ENGLISH); + admin = securityRealm.createAccount(SetupWizard.initialSetupAdminUserName, randomUUID); + admin.addProperty(new SetupWizard.AuthenticationKey(randomUUID)); + + AuthorizationStrategy as = Jenkins.getInstance().getAuthorizationStrategy(); + for (PermissionAdder adder : ExtensionList.lookup(PermissionAdder.class)) { + if (adder.add(as, admin, Jenkins.ADMINISTER)) { + return; + } + } + + // Lock Jenkins down: + FullControlOnceLoggedInAuthorizationStrategy authStrategy = new FullControlOnceLoggedInAuthorizationStrategy(); + authStrategy.setAllowAnonymousRead(false); + j.setAuthorizationStrategy(authStrategy); + + // Shut down all the ports we can by default: + j.setSlaveAgentPort(-1); // -1 to disable + + // require a crumb issuer + j.setCrumbIssuer(new DefaultCrumbIssuer(false)); + + // set master -> slave security: + j.getInjector().getInstance(AdminWhitelistRule.class) + .setMasterKillSwitch(false); + + try{ + j.save(); // !! + } finally { + bc.commit(); + } + } + else { + admin = j.getUser(SetupWizard.initialSetupAdminUserName); + } + + String setupKey = null; + if(admin != null && admin.getProperty(SetupWizard.AuthenticationKey.class) != null) { + setupKey = admin.getProperty(SetupWizard.AuthenticationKey.class).getKey(); + } + if(setupKey != null) { + LOGGER.info("\n\n*************************************************************\n" + + "*************************************************************\n" + + "*************************************************************\n" + + "\n" + + "Jenkins initial setup is required. A security token is required to proceed. \n" + + "Please use the following security token to proceed to installation: \n" + + "\n" + + "" + setupKey + "\n" + + "\n" + + "*************************************************************\n" + + "*************************************************************\n" + + "*************************************************************\n"); + } + + try { + PluginServletFilter.addFilter(FORCE_SETUP_WIZARD_FILTER); + } catch (ServletException e) { + throw new AssertionError(e); + } + } + + /** + * Remove the setupWizard filter, ensure all updates are written to disk, etc + */ + public void doCompleteSetupWizard() { + InstallUtil.saveLastExecVersion(); + try { + PluginServletFilter.removeFilter(FORCE_SETUP_WIZARD_FILTER); + } catch (ServletException e) { + throw new AssertionError(e); // never happen because our Filter.init is no-op + } + } + + // Stores a user property for the authentication key, which is really the auto-generated user's password + public static class AuthenticationKey extends UserProperty { + String key; + + public AuthenticationKey() { + } + + public AuthenticationKey(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + } + + /** + * This filter will validate that the security token is provided + */ + private final Filter FORCE_SETUP_WIZARD_FILTER = new Filter() { + @Override + public void init(FilterConfig cfg) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + // As an extra measure of security, the install wizard generates a security token, and + // requires the user to enter it before proceeding through the installation. Once set + // we'll set a cookie so the subsequent operations succeed + if (request instanceof HttpServletRequest) { + HttpServletRequest req = (HttpServletRequest)request; + //if (!Pattern.compile(".*[.](css|ttf|gif|woff|eot|png|js)").matcher(req.getRequestURI()).matches()) { + // Allow js & css requests through + if((req.getContextPath() + "/").equals(req.getRequestURI())) { + chain.doFilter(new HttpServletRequestWrapper(req) { + public String getRequestURI() { + return getContextPath() + "/setupWizard/"; + } + }, response); + return; + } + // fall through to handling the request normally + } + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } + }; +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 60154b64fe..e6a96e9d23 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -194,6 +194,7 @@ import jenkins.ExtensionRefreshException; import jenkins.InitReactorRunner; import jenkins.install.InstallState; import jenkins.install.InstallUtil; +import jenkins.install.SetupWizard; import jenkins.model.ProjectNamingStrategy.DefaultProjectNamingStrategy; import jenkins.security.ConfidentialKey; import jenkins.security.ConfidentialStore; @@ -333,7 +334,13 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * The Jenkins instance startup type i.e. NEW, UPGRADE etc */ - private InstallState installState; + private transient InstallState installState = InstallState.NEW; + + /** + * If we're in the process of an initial setup, + * this will be set + */ + private transient SetupWizard setupWizard; /** * Number of executors of the master node. @@ -759,7 +766,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve protected Jenkins(File root, ServletContext context, PluginManager pluginManager) throws IOException, InterruptedException, ReactorException { long start = System.currentTimeMillis(); - // As Jenkins is starting, grant this process full control + // As Jenkins is starting, grant this process full control ACL.impersonate(ACL.SYSTEM); try { this.root = root; @@ -773,7 +780,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if (installState == InstallState.RESTART || installState == InstallState.DOWNGRADE) { InstallUtil.saveLastExecVersion(); } - + if (!new File(root,"jobs").exists()) { // if this is a fresh install, use more modern default layout that's consistent with agents workspaceDir = "${JENKINS_HOME}/workspace/${ITEM_FULLNAME}"; @@ -834,6 +841,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if(KILL_AFTER_LOAD) System.exit(0); + if(!installState.isSetupComplete()) { + // Start immediately with the setup wizard for new installs + setupWizard = new SetupWizard(this); + } + launchTcpSlaveAgentListener(); if (UDPBroadcastThread.PORT != -1) { @@ -913,6 +925,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * Get the Jenkins {@link jenkins.install.InstallState install state}. * @return The Jenkins {@link jenkins.install.InstallState install state}. */ + @Nonnull @Restricted(NoExternalUse.class) public InstallState getInstallState() { return installState; @@ -923,7 +936,12 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @Restricted(NoExternalUse.class) public void setInstallState(@Nonnull InstallState newState) { + InstallState prior = installState; installState = newState; + if(setupWizard != null && !newState.equals(prior) && newState.isSetupComplete()) { + setupWizard.doCompleteSetupWizard(); + setupWizard = null; + } } /** @@ -1437,10 +1455,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @Exported(name="jobs") public List getItems() { - if (authorizationStrategy instanceof AuthorizationStrategy.Unsecured || - authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy) { - return new ArrayList(items.values()); - } + if (authorizationStrategy instanceof AuthorizationStrategy.Unsecured || + authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy) { + return new ArrayList(items.values()); + } List viewableItems = new ArrayList(); for (TopLevelItem item : items.values()) { @@ -1815,11 +1833,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } public DescribableList, NodePropertyDescriptor> getNodeProperties() { - return nodeProperties; + return nodeProperties; } public DescribableList, NodePropertyDescriptor> getGlobalNodeProperties() { - return globalNodeProperties; + return globalNodeProperties; } /** @@ -2428,7 +2446,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @Override public TopLevelItem getItem(String name) throws AccessDeniedException { if (name==null) return null; - TopLevelItem item = items.get(name); + TopLevelItem item = items.get(name); if (item==null) return null; if (!item.hasPermission(Item.READ)) { @@ -3079,10 +3097,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } public HttpResponse doToggleCollapse() throws ServletException, IOException { - final StaplerRequest request = Stapler.getCurrentRequest(); - final String paneId = request.getParameter("paneId"); + final StaplerRequest request = Stapler.getCurrentRequest(); + final String paneId = request.getParameter("paneId"); - PaneStatusProperties.forCurrentUser().toggleCollapsed(paneId); + PaneStatusProperties.forCurrentUser().toggleCollapsed(paneId); return HttpResponses.forwardToPreviousPage(); } @@ -3113,9 +3131,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve LOGGER.info("Failed to get thread dump for node " + c.getName() + ": " + e.getMessage()); } } - if (toComputer() == null) { - future.put("master", RemotingDiagnostics.getThreadDumpAsync(FilePath.localChannel)); - } + if (toComputer() == null) { + future.put("master", RemotingDiagnostics.getThreadDumpAsync(FilePath.localChannel)); + } // if the result isn't available in 5 sec, ignore that. // this is a precaution against hang nodes @@ -3891,6 +3909,13 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve public List getManagementLinks() { return ManagementLink.all(); } + + /** + * If set, a currently active setup wizard - e.g. installation + */ + public SetupWizard getSetupWizard() { + return setupWizard; + } /** * Exposes the current user to /me URL. diff --git a/core/src/main/resources/hudson/security/FullControlOnceLoggedInAuthorizationStrategy/config.jelly b/core/src/main/resources/hudson/security/FullControlOnceLoggedInAuthorizationStrategy/config.jelly new file mode 100644 index 0000000000..0e627effbc --- /dev/null +++ b/core/src/main/resources/hudson/security/FullControlOnceLoggedInAuthorizationStrategy/config.jelly @@ -0,0 +1,7 @@ + + + + + + diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryForm.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryForm.jelly index bb12a30aa9..346d8c4d53 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryForm.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryForm.jelly @@ -25,64 +25,43 @@ THE SOFTWARE. - - - - - - - - -

${title}

-
- -
- ${data.errorMessage} -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
${%Username}:
${%Password}:
${%Confirm password}:
${%Full name}:
${%E-mail address}:
${%Enter text as shown}: -
- [captcha] -
- - - +

${title}

+
+ +
+ ${data.errorMessage}
- - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
${%Username}:
${%Password}:
${%Confirm password}:
${%Full name}:
${%E-mail address}:
${%Enter text as shown}: +
+ [captcha] +
+
diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryFormPage.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryFormPage.jelly new file mode 100644 index 0000000000..8009ad6518 --- /dev/null +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryFormPage.jelly @@ -0,0 +1,50 @@ + + + + + + + + + + + + + +
+ + + + +
+
+
diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/addUser.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/addUser.jelly index aed3d5c5c8..daacc17bfc 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/addUser.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/addUser.jelly @@ -27,5 +27,5 @@ THE SOFTWARE. --> - + \ No newline at end of file diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/firstUser.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/firstUser.jelly index bc44b42acc..6ebd369ae0 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/firstUser.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/firstUser.jelly @@ -27,5 +27,5 @@ THE SOFTWARE. --> - + \ No newline at end of file diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/setupWizardFirstUser.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/setupWizardFirstUser.jelly new file mode 100644 index 0000000000..4f7475462f --- /dev/null +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/setupWizardFirstUser.jelly @@ -0,0 +1,53 @@ + + + + + +
+ + + +
+
diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly index 656e96c3c9..f2477da1df 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly @@ -27,5 +27,5 @@ THE SOFTWARE. --> - + diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signupWithFederatedIdentity.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signupWithFederatedIdentity.jelly index 01c0197a62..8daec8d381 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signupWithFederatedIdentity.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signupWithFederatedIdentity.jelly @@ -27,5 +27,5 @@ THE SOFTWARE. --> - + \ No newline at end of file diff --git a/core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly b/core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly new file mode 100644 index 0000000000..b22936d9ad --- /dev/null +++ b/core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly @@ -0,0 +1,44 @@ + + + +
+
+ +
+
+
+
diff --git a/core/src/main/resources/jenkins/install/SetupWizard/index.jelly b/core/src/main/resources/jenkins/install/SetupWizard/index.jelly new file mode 100644 index 0000000000..d001d433d5 --- /dev/null +++ b/core/src/main/resources/jenkins/install/SetupWizard/index.jelly @@ -0,0 +1,8 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -169,7 +178,7 @@ ${h.initPageVariables(context)} @@ -174,7 +165,7 @@ ${h.initPageVariables(context)} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
Quiet period
Number of seconds
SCM checkout retry count
+ + +
+ +
+ + +
Directory
Display Name
+ +
+ + + + + + +
#Build Triggers
+ + + + + + + + + + + +
#Build
+ + + + +
+
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/war/src/test/js/widgets/config/mocks.js b/war/src/test/js/widgets/config/mocks.js new file mode 100644 index 0000000000..d43c774af3 --- /dev/null +++ b/war/src/test/js/widgets/config/mocks.js @@ -0,0 +1,17 @@ +var jsTest = require("jenkins-js-test"); + +// mock the behaviors stuff. +var behaviorShim = jsTest.requireSrcModule('util/behavior-shim'); +behaviorShim.specify = function(selector, id, priority, behavior) { + behavior(); +}; + +// Mock out the fireBottomStickerAdjustEvent function ... it accesses Event. +var page = jsTest.requireSrcModule('util/page'); +page.fireBottomStickerAdjustEvent = function() {}; + +var windowHandle = require('window-handle'); +windowHandle.getWindow(function() { + var localStorage = jsTest.requireSrcModule('util/localStorage'); + localStorage.setMock(); +}); diff --git a/war/src/test/js/widgets/config/tabbar-spec.js b/war/src/test/js/widgets/config/tabbar-spec.js index 2d5a475919..0bc5720f9c 100644 --- a/war/src/test/js/widgets/config/tabbar-spec.js +++ b/war/src/test/js/widgets/config/tabbar-spec.js @@ -1,11 +1,13 @@ var jsTest = require("jenkins-js-test"); +require('./mocks'); + describe("tabbar-spec tests", function () { it("- test section count", function (done) { jsTest.onPage(function() { - var configTabBar = jsTest.requireSrcModule('widgets/config/tabbar'); - var firstTableMetadata = configTabBar.addTabsOnFirst(); + var tabbars = jsTest.requireSrcModule('config-tabbar.js'); + var firstTableMetadata = tabbars.tabs[0]; var jQD = require('jquery-detached'); var $ = jQD.getJQuery(); @@ -23,8 +25,8 @@ describe("tabbar-spec tests", function () { it("- test section activation", function (done) { jsTest.onPage(function() { - var configTabBar = jsTest.requireSrcModule('widgets/config/tabbar'); - var firstTableMetadata = configTabBar.addTabsOnFirst(); + var tabbars = jsTest.requireSrcModule('config-tabbar.js'); + var firstTableMetadata = tabbars.tabs[0]; // The first section ("General") should be active by default expect(firstTableMetadata.activeSection().id).toBe('config_general'); -- GitLab From cfab9441694d1099b755d8421f4e7fdc8c5f1de0 Mon Sep 17 00:00:00 2001 From: tfennelly Date: Wed, 16 Mar 2016 14:22:59 +0000 Subject: [PATCH 0415/2380] Fixing more scrollspy bugs + adding some tests --- war/src/main/js/config-scrollspy.js | 154 ++++++--- war/src/main/js/util/page.js | 28 +- .../js/widgets/config/model/ConfigSection.js | 39 +++ .../config/model/ConfigTableMetaData.js | 24 +- .../config/freestyle-config-scrollspy.html | 302 +++++++++--------- .../test/js/widgets/config/scrollspy-spec.js | 117 +++++++ war/src/test/js/widgets/config/tabbar-spec.js | 31 ++ 7 files changed, 486 insertions(+), 209 deletions(-) create mode 100644 war/src/test/js/widgets/config/scrollspy-spec.js diff --git a/war/src/main/js/config-scrollspy.js b/war/src/main/js/config-scrollspy.js index c2ae51eee9..4634ea7c77 100644 --- a/war/src/main/js/config-scrollspy.js +++ b/war/src/main/js/config-scrollspy.js @@ -1,87 +1,143 @@ var $ = require('jquery-detached').getJQuery(); var page = require('./util/page.js'); var windowHandle = require('window-handle'); +var isScrolling = false; +var ignoreNextScrollEvent = false; +var pageHeaderHeight = page.pageHeaderHeight(); +var breadcrumbBarHeight = page.breadcrumbBarHeight(); + +// Some stuff useful for testing. +exports.tabbars = []; +exports.scrollspeed = 500; +var eventListeners = []; +exports.on = function(listener) { + eventListeners.push(listener); +}; +function notify(event) { + for (var i = 0; i < eventListeners.length; i++) { + eventListeners[i](event); + } +} $(function() { var tabBarWidget = require('./widgets/config/tabbar.js'); tabBarWidget.addPageTabs('.config-table.scrollspy', function(tabBar) { + exports.tabbars.push(tabBar); + tabBarWidget.addFinderToggle(tabBar); tabBar.onShowSection(function() { // Scroll to the section. - scrollTo(this); + scrollTo(this, tabBar); // Hook back into hudson-behavior.js page.fireBottomStickerAdjustEvent(); }); watchScroll(tabBar); - $(windowHandle.getWindow()).on('scroll',function(){watchScroll(tabBar);}); + page.onWinScroll(function () { + watchScroll(tabBar); + }); }); }); -function scrollTo(section) { +function scrollTo(section, tabBar) { var $header = section.headerRow; var scrollTop = $header.offset().top - ($('#main-panel .jenkins-config-widgets').outerHeight() + 15); + isScrolling = true; $('html,body').animate({ - scrollTop: scrollTop - }, 500); - setTimeout(function(){ - section.activator.closest('.tabBar').find('.active').removeClass('active'); - section.activator.addClass('active'); - }, 510); + scrollTop: scrollTop + }, exports.scrollspeed, function() { + if (isScrolling) { + notify({ + type: 'click_scrollto', + section: section + }); + isScrolling = false; + ignoreNextScrollEvent = stickTabbar(tabBar); + } + }); } -function watchScroll(tabControl) { - var $window = $(windowHandle.getWindow()); - var $tabBox = tabControl.configWidgets; - var $tabs = $tabBox.find('.tab'); - var $table = tabControl.configTable; - var $jenkTools = $('#breadcrumbBar'); - var winScoll = $window.scrollTop(); - var categories = tabControl.sections; - var jenkToolOffset = ($jenkTools.height() + $jenkTools.offset().top); - - // reset tabs to start... - $tabs.find('.active').removeClass('active'); - - function getCatTop($cat) { - return ($cat.length > 0) ? - $cat.offset().top - jenkToolOffset - : 0; +/** + * Watch page scrolling, changing the active tab as needed + moving the + * tabbar to stick it to the top of the visible area. + * @param tabBar The tabbar. + */ +function watchScroll(tabBar) { + if (isScrolling === true) { + // Ignore window scroll events while we are doing a scroll. + // See scrollTo function. + return; } + if (ignoreNextScrollEvent === true) { + // Things like repositioning of the tabbar (see stickTabbar) + // can trigger scroll events that we want to ignore. + ignoreNextScrollEvent = false; + return; + } + + var winScrollTop = page.winScrollTop(); + var sections = tabBar.sections; // calculate the top and height of each section to know where to switch the tabs... - $.each(categories, function (i, cat) { - var $cat = $(cat.headerRow); - var $nextCat = (i + 1 < categories.length) ? - $(categories[i + 1].headerRow) : - $cat; - // each category enters the viewport at its distance down the page, less the height of the toolbar, which hangs down the page... - // or it is zero if the category doesn't match or was removed... - var catTop = getCatTop($cat); + $.each(sections, function (i, section) { + // each section enters the viewport at its distance down the page, less the height of + // the toolbar, which hangs down the page. Or it is zero if the section doesn't + // match or was removed... + var viewportEntryOffset = section.getViewportEntryOffset(); // height of this one is the top of the next, less the top of this one. - var catHeight = getCatTop($nextCat) - catTop; + var sectionHeight = 0; + var nextSection = section.getSibling(+1); + if (nextSection) { + sectionHeight = nextSection.getViewportEntryOffset() - viewportEntryOffset; + } - // the trigger point to change the tab happens when the scroll position passes below the height of the category... - // ...but we want to wait to advance the tab until the existing category is 75% off the top... - if (winScoll < (catTop + (0.75 * catHeight))) { - var $thisTab = $($tabs.get(i)); - var $nav = $thisTab.closest('.tabBar'); - $nav.find('.active').removeClass('active'); - $thisTab.addClass('active'); + // the trigger point to change the tab happens when the scroll position passes below the height of the section... + // ...but we want to wait to advance the tab until the existing section is 75% off the top... + // ### < 75% ADVANCED + if (winScrollTop < (viewportEntryOffset + (0.75 * sectionHeight))) { + section.markAsActive(); + notify({ + type: 'manual_scrollto', + section: section + }); return false; } }); - if (winScoll > $('#page-head').height() - 5) { - $tabBox.width($tabBox.width()).css({ + stickTabbar(tabBar); +} + +/** + * Stick the scrollspy tabbar to the top of the visible area as the user + * scrolls down the page. + * @param tabBar The tabbar. + */ +function stickTabbar(tabBar) { + var win = $(windowHandle.getWindow()); + var winScrollTop = page.winScrollTop(); + var widgetBox = tabBar.configWidgets; + var configTable = tabBar.configTable; + var configForm = tabBar.configForm; + var setWidth = function() { + widgetBox.width(configForm.outerWidth() - 2); + }; + + if (winScrollTop > pageHeaderHeight - 5) { + setWidth(); + widgetBox.css({ 'position': 'fixed', - 'top': ($jenkTools.height() - 5 ) + 'px' + 'top': (breadcrumbBarHeight - 5 ) + 'px', + 'margin': '0 auto !important' }); - $table.css({'margin-top': $tabBox.outerHeight() + 'px'}); - + configTable.css({'margin-top': widgetBox.outerHeight() + 'px'}); + win.resize(setWidth); + return true; } else { - $tabBox.add($table).removeAttr('style'); + widgetBox.removeAttr('style'); + configTable.removeAttr('style'); + win.unbind('resize', setWidth); + return false; } -} \ No newline at end of file +} diff --git a/war/src/main/js/util/page.js b/war/src/main/js/util/page.js index d8c84a3cb1..5cf35ad8bb 100644 --- a/war/src/main/js/util/page.js +++ b/war/src/main/js/util/page.js @@ -1,4 +1,24 @@ var jQD = require('jquery-detached'); +var windowHandle = require('window-handle'); + +exports.winScrollTop = function() { + var $ = jQD.getJQuery(); + var win = $(windowHandle.getWindow()); + return win.scrollTop(); +}; + +exports.onWinScroll = function(callback) { + var $ = jQD.getJQuery(); + $(windowHandle.getWindow()).on('scroll', callback); +}; + +exports.pageHeaderHeight = function() { + return elementHeight('#page-head'); +}; + +exports.breadcrumbBarHeight = function() { + return elementHeight('#breadcrumbBar'); +}; exports.fireBottomStickerAdjustEvent = function() { Event.fire(window, 'jenkins:bottom-sticker-adjust'); // jshint ignore:line @@ -28,9 +48,13 @@ exports.fixDragEvent = function(handle) { exports.removeTextHighlighting = function(selector) { var $ = jQD.getJQuery(); $('span.highlight-split', selector).each(function() { - console.log('remove'); var highlightSplit = $(this); highlightSplit.before(highlightSplit.text()); highlightSplit.remove(); }); -}; \ No newline at end of file +}; + +function elementHeight(selector) { + var $ = jQD.getJQuery(); + return $(selector).height(); +} \ No newline at end of file diff --git a/war/src/main/js/widgets/config/model/ConfigSection.js b/war/src/main/js/widgets/config/model/ConfigSection.js index 8ae381a0ad..823b7c2c65 100644 --- a/war/src/main/js/widgets/config/model/ConfigSection.js +++ b/war/src/main/js/widgets/config/model/ConfigSection.js @@ -2,6 +2,7 @@ var jQD = require('../../../util/jquery-ext.js'); var util = require('./util.js'); var page = require('../../../util/page.js'); var ConfigRowGrouping = require('./ConfigRowGrouping.js'); +var pageHeaderHeight = page.pageHeaderHeight(); module.exports = ConfigSection; @@ -26,6 +27,38 @@ ConfigSection.prototype.isTopLevelSection = function() { return (this.parentCMD.getSection(this.id) !== undefined); }; +/** + * Get the page offset (height) at which this section comes + * into view. + * @returns {number} + */ +ConfigSection.prototype.getViewportEntryOffset = function() { + return this.headerRow.offset().top - pageHeaderHeight; +}; + +/** + * Get the sibling section at the relative offset. + * @param relOffset + */ +ConfigSection.prototype.getSibling = function(relOffset) { + var sections = this.parentCMD.sections; + var endIndex = sections.length - 1; + + for (var i = 0; i < endIndex; i++) { + var testIndex = i + relOffset; + if (testIndex < 0) { + continue; + } else if (testIndex > endIndex) { + return undefined; + } + if (sections[i] === this) { + return sections[testIndex]; + } + } + + return undefined; +}; + /** * Move another top-level section into this section i.e. adopt it. *

@@ -94,6 +127,12 @@ ConfigSection.prototype.activate = function() { } }; +ConfigSection.prototype.markAsActive = function() { + this.parentCMD.hideSection(); + this.activator.addClass('active'); + this.markRowsAsActive(); +}; + ConfigSection.prototype.markRowsAsActive = function() { var rows = this.getRows(); for (var i = 0; i < rows.length; i++) { diff --git a/war/src/main/js/widgets/config/model/ConfigTableMetaData.js b/war/src/main/js/widgets/config/model/ConfigTableMetaData.js index 2ed5ca2030..507d921804 100644 --- a/war/src/main/js/widgets/config/model/ConfigTableMetaData.js +++ b/war/src/main/js/widgets/config/model/ConfigTableMetaData.js @@ -190,12 +190,20 @@ ConfigTableMetaData.prototype.activeSection = function() { } }; -ConfigTableMetaData.prototype.getSection = function(sectionId) { +ConfigTableMetaData.prototype.getSection = function(ref) { if (this.hasSections()) { - for (var i = 0; i < this.sections.length; i++) { - var section = this.sections[i]; - if (section.id === sectionId) { - return section; + if (typeof ref === 'number') { + // It's a section index... + if (ref >= 0 && ref <= this.sections.length - 1) { + return this.sections[ref]; + } + } else { + // It's a section ID... + for (var i = 0; i < this.sections.length; i++) { + var section = this.sections[i]; + if (section.id === ref) { + return section; + } } } } @@ -245,12 +253,8 @@ ConfigTableMetaData.prototype.showSection = function(section) { if (section) { var topRows = this.getTopRows(); - // Deactivate currently active section ... - this.hideSection(); - // Active the specified section - section.activator.addClass('active'); - section.markRowsAsActive(); + section.markAsActive(); // and always show the buttons topRows.filter('.config_buttons').show(); diff --git a/war/src/test/js/widgets/config/freestyle-config-scrollspy.html b/war/src/test/js/widgets/config/freestyle-config-scrollspy.html index 9068acfc25..1f3a6be104 100644 --- a/war/src/test/js/widgets/config/freestyle-config-scrollspy.html +++ b/war/src/test/js/widgets/config/freestyle-config-scrollspy.html @@ -1,163 +1,169 @@ -

- - - - - + + + + + + + + + + + + + + + + +
Project name +
+ + + + + + + - - - - + + + + + - - + + - - - - - - - - - - + + + + + + + + + + - - - - - + + + + - - - - - - - - - - - - - - - - -
Project name -
-
Loading...
-
Strategy
+
Loading...
+
Strategy
-
#Advanced Project Options
-
- -
+
#Advanced Project Options
+
+ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
Quiet period
Number of seconds
SCM checkout retry count
- - -
- -
- - -
Directory
Display Name
- -
-
-
#Build Triggers
-
- - - -
-
#Build
-
-
-
-
-
- -
-
-
- \ No newline at end of file +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
Quiet period
Number of seconds
SCM checkout retry count
+ + +
+ +
+ + +
Directory
Display Name
+ +
+
+
#Build Triggers
+
+ + + +
+
#Build
+
+
+
+
+
+ +
+
+
+ +
diff --git a/war/src/test/js/widgets/config/scrollspy-spec.js b/war/src/test/js/widgets/config/scrollspy-spec.js new file mode 100644 index 0000000000..e072ee34be --- /dev/null +++ b/war/src/test/js/widgets/config/scrollspy-spec.js @@ -0,0 +1,117 @@ +var jsTest = require("jenkins-js-test"); + +require('./mocks'); + +describe("scrollspy-spec tests", function () { + + it("- test scrolling", function (done) { + jsTest.onPage(function () { + var manualScroller = newManualScroller(); + var tabbars = jsTest.requireSrcModule('config-scrollspy.js'); + tabbars.scrollspeed = 1; // speed up the scroll speed for testing + + var tabbar = tabbars.tabbars[0]; + + // We need to trick it into thinking that the sections have + // some height. We need height if we want to scroll. + doSectionFunctionMocking(tabbar); + + // console.log('**** ' + tabbar.sectionIds()); + // **** config_general,config__advanced_project_options,config__build_triggers,config__build + + var scrollToLog = []; + var click_scrollto_done = false; + var manual_scrollto_done = true; + tabbars.on(function (event) { + if (event.type === 'manual_scrollto') { + manual_scrollto_done = true; + } + if (event.type === 'click_scrollto') { + expect(event.section.id).toBe('config__build'); + click_scrollto_done = true; + } + scrollToLog.push(event.section.id); + + if (click_scrollto_done && manual_scrollto_done) { + var scrollEvents = JSON.stringify(scrollToLog); + // see the calls to manualScroller.scrollTo (below) + if (scrollEvents === '["config_general","config__advanced_project_options","config__build_triggers","config_general","config__build"]') { + done(); + } + } + }); + + // Lets mimic scrolling. This should trigger the + // scrollspy into activating different sections + // as the user scrolls down the page. + // See the test console output (gulp test) for a printout + // of the positions/offsets of each section. + // i.e. ... + // config_general: 100 + // config__advanced_project_options: 140 + // config__build_triggers: 180 + // config__build: 220 + + manualScroller.scrollTo(100); + manualScroller.scrollTo(140); + manualScroller.scrollTo(180); + + // Scrolling to the last section offset will not trigger its + // tab into being activated. Search for "### < 75% ADVANCED" + // in config-scrollspy.js + // So, "config__build" should not be added to 'scrollToLog' (see above). + // But, a manual click on the tab (vs a scroll) should result in it + // being added later. + manualScroller.scrollTo(220); // This will not trigger a + + // Scroll back to the General section ... that should work and log + // to 'scrollToLog' (see above). + manualScroller.scrollTo(100); + + // Now, manually activate the the "General" section i.e. "activate" it. + // This should result in 'config__build' being added to 'scrollToLog', + // which is not what happens if you try scrolling to this last section + // (see above). + tabbar.getSection('config__build').activate(); + }, 'widgets/config/freestyle-config-scrollspy.html'); + }); +}); + +function doSectionFunctionMocking(tabbar) { + function doMocks(section, viewportEntryOffset) { + section.getViewportEntryOffset = function() { + return viewportEntryOffset; + }; + } + + var mainOffset = 100; + var height = 40; + console.log('*** Mocking the position/offset of the form sections:'); + for (var i = 0; i < tabbar.sections.length; i++) { + var section = tabbar.sections[i]; + var offset = (mainOffset + (height * i)); + console.log('\t' + section.id + ': ' + offset); + doMocks(section, offset); + } +} + +function newManualScroller() { + var page = jsTest.requireSrcModule('util/page.js'); + var scrollListeners = []; + var curScrollToTop = 0; + + page.winScrollTop = function() { + return curScrollToTop; + }; + page.onWinScroll = function(listener) { + scrollListeners.push(listener); + }; + return { + scrollTo: function(position) { + curScrollToTop = position; + for (var i = 0; i < scrollListeners.length; i++) { + scrollListeners[i](); + } + } + }; +} \ No newline at end of file diff --git a/war/src/test/js/widgets/config/tabbar-spec.js b/war/src/test/js/widgets/config/tabbar-spec.js index 0bc5720f9c..7a7810e45a 100644 --- a/war/src/test/js/widgets/config/tabbar-spec.js +++ b/war/src/test/js/widgets/config/tabbar-spec.js @@ -185,6 +185,37 @@ describe("tabbar-spec tests", function () { }, 'widgets/config/freestyle-config-tabbed.html'); }); + it("- test getSibling ", function (done) { + jsTest.onPage(function() { + var configTabBarWidget = jsTest.requireSrcModule('widgets/config/tabbar'); + var configTabBar = configTabBarWidget.addTabsOnFirst(); + + // console.log('**** ' + configTabBar.sectionIds()); + // config_general,config__advanced_project_options,config__build_triggers,config__build + + var config_general = configTabBar.getSection('config_general'); + var config__advanced_project_options = configTabBar.getSection('config__advanced_project_options'); + var config__build_triggers = configTabBar.getSection('config__build_triggers'); + var config__build = configTabBar.getSection('config__build'); + + expect(config_general.getSibling(-1)).toBeUndefined(); + expect(config_general.getSibling(0)).toBe(config_general); + expect(config_general.getSibling(+1)).toBe(config__advanced_project_options); + expect(config_general.getSibling(+2)).toBe(config__build_triggers); + expect(config_general.getSibling(+3)).toBe(config__build); + expect(config_general.getSibling(+4)).toBeUndefined(); + + expect(config__advanced_project_options.getSibling(-2)).toBeUndefined(); + expect(config__advanced_project_options.getSibling(-1)).toBe(config_general); + expect(config__advanced_project_options.getSibling(0)).toBe(config__advanced_project_options); + expect(config__advanced_project_options.getSibling(+1)).toBe(config__build_triggers); + expect(config__advanced_project_options.getSibling(+2)).toBe(config__build); + expect(config__advanced_project_options.getSibling(+3)).toBeUndefined(); + + done(); + }, 'widgets/config/freestyle-config-tabbed.html'); + }); + function keydowns(text, onInput) { var jQD = require('jquery-detached'); var $ = jQD.getJQuery(); -- GitLab From 4adee7597aad7a338db8d3eb320575ae618a8c81 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 16 Mar 2016 10:34:05 -0400 Subject: [PATCH 0416/2380] [JENKINS-33467] Clarifying that CauseAction.getCauses is immutable and you should construct the action with the causes you want. --- core/src/main/java/hudson/model/CauseAction.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/hudson/model/CauseAction.java b/core/src/main/java/hudson/model/CauseAction.java index 09e5b03274..b065715db8 100644 --- a/core/src/main/java/hudson/model/CauseAction.java +++ b/core/src/main/java/hudson/model/CauseAction.java @@ -82,14 +82,21 @@ public class CauseAction implements FoldableAction, RunAction2 { public CauseAction(CauseAction ca) { addCauses(ca.getCauses()); } - + + /** + * Lists all causes of this build. + * Note that the current implementation does not preserve insertion order of duplicates. + * @return an immutable list; + * to create an action with multiple causes use either of the constructors that support this; + * to append causes retroactively to a build you must create a new {@link CauseAction} and replace the old + */ @Exported(visibility=2) public List getCauses() { List r = new ArrayList<>(); for (Map.Entry entry : causeBag.entrySet()) { r.addAll(Collections.nCopies(entry.getValue(), entry.getKey())); } - return r; + return Collections.unmodifiableList(r); } /** -- GitLab From f540e30b7332834a894eef9f3b3c2b9bc690831a Mon Sep 17 00:00:00 2001 From: tfennelly Date: Wed, 16 Mar 2016 15:06:23 +0000 Subject: [PATCH 0417/2380] Fix rowGroupContainer undefined bug --- war/src/main/js/widgets/config/model/ConfigSection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/war/src/main/js/widgets/config/model/ConfigSection.js b/war/src/main/js/widgets/config/model/ConfigSection.js index 823b7c2c65..6260d69ee8 100644 --- a/war/src/main/js/widgets/config/model/ConfigSection.js +++ b/war/src/main/js/widgets/config/model/ConfigSection.js @@ -223,7 +223,7 @@ ConfigSection.prototype.gatherRowGroups = function(rows) { } rowGroupContainer = newRowGroup; newRowGroup.findToggleWidget(row); - } else { + } else if (rowGroupContainer) { if (row.hasClass('row-group-end')) { rowGroupContainer.endRow = row; rowGroupContainer = rowGroupContainer.parentRowGroupContainer; // pop back off the "stack" -- GitLab From 08c205535d369bf6731cec58ccc16335771f2319 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 16 Mar 2016 11:55:01 -0400 Subject: [PATCH 0418/2380] Setting font-size does in fact work here. --- war/src/main/webapp/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/war/src/main/webapp/css/style.css b/war/src/main/webapp/css/style.css index 654836ad0a..2bef219c25 100644 --- a/war/src/main/webapp/css/style.css +++ b/war/src/main/webapp/css/style.css @@ -1690,7 +1690,7 @@ table.progress-bar.red td.progress-bar-done { /* ========================= logRecords.jelly ================== */ .logrecord-metadata { - // TODO does not work: font-size: 70%; + font-size: 70%; } .logrecord-metadata-new { -- GitLab From 57fced93596b1f8bd69f00f154430a11530393de Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 16 Mar 2016 12:22:54 -0400 Subject: [PATCH 0419/2380] [FIXED JENKINS-18032] Crumbs must be appended when using post=true requiresConfirmation=true. --- core/src/main/resources/lib/layout/breadcrumbs.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/resources/lib/layout/breadcrumbs.js b/core/src/main/resources/lib/layout/breadcrumbs.js index 90b019f620..adbebc8435 100644 --- a/core/src/main/resources/lib/layout/breadcrumbs.js +++ b/core/src/main/resources/lib/layout/breadcrumbs.js @@ -61,6 +61,9 @@ var breadcrumbs = (function() { var form = document.createElement('form'); form.setAttribute('method', cfg.post ? 'POST' : 'GET'); form.setAttribute('action', cfg.url); + if (cfg.post) { + crumb.appendToForm(form); + } document.body.appendChild(form); form.submit(); } -- GitLab From 50ee9f28518975eca39f213a3ccacec2bf930e0b Mon Sep 17 00:00:00 2001 From: tfennelly Date: Wed, 16 Mar 2016 17:19:09 +0000 Subject: [PATCH 0420/2380] Hide the finder widget for scrollspy As requested by DanielB and Tyler --- war/src/main/js/config-scrollspy.less | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/war/src/main/js/config-scrollspy.less b/war/src/main/js/config-scrollspy.less index 9e0db8f0b2..b6521a21c9 100644 --- a/war/src/main/js/config-scrollspy.less +++ b/war/src/main/js/config-scrollspy.less @@ -26,3 +26,11 @@ display: none; } } + +.jenkins-config-widgets { + .find-container { + .find { + display: none; + } + } +} \ No newline at end of file -- GitLab From 1d9ec2fb770dd41859abfba57b0d9add0710ecef Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Wed, 16 Mar 2016 18:42:29 +0100 Subject: [PATCH 0421/2380] Add a title for the confusing 'Use browser' option --- .../resources/jenkins/model/DownloadSettings/config.groovy | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/resources/jenkins/model/DownloadSettings/config.groovy b/core/src/main/resources/jenkins/model/DownloadSettings/config.groovy index 6b0751e563..f766281945 100644 --- a/core/src/main/resources/jenkins/model/DownloadSettings/config.groovy +++ b/core/src/main/resources/jenkins/model/DownloadSettings/config.groovy @@ -2,7 +2,8 @@ package jenkins.security.DownloadSettings def f = namespace(lib.FormTagLib) -// TODO avoid indentation somehow -f.entry(field: "useBrowser") { - f.checkbox(title: _("Use browser for metadata download")) +f.section(title:_("Plugin Manager")) { + f.entry(field: "useBrowser") { + f.checkbox(title: _("Use browser for metadata download")) + } } -- GitLab From b33c316f5d1acec16e2f0d1e8d401eec4e61189a Mon Sep 17 00:00:00 2001 From: tfennelly Date: Wed, 16 Mar 2016 17:52:47 +0000 Subject: [PATCH 0422/2380] Hide tabs for invisible sections --- war/src/main/js/widgets/config/model/ConfigSection.js | 5 +++++ war/src/main/js/widgets/config/tabbar.js | 3 +++ 2 files changed, 8 insertions(+) diff --git a/war/src/main/js/widgets/config/model/ConfigSection.js b/war/src/main/js/widgets/config/model/ConfigSection.js index 6260d69ee8..7a1819c0e1 100644 --- a/war/src/main/js/widgets/config/model/ConfigSection.js +++ b/war/src/main/js/widgets/config/model/ConfigSection.js @@ -27,6 +27,11 @@ ConfigSection.prototype.isTopLevelSection = function() { return (this.parentCMD.getSection(this.id) !== undefined); }; +ConfigSection.prototype.isVisible = function() { + var $ = jQD.getJQuery(); + return $(this.headerRow).is(':visible'); +}; + /** * Get the page offset (height) at which this section comes * into view. diff --git a/war/src/main/js/widgets/config/tabbar.js b/war/src/main/js/widgets/config/tabbar.js index faf4fbf209..01deeaf995 100644 --- a/war/src/main/js/widgets/config/tabbar.js +++ b/war/src/main/js/widgets/config/tabbar.js @@ -102,6 +102,9 @@ exports.addTabs = function(configTable) { var tab = newTab(section); tabBar.append(tab); section.setActivator(tab); + if (!section.isVisible()) { + tab.hide(); + } } var tabs = $('
'); -- GitLab From 003f1e5b0c5ee076aa29ccb9296abf12f32481f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 16 Mar 2016 19:23:39 +0100 Subject: [PATCH 0423/2380] Towards 1.651.1 --- cli/pom.xml | 2 +- core/pom.xml | 2 +- pom.xml | 4 ++-- test/pom.xml | 2 +- war/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/pom.xml b/cli/pom.xml index 0a4446727d..614f4fb4f8 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.main pom - 1.651 + 1.651.1-SNAPSHOT cli diff --git a/core/pom.xml b/core/pom.xml index 4852592cab..4351e7e87b 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 1.651 + 1.651.1-SNAPSHOT jenkins-core diff --git a/pom.xml b/pom.xml index c9af50a84a..103a9469df 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 1.651 + 1.651.1-SNAPSHOT pom Jenkins main module @@ -58,7 +58,7 @@ THE SOFTWARE. scm:git:git://github.com/jenkinsci/jenkins.git scm:git:ssh://git@github.com/jenkinsci/jenkins.git https://github.com/jenkinsci/jenkins - jenkins-1.651 + HEAD diff --git a/test/pom.xml b/test/pom.xml index 0430e344e5..c3627abc29 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 1.651 + 1.651.1-SNAPSHOT test diff --git a/war/pom.xml b/war/pom.xml index acaf96d382..c5b4c95719 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 1.651 + 1.651.1-SNAPSHOT jenkins-war -- GitLab From 24d88543270ecc9611a621e2711f554a559f3870 Mon Sep 17 00:00:00 2001 From: gusreiber Date: Tue, 15 Mar 2016 15:06:05 -0700 Subject: [PATCH 0424/2380] fixing for 'displayName'...also gulpfule in --- war/gulpfile.js | 6 + war/src/main/js/add-item.js | 293 ++++++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 war/src/main/js/add-item.js diff --git a/war/gulpfile.js b/war/gulpfile.js index cd1f43067a..6266fcbf49 100644 --- a/war/gulpfile.js +++ b/war/gulpfile.js @@ -30,3 +30,9 @@ builder.bundle('src/main/js/config-tabbar.js') .withExternalModuleMapping('jquery-detached', 'core-assets/jquery-detached:jquery2') .less('src/main/js/widgets/jenkins-widgets.less') .inDir('src/main/webapp/jsbundles'); + +builder.bundle('src/main/js/add-item.js') + .withExternalModuleMapping('jquery-detached', 'core-assets/jquery-detached:jquery2') + .less('src/main/js/widgets/jenkins-widgets.less') + .less('src/main/js/widgets/layout-mixins.less') + .inDir('src/main/webapp/jsbundles'); diff --git a/war/src/main/js/add-item.js b/war/src/main/js/add-item.js new file mode 100644 index 0000000000..91f46a7866 --- /dev/null +++ b/war/src/main/js/add-item.js @@ -0,0 +1,293 @@ +// Initialize all modules by requiring them. Also makes sure they get bundled (see gulpfile.js). +var $jq = require('jquery-detached').getJQuery(); + +var getItems = function(root){ + var $ = $jq; + var d = $.Deferred(); + $.get(root+'categories?depth=3').done( + function(data){ + d.resolve(data); + } + ); + return d.promise(); +}; + +var root = $jq('#jenkins').attr('data-root'); + +$jq.when(getItems(root)).done(function(data){ + $jq(function($) { + + ////////////////////////////// + // helpful reference DOM + + var jRoot = $('head').attr('data-rooturl'); + var defaultMinToShow = 2; + var $root = $jq('#main-panel'); + var $form = $root.find('form[name="createItem"]').addClass('jenkins-config new-view'); + var $newView = $jq('
') + .attr('name','createItem') + .attr('action','craetItem') + .prependTo($form); + var $tabs = $('
').appendTo($newView); + var $categories = $('
').appendTo($newView); + var sectionsToShow = []; + + + //////////////////////////////// + // scroll action...... + + function watchScroll(){ + var $window = $(window); + var $jenkTools = $('#breadcrumbBar'); + var winScoll = $window.scrollTop(); + var jenkToolOffset = $jenkTools.height() + $jenkTools.offset().top + 15; + + $tabs.find('.active').removeClass('active'); + $.each(data.categories,function(i,cat){ + var domId = '#j-add-item-type-'+cat.id; + var $cat = $(domId); + var catHeight = ($cat.length > 0)? + $cat.offset().top + $cat.outerHeight() - (jenkToolOffset + 100): + 0; + + if(winScoll < catHeight){ + var $thisTab = $tabs.find(['[href="',cleanHref(domId),'"]'].join('')); + resetActiveTab($thisTab); + return false; + } + }); + + if(winScoll > $('#page-head').height() - 5 ){ + $tabs.width($tabs.width()).css({ + 'position':'fixed', + 'top':($jenkTools.height() - 5 )+'px'}); + $categories.css({'margin-top':$tabs.outerHeight()+'px'}); + } + else{ + $tabs.add($categories).removeAttr('style'); + } + } + + ////////////////////////// + // helper functions... + + function addCopyOption(data){ + var $copy = $('#copy').closest('tr'); + if($copy.length === 0) return data; // exit if copy should not be added to page. Jelly page holds that logic. + var copyTitle = $copy.find('label').text(); + var copyDom = $copy.next().find('.setting-main').html(); + var copy = { + name:'Copy', + id:'copy', + minToShow:0, + weight:-999999999999, ///some really big number so it can be last... + items:[ + { + class:"copy", + description:copyDom, + name:copyTitle, + } + ] + }; + var newData = []; + + $.each(data,function(i,elem){ + if(elem.id !== "category-id-copy") + { newData.push(elem); } + }); + + newData.push(copy); + + + return newData; + } + + function sortItemsByOrder(itemTypes) { + function sortByOrder(a, b) { + var aOrder = a.weight; + var bOrder = b.weight; + return ( (aOrder < bOrder) ? -1 : ( (aOrder > bOrder) ? 1 : 0)); + } + return itemTypes.sort(sortByOrder); + } + + function hideAllTabsIfUnnecesary(sectionsToShow){ + if(sectionsToShow.length < 2){ + $tabs.find('.tab').hide(); + $categories.find('.category-header').hide(); + } + } + + function checkCatCount(elem){ + var minToShow = (typeof elem.minToShow === 'number')? elem.minToShow : 9999999; + return ($.isArray(elem.items) && elem.items.length >= Math.min(minToShow,defaultMinToShow)); + } + + function cleanClassName(className){ + return className.replace(/\./g,'_'); + } + + function cleanHref(id,reverse){ + if(reverse){ + var gotHash = (id.indexOf('#') === 0)? + '#j-add-item-type-'+ id.substring(1): + 'j-add-item-type-'+ id; + return gotHash; + } + else{ + return id.replace('j-add-item-type-',''); + } + } + + function cleanLayout(){ + // Do a little shimmy-hack to force legacy code to resize correctly and set tab state. + $('html,body').animate({scrollTop: 1}, 1); + $('html,body').animate({scrollTop: 0}, 10); + + setTimeout(fireBottomStickerAdjustEvent,410); + } + + function resetActiveTab($this){ + var $nav = $this.closest('.nav'); + $nav.find('.active').removeClass('active'); + $this.addClass('active'); + } + + ////////////////////////////////// + // Draw functions + + function drawName() { + var $name = $('
'); + + var $input = $('') + .change(function(){ + $form.find('input[name="name"]').val($(this).val()); + window.updateOk($form[0]); + }) + .appendTo($name); + + $tabs.prepend($name); + $input.focus(); + setTimeout(function(){$input.focus();},100); + } + + function drawTabs(data){ + $('body').addClass('add-item'); + setTimeout(function(){$('body').addClass('hide-side');},200); + $('#main-panel').addClass('container'); + var $navBox = $('