UpdateCenter.java 28.4 KB
Newer Older
K
kohsuke 已提交
1 2 3
/*
 * The MIT License
 * 
4
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., Seiji Sogabe
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.
 */
24 25
package hudson.model;

26 27
import hudson.BulkChange;
import hudson.Extension;
28
import hudson.ExtensionPoint;
K
kohsuke 已提交
29
import hudson.Functions;
30 31
import hudson.PluginManager;
import hudson.PluginWrapper;
32
import hudson.ProxyConfiguration;
33 34
import hudson.Util;
import hudson.XmlFile;
35
import hudson.lifecycle.Lifecycle;
36 37
import hudson.model.UpdateSite.Data;
import hudson.model.UpdateSite.Plugin;
38
import hudson.util.DaemonThreadFactory;
39
import hudson.util.IOException2;
40 41
import hudson.util.PersistedList;
import hudson.util.XStream2;
42
import org.acegisecurity.Authentication;
43
import org.apache.commons.io.IOUtils;
44
import org.apache.commons.io.input.CountingInputStream;
K
kohsuke 已提交
45
import org.apache.commons.io.output.NullOutputStream;
K
kohsuke 已提交
46
import org.kohsuke.stapler.StaplerResponse;
47

K
kohsuke 已提交
48
import javax.net.ssl.SSLHandshakeException;
49
import javax.servlet.ServletException;
50
import java.io.File;
51
import java.io.FileOutputStream;
52
import java.io.IOException;
53
import java.io.InputStream;
54 55
import java.io.OutputStream;
import java.net.MalformedURLException;
56
import java.net.URL;
57
import java.net.URLConnection;
58
import java.net.UnknownHostException;
59
import java.util.ArrayList;
60
import java.util.HashSet;
61
import java.util.List;
62
import java.util.Set;
63 64
import java.util.Vector;
import java.util.concurrent.ExecutorService;
K
kohsuke 已提交
65
import java.util.concurrent.Executors;
K
kohsuke 已提交
66
import java.util.concurrent.Future;
67
import java.util.concurrent.ThreadFactory;
K
kohsuke 已提交
68
import java.util.concurrent.atomic.AtomicInteger;
69 70
import java.util.logging.Level;
import java.util.logging.Logger;
71 72 73 74

/**
 * Controls update center capability.
 *
75 76 77
 * <p>
 * The main job of this class is to keep track of the latest update center metadata file, and perform installations.
 * Much of the UI about choosing plugins to install is done in {@link PluginManager}.
78 79 80 81 82 83
 * <p>
 * The update center can be configured to contact alternate servers for updates
 * and plugins, and to use alternate strategies for downloading, installing
 * and updating components. See the Javadocs for {@link UpdateCenterConfiguration}
 * for more information.
 * 
84
 * @author Kohsuke Kawaguchi
K
kohsuke 已提交
85
 * @since 1.220
86
 */
87
public class UpdateCenter extends AbstractModelObject implements Saveable {
88 89 90
    /**
     * {@link ExecutorService} that performs installation.
     */
K
kohsuke 已提交
91
    private final ExecutorService installerService = Executors.newSingleThreadExecutor(
92 93 94 95 96 97
        new DaemonThreadFactory(new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("Update center installer thread");
                return t;
            }
K
kohsuke 已提交
98
        }));
99

100
    /**
101
     * List of created {@link UpdateCenterJob}s. Access needs to be synchronized.
102
     */
103
    private final Vector<UpdateCenterJob> jobs = new Vector<UpdateCenterJob>();
104

105
    /**
106 107
     * {@link UpdateSite}s from which we've already installed a plugin at least once.
     * This is used to skip network tests.
108
     */
109 110
    private final Set<UpdateSite> sourcesUsed = new HashSet<UpdateSite>();

111
    /**
112
     * List of {@link UpdateSite}s to be used.
113
     */
114 115 116 117 118 119 120 121
    private final PersistedList<UpdateSite> sites = new PersistedList<UpdateSite>(this);

    /**
     * Update center configuration data
     */
    private UpdateCenterConfiguration config;

    public UpdateCenter() {
122 123
        configure(new UpdateCenterConfiguration());
    }
124

125 126 127 128
    /**
     * Configures update center to get plugins/updates from alternate servers,
     * and optionally using alternate strategies for downloading, installing
     * and upgrading.
129
     *
130 131 132 133 134 135 136 137
     * @param config Configuration data
     * @see UpdateCenterConfiguration
     */
    public void configure(UpdateCenterConfiguration config) {
        if (config!=null) {
            this.config = config;
        }
    }
138

139
    /**
140
     * Returns the list of {@link UpdateCenterJob} representing scheduled installation attempts.
141 142
     *
     * @return
K
kohsuke 已提交
143
     *      can be empty but never null. Oldest entries first.
144
     */
145
    public List<UpdateCenterJob> getJobs() {
K
kohsuke 已提交
146
        synchronized (jobs) {
147
            return new ArrayList<UpdateCenterJob>(jobs);
K
kohsuke 已提交
148
        }
149 150
    }

151
    /**
152 153 154 155 156
     * Returns the list of {@link UpdateSite}s to be used.
     * This is a live list, whose change will be persisted automatically.
     *
     * @return
     *      can be empty but never null.
157
     */
158 159
    public PersistedList<UpdateSite> getSites() {
        return sites;
160 161
    }

162
    /**
163 164
     * Gets the string representing how long ago the data was obtained.
     * Will be the newest of all {@link UpdateSite}s.
165
     */
166 167 168 169 170 171
    public String getLastUpdatedString() {
        long newestTs = -1;
        for (UpdateSite s : sites) {
            if (s.getDataTimestamp()>newestTs) {
                newestTs = s.getDataTimestamp();
            }
172
        }
173 174
        if(newestTs<0)     return "N/A";
        return Util.getPastTimeString(System.currentTimeMillis()-newestTs);
175 176
    }

177
    /**
178 179
     * Gets {@link UpdateSite} by its ID.
     * Used to bind them to URL.
180
     */
181 182 183 184
    public UpdateSite getById(String id) {
        for (UpdateSite s : sites) {
            if (s.getId().equals(id)) {
                return s;
185
            }
186
        }
187 188
        return null;
    }
189

190 191 192 193 194 195 196 197 198 199 200 201
    /**
     * Gets the {@link UpdateSite} from which we receive updates for <tt>hudson.war</tt>.
     *
     * @return
     *      null if no such update center is provided.
     */
    public UpdateSite getCoreSource() {
        for (UpdateSite s : sites)
            if (s.getData().core!=null)
                return s;
        return null;
    }
202

203 204 205 206 207 208 209 210 211 212
    /**
     * Gets the default base URL.
     *
     * @deprecated
     *      TODO: revisit tool update mechanism, as that should be de-centralized, too. In the mean time,
     *      please try not to use this method, and instead ping us to get this part completed.
     */
    public String getDefaultBaseUrl() {
        return config.getUpdateCenterUrl();
    }
213

214 215 216 217 218 219 220
    /**
     * Gets the plugin with the given name from the first {@link UpdateSite} to contain it.
     */
    public Plugin getPlugin(String artifactId) {
        for (UpdateSite s : sites) {
            Plugin p = s.getPlugin(artifactId);
            if (p!=null) return p;
221
        }
222
        return null;
223 224
    }

225 226 227 228
    /**
     * Schedules a Hudson upgrade.
     */
    public void doUpgrade(StaplerResponse rsp) throws IOException, ServletException {
229
        requirePOST();
230
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
231
        HudsonUpgradeJob job = new HudsonUpgradeJob(getCoreSource(), Hudson.getAuthentication());
232
        if(!Lifecycle.get().canRewriteHudsonWar()) {
233 234 235 236 237 238 239 240 241
            sendError("Hudson upgrade not supported in this running mode");
            return;
        }

        LOGGER.info("Scheduling the core upgrade");
        addJob(job);
        rsp.sendRedirect2(".");
    }

242
    /*package*/ synchronized Future<UpdateCenterJob> addJob(UpdateCenterJob job) {
243
        // the first job is always the connectivity check
244 245
        if (sourcesUsed.add(job.site))
            new ConnectionCheckJob(job.site).submit();
K
kohsuke 已提交
246
        return job.submit();
247 248
    }

249 250 251 252 253 254
    public String getDisplayName() {
        return "Update center";
    }

    public String getSearchUrl() {
        return "updateCenter";
255 256
    }

257
    /**
258
     * Saves the configuration info to the disk.
259
     */
260 261 262 263 264 265
    public synchronized void save() {
        if(BulkChange.contains(this))   return;
        try {
            getConfigFile().write(sites);
        } catch (IOException e) {
            LOGGER.log(Level.WARNING, "Failed to save "+getConfigFile(),e);
266 267 268
        }
    }

269
    /**
270
     * Loads the data from the disk into this object.
271
     */
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
    public synchronized void load() throws IOException {
        XmlFile file = getConfigFile();
        if(file.exists()) {
            try {
                sites.replaceBy(((PersistedList)file.unmarshal(sites)).toList());
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Failed to load "+file, e);
            }
        } else {
            // If there aren't already any UpdateSources, add the default one.
            if (sites.isEmpty()) {
                // to maintain compatibility with existing UpdateCenterConfiguration, create the default one as specified by UpdateCenterConfiguration
                sites.add(new UpdateSite("default",config.getUpdateCenterUrl()+"update-center.json"));
            }
        }
287 288
    }

289 290 291
    private XmlFile getConfigFile() {
        return new XmlFile(XSTREAM,new File(Hudson.getInstance().root,
                                    UpdateCenter.class.getName()+".xml"));
292 293
    }

294 295
    public List<Plugin> getAvailables() {
        List<Plugin> plugins = new ArrayList<Plugin>();
296

297 298
        for (UpdateSite s : sites) {
            plugins.addAll(s.getAvailables());
299 300
        }

301
        return plugins;
302 303
    }

304 305
    public List<Plugin> getUpdates() {
        List<Plugin> plugins = new ArrayList<Plugin>();
306

307 308 309
        for (UpdateSite s : sites) {
            plugins.addAll(s.getUpdates());
        }
K
kohsuke 已提交
310

311
        return plugins;
312 313
    }

314 315 316 317

    /**
     * {@link AdministrativeMonitor} that checks if there's Hudson update.
     */
318 319
    @Extension
    public static final class CoreUpdateMonitor extends AdministrativeMonitor {
320 321 322 323 324 325
        public boolean isActivated() {
            Data data = getData();
            return data!=null && data.hasCoreUpdates();
        }

        public Data getData() {
326 327 328
            UpdateSite cs = Hudson.getInstance().getUpdateCenter().getCoreSource();
            if (cs!=null)   return cs.getData();
            return null;
329 330 331
        }
    }

332

333
    /**
334 335 336 337 338 339 340 341 342 343 344 345
     * Strategy object for controlling the update center's behaviors.
     *
     * <p>
     * Until 1.MULTIUPDATE, this extension point used to control the configuration of
     * where to get updates (hence the name of this class), but with the introduction
     * of multiple update center sites capability, that functionality is achieved by
     * simply installing another {@link UpdateSite}.
     *
     * <p>
     * See {@link UpdateSite} for how to manipulate them programmatically.
     *
     * @since 1.266
346
     */
347 348
    @SuppressWarnings({"UnusedDeclaration"})
    public static class UpdateCenterConfiguration implements ExtensionPoint {
K
kohsuke 已提交
349
        /**
350
         * Creates default update center configuration - uses settings for global update center.
K
kohsuke 已提交
351
         */
352
        public UpdateCenterConfiguration() {
K
kohsuke 已提交
353
        }
354

355 356 357
        /**
         * Check network connectivity by trying to establish a connection to
         * the host in connectionCheckUrl.
358
         *
359 360 361 362 363 364 365 366
         * @param job The connection checker that is invoking this strategy.
         * @param connectionCheckUrl A string containing the URL of a domain
         *          that is assumed to be always available.
         * @throws IOException if a connection can't be established
         */
        public void checkConnection(ConnectionCheckJob job, String connectionCheckUrl) throws IOException {
            testConnection(new URL(connectionCheckUrl));
        }
367

368 369
        /**
         * Check connection to update center server.
370
         *
371 372 373 374 375 376 377
         * @param job The connection checker that is invoking this strategy.
         * @param updateCenterUrl A sting containing the URL of the update center host.
         * @throws IOException if a connection to the update center server can't be established.
         */
        public void checkUpdateCenter(ConnectionCheckJob job, String updateCenterUrl) throws IOException {
            testConnection(new URL(updateCenterUrl + "?uctest"));
        }
378

379
        /**
380 381
         * Validate the URL of the resource before downloading it.
         *
382 383 384 385 386 387 388
         * @param job The download job that is invoking this strategy. This job is
         *          responsible for managing the status of the download and installation.
         * @param src The location of the resource on the network
         * @throws IOException if the validation fails
         */
        public void preValidate(DownloadJob job, URL src) throws IOException {
        }
389

390 391 392
        /**
         * Validate the resource after it has been downloaded, before it is
         * installed. The default implementation does nothing.
393
         *
394 395 396 397 398 399 400
         * @param job The download job that is invoking this strategy. This job is
         *          responsible for managing the status of the download and installation.
         * @param src The location of the downloaded resource.
         * @throws IOException if the validation fails.
         */
        public void postValidate(DownloadJob job, File src) throws IOException {
        }
401

402 403 404 405 406
        /**
         * Download a plugin or core upgrade in preparation for installing it
         * into its final location. Implementations will normally download the
         * resource into a temporary location and hand off a reference to this
         * location to the install or upgrade strategy to move into the final location.
407
         *
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
         * @param job The download job that is invoking this strategy. This job is
         *          responsible for managing the status of the download and installation.
         * @param src The URL to the resource to be downloaded.
         * @return A File object that describes the downloaded resource.
         * @throws IOException if there were problems downloading the resource.
         * @see DownloadJob
         */
        public File download(DownloadJob job, URL src) throws IOException {
            URLConnection con = ProxyConfiguration.open(src);
            int total = con.getContentLength();
            CountingInputStream in = new CountingInputStream(con.getInputStream());
            byte[] buf = new byte[8192];
            int len;

            File dst = job.getDestination();
            File tmp = new File(dst.getPath()+".tmp");
            OutputStream out = new FileOutputStream(tmp);

            LOGGER.info("Downloading "+job.getName());
427 428 429 430 431 432 433
            try {
                while((len=in.read(buf))>=0) {
                    out.write(buf,0,len);
                    job.status = job.new Installing(total==-1 ? -1 : in.getCount()*100/total);
                }
            } catch (IOException e) {
                throw new IOException2("Failed to load "+src+" to "+tmp,e);
434 435 436 437
            }

            in.close();
            out.close();
438 439 440 441 442 443 444

            if (total!=-1 && total!=tmp.length()) {
                // don't know exactly how this happens, but report like
                // http://www.ashlux.com/wordpress/2009/08/14/hudson-and-the-sonar-plugin-fail-maveninstallation-nosuchmethoderror/
                // indicates that this kind of inconsistency can happen. So let's be defensive
                throw new IOException("Inconsistent file length: expected "+total+" but only got "+tmp.length());
            }
445

446 447
            return tmp;
        }
448

449 450 451
        /**
         * Called after a plugin has been downloaded to move it into its final
         * location. The default implementation is a file rename.
452
         *
453 454 455 456 457 458 459 460
         * @param job The install job that is invoking this strategy.
         * @param src The temporary location of the plugin.
         * @param dst The final destination to install the plugin to.
         * @throws IOException if there are problems installing the resource.
         */
        public void install(DownloadJob job, File src, File dst) throws IOException {
            job.replace(dst, src);
        }
461

462 463 464
        /**
         * Called after an upgrade has been downloaded to move it into its final
         * location. The default implementation is a file rename.
465
         *
466 467 468 469 470 471 472
         * @param job The upgrade job that is invoking this strategy.
         * @param src The temporary location of the upgrade.
         * @param dst The final destination to install the upgrade to.
         * @throws IOException if there are problems installing the resource.
         */
        public void upgrade(DownloadJob job, File src, File dst) throws IOException {
            job.replace(dst, src);
473
        }
474 475

        /**
476 477 478 479 480 481
         * Returns an "always up" server for Internet connectivity testing.
         *
         * @deprecated as of 1.MULTIUPDATE
         *      With the introduction of multiple update center capability, this information
         *      is now a part of the <tt>update-center.json</tt> file. See
         *      <tt>http://hudson-ci.org/update-center.json</tt> as an example.
482 483 484 485
         */
        public String getConnectionCheckUrl() {
            return "http://www.google.com";
        }
486

487 488 489
        /**
         * Returns the URL of the server that hosts the update-center.json
         * file.
K
kohsuke 已提交
490
         *
491 492 493
         * @deprecated as of 1.MULTIUPDATE
         *      With the introduction of multiple update center capability, this information
         *      is now moved to {@link UpdateSite}.
K
kohsuke 已提交
494 495
         * @return
         *      Absolute URL that ends with '/'.
496 497
         */
        public String getUpdateCenterUrl() {
498
            return "http://hudson-ci.org/";
499
        }
500

501 502
        /**
         * Returns the URL of the server that hosts plugins and core updates.
503 504 505 506
         *
         * @deprecated as of 1.MULTIUPDATE
         *      <tt>update-center.json</tt> is now signed, so we don't have to further make sure that
         *      we aren't downloading from anywhere unsecure.
507 508
         */
        public String getPluginRepositoryBaseUrl() {
509
            return "http://hudson-ci.org/";
510
        }
511

512

513
        private void testConnection(URL url) throws IOException {
K
kohsuke 已提交
514 515 516 517 518 519 520 521 522
            try {
                InputStream in = ProxyConfiguration.open(url).getInputStream();
                IOUtils.copy(in,new NullOutputStream());
                in.close();
            } catch (SSLHandshakeException e) {
                if (e.getMessage().contains("PKIX path building failed"))
                   // fix up this crappy error message from JDK
                    throw new IOException2("Failed to validate the SSL certificate of "+url,e);
            }
523
        }
524
    }
525

526 527 528 529 530 531
    /**
     * Things that {@link UpdateCenter#installerService} executes.
     *
     * This object will have the <tt>row.jelly</tt> which renders the job on UI.
     */
    public abstract class UpdateCenterJob implements Runnable {
532 533 534 535 536 537 538 539 540
        /**
         * Which {@link UpdateSite} does this belong to?
         */
        public final UpdateSite site;

        protected UpdateCenterJob(UpdateSite site) {
            this.site = site;
        }

K
kohsuke 已提交
541 542 543 544
        /**
         * @deprecated as of 1.326
         *      Use {@link #submit()} instead.
         */
545
        public void schedule() {
K
kohsuke 已提交
546 547 548 549 550 551 552 553 554
            submit();
        }

        /**
         * Schedules this job for an execution
         * @return
         *      {@link Future} to keeps track of the status of the execution.
         */
        public Future<UpdateCenterJob> submit() {
555
            LOGGER.fine("Scheduling "+this+" to installerService");
556
            jobs.add(this);
K
kohsuke 已提交
557
            return installerService.submit(this,this);
558 559 560 561 562 563 564 565 566
        }
    }

    /**
     * Tests the internet connectivity.
     */
    public final class ConnectionCheckJob extends UpdateCenterJob {
        private final Vector<String> statuses= new Vector<String>();

567 568 569 570
        public ConnectionCheckJob(UpdateSite site) {
            super(site);
        }

571
        public void run() {
572
            LOGGER.fine("Doing a connectivity check");
573
            try {
574 575 576 577 578 579 580 581 582 583 584
                String connectionCheckUrl = site.getConnectionCheckUrl();
                if (connectionCheckUrl!=null) {
                    statuses.add(Messages.UpdateCenter_Status_CheckingInternet());
                    try {
                        config.checkConnection(this, connectionCheckUrl);
                    } catch (IOException e) {
                        if(e.getMessage().contains("Connection timed out")) {
                            // Google can't be down, so this is probably a proxy issue
                            statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl));
                            return;
                        }
585 586
                    }
                }
587

588
                statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet());
589
                config.checkUpdateCenter(this, site.getUrl());
590

591
                statuses.add(Messages.UpdateCenter_Status_Success());
592
            } catch (UnknownHostException e) {
593
                statuses.add(Messages.UpdateCenter_Status_UnknownHostException(e.getMessage()));
594
                addStatus(e);
595 596 597 598 599
            } catch (IOException e) {
                statuses.add(Functions.printThrowable(e));
            }
        }

600 601 602 603
        private void addStatus(UnknownHostException e) {
            statuses.add("<pre>"+ Functions.xmlEscape(Functions.printThrowable(e))+"</pre>");
        }

604 605 606 607 608 609 610
        public String[] getStatuses() {
            synchronized (statuses) {
                return statuses.toArray(new String[statuses.size()]);
            }
        }
    }

611
    /**
612
     * Base class for a job that downloads a file from the Hudson project.
613
     */
614
    public abstract class DownloadJob extends UpdateCenterJob {
K
kohsuke 已提交
615 616 617 618 619 620 621 622
        /**
         * Unique ID that identifies this job.
         */
        public final int id = iota.incrementAndGet();
        /**
         * Immutable object representing the current state of this job.
         */
        public volatile InstallationStatus status = new Pending();
623

624 625 626 627 628 629 630 631 632 633
        /**
         * Where to download the file from.
         */
        protected abstract URL getURL() throws MalformedURLException;

        /**
         * Where to download the file to.
         */
        protected abstract File getDestination();

K
kohsuke 已提交
634
        public abstract String getName();
635 636 637 638 639

        /**
         * Called when the whole thing went successfully.
         */
        protected abstract void onSuccess();
640

641

642
        private Authentication authentication;
643

644 645 646 647 648 649 650
        /**
         * Get the user that initiated this job
         */
        public Authentication getUser()
        {
            return this.authentication;
        }
651 652 653

        protected DownloadJob(UpdateSite site, Authentication authentication) {
            super(site);
654 655
            this.authentication = authentication;
        }
656

K
kohsuke 已提交
657 658
        public void run() {
            try {
659
                LOGGER.info("Starting the installation of "+getName()+" on behalf of "+getUser().getName());
660

661
                _run();
662

663
                LOGGER.info("Installation successful: "+getName());
K
kohsuke 已提交
664
                status = new Success();
665
                onSuccess();
K
kohsuke 已提交
666
            } catch (IOException e) {
667
                LOGGER.log(Level.SEVERE, "Failed to install "+getName(),e);
K
kohsuke 已提交
668 669
                status = new Failure(e);
            }
670 671
        }

672 673 674 675 676 677 678 679 680 681 682 683
        protected void _run() throws IOException {
            URL src = getURL();

            config.preValidate(this, src);

            File dst = getDestination();
            File tmp = config.download(this, src);

            config.postValidate(this, tmp);
            config.install(this, tmp, dst);
        }

684 685 686 687 688
        /**
         * Called when the download is completed to overwrite
         * the old file with the new file.
         */
        protected void replace(File dst, File src) throws IOException {
689 690 691 692
            File bak = Util.changeExtension(dst,".bak");
            bak.delete();
            dst.renameTo(bak);
            dst.delete(); // any failure up to here is no big deal
693 694 695 696 697
            if(!src.renameTo(dst)) {
                throw new IOException("Failed to rename "+src+" to "+dst);
            }
        }

K
kohsuke 已提交
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
        /**
         * Indicates the status or the result of a plugin installation.
         * <p>
         * Instances of this class is immutable.
         */
        public abstract class InstallationStatus {
            public final int id = iota.incrementAndGet();
        }

        /**
         * Indicates that the installation of a plugin failed.
         */
        public class Failure extends InstallationStatus {
            public final Throwable problem;

            public Failure(Throwable problem) {
                this.problem = problem;
            }

            public String getStackTrace() {
                return Functions.printThrowable(problem);
            }
720
        }
721

722
        /**
K
kohsuke 已提交
723
         * Indicates that the plugin was successfully installed.
724
         */
K
kohsuke 已提交
725 726
        public class Success extends InstallationStatus {
        }
727

K
kohsuke 已提交
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
        /**
         * Indicates that the plugin is waiting for its turn for installation.
         */
        public class Pending extends InstallationStatus {
        }

        /**
         * Installation of a plugin is in progress.
         */
        public class Installing extends InstallationStatus {
            /**
             * % completed download, or -1 if the percentage is not known.
             */
            public final int percentage;

            public Installing(int percentage) {
                this.percentage = percentage;
            }
746 747 748
        }
    }

749 750 751 752 753 754 755 756 757 758 759
    /**
     * Represents the state of the installation activity of one plugin.
     */
    public final class InstallationJob extends DownloadJob {
        /**
         * What plugin are we trying to install?
         */
        public final Plugin plugin;

        private final PluginManager pm = Hudson.getInstance().getPluginManager();

760 761
        public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth) {
            super(site, auth);
762 763 764 765 766 767 768 769 770 771 772 773
            this.plugin = plugin;
        }

        protected URL getURL() throws MalformedURLException {
            return new URL(plugin.url);
        }

        protected File getDestination() {
            File baseDir = pm.rootDir;
            return new File(baseDir, plugin.name + ".hpi");
        }

K
kohsuke 已提交
774 775
        public String getName() {
            return plugin.getDisplayName();
776 777
        }

778 779
        @Override
        public void _run() throws IOException {
K
kohsuke 已提交
780
            super._run();
781 782 783 784 785 786 787

            // if this is a bundled plugin, make sure it won't get overwritten
            PluginWrapper pw = plugin.getInstalled();
            if (pw!=null && pw.isBundled())
                pw.doPin();
        }

788 789 790
        protected void onSuccess() {
            pm.pluginUploaded = true;
        }
791 792 793 794 795

        @Override
        public String toString() {
            return super.toString()+"[plugin="+plugin.title+"]";
        }
796 797
    }

798 799 800 801
    /**
     * Represents the state of the upgrade activity of Hudson core.
     */
    public final class HudsonUpgradeJob extends DownloadJob {
802 803
        public HudsonUpgradeJob(UpdateSite site, Authentication auth) {
            super(site, auth);
804 805 806
        }

        protected URL getURL() throws MalformedURLException {
807
            return new URL(site.getData().core.url);
808 809 810 811 812 813
        }

        protected File getDestination() {
            return Lifecycle.get().getHudsonWar();
        }

K
kohsuke 已提交
814
        public String getName() {
815 816 817 818
            return "hudson.war";
        }

        protected void onSuccess() {
819
            status = new Success();
K
kohsuke 已提交
820 821
        }

822 823 824 825
        @Override
        protected void replace(File dst, File src) throws IOException {
            Lifecycle.get().rewriteHudsonWar(src);
        }
826 827
    }

828 829 830
    /**
     * Adds the update center data retriever to HTML.
     */
831
    @Extension
832 833 834 835 836 837
    public static class PageDecoratorImpl extends PageDecorator {
        public PageDecoratorImpl() {
            super(PageDecoratorImpl.class);
        }
    }

K
kohsuke 已提交
838 839 840 841 842
    /**
     * Sequence number generator.
     */
    private static final AtomicInteger iota = new AtomicInteger();

843
    private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName());
844 845

    public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never");
846

847 848 849 850 851
    public static final XStream2 XSTREAM = new XStream2();
    static {
        XSTREAM.alias("site",UpdateSite.class);
        XSTREAM.alias("sites",PersistedList.class);
    }
852
}