UpdateCenter.java 30.8 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
import hudson.ExtensionPoint;
K
kohsuke 已提交
27
import hudson.Functions;
28 29
import hudson.PluginManager;
import hudson.PluginWrapper;
30
import hudson.Util;
31
import hudson.ProxyConfiguration;
32
import hudson.Extension;
33
import hudson.lifecycle.Lifecycle;
34
import hudson.util.DaemonThreadFactory;
35
import hudson.util.TextFile;
K
kohsuke 已提交
36
import hudson.util.VersionNumber;
37
import hudson.util.IOException2;
38 39
import static hudson.util.TimeUnit2.DAYS;
import net.sf.json.JSONObject;
40
import org.acegisecurity.Authentication;
41
import org.apache.commons.io.input.CountingInputStream;
42
import org.apache.commons.io.IOUtils;
K
kohsuke 已提交
43
import org.apache.commons.io.output.NullOutputStream;
44
import org.kohsuke.stapler.DataBoundConstructor;
45
import org.kohsuke.stapler.StaplerRequest;
K
kohsuke 已提交
46
import org.kohsuke.stapler.StaplerResponse;
47

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

/**
 * Controls update center capability.
 *
77 78 79
 * <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}.
80 81 82 83 84 85
 * <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.
 * 
86
 * @author Kohsuke Kawaguchi
K
kohsuke 已提交
87
 * @since 1.220
88
 */
89
public class UpdateCenter extends AbstractModelObject {
90 91 92
    /**
     * What's the time stamp of data file?
     */
93 94 95 96 97 98 99 100 101 102 103 104
    private long dataTimestamp = -1;

    /**
     * When was the last time we asked a browser to check the data for us?
     *
     * <p>
     * There's normally some delay between when we send HTML that includes the check code,
     * until we get the data back, so this variable is used to avoid asking too many browseres
     * all at once.
     */
    private volatile long lastAttempt = -1;

105 106 107
    /**
     * {@link ExecutorService} that performs installation.
     */
K
kohsuke 已提交
108
    private final ExecutorService installerService = Executors.newSingleThreadExecutor(
109 110 111 112 113 114
        new DaemonThreadFactory(new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("Update center installer thread");
                return t;
            }
K
kohsuke 已提交
115
        }));
116

117
    /**
118
     * List of created {@link UpdateCenterJob}s. Access needs to be synchronized.
119
     */
120
    private final Vector<UpdateCenterJob> jobs = new Vector<UpdateCenterJob>();
121

122 123 124 125 126 127 128 129
    /**
     * Update center configuration data
     */
    private UpdateCenterConfiguration config;
    
    /**
     * Create update center to get plugins/updates from hudson.dev.java.net
     */
130
    public UpdateCenter(Hudson parent) {
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
        configure(new UpdateCenterConfiguration());
    }
    
    /**
     * Configures update center to get plugins/updates from alternate servers,
     * and optionally using alternate strategies for downloading, installing
     * and upgrading.
     * 
     * @param config Configuration data
     * @see UpdateCenterConfiguration
     */
    public void configure(UpdateCenterConfiguration config) {
        if (config!=null) {
            this.config = config;
        }
    }
    
148 149 150 151
    /**
     * Returns true if it's time for us to check for new version.
     */
    public boolean isDue() {
152
        if(neverUpdate)     return false;
153 154
        if(dataTimestamp==-1)
            dataTimestamp = getDataFile().file.lastModified();
155 156 157 158 159 160
        long now = System.currentTimeMillis();
        boolean due = now - dataTimestamp > DAY && now - lastAttempt > 15000;
        if(due)     lastAttempt = now;
        return due;
    }

161
    /**
162
     * Returns the list of {@link UpdateCenterJob} representing scheduled installation attempts.
163 164
     *
     * @return
K
kohsuke 已提交
165
     *      can be empty but never null. Oldest entries first.
166
     */
167
    public List<UpdateCenterJob> getJobs() {
K
kohsuke 已提交
168
        synchronized (jobs) {
169
            return new ArrayList<UpdateCenterJob>(jobs);
K
kohsuke 已提交
170
        }
171 172
    }

173 174 175 176 177 178 179 180
    /**
     * Gets the string representing how long ago the data was obtained.
     */
    public String getLastUpdatedString() {
        if(dataTimestamp<0)     return "N/A";
        return Util.getPastTimeString(System.currentTimeMillis()-dataTimestamp);
    }

181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
    /**
     * This is the endpoint that receives the update center data file from the browser.
     */
    public void doPostBack(StaplerRequest req) throws IOException {
        dataTimestamp = System.currentTimeMillis();
        String p = req.getParameter("json");
        JSONObject o = JSONObject.fromObject(p);
                
        int v = o.getInt("updateCenterVersion");
        if(v !=1) {
            LOGGER.warning("Unrecognized update center version: "+v);
            return;
        }

        LOGGER.info("Obtained the latest update center data file");
        getDataFile().write(p);
    }

199 200 201 202
    /**
     * Schedules a Hudson upgrade.
     */
    public void doUpgrade(StaplerResponse rsp) throws IOException, ServletException {
203
        requirePOST();
204
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
205
        HudsonUpgradeJob job = new HudsonUpgradeJob(Hudson.getAuthentication());
206
        if(!Lifecycle.get().canRewriteHudsonWar()) {
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
            sendError("Hudson upgrade not supported in this running mode");
            return;
        }

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

    private void addJob(UpdateCenterJob job) {
        // the first job is always the connectivity check
        if(jobs.size()==0)
            new ConnectionCheckJob().schedule();
        job.schedule();
    }

223 224 225 226 227
    /**
     * Loads the update center data, if any.
     *
     * @return  null if no data is available.
     */
228
    public Data getData() {
229 230
        TextFile df = getDataFile();
        if(df.exists()) {
231 232 233 234 235 236 237
            try {
                return new Data(JSONObject.fromObject(df.read()));
            } catch (IOException e) {
                LOGGER.log(Level.SEVERE,"Failed to parse "+df,e);
                df.delete(); // if we keep this file, it will cause repeated failures
                return null;
            }
238 239 240 241 242
        } else {
            return null;
        }
    }

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    /**
     * Returns a list of plugins that should be shown in the "available" tab.
     * These are "all plugins - installed plugins".
     */
    public List<Plugin> getAvailables() {
        List<Plugin> r = new ArrayList<Plugin>();
        Data data = getData();
        if(data ==null)     return Collections.emptyList();
        for (Plugin p : data.plugins.values()) {
            if(p.getInstalled()==null)
                r.add(p);
        }
        return r;
    }

258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
    /**
     * Gets the information about a specific plugin.
     *
     * @param artifactId
     *      The short name of the plugin. Corresponds to {@link PluginWrapper#getShortName()}.
     *
     * @return
     *      null if no such information is found.
     */
    public Plugin getPlugin(String artifactId) {
        Data dt = getData();
        if(dt==null)    return null;
        return dt.plugins.get(artifactId);
    }

273 274 275 276 277 278 279
    /**
     * This is where we store the update center data.
     */
    private TextFile getDataFile() {
        return new TextFile(new File(Hudson.getInstance().root,"update-center.json"));
    }

280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
    /**
     * Returns the list of plugins that are updates to currently installed ones.
     *
     * @return
     *      can be empty but never null.
     */
    public List<Plugin> getUpdates() {
        Data data = getData();
        if(data==null)      return Collections.emptyList(); // fail to determine

        List<Plugin> r = new ArrayList<Plugin>();
        for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) {
            Plugin p = pw.getUpdateInfo();
            if(p!=null) r.add(p);
        }

        return r;
    }

299 300 301 302 303 304 305 306 307 308 309 310 311
    /**
     * Does any of the plugin has updates?
     */
    public boolean hasUpdates() {
        Data data = getData();
        if(data==null)      return false;

        for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) {
            if(pw.getUpdateInfo() !=null) return true;
        }
        return false;
    }

K
kohsuke 已提交
312 313 314 315
    public String getDisplayName() {
        return "Update center";
    }

316 317 318 319
    public String getSearchUrl() {
        return "updateCenter";
    }

320 321 322 323 324 325 326
    /**
     * Exposed to get rid of hardcoding of the URL that serves up update-center.json
     * in Javascript.
     */
    public String getUrl() {
        return config.getUpdateCenterUrl();
    }
327 328 329 330

    /**
     * {@link AdministrativeMonitor} that checks if there's Hudson update.
     */
331 332
    @Extension
    public static final class CoreUpdateMonitor extends AdministrativeMonitor {
333 334 335 336 337 338
        public boolean isActivated() {
            Data data = getData();
            return data!=null && data.hasCoreUpdates();
        }

        public Data getData() {
339
            return Hudson.getInstance().getUpdateCenter().getData();
340 341 342
        }
    }

343 344 345
    /**
     * In-memory representation of the update center data.
     */
346
    public final class Data {
347 348 349 350 351 352 353
        /**
         * The latest hudson.war.
         */
        public final Entry core;
        /**
         * Plugins in the official repository, keyed by their artifact IDs.
         */
354
        public final Map<String,Plugin> plugins = new TreeMap<String,Plugin>(String.CASE_INSENSITIVE_ORDER);
355 356 357 358 359 360 361
        
        Data(JSONObject o) {
            core = new Entry(o.getJSONObject("core"));
            for(Map.Entry<String,JSONObject> e : (Set<Map.Entry<String,JSONObject>>)o.getJSONObject("plugins").entrySet()) {
                plugins.put(e.getKey(),new Plugin(e.getValue()));
            }
        }
362 363 364 365 366 367 368

        /**
         * Is there a new version of the core?
         */
        public boolean hasCoreUpdates() {
            return core.isNewerThan(Hudson.VERSION);
        }
369 370 371 372 373 374 375

        /**
         * Do we support upgrade?
         */
        public boolean canUpgrade() {
            return Lifecycle.get().canRewriteHudsonWar();
        }
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
    }

    public static class Entry {
        /**
         * Artifact ID.
         */
        public final String name;
        /**
         * The version.
         */
        public final String version;
        /**
         * Download URL.
         */
        public final String url;

        public Entry(JSONObject o) {
            this.name = o.getString("name");
            this.version = o.getString("version");
            this.url = o.getString("url");
        }
K
kohsuke 已提交
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414

        /**
         * Checks if the specified "current version" is older than the version of this entry.
         *
         * @param currentVersion
         *      The string that represents the version number to be compared.
         * @return
         *      true if the version listed in this entry is newer.
         *      false otherwise, including the situation where the strings couldn't be parsed as version numbers.
         */
        public boolean isNewerThan(String currentVersion) {
            try {
                return new VersionNumber(currentVersion).compareTo(new VersionNumber(version)) < 0;
            } catch (IllegalArgumentException e) {
                // couldn't parse as the version number.
                return false;
            }
        }
415 416
    }

417
    public final class Plugin extends Entry {
418 419 420 421 422 423
        /**
         * Optional URL to the Wiki page that discusses this plugin.
         */
        public final String wiki;
        /**
         * Human readable title of the plugin, taken from Wiki page.
424 425 426 427
         * Can be null.
         *
         * <p>
         * beware of XSS vulnerability since this data comes from Wiki 
428 429
         */
        public final String title;
K
kohsuke 已提交
430 431 432 433
        /**
         * Optional excerpt string.
         */
        public final String excerpt;
434 435 436 437 438 439

        @DataBoundConstructor
        public Plugin(JSONObject o) {
            super(o);
            this.wiki = get(o,"wiki");
            this.title = get(o,"title");
K
kohsuke 已提交
440
            this.excerpt = get(o,"excerpt");
441 442 443 444 445 446 447 448
        }

        private String get(JSONObject o, String prop) {
            if(o.has(prop))
                return o.getString(prop);
            else
                return null;
        }
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472

        public String getDisplayName() {
            if(title!=null) return title;
            return name;
        }

        /**
         * If some version of this plugin is currently installed, return {@link PluginWrapper}.
         * Otherwise null.
         */
        public PluginWrapper getInstalled() {
            PluginManager pm = Hudson.getInstance().getPluginManager();
            return pm.getPlugin(name);
        }

        /**
         * Schedules the installation of this plugin.
         *
         * <p>
         * This is mainly intended to be called from the UI. The actual installation work happens
         * asynchronously in another thread.
         */
        public void install() {
            Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
473
            addJob(new InstallationJob(this, Hudson.getAuthentication()));
474
        }
K
kohsuke 已提交
475 476 477 478 479 480 481 482

        /**
         * Making the installation web bound.
         */
        public void doInstall(StaplerResponse rsp) throws IOException {
            install();
            rsp.sendRedirect2("../..");
        }
483 484
    }

485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
    /**
     * Configuration data for controlling the update center's behaviors. The update
     * center's defaults will check internet connectivity by trying to connect
     * to www.google.com; will download plugins, the plugin catalog and updates
     * from hudson.dev.java.net; and will install plugins with file system
     * operations.
     * 
     * @since 1.266
     */
    public static class UpdateCenterConfiguration implements ExtensionPoint {
        /**
         * Check network connectivity by trying to establish a connection to
         * the host in connectionCheckUrl.
         * 
         * @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));
        }
        
        /**
         * Check connection to update center server.
         * 
         * @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"));
        }
        
        /**
         * Validate the URL of the resource before downloading it. The default
         * implementation enforces that the base of the resource URL starts
         * with the string returned by {@link #getPluginRepositoryBaseUrl()}.
         * 
         * @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 {
            // In the future if we are to open up update center to 3rd party, we need more elaborate scheme
            // like signing to ensure the safety of the bits.
            if(!src.toExternalForm().startsWith(getPluginRepositoryBaseUrl())) {
                throw new IOException("Installation of plugin from "+src+" is not allowed");
            }                    
        }
        
        /**
         * Validate the resource after it has been downloaded, before it is
         * installed. The default implementation does nothing.
         * 
         * @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 {
        }
        
        /**
         * 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.
         * 
         * @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());
574 575 576 577 578 579 580
            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);
581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
            }

            in.close();
            out.close();
            
            return tmp;
        }
        
        /**
         * Called after a plugin has been downloaded to move it into its final
         * location. The default implementation is a file rename.
         * 
         * @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);
        }
        
        /**
         * Called after an upgrade has been downloaded to move it into its final
         * location. The default implementation is a file rename.
         * 
         * @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);
        }        

        /**
         * Returns an "always up" server for Internet connectivity testing
         */
        public String getConnectionCheckUrl() {
            return "http://www.google.com";
        }
        
        /**
         * Returns the URL of the server that hosts the update-center.json
         * file.
K
kohsuke 已提交
625 626 627
         *
         * @return
         *      Absolute URL that ends with '/'.
628 629 630 631 632 633 634 635 636 637 638 639 640 641
         */
        public String getUpdateCenterUrl() {
            return "https://hudson.dev.java.net/";
        }
        
        /**
         * Returns the URL of the server that hosts plugins and core updates.
         */
        public String getPluginRepositoryBaseUrl() {
            return "https://hudson.dev.java.net/";
        }
        
        
        private void testConnection(URL url) throws IOException {
K
kohsuke 已提交
642 643 644 645 646 647 648 649 650
            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);
            }
651 652 653
        }                    
    }
    
654 655 656 657 658 659 660
    /**
     * 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 {
        public void schedule() {
661
            LOGGER.fine("Scheduling "+this+" to installerService");
662 663 664 665 666 667 668 669 670 671 672 673
            jobs.add(this);
            installerService.submit(this);
        }
    }

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

        public void run() {
674
            LOGGER.fine("Doing a connectivity check");
675
            try {
676 677
                String connectionCheckUrl = config.getConnectionCheckUrl();
                
678
                statuses.add(Messages.UpdateCenter_Status_CheckingInternet());
679
                try {
680
                    config.checkConnection(this, connectionCheckUrl);
681 682 683
                } catch (IOException e) {
                    if(e.getMessage().contains("Connection timed out")) {
                        // Google can't be down, so this is probably a proxy issue
684
                        statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl));
685 686 687
                        return;
                    }
                }
688

689
                statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet());
690
                config.checkUpdateCenter(this, config.getUpdateCenterUrl());
691

692
                statuses.add(Messages.UpdateCenter_Status_Success());
693
            } catch (UnknownHostException e) {
694
                statuses.add(Messages.UpdateCenter_Status_UnknownHostException(e.getMessage()));
695
                addStatus(e);
696 697 698 699 700
            } catch (IOException e) {
                statuses.add(Functions.printThrowable(e));
            }
        }

701 702 703 704
        private void addStatus(UnknownHostException e) {
            statuses.add("<pre>"+ Functions.xmlEscape(Functions.printThrowable(e))+"</pre>");
        }

705 706 707 708 709 710 711
        public String[] getStatuses() {
            synchronized (statuses) {
                return statuses.toArray(new String[statuses.size()]);
            }
        }
    }

712
    /**
713
     * Base class for a job that downloads a file from the Hudson project.
714
     */
715
    public abstract class DownloadJob extends UpdateCenterJob {
K
kohsuke 已提交
716 717 718 719 720 721 722 723
        /**
         * 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();
724

725 726 727 728 729 730 731 732 733 734
        /**
         * Where to download the file from.
         */
        protected abstract URL getURL() throws MalformedURLException;

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

K
kohsuke 已提交
735
        public abstract String getName();
736 737 738 739 740

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

742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757
        
        private Authentication authentication;
        
        /**
         * Get the user that initiated this job
         */
        public Authentication getUser()
        {
            return this.authentication;
        }
        
        protected DownloadJob(Authentication authentication)
        {
            this.authentication = authentication;
        }
        
K
kohsuke 已提交
758 759
        public void run() {
            try {
760
                LOGGER.info("Starting the installation of "+getName()+" on behalf of "+getUser().getName());
761 762

                URL src = getURL();
K
kohsuke 已提交
763

764
                config.preValidate(this, src);
K
kohsuke 已提交
765

766
                File dst = getDestination();
767 768 769 770 771
                File tmp = config.download(this, src);
                
                config.postValidate(this, tmp);
                config.install(this, tmp, dst);
                
772
                LOGGER.info("Installation successful: "+getName());
K
kohsuke 已提交
773
                status = new Success();
774
                onSuccess();
K
kohsuke 已提交
775
            } catch (IOException e) {
776
                LOGGER.log(Level.SEVERE, "Failed to install "+getName(),e);
K
kohsuke 已提交
777 778
                status = new Failure(e);
            }
779 780
        }

781 782 783 784 785 786 787 788 789 790 791
        /**
         * Called when the download is completed to overwrite
         * the old file with the new file.
         */
        protected void replace(File dst, File src) throws IOException {
            dst.delete();
            if(!src.renameTo(dst)) {
                throw new IOException("Failed to rename "+src+" to "+dst);
            }
        }

K
kohsuke 已提交
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813
        /**
         * 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);
            }
814
        }
815

816
        /**
K
kohsuke 已提交
817
         * Indicates that the plugin was successfully installed.
818
         */
K
kohsuke 已提交
819 820
        public class Success extends InstallationStatus {
        }
821

K
kohsuke 已提交
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839
        /**
         * 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;
            }
840 841 842
        }
    }

843 844 845 846 847 848 849 850 851 852 853
    /**
     * 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();

854 855
        public InstallationJob(Plugin plugin, Authentication auth) {
            super(auth);
856 857 858 859 860 861 862 863 864 865 866 867
            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 已提交
868 869
        public String getName() {
            return plugin.getDisplayName();
870 871 872 873 874
        }

        protected void onSuccess() {
            pm.pluginUploaded = true;
        }
875 876 877 878 879

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

882 883 884 885
    /**
     * Represents the state of the upgrade activity of Hudson core.
     */
    public final class HudsonUpgradeJob extends DownloadJob {
886 887
        public HudsonUpgradeJob(Authentication auth) {
            super(auth);
888 889 890 891 892 893 894 895 896 897
        }

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

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

K
kohsuke 已提交
898
        public String getName() {
899 900 901 902
            return "hudson.war";
        }

        protected void onSuccess() {
903
            status = new Success();
K
kohsuke 已提交
904 905
        }

906 907 908 909
        @Override
        protected void replace(File dst, File src) throws IOException {
            Lifecycle.get().rewriteHudsonWar(src);
        }
910 911
    }

912 913 914
    /**
     * Adds the update center data retriever to HTML.
     */
915
    @Extension
916 917 918 919 920 921
    public static class PageDecoratorImpl extends PageDecorator {
        public PageDecoratorImpl() {
            super(PageDecoratorImpl.class);
        }
    }

K
kohsuke 已提交
922 923 924 925 926
    /**
     * Sequence number generator.
     */
    private static final AtomicInteger iota = new AtomicInteger();

927 928 929
    private static final long DAY = DAYS.toMillis(1);

    private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName());
930 931

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