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

K
kohsuke 已提交
26
import hudson.model.Hudson;
27
import hudson.model.UpdateCenter;
28
import hudson.model.UpdateSite;
29
import hudson.util.VersionNumber;
K
kohsuke 已提交
30 31 32 33 34

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
35
import java.io.Closeable;
K
kohsuke 已提交
36 37 38 39
import java.net.URL;
import java.util.List;
import java.util.jar.Manifest;
import java.util.logging.Logger;
40
import static java.util.logging.Level.WARNING;
K
kohsuke 已提交
41

42
import org.apache.commons.logging.LogFactory;
43 44
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
45

K
kohsuke 已提交
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
/**
 * Represents a Hudson plug-in and associated control information
 * for Hudson to control {@link Plugin}.
 *
 * <p>
 * A plug-in is packaged into a jar file whose extension is <tt>".hpi"</tt>,
 * A plugin needs to have a special manifest entry to identify what it is.
 *
 * <p>
 * At the runtime, a plugin has two distinct state axis.
 * <ol>
 *  <li>Enabled/Disabled. If enabled, Hudson is going to use it
 *      next time Hudson runs. Otherwise the next run will ignore it.
 *  <li>Activated/Deactivated. If activated, that means Hudson is using
 *      the plugin in this session. Otherwise it's not.
 * </ol>
 * <p>
 * For example, an activated but disabled plugin is still running but the next
 * time it won't.
 *
 * @author Kohsuke Kawaguchi
 */
public final class PluginWrapper {
    /**
     * Plugin manifest.
     * Contains description of the plugin.
     */
    private final Manifest manifest;

    /**
     * Loaded plugin instance.
     * Null if disabled.
     */
79
    private Plugin plugin;
K
kohsuke 已提交
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

    /**
     * {@link ClassLoader} for loading classes from this plugin.
     * Null if disabled.
     */
    public final ClassLoader classLoader;

    /**
     * Base URL for loading static resources from this plugin.
     * Null if disabled. The static resources are mapped under
     * <tt>hudson/plugin/SHORTNAME/</tt>.
     */
    public final URL baseResourceURL;

    /**
     * Used to control enable/disable setting of the plugin.
     * If this file exists, plugin will be disabled.
     */
    private final File disableFile;

100 101 102 103 104 105 106 107 108
    /**
     * Used to control the unpacking of the bundled plugin.
     * If a pin file exists, Hudson assumes that the user wants to pin down a particular version
     * of a plugin, and will not try to overwrite it. Otherwise, it'll be overwritten
     * by a bundled copy, to ensure consistency across upgrade/downgrade.
     * @since 1.325
     */
    private final File pinFile;

K
kohsuke 已提交
109
    /**
110 111 112 113
     * Short name of the plugin. The artifact Id of the plugin.
     * This is also used in the URL within Hudson, so it needs
     * to remain stable even when the *.hpi file name is changed
     * (like Maven does.)
K
kohsuke 已提交
114 115 116
     */
    private final String shortName;

117
    /**
118 119 120 121 122
     * True if this plugin is activated for this session.
     * The snapshot of <tt>disableFile.exists()</tt> as of the start up.
     */
    private final boolean active;

123
    private final List<Dependency> dependencies;
124
    private final List<Dependency> optionalDependencies;
125

126 127 128 129 130
    /**
     * Is this plugin bundled in hudson.war?
     */
    /*package*/ boolean isBundled;

131
    static final class Dependency {
132 133
        public final String shortName;
        public final String version;
134
        public final boolean optional;
135 136 137

        public Dependency(String s) {
            int idx = s.indexOf(':');
K
kohsuke 已提交
138 139
            if(idx==-1)
                throw new IllegalArgumentException("Illegal dependency specifier "+s);
140 141
            this.shortName = s.substring(0,idx);
            this.version = s.substring(idx+1);
142 143 144 145 146 147 148 149 150 151
            
            boolean isOptional = false;
            String[] osgiProperties = s.split(";");
            for (int i = 1; i < osgiProperties.length; i++) {
                String osgiProperty = osgiProperties[i].trim();
                if (osgiProperty.equalsIgnoreCase("resolution:=optional")) {
                    isOptional = true;
                }
            }
            this.optional = isOptional;
152
        }
153 154 155 156 157

        @Override
        public String toString() {
            return shortName + " (" + version + ")";
        }        
158 159
    }

K
kohsuke 已提交
160 161 162
    /**
     * @param archive
     *      A .hpi archive file jar file, or a .hpl linked plugin.
163 164 165 166 167 168 169 170 171 172
     *  @param manifest
     *  	The manifest for the plugin
     *  @param baseResourceURL
     *  	A URL pointing to the resources for this plugin
     *  @param classLoader
     *  	a classloader that loads classes from this plugin and its dependencies
     *  @param disableFile
     *  	if this file exists on startup, the plugin will not be activated
     *  @param dependencies a list of mandatory dependencies
     *  @param optionalDependencies a list of optional dependencies
K
kohsuke 已提交
173
     */
174 175 176 177 178 179 180 181
	public PluginWrapper(File archive, Manifest manifest, URL baseResourceURL, 
			ClassLoader classLoader, File disableFile, 
			List<Dependency> dependencies, List<Dependency> optionalDependencies) {
		this.manifest = manifest;
		this.shortName = computeShortName(manifest, archive);
		this.baseResourceURL = baseResourceURL;
		this.classLoader = classLoader;
		this.disableFile = disableFile;
182
        this.pinFile = new File(archive.getPath() + ".pinned");
183 184 185 186
		this.active = !disableFile.exists();
		this.dependencies = dependencies;
		this.optionalDependencies = optionalDependencies;
	}
K
kohsuke 已提交
187

188 189 190 191 192
    /**
     * Returns the URL of the index page jelly script.
     */
    public URL getIndexPage() {
        return classLoader.getResource("index.jelly");
193 194
    }

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
    private String computeShortName(Manifest manifest, File archive) {
        // use the name captured in the manifest, as often plugins
        // depend on the specific short name in its URLs.
        String n = manifest.getMainAttributes().getValue("Short-Name");
        if(n!=null)     return n;

        // maven seems to put this automatically, so good fallback to check.
        n = manifest.getMainAttributes().getValue("Extension-Name");
        if(n!=null)     return n;

        // otherwise infer from the file name, since older plugins don't have
        // this entry.
        return getBaseName(archive);
    }

K
kohsuke 已提交
210

211
    /**
212
     * Gets the "abc" portion from "abc.ext".
213
     */
214 215 216 217 218 219
    static String getBaseName(File archive) {
        String n = archive.getName();
        int idx = n.lastIndexOf('.');
        if(idx>=0)
            n = n.substring(0,idx);
        return n;
220 221
    }

222
    public List<Dependency> getDependencies() {
223 224
        return dependencies;
    }
225

226 227 228
    public List<Dependency> getOptionalDependencies() {
        return optionalDependencies;
    }
K
kohsuke 已提交
229 230 231 232 233 234 235 236 237


    /**
     * Returns the short name suitable for URL.
     */
    public String getShortName() {
        return shortName;
    }

238 239 240 241 242 243 244
    /**
     * Gets the instance of {@link Plugin} contributed by this plugin.
     */
    public Plugin getPlugin() {
        return plugin;
    }

245 246 247 248 249 250 251 252 253 254 255 256
    /**
     * Gets the URL that shows more information about this plugin.
     * @return
     *      null if this information is unavailable.
     * @since 1.283
     */
    public String getUrl() {
        // first look for the manifest entry. This is new in maven-hpi-plugin 1.30
        String url = manifest.getMainAttributes().getValue("Url");
        if(url!=null)      return url;

        // fallback to update center metadata
257
        UpdateSite.Plugin ui = getInfo();
258 259 260 261 262
        if(ui!=null)    return ui.wiki;

        return null;
    }

263 264 265 266 267
    @Override
    public String toString() {
        return "Plugin:" + getShortName();
    }

K
kohsuke 已提交
268 269 270 271
    /**
     * Returns a one-line descriptive name of this plugin.
     */
    public String getLongName() {
272
        String name = manifest.getMainAttributes().getValue("Long-Name");
K
kohsuke 已提交
273 274 275 276
        if(name!=null)      return name;
        return shortName;
    }

277 278 279 280 281 282
    /**
     * Returns the version number of this plugin
     */
    public String getVersion() {
        String v = manifest.getMainAttributes().getValue("Plugin-Version");
        if(v!=null)      return v;
K
kohsuke 已提交
283 284 285 286 287

        // plugins generated before maven-hpi-plugin 1.3 should still have this attribute
        v = manifest.getMainAttributes().getValue("Implementation-Version");
        if(v!=null)      return v;

288 289 290
        return "???";
    }

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
    /**
     * Returns the version number of this plugin
     */
    public VersionNumber getVersionNumber() {
        return new VersionNumber(getVersion());
    }

    /**
     * Returns true if the version of this plugin is older than the given version.
     */
    public boolean isOlderThan(VersionNumber v) {
        try {
            return getVersionNumber().compareTo(v) < 0;
        } catch (IllegalArgumentException e) {
            // if we can't figure out our current version, it probably means it's very old,
            // since the version information is missing only from the very old plugins 
            return true;
        }
    }

K
kohsuke 已提交
311 312 313 314 315 316 317 318
    /**
     * Terminates the plugin.
     */
    void stop() {
        LOGGER.info("Stopping "+shortName);
        try {
            plugin.stop();
        } catch(Throwable t) {
K
kohsuke 已提交
319
            LOGGER.log(WARNING, "Failed to shut down "+shortName, t);
K
kohsuke 已提交
320
        }
321 322 323
        // Work around a bug in commons-logging.
        // See http://www.szegedi.org/articles/memleak.html
        LogFactory.release(classLoader);
324 325 326 327 328 329 330
        
        if (classLoader instanceof Closeable)
            try {
                ((Closeable) classLoader).close();
            } catch (IOException e) {
                LOGGER.log(WARNING, "Failed to shut down classloader",e);
            }
K
kohsuke 已提交
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
    }

    /**
     * Enables this plugin next time Hudson runs.
     */
    public void enable() throws IOException {
        if(!disableFile.delete())
            throw new IOException("Failed to delete "+disableFile);
    }

    /**
     * Disables this plugin next time Hudson runs.
     */
    public void disable() throws IOException {
        // creates an empty file
        OutputStream os = new FileOutputStream(disableFile);
        os.close();
    }

    /**
     * Returns true if this plugin is enabled for this session.
     */
    public boolean isActive() {
354
        return active;
K
kohsuke 已提交
355 356
    }

357 358 359 360
    public boolean isBundled() {
        return isBundled;
    }

K
kohsuke 已提交
361 362 363 364 365 366 367 368
    /**
     * If true, the plugin is going to be activated next time
     * Hudson runs.
     */
    public boolean isEnabled() {
        return !disableFile.exists();
    }

369
    public Manifest getManifest() {
370 371
        return manifest;
    }
372

373 374 375 376 377 378 379 380
    public void setPlugin(Plugin plugin) {
        this.plugin = plugin;
        plugin.wrapper = this;
    }

    public String getPluginClass() {
        return manifest.getMainAttributes().getValue("Plugin-Class");
    }
381

382 383
    /**
     * If the plugin has {@link #getUpdateInfo() an update},
384
     * returns the {@link UpdateSite.Plugin} object.
K
kohsuke 已提交
385 386 387 388
     *
     * @return
     *      This method may return null &mdash; for example,
     *      the user may have installed a plugin locally developed.
389
     */
390
    public UpdateSite.Plugin getUpdateInfo() {
391
        UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
392
        UpdateSite.Plugin p = uc.getPlugin(getShortName());
K
kohsuke 已提交
393 394
        if(p!=null && p.isNewerThan(getVersion())) return p;
        return null;
395
    }
396 397
    
    /**
398
     * returns the {@link UpdateSite.Plugin} object, or null.
399
     */
400
    public UpdateSite.Plugin getInfo() {
401 402 403
        UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
        return uc.getPlugin(getShortName());
    }
404 405 406 407 408 409 410 411 412 413 414 415

    /**
     * Returns true if this plugin has update in the update center.
     *
     * <p>
     * This method is conservative in the sense that if the version number is incomprehensible,
     * it always returns false.
     */
    public boolean hasUpdate() {
        return getUpdateInfo()!=null;
    }

K
kohsuke 已提交
416 417 418 419 420
//
//
// Action methods
//
//
421
    public HttpResponse doMakeEnabled() throws IOException {
422
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
423
        enable();
424
        return HttpResponses.ok();
K
kohsuke 已提交
425
    }
426

427
    public HttpResponse doMakeDisabled() throws IOException {
428
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
429
        disable();
430 431 432 433 434 435 436 437 438 439 440 441 442
        return HttpResponses.ok();
    }

    public HttpResponse doPin() throws IOException {
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
        new FileOutputStream(pinFile).close();
        return HttpResponses.ok();
    }

    public HttpResponse doUnpin() throws IOException {
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
        pinFile.delete();
        return HttpResponses.ok();
K
kohsuke 已提交
443 444 445 446 447 448
    }


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

}