File D:\maven-qcr-plugin-testsunitaires\qcr4java\jenkins-master\target\site\qc\core\src\main\java\hudson\PluginManager.java.html

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, Tom Huybrechts
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 */
24 package hudson;
25
26 import hudson.PluginWrapper.Dependency;
27 import hudson.init.InitMilestone;
28 import hudson.init.InitStrategy;
29 import hudson.init.InitializerFinder;
30 import hudson.model.AbstractItem;
31 import hudson.model.AbstractModelObject;
32 import hudson.model.AdministrativeMonitor;
33 import hudson.model.Api;
34 import hudson.model.Descriptor;
35 import hudson.model.Failure;
36 import hudson.model.ItemGroupMixIn;
37 import hudson.model.UpdateCenter;
38 import hudson.model.UpdateSite;
39 import hudson.security.Permission;
40 import hudson.security.PermissionScope;
41 import hudson.util.CyclicGraphDetector;
42 import hudson.util.CyclicGraphDetector.CycleDetectedException;
43 import hudson.util.IOException2;
44 import hudson.util.PersistedList;
45 import hudson.util.Service;
46 import hudson.util.VersionNumber;
47 import hudson.util.XStream2;
48 import jenkins.ClassLoaderReflectionToolkit;
49 import jenkins.InitReactorRunner;
50 import jenkins.RestartRequiredException;
51 import jenkins.YesNoMaybe;
52 import jenkins.model.Jenkins;
53 import jenkins.util.io.OnMaster;
54 import net.sf.json.JSONArray;
55 import net.sf.json.JSONObject;
56 import org.apache.commons.fileupload.FileItem;
57 import org.apache.commons.fileupload.disk.DiskFileItemFactory;
58 import org.apache.commons.fileupload.servlet.ServletFileUpload;
59 import org.apache.commons.io.FileUtils;
60 import org.apache.commons.io.FilenameUtils;
61 import org.apache.commons.logging.LogFactory;
62 import org.jvnet.hudson.reactor.Executable;
63 import org.jvnet.hudson.reactor.Reactor;
64 import org.jvnet.hudson.reactor.ReactorException;
65 import org.jvnet.hudson.reactor.TaskBuilder;
66 import org.jvnet.hudson.reactor.TaskGraphBuilder;
67 import org.kohsuke.stapler.HttpRedirect;
68 import org.kohsuke.stapler.HttpResponse;
69 import org.kohsuke.stapler.HttpResponses;
70 import org.kohsuke.stapler.QueryParameter;
71 import org.kohsuke.stapler.StaplerRequest;
72 import org.kohsuke.stapler.StaplerResponse;
73 import org.kohsuke.stapler.export.Exported;
74 import org.kohsuke.stapler.export.ExportedBean;
75 import org.kohsuke.stapler.interceptor.RequirePOST;
76
77 import javax.servlet.ServletContext;
78 import javax.servlet.ServletException;
79 import javax.xml.parsers.ParserConfigurationException;
80 import javax.xml.parsers.SAXParserFactory;
81 import java.io.File;
82 import java.io.IOException;
83 import java.io.InputStream;
84 import java.lang.ref.WeakReference;
85 import java.lang.reflect.InvocationTargetException;
86 import java.lang.reflect.Method;
87 import java.net.URL;
88 import java.util.ArrayList;
89 import java.util.Collection;
90 import java.util.Collections;
91 import java.util.Enumeration;
92 import java.util.HashMap;
93 import java.util.HashSet;
94 import java.util.Hashtable;
95 import java.util.List;
96 import java.util.ListIterator;
97 import java.util.Map;
98 import java.util.Set;
99 import java.util.TreeMap;
100 import java.util.concurrent.ConcurrentHashMap;
101 import java.util.concurrent.ConcurrentMap;
102 import java.util.concurrent.CopyOnWriteArrayList;
103 import java.util.concurrent.Future;
104 import java.util.logging.Level;
105 import java.util.logging.Logger;
106 import org.xml.sax.Attributes;
107 import org.xml.sax.SAXException;
108 import org.xml.sax.helpers.DefaultHandler;
109
110 import static hudson.init.InitMilestone.*;
111 import static java.util.logging.Level.WARNING;
112
113 /**
114 * Manages {@link PluginWrapper}s.
115 *
116 * @author Kohsuke Kawaguchi
117 */
118 @ExportedBean
119 public abstract class PluginManager extends AbstractModelObject implements OnMaster {
120 /**
121 * All discovered plugins.
122 */
123 protected final List<PluginWrapper> plugins = new ArrayList<PluginWrapper>();
124
125 /**
126 * All active plugins, topologically sorted so that when X depends on Y, Y appears in the list before X does.
127 */
128 protected final List<PluginWrapper> activePlugins = new CopyOnWriteArrayList<PluginWrapper>();
129
130 protected final List<FailedPlugin> failedPlugins = new ArrayList<FailedPlugin>();
131
132 /**
133 * Plug-in root directory.
134 */
135 public final File rootDir;
136
137 /**
138 * @deprecated as of 1.355
139 * {@link PluginManager} can now live longer than {@link jenkins.model.Jenkins} instance, so
140 * use {@code Hudson.getInstance().servletContext} instead.
141 */
142 public final ServletContext context;
143
144 /**
145 * {@link ClassLoader} that can load all the publicly visible classes from plugins
146 * (and including the classloader that loads Hudson itself.)
147 *
148 */
149 // implementation is minimal --- just enough to run XStream
150 // and load plugin-contributed classes.
151 public final ClassLoader uberClassLoader = new UberClassLoader();
152
153 /**
154 * Once plugin is uploaded, this flag becomes true.
155 * This is used to report a message that Jenkins needs to be restarted
156 * for new plugins to take effect.
157 */
158 public volatile boolean pluginUploaded = false;
159
160 /**
161 * The initialization of {@link PluginManager} splits into two parts;
162 * one is the part about listing them, extracting them, and preparing classloader for them.
163 * The 2nd part is about creating instances. Once the former completes this flags become true,
164 * as the 2nd part can be repeated for each Hudson instance.
165 */
166 private boolean pluginListed = false;
167
168 /**
169 * Strategy for creating and initializing plugins
170 */
171 private final PluginStrategy strategy;
172
173 public PluginManager(ServletContext context, File rootDir) {
174 this.context = context;
175
176 this.rootDir = rootDir;
177 if(!rootDir.exists())
178 rootDir.mkdirs();
179
180 strategy = createPluginStrategy();
181 }
182
183 public Api getApi() {
184 return new Api(this);
185 }
186
187 /**
188 * Called immediately after the construction.
189 * This is a separate method so that code executed from here will see a valid value in
190 * {@link jenkins.model.Jenkins#pluginManager}.
191 */
192 public TaskBuilder initTasks(final InitStrategy initStrategy) {
193 TaskBuilder builder;
194 if (!pluginListed) {
195 builder = new TaskGraphBuilder() {
196 List<File> archives;
197 Collection<String> bundledPlugins;
198
199 {
200 Handle loadBundledPlugins = add("Loading bundled plugins", new Executable() {
201 public void run(Reactor session) throws Exception {
202 bundledPlugins = loadBundledPlugins();
203 }
204 });
205
206 Handle listUpPlugins = requires(loadBundledPlugins).add("Listing up plugins", new Executable() {
207 public void run(Reactor session) throws Exception {
208 archives = initStrategy.listPluginArchives(PluginManager.this);
209 }
210 });
211
212 requires(listUpPlugins).attains(PLUGINS_LISTED).add("Preparing plugins",new Executable() {
213 public void run(Reactor session) throws Exception {
214 // once we've listed plugins, we can fill in the reactor with plugin-specific initialization tasks
215 TaskGraphBuilder g = new TaskGraphBuilder();
216
217 final Map<String,File> inspectedShortNames = new HashMap<String,File>();
218
219 for( final File arc : archives ) {
220 g.followedBy().notFatal().attains(PLUGINS_LISTED).add("Inspecting plugin " + arc, new Executable() {
221 public void run(Reactor session1) throws Exception {
222 try {
223 PluginWrapper p = strategy.createPluginWrapper(arc);
224 if (isDuplicate(p)) return;
225
226 p.isBundled = containsHpiJpi(bundledPlugins, arc.getName());
227 plugins.add(p);
228 } catch (IOException e) {
229 failedPlugins.add(new FailedPlugin(arc.getName(),e));
230 throw e;
231 }
232 }
233
234 /**
235 * Inspects duplication. this happens when you run hpi:run on a bundled plugin,
236 * as well as putting numbered jpi files, like "cobertura-1.0.jpi" and "cobertura-1.1.jpi"
237 */
238 private boolean isDuplicate(PluginWrapper p) {
239 String shortName = p.getShortName();
240 if (inspectedShortNames.containsKey(shortName)) {
241 LOGGER.info("Ignoring "+arc+" because "+inspectedShortNames.get(shortName)+" is already loaded");
242 return true;
243 }
244
245 inspectedShortNames.put(shortName,arc);
246 return false;
247 }
248 });
249 }
250
251 g.followedBy().attains(PLUGINS_LISTED).add("Checking cyclic dependencies", new Executable() {
252 /**
253 * Makes sure there's no cycle in dependencies.
254 */
255 public void run(Reactor reactor) throws Exception {
256 try {
257 CyclicGraphDetector<PluginWrapper> cgd = new CyclicGraphDetector<PluginWrapper>() {
258 @Override
259 protected List<PluginWrapper> getEdges(PluginWrapper p) {
260 List<PluginWrapper> next = new ArrayList<PluginWrapper>();
261 addTo(p.getDependencies(), next);
262 addTo(p.getOptionalDependencies(), next);
263 return next;
264 }
265
266 private void addTo(List<Dependency> dependencies, List<PluginWrapper> r) {
267 for (Dependency d : dependencies) {
268 PluginWrapper p = getPlugin(d.shortName);
269 if (p != null)
270 r.add(p);
271 }
272 }
273
274 @Override
275 protected void reactOnCycle(PluginWrapper q, List<PluginWrapper> cycle)
276 throws hudson.util.CyclicGraphDetector.CycleDetectedException {
277
278 LOGGER.log(Level.SEVERE, "found cycle in plugin dependencies: (root="+q+", deactivating all involved) "+Util.join(cycle," -> "));
279 for (PluginWrapper pluginWrapper : cycle) {
280 pluginWrapper.setHasCycleDependency(true);
281 failedPlugins.add(new FailedPlugin(pluginWrapper.getShortName(), new CycleDetectedException(cycle)));
282 }
283 }
284
285 };
286 cgd.run(getPlugins());
287
288 // obtain topologically sorted list and overwrite the list
289 ListIterator<PluginWrapper> litr = plugins.listIterator();
290 for (PluginWrapper p : cgd.getSorted()) {
291 litr.next();
292 litr.set(p);
293 if(p.isActive())
294 activePlugins.add(p);
295 }
296 } catch (CycleDetectedException e) {
297 stop(); // disable all plugins since classloading from them can lead to StackOverflow
298 throw e; // let Hudson fail
299 }
300 }
301 });
302
303 session.addAll(g.discoverTasks(session));
304
305 pluginListed = true; // technically speaking this is still too early, as at this point tasks are merely scheduled, not necessarily executed.
306 }
307 });
308 }
309 };
310 } else {
311 builder = TaskBuilder.EMPTY_BUILDER;
312 }
313
314 final InitializerFinder initializerFinder = new InitializerFinder(uberClassLoader); // misc. stuff
315
316 // lists up initialization tasks about loading plugins.
317 return TaskBuilder.union(initializerFinder, // this scans @Initializer in the core once
318 builder,new TaskGraphBuilder() {{
319 requires(PLUGINS_LISTED).attains(PLUGINS_PREPARED).add("Loading plugins",new Executable() {
320 /**
321 * Once the plugins are listed, schedule their initialization.
322 */
323 public void run(Reactor session) throws Exception {
324 Jenkins.getInstance().lookup.set(PluginInstanceStore.class,new PluginInstanceStore());
325 TaskGraphBuilder g = new TaskGraphBuilder();
326
327 // schedule execution of loading plugins
328 for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) {
329 g.followedBy().notFatal().attains(PLUGINS_PREPARED).add("Loading plugin " + p.getShortName(), new Executable() {
330 public void run(Reactor session) throws Exception {
331 try {
332 p.resolvePluginDependencies();
333 strategy.load(p);
334 } catch (IOException e) {
335 failedPlugins.add(new FailedPlugin(p.getShortName(), e));
336 activePlugins.remove(p);
337 plugins.remove(p);
338 throw e;
339 }
340 }
341 });
342 }
343
344 // schedule execution of initializing plugins
345 for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) {
346 g.followedBy().notFatal().attains(PLUGINS_STARTED).add("Initializing plugin " + p.getShortName(), new Executable() {
347 public void run(Reactor session) throws Exception {
348 if (!activePlugins.contains(p)) {
349 return;
350 }
351 try {
352 p.getPlugin().postInitialize();
353 } catch (Exception e) {
354 failedPlugins.add(new FailedPlugin(p.getShortName(), e));
355 activePlugins.remove(p);
356 plugins.remove(p);
357 throw e;
358 }
359 }
360 });
361 }
362
363 g.followedBy().attains(PLUGINS_STARTED).add("Discovering plugin initialization tasks", new Executable() {
364 public void run(Reactor reactor) throws Exception {
365 // rescan to find plugin-contributed @Initializer
366 reactor.addAll(initializerFinder.discoverTasks(reactor));
367 }
368 });
369
370 // register them all
371 session.addAll(g.discoverTasks(session));
372 }
373 });
374 }});
375 }
376
377 /*
378 * contains operation that considers xxx.hpi and xxx.jpi as equal
379 * this is necessary since the bundled plugins are still called *.hpi
380 */
381 private boolean containsHpiJpi(Collection<String> bundledPlugins, String name) {
382 return bundledPlugins.contains(name.replaceAll("\\.hpi",".jpi"))
383 || bundledPlugins.contains(name.replaceAll("\\.jpi",".hpi"));
384 }
385
386 /**
387 * TODO: revisit where/how to expose this. This is an experiment.
388 */
389 public void dynamicLoad(File arc) throws IOException, InterruptedException, RestartRequiredException {
390 LOGGER.info("Attempting to dynamic load "+arc);
391 final PluginWrapper p = strategy.createPluginWrapper(arc);
392 String sn = p.getShortName();
393 if (getPlugin(sn)!=null)
394 throw new RestartRequiredException(Messages._PluginManager_PluginIsAlreadyInstalled_RestartRequired(sn));
395
396 if (p.supportsDynamicLoad()== YesNoMaybe.NO)
397 throw new RestartRequiredException(Messages._PluginManager_PluginDoesntSupportDynamicLoad_RestartRequired(sn));
398
399 // there's no need to do cyclic dependency check, because we are deploying one at a time,
400 // so existing plugins can't be depending on this newly deployed one.
401
402 plugins.add(p);
403 activePlugins.add(p);
404
405 try {
406 p.resolvePluginDependencies();
407 strategy.load(p);
408
409 Jenkins.getInstance().refreshExtensions();
410
411 p.getPlugin().postInitialize();
412 } catch (Exception e) {
413 failedPlugins.add(new FailedPlugin(sn, e));
414 activePlugins.remove(p);
415 plugins.remove(p);
416 throw new IOException2("Failed to install "+ sn +" plugin",e);
417 }
418
419 // run initializers in the added plugin
420 Reactor r = new Reactor(InitMilestone.ordering());
421 r.addAll(new InitializerFinder(p.classLoader) {
422 @Override
423 protected boolean filter(Method e) {
424 return e.getDeclaringClass().getClassLoader()!=p.classLoader || super.filter(e);
425 }
426 }.discoverTasks(r));
427 try {
428 new InitReactorRunner().run(r);
429 } catch (ReactorException e) {
430 throw new IOException2("Failed to initialize "+ sn +" plugin",e);
431 }
432 LOGGER.info("Plugin " + sn + " dynamically installed");
433 }
434
435 /**
436 * If the war file has any "/WEB-INF/plugins/[*.jpi | *.hpi]", extract them into the plugin directory.
437 *
438 * @return
439 * File names of the bundled plugins. Like {"ssh-slaves.hpi","subvesrion.jpi"}
440 * @throws Exception
441 * Any exception will be reported and halt the startup.
442 */
443 protected abstract Collection<String> loadBundledPlugins() throws Exception;
444
445 /**
446 * Copies the bundled plugin from the given URL to the destination of the given file name (like 'abc.jpi'),
447 * with a reasonable up-to-date check. A convenience method to be used by the {@link #loadBundledPlugins()}.
448 */
449 protected void copyBundledPlugin(URL src, String fileName) throws IOException {
450 fileName = fileName.replace(".hpi",".jpi"); // normalize fileNames to have the correct suffix
451 String legacyName = fileName.replace(".jpi",".hpi");
452 long lastModified = src.openConnection().getLastModified();
453 File file = new File(rootDir, fileName);
454 File pinFile = new File(rootDir, fileName+".pinned");
455
456 // normalization first, if the old file exists.
457 rename(new File(rootDir,legacyName),file);
458 rename(new File(rootDir,legacyName+".pinned"),pinFile);
459
460 // update file if:
461 // - no file exists today
462 // - bundled version and current version differs (by timestamp), and the file isn't pinned.
463 if (!file.exists() || (file.lastModified() != lastModified && !pinFile.exists())) {
464 FileUtils.copyURLToFile(src, file);
465 file.setLastModified(src.openConnection().getLastModified());
466 // lastModified is set for two reasons:
467 // - to avoid unpacking as much as possible, but still do it on both upgrade and downgrade
468 // - to make sure the value is not changed after each restart, so we can avoid
469 // unpacking the plugin itself in ClassicPluginStrategy.explode
470 }
471 }
472
473 /**
474 * Rename a legacy file to a new name, with care to Windows where {@link File#renameTo(File)}
475 * doesn't work if the destination already exists.
476 */
477 private void rename(File legacyFile, File newFile) throws IOException {
478 if (!legacyFile.exists()) return;
479 if (newFile.exists()) {
480 Util.deleteFile(newFile);
481 }
482 if (!legacyFile.renameTo(newFile)) {
483 LOGGER.warning("Failed to rename " + legacyFile + " to " + newFile);
484 }
485 }
486
487 /**
488 * Creates a hudson.PluginStrategy, looking at the corresponding system property.
489 */
490 protected PluginStrategy createPluginStrategy() {
491 String strategyName = System.getProperty(PluginStrategy.class.getName());
492 if (strategyName != null) {
493 try {
494 Class<?> klazz = getClass().getClassLoader().loadClass(strategyName);
495 Object strategy = klazz.getConstructor(PluginManager.class)
496 .newInstance(this);
497 if (strategy instanceof PluginStrategy) {
498 LOGGER.info("Plugin strategy: " + strategyName);
499 return (PluginStrategy) strategy;
500 } else {
501 LOGGER.warning("Plugin strategy (" + strategyName +
502 ") is not an instance of hudson.PluginStrategy");
503 }
504 } catch (ClassNotFoundException e) {
505 LOGGER.warning("Plugin strategy class not found: "
506 + strategyName);
507 } catch (Exception e) {
508 LOGGER.log(WARNING, "Could not instantiate plugin strategy: "
509 + strategyName + ". Falling back to ClassicPluginStrategy", e);
510 }
511 LOGGER.info("Falling back to ClassicPluginStrategy");
512 }
513
514 // default and fallback
515 return new ClassicPluginStrategy(this);
516 }
517
518 public PluginStrategy getPluginStrategy() {
519 return strategy;
520 }
521
522 /**
523 * Returns true if any new plugin was added.
524 */
525 public boolean isPluginUploaded() {
526 return pluginUploaded;
527 }
528
529 /**
530 * All discovered plugins.
531 */
532 @Exported
533 public List<PluginWrapper> getPlugins() {
534 return plugins;
535 }
536
537 public List<FailedPlugin> getFailedPlugins() {
538 return failedPlugins;
539 }
540
541 /**
542 * Get the plugin instance with the given short name.
543 * @param shortName the short name of the plugin
544 * @return The plugin singleton or <code>null</code> if a plugin with the given short name does not exist.
545 */
546 public PluginWrapper getPlugin(String shortName) {
547 for (PluginWrapper p : plugins) {
548 if(p.getShortName().equals(shortName))
549 return p;
550 }
551 return null;
552 }
553
554 /**
555 * Get the plugin instance that implements a specific class, use to find your plugin singleton.
556 * Note: beware the classloader fun.
557 * @param pluginClazz The class that your plugin implements.
558 * @return The plugin singleton or <code>null</code> if for some reason the plugin is not loaded.
559 */
560 public PluginWrapper getPlugin(Class<? extends Plugin> pluginClazz) {
561 for (PluginWrapper p : plugins) {
562 if(pluginClazz.isInstance(p.getPlugin()))
563 return p;
564 }
565 return null;
566 }
567
568 /**
569 * Get the plugin instances that extend a specific class, use to find similar plugins.
570 * Note: beware the classloader fun.
571 * @param pluginSuperclass The class that your plugin is derived from.
572 * @return The list of plugins implementing the specified class.
573 */
574 public List<PluginWrapper> getPlugins(Class<? extends Plugin> pluginSuperclass) {
575 List<PluginWrapper> result = new ArrayList<PluginWrapper>();
576 for (PluginWrapper p : plugins) {
577 if(pluginSuperclass.isInstance(p.getPlugin()))
578 result.add(p);
579 }
580 return Collections.unmodifiableList(result);
581 }
582
583 public String getDisplayName() {
584 return Messages.PluginManager_DisplayName();
585 }
586
587 public String getSearchUrl() {
588 return "pluginManager";
589 }
590
591 /**
592 * Discover all the service provider implementations of the given class,
593 * via <tt>META-INF/services</tt>.
594 */
595 public <T> Collection<Class<? extends T>> discover( Class<T> spi ) {
596 Set<Class<? extends T>> result = new HashSet<Class<? extends T>>();
597
598 for (PluginWrapper p : activePlugins) {
599 Service.load(spi, p.classLoader, result);
600 }
601
602 return result;
603 }
604
605 /**
606 * Return the {@link PluginWrapper} that loaded the given class 'c'.
607 *
608 * @since 1.402.
609 */
610 public PluginWrapper whichPlugin(Class c) {
611 PluginWrapper oneAndOnly = null;
612 ClassLoader cl = c.getClassLoader();
613 for (PluginWrapper p : activePlugins) {
614 if (p.classLoader==cl) {
615 if (oneAndOnly!=null)
616 return null; // ambigious
617 oneAndOnly = p;
618 }
619 }
620 return oneAndOnly;
621 }
622
623 /**
624 * Orderly terminates all the plugins.
625 */
626 public void stop() {
627 for (PluginWrapper p : activePlugins) {
628 p.stop();
629 p.releaseClassLoader();
630 }
631 activePlugins.clear();
632 // Work around a bug in commons-logging.
633 // See http://www.szegedi.org/articles/memleak.html
634 LogFactory.release(uberClassLoader);
635 }
636
637 public HttpResponse doUpdateSources(StaplerRequest req) throws IOException {
638 Jenkins.getInstance().checkPermission(CONFIGURE_UPDATECENTER);
639
640 if (req.hasParameter("remove")) {
641 UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
642 BulkChange bc = new BulkChange(uc);
643 try {
644 for (String id : req.getParameterValues("sources"))
645 uc.getSites().remove(uc.getById(id));
646 } finally {
647 bc.commit();
648 }
649 } else
650 if (req.hasParameter("add"))
651 return new HttpRedirect("addSite");
652
653 return new HttpRedirect("./sites");
654 }
655
656 /**
657 * Performs the installation of the plugins.
658 */
659 public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
660 boolean dynamicLoad = req.getParameter("dynamicLoad")!=null;
661
662 Enumeration<String> en = req.getParameterNames();
663 while (en.hasMoreElements()) {
664 String n = en.nextElement();
665 if(n.startsWith("plugin.")) {
666 n = n.substring(7);
667 if (n.indexOf(".") > 0) {
668 String[] pluginInfo = n.split("\\.");
669 UpdateSite.Plugin p = Jenkins.getInstance().getUpdateCenter().getById(pluginInfo[1]).getPlugin(pluginInfo[0]);
670 if(p==null)
671 throw new Failure("No such plugin: "+n);
672 p.deploy(dynamicLoad);
673 }
674 }
675 }
676 rsp.sendRedirect("../updateCenter/");
677 }
678
679
680 /**
681 * Bare-minimum configuration mechanism to change the update center.
682 */
683 public HttpResponse doSiteConfigure(@QueryParameter String site) throws IOException {
684 Jenkins hudson = Jenkins.getInstance();
685 hudson.checkPermission(CONFIGURE_UPDATECENTER);
686 UpdateCenter uc = hudson.getUpdateCenter();
687 PersistedList<UpdateSite> sites = uc.getSites();
688 for (UpdateSite s : sites) {
689 if (s.getId().equals(UpdateCenter.ID_DEFAULT))
690 sites.remove(s);
691 }
692 sites.add(new UpdateSite(UpdateCenter.ID_DEFAULT, site));
693
694 return HttpResponses.redirectToContextRoot();
695 }
696
697
698 public HttpResponse doProxyConfigure(StaplerRequest req) throws IOException, ServletException {
699 Jenkins jenkins = Jenkins.getInstance();
700 jenkins.checkPermission(CONFIGURE_UPDATECENTER);
701
702 ProxyConfiguration pc = req.bindJSON(ProxyConfiguration.class, req.getSubmittedForm());
703 if (pc.name==null) {
704 jenkins.proxy = null;
705 ProxyConfiguration.getXmlFile().delete();
706 } else {
707 jenkins.proxy = pc;
708 jenkins.proxy.save();
709 }
710 return new HttpRedirect("advanced");
711 }
712
713 /**
714 * Uploads a plugin.
715 */
716 public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, ServletException {
717 try {
718 Jenkins.getInstance().checkPermission(UPLOAD_PLUGINS);
719
720 ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());
721
722 // Parse the request
723 FileItem fileItem = (FileItem) upload.parseRequest(req).get(0);
724 String fileName = Util.getFileName(fileItem.getName());
725 if("".equals(fileName)){
726 return new HttpRedirect("advanced");
727 }
728 // we allow the upload of the new jpi's and the legacy hpi's
729 if(!fileName.endsWith(".jpi") && !fileName.endsWith(".hpi")){
730 throw new Failure(hudson.model.Messages.Hudson_NotAPlugin(fileName));
731 }
732 final String baseName = FilenameUtils.getBaseName(fileName);
733 new File(rootDir, baseName + ".hpi").delete(); // don't keep confusing legacy *.hpi
734 fileItem.write(new File(rootDir, baseName + ".jpi")); // rename all new plugins to *.jpi
735 fileItem.delete();
736
737 PluginWrapper existing = getPlugin(baseName);
738 if (existing!=null && existing.isBundled){
739 existing.doPin();
740 }
741
742 pluginUploaded = true;
743
744 return new HttpRedirect(".");
745 } catch (IOException e) {
746 throw e;
747 } catch (Exception e) {// grrr. fileItem.write throws this
748 throw new ServletException(e);
749 }
750 }
751
752 public Descriptor<ProxyConfiguration> getProxyDescriptor() {
753 return Jenkins.getInstance().getDescriptor(ProxyConfiguration.class);
754 }
755
756 /**
757 * Prepares plugins for some expected XML configuration.
758 * If the configuration (typically a job’s {@code config.xml})
759 * needs some plugins to be installed (or updated), those jobs
760 * will be triggered.
761 * Plugins are dynamically loaded whenever possible.
762 * Requires {@link Jenkins#ADMINISTER}.
763 * @param configXml configuration that might be uploaded
764 * @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
765 * @throws IOException if loading or parsing the configuration failed
766 * @see ItemGroupMixIn#createProjectFromXML
767 * @see AbstractItem#updateByXml(javax.xml.transform.Source)
768 * @see XStream2
769 * @see hudson.model.UpdateSite.Plugin#deploy(boolean)
770 * @see PluginWrapper#supportsDynamicLoad
771 * @see hudson.model.UpdateCenter.DownloadJob.SuccessButRequiresRestart
772 * @since 1.483
773 */
774 public List<Future<UpdateCenter.UpdateCenterJob>> prevalidateConfig(InputStream configXml) throws IOException {
775 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
776 List<Future<UpdateCenter.UpdateCenterJob>> jobs = new ArrayList<Future<UpdateCenter.UpdateCenterJob>>();
777 UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
778 // XXX call uc.updateAllSites() when available? perhaps not, since we should not block on network here
779 for (Map.Entry<String,VersionNumber> requestedPlugin : parseRequestedPlugins(configXml).entrySet()) {
780 PluginWrapper pw = getPlugin(requestedPlugin.getKey());
781 if (pw == null) { // install new
782 UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey());
783 if (toInstall == null) {
784 LOGGER.log(WARNING, "No such plugin {0} to install", requestedPlugin.getKey());
785 continue;
786 }
787 if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) {
788 LOGGER.log(WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version});
789 }
790 if (toInstall.isForNewerHudson()) {
791 LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version});
792 }
793 jobs.add(toInstall.deploy(true));
794 } else if (pw.isOlderThan(requestedPlugin.getValue())) { // upgrade
795 UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey());
796 if (toInstall == null) {
797 LOGGER.log(WARNING, "No such plugin {0} to upgrade", requestedPlugin.getKey());
798 continue;
799 }
800 if (!pw.isOlderThan(new VersionNumber(toInstall.version))) {
801 LOGGER.log(WARNING, "{0}@{1} is no newer than what we already have", new Object[] {toInstall.name, toInstall.version});
802 continue;
803 }
804 if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) {
805 LOGGER.log(WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version});
806 }
807 if (toInstall.isForNewerHudson()) {
808 LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version});
809 }
810 if (!toInstall.isCompatibleWithInstalledVersion()) {
811 LOGGER.log(WARNING, "{0}@{1} is incompatible with the installed @{2}", new Object[] {toInstall.name, toInstall.version, pw.getVersion()});
812 }
813 jobs.add(toInstall.deploy(true)); // dynamicLoad=true => sure to throw RestartRequiredException, but at least message is nicer
814 } // else already good
815 }
816 return jobs;
817 }
818
819 /**
820 * Like {@link #doInstallNecessaryPlugins(StaplerRequest)} but only checks if everything is installed
821 * or if some plugins need updates or installation.
822 *
823 * This method runs without side-effect. I'm still requiring the ADMINISTER permission since
824 * XML file can contain various external references and we don't configure parsers properly against
825 * that.
826 *
827 * @since 1.483
828 */
829 @RequirePOST
830 public JSONArray doPrevalidateConfig(StaplerRequest req) throws IOException {
831 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
832
833 JSONArray response = new JSONArray();
834
835 for (Map.Entry<String,VersionNumber> p : parseRequestedPlugins(req.getInputStream()).entrySet()) {
836 PluginWrapper pw = getPlugin(p.getKey());
837 JSONObject j = new JSONObject()
838 .accumulate("name", p.getKey())
839 .accumulate("version", p.getValue().toString());
840 if (pw == null) { // install new
841 response.add(j.accumulate("mode", "missing"));
842 } else if (pw.isOlderThan(p.getValue())) { // upgrade
843 response.add(j.accumulate("mode", "old"));
844 } // else already good
845 }
846
847 return response;
848 }
849
850 /**
851 * Runs {@link #prevalidateConfig} on posted XML and redirects to the {@link UpdateCenter}.
852 * @since 1.483
853 */
854 @RequirePOST
855 public HttpResponse doInstallNecessaryPlugins(StaplerRequest req) throws IOException {
856 prevalidateConfig(req.getInputStream());
857 return HttpResponses.redirectViaContextPath("updateCenter");
858 }
859
860 /**
861 * Parses configuration XML files and picks up references to XML files.
862 */
863 public Map<String,VersionNumber> parseRequestedPlugins(InputStream configXml) throws IOException {
864 final Map<String,VersionNumber> requestedPlugins = new TreeMap<String,VersionNumber>();
865 try {
866 SAXParserFactory.newInstance().newSAXParser().parse(configXml, new DefaultHandler() {
867 @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
868 String plugin = attributes.getValue("plugin");
869 if (plugin == null) {
870 return;
871 }
872 if (!plugin.matches("[^@]+@[^@]+")) {
873 throw new SAXException("Malformed plugin attribute: " + plugin);
874 }
875 int at = plugin.indexOf('@');
876 String shortName = plugin.substring(0, at);
877 VersionNumber existing = requestedPlugins.get(shortName);
878 VersionNumber requested = new VersionNumber(plugin.substring(at + 1));
879 if (existing == null || existing.compareTo(requested) < 0) {
880 requestedPlugins.put(shortName, requested);
881 }
882 }
883 });
884 } catch (SAXException x) {
885 throw new IOException2("Failed to parse XML",x);
886 } catch (ParserConfigurationException e) {
887 throw new AssertionError(e); // impossible since we don't tweak XMLParser
888 }
889 return requestedPlugins;
890 }
891
892 /**
893 * {@link ClassLoader} that can see all plugins.
894 */
895 public final class UberClassLoader extends ClassLoader {
896 /**
897 * Make generated types visible.
898 * Keyed by the generated class name.
899 */
900 private ConcurrentMap<String, WeakReference<Class>> generatedClasses = new ConcurrentHashMap<String, WeakReference<Class>>();
901
902 private ClassLoaderReflectionToolkit clt = new ClassLoaderReflectionToolkit();
903
904 public UberClassLoader() {
905 super(PluginManager.class.getClassLoader());
906 }
907
908 public void addNamedClass(String className, Class c) {
909 generatedClasses.put(className,new WeakReference<Class>(c));
910 }
911
912 @Override
913 protected Class<?> findClass(String name) throws ClassNotFoundException {
914 WeakReference<Class> wc = generatedClasses.get(name);
915 if (wc!=null) {
916 Class c = wc.get();
917 if (c!=null) return c;
918 else generatedClasses.remove(name,wc);
919 }
920
921 if (FAST_LOOKUP) {
922 for (PluginWrapper p : activePlugins) {
923 try {
924 Class c = clt.findLoadedClass(p.classLoader,name);
925 if (c!=null) return c;
926 // calling findClass twice appears to cause LinkageError: duplicate class def
927 return clt.findClass(p.classLoader,name);
928 } catch (InvocationTargetException e) {
929 //not found. try next
930 }
931 }
932 } else {
933 for (PluginWrapper p : activePlugins) {
934 try {
935 return p.classLoader.loadClass(name);
936 } catch (ClassNotFoundException e) {
937 //not found. try next
938 }
939 }
940 }
941 // not found in any of the classloader. delegate.
942 throw new ClassNotFoundException(name);
943 }
944
945 @Override
946 protected URL findResource(String name) {
947 if (FAST_LOOKUP) {
948 try {
949 for (PluginWrapper p : activePlugins) {
950 URL url = clt.findResource(p.classLoader,name);
951 if(url!=null)
952 return url;
953 }
954 } catch (InvocationTargetException e) {
955 throw new Error(e);
956 }
957 } else {
958 for (PluginWrapper p : activePlugins) {
959 URL url = p.classLoader.getResource(name);
960 if(url!=null)
961 return url;
962 }
963 }
964 return null;
965 }
966
967 @Override
968 protected Enumeration<URL> findResources(String name) throws IOException {
969 List<URL> resources = new ArrayList<URL>();
970 if (FAST_LOOKUP) {
971 try {
972 for (PluginWrapper p : activePlugins) {
973 resources.addAll(Collections.list(clt.findResources(p.classLoader, name)));
974 }
975 } catch (InvocationTargetException e) {
976 throw new Error(e);
977 }
978 } else {
979 for (PluginWrapper p : activePlugins) {
980 resources.addAll(Collections.list(p.classLoader.getResources(name)));
981 }
982 }
983 return Collections.enumeration(resources);
984 }
985
986 @Override
987 public String toString() {
988 // only for debugging purpose
989 return "classLoader " + getClass().getName();
990 }
991 }
992
993 private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName());
994
995 public static boolean FAST_LOOKUP = !Boolean.getBoolean(PluginManager.class.getName()+".noFastLookup");
996
997 public static final Permission UPLOAD_PLUGINS = new Permission(Jenkins.PERMISSIONS, "UploadPlugins", Messages._PluginManager_UploadPluginsPermission_Description(),Jenkins.ADMINISTER,PermissionScope.JENKINS);
998 public static final Permission CONFIGURE_UPDATECENTER = new Permission(Jenkins.PERMISSIONS, "ConfigureUpdateCenter", Messages._PluginManager_ConfigureUpdateCenterPermission_Description(),Jenkins.ADMINISTER,PermissionScope.JENKINS);
999
1000 /**
1001 * Remembers why a plugin failed to deploy.
1002 */
1003 public static final class FailedPlugin {
1004 public final String name;
1005 public final Exception cause;
1006
1007 public FailedPlugin(String name, Exception cause) {
1008 this.name = name;
1009 this.cause = cause;
1010 }
1011
1012 public String getExceptionString() {
1013 return Functions.printThrowable(cause);
1014 }
1015 }
1016
1017 /**
1018 * Stores {@link Plugin} instances.
1019 */
1020 /*package*/ static final class PluginInstanceStore {
1021 final Map<PluginWrapper,Plugin> store = new Hashtable<PluginWrapper,Plugin>();
1022 }
1023
1024 /**
1025 * {@link AdministrativeMonitor} that checks if there are any plugins with cycle dependencies.
1026 */
1027 @Extension
1028 public static final class PluginCycleDependenciesMonitor extends AdministrativeMonitor {
1029
1030 private transient volatile boolean isActive = false;
1031
1032 private transient volatile List<String> pluginsWithCycle;
1033
1034 public boolean isActivated() {
1035 if(pluginsWithCycle == null){
1036 pluginsWithCycle = new ArrayList<String>();
1037 for (PluginWrapper p : Jenkins.getInstance().getPluginManager().getPlugins()) {
1038 if(p.hasCycleDependency()){
1039 pluginsWithCycle.add(p.getShortName());
1040 isActive = true;
1041 }
1042 }
1043 }
1044 return isActive;
1045 }
1046
1047 public List<String> getPluginsWithCycle() {
1048 return pluginsWithCycle;
1049 }
1050 }
1051
1052 /**
1053 * {@link AdministrativeMonitor} that informs the administrator about a required plugin update.
1054 * @since 1.491
1055 */
1056 @Extension
1057 public static final class PluginUpdateMonitor extends AdministrativeMonitor {
1058
1059 private Map<String, PluginUpdateInfo> pluginsToBeUpdated = new HashMap<String, PluginManager.PluginUpdateMonitor.PluginUpdateInfo>();
1060
1061 /**
1062 * Convenience method to ease access to this monitor, this allows other plugins to register required updates.
1063 * @return this monitor.
1064 */
1065 public static final PluginUpdateMonitor getInstance() {
1066 return Jenkins.getInstance().getExtensionList(PluginUpdateMonitor.class).get(0);
1067 }
1068
1069 /**
1070 * Report to the administrator if the plugin with the given name is older then the required version.
1071 *
1072 * @param pluginName shortName of the plugin (artifactId)
1073 * @param requiredVersion the lowest version which is OK (e.g. 2.2.2)
1074 * @param message the message to show (plain text)
1075 */
1076 public void ifPluginOlderThenReport(String pluginName, String requiredVersion, String message){
1077 Plugin plugin = Jenkins.getInstance().getPlugin(pluginName);
1078 if(plugin != null){
1079 if(plugin.getWrapper().getVersionNumber().isOlderThan(new VersionNumber(requiredVersion))) {
1080 pluginsToBeUpdated.put(pluginName, new PluginUpdateInfo(pluginName, message));
1081 }
1082 }
1083 }
1084
1085 public boolean isActivated() {
1086 return !pluginsToBeUpdated.isEmpty();
1087 }
1088
1089 /**
1090 * adds a message about a plugin to the manage screen
1091 * @param pluginName the plugins name
1092 * @param message the message to be displayed
1093 */
1094 public void addPluginToUpdate(String pluginName, String message) {
1095 this.pluginsToBeUpdated.put(pluginName, new PluginUpdateInfo(pluginName, message));
1096 }
1097
1098 public Collection<PluginUpdateInfo> getPluginsToBeUpdated() {
1099 return pluginsToBeUpdated.values();
1100 }
1101
1102 public static class PluginUpdateInfo {
1103 public final String pluginName;
1104 public final String message;
1105 private PluginUpdateInfo(String pluginName, String message) {
1106 this.pluginName = pluginName;
1107 this.message = message;
1108 }
1109 }
1110
1111 }
1112 }
1113