diff --git a/core/pom.xml b/core/pom.xml index b5913b102f382c954155f35a03662b6598359793..d9ffd0294c2ca50eb9ab42e50ac8f481d9100382 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -895,5 +895,16 @@ THE SOFTWARE. true + + all-tests + + + !test + + + + true + + diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index ac8652f92a28d571d4ff3aa02541e5435f076a93..5b764b83ec14427d9629e0cade554134f0718cc7 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -3279,7 +3279,7 @@ public final class FilePath implements SerializableOnlyOverRemoting { return act(new IsDescendant(potentialChildRelativePath)); } - private class IsDescendant extends SecureFileCallable { + private static class IsDescendant extends SecureFileCallable { private static final long serialVersionUID = 1L; private String potentialChildRelativePath; diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java index 1de67181edc20892b04036fd73543ae628786f36..d86b534fb1f14c91f33d72bb24756dba8f3590c3 100644 --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java @@ -360,7 +360,7 @@ public final class TcpSlaveAgentListener extends Thread { } // This is essentially just to be able to pass the parent thread into the callback, as it can't access it otherwise - private abstract class ConnectionHandlerFailureCallback { + private static abstract class ConnectionHandlerFailureCallback { private Thread parentThread; public ConnectionHandlerFailureCallback(Thread parentThread) { diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index f95b5275a6dfd3c5d707d6cbb1319b5aa20e8926..a10a8864e1b73a32b27c953be2bac043b9c6bf78 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -86,6 +86,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import javax.crypto.SecretKey; @@ -1205,6 +1206,65 @@ public class Util { return createFileSet(baseDir,includes,null); } + private static void tryToDeleteSymlink(@NonNull File symlink) { + if (!symlink.delete()) { + LogRecord record = new LogRecord(Level.FINE, "Failed to delete temporary symlink {0}"); + record.setParameters(new Object[]{symlink.getAbsolutePath()}); + LOGGER.log(record); + } + } + + private static void reportAtomicFailure(@NonNull Path pathForSymlink, @NonNull Exception ex) { + LogRecord record = new LogRecord(Level.FINE, "Failed to atomically create/replace symlink {0}"); + record.setParameters(new Object[]{pathForSymlink.toAbsolutePath().toString()}); + record.setThrown(ex); + LOGGER.log(record); + } + + /** + * Creates a symlink to targetPath at baseDir+symlinkPath. + * + * @param pathForSymlink + * The absolute path of the symlink itself as a path object. + * @param fileForSymlink + * The absolute path of the symlink itself as a file object. + * @param target + * The path that the symlink should point to. Usually relative to the directory of the symlink but may instead be an absolute path. + * @param symlinkPath + * Where to create a symlink in (relative to {@code baseDir}) + * + * Returns true on success + */ + @CheckReturnValue + private static boolean createSymlinkAtomic(@NonNull Path pathForSymlink, @NonNull File fileForSymlink, @NonNull Path target, @NonNull String symlinkPath) { + try { + File symlink = File.createTempFile("symtmp", null, fileForSymlink); + tryToDeleteSymlink(symlink); + Path tempSymlinkPath = symlink.toPath(); + Files.createSymbolicLink(tempSymlinkPath, target); + try { + Files.move(tempSymlinkPath, pathForSymlink, java.nio.file.StandardCopyOption.ATOMIC_MOVE); + return true; + } catch ( + UnsupportedOperationException | + SecurityException | + IOException ex) { + // If we couldn't perform an atomic move or the setup, we fall through to another approach + reportAtomicFailure(pathForSymlink, ex); + } + // If we didn't return after our atomic move, then we want to clean up our symlink + tryToDeleteSymlink(symlink); + } catch ( + SecurityException | + InvalidPathException | + UnsupportedOperationException | + IOException ex) { + // We couldn't perform an atomic move or the setup. + reportAtomicFailure(pathForSymlink, ex); + } + return false; + } + /** * Creates a symlink to targetPath at baseDir+symlinkPath. *

@@ -1219,16 +1279,21 @@ public class Util { */ public static void createSymlink(@NonNull File baseDir, @NonNull String targetPath, @NonNull String symlinkPath, @NonNull TaskListener listener) throws InterruptedException { + File fileForSymlink = new File(baseDir, symlinkPath); try { - Path path = fileToPath(new File(baseDir, symlinkPath)); + Path pathForSymlink = fileToPath(fileForSymlink); Path target = Paths.get(targetPath, MemoryReductionUtil.EMPTY_STRING_ARRAY); + if (createSymlinkAtomic(pathForSymlink, fileForSymlink, target, symlinkPath)) { + return; + } + final int maxNumberOfTries = 4; final int timeInMillis = 100; for (int tryNumber = 1; tryNumber <= maxNumberOfTries; tryNumber++) { - Files.deleteIfExists(path); + Files.deleteIfExists(pathForSymlink); try { - Files.createSymbolicLink(path, target); + Files.createSymbolicLink(pathForSymlink, target); break; } catch (FileAlreadyExistsException fileAlreadyExistsException) { if (tryNumber < maxNumberOfTries) { @@ -1249,7 +1314,7 @@ public class Util { return; } PrintStream log = listener.getLogger(); - log.printf("ln %s %s failed%n",targetPath, new File(baseDir, symlinkPath)); + log.printf("ln %s %s failed%n", targetPath, fileForSymlink); Functions.printStackTrace(e, log); } } diff --git a/core/src/main/java/hudson/cli/CLIAction.java b/core/src/main/java/hudson/cli/CLIAction.java index 102dbbc00b0de81ef424d6bd4e50cde403bac338..efca422c443764121595943c1c2e89926cd6242a 100644 --- a/core/src/main/java/hudson/cli/CLIAction.java +++ b/core/src/main/java/hudson/cli/CLIAction.java @@ -190,7 +190,7 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy { } } - class ServerSideImpl extends PlainCLIProtocol.ServerSide { + static class ServerSideImpl extends PlainCLIProtocol.ServerSide { private Thread runningThread; private boolean ready; private final List args = new ArrayList<>(); diff --git a/core/src/main/java/hudson/fsp/WorkspaceSnapshotSCM.java b/core/src/main/java/hudson/fsp/WorkspaceSnapshotSCM.java index e8c19e62a896c4e717b5a57869aa7ab7739130af..426ad5b7e15d3151d66ba84d0afab55cfe3820be 100644 --- a/core/src/main/java/hudson/fsp/WorkspaceSnapshotSCM.java +++ b/core/src/main/java/hudson/fsp/WorkspaceSnapshotSCM.java @@ -69,7 +69,7 @@ public class WorkspaceSnapshotSCM extends SCM { /** * {@link Exception} indicating that the resolution of the job/permalink failed. */ - private final class ResolvedFailedException extends Exception { + private static final class ResolvedFailedException extends Exception { private ResolvedFailedException(String message) { super(message); } diff --git a/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java b/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java index 1b46039987fa7e597e31595816addb33cb98ad99..92a75d850f1e8b04bae97d58b21651c7671554ac 100644 --- a/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java +++ b/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java @@ -127,7 +127,7 @@ public class WindowsInstallerLink extends ManagementLink { sendError("Installation is already complete",req,rsp); return; } - if(!DotNet.isInstalled(2,0)) { + if(!DotNet.isInstalled(4,0) && !DotNet.isInstalled(2,0)) { sendError(".NET Framework 2.0 or later is required for this feature",req,rsp); return; } diff --git a/core/src/main/java/hudson/util/HistoricalSecrets.java b/core/src/main/java/hudson/util/HistoricalSecrets.java index 7b1d0ee3bb8594947c42d1616d7bfe85a44d0d07..ed0bbf9d4de59e8c65e7ceafe34a98193a5aa445 100644 --- a/core/src/main/java/hudson/util/HistoricalSecrets.java +++ b/core/src/main/java/hudson/util/HistoricalSecrets.java @@ -81,9 +81,15 @@ public class HistoricalSecrets { */ @Deprecated /*package*/ static SecretKey getLegacyKey() throws GeneralSecurityException { - String secret = Secret.SECRET; - if(secret==null) return Jenkins.get().getSecretKeyAsAES128(); - return Util.toAes128Key(secret); + if (Secret.SECRET != null) { + return Util.toAes128Key(Secret.SECRET); + } + Jenkins j = Jenkins.getInstanceOrNull(); + if (j != null) { + return j.getSecretKeyAsAES128(); + } else { + return Util.toAes128Key("mock"); + } } static final String MAGIC = "::::MAGIC::::"; diff --git a/core/src/main/java/hudson/util/Secret.java b/core/src/main/java/hudson/util/Secret.java index 5a5b4ff9e90d9651d4c176be73c7ebd744c0ee01..bbda0d6d6a663953522900f8bb346b8b86137aca 100644 --- a/core/src/main/java/hudson/util/Secret.java +++ b/core/src/main/java/hudson/util/Secret.java @@ -31,7 +31,6 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import jenkins.util.SystemProperties; import java.util.Arrays; -import jenkins.model.Jenkins; import hudson.Util; import jenkins.security.CryptoConfidentialKey; import org.kohsuke.stapler.Stapler; @@ -289,8 +288,10 @@ public final class Secret implements Serializable { private static final String PROVIDER = SystemProperties.getString(Secret.class.getName()+".provider"); /** - * For testing only. Override the secret key so that we can test this class without {@link Jenkins}. + * For testing only. + * @deprecated Normally unnecessary. */ + @Deprecated /*package*/ static String SECRET = null; /** @@ -303,6 +304,9 @@ public final class Secret implements Serializable { static { Stapler.CONVERT_UTILS.register(new org.apache.commons.beanutils.Converter() { public Secret convert(Class type, Object value) { + if (value == null) { + return null; + } if (value instanceof Secret) { return (Secret) value; } diff --git a/core/src/main/java/hudson/util/XStream2.java b/core/src/main/java/hudson/util/XStream2.java index 4a7b1f3f27ccd3d279e026437d886d23bc8d0362..c86e82308ec0eec049ba64814cb6104ac1fdd6d9 100644 --- a/core/src/main/java/hudson/util/XStream2.java +++ b/core/src/main/java/hudson/util/XStream2.java @@ -327,7 +327,7 @@ public class XStream2 extends XStream { mapperInjectionPoint.setDelegate(m); } - final class MapperInjectionPoint extends MapperDelegate { + final static class MapperInjectionPoint extends MapperDelegate { public MapperInjectionPoint(Mapper wrapped) { super(wrapped); } diff --git a/core/src/main/java/hudson/util/jna/DotNet.java b/core/src/main/java/hudson/util/jna/DotNet.java index 4d820149ca0afb78100f23e5fed90166d1017dab..d3a453a46c35074fdf71b0807cd330a2db1de944 100644 --- a/core/src/main/java/hudson/util/jna/DotNet.java +++ b/core/src/main/java/hudson/util/jna/DotNet.java @@ -30,8 +30,6 @@ import org.jinterop.winreg.JIPolicyHandle; import org.jinterop.winreg.JIWinRegFactory; import java.net.UnknownHostException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * .NET related code. @@ -39,74 +37,233 @@ import java.util.regex.Pattern; * @author Kohsuke Kawaguchi */ public class DotNet { + private static final String PATH10 = "SOFTWARE\\Microsoft\\.NETFramework\\Policy\\v1.0\\3705"; + private static final String PATH11 = "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v1.1.4322"; + private static final String PATH20 = "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v2.0.50727"; + private static final String PATH30 = "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v3.0\\Setup"; + private static final String PATH35 = "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v3.5"; + private static final String PATH4 = "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full"; + + private static final String VALUE_INSTALL = "Install"; + private static final String VALUE_INSTALL_SUCCESS = "InstallSuccess"; + private static final String VALUE_RELEASE = "Release"; + /** - * Returns true if the .NET framework of the given version (or greater) is installed. + * Returns true if the .NET framework of a compatible version is installed. */ public static boolean isInstalled(int major, int minor) { try { - // see http://support.microsoft.com/?scid=kb;en-us;315291 for the basic algorithm - // observation in my registry shows that the actual key name can be things like "v2.0 SP1" - // or "v2.0.50727", so the regexp is written to accommodate this. - RegistryKey key = RegistryKey.LOCAL_MACHINE.openReadonly("SOFTWARE\\Microsoft\\.NETFramework"); - try { - for( String keyName : key.getSubKeys() ) { - if (matches(keyName, major, minor)) - return true; - } + if (major == 4 && minor >= 5) { + return isV45PlusInstalled(minor); + } else if (major == 4 && minor == 0) { + return isV40Installed(); + } else if (major == 3 && minor == 5) { + return isV35Installed(); + } else if (major == 3 && minor == 0) { + return isV35Installed() || isV30Installed(); + } else if (major == 2 && minor == 0) { + return isV35Installed() || isV30Installed() || isV20Installed(); + } else if (major == 1 && minor == 1) { + return isV11Installed(); + } else if (major == 1 && minor == 0) { + return isV11Installed() || isV10Installed(); + } else { return false; - } finally { - key.dispose(); } } catch (JnaException e) { - if(e.getErrorCode()==2) // thrown when openReadonly fails because the key doesn't exist. + if (e.getErrorCode() == 2) { + // thrown when openReadonly fails because the key doesn't exist. return false; + } throw e; } } + private static boolean isV45PlusInstalled(int minor) { + try (RegistryKey key = RegistryKey.LOCAL_MACHINE.openReadonly(PATH4)) { + return key.getIntValue(VALUE_RELEASE) >= GetV45PlusMinRelease(minor); + } + } + + private static boolean isV40Installed() { + try (RegistryKey key = RegistryKey.LOCAL_MACHINE.openReadonly(PATH4)) { + return key.getIntValue(VALUE_INSTALL) == 1; + } + } + + private static boolean isV35Installed() { + try (RegistryKey key = RegistryKey.LOCAL_MACHINE.openReadonly(PATH35)) { + return key.getIntValue(VALUE_INSTALL) == 1; + } + } + + private static boolean isV30Installed() { + try (RegistryKey key = RegistryKey.LOCAL_MACHINE.openReadonly(PATH30)) { + return key.getIntValue(VALUE_INSTALL_SUCCESS) == 1; + } + } + + private static boolean isV20Installed() { + try (RegistryKey key = RegistryKey.LOCAL_MACHINE.openReadonly(PATH20)) { + return key.getIntValue(VALUE_INSTALL) == 1; + } + } + + private static boolean isV11Installed() { + try (RegistryKey key = RegistryKey.LOCAL_MACHINE.openReadonly(PATH11)) { + return key.getIntValue(VALUE_INSTALL) == 1; + } + } + + private static boolean isV10Installed() { + try (RegistryKey key = RegistryKey.LOCAL_MACHINE.openReadonly(PATH10)) { + return key.getStringValue(VALUE_INSTALL) == "1"; + } + } + /** - * Returns true if the .NET framework of the given version (or grater) is installed - * on a remote machine. + * Returns true if the .NET framework of a compatible version is installed on a remote machine. */ public static boolean isInstalled(int major, int minor, String targetMachine, IJIAuthInfo session) throws JIException, UnknownHostException { - IJIWinReg registry = JIWinRegFactory.getSingleTon().getWinreg(session,targetMachine,true); - JIPolicyHandle hklm=null; - JIPolicyHandle key=null; - + IJIWinReg registry = JIWinRegFactory.getSingleTon().getWinreg(session, targetMachine, true); + JIPolicyHandle hklm = null; try { hklm = registry.winreg_OpenHKLM(); - key = registry.winreg_OpenKey(hklm,"SOFTWARE\\Microsoft\\.NETFramework", IJIWinReg.KEY_READ ); - - for( int i=0; ; i++ ) { - String keyName = registry.winreg_EnumKey(key,i)[0]; - if(matches(keyName,major,minor)) - return true; + if (major == 4 && minor >= 5) { + return isV45PlusInstalled(minor, registry, hklm); + } else if (major == 4 && minor == 0) { + return isV40Installed(registry, hklm); + } else if (major == 3 && minor == 5) { + return isV35Installed(registry, hklm); + } else if (major == 3 && minor == 0) { + return isV35Installed(registry, hklm) || isV30Installed(registry, hklm); + } else if (major == 2 && minor == 0) { + return isV35Installed(registry, hklm) || isV30Installed(registry, hklm) || isV20Installed(registry, hklm); + } else if (major == 1 && minor == 1) { + return isV11Installed(registry, hklm); + } else if (major == 1 && minor == 0) { + return isV11Installed(registry, hklm) || isV10Installed(registry, hklm); + } else { + return false; } } catch (JIException e) { - if(e.getErrorCode()==2) - return false; // not found + if (e.getErrorCode() == 2) { + // not found + return false; + } throw e; } finally { - if(hklm!=null) + if (hklm != null) { registry.winreg_CloseKey(hklm); - if(key!=null) - registry.winreg_CloseKey(key); + } registry.closeConnection(); } } - private static boolean matches(String keyName, int major, int minor) { - Matcher m = VERSION_PATTERN.matcher(keyName); - if(m.matches()) { - int mj = Integer.parseInt(m.group(1)); - if(mj>=major) { - int mn = Integer.parseInt(m.group(2)); - if(mn>=minor) - return true; + private static boolean isV45PlusInstalled(int minor, IJIWinReg registry, JIPolicyHandle hklm) throws JIException { + JIPolicyHandle key = null; + try { + key = registry.winreg_OpenKey(hklm, PATH4, IJIWinReg.KEY_READ); + return GetIntValue(registry, key, VALUE_RELEASE) >= GetV45PlusMinRelease(minor); + } finally { + if (key != null) { + registry.winreg_CloseKey(key); + } + } + } + + private static boolean isV40Installed(IJIWinReg registry, JIPolicyHandle hklm) throws JIException { + JIPolicyHandle key = null; + try { + key = registry.winreg_OpenKey(hklm, PATH4, IJIWinReg.KEY_READ); + return GetIntValue(registry, key, VALUE_INSTALL) == 1; + } finally { + if (key != null) { + registry.winreg_CloseKey(key); + } + } + } + + private static boolean isV35Installed(IJIWinReg registry, JIPolicyHandle hklm) throws JIException { + JIPolicyHandle key = null; + try { + key = registry.winreg_OpenKey(hklm, PATH35, IJIWinReg.KEY_READ); + return GetIntValue(registry, key, VALUE_INSTALL) == 1; + } finally { + if (key != null) { + registry.winreg_CloseKey(key); + } + } + } + + private static boolean isV30Installed(IJIWinReg registry, JIPolicyHandle hklm) throws JIException { + JIPolicyHandle key = null; + try { + key = registry.winreg_OpenKey(hklm, PATH30, IJIWinReg.KEY_READ); + return GetIntValue(registry, key, VALUE_INSTALL_SUCCESS) == 1; + } finally { + if (key != null) { + registry.winreg_CloseKey(key); + } + } + } + + private static boolean isV20Installed(IJIWinReg registry, JIPolicyHandle hklm) throws JIException { + JIPolicyHandle key = null; + try { + key = registry.winreg_OpenKey(hklm, PATH20, IJIWinReg.KEY_READ); + return GetIntValue(registry, key, VALUE_INSTALL) == 1; + } finally { + if (key != null) { + registry.winreg_CloseKey(key); } } - return false; } - private static final Pattern VERSION_PATTERN = Pattern.compile("v(\\d+)\\.(\\d+).*"); + private static boolean isV11Installed(IJIWinReg registry, JIPolicyHandle hklm) throws JIException { + JIPolicyHandle key = null; + try { + key = registry.winreg_OpenKey(hklm, PATH11, IJIWinReg.KEY_READ); + return GetIntValue(registry, key, VALUE_INSTALL) == 1; + } finally { + if (key != null) { + registry.winreg_CloseKey(key); + } + } + } + + private static boolean isV10Installed(IJIWinReg registry, JIPolicyHandle hklm) throws JIException { + JIPolicyHandle key = null; + try { + key = registry.winreg_OpenKey(hklm, PATH10, IJIWinReg.KEY_READ); + return GetStringValue(registry, key, VALUE_INSTALL) == "1"; + } finally { + if (key != null) { + registry.winreg_CloseKey(key); + } + } + } + + private static int GetIntValue(IJIWinReg registry, JIPolicyHandle key, String name) throws JIException { + return RegistryKey.convertBufferToInt((byte[])registry.winreg_QueryValue(key, name, Integer.BYTES)[1]); + } + + private static String GetStringValue(IJIWinReg registry, JIPolicyHandle key, String name) throws JIException { + return RegistryKey.convertBufferToString((byte[])registry.winreg_QueryValue(key, name, Character.BYTES * 2)[1]); + } + + private static int GetV45PlusMinRelease(int minor) { + switch (minor) { + case 5: + return 378389; + case 6: + return 393295; + case 7: + return 460798; + case 8: + return 528040; + default: + return Integer.MAX_VALUE; + } + } } diff --git a/core/src/main/java/hudson/util/jna/RegistryKey.java b/core/src/main/java/hudson/util/jna/RegistryKey.java index 81ed3f06b3021e6f6a6c3e6b04ce257643854a48..554a1263ec8f731ff2ac207973bee18028bb6cff 100644 --- a/core/src/main/java/hudson/util/jna/RegistryKey.java +++ b/core/src/main/java/hudson/util/jna/RegistryKey.java @@ -27,7 +27,7 @@ import java.util.TreeSet; * * @author Kohsuke Kawaguchi */ -public class RegistryKey { +public class RegistryKey implements AutoCloseable { /** * 32bit Windows key value. */ @@ -64,7 +64,7 @@ public class RegistryKey { * @throws java.io.UnsupportedEncodingException on error * @return String */ - private static String convertBufferToString(byte[] buf) { + static String convertBufferToString(byte[] buf) { return new String(buf, 0, buf.length - 2, StandardCharsets.UTF_16LE); } @@ -74,7 +74,7 @@ public class RegistryKey { * @param buf buffer * @return int */ - private static int convertBufferToInt(byte[] buf) { + static int convertBufferToInt(byte[] buf) { return ((buf[0] & 0xff) + ((buf[1] & 0xff) << 8) + ((buf[2] & 0xff) << 16) + ((buf[3] & 0xff) << 24)); } @@ -287,6 +287,10 @@ public class RegistryKey { handle = 0; } + public void close() { + dispose(); + } + // // Root keys // diff --git a/core/src/main/java/jenkins/FilePathFilterAggregator.java b/core/src/main/java/jenkins/FilePathFilterAggregator.java index 407d8da2cbbb01f15466eae88b9e10371cf36bff..f338d25888e797aa3608428ddf2a5ff8f50b8459 100644 --- a/core/src/main/java/jenkins/FilePathFilterAggregator.java +++ b/core/src/main/java/jenkins/FilePathFilterAggregator.java @@ -20,7 +20,7 @@ import java.util.concurrent.CopyOnWriteArrayList; class FilePathFilterAggregator extends FilePathFilter { private final CopyOnWriteArrayList all = new CopyOnWriteArrayList<>(); - private class Entry implements Comparable { + private static class Entry implements Comparable { final FilePathFilter filter; final double ordinal; diff --git a/core/src/main/java/jenkins/model/GlobalBuildDiscarderConfiguration.java b/core/src/main/java/jenkins/model/GlobalBuildDiscarderConfiguration.java index 9b0ca5aaf8add1f5e18b4db32bdd586fb9217ab0..8a1d5cf8c85520807d6d95eb18beaccd2da4c8e2 100644 --- a/core/src/main/java/jenkins/model/GlobalBuildDiscarderConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalBuildDiscarderConfiguration.java @@ -48,6 +48,10 @@ public class GlobalBuildDiscarderConfiguration extends GlobalConfiguration { return ExtensionList.lookupSingleton(GlobalBuildDiscarderConfiguration.class); } + public GlobalBuildDiscarderConfiguration() { + load(); + } + private final DescribableList configuredBuildDiscarders = new DescribableList<>(this, Collections.singletonList(new JobGlobalBuildDiscarderStrategy())); diff --git a/core/src/main/java/jenkins/security/ConfidentialStore.java b/core/src/main/java/jenkins/security/ConfidentialStore.java index 374f5ca34409f2d2a10da569ba15ce78eaead208..b3194e0611d07fa4f49adf47d38241594b2e9f48 100644 --- a/core/src/main/java/jenkins/security/ConfidentialStore.java +++ b/core/src/main/java/jenkins/security/ConfidentialStore.java @@ -10,10 +10,14 @@ import org.kohsuke.MetaInfServices; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Iterator; +import java.util.Map; +import java.util.Random; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -51,6 +55,9 @@ public abstract class ConfidentialStore { */ protected abstract @CheckForNull byte[] load(ConfidentialKey key) throws IOException; + // TODO consider promoting to public, and offering a default implementation of randomBytes (via the usual Util.isOverridden binary compat trick) + abstract SecureRandom secureRandom(); + /** * Works like {@link SecureRandom#nextBytes(byte[])}. * @@ -64,7 +71,10 @@ public abstract class ConfidentialStore { public static @NonNull ConfidentialStore get() { if (TEST!=null) return TEST.get(); - Jenkins j = Jenkins.get(); + Jenkins j = Jenkins.getInstanceOrNull(); + if (j == null) { + return Mock.INSTANCE; + } Lookup lookup = j.lookup; ConfidentialStore cs = lookup.get(ConfidentialStore.class); if (cs==null) { @@ -91,10 +101,60 @@ public abstract class ConfidentialStore { return cs; } - /** - * Testing only. Used for testing {@link ConfidentialKey} without {@link Jenkins} + /** + * @deprecated No longer needed. */ + @Deprecated /*package*/ static ThreadLocal TEST = null; + static final class Mock extends ConfidentialStore { + + static final Mock INSTANCE = new Mock(); + + private final SecureRandom rand; + + private final Map data = new ConcurrentHashMap<>(); + + Mock() { + // Use a predictable seed to make tests more reproducible. + try { + rand = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException x) { + throw new AssertionError("https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SecureRandom", x); + } + rand.setSeed(new byte[] {1, 2, 3, 4}); + } + + void clear() { + data.clear(); + } + + @Override + protected void store(ConfidentialKey key, byte[] payload) throws IOException { + LOGGER.fine(() -> "storing " + key.getId() + " " + hudson.Util.getDigestOf(hudson.Util.toHexString(payload))); + data.put(key.getId(), payload); + } + + @Override + protected byte[] load(ConfidentialKey key) throws IOException { + byte[] payload = data.get(key.getId()); + LOGGER.fine(() -> "loading " + key.getId() + " " + (payload != null ? hudson.Util.getDigestOf(hudson.Util.toHexString(payload)) : "null")); + return payload; + } + + @Override + SecureRandom secureRandom() { + return rand; + } + + @Override + public byte[] randomBytes(int size) { + byte[] random = new byte[size]; + rand.nextBytes(random); + return random; + } + + } + private static final Logger LOGGER = Logger.getLogger(ConfidentialStore.class.getName()); } diff --git a/core/src/main/java/jenkins/security/DefaultConfidentialStore.java b/core/src/main/java/jenkins/security/DefaultConfidentialStore.java index 5a16cefc707488e33047f84a59c09b08b01f29d5..4d18357b32b804138a2f2b9125e0505005516454 100644 --- a/core/src/main/java/jenkins/security/DefaultConfidentialStore.java +++ b/core/src/main/java/jenkins/security/DefaultConfidentialStore.java @@ -140,6 +140,11 @@ public class DefaultConfidentialStore extends ConfidentialStore { return new File(rootDir, key.getId()); } + @Override + SecureRandom secureRandom() { + return sr; + } + public byte[] randomBytes(int size) { byte[] random = new byte[size]; sr.nextBytes(random); diff --git a/core/src/main/java/jenkins/security/RSAConfidentialKey.java b/core/src/main/java/jenkins/security/RSAConfidentialKey.java index fe30c622de0608ece1164a31cafaddb30b92d579..8c5f64b317488ca552cb8d6da72ebf4c2573f59b 100644 --- a/core/src/main/java/jenkins/security/RSAConfidentialKey.java +++ b/core/src/main/java/jenkins/security/RSAConfidentialKey.java @@ -30,7 +30,6 @@ import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; -import java.security.SecureRandom; import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; @@ -79,7 +78,7 @@ public abstract class RSAConfidentialKey extends ConfidentialKey { byte[] payload = load(); if (payload == null) { KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); - gen.initialize(2048, new SecureRandom()); // going beyond 2048 requires crypto extension + gen.initialize(2048, cs.secureRandom()); // going beyond 2048 requires crypto extension KeyPair keys = gen.generateKeyPair(); priv = (RSAPrivateKey) keys.getPrivate(); pub = (RSAPublicKey) keys.getPublic(); diff --git a/core/src/main/java/jenkins/util/AntClassLoader.java b/core/src/main/java/jenkins/util/AntClassLoader.java index e10779dfb4ebce7a8a882c1e41684de669aabb1a..7843abb69ae9f5953395c723f4404b536a854710 100644 --- a/core/src/main/java/jenkins/util/AntClassLoader.java +++ b/core/src/main/java/jenkins/util/AntClassLoader.java @@ -33,6 +33,7 @@ import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -766,6 +767,30 @@ public class AntClassLoader extends ClassLoader implements SubBuildListener { return parent == null ? super.getResourceAsStream(name) : parent.getResourceAsStream(name); } + @FunctionalInterface + private interface Converter { + T convert(@NonNull JarFile jarFile, @NonNull JarEntry entry) throws IOException; + } + + @CheckForNull + private T storeAndConvert(JarFile jarFile, File file, String resourceName, Converter converter) throws IOException { + if (jarFile == null) { + if (file.exists()) { + jarFile = new JarFile(file); + jarFiles.put(file, jarFile); + } else { + return null; + } + //to eliminate a race condition, retrieve the entry + //that is in the hash table under that filename + jarFile = (JarFile) jarFiles.get(file); + } + JarEntry entry = jarFile.getJarEntry(resourceName); + if (entry == null) + return null; + return converter.convert(jarFile, entry); + } + /** * Returns an inputstream to a given resource in the given file which may * either be a directory or a zip file. @@ -787,21 +812,7 @@ public class AntClassLoader extends ClassLoader implements SubBuildListener { return Files.newInputStream(resource.toPath()); } } else { - if (jarFile == null) { - if (file.exists()) { - jarFile = new JarFile(file); - jarFiles.put(file, jarFile); - } else { - return null; - } - //to eliminate a race condition, retrieve the entry - //that is in the hash table under that filename - jarFile = (JarFile) jarFiles.get(file); - } - JarEntry entry = jarFile.getJarEntry(resourceName); - if (entry != null) { - return jarFile.getInputStream(entry); - } + return storeAndConvert(jarFile, file, resourceName, JarFile::getInputStream); } } catch (Exception e) { log("Ignoring Exception " + e.getClass().getName() + ": " + e.getMessage() @@ -1019,24 +1030,13 @@ public class AntClassLoader extends ClassLoader implements SubBuildListener { } } } else { - if (jarFile == null) { - if (file.exists()) { - jarFile = new JarFile(file); - jarFiles.put(file, jarFile); - } else { - return null; - } - // potential race-condition - jarFile = (JarFile) jarFiles.get(file); - } - JarEntry entry = jarFile.getJarEntry(resourceName); - if (entry != null) { + return storeAndConvert(jarFile, file, resourceName, (jar, entry) -> { try { return new URL("jar:" + FILE_UTILS.getFileURL(file) + "!/" + entry); } catch (MalformedURLException ex) { return null; } - } + }); } } catch (Exception e) { String msg = "Unable to obtain resource from " + file + ": "; diff --git a/core/src/main/resources/hudson/ProxyConfiguration/config.groovy b/core/src/main/resources/hudson/ProxyConfiguration/config.groovy index 0be2a539b6887e5e3df4bcd8322db5951cc8cb56..c6f2603e94e05f8e2940c4a65cf0f5c7f34031eb 100644 --- a/core/src/main/resources/hudson/ProxyConfiguration/config.groovy +++ b/core/src/main/resources/hudson/ProxyConfiguration/config.groovy @@ -22,5 +22,5 @@ f.advanced(){ f.textbox() } f.validateButton(title:_("Validate Proxy"), - method:"validateProxy", with:"testUrl,name,port,userName,password,noProxyHost") + method:"validateProxy", with:"testUrl,name,port,userName,secretPassword,noProxyHost") } diff --git a/core/src/test/java/hudson/BulkChangeTest.java b/core/src/test/java/hudson/BulkChangeTest.java index 7e93dc4e097da6708374ce4fc6a3675c59fca928..e738e9259f774902dc4bbd2a079514bc01866878 100644 --- a/core/src/test/java/hudson/BulkChangeTest.java +++ b/core/src/test/java/hudson/BulkChangeTest.java @@ -37,7 +37,7 @@ import java.io.IOException; */ public class BulkChangeTest { - private class Point implements Saveable { + private static class Point implements Saveable { /** * Don't actually do any save, but just remember how many the actual I/O would have happened. */ diff --git a/core/src/test/java/hudson/model/AbstractItemTest.java b/core/src/test/java/hudson/model/AbstractItemTest.java index b5dcdf41d2c070a84963846d9b3f66bdf5285367..4ee09dfad0a50da8d163d0d369ffbca41734aee7 100644 --- a/core/src/test/java/hudson/model/AbstractItemTest.java +++ b/core/src/test/java/hudson/model/AbstractItemTest.java @@ -16,7 +16,7 @@ import org.jvnet.hudson.test.Issue; @SuppressWarnings("unchecked") public class AbstractItemTest { - private class StubAbstractItem extends AbstractItem { + private static class StubAbstractItem extends AbstractItem { protected StubAbstractItem() { // sending in null as parent as I don't care for my current tests @@ -91,7 +91,7 @@ public class AbstractItemTest { assertEquals(displayName, i.getDisplayName()); } - private class NameNotEditableItem extends AbstractItem { + private static class NameNotEditableItem extends AbstractItem { protected NameNotEditableItem(ItemGroup parent, String name){ super(parent, name); diff --git a/core/src/test/java/hudson/model/EnvironmentContributingActionTest.java b/core/src/test/java/hudson/model/EnvironmentContributingActionTest.java index 8bf6c69b30b72eef1795d6c738ae3b4183b00057..aee451908a112e023b6e471a83297ace359d1049 100644 --- a/core/src/test/java/hudson/model/EnvironmentContributingActionTest.java +++ b/core/src/test/java/hudson/model/EnvironmentContributingActionTest.java @@ -11,7 +11,7 @@ import static org.mockito.Mockito.when; public class EnvironmentContributingActionTest { - class OverrideRun extends InvisibleAction implements EnvironmentContributingAction { + static class OverrideRun extends InvisibleAction implements EnvironmentContributingAction { private boolean wasCalled = false; @Override @@ -24,7 +24,7 @@ public class EnvironmentContributingActionTest { } } - class OverrideAbstractBuild extends InvisibleAction implements EnvironmentContributingAction { + static class OverrideAbstractBuild extends InvisibleAction implements EnvironmentContributingAction { private boolean wasCalled = false; @Override @@ -38,7 +38,7 @@ public class EnvironmentContributingActionTest { } } - class OverrideBoth extends InvisibleAction implements EnvironmentContributingAction { + static class OverrideBoth extends InvisibleAction implements EnvironmentContributingAction { private boolean wasCalledAbstractBuild = false; private boolean wasCalledRun = false; diff --git a/core/src/test/java/hudson/model/FingerprintCleanupThreadTest.java b/core/src/test/java/hudson/model/FingerprintCleanupThreadTest.java index f26ac7734466326db189935af256e03e3c4cf549..eaa1510b7d53bb9a65197487166d5242586cac24 100644 --- a/core/src/test/java/hudson/model/FingerprintCleanupThreadTest.java +++ b/core/src/test/java/hudson/model/FingerprintCleanupThreadTest.java @@ -107,7 +107,7 @@ public class FingerprintCleanupThreadTest { tempDirectory.toFile().deleteOnExit(); } - private class TestTaskListener implements TaskListener { + private static class TestTaskListener implements TaskListener { private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); private PrintStream logStream = new PrintStream(outputStream); @@ -126,7 +126,6 @@ public class FingerprintCleanupThreadTest { public TestFingerprintCleanupThread(Fingerprint fingerprintToLoad) throws IOException { this.fingerprintToLoad = fingerprintToLoad; - return; } @Override @@ -146,7 +145,7 @@ public class FingerprintCleanupThreadTest { } - private class TestFingerprint extends Fingerprint { + private static class TestFingerprint extends Fingerprint { private boolean isAlive = true; diff --git a/core/src/test/java/hudson/model/LoadStatisticsTest.java b/core/src/test/java/hudson/model/LoadStatisticsTest.java index 246bdb3f1d40d2d6c23a07e3d562fb101236be3f..d49fad26099f9dfa8be0f84f1aad603cfdabec6c 100644 --- a/core/src/test/java/hudson/model/LoadStatisticsTest.java +++ b/core/src/test/java/hudson/model/LoadStatisticsTest.java @@ -101,7 +101,7 @@ public class LoadStatisticsTest { assertThat(LoadStatistics.isModern(LoadStatistics.class), is(false)); } - private class Modern extends LoadStatistics { + private static class Modern extends LoadStatistics { protected Modern(int initialOnlineExecutors, int initialBusyExecutors) { super(initialOnlineExecutors, initialBusyExecutors); diff --git a/core/src/test/java/hudson/util/CyclicGraphDetectorTest.java b/core/src/test/java/hudson/util/CyclicGraphDetectorTest.java index d99640068bedb7132bf8cfa05053c383f198e5cd..42c68e0e27280ab1882be330cddfbbc3538d6eb8 100644 --- a/core/src/test/java/hudson/util/CyclicGraphDetectorTest.java +++ b/core/src/test/java/hudson/util/CyclicGraphDetectorTest.java @@ -16,7 +16,7 @@ import java.util.Set; */ public class CyclicGraphDetectorTest { - private class Edge { + private static class Edge { String src,dst; private Edge(String src, String dst) { @@ -25,7 +25,7 @@ public class CyclicGraphDetectorTest { } } - private class Graph extends ArrayList { + private static class Graph extends ArrayList { Graph e(String src, String dst) { add(new Edge(src,dst)); return this; diff --git a/core/src/test/java/hudson/util/IsOverriddenTest.java b/core/src/test/java/hudson/util/IsOverriddenTest.java index 36f45c3de5d3daab883560f7db80d58fad94ad28..0048a69553f98132bd0e47369e15394b28505fdb 100644 --- a/core/src/test/java/hudson/util/IsOverriddenTest.java +++ b/core/src/test/java/hudson/util/IsOverriddenTest.java @@ -62,7 +62,7 @@ public class IsOverriddenTest { Util.isOverridden(Base.class, Intermediate.class, "aPrivateMethod"); } - public abstract class Base { + public static abstract class Base { protected abstract void method(); private void aPrivateMethod() {} public void setX(T t) {} diff --git a/core/src/test/java/hudson/util/MockSecretRule.java b/core/src/test/java/hudson/util/MockSecretRule.java deleted file mode 100644 index f3c11ea381de23b09741d1ab0be099615926c22b..0000000000000000000000000000000000000000 --- a/core/src/test/java/hudson/util/MockSecretRule.java +++ /dev/null @@ -1,33 +0,0 @@ -package hudson.util; - -import hudson.Util; -import org.junit.rules.ExternalResource; - -import java.security.SecureRandom; - -/** - * JUnit rule that cleans that sets a temporary {@link Secret#SECRET} value. - * - * @author Kohsuke Kawaguchi - */ -public class MockSecretRule extends ExternalResource { - - private String value; - - @Override - protected void before() throws Throwable { - byte[] random = new byte[32]; - sr.nextBytes(random); - value = Util.toHexString(random); - Secret.SECRET = value; - } - - @Override - protected void after() { - if (!Secret.SECRET.equals(value)) - throw new IllegalStateException("Someone tinkered with Secret.SECRET"); - Secret.SECRET = null; - } - - private static final SecureRandom sr = new SecureRandom(); -} diff --git a/core/src/test/java/hudson/util/SecretRewriterTest.java b/core/src/test/java/hudson/util/SecretRewriterTest.java index 881f278518f7db62adb52800513e9d7d44694500..bb7e6adf68faa56bdd20ab24f80c85447fbdf036 100644 --- a/core/src/test/java/hudson/util/SecretRewriterTest.java +++ b/core/src/test/java/hudson/util/SecretRewriterTest.java @@ -22,9 +22,6 @@ import org.junit.rules.TemporaryFolder; public class SecretRewriterTest { - @Rule - public MockSecretRule mockSecretRule = new MockSecretRule(); - @Rule public ConfidentialStoreRule confidentialStoreRule = new ConfidentialStoreRule(); diff --git a/core/src/test/java/hudson/util/SecretTest.java b/core/src/test/java/hudson/util/SecretTest.java index 32b8d86cebec50a6a0c4ef65fd3243833351d491..19fec48e70688f0111f7c598ba98be921d259c73 100644 --- a/core/src/test/java/hudson/util/SecretTest.java +++ b/core/src/test/java/hudson/util/SecretTest.java @@ -43,9 +43,6 @@ public class SecretTest { @Rule public ConfidentialStoreRule confidentialStore = new ConfidentialStoreRule(); - @Rule - public MockSecretRule mockSecretRule = new MockSecretRule(); - private static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?"); @Test diff --git a/core/src/test/java/hudson/util/io/TarArchiverTest.java b/core/src/test/java/hudson/util/io/TarArchiverTest.java index 8900f83193693adba9f8211f58f4c2fa038a3773..0983d9a1996f305c789cc7d2027b438a9d17d733 100644 --- a/core/src/test/java/hudson/util/io/TarArchiverTest.java +++ b/core/src/test/java/hudson/util/io/TarArchiverTest.java @@ -136,7 +136,7 @@ public class TarArchiverTest { t1.join(); } - private class GrowingFileRunnable implements Runnable { + private static class GrowingFileRunnable implements Runnable { private boolean finish = false; private Exception ex = null; private File file; diff --git a/core/src/test/java/jenkins/security/ConfidentialStoreRule.java b/core/src/test/java/jenkins/security/ConfidentialStoreRule.java index e060cfb3f6c2e92a228eddc1360875dc2fd3225f..5fbc242a7f8ff6009e8765e553fb5d38b4afa8ad 100644 --- a/core/src/test/java/jenkins/security/ConfidentialStoreRule.java +++ b/core/src/test/java/jenkins/security/ConfidentialStoreRule.java @@ -2,28 +2,15 @@ package jenkins.security; import org.junit.rules.ExternalResource; -import org.junit.rules.TemporaryFolder; /** - * Test rule that injects a temporary {@link DefaultConfidentialStore} - * @author Kohsuke Kawaguchi + * Test rule that makes {@link ConfidentialStore#get} be reset for each test. */ public class ConfidentialStoreRule extends ExternalResource { - private final TemporaryFolder tmp = new TemporaryFolder(); @Override protected void before() throws Throwable { - tmp.create(); - ConfidentialStore.TEST.set(new DefaultConfidentialStore(tmp.getRoot())); + ConfidentialStore.Mock.INSTANCE.clear(); } - @Override - protected void after() { - ConfidentialStore.TEST.set(null); - tmp.delete(); - } - - static { - ConfidentialStore.TEST = new ThreadLocal<>(); - } } diff --git a/core/src/test/java/jenkins/util/MarkFindingOutputStreamTest.java b/core/src/test/java/jenkins/util/MarkFindingOutputStreamTest.java index 3de38b3449d72baaed1523cbc280b26ed561d366..19e18af62bb3029fa772117b4d401f493b02749e 100644 --- a/core/src/test/java/jenkins/util/MarkFindingOutputStreamTest.java +++ b/core/src/test/java/jenkins/util/MarkFindingOutputStreamTest.java @@ -84,7 +84,7 @@ public class MarkFindingOutputStreamTest { m.write(s.charAt(i)); } - class MarkCountingOutputStream extends MarkFindingOutputStream { + static class MarkCountingOutputStream extends MarkFindingOutputStream { int count = 0; MarkCountingOutputStream(OutputStream base) { diff --git a/pom.xml b/pom.xml index f7836526c5229f60fa93a41faac155ee25e75642..bc8dac441687a054d16ba67fc96bc8b267e4e181 100755 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ THE SOFTWARE. - 2.229 + 2.230 -SNAPSHOT