PluginWrapper.java 17.9 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;
28
import hudson.model.Api;
29
import jenkins.YesNoMaybe;
30
import jenkins.model.Jenkins;
31
import hudson.model.UpdateCenter;
32
import hudson.model.UpdateSite;
33
import hudson.util.VersionNumber;
K
kohsuke 已提交
34 35 36 37 38

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

47
import org.apache.commons.logging.LogFactory;
48 49
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
50 51
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
52

53 54 55
import java.util.Enumeration;
import java.util.jar.JarFile;

K
kohsuke 已提交
56
/**
A
alanharder 已提交
57 58
 * Represents a Jenkins plug-in and associated control information
 * for Jenkins to control {@link Plugin}.
K
kohsuke 已提交
59 60
 *
 * <p>
I
imod 已提交
61
 * A plug-in is packaged into a jar file whose extension is <tt>".jpi"</tt> (or <tt>".hpi"</tt> for backward compatability),
K
kohsuke 已提交
62 63 64 65 66
 * 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>
A
alanharder 已提交
67 68 69
 *  <li>Enabled/Disabled. If enabled, Jenkins is going to use it
 *      next time Jenkins runs. Otherwise the next run will ignore it.
 *  <li>Activated/Deactivated. If activated, that means Jenkins is using
K
kohsuke 已提交
70 71 72 73 74 75 76 77
 *      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
 */
78
@ExportedBean
79
public class PluginWrapper implements Comparable<PluginWrapper> {
K
kohsuke 已提交
80
    /**
81
     * {@link PluginManager} to which this belongs to.
K
kohsuke 已提交
82
     */
83
    public final PluginManager parent;
K
kohsuke 已提交
84 85

    /**
86 87
     * Plugin manifest.
     * Contains description of the plugin.
K
kohsuke 已提交
88
     */
89
    private final Manifest manifest;
K
kohsuke 已提交
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
K
Kohsuke Kawaguchi 已提交
100
     * <tt>CONTEXTPATH/plugin/SHORTNAME/</tt>.
K
kohsuke 已提交
101 102 103 104 105 106 107 108 109
     */
    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;

110 111
    /**
     * Used to control the unpacking of the bundled plugin.
A
alanharder 已提交
112
     * If a pin file exists, Jenkins assumes that the user wants to pin down a particular version
113 114 115 116 117 118
     * 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 已提交
119
    /**
120
     * Short name of the plugin. The artifact Id of the plugin.
A
alanharder 已提交
121
     * This is also used in the URL within Jenkins, so it needs
I
imod 已提交
122
     * to remain stable even when the *.jpi file name is changed
123
     * (like Maven does.)
K
kohsuke 已提交
124 125 126
     */
    private final String shortName;

127
    /**
128 129 130 131
     * 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;
132 133
    
    private boolean hasCycleDependency = false;
134

135
    private final List<Dependency> dependencies;
136
    private final List<Dependency> optionalDependencies;
137

138
    /**
A
alanharder 已提交
139
     * Is this plugin bundled in jenkins.war?
140 141 142
     */
    /*package*/ boolean isBundled;

143 144 145
    @ExportedBean
    public static final class Dependency {
        @Exported
146
        public final String shortName;
147
        @Exported
148
        public final String version;
149
        @Exported
150
        public final boolean optional;
151 152 153

        public Dependency(String s) {
            int idx = s.indexOf(':');
K
kohsuke 已提交
154 155
            if(idx==-1)
                throw new IllegalArgumentException("Illegal dependency specifier "+s);
156 157
            this.shortName = s.substring(0,idx);
            this.version = s.substring(idx+1);
158 159 160 161 162 163 164 165 166 167
            
            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;
168
        }
169 170 171 172 173

        @Override
        public String toString() {
            return shortName + " (" + version + ")";
        }        
174 175
    }

K
kohsuke 已提交
176 177
    /**
     * @param archive
I
imod 已提交
178
     *      A .jpi archive file jar file, or a .jpl linked plugin.
179 180 181 182 183 184 185 186 187 188
     *  @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 已提交
189
     */
190
    public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL baseResourceURL, 
191 192
			ClassLoader classLoader, File disableFile, 
			List<Dependency> dependencies, List<Dependency> optionalDependencies) {
193
        this.parent = parent;
194 195 196 197 198
		this.manifest = manifest;
		this.shortName = computeShortName(manifest, archive);
		this.baseResourceURL = baseResourceURL;
		this.classLoader = classLoader;
		this.disableFile = disableFile;
199
        this.pinFile = new File(archive.getPath() + ".pinned");
200 201 202
		this.active = !disableFile.exists();
		this.dependencies = dependencies;
		this.optionalDependencies = optionalDependencies;
203
    }
K
kohsuke 已提交
204

205 206 207 208
    public Api getApi() {
        return new Api(this);
    }

209 210 211 212
    /**
     * Returns the URL of the index page jelly script.
     */
    public URL getIndexPage() {
213 214 215 216 217 218 219 220
        // 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) { }
221 222 223
        // 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;
224 225
    }

226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
    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 已提交
241

242
    /**
243
     * Gets the "abc" portion from "abc.ext".
244
     */
245 246 247 248 249 250
    static String getBaseName(File archive) {
        String n = archive.getName();
        int idx = n.lastIndexOf('.');
        if(idx>=0)
            n = n.substring(0,idx);
        return n;
251 252
    }

253
    @Exported
254
    public List<Dependency> getDependencies() {
255 256
        return dependencies;
    }
257

258 259 260
    public List<Dependency> getOptionalDependencies() {
        return optionalDependencies;
    }
K
kohsuke 已提交
261 262 263 264 265


    /**
     * Returns the short name suitable for URL.
     */
266
    @Exported
K
kohsuke 已提交
267 268 269 270
    public String getShortName() {
        return shortName;
    }

271 272 273 274
    /**
     * Gets the instance of {@link Plugin} contributed by this plugin.
     */
    public Plugin getPlugin() {
275
        return Jenkins.lookup(PluginInstanceStore.class).store.get(this);
276 277
    }

278 279 280 281 282 283
    /**
     * Gets the URL that shows more information about this plugin.
     * @return
     *      null if this information is unavailable.
     * @since 1.283
     */
284
    @Exported
285 286 287 288 289 290
    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
291
        UpdateSite.Plugin ui = getInfo();
292 293 294 295
        if(ui!=null)    return ui.wiki;

        return null;
    }
296 297
    
    
298

299 300 301 302 303
    @Override
    public String toString() {
        return "Plugin:" + getShortName();
    }

K
kohsuke 已提交
304 305 306
    /**
     * Returns a one-line descriptive name of this plugin.
     */
307
    @Exported
K
kohsuke 已提交
308
    public String getLongName() {
309
        String name = manifest.getMainAttributes().getValue("Long-Name");
K
kohsuke 已提交
310 311 312 313
        if(name!=null)      return name;
        return shortName;
    }

314 315 316
    /**
     * Does this plugin supports dynamic loading?
     */
317
    @Exported
318 319 320 321 322 323
    public YesNoMaybe supportsDynamicLoad() {
        String v = manifest.getMainAttributes().getValue("Support-Dynamic-Loading");
        if (v==null) return YesNoMaybe.MAYBE;
        return Boolean.parseBoolean(v) ? YesNoMaybe.YES : YesNoMaybe.NO;
    }

324 325 326
    /**
     * Returns the version number of this plugin
     */
327
    @Exported
328 329 330
    public String getVersion() {
        String v = manifest.getMainAttributes().getValue("Plugin-Version");
        if(v!=null)      return v;
K
kohsuke 已提交
331 332 333 334 335

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

336 337 338
        return "???";
    }

339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
    /**
     * 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 已提交
359 360 361
    /**
     * Terminates the plugin.
     */
362
    public void stop() {
K
kohsuke 已提交
363 364
        LOGGER.info("Stopping "+shortName);
        try {
365
            getPlugin().stop();
K
kohsuke 已提交
366
        } catch(Throwable t) {
K
kohsuke 已提交
367
            LOGGER.log(WARNING, "Failed to shut down "+shortName, t);
K
kohsuke 已提交
368
        }
369 370 371
        // Work around a bug in commons-logging.
        // See http://www.szegedi.org/articles/memleak.html
        LogFactory.release(classLoader);
372 373 374
    }

    public void releaseClassLoader() {
375 376 377 378 379 380
        if (classLoader instanceof Closeable)
            try {
                ((Closeable) classLoader).close();
            } catch (IOException e) {
                LOGGER.log(WARNING, "Failed to shut down classloader",e);
            }
K
kohsuke 已提交
381 382 383
    }

    /**
A
alanharder 已提交
384
     * Enables this plugin next time Jenkins runs.
K
kohsuke 已提交
385 386 387 388 389 390 391
     */
    public void enable() throws IOException {
        if(!disableFile.delete())
            throw new IOException("Failed to delete "+disableFile);
    }

    /**
A
alanharder 已提交
392
     * Disables this plugin next time Jenkins runs.
K
kohsuke 已提交
393 394 395 396 397 398 399 400 401 402
     */
    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.
     */
403
    @Exported
K
kohsuke 已提交
404
    public boolean isActive() {
405 406 407 408 409
        return active && !hasCycleDependency();
    }
    
    public boolean hasCycleDependency(){
        return hasCycleDependency;
K
kohsuke 已提交
410 411
    }

412 413 414 415
    public void setHasCycleDependency(boolean hasCycle){
        hasCycleDependency = hasCycle;
    }
    
416
    @Exported
417 418 419 420
    public boolean isBundled() {
        return isBundled;
    }

K
kohsuke 已提交
421 422
    /**
     * If true, the plugin is going to be activated next time
A
alanharder 已提交
423
     * Jenkins runs.
K
kohsuke 已提交
424
     */
425
    @Exported
K
kohsuke 已提交
426 427 428 429
    public boolean isEnabled() {
        return !disableFile.exists();
    }

430
    public Manifest getManifest() {
431 432
        return manifest;
    }
433

434
    public void setPlugin(Plugin plugin) {
435
        Jenkins.lookup(PluginInstanceStore.class).store.put(this,plugin);
436 437 438 439 440 441
        plugin.wrapper = this;
    }

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

443 444 445 446 447 448 449 450 451
    public boolean hasLicensesXml() {
        try {
            new URL(baseResourceURL,"WEB-INF/licenses.xml").openStream().close();
            return true;
        } catch (IOException e) {
            return false;
        }
    }

452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
    /**
     * 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);
        }
    }

476 477
    /**
     * If the plugin has {@link #getUpdateInfo() an update},
478
     * returns the {@link UpdateSite.Plugin} object.
K
kohsuke 已提交
479 480 481 482
     *
     * @return
     *      This method may return null &mdash; for example,
     *      the user may have installed a plugin locally developed.
483
     */
484
    public UpdateSite.Plugin getUpdateInfo() {
485
        UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
486
        UpdateSite.Plugin p = uc.getPlugin(getShortName());
K
kohsuke 已提交
487 488
        if(p!=null && p.isNewerThan(getVersion())) return p;
        return null;
489
    }
490 491
    
    /**
492
     * returns the {@link UpdateSite.Plugin} object, or null.
493
     */
494
    public UpdateSite.Plugin getInfo() {
495
        UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
496 497
        return uc.getPlugin(getShortName());
    }
498 499 500 501 502 503 504 505

    /**
     * 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.
     */
506
    @Exported
507 508 509
    public boolean hasUpdate() {
        return getUpdateInfo()!=null;
    }
K
kohsuke 已提交
510
    
511
    @Exported
K
kohsuke 已提交
512 513 514
    public boolean isPinned() {
        return pinFile.exists();
    }
515

M
mindless 已提交
516 517 518 519 520 521 522
    /**
     * Sort by short name.
     */
    public int compareTo(PluginWrapper pw) {
        return shortName.compareToIgnoreCase(pw.shortName);
    }

523 524 525
    /**
     * returns true if backup of previous version of plugin exists
     */
526
    @Exported
527 528 529 530 531 532 533 534
    public boolean isDowngradable() {
        return getBackupFile().exists();
    }

    /**
     * Where is the backup file?
     */
    public File getBackupFile() {
535
        return new File(Jenkins.getInstance().getRootDir(),"plugins/"+getShortName() + ".bak");
536 537 538 539 540 541
    }

    /**
     * returns the version of the backed up plugin,
     * or null if there's no back up.
     */
542
    @Exported
543
    public String getBackupVersion() {
544 545
        File backup = getBackupFile();
        if (backup.exists()) {
546
            try {
547
                JarFile backupPlugin = new JarFile(backup);
548 549 550 551 552 553 554 555 556
                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 已提交
557 558 559 560 561
//
//
// Action methods
//
//
562
    public HttpResponse doMakeEnabled() throws IOException {
563
        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
K
kohsuke 已提交
564
        enable();
565
        return HttpResponses.ok();
K
kohsuke 已提交
566
    }
567

568
    public HttpResponse doMakeDisabled() throws IOException {
569
        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
K
kohsuke 已提交
570
        disable();
571 572 573 574
        return HttpResponses.ok();
    }

    public HttpResponse doPin() throws IOException {
575
        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
576 577 578 579 580
        new FileOutputStream(pinFile).close();
        return HttpResponses.ok();
    }

    public HttpResponse doUnpin() throws IOException {
581
        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
582 583
        pinFile.delete();
        return HttpResponses.ok();
K
kohsuke 已提交
584 585 586 587 588 589
    }


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

}