/** TODO: revisit where/how to expose this. This is an experiment. */ public void dynamicLoad(File arc) throws IOException, InterruptedException, RestartRequiredException { LOGGER.info("Attempting to dynamic load " + arc); final PluginWrapper p = strategy.createPluginWrapper(arc); String sn = p.getShortName(); if (getPlugin(sn) != null) throw new RestartRequiredException( Messages._PluginManager_PluginIsAlreadyInstalled_RestartRequired(sn)); if (p.supportsDynamicLoad() == YesNoMaybe.NO) throw new RestartRequiredException( Messages._PluginManager_PluginDoesntSupportDynamicLoad_RestartRequired(sn)); // there's no need to do cyclic dependency check, because we are deploying one at a time, // so existing plugins can't be depending on this newly deployed one. plugins.add(p); activePlugins.add(p); try { p.resolvePluginDependencies(); strategy.load(p); Jenkins.getInstance().refreshExtensions(); p.getPlugin().postInitialize(); } catch (Exception e) { failedPlugins.add(new FailedPlugin(sn, e)); activePlugins.remove(p); plugins.remove(p); throw new IOException("Failed to install " + sn + " plugin", e); } // run initializers in the added plugin Reactor r = new Reactor(InitMilestone.ordering()); r.addAll( new InitializerFinder(p.classLoader) { @Override protected boolean filter(Method e) { return e.getDeclaringClass().getClassLoader() != p.classLoader || super.filter(e); } }.discoverTasks(r)); try { new InitReactorRunner().run(r); } catch (ReactorException e) { throw new IOException("Failed to initialize " + sn + " plugin", e); } LOGGER.info("Plugin " + sn + " dynamically installed"); }
public String getDisplayName() { return Messages.PluginManager_DisplayName(); }
/** * Manages {@link PluginWrapper}s. * * @author Kohsuke Kawaguchi */ @ExportedBean public abstract class PluginManager extends AbstractModelObject implements OnMaster { /** All discovered plugins. */ protected final List<PluginWrapper> plugins = new ArrayList<PluginWrapper>(); /** * All active plugins, topologically sorted so that when X depends on Y, Y appears in the list * before X does. */ protected final List<PluginWrapper> activePlugins = new CopyOnWriteArrayList<PluginWrapper>(); protected final List<FailedPlugin> failedPlugins = new ArrayList<FailedPlugin>(); /** Plug-in root directory. */ public final File rootDir; /** * @deprecated as of 1.355 {@link PluginManager} can now live longer than {@link * jenkins.model.Jenkins} instance, so use {@code Hudson.getInstance().servletContext} * instead. */ public final ServletContext context; /** * {@link ClassLoader} that can load all the publicly visible classes from plugins (and including * the classloader that loads Hudson itself.) */ // implementation is minimal --- just enough to run XStream // and load plugin-contributed classes. public final ClassLoader uberClassLoader = new UberClassLoader(); private final Transformer compatibilityTransformer = new Transformer(); /** * Once plugin is uploaded, this flag becomes true. This is used to report a message that Jenkins * needs to be restarted for new plugins to take effect. */ public volatile boolean pluginUploaded = false; /** * The initialization of {@link PluginManager} splits into two parts; one is the part about * listing them, extracting them, and preparing classloader for them. The 2nd part is about * creating instances. Once the former completes this flags become true, as the 2nd part can be * repeated for each Hudson instance. */ private boolean pluginListed = false; /** Strategy for creating and initializing plugins */ private final PluginStrategy strategy; public PluginManager(ServletContext context, File rootDir) { this.context = context; this.rootDir = rootDir; if (!rootDir.exists()) rootDir.mkdirs(); strategy = createPluginStrategy(); // load up rules for the core first try { compatibilityTransformer.loadRules(getClass().getClassLoader()); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to load compatibility rewrite rules", e); } } public Transformer getCompatibilityTransformer() { return compatibilityTransformer; } public Api getApi() { return new Api(this); } /** * Called immediately after the construction. This is a separate method so that code executed from * here will see a valid value in {@link jenkins.model.Jenkins#pluginManager}. */ public TaskBuilder initTasks(final InitStrategy initStrategy) { TaskBuilder builder; if (!pluginListed) { builder = new TaskGraphBuilder() { List<File> archives; Collection<String> bundledPlugins; { Handle loadBundledPlugins = add( "Loading bundled plugins", new Executable() { public void run(Reactor session) throws Exception { bundledPlugins = loadBundledPlugins(); } }); Handle listUpPlugins = requires(loadBundledPlugins) .add( "Listing up plugins", new Executable() { public void run(Reactor session) throws Exception { archives = initStrategy.listPluginArchives(PluginManager.this); } }); requires(listUpPlugins) .attains(PLUGINS_LISTED) .add( "Preparing plugins", new Executable() { public void run(Reactor session) throws Exception { // once we've listed plugins, we can fill in the reactor with // plugin-specific initialization tasks TaskGraphBuilder g = new TaskGraphBuilder(); final Map<String, File> inspectedShortNames = new HashMap<String, File>(); for (final File arc : archives) { g.followedBy() .notFatal() .attains(PLUGINS_LISTED) .add( "Inspecting plugin " + arc, new Executable() { public void run(Reactor session1) throws Exception { try { PluginWrapper p = strategy.createPluginWrapper(arc); if (isDuplicate(p)) return; p.isBundled = containsHpiJpi(bundledPlugins, arc.getName()); plugins.add(p); } catch (IOException e) { failedPlugins.add(new FailedPlugin(arc.getName(), e)); throw e; } } /** * Inspects duplication. this happens when you run hpi:run on * a bundled plugin, as well as putting numbered jpi files, * like "cobertura-1.0.jpi" and "cobertura-1.1.jpi" */ private boolean isDuplicate(PluginWrapper p) { String shortName = p.getShortName(); if (inspectedShortNames.containsKey(shortName)) { LOGGER.info( "Ignoring " + arc + " because " + inspectedShortNames.get(shortName) + " is already loaded"); return true; } inspectedShortNames.put(shortName, arc); return false; } }); } g.followedBy() .attains(PLUGINS_LISTED) .add( "Checking cyclic dependencies", new Executable() { /** Makes sure there's no cycle in dependencies. */ public void run(Reactor reactor) throws Exception { try { CyclicGraphDetector<PluginWrapper> cgd = new CyclicGraphDetector<PluginWrapper>() { @Override protected List<PluginWrapper> getEdges( PluginWrapper p) { List<PluginWrapper> next = new ArrayList<PluginWrapper>(); addTo(p.getDependencies(), next); addTo(p.getOptionalDependencies(), next); return next; } private void addTo( List<Dependency> dependencies, List<PluginWrapper> r) { for (Dependency d : dependencies) { PluginWrapper p = getPlugin(d.shortName); if (p != null) r.add(p); } } @Override protected void reactOnCycle( PluginWrapper q, List<PluginWrapper> cycle) throws hudson.util.CyclicGraphDetector .CycleDetectedException { LOGGER.log( Level.SEVERE, "found cycle in plugin dependencies: (root=" + q + ", deactivating all involved) " + Util.join(cycle, " -> ")); for (PluginWrapper pluginWrapper : cycle) { pluginWrapper.setHasCycleDependency(true); failedPlugins.add( new FailedPlugin( pluginWrapper.getShortName(), new CycleDetectedException(cycle))); } } }; cgd.run(getPlugins()); // obtain topologically sorted list and overwrite the list ListIterator<PluginWrapper> litr = plugins.listIterator(); for (PluginWrapper p : cgd.getSorted()) { litr.next(); litr.set(p); if (p.isActive()) activePlugins.add(p); } } catch (CycleDetectedException e) { stop(); // disable all plugins since classloading from them // can lead to StackOverflow throw e; // let Hudson fail } } }); // Let's see for a while until we open this functionality up to plugins // // g.followedBy().attains(PLUGINS_LISTED).add("Load compatibility rules", // new Executable() { // public void run(Reactor reactor) throws // Exception { // // compatibilityTransformer.loadRules(uberClassLoader); // } // }); session.addAll(g.discoverTasks(session)); pluginListed = true; // technically speaking this is still too early, as at this // point tasks are merely scheduled, not necessarily executed. } }); } }; } else { builder = TaskBuilder.EMPTY_BUILDER; } final InitializerFinder initializerFinder = new InitializerFinder(uberClassLoader); // misc. stuff // 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() { /** Once the plugins are listed, schedule their initialization. */ public void run(Reactor session) throws Exception { Jenkins.getInstance() .lookup .set(PluginInstanceStore.class, new PluginInstanceStore()); TaskGraphBuilder g = new TaskGraphBuilder(); // schedule execution of loading plugins for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) { g.followedBy() .notFatal() .attains(PLUGINS_PREPARED) .add( "Loading plugin " + p.getShortName(), new Executable() { public void run(Reactor session) throws Exception { try { p.resolvePluginDependencies(); strategy.load(p); } catch (IOException e) { failedPlugins.add(new FailedPlugin(p.getShortName(), e)); activePlugins.remove(p); plugins.remove(p); throw e; } } }); } // schedule execution of initializing plugins for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) { g.followedBy() .notFatal() .attains(PLUGINS_STARTED) .add( "Initializing plugin " + p.getShortName(), new Executable() { public void run(Reactor session) throws Exception { if (!activePlugins.contains(p)) { return; } try { p.getPlugin().postInitialize(); } catch (Exception e) { failedPlugins.add(new FailedPlugin(p.getShortName(), e)); activePlugins.remove(p); plugins.remove(p); throw e; } } }); } g.followedBy() .attains(PLUGINS_STARTED) .add( "Discovering plugin initialization tasks", new Executable() { public void run(Reactor reactor) throws Exception { // rescan to find plugin-contributed @Initializer reactor.addAll(initializerFinder.discoverTasks(reactor)); } }); // register them all session.addAll(g.discoverTasks(session)); } }); } }); } /* * contains operation that considers xxx.hpi and xxx.jpi as equal * this is necessary since the bundled plugins are still called *.hpi */ private boolean containsHpiJpi(Collection<String> bundledPlugins, String name) { return bundledPlugins.contains(name.replaceAll("\\.hpi", ".jpi")) || bundledPlugins.contains(name.replaceAll("\\.jpi", ".hpi")); } /** TODO: revisit where/how to expose this. This is an experiment. */ public void dynamicLoad(File arc) throws IOException, InterruptedException, RestartRequiredException { LOGGER.info("Attempting to dynamic load " + arc); final PluginWrapper p = strategy.createPluginWrapper(arc); String sn = p.getShortName(); if (getPlugin(sn) != null) throw new RestartRequiredException( Messages._PluginManager_PluginIsAlreadyInstalled_RestartRequired(sn)); if (p.supportsDynamicLoad() == YesNoMaybe.NO) throw new RestartRequiredException( Messages._PluginManager_PluginDoesntSupportDynamicLoad_RestartRequired(sn)); // there's no need to do cyclic dependency check, because we are deploying one at a time, // so existing plugins can't be depending on this newly deployed one. plugins.add(p); activePlugins.add(p); try { p.resolvePluginDependencies(); strategy.load(p); Jenkins.getInstance().refreshExtensions(); p.getPlugin().postInitialize(); } catch (Exception e) { failedPlugins.add(new FailedPlugin(sn, e)); activePlugins.remove(p); plugins.remove(p); throw new IOException("Failed to install " + sn + " plugin", e); } // run initializers in the added plugin Reactor r = new Reactor(InitMilestone.ordering()); r.addAll( new InitializerFinder(p.classLoader) { @Override protected boolean filter(Method e) { return e.getDeclaringClass().getClassLoader() != p.classLoader || super.filter(e); } }.discoverTasks(r)); try { new InitReactorRunner().run(r); } catch (ReactorException e) { throw new IOException("Failed to initialize " + sn + " plugin", e); } LOGGER.info("Plugin " + sn + " dynamically installed"); } /** * If the war file has any "/WEB-INF/plugins/[*.jpi | *.hpi]", extract them into the plugin * directory. * * @return File names of the bundled plugins. Like {"ssh-slaves.hpi","subvesrion.jpi"} * @throws Exception Any exception will be reported and halt the startup. */ protected abstract Collection<String> loadBundledPlugins() throws Exception; /** * Copies the bundled plugin from the given URL to the destination of the given file name (like * 'abc.jpi'), with a reasonable up-to-date check. A convenience method to be used by the {@link * #loadBundledPlugins()}. */ protected void copyBundledPlugin(URL src, String fileName) throws IOException { fileName = fileName.replace(".hpi", ".jpi"); // normalize fileNames to have the correct suffix 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())) { FileUtils.copyURLToFile(src, file); file.setLastModified(src.openConnection().getLastModified()); // lastModified is set for two reasons: // - to avoid unpacking as much as possible, but still do it on both upgrade and downgrade // - to make sure the value is not changed after each restart, so we can avoid // unpacking the plugin itself in ClassicPluginStrategy.explode } } /** * Rename a legacy file to a new name, with care to Windows where {@link File#renameTo(File)} * doesn't work if the destination already exists. */ private void rename(File legacyFile, File newFile) throws IOException { if (!legacyFile.exists()) return; if (newFile.exists()) { Util.deleteFile(newFile); } if (!legacyFile.renameTo(newFile)) { LOGGER.warning("Failed to rename " + legacyFile + " to " + newFile); } } /** Creates a hudson.PluginStrategy, looking at the corresponding system property. */ protected PluginStrategy createPluginStrategy() { String strategyName = System.getProperty(PluginStrategy.class.getName()); if (strategyName != null) { try { Class<?> klazz = getClass().getClassLoader().loadClass(strategyName); Object strategy = klazz.getConstructor(PluginManager.class).newInstance(this); if (strategy instanceof PluginStrategy) { LOGGER.info("Plugin strategy: " + strategyName); return (PluginStrategy) strategy; } else { LOGGER.warning( "Plugin strategy (" + strategyName + ") is not an instance of hudson.PluginStrategy"); } } catch (ClassNotFoundException e) { LOGGER.warning("Plugin strategy class not found: " + strategyName); } catch (Exception e) { LOGGER.log( WARNING, "Could not instantiate plugin strategy: " + strategyName + ". Falling back to ClassicPluginStrategy", e); } LOGGER.info("Falling back to ClassicPluginStrategy"); } // default and fallback return new ClassicPluginStrategy(this); } public PluginStrategy getPluginStrategy() { return strategy; } /** Returns true if any new plugin was added. */ public boolean isPluginUploaded() { return pluginUploaded; } /** All discovered plugins. */ @Exported public List<PluginWrapper> getPlugins() { return plugins; } public List<FailedPlugin> getFailedPlugins() { return failedPlugins; } /** * Get the plugin instance with the given short name. * * @param shortName the short name of the plugin * @return The plugin singleton or <code>null</code> if a plugin with the given short name does * not exist. */ public PluginWrapper getPlugin(String shortName) { for (PluginWrapper p : plugins) { if (p.getShortName().equals(shortName)) return p; } return null; } /** * Get the plugin instance that implements a specific class, use to find your plugin singleton. * Note: beware the classloader fun. * * @param pluginClazz The class that your plugin implements. * @return The plugin singleton or <code>null</code> if for some reason the plugin is not loaded. */ public PluginWrapper getPlugin(Class<? extends Plugin> pluginClazz) { for (PluginWrapper p : plugins) { if (pluginClazz.isInstance(p.getPlugin())) return p; } return null; } /** * Get the plugin instances that extend a specific class, use to find similar plugins. Note: * beware the classloader fun. * * @param pluginSuperclass The class that your plugin is derived from. * @return The list of plugins implementing the specified class. */ public List<PluginWrapper> getPlugins(Class<? extends Plugin> pluginSuperclass) { List<PluginWrapper> result = new ArrayList<PluginWrapper>(); for (PluginWrapper p : plugins) { if (pluginSuperclass.isInstance(p.getPlugin())) result.add(p); } return Collections.unmodifiableList(result); } public String getDisplayName() { return Messages.PluginManager_DisplayName(); } public String getSearchUrl() { return "pluginManager"; } /** * Discover all the service provider implementations of the given class, via * <tt>META-INF/services</tt>. */ public <T> Collection<Class<? extends T>> discover(Class<T> spi) { Set<Class<? extends T>> result = new HashSet<Class<? extends T>>(); for (PluginWrapper p : activePlugins) { Service.load(spi, p.classLoader, result); } return result; } /** * Return the {@link PluginWrapper} that loaded the given class 'c'. * * @since 1.402. */ public PluginWrapper whichPlugin(Class c) { PluginWrapper oneAndOnly = null; ClassLoader cl = c.getClassLoader(); for (PluginWrapper p : activePlugins) { if (p.classLoader == cl) { if (oneAndOnly != null) return null; // ambigious oneAndOnly = p; } } return oneAndOnly; } /** Orderly terminates all the plugins. */ public void stop() { for (PluginWrapper p : activePlugins) { p.stop(); p.releaseClassLoader(); } activePlugins.clear(); // Work around a bug in commons-logging. // See http://www.szegedi.org/articles/memleak.html LogFactory.release(uberClassLoader); } public HttpResponse doUpdateSources(StaplerRequest req) throws IOException { Jenkins.getInstance().checkPermission(CONFIGURE_UPDATECENTER); if (req.hasParameter("remove")) { UpdateCenter uc = Jenkins.getInstance().getUpdateCenter(); BulkChange bc = new BulkChange(uc); try { for (String id : req.getParameterValues("sources")) uc.getSites().remove(uc.getById(id)); } finally { bc.commit(); } } else if (req.hasParameter("add")) return new HttpRedirect("addSite"); return new HttpRedirect("./sites"); } /** Performs the installation of the plugins. */ public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { boolean dynamicLoad = req.getParameter("dynamicLoad") != null; Enumeration<String> en = req.getParameterNames(); while (en.hasMoreElements()) { String n = en.nextElement(); if (n.startsWith("plugin.")) { n = n.substring(7); if (n.indexOf(".") > 0) { String[] pluginInfo = n.split("\\."); UpdateSite.Plugin p = Jenkins.getInstance() .getUpdateCenter() .getById(pluginInfo[1]) .getPlugin(pluginInfo[0]); if (p == null) throw new Failure("No such plugin: " + n); p.deploy(dynamicLoad); } } } rsp.sendRedirect("../updateCenter/"); } /** Bare-minimum configuration mechanism to change the update center. */ public HttpResponse doSiteConfigure(@QueryParameter String site) throws IOException { Jenkins hudson = Jenkins.getInstance(); hudson.checkPermission(CONFIGURE_UPDATECENTER); UpdateCenter uc = hudson.getUpdateCenter(); PersistedList<UpdateSite> sites = uc.getSites(); for (UpdateSite s : sites) { if (s.getId().equals(UpdateCenter.ID_DEFAULT)) sites.remove(s); } sites.add(new UpdateSite(UpdateCenter.ID_DEFAULT, site)); return HttpResponses.redirectToContextRoot(); } public HttpResponse doProxyConfigure(StaplerRequest req) throws IOException, ServletException { Jenkins jenkins = Jenkins.getInstance(); jenkins.checkPermission(CONFIGURE_UPDATECENTER); ProxyConfiguration pc = req.bindJSON(ProxyConfiguration.class, req.getSubmittedForm()); if (pc.name == null) { jenkins.proxy = null; ProxyConfiguration.getXmlFile().delete(); } else { jenkins.proxy = pc; jenkins.proxy.save(); } return new HttpRedirect("advanced"); } /** Uploads a plugin. */ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, ServletException { try { Jenkins.getInstance().checkPermission(UPLOAD_PLUGINS); ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory()); // Parse the request FileItem fileItem = (FileItem) upload.parseRequest(req).get(0); String fileName = Util.getFileName(fileItem.getName()); if ("".equals(fileName)) { return new HttpRedirect("advanced"); } // we allow the upload of the new jpi's and the legacy hpi's if (!fileName.endsWith(".jpi") && !fileName.endsWith(".hpi")) { throw new Failure(hudson.model.Messages.Hudson_NotAPlugin(fileName)); } // first copy into a temporary file name File t = File.createTempFile("uploaded", ".jpi"); t.deleteOnExit(); fileItem.write(t); fileItem.delete(); final String baseName = identifyPluginShortName(t); pluginUploaded = true; // Now create a dummy plugin that we can dynamically load (the InstallationJob will force a // restart if one is needed): JSONObject cfg = new JSONObject() .element("name", baseName) .element("version", "0") . // unused but mandatory element("url", t.toURI().toString()) .element("dependencies", new JSONArray()); new UpdateSite(UpdateCenter.ID_UPLOAD, null).new Plugin(UpdateCenter.ID_UPLOAD, cfg) .deploy(true); return new HttpRedirect("../updateCenter"); } catch (IOException e) { throw e; } catch (Exception e) { // grrr. fileItem.write throws this throw new ServletException(e); } } protected String identifyPluginShortName(File t) { try { JarFile j = new JarFile(t); try { String name = j.getManifest().getMainAttributes().getValue("Short-Name"); if (name != null) return name; } finally { j.close(); } } catch (IOException e) { LOGGER.log(WARNING, "Failed to identify the short name from " + t, e); } return FilenameUtils.getBaseName(t.getName()); // fall back to the base name of what's uploaded } public Descriptor<ProxyConfiguration> getProxyDescriptor() { return Jenkins.getInstance().getDescriptor(ProxyConfiguration.class); } /** * Prepares plugins for some expected XML configuration. If the configuration (typically a job’s * {@code config.xml}) needs some plugins to be installed (or updated), those jobs will be * triggered. Plugins are dynamically loaded whenever possible. Requires {@link * Jenkins#ADMINISTER}. * * @param configXml configuration that might be uploaded * @return an empty list if all is well, else a list of submitted jobs which must be completed * before this configuration can be fully read * @throws IOException if loading or parsing the configuration failed * @see ItemGroupMixIn#createProjectFromXML * @see AbstractItem#updateByXml(javax.xml.transform.Source) * @see XStream2 * @see hudson.model.UpdateSite.Plugin#deploy(boolean) * @see PluginWrapper#supportsDynamicLoad * @see hudson.model.UpdateCenter.DownloadJob.SuccessButRequiresRestart * @since 1.483 */ public List<Future<UpdateCenter.UpdateCenterJob>> prevalidateConfig(InputStream configXml) throws IOException { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); List<Future<UpdateCenter.UpdateCenterJob>> jobs = new ArrayList<Future<UpdateCenter.UpdateCenterJob>>(); UpdateCenter uc = Jenkins.getInstance().getUpdateCenter(); // TODO call uc.updateAllSites() when available? perhaps not, since we should not block on // network here for (Map.Entry<String, VersionNumber> requestedPlugin : parseRequestedPlugins(configXml).entrySet()) { PluginWrapper pw = getPlugin(requestedPlugin.getKey()); if (pw == null) { // install new UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey()); if (toInstall == null) { LOGGER.log(WARNING, "No such plugin {0} to install", requestedPlugin.getKey()); continue; } if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) { LOGGER.log( WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version}); } if (toInstall.isForNewerHudson()) { LOGGER.log( WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version}); } jobs.add(toInstall.deploy(true)); } else if (pw.isOlderThan(requestedPlugin.getValue())) { // upgrade UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey()); if (toInstall == null) { LOGGER.log(WARNING, "No such plugin {0} to upgrade", requestedPlugin.getKey()); continue; } if (!pw.isOlderThan(new VersionNumber(toInstall.version))) { LOGGER.log( WARNING, "{0}@{1} is no newer than what we already have", new Object[] {toInstall.name, toInstall.version}); continue; } if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) { LOGGER.log( WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version}); } if (toInstall.isForNewerHudson()) { LOGGER.log( WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version}); } if (!toInstall.isCompatibleWithInstalledVersion()) { LOGGER.log( WARNING, "{0}@{1} is incompatible with the installed @{2}", new Object[] {toInstall.name, toInstall.version, pw.getVersion()}); } jobs.add( toInstall.deploy( true)); // dynamicLoad=true => sure to throw RestartRequiredException, but at least // message is nicer } // else already good } return jobs; } /** * Like {@link #doInstallNecessaryPlugins(StaplerRequest)} but only checks if everything is * installed or if some plugins need updates or installation. * * <p>This method runs without side-effect. I'm still requiring the ADMINISTER permission since * XML file can contain various external references and we don't configure parsers properly * against that. * * @since 1.483 */ @RequirePOST public JSONArray doPrevalidateConfig(StaplerRequest req) throws IOException { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); JSONArray response = new JSONArray(); for (Map.Entry<String, VersionNumber> p : parseRequestedPlugins(req.getInputStream()).entrySet()) { PluginWrapper pw = getPlugin(p.getKey()); JSONObject j = new JSONObject() .accumulate("name", p.getKey()) .accumulate("version", p.getValue().toString()); if (pw == null) { // install new response.add(j.accumulate("mode", "missing")); } else if (pw.isOlderThan(p.getValue())) { // upgrade response.add(j.accumulate("mode", "old")); } // else already good } return response; } /** * Runs {@link #prevalidateConfig} on posted XML and redirects to the {@link UpdateCenter}. * * @since 1.483 */ @RequirePOST public HttpResponse doInstallNecessaryPlugins(StaplerRequest req) throws IOException { prevalidateConfig(req.getInputStream()); return HttpResponses.redirectViaContextPath("updateCenter"); } /** Parses configuration XML files and picks up references to XML files. */ public Map<String, VersionNumber> parseRequestedPlugins(InputStream configXml) throws IOException { final Map<String, VersionNumber> requestedPlugins = new TreeMap<String, VersionNumber>(); try { SAXParserFactory.newInstance() .newSAXParser() .parse( configXml, new DefaultHandler() { @Override public void startElement( String uri, String localName, String qName, Attributes attributes) throws SAXException { String plugin = attributes.getValue("plugin"); if (plugin == null) { return; } if (!plugin.matches("[^@]+@[^@]+")) { throw new SAXException("Malformed plugin attribute: " + plugin); } int at = plugin.indexOf('@'); String shortName = plugin.substring(0, at); VersionNumber existing = requestedPlugins.get(shortName); VersionNumber requested = new VersionNumber(plugin.substring(at + 1)); if (existing == null || existing.compareTo(requested) < 0) { requestedPlugins.put(shortName, requested); } } }); } catch (SAXException x) { throw new IOException("Failed to parse XML", x); } catch (ParserConfigurationException e) { throw new AssertionError(e); // impossible since we don't tweak XMLParser } return requestedPlugins; } /** {@link ClassLoader} that can see all plugins. */ public final class UberClassLoader extends ClassLoader { /** Make generated types visible. Keyed by the generated class name. */ private ConcurrentMap<String, WeakReference<Class>> generatedClasses = new ConcurrentHashMap<String, WeakReference<Class>>(); private ClassLoaderReflectionToolkit clt = new ClassLoaderReflectionToolkit(); public UberClassLoader() { super(PluginManager.class.getClassLoader()); } public void addNamedClass(String className, Class c) { generatedClasses.put(className, new WeakReference<Class>(c)); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { WeakReference<Class> wc = generatedClasses.get(name); if (wc != null) { Class c = wc.get(); if (c != null) return c; else generatedClasses.remove(name, wc); } if (FAST_LOOKUP) { for (PluginWrapper p : activePlugins) { try { Class c = clt.findLoadedClass(p.classLoader, name); if (c != null) return c; // calling findClass twice appears to cause LinkageError: duplicate class def return clt.findClass(p.classLoader, name); } catch (InvocationTargetException e) { // not found. try next } } } else { for (PluginWrapper p : activePlugins) { try { return p.classLoader.loadClass(name); } catch (ClassNotFoundException e) { // not found. try next } } } // not found in any of the classloader. delegate. throw new ClassNotFoundException(name); } @Override protected URL findResource(String name) { if (FAST_LOOKUP) { try { for (PluginWrapper p : activePlugins) { URL url = clt.findResource(p.classLoader, name); if (url != null) return url; } } catch (InvocationTargetException e) { throw new Error(e); } } else { for (PluginWrapper p : activePlugins) { URL url = p.classLoader.getResource(name); if (url != null) return url; } } return null; } @Override protected Enumeration<URL> findResources(String name) throws IOException { List<URL> resources = new ArrayList<URL>(); if (FAST_LOOKUP) { try { for (PluginWrapper p : activePlugins) { resources.addAll(Collections.list(clt.findResources(p.classLoader, name))); } } catch (InvocationTargetException e) { throw new Error(e); } } else { for (PluginWrapper p : activePlugins) { resources.addAll(Collections.list(p.classLoader.getResources(name))); } } return Collections.enumeration(resources); } @Override public String toString() { // only for debugging purpose return "classLoader " + getClass().getName(); } } private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName()); public static boolean FAST_LOOKUP = !Boolean.getBoolean(PluginManager.class.getName() + ".noFastLookup"); public static final Permission UPLOAD_PLUGINS = new Permission( Jenkins.PERMISSIONS, "UploadPlugins", Messages._PluginManager_UploadPluginsPermission_Description(), Jenkins.ADMINISTER, PermissionScope.JENKINS); public static final Permission CONFIGURE_UPDATECENTER = new Permission( Jenkins.PERMISSIONS, "ConfigureUpdateCenter", Messages._PluginManager_ConfigureUpdateCenterPermission_Description(), Jenkins.ADMINISTER, PermissionScope.JENKINS); /** Remembers why a plugin failed to deploy. */ public static final class FailedPlugin { public final String name; public final Exception cause; public FailedPlugin(String name, Exception cause) { this.name = name; this.cause = cause; } public String getExceptionString() { return Functions.printThrowable(cause); } } /** Stores {@link Plugin} instances. */ /*package*/ static final class PluginInstanceStore { final Map<PluginWrapper, Plugin> store = new Hashtable<PluginWrapper, Plugin>(); } /** {@link AdministrativeMonitor} that checks if there are any plugins with cycle dependencies. */ @Extension public static final class PluginCycleDependenciesMonitor extends AdministrativeMonitor { private transient volatile boolean isActive = false; private transient volatile List<String> pluginsWithCycle; public boolean isActivated() { if (pluginsWithCycle == null) { pluginsWithCycle = new ArrayList<String>(); for (PluginWrapper p : Jenkins.getInstance().getPluginManager().getPlugins()) { if (p.hasCycleDependency()) { pluginsWithCycle.add(p.getShortName()); isActive = true; } } } return isActive; } public List<String> getPluginsWithCycle() { return pluginsWithCycle; } } /** * {@link AdministrativeMonitor} that informs the administrator about a required plugin update. * * @since 1.491 */ @Extension public static final class PluginUpdateMonitor extends AdministrativeMonitor { private Map<String, PluginUpdateInfo> pluginsToBeUpdated = new HashMap<String, PluginManager.PluginUpdateMonitor.PluginUpdateInfo>(); /** * Convenience method to ease access to this monitor, this allows other plugins to register * required updates. * * @return this monitor. */ public static final PluginUpdateMonitor getInstance() { return Jenkins.getInstance().getExtensionList(PluginUpdateMonitor.class).get(0); } /** * Report to the administrator if the plugin with the given name is older then the required * version. * * @param pluginName shortName of the plugin (artifactId) * @param requiredVersion the lowest version which is OK (e.g. 2.2.2) * @param message the message to show (plain text) */ public void ifPluginOlderThenReport(String pluginName, String requiredVersion, String message) { Plugin plugin = Jenkins.getInstance().getPlugin(pluginName); if (plugin != null) { if (plugin .getWrapper() .getVersionNumber() .isOlderThan(new VersionNumber(requiredVersion))) { pluginsToBeUpdated.put(pluginName, new PluginUpdateInfo(pluginName, message)); } } } public boolean isActivated() { return !pluginsToBeUpdated.isEmpty(); } /** * adds a message about a plugin to the manage screen * * @param pluginName the plugins name * @param message the message to be displayed */ public void addPluginToUpdate(String pluginName, String message) { this.pluginsToBeUpdated.put(pluginName, new PluginUpdateInfo(pluginName, message)); } public Collection<PluginUpdateInfo> getPluginsToBeUpdated() { return pluginsToBeUpdated.values(); } public static class PluginUpdateInfo { public final String pluginName; public final String message; private PluginUpdateInfo(String pluginName, String message) { this.pluginName = pluginName; this.message = message; } } } }