diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index b90e6fcfda9e..a3b6a07e98ec 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -65,6 +65,7 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.Vector; @@ -74,6 +75,10 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.jenkinsci.bytecode.Transformer; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; import static org.apache.commons.io.FilenameUtils.getBaseName; @@ -279,10 +284,66 @@ protected ClassLoader createClassLoader(List paths, ClassLoader parent, At return classLoader; } + /** + * Get the list of all plugins that have ever been {@link DetachedPlugin detached} from Jenkins core. + * @return A {@link List} of {@link DetachedPlugin}s. + */ + @Restricted(NoExternalUse.class) + public static @Nonnull List getDetachedPlugins() { + return DETACHED_LIST; + } + + /** + * Get the list of plugins that have been detached since a specific Jenkins release version. + * @param since The Jenkins version. + * @return A {@link List} of {@link DetachedPlugin}s. + */ + @Restricted(NoExternalUse.class) + public static @Nonnull List getDetachedPlugins(@Nonnull VersionNumber since) { + List detachedPlugins = new ArrayList<>(); + + for (DetachedPlugin detachedPlugin : DETACHED_LIST) { + if (!detachedPlugin.getSplitWhen().isOlderThan(since)) { + detachedPlugins.add(detachedPlugin); + } + } + + return detachedPlugins; + } + + /** + * Is the named plugin a plugin that was detached from Jenkins at some point in the past. + * @param pluginId The plugin ID. + * @return {@code true} if the plugin is a plugin that was detached from Jenkins at some + * point in the past, otherwise {@code false}. + */ + @Restricted(NoExternalUse.class) + public static boolean isDetachedPlugin(@Nonnull String pluginId) { + for (DetachedPlugin detachedPlugin : DETACHED_LIST) { + if (detachedPlugin.getShortName().equals(pluginId)) { + return true; + } + } + + return false; + } + /** * Information about plugins that were originally in the core. + *

+ * A detached plugin is one that has any of the following characteristics: + *

    + *
  • + * Was an existing plugin that at some time previously bundled with the Jenkins war file. + *
  • + *
  • + * Was previous code in jenkins core that was split to a separate-plugin (but may not have + * ever been bundled in a jenkins war file - i.e. it gets split after this 2.0 update). + *
  • + *
*/ - private static final class DetachedPlugin { + @Restricted(NoExternalUse.class) + public static final class DetachedPlugin { private final String shortName; /** * Plugins built for this Jenkins version (and earlier) will automatically be assumed to have @@ -300,6 +361,22 @@ private DetachedPlugin(String shortName, String splitWhen, String requireVersion this.requireVersion = requireVersion; } + /** + * Get the short name of the plugin. + * @return The short name of the plugin. + */ + public String getShortName() { + return shortName; + } + + /** + * Get the Jenkins version from which the plugin was detached. + * @return The Jenkins version from which the plugin was detached. + */ + public VersionNumber getSplitWhen() { + return splitWhen; + } + private void fix(Attributes atts, List optionalDependencies) { // don't fix the dependency for yourself, or else we'll have a cycle String yourName = atts.getValue("Short-Name"); @@ -320,22 +397,22 @@ private void fix(Attributes atts, List optionalDepende } } - private static final List DETACHED_LIST = Arrays.asList( - new DetachedPlugin("maven-plugin","1.296","1.296"), - new DetachedPlugin("subversion","1.310","1.0"), - new DetachedPlugin("cvs","1.340","0.1"), - new DetachedPlugin("ant","1.430.*","1.0"), - new DetachedPlugin("javadoc","1.430.*","1.0"), - new DetachedPlugin("external-monitor-job","1.467.*","1.0"), - new DetachedPlugin("ldap","1.467.*","1.0"), - new DetachedPlugin("pam-auth","1.467.*","1.0"), - new DetachedPlugin("mailer","1.493.*","1.2"), - new DetachedPlugin("matrix-auth","1.535.*","1.0.2"), - new DetachedPlugin("windows-slaves","1.547.*","1.0"), - new DetachedPlugin("antisamy-markup-formatter","1.553.*","1.0"), - new DetachedPlugin("matrix-project","1.561.*","1.0"), - new DetachedPlugin("junit","1.577.*","1.0") - ); + private static final List DETACHED_LIST = Collections.unmodifiableList(Arrays.asList( + new DetachedPlugin("maven-plugin", "1.296", "1.296"), + new DetachedPlugin("subversion", "1.310", "1.0"), + new DetachedPlugin("cvs", "1.340", "0.1"), + new DetachedPlugin("ant", "1.430.*", "1.0"), + new DetachedPlugin("javadoc", "1.430.*", "1.0"), + new DetachedPlugin("external-monitor-job", "1.467.*", "1.0"), + new DetachedPlugin("ldap", "1.467.*", "1.0"), + new DetachedPlugin("pam-auth", "1.467.*", "1.0"), + new DetachedPlugin("mailer", "1.493.*", "1.2"), + new DetachedPlugin("matrix-auth", "1.535.*", "1.0.2"), + new DetachedPlugin("windows-slaves", "1.547.*", "1.0"), + new DetachedPlugin("antisamy-markup-formatter", "1.553.*", "1.0"), + new DetachedPlugin("matrix-project", "1.561.*", "1.0"), + new DetachedPlugin("junit", "1.577.*", "1.0") + )); /** Implicit dependencies that are known to be unnecessary and which must be cut out to prevent a dependency cycle among bundled plugins. */ private static final Set BREAK_CYCLES = new HashSet(Arrays.asList( diff --git a/core/src/main/java/hudson/LocalPluginManager.java b/core/src/main/java/hudson/LocalPluginManager.java index 4cfbf6ad9334..d5926dff2ab6 100644 --- a/core/src/main/java/hudson/LocalPluginManager.java +++ b/core/src/main/java/hudson/LocalPluginManager.java @@ -26,15 +26,9 @@ import jenkins.model.Jenkins; -import javax.servlet.ServletContext; import java.io.File; -import java.io.IOException; -import java.net.URL; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.logging.Level; import java.util.logging.Logger; /** @@ -64,28 +58,11 @@ protected Collection loadBundledPlugins() { return Collections.emptySet(); } - Set names = new HashSet(); - - ServletContext context = Jenkins.getInstance().servletContext; - - for( String path : Util.fixNull((Set)context.getResourcePaths("/WEB-INF/plugins"))) { - String fileName = path.substring(path.lastIndexOf('/')+1); - if(fileName.length()==0) { - // see http://www.nabble.com/404-Not-Found-error-when-clicking-on-help-td24508544.html - // I suspect some containers are returning directory names. - continue; - } - try { - names.add(fileName); - - URL url = context.getResource(path); - copyBundledPlugin(url, fileName); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Failed to extract the bundled plugin "+fileName,e); - } + try { + return loadPluginsFromWar("/WEB-INF/plugins"); + } finally { + loadDetachedPlugins(); } - - return names; } private static final Logger LOGGER = Logger.getLogger(LocalPluginManager.class.getName()); diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index d7eb14f7b6dd..4e9e7f773f4a 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -40,7 +40,6 @@ import hudson.security.PermissionScope; import hudson.util.CyclicGraphDetector; import hudson.util.CyclicGraphDetector.CycleDetectedException; -import hudson.util.IOUtils; import hudson.util.PersistedList; import hudson.util.Service; import hudson.util.VersionNumber; @@ -49,6 +48,8 @@ import jenkins.InitReactorRunner; import jenkins.RestartRequiredException; import jenkins.YesNoMaybe; +import jenkins.install.InstallState; +import jenkins.install.InstallUtil; import jenkins.model.Jenkins; import jenkins.util.io.OnMaster; import jenkins.util.xml.RestrictiveEntityResolver; @@ -60,6 +61,7 @@ import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.logging.LogFactory; import org.jenkinsci.bytecode.Transformer; import org.jvnet.hudson.reactor.Executable; @@ -67,6 +69,7 @@ import org.jvnet.hudson.reactor.ReactorException; import org.jvnet.hudson.reactor.TaskBuilder; import org.jvnet.hudson.reactor.TaskGraphBuilder; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -79,16 +82,20 @@ import org.kohsuke.stapler.interceptor.RequirePOST; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import java.io.Closeable; import java.io.File; +import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; @@ -98,11 +105,13 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; +import java.util.LinkedHashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -120,6 +129,7 @@ import hudson.model.DownloadService; import hudson.util.FormValidation; +import static java.util.logging.Level.INFO; import static java.util.logging.Level.SEVERE; import static java.util.logging.Level.WARNING; import org.kohsuke.accmod.Restricted; @@ -188,11 +198,6 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas */ private final PluginStrategy strategy; - /** - * Manifest of the plugin binaries that are bundled with core. - */ - private final Map bundledPluginManifests = new HashMap(); - public PluginManager(ServletContext context, File rootDir) { this.context = context; @@ -366,13 +371,13 @@ protected void reactOnCycle(PluginWrapper q, List cycle) // lists up initialization tasks about loading plugins. return TaskBuilder.union(initializerFinder, // this scans @Initializer in the core once - builder,new TaskGraphBuilder() {{ - requires(PLUGINS_LISTED).attains(PLUGINS_PREPARED).add("Loading plugins",new Executable() { + builder, new TaskGraphBuilder() {{ + requires(PLUGINS_LISTED).attains(PLUGINS_PREPARED).add("Loading plugins", new Executable() { /** * Once the plugins are listed, schedule their initialization. */ public void run(Reactor session) throws Exception { - Jenkins.getInstance().lookup.set(PluginInstanceStore.class,new PluginInstanceStore()); + Jenkins.getInstance().lookup.set(PluginInstanceStore.class, new PluginInstanceStore()); TaskGraphBuilder g = new TaskGraphBuilder(); // schedule execution of loading plugins @@ -425,6 +430,190 @@ public void run(Reactor reactor) throws Exception { }}); } + protected @Nonnull Set loadPluginsFromWar(@Nonnull String fromPath) { + return loadPluginsFromWar(fromPath, null); + } + + protected @Nonnull Set loadPluginsFromWar(@Nonnull String fromPath, @CheckForNull FilenameFilter filter) { + Set names = new HashSet(); + + ServletContext context = Jenkins.getActiveInstance().servletContext; + Set plugins = Util.fixNull((Set) context.getResourcePaths(fromPath)); + Set copiedPlugins = new HashSet<>(); + Set dependencies = new HashSet<>(); + + for( String pluginPath : plugins) { + String fileName = pluginPath.substring(pluginPath.lastIndexOf('/')+1); + if(fileName.length()==0) { + // see http://www.nabble.com/404-Not-Found-error-when-clicking-on-help-td24508544.html + // I suspect some containers are returning directory names. + continue; + } + try { + URL url = context.getResource(pluginPath); + if (filter != null && url != null) { + if (!filter.accept(new File(url.getFile()).getParentFile(), fileName)) { + continue; + } + } + + names.add(fileName); + copyBundledPlugin(url, fileName); + copiedPlugins.add(url); + try { + addDependencies(url, fromPath, dependencies); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to resolve dependencies for the bundled plugin " + fileName, e); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to extract the bundled plugin "+fileName,e); + } + } + + // Copy dependencies. These are not detached plugins, but are required by them. + for (URL dependency : dependencies) { + if (copiedPlugins.contains(dependency)) { + // Ignore. Already copied. + continue; + } + + String fileName = new File(dependency.getFile()).getName(); + try { + names.add(fileName); + copyBundledPlugin(dependency, fileName); + copiedPlugins.add(dependency); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to extract the bundled dependency plugin " + fileName, e); + } + } + + return names; + } + + protected static void addDependencies(URL hpiResUrl, String fromPath, Set dependencySet) throws URISyntaxException, MalformedURLException { + if (dependencySet.contains(hpiResUrl)) { + return; + } + + Manifest manifest = parsePluginManifest(hpiResUrl); + String dependencySpec = manifest.getMainAttributes().getValue("Plugin-Dependencies"); + if (dependencySpec != null) { + String[] dependencyTokens = dependencySpec.split(","); + ServletContext context = Jenkins.getActiveInstance().servletContext; + + for (String dependencyToken : dependencyTokens) { + if (dependencyToken.endsWith(";resolution:=optional")) { + // ignore optional dependencies + continue; + } + + String artifactId = dependencyToken.split(":")[0]; + URL dependencyURL = context.getResource(fromPath + "/" + artifactId + ".hpi"); + + if (dependencyURL == null) { + // Maybe bundling has changed .jpi files + dependencyURL = context.getResource(fromPath + "/" + artifactId + ".jpi"); + } + + if (dependencyURL != null) { + dependencySet.add(dependencyURL); + // And transitive deps... + addDependencies(dependencyURL, fromPath, dependencySet); + } + } + } + } + + /** + * Load detached plugins and their dependencies. + *

+ * Only loads plugins that: + *

    + *
  • Have been detached since the last running version.
  • + *
  • Are already installed and need to be upgraded. This can be the case if this Jenkins install has been running since before plugins were "unbundled".
  • + *
  • Are dependencies of one of the above e.g. script-security is not one of the detached plugins but it must be loaded if matrix-project is loaded.
  • + *
+ */ + protected void loadDetachedPlugins() { + InstallState installState = Jenkins.getActiveInstance().getInstallState(); + if (installState == InstallState.UPGRADE) { + VersionNumber lastExecVersion = new VersionNumber(InstallUtil.getLastExecVersion()); + + LOGGER.log(INFO, "Upgrading Jenkins. The last running version was {0}. This Jenkins is version {1}.", + new Object[] {lastExecVersion, Jenkins.VERSION}); + + final List detachedPlugins = ClassicPluginStrategy.getDetachedPlugins(lastExecVersion); + + Set loadedDetached = loadPluginsFromWar("/WEB-INF/detached-plugins", new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + name = normalisePluginName(name); + + // If this was a plugin that was detached some time in the past i.e. not just one of the + // plugins that was bundled "for fun". + if (ClassicPluginStrategy.isDetachedPlugin(name)) { + // If it's already installed and the installed version is older + // than the bundled version, then we upgrade. The bundled version is the min required version + // for "this" version of Jenkins, so we must upgrade. + VersionNumber installedVersion = getPluginVersion(rootDir, name); + VersionNumber bundledVersion = getPluginVersion(dir, name); + if (installedVersion != null && bundledVersion != null && installedVersion.isOlderThan(bundledVersion)) { + return true; + } + } + + // If it's a plugin that was detached since the last running version. + for (ClassicPluginStrategy.DetachedPlugin detachedPlugin : detachedPlugins) { + if (detachedPlugin.getShortName().equals(name)) { + return true; + } + } + + // Otherwise skip this and do not install. + return false; + } + }); + + LOGGER.log(INFO, "Upgraded Jenkins from version {0} to version {1}. Loaded detached plugins (and dependencies): {2}", + new Object[] {lastExecVersion, Jenkins.VERSION, loadedDetached}); + + InstallUtil.saveLastExecVersion(); + } + } + + private String normalisePluginName(@Nonnull String name) { + // Normalise the name by stripping off the file extension (if present)... + return name.replace(".jpi", "").replace(".hpi", ""); + } + + private @CheckForNull VersionNumber getPluginVersion(@Nonnull File dir, @Nonnull String pluginId) { + VersionNumber version = getPluginVersion(new File(dir, pluginId + ".jpi")); + if (version == null) { + version = getPluginVersion(new File(dir, pluginId + ".hpi")); + } + return version; + } + + private @CheckForNull VersionNumber getPluginVersion(@Nonnull File pluginFile) { + if (!pluginFile.exists()) { + return null; + } + try { + return getPluginVersion(pluginFile.toURI().toURL()); + } catch (MalformedURLException e) { + return null; + } + } + + private @CheckForNull VersionNumber getPluginVersion(@Nonnull URL pluginURL) { + Manifest manifest = parsePluginManifest(pluginURL); + if (manifest == null) { + return null; + } + String versionSpec = manifest.getMainAttributes().getValue("Plugin-Version"); + return new VersionNumber(versionSpec); + } + /* * contains operation that considers xxx.hpi and xxx.jpi as equal * this is necessary since the bundled plugins are still called *.hpi @@ -437,8 +626,9 @@ private boolean containsHpiJpi(Collection bundledPlugins, String name) { /** * Returns the manifest of a bundled but not-extracted plugin. */ + @Deprecated // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ public @CheckForNull Manifest getBundledPluginManifest(String shortName) { - return bundledPluginManifests.get(shortName); + return null; } /** @@ -568,16 +758,14 @@ protected void copyBundledPlugin(URL src, String fileName) throws IOException { String legacyName = fileName.replace(".jpi",".hpi"); long lastModified = src.openConnection().getLastModified(); File file = new File(rootDir, fileName); - File pinFile = new File(rootDir, fileName+".pinned"); // normalization first, if the old file exists. rename(new File(rootDir,legacyName),file); - rename(new File(rootDir,legacyName+".pinned"),pinFile); // update file if: // - no file exists today - // - bundled version and current version differs (by timestamp), and the file isn't pinned. - if (!file.exists() || (file.lastModified() != lastModified && !pinFile.exists())) { + // - bundled version and current version differs (by timestamp). + if (!file.exists() || file.lastModified() != lastModified) { FileUtils.copyURLToFile(src, file); file.setLastModified(src.openConnection().getLastModified()); // lastModified is set for two reasons: @@ -585,15 +773,12 @@ protected void copyBundledPlugin(URL src, String fileName) throws IOException { // - to make sure the value is not changed after each restart, so we can avoid // unpacking the plugin itself in ClassicPluginStrategy.explode } - if (pinFile.exists()) - parsePinnedBundledPluginManifest(src); + + // Plugin pinning has been deprecated. + // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ } - /** - * When a pin file prevented a bundled plugin from getting extracted, check if the one we currently have - * is older than we bundled. - */ - private void parsePinnedBundledPluginManifest(URL bundledJpi) { + private static @CheckForNull Manifest parsePluginManifest(URL bundledJpi) { try { URLClassLoader cl = new URLClassLoader(new URL[]{bundledJpi}); InputStream in=null; @@ -602,8 +787,7 @@ private void parsePinnedBundledPluginManifest(URL bundledJpi) { if (res!=null) { in = res.openStream(); Manifest manifest = new Manifest(in); - String shortName = PluginWrapper.computeShortName(manifest, FilenameUtils.getName(bundledJpi.getPath())); - bundledPluginManifests.put(shortName, manifest); + return manifest; } } finally { IOUtils.closeQuietly(in); @@ -613,6 +797,7 @@ private void parsePinnedBundledPluginManifest(URL bundledJpi) { } catch (IOException e) { LOGGER.log(WARNING, "Failed to parse manifest of "+bundledJpi, e); } + return null; } /** @@ -781,6 +966,54 @@ public void stop() { LogFactory.release(uberClassLoader); } + /** + * Get the list of all plugins - available and installed. + * @return The list of all plugins - available and installed. + */ + @Restricted(DoNotUse.class) // WebOnly + public HttpResponse doPlugins() { + JSONArray response = new JSONArray(); + Map allPlugins = new HashMap<>(); + for (PluginWrapper plugin : plugins) { + JSONObject pluginInfo = new JSONObject(); + pluginInfo.put("installed", true); + pluginInfo.put("name", plugin.getShortName()); + pluginInfo.put("title", plugin.getDisplayName()); + pluginInfo.put("active", plugin.isActive()); + pluginInfo.put("enabled", plugin.isEnabled()); + pluginInfo.put("bundled", plugin.isBundled); + pluginInfo.put("deleted", plugin.isDeleted()); + pluginInfo.put("downgradable", plugin.isDowngradable()); + List dependencies = plugin.getDependencies(); + if (dependencies != null && !dependencies.isEmpty()) { + Map dependencyMap = new HashMap<>(); + for (Dependency dependency : dependencies) { + dependencyMap.put(dependency.shortName, dependency.version); + } + pluginInfo.put("dependencies", dependencyMap); + } else { + pluginInfo.put("dependencies", Collections.emptyMap()); + } + response.add(pluginInfo); + } + for (UpdateSite site : Jenkins.getActiveInstance().getUpdateCenter().getSiteList()) { + for (UpdateSite.Plugin plugin: site.getAvailables()) { + JSONObject pluginInfo = allPlugins.get(plugin.name); + if(pluginInfo == null) { + pluginInfo = new JSONObject(); + pluginInfo.put("installed", false); + } + pluginInfo.put("name", plugin.name); + pluginInfo.put("title", plugin.getDisplayName()); + pluginInfo.put("excerpt", plugin.excerpt); + pluginInfo.put("site", site.getId()); + pluginInfo.put("dependencies", plugin.dependencies); + response.add(pluginInfo); + } + } + return hudson.util.HttpResponses.okJSON(response); + } + public HttpResponse doUpdateSources(StaplerRequest req) throws IOException { Jenkins.getInstance().checkPermission(CONFIGURE_UPDATECENTER); @@ -804,43 +1037,141 @@ public HttpResponse doUpdateSources(StaplerRequest req) throws IOException { * Performs the installation of the plugins. */ public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - boolean dynamicLoad = req.getParameter("dynamicLoad")!=null; - final List> deployJobs = new ArrayList<>(); + Set plugins = new LinkedHashSet<>(); Enumeration en = req.getParameterNames(); while (en.hasMoreElements()) { String n = en.nextElement(); if(n.startsWith("plugin.")) { n = n.substring(7); - // JENKINS-22080 plugin names can contain '.' as could (according to rumour) update sites - int index = n.indexOf('.'); - UpdateSite.Plugin p = null; + plugins.add(n); + } + } + + boolean dynamicLoad = req.getParameter("dynamicLoad")!=null; + install(plugins, dynamicLoad); + + rsp.sendRedirect("../updateCenter/"); + } + + /** + * Installs a list of plugins from a JSON POST. + * @param req The request object. + * @return A JSON response that includes a "correlationId" in the "data" element. + * That "correlationId" can then be used in calls to + * {@link UpdateCenter#doInstallStatus(org.kohsuke.stapler.StaplerRequest)}. + * @throws IOException Error reading JSON payload fro request. + */ + @RequirePOST + @Restricted(DoNotUse.class) // WebOnly + public HttpResponse doInstallPlugins(StaplerRequest req) throws IOException { + String payload = IOUtils.toString(req.getInputStream(), req.getCharacterEncoding()); + JSONObject request = JSONObject.fromObject(payload); + JSONArray pluginListJSON = request.getJSONArray("plugins"); + List plugins = new ArrayList<>(); + + for (int i = 0; i < pluginListJSON.size(); i++) { + plugins.add(pluginListJSON.getString(i)); + } + + UUID correlationId = UUID.randomUUID(); + try { + boolean dynamicLoad = request.getBoolean("dynamicLoad"); + install(plugins, dynamicLoad, correlationId); + + JSONObject responseData = new JSONObject(); + responseData.put("correlationId", correlationId.toString()); + + return hudson.util.HttpResponses.okJSON(responseData); + } catch (Exception e) { + return hudson.util.HttpResponses.errorJSON(e.getMessage()); + } + } + + /** + * Performs the installation of the plugins. + * @param plugins The collection of plugins to install. + * @param dynamicLoad If true, the plugin will be dynamically loaded into this Jenkins. If false, + * the plugin will only take effect after the reboot. + * See {@link UpdateCenter#isRestartRequiredForCompletion()} + * @return The install job list. + * @since FIXME + */ + public List> install(@Nonnull Collection plugins, boolean dynamicLoad) { + return install(plugins, dynamicLoad, null); + } + + private List> install(@Nonnull Collection plugins, boolean dynamicLoad, @CheckForNull UUID correlationId) { + List> installJobs = new ArrayList<>(); + + for (String n : plugins) { + // JENKINS-22080 plugin names can contain '.' as could (according to rumour) update sites + int index = n.indexOf('.'); + UpdateSite.Plugin p = null; + + if (index == -1) { + p = getPlugin(n, UpdateCenter.ID_DEFAULT); + } else { while (index != -1) { if (index + 1 >= n.length()) { break; } String pluginName = n.substring(0, index); String siteName = n.substring(index + 1); - UpdateSite updateSite = Jenkins.getInstance().getUpdateCenter().getById(siteName); - if (updateSite == null) { - throw new Failure("No such update center: " + siteName); - } else { - UpdateSite.Plugin plugin = updateSite.getPlugin(pluginName); - if (plugin != null) { - if (p != null) { - throw new Failure("Ambiguous plugin: " + n); - } - p = plugin; + UpdateSite.Plugin plugin = getPlugin(pluginName, siteName); + // TODO: Someone that understands what the following logic is about, please add a comment. + if (plugin != null) { + if (p != null) { + throw new Failure("Ambiguous plugin: " + n); } + p = plugin; } index = n.indexOf('.', index + 1); } - if (p == null) { - throw new Failure("No such plugin: " + n); - } - - deployJobs.add(p.deploy(dynamicLoad)); } + if (p == null) { + throw new Failure("No such plugin: " + n); + } + Future jobFuture = p.deploy(dynamicLoad, correlationId); + installJobs.add(jobFuture); + } + + if (Jenkins.getActiveInstance().getInstallState() == InstallState.NEW) { + trackInitialPluginInstall(installJobs); + } + + return installJobs; + } + + private void trackInitialPluginInstall(@Nonnull final List> installJobs) { + final Jenkins jenkins = Jenkins.getActiveInstance(); + final UpdateCenter updateCenter = jenkins.getUpdateCenter(); + + updateCenter.persistInstallStatus(); + if (jenkins.getInstallState() == InstallState.NEW) { + jenkins.setInstallState(InstallState.INITIAL_PLUGINS_INSTALLING); + new Thread() { + @Override + public void run() { + INSTALLING: while (true) { + try { + updateCenter.persistInstallStatus(); + Thread.sleep(500); + for (Future jobFuture : installJobs) { + if(!jobFuture.isDone() && !jobFuture.isCancelled()) { + continue INSTALLING; + } + } + } catch (InterruptedException e) { + LOGGER.log(WARNING, "Unexpected error while waiting for initial plugin set to install.", e); + } + break; + } + updateCenter.persistInstallStatus(); + jenkins.setInstallState(InstallState.INITIAL_PLUGINS_INSTALLED); + InstallUtil.saveLastExecVersion(); + } + }.start(); } // Fire a one-off thread to wait for the plugins to be deployed and then @@ -849,7 +1180,7 @@ public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOExceptio @Override public void run() { INSTALLING: while (true) { - for (Future deployJob : deployJobs) { + for (Future deployJob : installJobs) { try { Thread.sleep(500); } catch (InterruptedException e) { @@ -868,9 +1199,15 @@ public void run() { } }.start(); - rsp.sendRedirect("../updateCenter/"); } + private UpdateSite.Plugin getPlugin(String pluginName, String siteName) { + UpdateSite updateSite = Jenkins.getInstance().getUpdateCenter().getById(siteName); + if (updateSite == null) { + throw new Failure("No such update center: " + siteName); + } + return updateSite.getPlugin(pluginName); + } /** * Bare-minimum configuration mechanism to change the update center. diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 60eabf68cf50..c30dba8ae21b 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -117,15 +117,6 @@ public class PluginWrapper implements Comparable, ModelObject { */ private final File disableFile; - /** - * Used to control the unpacking of the bundled plugin. - * If a pin file exists, Jenkins assumes that the user wants to pin down a particular version - * of a plugin, and will not try to overwrite it. Otherwise, it'll be overwritten - * by a bundled copy, to ensure consistency across upgrade/downgrade. - * @since 1.325 - */ - private final File pinFile; - /** * A .jpi file, an exploded plugin directory, or a .jpl file. */ @@ -259,7 +250,6 @@ public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL this.baseResourceURL = baseResourceURL; this.classLoader = classLoader; this.disableFile = disableFile; - this.pinFile = new File(archive.getPath() + ".pinned"); this.active = !disableFile.exists(); this.dependencies = dependencies; this.optionalDependencies = optionalDependencies; @@ -575,8 +565,9 @@ public boolean hasUpdate() { } @Exported + @Deprecated // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ public boolean isPinned() { - return pinFile.exists(); + return false; } /** @@ -638,16 +629,9 @@ public String getBackupVersion() { /** * Checks if this plugin is pinned and that's forcing us to use an older version than the bundled one. */ + @Deprecated // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ public boolean isPinningForcingOldVersion() { - if (!isPinned()) return false; - - Manifest bundled = Jenkins.getInstance().pluginManager.getBundledPluginManifest(getShortName()); - if (bundled==null) return false; - - VersionNumber you = new VersionNumber(getVersionOf(bundled)); - VersionNumber me = getVersionNumber(); - - return me.isOlderThan(you); + return false; } // @@ -670,16 +654,18 @@ public HttpResponse doMakeDisabled() throws IOException { } @RequirePOST + @Deprecated public HttpResponse doPin() throws IOException { - Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); - new FileOutputStream(pinFile).close(); + // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ + LOGGER.log(WARNING, "Call to pin plugin has been ignored. Plugin name: " + shortName); return HttpResponses.ok(); } @RequirePOST + @Deprecated public HttpResponse doUnpin() throws IOException { - Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); - pinFile.delete(); + // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ + LOGGER.log(WARNING, "Call to unpin plugin has been ignored. Plugin name: " + shortName); return HttpResponses.ok(); } diff --git a/core/src/main/java/hudson/init/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java index 7b8497405d4e..9012b4c65a3d 100644 --- a/core/src/main/java/hudson/init/InitStrategy.java +++ b/core/src/main/java/hudson/init/InitStrategy.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; @@ -50,15 +49,6 @@ public List listPluginArchives(PluginManager pm) throws IOException { // for example, while doing "mvn jpi:run" or "mvn hpi:run" on a plugin that's bundled with Jenkins, we want to the // *.jpl file to override the bundled jpi/hpi file. getBundledPluginsFromProperty(r); - Iterator it = r.iterator(); - while (it.hasNext()) { - File f = it.next(); - if (new File(pm.rootDir, f.getName().replace(".hpi", ".jpi") + ".pinned").isFile()) { - // Cf. PluginManager.copyBundledPlugin, which is not called in this case. - LOGGER.log(Level.INFO, "ignoring {0} since this plugin is pinned", f); - it.remove(); - } - } // similarly, we prefer *.jpi over *.hpi listPluginFiles(pm, ".jpl", r); // linked plugin. for debugging. diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index b5fde8aac6c0..a8e9122e7c9f 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -33,6 +33,9 @@ import hudson.Util; import hudson.XmlFile; import static hudson.init.InitMilestone.PLUGINS_STARTED; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + import hudson.init.Initializer; import hudson.lifecycle.Lifecycle; import hudson.lifecycle.RestartNotSupportedException; @@ -50,14 +53,18 @@ import hudson.util.PersistedList; import hudson.util.XStream2; import jenkins.RestartRequiredException; +import jenkins.install.InstallState; +import jenkins.install.InstallUtil; import jenkins.model.Jenkins; import jenkins.util.io.OnMaster; +import net.sf.json.JSONArray; import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContext; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.io.output.NullOutputStream; import org.jvnet.localizer.Localizable; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -86,7 +93,9 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.UUID; import java.util.Vector; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -170,10 +179,44 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas private boolean requiresRestart; + /** + * Simple connection status enum. + */ + @Restricted(NoExternalUse.class) + static enum ConnectionStatus { + /** + * Connection status has not started yet. + */ + PRECHECK, + /** + * Connection status is being checked at this time. + */ + CHECKING, + /** + * Connection status was not checked. + */ + UNCHECKED, + /** + * Connection is ok. + */ + OK, + /** + * Connection status check failed. + */ + FAILED; + + static final String INTERNET = "internet"; + static final String UPDATE_SITE = "updatesite"; + } + public UpdateCenter() { configure(new UpdateCenterConfiguration()); } + UpdateCenter(@Nonnull UpdateCenterConfiguration configuration) { + configure(configuration); + } + public Api getApi() { return new Api(this); } @@ -236,6 +279,133 @@ public InstallationJob getJob(Plugin plugin) { return null; } + /** + * Get the current connection status. + *

+ * Supports a "siteId" request parameter, defaulting to "default" for the default + * update site. + * + * @return The current connection status. + */ + @Restricted(DoNotUse.class) + public HttpResponse doConnectionStatus(StaplerRequest request) { + try { + String siteId = request.getParameter("siteId"); + if (siteId == null) { + siteId = ID_DEFAULT; + } + ConnectionCheckJob checkJob = getConnectionCheckJob(siteId); + if (checkJob == null) { + UpdateSite site = getSite(siteId); + if (site != null) { + checkJob = addConnectionCheckJob(site); + } + } + if (checkJob != null) { + return HttpResponses.okJSON(checkJob.connectionStates); + } else { + return HttpResponses.errorJSON(String.format("Unknown site '%s'.", siteId)); + } + } catch (Exception e) { + return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage())); + } + } + + /** + * 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(); + } + return HttpResponses.okJSON(jobs); + } catch (Exception e) { + return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage())); + } + } + + /** + * Called to persist the currently installing plugin states. This allows + * us to support install resume if Jenkins is restarted while plugins are + * being installed. + */ + public synchronized void persistInstallStatus() { + List jobs = getJobs(); + + boolean activeInstalls = false; + for (UpdateCenterJob job : jobs) { + if (job instanceof InstallationJob) { + InstallationJob installationJob = (InstallationJob) job; + if(!installationJob.status.isSuccess()) { + activeInstalls = true; + } + } + } + + if(activeInstalls) { + InstallUtil.persistInstallStatus(jobs); // save this info + } + else { + InstallUtil.clearInstallStatus(); // clear this info + } + } + + /** + * Get the current installation status of a plugin set. + *

+ * Supports a "correlationId" request parameter if you only want to get the + * install status of a set of plugins requested for install through + * {@link PluginManager#doInstallPlugins(org.kohsuke.stapler.StaplerRequest)}. + * + * @return The current installation status of a plugin set. + */ + @Restricted(DoNotUse.class) + public HttpResponse doInstallStatus(StaplerRequest request) { + try { + String correlationId = request.getParameter("correlationId"); + List> installStates = new ArrayList<>(); + List jobCopy = getJobs(); + + for (UpdateCenterJob job : jobCopy) { + if (job instanceof InstallationJob) { + UUID jobCorrelationId = job.getCorrelationId(); + if (correlationId == null || (jobCorrelationId != null && correlationId.equals(jobCorrelationId.toString()))) { + InstallationJob installationJob = (InstallationJob) job; + Map pluginInfo = new LinkedHashMap<>(); + pluginInfo.put("name", installationJob.plugin.name); + pluginInfo.put("version", installationJob.plugin.version); + pluginInfo.put("title", installationJob.plugin.title); + pluginInfo.put("installStatus", installationJob.status.getType()); + pluginInfo.put("requiresRestart", Boolean.toString(installationJob.status.requiresRestart())); + if (jobCorrelationId != null) { + pluginInfo.put("correlationId", jobCorrelationId.toString()); + } + installStates.add(pluginInfo); + } + } + } + return HttpResponses.okJSON(JSONArray.fromObject(installStates)); + } catch (Exception e) { + return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage())); + } + } + /** * Returns latest Jenkins upgrade job. * @return HudsonUpgradeJob or null if not found @@ -497,12 +667,60 @@ public String getBackupVersion() { } /*package*/ synchronized Future addJob(UpdateCenterJob job) { - // the first job is always the connectivity check - if (sourcesUsed.add(job.site)) - new ConnectionCheckJob(job.site).submit(); + addConnectionCheckJob(job.site); return job.submit(); } + private @Nonnull ConnectionCheckJob addConnectionCheckJob(@Nonnull UpdateSite site) { + // Create a connection check job if the site was not already in the sourcesUsed set i.e. the first + // job (in the jobs list) relating to a site must be the connection check job. + if (sourcesUsed.add(site)) { + ConnectionCheckJob connectionCheckJob = newConnectionCheckJob(site); + connectionCheckJob.submit(); + return connectionCheckJob; + } else { + // Find the existing connection check job for that site and return it. + ConnectionCheckJob connectionCheckJob = getConnectionCheckJob(site); + if (connectionCheckJob != null) { + return connectionCheckJob; + } else { + throw new IllegalStateException("Illegal addition of an UpdateCenter job without calling UpdateCenter.addJob. " + + "No ConnectionCheckJob found for the site."); + } + } + } + + /** + * Create a {@link ConnectionCheckJob} for the specified update site. + *

+ * Does not start/submit the job. + * @param site The site for which the Job is to be created. + * @return A {@link ConnectionCheckJob} for the specified update site. + */ + @Restricted(NoExternalUse.class) + ConnectionCheckJob newConnectionCheckJob(UpdateSite site) { + return new ConnectionCheckJob(site); + } + + private @CheckForNull ConnectionCheckJob getConnectionCheckJob(@Nonnull String siteId) { + UpdateSite site = getSite(siteId); + if (site == null) { + return null; + } + return getConnectionCheckJob(site); + } + + private @CheckForNull ConnectionCheckJob getConnectionCheckJob(@Nonnull UpdateSite site) { + synchronized (jobs) { + for (UpdateCenterJob job : jobs) { + if (job instanceof ConnectionCheckJob && job.site.getId().equals(site.getId())) { + return (ConnectionCheckJob) job; + } + } + } + return null; + } + public String getDisplayName() { return "Update center"; } @@ -558,6 +776,7 @@ private XmlFile getConfigFile() { UpdateCenter.class.getName()+".xml")); } + @Exported public List getAvailables() { Map pluginMap = new LinkedHashMap(); for (UpdateSite site : sites) { @@ -931,6 +1150,12 @@ public abstract class UpdateCenterJob implements Runnable { */ public final UpdateSite site; + /** + * Simple correlation ID that can be used to associated a batch of jobs e.g. the + * installation of a set of plugins. + */ + private UUID correlationId = null; + /** * If this job fails, set to the error. */ @@ -944,6 +1169,17 @@ public Api getApi() { return new Api(this); } + public UUID getCorrelationId() { + return correlationId; + } + + public void setCorrelationId(UUID correlationId) { + if (this.correlationId != null) { + throw new IllegalStateException("Illegal call to set the 'correlationId'. Already set."); + } + this.correlationId = correlationId; + } + /** * @deprecated as of 1.326 * Use {@link #submit()} instead. @@ -965,6 +1201,8 @@ public String getType() { */ public Future submit() { LOGGER.fine("Scheduling "+this+" to installerService"); + // TODO: seems like this access to jobs should be synchronized, no? + // It might get synch'd accidentally via the addJob method, but that wouldn't be good. jobs.add(this); return installerService.submit(this,this); } @@ -1051,11 +1289,17 @@ public class Canceled extends RestartJenkinsJobStatus { public final class ConnectionCheckJob extends UpdateCenterJob { private final Vector statuses= new Vector(); + final Map connectionStates = new ConcurrentHashMap<>(); + public ConnectionCheckJob(UpdateSite site) { super(site); + connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.PRECHECK); + connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.PRECHECK); } public void run() { + connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.UNCHECKED); + connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.UNCHECKED); if (ID_UPLOAD.equals(site.getId())) { return; } @@ -1063,27 +1307,35 @@ public void run() { try { String connectionCheckUrl = site.getConnectionCheckUrl(); if (connectionCheckUrl!=null) { + connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.CHECKING); statuses.add(Messages.UpdateCenter_Status_CheckingInternet()); try { config.checkConnection(this, connectionCheckUrl); - } catch (IOException e) { + } catch (Exception e) { if(e.getMessage().contains("Connection timed out")) { // Google can't be down, so this is probably a proxy issue + connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.FAILED); statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl)); return; } } + connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.OK); } + connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.CHECKING); statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet()); + config.checkUpdateCenter(this, site.getUrl()); + connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.OK); statuses.add(Messages.UpdateCenter_Status_Success()); } catch (UnknownHostException e) { + connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.FAILED); statuses.add(Messages.UpdateCenter_Status_UnknownHostException(e.getMessage())); addStatus(e); error = e; - } catch (IOException e) { + } catch (Exception e) { + connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.FAILED); statuses.add(Functions.printThrowable(e)); error = e; } @@ -1098,6 +1350,8 @@ public String[] getStatuses() { return statuses.toArray(new String[statuses.size()]); } } + + } /** @@ -1621,6 +1875,21 @@ public static class PageDecoratorImpl extends PageDecorator { @Initializer(after=PLUGINS_STARTED, fatal=false) public static void init(Jenkins h) throws IOException { h.getUpdateCenter().load(); + if (Jenkins.getActiveInstance().getInstallState() == InstallState.NEW) { + LOGGER.log(INFO, "This is a new Jenkins instance. The Plugin Install Wizard will be launched."); + // Force update of the default site file (updates/default.json). + updateDefaultSite(); + } + } + + private static void updateDefaultSite() { + try { + // Need to do the following because the plugin manager will attempt to access + // $JENKINS_HOME/updates/default.json. Needs to be up to date. + Jenkins.getActiveInstance().getUpdateCenter().getSite(UpdateCenter.ID_DEFAULT).updateDirectlyNow(true); + } catch (Exception e) { + LOGGER.log(WARNING, "Upgrading Jenkins. Failed to update default UpdateSite. Plugin upgrades may fail.", e); + } } /** diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 061091bb6719..09c38fb6e769 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -49,6 +49,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.logging.Level; @@ -814,6 +815,23 @@ public Future deploy() { * See {@link UpdateCenter#isRestartRequiredForCompletion()} */ public Future deploy(boolean dynamicLoad) { + return deploy(dynamicLoad, null); + } + + /** + * Schedules the installation of this plugin. + * + *

+ * This is mainly intended to be called from the UI. The actual installation work happens + * asynchronously in another thread. + * + * @param dynamicLoad + * If true, the plugin will be dynamically loaded into this Jenkins. If false, + * the plugin will only take effect after the reboot. + * See {@link UpdateCenter#isRestartRequiredForCompletion()} + * @param correlationId A correlation ID to be set on the job. + */ + public Future deploy(boolean dynamicLoad, @CheckForNull UUID correlationId) { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); UpdateCenter uc = Jenkins.getInstance().getUpdateCenter(); for (Plugin dep : getNeededDependencies()) { @@ -825,7 +843,9 @@ public Future deploy(boolean dynamicLoad) { LOGGER.log(Level.WARNING, "Dependent install of " + dep.name + " for plugin " + name + " already added, skipping"); } } - return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication(), dynamicLoad)); + UpdateCenter.InstallationJob job = uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication(), dynamicLoad); + job.setCorrelationId(correlationId); + return uc.addJob(job); } /** diff --git a/core/src/main/java/hudson/util/HttpResponses.java b/core/src/main/java/hudson/util/HttpResponses.java index 16755adc4c98..1fb7cf6084cf 100644 --- a/core/src/main/java/hudson/util/HttpResponses.java +++ b/core/src/main/java/hudson/util/HttpResponses.java @@ -23,10 +23,18 @@ */ package hudson.util; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import javax.annotation.Nonnull; +import javax.servlet.ServletException; import java.io.File; import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Map; /** * Various {@link HttpResponse} implementations. @@ -40,4 +48,126 @@ public class HttpResponses extends org.kohsuke.stapler.HttpResponses { public static HttpResponse staticResource(File f) throws IOException { return staticResource(f.toURI().toURL()); } + + /** + * Create an empty "ok" response. + */ + public static HttpResponse okJSON() { + return new JSONObjectResponse(); + } + + /** + * Create a response containing the supplied "data". + * @param data The data. + */ + public static HttpResponse okJSON(@Nonnull JSONObject data) { + return new JSONObjectResponse(data); + } + + /** + * Create a response containing the supplied "data". + * @param data The data. + */ + public static HttpResponse okJSON(@Nonnull JSONArray data) { + return new JSONObjectResponse(data); + } + + /** + * Create a response containing the supplied "data". + * @param data The data. + */ + public static HttpResponse okJSON(@Nonnull Map data) { + return new JSONObjectResponse(data); + } + + /** + * Set the response as an error response. + * @param message The error "message" set on the response. + * @return {@link this} object. + */ + public static HttpResponse errorJSON(@Nonnull String message) { + return new JSONObjectResponse().error(message); + } + + /** + * {@link net.sf.json.JSONObject} response. + * + * @author tom.fennelly@gmail.com + */ + static class JSONObjectResponse implements HttpResponse { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private final JSONObject jsonObject; + + /** + * Create an empty "ok" response. + */ + JSONObjectResponse() { + this.jsonObject = new JSONObject(); + status("ok"); + } + + /** + * Create a response containing the supplied "data". + * @param data The data. + */ + JSONObjectResponse(@Nonnull JSONObject data) { + this(); + this.jsonObject.put("data", data); + } + + /** + * Create a response containing the supplied "data". + * @param data The data. + */ + JSONObjectResponse(@Nonnull JSONArray data) { + this(); + this.jsonObject.put("data", data); + } + + /** + * Create a response containing the supplied "data". + * @param data The data. + */ + JSONObjectResponse(@Nonnull Map data) { + this(); + this.jsonObject.put("data", JSONObject.fromObject(data)); + } + + /** + * Set the response as an error response. + * @param message The error "message" set on the response. + * @return {@link this} object. + */ + @Nonnull JSONObjectResponse error(@Nonnull String message) { + status("error"); + this.jsonObject.put("message", message); + return this; + } + + /** + * Get the JSON response object. + * @return The JSON response object. + */ + @Nonnull JSONObject getJsonObject() { + return jsonObject; + } + + private @Nonnull JSONObjectResponse status(@Nonnull String status) { + this.jsonObject.put("status", status); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { + byte[] bytes = jsonObject.toString().getBytes(UTF8); + rsp.setContentType("application/json; charset=UTF-8"); + rsp.setContentLength(bytes.length); + rsp.getOutputStream().write(bytes); + } + } } diff --git a/core/src/main/java/jenkins/I18n.java b/core/src/main/java/jenkins/I18n.java new file mode 100644 index 000000000000..299b7328e335 --- /dev/null +++ b/core/src/main/java/jenkins/I18n.java @@ -0,0 +1,113 @@ +/* + * 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 hudson.Extension; +import hudson.model.RootAction; +import hudson.util.HttpResponses; +import jenkins.util.ResourceBundleUtil; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; + +import java.util.Locale; + +/** + * Internationalization REST (ish) API. + * @author tom.fennelly@gmail.com + * @since FIXME + */ +@Extension +@Restricted(NoExternalUse.class) +public class I18n implements RootAction { + + /** + * {@inheritDoc} + */ + @Override + public String getIconFileName() { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public String getUrlName() { + return "i18n"; + } + + /** + * Get a localised resource bundle. + *

+ * URL: {@code i18n/resourceBundle}. + *

+ * Parameters: + *

    + *
  • {@code baseName}: The resource bundle base name.
  • + *
  • {@code language}: {@link Locale} Language. (optional)
  • + *
  • {@code country}: {@link Locale} Country. (optional)
  • + *
  • {@code variant}: {@link Locale} Language variant. (optional)
  • + *
+ * + * @param request The request. + * @return The JSON response. + */ + public HttpResponse doResourceBundle(StaplerRequest request) { + String baseName = request.getParameter("baseName"); + + if (baseName == null) { + return HttpResponses.errorJSON("Mandatory parameter 'baseName' not specified."); + } + + String language = request.getParameter("language"); + String country = request.getParameter("country"); + String variant = request.getParameter("variant"); + + try { + Locale locale = request.getLocale(); + + if (language != null && country != null && variant != null) { + locale = new Locale(language, country, variant); + } else if (language != null && country != null) { + locale = new Locale(language, country); + } else if (language != null) { + locale = new Locale(language); + } + + return HttpResponses.okJSON(ResourceBundleUtil.getBundle(baseName, locale)); + } catch (Exception e) { + return HttpResponses.errorJSON(e.getMessage()); + } + } +} diff --git a/core/src/main/java/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor.java b/core/src/main/java/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor.java deleted file mode 100644 index 0fb30e10fbaf..000000000000 --- a/core/src/main/java/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor.java +++ /dev/null @@ -1,44 +0,0 @@ -package jenkins.diagnostics; - -import com.google.common.collect.ImmutableList; -import hudson.Extension; -import hudson.PluginWrapper; -import hudson.model.AdministrativeMonitor; -import jenkins.model.Jenkins; - -import javax.inject.Inject; -import java.util.ArrayList; -import java.util.List; - -/** - * Fires off when we have any pinned plugins that's blocking upgrade from the bundled version. - * - * @author Kohsuke Kawaguchi - */ -@Extension -public class PinningIsBlockingBundledPluginMonitor extends AdministrativeMonitor { - @Inject - Jenkins jenkins; - - private List offenders; - - @Override - public boolean isActivated() { - return !getOffenders().isEmpty(); - } - - private void compute() { - List offenders = new ArrayList(); - for (PluginWrapper p : jenkins.pluginManager.getPlugins()) { - if (p.isPinningForcingOldVersion()) - offenders.add(p); - } - this.offenders = ImmutableList.copyOf(offenders); - } - - public List getOffenders() { - if (offenders==null) - compute(); - return offenders; - } -} diff --git a/core/src/main/java/jenkins/install/InstallState.java b/core/src/main/java/jenkins/install/InstallState.java new file mode 100644 index 000000000000..510feb6c78a1 --- /dev/null +++ b/core/src/main/java/jenkins/install/InstallState.java @@ -0,0 +1,65 @@ +/* + * 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 org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Jenkins install state. + * + * @author tom.fennelly@gmail.com + */ +@Restricted(NoExternalUse.class) +public enum InstallState { + /** + * New Jenkins install. + */ + NEW, + /** + * New Jenkins install. The user has kicked off the process of installing an + * initial set of plugins (via the install wizard). + */ + INITIAL_PLUGINS_INSTALLING, + /** + * New Jenkins install. The initial set of plugins are now installed. + */ + INITIAL_PLUGINS_INSTALLED, + /** + * Restart of an existing Jenkins install. + */ + RESTART, + /** + * Upgrade of an existing Jenkins install. + */ + UPGRADE, + /** + * Downgrade of an existing Jenkins install. + */ + DOWNGRADE, + /** + * Jenkins started in test mode (JenkinsRule). + */ + TEST +} diff --git a/core/src/main/java/jenkins/install/InstallUtil.java b/core/src/main/java/jenkins/install/InstallUtil.java new file mode 100644 index 000000000000..e9622b1a9ac6 --- /dev/null +++ b/core/src/main/java/jenkins/install/InstallUtil.java @@ -0,0 +1,230 @@ +/* + * 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 static java.util.logging.Level.SEVERE; +import static java.util.logging.Level.WARNING; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import org.apache.commons.io.FileUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import com.thoughtworks.xstream.XStream; + +import hudson.Functions; +import hudson.model.UpdateCenter.DownloadJob.InstallationStatus; +import hudson.model.UpdateCenter.DownloadJob.Installing; +import hudson.model.UpdateCenter.InstallationJob; +import hudson.model.UpdateCenter.UpdateCenterJob; +import hudson.util.VersionNumber; +import jenkins.model.Jenkins; +import jenkins.util.xml.XMLUtils; + +/** + * Jenkins install utilities. + * + * @author tom.fennelly@gmail.com + */ +@Restricted(NoExternalUse.class) +public class InstallUtil { + + private static final Logger LOGGER = Logger.getLogger(InstallUtil.class.getName()); + + private static final VersionNumber NEW_INSTALL_VERSION = new VersionNumber("1.0"); + + /** + * Get the current installation state. + * @return The type of "startup" currently under way in Jenkins. + */ + public static InstallState getInstallState() { + if (Functions.getIsUnitTest()) { + return InstallState.TEST; + } + + VersionNumber lastRunVersion = new VersionNumber(getLastExecVersion()); + + // Neither the top level config or the lastExecVersionFile have a version + // stored in them, which means it's a new install. + if (lastRunVersion.compareTo(NEW_INSTALL_VERSION) == 0) { + return InstallState.NEW; + } + + // We have a last version. + + VersionNumber currentRunVersion = new VersionNumber(getCurrentExecVersion()); + if (lastRunVersion.isOlderThan(currentRunVersion)) { + return InstallState.UPGRADE; + } else if (lastRunVersion.isNewerThan(currentRunVersion)) { + return InstallState.DOWNGRADE; + } else { + // Last running version was the same as "this" running version. + return InstallState.RESTART; + } + } + + /** + * Save the current Jenkins instance version as the last executed version. + *

+ * This state information is required in order to determine whether or not the Jenkins instance + * is just restarting, or is being upgraded from an earlier version. + */ + public static void saveLastExecVersion() { + if (Jenkins.VERSION.equals(Jenkins.UNCOMPUTED_VERSION)) { + // This should never happen!! Only adding this check in case someone moves the call to this method to the wrong place. + throw new IllegalStateException("Unexpected call to InstallUtil.saveLastExecVersion(). Jenkins.VERSION has not been initialized. Call computeVersion() first."); + } + saveLastExecVersion(Jenkins.VERSION); + } + + /** + * Get the last saved Jenkins instance version. + * @return The last saved Jenkins instance version. + * @see #saveLastExecVersion() + */ + public static @Nonnull String getLastExecVersion() { + File lastExecVersionFile = getLastExecVersionFile(); + if (lastExecVersionFile.exists()) { + try { + return FileUtils.readFileToString(lastExecVersionFile); + } catch (IOException e) { + LOGGER.log(SEVERE, "Unexpected Error. Unable to read " + lastExecVersionFile.getAbsolutePath(), e); + LOGGER.log(WARNING, "Unable to determine the last running version (see error above). Treating this as a restart. No plugins will be updated."); + return getCurrentExecVersion(); + } + } else { + // Backward compatibility. Use the last version stored in the top level config.xml. + // Going to read the value directly from the config.xml file Vs hoping that the + // Jenkins startup sequence has moved far enough along that it has loaded the + // global config. It can't load the global config until well into the startup + // sequence because the unmarshal requires numerous objects to be created e.g. + // it requires the Plugin Manager. It happens too late and it's too risky to + // change how it currently works. + File configFile = getConfigFile(); + if (configFile.exists()) { + try { + String lastVersion = XMLUtils.getValue("/hudson/version", configFile); + if (lastVersion.length() > 0) { + return lastVersion; + } + } catch (Exception e) { + LOGGER.log(SEVERE, "Unexpected error reading global config.xml", e); + } + } + return NEW_INSTALL_VERSION.toString(); + } + } + + /** + * Save a specific version as the last execute version. + * @param version The version to save. + */ + static void saveLastExecVersion(@Nonnull String version) { + File lastExecVersionFile = getLastExecVersionFile(); + try { + FileUtils.write(lastExecVersionFile, version); + } catch (IOException e) { + LOGGER.log(SEVERE, "Failed to save " + lastExecVersionFile.getAbsolutePath(), e); + } + } + + static File getConfigFile() { + return new File(Jenkins.getActiveInstance().getRootDir(), "config.xml"); + } + + static File getLastExecVersionFile() { + return new File(Jenkins.getActiveInstance().getRootDir(), ".last_exec_version"); + } + + static File getInstallingPluginsFile() { + return new File(Jenkins.getActiveInstance().getRootDir(), ".installing_plugins"); + } + + private static String getCurrentExecVersion() { + if (Jenkins.VERSION.equals(Jenkins.UNCOMPUTED_VERSION)) { + // This should never happen!! Only adding this check in case someone moves the call to this method to the wrong place. + throw new IllegalStateException("Unexpected call to InstallUtil.getCurrentExecVersion(). Jenkins.VERSION has not been initialized. Call computeVersion() first."); + } + return Jenkins.VERSION; + } + + /** + * Returns a list of any plugins that are persisted in the installing list + */ + @SuppressWarnings("unchecked") + public static synchronized @CheckForNull Map getPersistedInstallStatus() { + File installingPluginsFile = getInstallingPluginsFile(); + if(installingPluginsFile == null || !installingPluginsFile.exists()) { + return null; + } + return (Map)new XStream().fromXML(installingPluginsFile); + } + + /** + * Persists a list of installing plugins; this is used in the case Jenkins fails mid-installation and needs to be restarted + * @param installingPlugins + */ + public static synchronized void persistInstallStatus(List installingPlugins) { + File installingPluginsFile = getInstallingPluginsFile(); + if(installingPlugins == null || installingPlugins.isEmpty()) { + installingPluginsFile.delete(); + return; + } + LOGGER.fine("Writing install state to: " + installingPluginsFile.getAbsolutePath()); + Map statuses = new HashMap(); + for(UpdateCenterJob j : installingPlugins) { + if(j instanceof InstallationJob && j.getCorrelationId() != null) { // only include install jobs with a correlation id (directly selected) + InstallationJob ij = (InstallationJob)j; + InstallationStatus status = ij.status; + String statusText = status.getType(); + if(status instanceof Installing) { // flag currently installing plugins as pending + statusText = "Pending"; + } + statuses.put(ij.plugin.name, statusText); + } + } + try { + String installingPluginXml = new XStream().toXML(statuses); + FileUtils.write(installingPluginsFile, installingPluginXml); + } catch (IOException e) { + LOGGER.log(SEVERE, "Failed to save " + installingPluginsFile.getAbsolutePath(), e); + } + } + + /** + * Call to remove any active install status + */ + public static void clearInstallStatus() { + persistInstallStatus(null); + } +} diff --git a/core/src/main/java/jenkins/model/AssetManager.java b/core/src/main/java/jenkins/model/AssetManager.java new file mode 100644 index 000000000000..d886ab70fd93 --- /dev/null +++ b/core/src/main/java/jenkins/model/AssetManager.java @@ -0,0 +1,112 @@ +package jenkins.model; + +import hudson.Extension; +import hudson.model.UnprotectedRootAction; +import hudson.util.TimeUnit2; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Enumeration; + +/** + * Serves files located in the {@code /assets} classpath directory via the Jenkins core ClassLoader. + * e.g. the URL {@code /assets/jquery-detached/jsmodules/jquery2.js} will load {@code jquery-detached/jsmodules/jquery2.js} + * resource from the classpath below {@code /assets}. + * + * @author Kohsuke Kawaguchi + */ +@Extension +public class AssetManager implements UnprotectedRootAction { + + // not shown in the UI + @Override + public String getIconFileName() { + return null; + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return "assets"; + } + + /** + * Exposes assets in the core classloader over HTTP. + */ + public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + String path = req.getRestOfPath(); + URL resource = findResource(path); + + if (resource == null) { + rsp.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // Stapler routes requests like the "/static/.../foo/bar/zot" to be treated like "/foo/bar/zot" + // and this is used to serve long expiration header, by using Jenkins.VERSION_HASH as "..." + // to create unique URLs. Recognize that and set a long expiration header. + String requestPath = req.getRequestURI().substring(req.getContextPath().length()); + boolean staticLink = requestPath.startsWith("/static/"); + long expires = staticLink ? TimeUnit2.DAYS.toMillis(365) : -1; + + // use serveLocalizedFile to support automatic locale selection + rsp.serveLocalizedFile(req, resource, expires); + } + + /** + * Locates the asset from the classloader. + * + *

+ * To allow plugins to bring its own assets without worrying about colliding with the assets in core, + * look for child classloader first. But to support plugins that get split, if the child classloader + * doesn't find it, fall back to the parent classloader. + */ + private URL findResource(String path) throws IOException { + try { + if (path.contains("..")) // crude avoidance of directory traversal attack + throw new IllegalArgumentException(path); + + String name; + if (path.charAt(0) == '/') { + name = "assets" + path; + } else { + name = "assets/" + path; + } + + ClassLoader cl = Jenkins.class.getClassLoader(); + URL url = (URL) $findResource.invoke(cl, name); + if (url==null) { + // pick the last one, which is the one closest to the leaf of the classloader tree. + Enumeration e = cl.getResources(name); + while (e.hasMoreElements()) { + url = e.nextElement(); + } + } + return url; + } catch (InvocationTargetException|IllegalAccessException e) { + throw new Error(e); + } + } + + private static final Method $findResource = init(); + + private static Method init() { + try { + Method m = ClassLoader.class.getDeclaredMethod("findResource", String.class); + m.setAccessible(true); + return m; + } catch (NoSuchMethodException e) { + throw (Error)new NoSuchMethodError().initCause(e); + } + } +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 7fcf37c7ca3b..081b9e4b24ab 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -192,6 +192,8 @@ import jenkins.ExtensionComponentSet; import jenkins.ExtensionRefreshException; import jenkins.InitReactorRunner; +import jenkins.install.InstallState; +import jenkins.install.InstallUtil; import jenkins.model.ProjectNamingStrategy.DefaultProjectNamingStrategy; import jenkins.security.ConfidentialKey; import jenkins.security.ConfidentialStore; @@ -328,6 +330,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve // this field needs to be at the very top so that other components can look at this value even during unmarshalling private String version = "1.0"; + /** + * The Jenkins instance startup type i.e. NEW, UPGRADE etc + */ + private InstallState installState; + /** * Number of executors of the master node. */ @@ -762,6 +769,11 @@ protected Jenkins(File root, ServletContext context, PluginManager pluginManager throw new IllegalStateException("second instance"); theInstance = this; + installState = InstallUtil.getInstallState(); + 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 slaves workspaceDir = "${JENKINS_HOME}/workspace/${ITEM_FULLNAME}"; @@ -896,6 +908,32 @@ private Object readResolve() { } return this; } + + /** + * Get the Jenkins {@link jenkins.install.InstallState install state}. + * @return The Jenkins {@link jenkins.install.InstallState install state}. + */ + @Restricted(NoExternalUse.class) + public InstallState getInstallState() { + return installState; + } + + /** + * Update the current install state. + */ + @Restricted(NoExternalUse.class) + public void setInstallState(@Nonnull InstallState newState) { + installState = newState; + } + + /** + * Get the URL path to the Install Wizard JavaScript. + * @return The URL path to the Install Wizard JavaScript. + */ + @Restricted(NoExternalUse.class) + public String getInstallWizardPath() { + return servletContext.getInitParameter("install-wizard-path"); + } /** * Executes a reactor. @@ -2671,6 +2709,19 @@ public Computer createComputer() { return new Hudson.MasterComputer(); } + private void loadConfig() throws IOException { + XmlFile cfg = getConfigFile(); + if (cfg.exists()) { + // reset some data that may not exist in the disk file + // so that we can take a proper compensation action later. + primaryView = null; + views.clear(); + + // load from disk + cfg.unmarshal(Jenkins.this); + } + } + private synchronized TaskBuilder loadTasks() throws IOException { File projectsDir = new File(root,"jobs"); if(!projectsDir.getCanonicalFile().isDirectory() && !projectsDir.mkdirs()) { @@ -2685,17 +2736,7 @@ private synchronized TaskBuilder loadTasks() throws IOException { TaskGraphBuilder g = new TaskGraphBuilder(); Handle loadJenkins = g.requires(EXTENSIONS_AUGMENTED).attains(JOB_LOADED).add("Loading global config", new Executable() { public void run(Reactor session) throws Exception { - XmlFile cfg = getConfigFile(); - if (cfg.exists()) { - // reset some data that may not exist in the disk file - // so that we can take a proper compensation action later. - primaryView = null; - views.clear(); - - // load from disk - cfg.unmarshal(Jenkins.this); - } - + loadConfig(); // if we are loading old data that doesn't have this field if (slaves != null && !slaves.isEmpty() && nodes.isLegacy()) { nodes.setNodes(slaves); @@ -4160,14 +4201,14 @@ private static void computeVersion(ServletContext context) { IOUtils.closeQuietly(is); } String ver = props.getProperty("version"); - if(ver==null) ver="?"; + if(ver==null) ver = UNCOMPUTED_VERSION; VERSION = ver; context.setAttribute("version",ver); 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(UNCOMPUTED_VERSION) || Boolean.getBoolean("hudson.script.noCache")) RESOURCE_PATH = ""; else RESOURCE_PATH = "/static/"+SESSION_HASH; @@ -4175,24 +4216,55 @@ private static void computeVersion(ServletContext context) { VIEW_RESOURCE_PATH = "/resources/"+ SESSION_HASH; } + /** + * The version number before it is "computed" (by a call to computeVersion()). + * @since FIXME + */ + public static final String UNCOMPUTED_VERSION = "?"; + /** * Version number of this Jenkins. */ - public static String VERSION="?"; + public static String VERSION = UNCOMPUTED_VERSION; /** * Parses {@link #VERSION} into {@link VersionNumber}, or null if it's not parseable as a version number * (such as when Jenkins is run with "mvn hudson-dev:run") */ - public static VersionNumber getVersion() { + public @CheckForNull static VersionNumber getVersion() { + return toVersion(VERSION); + } + + /** + * Get the stored version of Jenkins, as stored by + * {@link #doConfigSubmit(org.kohsuke.stapler.StaplerRequest, org.kohsuke.stapler.StaplerResponse)}. + *

+ * Parses the version into {@link VersionNumber}, or null if it's not parseable as a version number + * (such as when Jenkins is run with "mvn hudson-dev:run") + * @since FIXME + */ + public @CheckForNull static VersionNumber getStoredVersion() { + return toVersion(Jenkins.getActiveInstance().version); + } + + /** + * Parses a version string into {@link VersionNumber}, or null if it's not parseable as a version number + * (such as when Jenkins is run with "mvn hudson-dev:run") + */ + private static @CheckForNull VersionNumber toVersion(@CheckForNull String versionString) { + if (versionString == null) { + return null; + } + try { - return new VersionNumber(VERSION); + return new VersionNumber(versionString); } catch (NumberFormatException e) { try { // for non-released version of Jenkins, this looks like "1.345 (private-foobar), so try to approximate. - int idx = VERSION.indexOf(' '); - if (idx>0) - return new VersionNumber(VERSION.substring(0,idx)); + int idx = versionString.indexOf(' '); + if (idx > 0) { + return new VersionNumber(versionString.substring(0,idx)); + } } catch (NumberFormatException _) { // fall through } diff --git a/core/src/main/java/jenkins/util/ResourceBundleUtil.java b/core/src/main/java/jenkins/util/ResourceBundleUtil.java new file mode 100644 index 000000000000..1649c6c77cbe --- /dev/null +++ b/core/src/main/java/jenkins/util/ResourceBundleUtil.java @@ -0,0 +1,91 @@ +/* + * 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.util; + +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Simple {@link java.util.ResourceBundle} utility class. + * @author tom.fennelly@gmail.com + * @since FIXME + */ +public class ResourceBundleUtil { + + private static final Map bundles = new ConcurrentHashMap<>(); + + private ResourceBundleUtil() { + } + + /** + * Get a bundle JSON using the default Locale. + * @param baseName The bundle base name. + * @return The bundle JSON. + * @throws MissingResourceException Missing resource bundle. + */ + @Restricted(NoExternalUse.class) + public static @Nonnull JSONObject getBundle(@Nonnull String baseName) throws MissingResourceException { + return getBundle(baseName, Locale.getDefault()); + } + + /** + * Get a bundle JSON using the supplied Locale. + * @param baseName The bundle base name. + * @param locale The Locale. + * @return The bundle JSON. + * @throws MissingResourceException Missing resource bundle. + */ + @Restricted(NoExternalUse.class) + public static @Nonnull JSONObject getBundle(@Nonnull String baseName, @Nonnull Locale locale) throws MissingResourceException { + String bundleKey = baseName + ":" + locale.toString(); + JSONObject bundleJSON = bundles.get(bundleKey); + + if (bundleJSON != null) { + return bundleJSON; + } + + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale); + + bundleJSON = toJSONObject(bundle); + bundles.put(bundleKey, bundleJSON); + + return bundleJSON; + } + + private static JSONObject toJSONObject(@Nonnull ResourceBundle bundle) { + JSONObject json = new JSONObject(); + for (String key : bundle.keySet()) { + json.put(key, bundle.getString(key)); + } + return json; + } +} diff --git a/core/src/main/java/jenkins/util/xml/XMLUtils.java b/core/src/main/java/jenkins/util/xml/XMLUtils.java index 84e6f6151bf6..bf96669052b3 100644 --- a/core/src/main/java/jenkins/util/xml/XMLUtils.java +++ b/core/src/main/java/jenkins/util/xml/XMLUtils.java @@ -1,17 +1,28 @@ package jenkins.util.xml; +import org.apache.commons.io.IOUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; import javax.annotation.Nonnull; import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; @@ -19,6 +30,9 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; /** * Utilities useful when working with various XML types. @@ -29,6 +43,9 @@ 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"; + private static final String FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; + private static final String FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; + /** * 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. @@ -47,11 +64,11 @@ public static void safeTransform(@Nonnull Source source, @Nonnull Result out) th 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 @@ -81,6 +98,106 @@ public static void safeTransform(@Nonnull Source source, @Nonnull Result out) th } } + /** + * Parse the supplied XML stream data to a {@link Document}. + *

+ * This function does not close the stream. + * + * @param stream The XML stream. + * @return The XML {@link Document}. + * @throws SAXException Error parsing the XML stream data e.g. badly formed XML. + * @throws IOException Error reading from the steam. + * @since FIXME + */ + public static @Nonnull Document parse(@Nonnull Reader stream) throws SAXException, IOException { + DocumentBuilder docBuilder; + + try { + docBuilder = newDocumentBuilderFactory().newDocumentBuilder(); + docBuilder.setEntityResolver(RestrictiveEntityResolver.INSTANCE); + } catch (ParserConfigurationException e) { + throw new IllegalStateException("Unexpected error creating DocumentBuilder.", e); + } + + return docBuilder.parse(new InputSource(stream)); + } + + /** + * Parse the supplied XML file data to a {@link Document}. + * @param file The file to parse. + * @param encoding The encoding of the XML in the file. + * @return The parsed document. + * @throws SAXException Error parsing the XML file data e.g. badly formed XML. + * @throws IOException Error reading from the file. + * @since FIXME + */ + public static @Nonnull Document parse(@Nonnull File file, @Nonnull String encoding) throws SAXException, IOException { + if (!file.exists() || !file.isFile()) { + throw new IllegalArgumentException(String.format("File %s does not exist or is not a 'normal' file.", file.getAbsolutePath())); + } + + FileInputStream fileInputStream = new FileInputStream(file); + try { + InputStreamReader fileReader = new InputStreamReader(fileInputStream, encoding); + try { + return parse(fileReader); + } finally { + IOUtils.closeQuietly(fileReader); + } + } finally { + IOUtils.closeQuietly(fileInputStream); + } + } + + /** + * The a "value" from an XML file using XPath. + *

+ * Uses the system encoding for reading the file. + * + * @param xpath The XPath expression to select the value. + * @param file The file to read. + * @return The data value. An empty {@link String} is returned when the expression does not evaluate + * to anything in the document. + * @throws IOException Error reading from the file. + * @throws SAXException Error parsing the XML file data e.g. badly formed XML. + * @throws XPathExpressionException Invalid XPath expression. + * @since FIXME + */ + public static @Nonnull String getValue(@Nonnull String xpath, @Nonnull File file) throws IOException, SAXException, XPathExpressionException { + return getValue(xpath, file, Charset.defaultCharset().toString()); + } + + /** + * The a "value" from an XML file using XPath. + * @param xpath The XPath expression to select the value. + * @param file The file to read. + * @param fileDataEncoding The file data format. + * @return The data value. An empty {@link String} is returned when the expression does not evaluate + * to anything in the document. + * @throws IOException Error reading from the file. + * @throws SAXException Error parsing the XML file data e.g. badly formed XML. + * @throws XPathExpressionException Invalid XPath expression. + * @since FIXME + */ + 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); + } + /** * potentially unsafe XML transformation. * @param source The XML input to transform. @@ -96,4 +213,25 @@ private static void _transform(Source source, Result out) throws TransformerExce t.transform(source, out); } + private static DocumentBuilderFactory newDocumentBuilderFactory() { + DocumentBuilderFactory 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(documentBuilderFactory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + setDocumentBuilderFactoryFeature(documentBuilderFactory, FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES, false); + setDocumentBuilderFactoryFeature(documentBuilderFactory, FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES, false); + setDocumentBuilderFactoryFeature(documentBuilderFactory, "http://apache.org/xml/features/disallow-doctype-decl", true); + + return documentBuilderFactory; + } + private static void setDocumentBuilderFactoryFeature(DocumentBuilderFactory documentBuilderFactory, String feature, boolean state) { + try { + documentBuilderFactory.setFeature(feature, state); + } catch (Exception e) {} + } } diff --git a/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message.jelly b/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message.jelly deleted file mode 100644 index c91dfb851695..000000000000 --- a/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message.jelly +++ /dev/null @@ -1,13 +0,0 @@ - - -

-

- ${%blurb} -

-
    - -
  • ${p.longName}
  • -
    -
-
- diff --git a/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message.properties b/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message.properties deleted file mode 100644 index c1fc741c55c8..000000000000 --- a/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message.properties +++ /dev/null @@ -1,4 +0,0 @@ -blurb=This version of Jenkins comes with new versions of the following plugins that are currently \ - pinned in \ - the plugin manager. \ - It is recommended to upgrade them to at least the version bundled with Jenkins. diff --git a/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message_ja.properties b/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message_ja.properties deleted file mode 100644 index b0eb2a9b6061..000000000000 --- a/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message_ja.properties +++ /dev/null @@ -1,32 +0,0 @@ -# The MIT License -# -# Copyright (c) 2015, 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 -# 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. - -# This version of Jenkins comes with new versions of the following plugins that are currently \ -# pinned in \ -# the plugin manager. \ -# It is recommended to upgrade them to at least the version bundled with Jenkins. - - -blurb=\u3053\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306eJenkins\u3067\u306f\u3001\u30d7\u30e9\u30b0\u30a4\u30f3\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u3067\ -\u30d4\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u6b21\u306e\u30d7\u30e9\u30b0\u30a4\u30f3\u304c\ -\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u3002\ -\u30d4\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u30d7\u30e9\u30b0\u30a4\u30f3\u3092\u3001\u5c11\u306a\u304f\u3068\u3082Jenkins\u306b\u30d0\u30f3\u30c9\u30eb\u3055\u308c\u3066\u3044\u308b\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3059\u308b\u3053\u3068\u3092\u63a8\u5968\u3057\u307e\u3059\u3002 diff --git a/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message_pt_BR.properties b/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message_pt_BR.properties deleted file mode 100644 index c2aeee608320..000000000000 --- a/core/src/main/resources/jenkins/diagnostics/PinningIsBlockingBundledPluginMonitor/message_pt_BR.properties +++ /dev/null @@ -1,30 +0,0 @@ -# The MIT License -# -# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributers -# -# 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. - -# This version of Jenkins comes with new versions of the following plugins that are currently \ -# pinned in \ -# the plugin manager. \ -# It is recommended to upgrade them to at least the version bundled with Jenkins. -blurb=Essa vers\u00e3o do Jenkins vem com as vers\u00f5es novas dos seguintes plugins \ - pinned no \ - gerenciador de plugin. \ - \u00c9 recomendado atualizar para pelo menos a vers\u00e0o que vem junto com o Jenkins. diff --git a/core/src/main/resources/jenkins/install/pluginSetupWizard.properties b/core/src/main/resources/jenkins/install/pluginSetupWizard.properties new file mode 100644 index 000000000000..f86316fc8368 --- /dev/null +++ b/core/src/main/resources/jenkins/install/pluginSetupWizard.properties @@ -0,0 +1,38 @@ +installWizard_welcomePanel_title=Getting Started +installWizard_welcomePanel_banner=Initial Jenkins Setup +installWizard_welcomePanel_message=Choose plugins. This will add features to your Jenkins environment. +installWizard_welcomePanel_recommendedActionTitle=Start with recommended plugins +installWizard_welcomePanel_recommendedActionDetails=Install the set of plugins the community finds most useful +installWizard_welcomePanel_customizeActionTitle=Customize your plugins +installWizard_welcomePanel_customizeActionDetails=Select from a community approved list of plugins +installWizard_offline_title=Offline +installWizard_offline_message=This Jenkins instance appears to be offline. \ +

\ +For information about installing Jenkins without an internet connection, see the \ +Offline Jenkins Installation Documentation.

\ +If you need to configure a proxy, you may close this wizard and use the Plugin Manager. \ +

+installWizard_error_header=An error occurred +installWizard_error_message=An error occurred during installation: +installWizard_error_connection=Unable to connect to Jenkins +installWizard_installCustom_title=Plugin Selection +installWizard_installCustom_selectAll=All +installWizard_installCustom_selectNone=None +installWizard_installCustom_selectRecommended=Recommended +installWizard_installCustom_selected=Selected +installWizard_installCustom_dependenciesPrefix=Dependencies +installWizard_installCustom_pluginListDesc=Note that the full list of plugins is not shown here. Additional plugins can be installed in the Plugin Manager once the initial setup is complete. See the Wiki for more information. +installWizard_goBack=Back +installWizard_goInstall=Install +installWizard_installing_title=Installing... +installWizard_installing_detailsLink=Details... +installWizard_installComplete_title=Installed +installWizard_installComplete_banner=Jenkins is ready! +installWizard_installComplete_message=Your plugin installations are complete. +installWizard_installComplete_finishButtonLabel=Get Started +installWizard_installComplete_restartRequiredMessage=Some plugins require Jenkins to be restarted. +installWizard_installComplete_restartLabel=Restart +installWizard_installIncomplete_title=Resume Installation +installWizard_installIncomplete_banner=Resume Installation +installWizard_installIncomplete_message=Jenknins was restarted during installation and some plugins didn't seem to get installed. +installWizard_installIncomplete_resumeInstallationButtonLabel=Resume diff --git a/core/src/main/resources/jenkins/install/pluginSetupWizard_de.properties b/core/src/main/resources/jenkins/install/pluginSetupWizard_de.properties new file mode 100644 index 000000000000..0495cd6d440e --- /dev/null +++ b/core/src/main/resources/jenkins/install/pluginSetupWizard_de.properties @@ -0,0 +1 @@ +installWizard_welcomePanel_banner=Willkommen zu Jenkins diff --git a/core/src/main/resources/jenkins/install/pluginSetupWizard_es.properties b/core/src/main/resources/jenkins/install/pluginSetupWizard_es.properties new file mode 100644 index 000000000000..d856117ab402 --- /dev/null +++ b/core/src/main/resources/jenkins/install/pluginSetupWizard_es.properties @@ -0,0 +1 @@ +installWizard_welcomePanel_banner=Bienvenido a Jenkins diff --git a/core/src/main/resources/lib/layout/layout.jelly b/core/src/main/resources/lib/layout/layout.jelly index 9746a6a1734a..2caba28563ba 100644 --- a/core/src/main/resources/lib/layout/layout.jelly +++ b/core/src/main/resources/lib/layout/layout.jelly @@ -80,7 +80,7 @@ ${h.initPageVariables(context)} - + ${h.checkPermission(it,permission)} @@ -155,6 +155,13 @@ ${h.initPageVariables(context)} + + + +