diff --git a/Jenkinsfile b/Jenkinsfile index f57b71beca..a2b8ed5e0b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -78,7 +78,7 @@ for (i = 0; i < buildTypes.size(); i++) { if (!fileExists('test/target/surefire-reports/TEST-jenkins.Junit4TestsRanTest.xml')) { error 'JUnit 4 tests are no longer being run for the test package' } - // cli has been migrated to JUnit 5 + // cli and war have been migrated to JUnit 5 if (failFast && currentBuild.result == 'UNSTABLE') { error 'There were test failures; halting early' } diff --git a/war/pom.xml b/war/pom.xml index 8a06a201ea..7539101ea5 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -117,12 +117,6 @@ THE SOFTWARE. org.slf4j slf4j-jdk14 - - org.jenkins-ci - executable-war - 2.8 - provided - + + maven-enforcer-plugin + + + display-info + + + + 1.8 + + + 1.8 + + org.jenkins-ci.main:cli + org.jenkins-ci.main:jenkins-core + + + + + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + 1.8 + 1.8 + + + + + + maven-javadoc-plugin + + 1.8 + + + maven-war-plugin @@ -154,7 +208,7 @@ THE SOFTWARE. - Main + executable.Main @@ -183,21 +237,6 @@ THE SOFTWARE. ${project.build.outputDirectory}/dependencies.txt - - - executable-war-header - - unpack-dependencies - - generate-resources - - org.jenkins-ci - executable-war - provided - **/*.class - ${project.build.directory}/${project.build.finalName} - - resgen @@ -210,7 +249,7 @@ THE SOFTWARE. org.jenkins-ci winstone - ${project.build.directory}/${project.build.finalName} + ${project.build.directory}/${project.build.finalName}/executable winstone.jar @@ -479,6 +518,27 @@ THE SOFTWARE. + + maven-antrun-plugin + + + classes-copy + + run + + prepare-package + + + + + + + + + + + + com.cloudbees diff --git a/war/src/main/java/executable/LogFileOutputStream.java b/war/src/main/java/executable/LogFileOutputStream.java new file mode 100644 index 0000000000..d860dce2a9 --- /dev/null +++ b/war/src/main/java/executable/LogFileOutputStream.java @@ -0,0 +1,123 @@ +/* + * The MIT License + * + * Copyright (c) 2008, Sun Microsystems, 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 executable; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import sun.misc.Signal; + +/** + * {@link OutputStream} that writes to a log file. + * + *

+ * Unlike the plain {@link FileOutputStream}, this implementation + * listens to SIGALRM and reopens the log file. This behavior is + * necessary for allowing log rotations to happen smoothly. + * + *

+ * Because the reopen operation needs to happen atomically, + * write operations are synchronized. + * + * @author Kohsuke Kawaguchi + */ +final class LogFileOutputStream extends FilterOutputStream { + /** + * This is where we are writing. + */ + private final File file; + + LogFileOutputStream(File file) throws FileNotFoundException { + super(null); + this.file = file; + out = new FileOutputStream(file, true); + + if (File.pathSeparatorChar == ':') { + Signal.handle(new Signal("ALRM"), signal -> { + try { + reopen(); + } catch (IOException e) { + throw new UncheckedIOException(e); // failed to reopen + } + }); + } + } + + public synchronized void reopen() throws IOException { + out.close(); + out = NULL; // in case reopen fails, initialize with NULL first + out = new FileOutputStream(file, true); + } + + @Override + public synchronized void write(@NonNull byte[] b) throws IOException { + out.write(b); + } + + @Override + public synchronized void write(@NonNull byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + + @Override + public synchronized void flush() throws IOException { + out.flush(); + } + + @Override + public synchronized void close() throws IOException { + out.close(); + } + + @Override + public synchronized void write(int b) throws IOException { + out.write(b); + } + + @Override + public String toString() { + return getClass().getName() + " -> " + file; + } + + /** + * /dev/null + */ + private static final OutputStream NULL = new OutputStream() { + @Override + public void write(int b) { + // noop + } + + @Override + public void write(@NonNull byte[] b, int off, int len) { + // noop + } + }; +} diff --git a/war/src/main/java/executable/Main.java b/war/src/main/java/executable/Main.java new file mode 100644 index 0000000000..59425a5f8b --- /dev/null +++ b/war/src/main/java/executable/Main.java @@ -0,0 +1,587 @@ +/* + * The MIT License + * + * Copyright (c) 2008-2011, Sun Microsystems, Inc., Alan Harder, Jerome Lacoste, Kohsuke Kawaguchi, + * bap2000, 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 executable; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.MissingResourceException; +import java.util.Set; +import java.util.UUID; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Launcher class for stand-alone execution of Jenkins as + * {@code java -jar jenkins.war}. + * + * @author Kohsuke Kawaguchi + */ +public class Main { + + private static final int MINIMUM_JAVA_VERSION = 11; + private static final int MAXIMUM_JAVA_VERSION = 17; + private static final Set SUPPORTED_JAVA_VERSIONS = + new HashSet<>(Arrays.asList(MINIMUM_JAVA_VERSION, MAXIMUM_JAVA_VERSION)); + private static final int MINIMUM_JAVA_CLASS_VERSION = 55; + private static final int MAXIMUM_JAVA_CLASS_VERSION = 61; + private static final Set SUPPORTED_JAVA_CLASS_VERSIONS = + new HashSet<>(Arrays.asList(MINIMUM_JAVA_CLASS_VERSION, MAXIMUM_JAVA_CLASS_VERSION)); + + private static final Logger LOGGER = Logger.getLogger(Main.class.getName()); + + /** + * Sets custom session cookie name. + * It may be used to prevent randomization of JSESSIONID cookies and issues like + * JENKINS-25046. + * @since TODO + */ + private static final String JSESSIONID_COOKIE_NAME = + System.getProperty("executableWar.jetty.sessionIdCookieName"); + + /** + * Disables usage of the custom cookie names when starting the WAR file. + * If the flag is specified, the session ID will be defined by the internal Jetty logic. + * In such case it becomes configurable via + * Jetty XML Config file> + * or via system properties. + * @since TODO + */ + private static final boolean DISABLE_CUSTOM_JSESSIONID_COOKIE_NAME = + Boolean.getBoolean("executableWar.jetty.disableCustomSessionIdCookieName"); + + /** + * Flag to bypass the Java version check when starting. + */ + private static final String ENABLE_FUTURE_JAVA_CLI_SWITCH = "--enable-future-java"; + + public static void main(String[] args) throws IllegalAccessException { + try { + String v = System.getProperty("java.class.version"); + if (v != null) { + String classVersionString = v.split("\\.")[0]; + try { + int javaVersion = Integer.parseInt(classVersionString); + verifyJavaVersion(javaVersion, isFutureJavaEnabled(args)); + } catch (NumberFormatException e) { + // err on the safe side and keep on going + LOGGER.log(Level.WARNING, "Failed to parse java.class.version: {0}. Will continue execution", v); + } + } + + _main(args); + } catch (UnsupportedClassVersionError e) { + System.err.printf( + "Jenkins requires Java versions %s but you are running with Java %s from %s%n", + SUPPORTED_JAVA_VERSIONS, System.getProperty("java.specification.version"), System.getProperty("java.home")); + e.printStackTrace(); + } + } + + /*package*/ static void verifyJavaVersion(int javaClassVersion, boolean enableFutureJava) { + final String displayVersion = String.format("%d.0", javaClassVersion); + if (SUPPORTED_JAVA_CLASS_VERSIONS.contains(javaClassVersion)) { + // Great! + } else if (javaClassVersion > MINIMUM_JAVA_CLASS_VERSION) { + if (enableFutureJava) { + LOGGER.log(Level.WARNING, + String.format("Running with Java class version %s which is not in the list of supported versions: %s. " + + "Argument %s is set, so will continue. " + + "See https://jenkins.io/redirect/java-support/", + javaClassVersion, SUPPORTED_JAVA_CLASS_VERSIONS, ENABLE_FUTURE_JAVA_CLI_SWITCH)); + } else { + Error error = new UnsupportedClassVersionError(displayVersion); + LOGGER.log(Level.SEVERE, String.format("Running with Java class version %s which is not in the list of supported versions: %s. " + + "Run with the " + ENABLE_FUTURE_JAVA_CLI_SWITCH + " flag to enable such behavior. " + + "See https://jenkins.io/redirect/java-support/", + javaClassVersion, SUPPORTED_JAVA_CLASS_VERSIONS), error); + throw error; + } + } else { + Error error = new UnsupportedClassVersionError(displayVersion); + LOGGER.log(Level.SEVERE, + String.format("Running with Java class version %s, which is older than the Minimum required version %s. " + + "See https://jenkins.io/redirect/java-support/", + javaClassVersion, MINIMUM_JAVA_CLASS_VERSION), error); + throw error; + } + } + + /** + * Returns true if the Java runtime version check should not be done, and any version allowed. + * + * @see #ENABLE_FUTURE_JAVA_CLI_SWITCH + */ + private static boolean isFutureJavaEnabled(String[] args) { + return hasArgument(ENABLE_FUTURE_JAVA_CLI_SWITCH, args) || Boolean.parseBoolean(System.getenv("JENKINS_ENABLE_FUTURE_JAVA")); + } + + // TODO: Rework everything to use List + private static boolean hasArgument(@NonNull String argument, @NonNull String[] args) { + for (String arg : args) { + if (argument.equals(arg)) { + return true; + } + } + return false; + } + + @SuppressFBWarnings( + value = {"PATH_TRAVERSAL_IN", "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION"}, + justification = "User provided values for running the program and intentional propagation of reflection errors") + private static void _main(String[] args) throws IllegalAccessException { + //Allows to pass arguments through stdin to "hide" sensitive parameters like httpsKeyStorePassword + //to achieve this use --paramsFromStdIn + if (hasArgument("--paramsFromStdIn", args)) { + System.out.println("--paramsFromStdIn detected. Parameters are going to be read from stdin. Other parameters passed directly will be ignored."); + String argsInStdIn = readStringNonBlocking(System.in, 131072).trim(); + args = argsInStdIn.split(" +"); + } + // If someone just wants to know the version, print it out as soon as possible, with no extraneous file or webroot info. + // This makes it easier to grab the version from a script + final List arguments = new ArrayList<>(Arrays.asList(args)); + if (arguments.contains("--version")) { + System.out.println(getVersion("?")); + return; + } + + File extractedFilesFolder = null; + for (String arg : args) { + if (arg.startsWith("--extractedFilesFolder=")) { + extractedFilesFolder = new File(arg.substring("--extractedFilesFolder=".length())); + if (!extractedFilesFolder.isDirectory()) { + System.err.println("The extractedFilesFolder value is not a directory. Ignoring."); + extractedFilesFolder = null; + } + } + } + + // if the output should be redirect to a file, do it now + for (int i = 0; i < args.length; i++) { + if (args[i].startsWith("--logfile=")) { + PrintStream ps = createLogFileStream(new File(args[i].substring("--logfile=".length()))); + System.setOut(ps); + System.setErr(ps); + // don't let winstone see this + List _args = new ArrayList<>(Arrays.asList(args)); + _args.remove(i); + args = _args.toArray(new String[0]); + break; + } + } + for (String arg : args) { + if (arg.startsWith("--pluginroot=")) { + System.setProperty("hudson.PluginManager.workDir", + new File(arg.substring("--pluginroot=".length())).getAbsolutePath()); + // if specified multiple times, the first one wins + break; + } + } + + // this is so that JFreeChart can work nicely even if we are launched as a daemon + System.setProperty("java.awt.headless", "true"); + + File me = whoAmI(extractedFilesFolder); + System.out.println("Running from: " + me); + System.setProperty("executable-war", me.getAbsolutePath()); // remember the location so that we can access it from within webapp + + // figure out the arguments + trimOffOurOptions(arguments); + arguments.add(0, "--warfile=" + me.getAbsolutePath()); + if (!hasOption(arguments, "--webroot=")) { + // defaults to ~/.jenkins/war since many users reported that cron job attempts to clean up + // the contents in the temporary directory. + final FileAndDescription describedHomeDir = getHomeDir(); + System.out.println("webroot: " + describedHomeDir.description); + arguments.add("--webroot=" + new File(describedHomeDir.file, "war")); + } + + // only do a cleanup if you set the extractedFilesFolder property. + if (extractedFilesFolder != null) { + deleteContentsFromFolder(extractedFilesFolder, "winstone.*\\.jar"); + } + + // put winstone jar in a file system so that we can load jars from there + File tmpJar = extractFromJar("winstone.jar", "winstone", ".jar", extractedFilesFolder); + tmpJar.deleteOnExit(); + + // clean up any previously extracted copy, since + // winstone doesn't do so and that causes problems when newer version of Jenkins + // is deployed. + File tempFile; + try { + tempFile = File.createTempFile("dummy", "dummy"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + deleteWinstoneTempContents(new File(tempFile.getParent(), "winstone/" + me.getName())); + if (!tempFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete the temporary file {0}", tempFile); + } + + // locate the Winstone launcher + ClassLoader cl; + try { + cl = new URLClassLoader(new URL[] {tmpJar.toURI().toURL()}); + } catch (MalformedURLException e) { + throw new UncheckedIOException(e); + } + Class launcher; + Method mainMethod; + try { + launcher = cl.loadClass("winstone.Launcher"); + mainMethod = launcher.getMethod("main", String[].class); + } catch (ClassNotFoundException | NoSuchMethodException e) { + throw new AssertionError(e); + } + + // override the usage screen + Field usage; + try { + usage = launcher.getField("USAGE"); + } catch (NoSuchFieldException e) { + throw new AssertionError(e); + } + usage.set(null, "Jenkins Automation Server Engine " + getVersion("") + "\n" + + "Usage: java -jar jenkins.war [--option=value] [--option=value]\n" + + "\n" + + "Options:\n" + + " --webroot = folder where the WAR file is expanded into. Default is ${JENKINS_HOME}/war\n" + + " --pluginroot = folder where the plugin archives are expanded into. Default is ${JENKINS_HOME}/plugins\n" + + " (NOTE: this option does not change the directory where the plugin archives are stored)\n" + + " --extractedFilesFolder = folder where extracted files are to be located. Default is the temp folder\n" + + " --logfile = redirect log messages to this file\n" + + " " + ENABLE_FUTURE_JAVA_CLI_SWITCH + " = allows running with new Java versions which are not fully supported (class version " + MINIMUM_JAVA_CLASS_VERSION + " and above)\n" + + "{OPTIONS}"); + + if (!DISABLE_CUSTOM_JSESSIONID_COOKIE_NAME) { + /* + Set an unique cookie name. + + As can be seen in discussions like http://stackoverflow.com/questions/1146112/jsessionid-collision-between-two-servers-on-same-ip-but-different-ports + and http://stackoverflow.com/questions/1612177/are-http-cookies-port-specific, RFC 2965 says + cookies from one port of one host may be sent to a different port of the same host. + This means if someone runs multiple Jenkins on different ports of the same host, + their sessions get mixed up. + + To fix the problem, use unique session cookie name. + + This change breaks the cluster mode of Winstone, as all nodes in the cluster must share the same session cookie name. + Jenkins doesn't support clustered operation anyway, so we need to do this here, and not in Winstone. + */ + try { + Field f = cl.loadClass("winstone.WinstoneSession").getField("SESSION_COOKIE_NAME"); + f.setAccessible(true); + if (JSESSIONID_COOKIE_NAME != null) { + // Use the user-defined cookie name + f.set(null, JSESSIONID_COOKIE_NAME); + } else { + // Randomize session names by default to prevent collisions when running multiple Jenkins instances on the same host. + f.set(null, "JSESSIONID." + UUID.randomUUID().toString().replace("-", "").substring(0, 8)); + } + } catch (ClassNotFoundException | NoSuchFieldException e) { + throw new AssertionError(e); + } + } + + // run + Thread.currentThread().setContextClassLoader(cl); + try { + mainMethod.invoke(null, new Object[] {arguments.toArray(new String[0])}); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else if (t instanceof IOException) { + throw new UncheckedIOException((IOException) t); + } else if (t instanceof Exception) { + throw new RuntimeException(t); + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw new RuntimeException(e); + } + } + } + + @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "--logfile relies on the default encoding, fine") + private static PrintStream createLogFileStream(File file) { + LogFileOutputStream los; + try { + los = new LogFileOutputStream(file); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + return new PrintStream(los); + } + + // TODO: Get rid of FB warning after updating to Java 7 + /** + * reads up to maxRead bytes from InputStream if available into a String + * + * @param in input stream to be read + * @param maxToRead maximum number of bytes to read from the in + * @return a String read from in + */ + @SuppressFBWarnings(value = {"DM_DEFAULT_ENCODING", "RR_NOT_CHECKED"}, justification = "Legacy behavior, We expect less input than maxToRead") + private static String readStringNonBlocking(InputStream in, int maxToRead) { + byte[] buffer; + try { + buffer = new byte[Math.min(in.available(), maxToRead)]; + in.read(buffer); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return new String(buffer); + } + + private static void trimOffOurOptions(List arguments) { + arguments.removeIf(arg -> arg.startsWith("--daemon") || arg.startsWith("--logfile") || arg.startsWith("--extractedFilesFolder") + || arg.startsWith("--pluginroot") || arg.startsWith(ENABLE_FUTURE_JAVA_CLI_SWITCH)); + } + + /** + * Figures out the version from the manifest. + */ + private static String getVersion(String fallback) { + try { + Enumeration manifests = Main.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); + while (manifests.hasMoreElements()) { + URL res = manifests.nextElement(); + Manifest manifest = new Manifest(res.openStream()); + String v = manifest.getMainAttributes().getValue("Jenkins-Version"); + if (v != null) { + return v; + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return fallback; + } + + private static boolean hasOption(List args, String prefix) { + for (String s : args) { + if (s.startsWith(prefix)) { + return true; + } + } + return false; + } + + /** + * Figures out the URL of {@code jenkins.war}. + */ + @SuppressFBWarnings(value = {"PATH_TRAVERSAL_IN", "URLCONNECTION_SSRF_FD"}, justification = "User provided values for running the program.") + public static File whoAmI(File directory) { + // JNLP returns the URL where the jar was originally placed (like http://jenkins-ci.org/...) + // not the local cached file. So we need a rather round about approach to get to + // the local file name. + // There is no portable way to find where the locally cached copy + // of jenkins.war/jar is; JDK 6 is too smart. (See JENKINS-2326.) + try { + URL classFile = Main.class.getClassLoader().getResource("Main.class"); + JarFile jf = ((JarURLConnection) classFile.openConnection()).getJarFile(); + return new File(jf.getName()); + } catch (Exception x) { + System.err.println("ZipFile.name trick did not work, using fallback: " + x); + } + File myself; + try { + myself = File.createTempFile("jenkins", ".jar", directory); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + myself.deleteOnExit(); + try (InputStream is = Main.class.getProtectionDomain().getCodeSource().getLocation().openStream(); + OutputStream os = new FileOutputStream(myself)) { + copyStream(is, os); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return myself; + } + + private static void copyStream(InputStream in, OutputStream out) throws IOException { + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } + + /** + * Extract a resource from jar, mark it for deletion upon exit, and return its location. + */ + @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "User provided values for running the program.") + private static File extractFromJar(String resource, String fileName, String suffix, File directory) { + URL res = Main.class.getResource(resource); + if (res == null) { + throw new MissingResourceException("Unable to find the resource: " + resource, Main.class.getName(), resource); + } + + // put this jar in a file system so that we can load jars from there + File tmp; + try { + tmp = File.createTempFile(fileName, suffix, directory); + } catch (IOException e) { + String tmpdir = directory == null ? System.getProperty("java.io.tmpdir") : directory.getAbsolutePath(); + throw new UncheckedIOException("Jenkins failed to create a temporary file in " + tmpdir + ": " + e, e); + } + try (InputStream is = res.openStream(); OutputStream os = new FileOutputStream(tmp)) { + copyStream(is, os); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + tmp.deleteOnExit(); + return tmp; + } + + /** + * Search contents to delete in a folder that match with some patterns. + * + * @param folder folder where the contents are. + * @param patterns patterns that identifies the contents to search. + */ + private static void deleteContentsFromFolder(File folder, final String... patterns) { + File[] files = folder.listFiles(); + + if (files != null) { + for (File file : files) { + for (String pattern : patterns) { + if (file.getName().matches(pattern)) { + LOGGER.log(Level.FINE, "Deleting the temporary file {0}", file); + deleteWinstoneTempContents(file); + } + } + } + } + } + + private static void deleteWinstoneTempContents(File file) { + if (!file.exists()) { + LOGGER.log(Level.FINEST, "No file found at {0}, nothing to delete.", file); + return; + } + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { // be defensive + for (File value : files) { + deleteWinstoneTempContents(value); + } + } + } + if (!file.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete the temporary Winstone file {0}", file); + } + } + + /** Add some metadata to a File, allowing to trace setup issues */ + private static class FileAndDescription { + final File file; + final String description; + + FileAndDescription(File file, String description) { + this.file = file; + this.description = description; + } + } + + /** + * Determines the home directory for Jenkins. + * + * People makes configuration mistakes, so we are trying to be nice + * with those by doing {@link String#trim()}. + * + * @return the File alongside with some description to help the user troubleshoot issues + */ + @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "User provided values for running the program.") + private static FileAndDescription getHomeDir() { + // check the system property for the home directory first + for (String name : HOME_NAMES) { + String sysProp = System.getProperty(name); + if (sysProp != null) + return new FileAndDescription(new File(sysProp.trim()), "System.getProperty(\"" + name + "\")"); + } + + // look at the env var next + try { + for (String name : HOME_NAMES) { + String env = System.getenv(name); + if (env != null) + return new FileAndDescription(new File(env.trim()).getAbsoluteFile(), "EnvVars.masterEnvVars.get(\"" + name + "\")"); + } + } catch (Throwable e) { + // this code fails when run on JDK1.4 + } + + // otherwise pick a place by ourselves + +/* ServletContext not available yet + String root = event.getServletContext().getRealPath("/WEB-INF/workspace"); + if(root!=null) { + File ws = new File(root.trim()); + if(ws.exists()) + // Hudson <1.42 used to prefer this before ~/.hudson, so + // check the existence and if it's there, use it. + // otherwise if this is a new installation, prefer ~/.hudson + return new FileAndDescription(ws, "getServletContext().getRealPath(\"/WEB-INF/workspace\")"); + } +*/ + + // if for some reason we can't put it within the webapp, use home directory. + File legacyHome = new File(new File(System.getProperty("user.home")), ".hudson"); + if (legacyHome.exists()) { + return new FileAndDescription(legacyHome, "$user.home/.hudson"); // before rename, this is where it was stored + } + + File newHome = new File(new File(System.getProperty("user.home")), ".jenkins"); + return new FileAndDescription(newHome, "$user.home/.jenkins"); + } + + private static final String[] HOME_NAMES = {"JENKINS_HOME", "HUDSON_HOME"}; +} diff --git a/war/src/test/java/executable/MainTest.java b/war/src/test/java/executable/MainTest.java new file mode 100644 index 0000000000..1948f2f551 --- /dev/null +++ b/war/src/test/java/executable/MainTest.java @@ -0,0 +1,84 @@ +package executable; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class MainTest { + + @Test + void shouldFailForOldJava() { + assertJavaCheckFails(52, false); + assertJavaCheckFails(52, true); + } + + @Test + void shouldBeOkForJava11() { + assertJavaCheckPasses(55, false); + assertJavaCheckPasses(55, true); + } + + @Test + void shouldFailForMidJavaVersionsIfNoFlag() { + assertJavaCheckFails(56, false); + assertJavaCheckPasses(56, true); + assertJavaCheckFails(57, false); + assertJavaCheckPasses(57, true); + assertJavaCheckFails(58, false); + assertJavaCheckPasses(58, true); + assertJavaCheckFails(59, false); + assertJavaCheckPasses(59, true); + assertJavaCheckFails(60, false); + assertJavaCheckPasses(60, true); + } + + @Test + void shouldBeOkForJava17() { + assertJavaCheckPasses(61, false); + assertJavaCheckPasses(61, true); + } + + @Test + void shouldFailForNewJavaVersionsIfNoFlag() { + assertJavaCheckFails(62, false); + assertJavaCheckPasses(62, true); + assertJavaCheckFails(63, false); + assertJavaCheckPasses(63, true); + } + + private static void assertJavaCheckFails(int classVersion, boolean enableFutureJava) { + assertJavaCheckFails(null, classVersion, enableFutureJava); + } + + private static void assertJavaCheckFails(@CheckForNull String message, int classVersion, boolean enableFutureJava) { + boolean failed = false; + try { + Main.verifyJavaVersion(classVersion, enableFutureJava); + } catch (Error error) { + failed = true; + System.out.printf("Java class version check failed as it was expected for Java class version %s.0 and enableFutureJava=%s%n", + classVersion, enableFutureJava); + error.printStackTrace(System.out); + } + + if (!failed) { + Assertions.fail(message != null ? message : + String.format("Java version Check should have failed for Java class version %s.0 and enableFutureJava=%s", + classVersion, enableFutureJava)); + } + } + + private static void assertJavaCheckPasses(int classVersion, boolean enableFutureJava) { + assertJavaCheckPasses(null, classVersion, enableFutureJava); + } + + private static void assertJavaCheckPasses(@CheckForNull String message, int classVersion, boolean enableFutureJava) { + try { + Main.verifyJavaVersion(classVersion, enableFutureJava); + } catch (Error error) { + throw new AssertionError(message != null ? message : + String.format("Java version Check should have passed for Java class version %s.0 and enableFutureJava=%s", + classVersion, enableFutureJava), error); + } + } +}