PluginManager.java 16.0 KB
Newer Older
K
kohsuke 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, Tom Huybrechts
 * 
 * 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.
 */
K
kohsuke 已提交
24 25
package hudson;

26
import hudson.model.*;
K
kohsuke 已提交
27 28
import hudson.util.Service;

29
import java.util.Enumeration;
K
kohsuke 已提交
30
import javax.servlet.ServletContext;
31
import javax.servlet.ServletException;
K
kohsuke 已提交
32 33 34 35 36 37
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
38
import java.util.Collections;
K
kohsuke 已提交
39 40 41
import java.util.HashSet;
import java.util.List;
import java.util.Set;
42
import java.util.Arrays;
K
kohsuke 已提交
43
import java.util.logging.Level;
K
kohsuke 已提交
44 45
import java.util.logging.Logger;

46
import org.apache.commons.logging.LogFactory;
47 48 49
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.FileItem;
50
import org.apache.commons.io.FileUtils;
51 52
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
53
import org.kohsuke.stapler.QueryParameter;
54
import org.kohsuke.stapler.WebApp;
55

K
kohsuke 已提交
56 57 58 59 60
/**
 * Manages {@link PluginWrapper}s.
 *
 * @author Kohsuke Kawaguchi
 */
61
public final class PluginManager extends AbstractModelObject {
K
kohsuke 已提交
62 63 64 65 66 67 68 69 70 71
    /**
     * All discovered plugins.
     */
    private final List<PluginWrapper> plugins = new ArrayList<PluginWrapper>();

    /**
     * All active plugins.
     */
    private final List<PluginWrapper> activePlugins = new ArrayList<PluginWrapper>();

72 73
    private final List<FailedPlugin> failedPlugins = new ArrayList<FailedPlugin>();

K
kohsuke 已提交
74 75 76 77 78 79 80 81 82 83 84 85
    /**
     * Plug-in root directory.
     */
    public final File rootDir;

    public final ServletContext context;

    /**
     * {@link ClassLoader} that can load all the publicly visible classes from plugins
     * (and including the classloader that loads Hudson itself.)
     *
     */
K
kohsuke 已提交
86
    // implementation is minimal --- just enough to run XStream
K
kohsuke 已提交
87 88 89
    // and load plugin-contributed classes.
    public final ClassLoader uberClassLoader = new UberClassLoader();

90 91 92 93 94
    /**
     * Once plugin is uploaded, this flag becomes true.
     * This is used to report a message that Hudson needs to be restarted
     * for new plugins to take effect.
     */
95
    public volatile boolean pluginUploaded =false;
96 97 98 99 100
    
    /**
     * Strategy for creating and initializing plugins
     */
    private PluginStrategy strategy;
101

K
kohsuke 已提交
102 103
    public PluginManager(ServletContext context) {
        this.context = context;
104
        // JSON binding needs to be able to see all the classes from all the plugins
105
        WebApp.get(context).setClassLoader(uberClassLoader);
106

K
kohsuke 已提交
107 108 109 110
        rootDir = new File(Hudson.getInstance().getRootDir(),"plugins");
        if(!rootDir.exists())
            rootDir.mkdirs();

111 112
        loadBundledPlugins();

K
kohsuke 已提交
113 114 115 116 117 118 119 120 121 122 123
        File[] archives = rootDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".hpi")        // plugin jar file
                    || name.endsWith(".hpl");       // linked plugin. for debugging.
            }
        });

        if(archives==null) {
            LOGGER.severe("Hudson is unable to create "+rootDir+"\nPerhaps its security privilege is insufficient");
            return;
        }
124 125
        
        strategy = createPluginStrategy();
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140

        // load plugins from a system property, for use in the "mvn hudson-dev:run"
        List<File> archivesList = new ArrayList<File>(Arrays.asList(archives));
        String hplProperty = System.getProperty("hudson.bundled.plugins");
        if (hplProperty != null) {
            for (String hplLocation: hplProperty.split(",")) {
                File hpl = new File(hplLocation.trim());
                if (hpl.exists())
                    archivesList.add(hpl);
                else
                    LOGGER.warning("bundled plugin " + hplLocation + " does not exist");
            }
        }

        for( File arc : archivesList ) {
K
kohsuke 已提交
141
            try {
142
                PluginWrapper p = strategy.createPluginWrapper(arc);
K
kohsuke 已提交
143 144 145 146
                plugins.add(p);
                if(p.isActive())
                    activePlugins.add(p);
            } catch (IOException e) {
147
                failedPlugins.add(new FailedPlugin(arc.getName(),e));
K
kohsuke 已提交
148 149 150
                LOGGER.log(Level.SEVERE, "Failed to load a plug-in " + arc, e);
            }
        }
151

152
        for (PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()]))
153
            try {
154
            	strategy.load(p);
155 156 157 158 159 160
            } catch (IOException e) {
                failedPlugins.add(new FailedPlugin(p.getShortName(),e));
                LOGGER.log(Level.SEVERE, "Failed to load a plug-in " + p.getShortName(), e);
                activePlugins.remove(p);
                plugins.remove(p);
            }
161

162
		for (PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) {
163 164
			strategy.initializeComponents(p);
		}
K
kohsuke 已提交
165 166
    }

167 168 169 170
    /**
     * If the war file has any "/WEB-INF/plugins/*.hpi", extract them into the plugin directory.
     */
    private void loadBundledPlugins() {
171 172 173 174
        // this is used in tests, when we want to override the default bundled plugins with .hpl versions
        if (System.getProperty("hudson.bundled.plugins") != null) {
            return;
        }
175 176 177 178 179
        Set paths = context.getResourcePaths("/WEB-INF/plugins");
        if(paths==null) return; // crap
        for( String path : (Set<String>) paths) {
            String fileName = path.substring(path.lastIndexOf('/')+1);
            try {
180 181 182 183 184 185 186 187 188 189 190
                URL url = context.getResource(path);
                long lastModified = url.openConnection().getLastModified();
                File file = new File(rootDir, fileName);
                if (file.exists() && file.lastModified() != lastModified) {
                    FileUtils.copyURLToFile(url, file);
                    file.setLastModified(url.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
                }
191 192 193 194 195 196
            } catch (IOException e) {
                LOGGER.log(Level.SEVERE, "Failed to extract the bundled plugin "+fileName,e);
            }
        }
    }

197
    /**
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
     * Creates a hudson.PluginStrategy, looking at the corresponding system property. 
     */
	private 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(Level.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;
	}

	/**
233 234 235 236 237 238
     * Retrurns true if any new plugin was added, which means a restart is required for the change to take effect.
     */
    public boolean isPluginUploaded() {
        return pluginUploaded;
    }
    
K
kohsuke 已提交
239 240 241 242
    public List<PluginWrapper> getPlugins() {
        return plugins;
    }

243 244 245 246
    public List<FailedPlugin> getFailedPlugins() {
        return failedPlugins;
    }

K
kohsuke 已提交
247 248 249 250 251 252 253 254
    public PluginWrapper getPlugin(String shortName) {
        for (PluginWrapper p : plugins) {
            if(p.getShortName().equals(shortName))
                return p;
        }
        return null;
    }

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
    /**
     * 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);
    }

284 285 286 287 288 289 290 291
    public String getDisplayName() {
        return "Plugin Manager";
    }

    public String getSearchUrl() {
        return "pluginManager";
    }

K
kohsuke 已提交
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
    /**
     * 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;
    }

    /**
     * Orderly terminates all the plugins.
     */
    public void stop() {
        for (PluginWrapper p : activePlugins) {
            p.stop();
        }
313 314 315
        // Work around a bug in commons-logging.
        // See http://www.szegedi.org/articles/memleak.html
        LogFactory.release(uberClassLoader);
K
kohsuke 已提交
316 317
    }

318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
    /**
     * Performs the installation of the plugins.
     */
    public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        Enumeration<String> en = req.getParameterNames();
        while (en.hasMoreElements()) {
            String n =  en.nextElement();
            if(n.startsWith("plugin.")) {
                n = n.substring(7);
                UpdateCenter.Plugin p = Hudson.getInstance().getUpdateCenter().getPlugin(n);
                if(p==null) {
                    sendError("No such plugin: "+n,req,rsp);
                    return;
                }
                p.install();
            }
        }
        rsp.sendRedirect("../updateCenter/");
    }

338 339 340 341 342 343
    public void doProxyConfigure(
            @QueryParameter("proxy.server") String server,
            @QueryParameter("proxy.port") String port,
            @QueryParameter("proxy.userName") String userName,
            @QueryParameter("proxy.password") String password,
            StaplerResponse rsp) throws IOException {
344 345
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);

346 347 348 349 350 351
        Hudson hudson = Hudson.getInstance();
        server = Util.fixEmptyAndTrim(server);
        if(server==null) {
            hudson.proxy = null;
            ProxyConfiguration.getXmlFile().delete();
        } else {
352 353
            hudson.proxy = new ProxyConfiguration(server,Integer.parseInt(Util.fixEmptyAndTrim(port)),
                    Util.fixEmptyAndTrim(userName),Util.fixEmptyAndTrim(password));
354 355
            hudson.proxy.save();
        }
356 357 358
        rsp.sendRedirect("./advanced");
    }

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    /**
     * Uploads a plugin.
     */
    public void doUploadPlugin( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
        try {
            Hudson.getInstance().checkPermission(Hudson.ADMINISTER);

            ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());

            // Parse the request
            FileItem fileItem = (FileItem) upload.parseRequest(req).get(0);
            String fileName = Util.getFileName(fileItem.getName());
            if(!fileName.endsWith(".hpi")) {
                sendError(hudson.model.Messages.Hudson_NotAPlugin(fileName),req,rsp);
                return;
            }
            fileItem.write(new File(rootDir, fileName));
            fileItem.delete();

            pluginUploaded=true;

            rsp.sendRedirect2(".");
        } catch (IOException e) {
            throw e;
        } catch (Exception e) {// grrr. fileItem.write throws this
            throw new ServletException(e);
        }
    }

K
kohsuke 已提交
388 389 390 391 392 393 394
    private final class UberClassLoader extends ClassLoader {
        public UberClassLoader() {
            super(PluginManager.class.getClassLoader());
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
395 396 397
            // first, use the context classloader so that plugins that are loading
            // can use its own classloader first.
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
398
            if(cl!=null && cl!=this)
399 400 401 402 403 404
                try {
                    return cl.loadClass(name);
                } catch(ClassNotFoundException e) {
                    // not found. try next
                }

K
kohsuke 已提交
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
            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) {
            for (PluginWrapper p : activePlugins) {
                URL url = p.classLoader.getResource(name);
                if(url!=null)
                    return url;
            }
            return null;
        }
425 426 427 428 429 430 431 432 433

        @Override
        protected Enumeration<URL> findResources(String name) throws IOException {
            List<URL> resources = new ArrayList<URL>();
            for (PluginWrapper p : activePlugins) {
                resources.addAll(Collections.list(p.classLoader.getResources(name)));
            }
            return Collections.enumeration(resources);
        }
K
kohsuke 已提交
434 435 436 437
    }

    private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName());

438 439 440 441 442 443 444 445 446 447 448 449 450
    /**
     * Remembers why a plugin failed to deploy.
     */
    public static final class FailedPlugin {
        public final String name;
        public final IOException cause;

        public FailedPlugin(String name, IOException cause) {
            this.name = name;
            this.cause = cause;
        }

        public String getExceptionString() {
451
            return Functions.printThrowable(cause);
452 453
        }
    }
K
kohsuke 已提交
454
}