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

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

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

45
import org.apache.commons.logging.LogFactory;
46 47
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
48

49 50 51
import java.util.Enumeration;
import java.util.jar.JarFile;

K
kohsuke 已提交
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
/**
 * 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
 */
74
public class PluginWrapper implements Comparable<PluginWrapper> {
K
kohsuke 已提交
75
    /**
76
     * {@link PluginManager} to which this belongs to.
K
kohsuke 已提交
77
     */
78
    public final PluginManager parent;
K
kohsuke 已提交
79 80

    /**
81 82
     * Plugin manifest.
     * Contains description of the plugin.
K
kohsuke 已提交
83
     */
84
    private final Manifest manifest;
K
kohsuke 已提交
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

    /**
     * {@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;

105 106 107 108 109 110 111 112 113
    /**
     * 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 已提交
114
    /**
115 116 117 118
     * 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 已提交
119 120 121
     */
    private final String shortName;

122
    /**
123 124 125 126 127
     * 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;

128
    private final List<Dependency> dependencies;
129
    private final List<Dependency> optionalDependencies;
130

131 132 133 134 135
    /**
     * Is this plugin bundled in hudson.war?
     */
    /*package*/ boolean isBundled;

136
    static final class Dependency {
137 138
        public final String shortName;
        public final String version;
139
        public final boolean optional;
140 141 142

        public Dependency(String s) {
            int idx = s.indexOf(':');
K
kohsuke 已提交
143 144
            if(idx==-1)
                throw new IllegalArgumentException("Illegal dependency specifier "+s);
145 146
            this.shortName = s.substring(0,idx);
            this.version = s.substring(idx+1);
147 148 149 150 151 152 153 154 155 156
            
            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;
157
        }
158 159 160 161 162

        @Override
        public String toString() {
            return shortName + " (" + version + ")";
        }        
163 164
    }

K
kohsuke 已提交
165 166 167
    /**
     * @param archive
     *      A .hpi archive file jar file, or a .hpl linked plugin.
168 169 170 171 172 173 174 175 176 177
     *  @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 已提交
178
     */
179
    public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL baseResourceURL, 
180 181
			ClassLoader classLoader, File disableFile, 
			List<Dependency> dependencies, List<Dependency> optionalDependencies) {
182
        this.parent = parent;
183 184 185 186 187
		this.manifest = manifest;
		this.shortName = computeShortName(manifest, archive);
		this.baseResourceURL = baseResourceURL;
		this.classLoader = classLoader;
		this.disableFile = disableFile;
188
        this.pinFile = new File(archive.getPath() + ".pinned");
189 190 191
		this.active = !disableFile.exists();
		this.dependencies = dependencies;
		this.optionalDependencies = optionalDependencies;
192
    }
K
kohsuke 已提交
193

194 195 196 197
    /**
     * Returns the URL of the index page jelly script.
     */
    public URL getIndexPage() {
198 199 200 201 202 203 204 205
        // In the current impl dependencies are checked first, so the plugin itself
        // will add the last entry in the getResources result.
        URL idx = null;
        try {
            Enumeration<URL> en = classLoader.getResources("index.jelly");
            while (en.hasMoreElements())
                idx = en.nextElement();
        } catch (IOException ignore) { }
206 207 208
        // In case plugin has dependencies but is missing its own index.jelly,
        // check that result has this plugin's artifactId in it:
        return idx != null && idx.toString().contains(shortName) ? idx : null;
209 210
    }

211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    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 已提交
226

227
    /**
228
     * Gets the "abc" portion from "abc.ext".
229
     */
230 231 232 233 234 235
    static String getBaseName(File archive) {
        String n = archive.getName();
        int idx = n.lastIndexOf('.');
        if(idx>=0)
            n = n.substring(0,idx);
        return n;
236 237
    }

238
    public List<Dependency> getDependencies() {
239 240
        return dependencies;
    }
241

242 243 244
    public List<Dependency> getOptionalDependencies() {
        return optionalDependencies;
    }
K
kohsuke 已提交
245 246 247 248 249 250 251 252 253


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

254 255 256 257
    /**
     * Gets the instance of {@link Plugin} contributed by this plugin.
     */
    public Plugin getPlugin() {
258
        return Hudson.lookup(PluginInstanceStore.class).store.get(this);
259 260
    }

261 262 263 264 265 266 267 268 269 270 271 272
    /**
     * 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
273
        UpdateSite.Plugin ui = getInfo();
274 275 276 277 278
        if(ui!=null)    return ui.wiki;

        return null;
    }

279 280 281 282 283
    @Override
    public String toString() {
        return "Plugin:" + getShortName();
    }

K
kohsuke 已提交
284 285 286 287
    /**
     * Returns a one-line descriptive name of this plugin.
     */
    public String getLongName() {
288
        String name = manifest.getMainAttributes().getValue("Long-Name");
K
kohsuke 已提交
289 290 291 292
        if(name!=null)      return name;
        return shortName;
    }

293 294 295 296 297 298
    /**
     * Returns the version number of this plugin
     */
    public String getVersion() {
        String v = manifest.getMainAttributes().getValue("Plugin-Version");
        if(v!=null)      return v;
K
kohsuke 已提交
299 300 301 302 303

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

304 305 306
        return "???";
    }

307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
    /**
     * 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 已提交
327 328 329
    /**
     * Terminates the plugin.
     */
330
    public void stop() {
K
kohsuke 已提交
331 332
        LOGGER.info("Stopping "+shortName);
        try {
333
            getPlugin().stop();
K
kohsuke 已提交
334
        } catch(Throwable t) {
K
kohsuke 已提交
335
            LOGGER.log(WARNING, "Failed to shut down "+shortName, t);
K
kohsuke 已提交
336
        }
337 338 339
        // Work around a bug in commons-logging.
        // See http://www.szegedi.org/articles/memleak.html
        LogFactory.release(classLoader);
340 341 342
    }

    public void releaseClassLoader() {
343 344 345 346 347 348
        if (classLoader instanceof Closeable)
            try {
                ((Closeable) classLoader).close();
            } catch (IOException e) {
                LOGGER.log(WARNING, "Failed to shut down classloader",e);
            }
K
kohsuke 已提交
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
    }

    /**
     * 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() {
372
        return active;
K
kohsuke 已提交
373 374
    }

375 376 377 378
    public boolean isBundled() {
        return isBundled;
    }

K
kohsuke 已提交
379 380 381 382 383 384 385 386
    /**
     * If true, the plugin is going to be activated next time
     * Hudson runs.
     */
    public boolean isEnabled() {
        return !disableFile.exists();
    }

387
    public Manifest getManifest() {
388 389
        return manifest;
    }
390

391
    public void setPlugin(Plugin plugin) {
392
        Hudson.lookup(PluginInstanceStore.class).store.put(this,plugin);
393 394 395 396 397 398
        plugin.wrapper = this;
    }

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

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
    /**
     * Makes sure that all the dependencies exist, and then accept optional dependencies
     * as real dependencies.
     *
     * @throws IOException
     *             thrown if one or several mandatory dependencies doesn't exists.
     */
    /*package*/ void resolvePluginDependencies() throws IOException {
        List<String> missingDependencies = new ArrayList<String>();
        // make sure dependencies exist
        for (Dependency d : dependencies) {
            if (parent.getPlugin(d.shortName) == null)
                missingDependencies.add(d.toString());
        }
        if (!missingDependencies.isEmpty())
            throw new IOException("Dependency "+Util.join(missingDependencies, ", ")+" doesn't exist");

        // add the optional dependencies that exists
        for (Dependency d : optionalDependencies) {
            if (parent.getPlugin(d.shortName) != null)
                dependencies.add(d);
        }
    }

424 425
    /**
     * If the plugin has {@link #getUpdateInfo() an update},
426
     * returns the {@link UpdateSite.Plugin} object.
K
kohsuke 已提交
427 428 429 430
     *
     * @return
     *      This method may return null &mdash; for example,
     *      the user may have installed a plugin locally developed.
431
     */
432
    public UpdateSite.Plugin getUpdateInfo() {
433
        UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
434
        UpdateSite.Plugin p = uc.getPlugin(getShortName());
K
kohsuke 已提交
435 436
        if(p!=null && p.isNewerThan(getVersion())) return p;
        return null;
437
    }
438 439
    
    /**
440
     * returns the {@link UpdateSite.Plugin} object, or null.
441
     */
442
    public UpdateSite.Plugin getInfo() {
443 444 445
        UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
        return uc.getPlugin(getShortName());
    }
446 447 448 449 450 451 452 453 454 455 456

    /**
     * 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 已提交
457 458 459 460
    
    public boolean isPinned() {
        return pinFile.exists();
    }
461

M
mindless 已提交
462 463 464 465 466 467 468
    /**
     * Sort by short name.
     */
    public int compareTo(PluginWrapper pw) {
        return shortName.compareToIgnoreCase(pw.shortName);
    }

469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
    /**
     * returns true if backup of previous version of plugin exists
     */
    public boolean isDowngradable() {
        return getBackupFile().exists();
    }

    /**
     * Where is the backup file?
     */
    public File getBackupFile() {
        return new File(Hudson.getInstance().getRootDir(),"plugins/"+getShortName() + ".bak");
    }

    /**
     * returns the version of the backed up plugin,
     * or null if there's no back up.
     */
    public String getBackupVersion() {
        if (getBackupFile().exists()) {
            try {
                JarFile backupPlugin = new JarFile(getBackupFile());
                return backupPlugin.getManifest().getMainAttributes().getValue("Plugin-Version");
            } catch (IOException e) {
                LOGGER.log(WARNING, "Failed to get backup version ", e);
                return null;
            }
        } else {
            return null;
        }
    }
K
kohsuke 已提交
500 501 502 503 504
//
//
// Action methods
//
//
505
    public HttpResponse doMakeEnabled() throws IOException {
506
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
507
        enable();
508
        return HttpResponses.ok();
K
kohsuke 已提交
509
    }
510

511
    public HttpResponse doMakeDisabled() throws IOException {
512
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
513
        disable();
514 515 516 517 518 519 520 521 522 523 524 525 526
        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 已提交
527 528 529 530 531 532
    }


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

}