File D:\maven-qcr-plugin-testsunitaires\qcr4java\jenkins-master\target\site\qc\core\src\main\java\hudson\model\UpdateCenter.java.html

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., Seiji Sogabe
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 */
24 package hudson.model;
25
26 import hudson.BulkChange;
27 import hudson.Extension;
28 import hudson.ExtensionPoint;
29 import hudson.Functions;
30 import hudson.PluginManager;
31 import hudson.PluginWrapper;
32 import hudson.ProxyConfiguration;
33 import hudson.Util;
34 import hudson.XmlFile;
35 import static hudson.init.InitMilestone.PLUGINS_STARTED;
36 import hudson.init.Initializer;
37 import hudson.lifecycle.Lifecycle;
38 import hudson.lifecycle.RestartNotSupportedException;
39 import hudson.model.UpdateSite.Data;
40 import hudson.model.UpdateSite.Plugin;
41 import hudson.model.listeners.SaveableListener;
42 import hudson.security.ACL;
43 import hudson.util.DaemonThreadFactory;
44 import hudson.util.FormValidation;
45 import hudson.util.HttpResponses;
46 import hudson.util.IOException2;
47 import hudson.util.PersistedList;
48 import hudson.util.XStream2;
49 import jenkins.RestartRequiredException;
50 import jenkins.model.Jenkins;
51 import jenkins.util.io.OnMaster;
52 import org.acegisecurity.Authentication;
53 import org.acegisecurity.context.SecurityContext;
54 import org.apache.commons.io.input.CountingInputStream;
55 import org.apache.commons.io.output.NullOutputStream;
56 import org.jvnet.localizer.Localizable;
57 import org.kohsuke.stapler.HttpResponse;
58 import org.kohsuke.stapler.StaplerRequest;
59 import org.kohsuke.stapler.StaplerResponse;
60
61 import javax.net.ssl.SSLHandshakeException;
62 import javax.servlet.ServletException;
63 import java.io.File;
64 import java.io.FileOutputStream;
65 import java.io.IOException;
66 import java.io.OutputStream;
67 import java.net.MalformedURLException;
68 import java.net.URL;
69 import java.net.URLConnection;
70 import java.net.UnknownHostException;
71 import java.util.ArrayList;
72 import java.util.Collections;
73 import java.util.HashSet;
74 import java.util.LinkedHashMap;
75 import java.util.List;
76 import java.util.Map;
77 import java.util.Set;
78 import java.util.TreeSet;
79 import java.util.Vector;
80 import java.util.concurrent.ExecutionException;
81 import java.util.concurrent.ExecutorService;
82 import java.util.concurrent.Executors;
83 import java.util.concurrent.Future;
84 import java.util.concurrent.ThreadFactory;
85 import java.util.concurrent.atomic.AtomicInteger;
86 import java.util.jar.Attributes;
87 import java.util.jar.JarFile;
88 import java.util.logging.Level;
89 import java.util.logging.Logger;
90 import javax.annotation.CheckForNull;
91 import org.acegisecurity.context.SecurityContextHolder;
92 import org.kohsuke.stapler.export.Exported;
93 import org.kohsuke.stapler.export.ExportedBean;
94 import org.kohsuke.stapler.interceptor.RequirePOST;
95
96
97 /**
98 * Controls update center capability.
99 *
100 * <p>
101 * The main job of this class is to keep track of the latest update center metadata file, and perform installations.
102 * Much of the UI about choosing plugins to install is done in {@link PluginManager}.
103 * <p>
104 * The update center can be configured to contact alternate servers for updates
105 * and plugins, and to use alternate strategies for downloading, installing
106 * and updating components. See the Javadocs for {@link UpdateCenterConfiguration}
107 * for more information.
108 *
109 * @author Kohsuke Kawaguchi
110 * @since 1.220
111 */
112 @ExportedBean
113 public class UpdateCenter extends AbstractModelObject implements Saveable, OnMaster {
114
115 private static final String UPDATE_CENTER_URL = System.getProperty(UpdateCenter.class.getName()+".updateCenterUrl","http://updates.jenkins-ci.org/");
116
117 /**
118 * {@linkplain UpdateSite#getId() ID} of the default update site.
119 * @since 1.483
120 */
121 public static final String ID_DEFAULT = "default";
122
123 /**
124 * {@link ExecutorService} that performs installation.
125 * @since 1.501
126 */
127 private final ExecutorService installerService = Executors.newSingleThreadExecutor(
128 new DaemonThreadFactory(new ThreadFactory() {
129 public Thread newThread(Runnable r) {
130 Thread t = new Thread(r);
131 t.setName("Update center installer thread");
132 return t;
133 }
134 }));
135
136 /**
137 * An {@link ExecutorService} for updating UpdateSites.
138 */
139 protected final ExecutorService updateService = Executors.newCachedThreadPool(
140 new DaemonThreadFactory(new ThreadFactory() {
141 public Thread newThread(Runnable r) {
142 Thread t = new Thread(r);
143 t.setName("Update site data downloader");
144 return t;
145 }
146 }));
147
148 /**
149 * List of created {@link UpdateCenterJob}s. Access needs to be synchronized.
150 */
151 private final Vector<UpdateCenterJob> jobs = new Vector<UpdateCenterJob>();
152
153 /**
154 * {@link UpdateSite}s from which we've already installed a plugin at least once.
155 * This is used to skip network tests.
156 */
157 private final Set<UpdateSite> sourcesUsed = new HashSet<UpdateSite>();
158
159 /**
160 * List of {@link UpdateSite}s to be used.
161 */
162 private final PersistedList<UpdateSite> sites = new PersistedList<UpdateSite>(this);
163
164 /**
165 * Update center configuration data
166 */
167 private UpdateCenterConfiguration config;
168
169 private boolean requiresRestart;
170
171 public UpdateCenter() {
172 configure(new UpdateCenterConfiguration());
173 }
174
175 public Api getApi() {
176 return new Api(this);
177 }
178
179 /**
180 * Configures update center to get plugins/updates from alternate servers,
181 * and optionally using alternate strategies for downloading, installing
182 * and upgrading.
183 *
184 * @param config Configuration data
185 * @see UpdateCenterConfiguration
186 */
187 public void configure(UpdateCenterConfiguration config) {
188 if (config!=null) {
189 this.config = config;
190 }
191 }
192
193 /**
194 * Returns the list of {@link UpdateCenterJob} representing scheduled installation attempts.
195 *
196 * @return
197 * can be empty but never null. Oldest entries first.
198 */
199 @Exported
200 public List<UpdateCenterJob> getJobs() {
201 synchronized (jobs) {
202 return new ArrayList<UpdateCenterJob>(jobs);
203 }
204 }
205
206 /**
207 * Gets a job by its ID.
208 *
209 * Primarily to make {@link UpdateCenterJob} bound to URL.
210 */
211 public UpdateCenterJob getJob(int id) {
212 synchronized (jobs) {
213 for (UpdateCenterJob job : jobs) {
214 if (job.id==id)
215 return job;
216 }
217 }
218 return null;
219 }
220
221 /**
222 * Returns latest install/upgrade job for the given plugin.
223 * @return InstallationJob or null if not found
224 */
225 public InstallationJob getJob(Plugin plugin) {
226 List<UpdateCenterJob> jobList = getJobs();
227 Collections.reverse(jobList);
228 for (UpdateCenterJob job : jobList)
229 if (job instanceof InstallationJob) {
230 InstallationJob ij = (InstallationJob)job;
231 if (ij.plugin.name.equals(plugin.name) && ij.plugin.sourceId.equals(plugin.sourceId))
232 return ij;
233 }
234 return null;
235 }
236
237 /**
238 * Returns latest Jenkins upgrade job.
239 * @return HudsonUpgradeJob or null if not found
240 */
241 public HudsonUpgradeJob getHudsonJob() {
242 List<UpdateCenterJob> jobList = getJobs();
243 Collections.reverse(jobList);
244 for (UpdateCenterJob job : jobList)
245 if (job instanceof HudsonUpgradeJob)
246 return (HudsonUpgradeJob)job;
247 return null;
248 }
249
250 /**
251 * Returns the list of {@link UpdateSite}s to be used.
252 * This is a live list, whose change will be persisted automatically.
253 *
254 * @return
255 * can be empty but never null.
256 */
257 public PersistedList<UpdateSite> getSites() {
258 return sites;
259 }
260
261 /**
262 * The same as {@link #getSites()} but for REST API.
263 */
264 @Exported(name="sites")
265 public List<UpdateSite> getSiteList() {
266 return sites.toList();
267 }
268
269 /**
270 * Alias for {@link #getById}.
271 */
272 public UpdateSite getSite(String id) {
273 return getById(id);
274 }
275
276 /**
277 * Gets the string representing how long ago the data was obtained.
278 * Will be the newest of all {@link UpdateSite}s.
279 */
280 public String getLastUpdatedString() {
281 long newestTs = -1;
282 for (UpdateSite s : sites) {
283 if (s.getDataTimestamp()>newestTs) {
284 newestTs = s.getDataTimestamp();
285 }
286 }
287 if(newestTs<0) return "N/A";
288 return Util.getPastTimeString(System.currentTimeMillis()-newestTs);
289 }
290
291 /**
292 * Gets {@link UpdateSite} by its ID.
293 * Used to bind them to URL.
294 */
295 public UpdateSite getById(String id) {
296 for (UpdateSite s : sites) {
297 if (s.getId().equals(id)) {
298 return s;
299 }
300 }
301 return null;
302 }
303
304 /**
305 * Gets the {@link UpdateSite} from which we receive updates for <tt>jenkins.war</tt>.
306 *
307 * @return
308 * null if no such update center is provided.
309 */
310 public UpdateSite getCoreSource() {
311 for (UpdateSite s : sites) {
312 Data data = s.getData();
313 if (data!=null && data.core!=null)
314 return s;
315 }
316 return null;
317 }
318
319 /**
320 * Gets the default base URL.
321 *
322 * @deprecated
323 * TODO: revisit tool update mechanism, as that should be de-centralized, too. In the mean time,
324 * please try not to use this method, and instead ping us to get this part completed.
325 */
326 public String getDefaultBaseUrl() {
327 return config.getUpdateCenterUrl();
328 }
329
330 /**
331 * Gets the plugin with the given name from the first {@link UpdateSite} to contain it.
332 */
333 public @CheckForNull Plugin getPlugin(String artifactId) {
334 for (UpdateSite s : sites) {
335 Plugin p = s.getPlugin(artifactId);
336 if (p!=null) return p;
337 }
338 return null;
339 }
340
341 /**
342 * Schedules a Jenkins upgrade.
343 */
344 @RequirePOST
345 public void doUpgrade(StaplerResponse rsp) throws IOException, ServletException {
346 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
347 HudsonUpgradeJob job = new HudsonUpgradeJob(getCoreSource(), Jenkins.getAuthentication());
348 if(!Lifecycle.get().canRewriteHudsonWar()) {
349 sendError("Jenkins upgrade not supported in this running mode");
350 return;
351 }
352
353 LOGGER.info("Scheduling the core upgrade");
354 addJob(job);
355 rsp.sendRedirect2(".");
356 }
357
358 /**
359 * Invalidates the update center JSON data for all the sites and force re-retrieval.
360 *
361 * @since 1.432
362 */
363 public HttpResponse doInvalidateData() {
364 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
365 for (UpdateSite site : sites) {
366 site.doInvalidateData();
367 }
368
369 return HttpResponses.ok();
370 }
371
372
373 /**
374 * Schedules a Jenkins restart.
375 */
376 public void doSafeRestart(StaplerRequest request, StaplerResponse response) throws IOException, ServletException {
377 synchronized (jobs) {
378 if (!isRestartScheduled()) {
379 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
380 addJob(new RestartJenkinsJob(getCoreSource()));
381 LOGGER.info("Scheduling Jenkins reboot");
382 }
383 }
384 response.sendRedirect2(".");
385 }
386
387 /**
388 * Cancel all scheduled jenkins restarts
389 */
390 public void doCancelRestart(StaplerResponse response) throws IOException, ServletException {
391 synchronized (jobs) {
392 for (UpdateCenterJob job : jobs) {
393 if (job instanceof RestartJenkinsJob) {
394 if (((RestartJenkinsJob) job).cancel()) {
395 LOGGER.info("Scheduled Jenkins reboot unscheduled");
396 }
397 }
398 }
399 }
400 response.sendRedirect2(".");
401 }
402
403 /**
404 * If any of the executed {@link UpdateCenterJob}s requires a restart
405 * to take effect, this method returns true.
406 *
407 * <p>
408 * This doesn't necessarily mean the user has scheduled or initiated
409 * the restart operation.
410 *
411 * @see #isRestartScheduled()
412 */
413 @Exported
414 public boolean isRestartRequiredForCompletion() {
415 return requiresRestart;
416 }
417
418 /**
419 * Checks if the restart operation is scheduled
420 * (which means in near future Jenkins will restart by itself)
421 *
422 * @see #isRestartRequiredForCompletion()
423 */
424 public boolean isRestartScheduled() {
425 for (UpdateCenterJob job : getJobs()) {
426 if (job instanceof RestartJenkinsJob) {
427 RestartJenkinsJob.RestartJenkinsJobStatus status = ((RestartJenkinsJob) job).status;
428 if (status instanceof RestartJenkinsJob.Pending
429 || status instanceof RestartJenkinsJob.Running) {
430 return true;
431 }
432 }
433 }
434 return false;
435 }
436
437 /**
438 * Returns true if backup of jenkins.war exists on the hard drive
439 */
440 public boolean isDowngradable() {
441 return new File(Lifecycle.get().getHudsonWar() + ".bak").exists();
442 }
443
444 /**
445 * Performs hudson downgrade.
446 */
447 @RequirePOST
448 public void doDowngrade(StaplerResponse rsp) throws IOException, ServletException {
449 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
450 if(!isDowngradable()) {
451 sendError("Jenkins downgrade is not possible, probably backup does not exist");
452 return;
453 }
454
455 HudsonDowngradeJob job = new HudsonDowngradeJob(getCoreSource(), Jenkins.getAuthentication());
456 LOGGER.info("Scheduling the core downgrade");
457 addJob(job);
458 rsp.sendRedirect2(".");
459 }
460
461 /**
462 * Performs hudson downgrade.
463 */
464 public void doRestart(StaplerResponse rsp) throws IOException, ServletException {
465 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
466 HudsonDowngradeJob job = new HudsonDowngradeJob(getCoreSource(), Jenkins.getAuthentication());
467 LOGGER.info("Scheduling the core downgrade");
468
469 addJob(job);
470 rsp.sendRedirect2(".");
471 }
472
473 /**
474 * Returns String with version of backup .war file,
475 * if the file does not exists returns null
476 */
477 public String getBackupVersion() {
478 try {
479 JarFile backupWar = new JarFile(new File(Lifecycle.get().getHudsonWar() + ".bak"));
480 Attributes attrs = backupWar.getManifest().getMainAttributes();
481 String v = attrs.getValue("Jenkins-Version");
482 if (v==null) v = attrs.getValue("Hudson-Version");
483 return v;
484 } catch (IOException e) {
485 LOGGER.log(Level.WARNING, "Failed to read backup version ", e);
486 return null;}
487
488 }
489
490 /*package*/ synchronized Future<UpdateCenterJob> addJob(UpdateCenterJob job) {
491 // the first job is always the connectivity check
492 if (sourcesUsed.add(job.site))
493 new ConnectionCheckJob(job.site).submit();
494 return job.submit();
495 }
496
497 public String getDisplayName() {
498 return "Update center";
499 }
500
501 public String getSearchUrl() {
502 return "updateCenter";
503 }
504
505 /**
506 * Saves the configuration info to the disk.
507 */
508 public synchronized void save() {
509 if(BulkChange.contains(this)) return;
510 try {
511 getConfigFile().write(sites);
512 SaveableListener.fireOnChange(this, getConfigFile());
513 } catch (IOException e) {
514 LOGGER.log(Level.WARNING, "Failed to save "+getConfigFile(),e);
515 }
516 }
517
518 /**
519 * Loads the data from the disk into this object.
520 */
521 public synchronized void load() throws IOException {
522 UpdateSite defaultSite = new UpdateSite(ID_DEFAULT, config.getUpdateCenterUrl() + "update-center.json");
523 XmlFile file = getConfigFile();
524 if(file.exists()) {
525 try {
526 sites.replaceBy(((PersistedList)file.unmarshal(sites)).toList());
527 } catch (IOException e) {
528 LOGGER.log(Level.WARNING, "Failed to load "+file, e);
529 }
530 for (UpdateSite site : sites) {
531 // replace the legacy site with the new site
532 if (site.isLegacyDefault()) {
533 sites.remove(site);
534 sites.add(defaultSite);
535 break;
536 }
537 }
538 } else {
539 if (sites.isEmpty()) {
540 // If there aren't already any UpdateSources, add the default one.
541 // to maintain compatibility with existing UpdateCenterConfiguration, create the default one as specified by UpdateCenterConfiguration
542 sites.add(defaultSite);
543 }
544 }
545 }
546
547 private XmlFile getConfigFile() {
548 return new XmlFile(XSTREAM,new File(Jenkins.getInstance().root,
549 UpdateCenter.class.getName()+".xml"));
550 }
551
552 public List<Plugin> getAvailables() {
553 Map<String,Plugin> pluginMap = new LinkedHashMap<String, Plugin>();
554 for (UpdateSite site : sites) {
555 for (Plugin plugin: site.getAvailables()) {
556 final Plugin existing = pluginMap.get(plugin.name);
557 if (existing == null) {
558 pluginMap.put(plugin.name, plugin);
559 } else if (!existing.version.equals(plugin.version)) {
560 // allow secondary update centers to publish different versions
561 // TODO refactor to consolidate multiple versions of the same plugin within the one row
562 final String altKey = plugin.name + ":" + plugin.version;
563 if (!pluginMap.containsKey(altKey)) {
564 pluginMap.put(altKey, plugin);
565 }
566 }
567 }
568 }
569
570 return new ArrayList<Plugin>(pluginMap.values());
571 }
572
573 /**
574 * Returns a list of plugins that should be shown in the "available" tab, grouped by category.
575 * A plugin with multiple categories will appear multiple times in the list.
576 */
577 public PluginEntry[] getCategorizedAvailables() {
578 TreeSet<PluginEntry> entries = new TreeSet<PluginEntry>();
579 for (Plugin p : getAvailables()) {
580 if (p.categories==null || p.categories.length==0)
581 entries.add(new PluginEntry(p, getCategoryDisplayName(null)));
582 else
583 for (String c : p.categories)
584 entries.add(new PluginEntry(p, getCategoryDisplayName(c)));
585 }
586 return entries.toArray(new PluginEntry[entries.size()]);
587 }
588
589 private static String getCategoryDisplayName(String category) {
590 if (category==null)
591 return Messages.UpdateCenter_PluginCategory_misc();
592 try {
593 return (String)Messages.class.getMethod(
594 "UpdateCenter_PluginCategory_" + category.replace('-', '_')).invoke(null);
595 } catch (Exception ex) {
596 return Messages.UpdateCenter_PluginCategory_unrecognized(category);
597 }
598 }
599
600 public List<Plugin> getUpdates() {
601 Map<String,Plugin> pluginMap = new LinkedHashMap<String, Plugin>();
602 for (UpdateSite site : sites) {
603 for (Plugin plugin: site.getUpdates()) {
604 final Plugin existing = pluginMap.get(plugin.name);
605 if (existing == null) {
606 pluginMap.put(plugin.name, plugin);
607 } else if (!existing.version.equals(plugin.version)) {
608 // allow secondary update centers to publish different versions
609 // TODO refactor to consolidate multiple versions of the same plugin within the one row
610 final String altKey = plugin.name + ":" + plugin.version;
611 if (!pluginMap.containsKey(altKey)) {
612 pluginMap.put(altKey, plugin);
613 }
614 }
615 }
616 }
617
618 return new ArrayList<Plugin>(pluginMap.values());
619 }
620
621 /**
622 * Ensure that all UpdateSites are up to date, without requiring a user to
623 * browse to the instance.
624 *
625 * @return a list of {@link FormValidation} for each updated Update Site
626 * @throws ExecutionException
627 * @throws InterruptedException
628 * @since 1.501
629 *
630 */
631 public List<FormValidation> updateAllSites() throws InterruptedException, ExecutionException {
632 List <Future<FormValidation>> futures = new ArrayList<Future<FormValidation>>();
633 for (UpdateSite site : getSites()) {
634 Future<FormValidation> future = site.updateDirectly(true);
635 if (future != null) {
636 futures.add(future);
637 }
638 }
639
640 List<FormValidation> results = new ArrayList<FormValidation>();
641 for (Future<FormValidation> f : futures) {
642 results.add(f.get());
643 }
644 return results;
645 }
646
647
648 /**
649 * {@link AdministrativeMonitor} that checks if there's Jenkins update.
650 */
651 @Extension
652 public static final class CoreUpdateMonitor extends AdministrativeMonitor {
653 public boolean isActivated() {
654 Data data = getData();
655 return data!=null && data.hasCoreUpdates();
656 }
657
658 public Data getData() {
659 UpdateSite cs = Jenkins.getInstance().getUpdateCenter().getCoreSource();
660 if (cs!=null) return cs.getData();
661 return null;
662 }
663 }
664
665
666 /**
667 * Strategy object for controlling the update center's behaviors.
668 *
669 * <p>
670 * Until 1.333, this extension point used to control the configuration of
671 * where to get updates (hence the name of this class), but with the introduction
672 * of multiple update center sites capability, that functionality is achieved by
673 * simply installing another {@link UpdateSite}.
674 *
675 * <p>
676 * See {@link UpdateSite} for how to manipulate them programmatically.
677 *
678 * @since 1.266
679 */
680 @SuppressWarnings({"UnusedDeclaration"})
681 public static class UpdateCenterConfiguration implements ExtensionPoint {
682 /**
683 * Creates default update center configuration - uses settings for global update center.
684 */
685 public UpdateCenterConfiguration() {
686 }
687
688 /**
689 * Check network connectivity by trying to establish a connection to
690 * the host in connectionCheckUrl.
691 *
692 * @param job The connection checker that is invoking this strategy.
693 * @param connectionCheckUrl A string containing the URL of a domain
694 * that is assumed to be always available.
695 * @throws IOException if a connection can't be established
696 */
697 public void checkConnection(ConnectionCheckJob job, String connectionCheckUrl) throws IOException {
698 testConnection(new URL(connectionCheckUrl));
699 }
700
701 /**
702 * Check connection to update center server.
703 *
704 * @param job The connection checker that is invoking this strategy.
705 * @param updateCenterUrl A sting containing the URL of the update center host.
706 * @throws IOException if a connection to the update center server can't be established.
707 */
708 public void checkUpdateCenter(ConnectionCheckJob job, String updateCenterUrl) throws IOException {
709 testConnection(new URL(updateCenterUrl + "?uctest"));
710 }
711
712 /**
713 * Validate the URL of the resource before downloading it.
714 *
715 * @param job The download job that is invoking this strategy. This job is
716 * responsible for managing the status of the download and installation.
717 * @param src The location of the resource on the network
718 * @throws IOException if the validation fails
719 */
720 public void preValidate(DownloadJob job, URL src) throws IOException {
721 }
722
723 /**
724 * Validate the resource after it has been downloaded, before it is
725 * installed. The default implementation does nothing.
726 *
727 * @param job The download job that is invoking this strategy. This job is
728 * responsible for managing the status of the download and installation.
729 * @param src The location of the downloaded resource.
730 * @throws IOException if the validation fails.
731 */
732 public void postValidate(DownloadJob job, File src) throws IOException {
733 }
734
735 /**
736 * Download a plugin or core upgrade in preparation for installing it
737 * into its final location. Implementations will normally download the
738 * resource into a temporary location and hand off a reference to this
739 * location to the install or upgrade strategy to move into the final location.
740 *
741 * @param job The download job that is invoking this strategy. This job is
742 * responsible for managing the status of the download and installation.
743 * @param src The URL to the resource to be downloaded.
744 * @return A File object that describes the downloaded resource.
745 * @throws IOException if there were problems downloading the resource.
746 * @see DownloadJob
747 */
748 public File download(DownloadJob job, URL src) throws IOException {
749 try {
750 URLConnection con = connect(job,src);
751 int total = con.getContentLength();
752 CountingInputStream in = new CountingInputStream(con.getInputStream());
753 byte[] buf = new byte[8192];
754 int len;
755
756 File dst = job.getDestination();
757 File tmp = new File(dst.getPath()+".tmp");
758 OutputStream out = new FileOutputStream(tmp);
759
760 LOGGER.info("Downloading "+job.getName());
761 try {
762 while((len=in.read(buf))>=0) {
763 out.write(buf,0,len);
764 job.status = job.new Installing(total==-1 ? -1 : in.getCount()*100/total);
765 }
766 } catch (IOException e) {
767 throw new IOException2("Failed to load "+src+" to "+tmp,e);
768 }
769
770 in.close();
771 out.close();
772
773 if (total!=-1 && total!=tmp.length()) {
774 // don't know exactly how this happens, but report like
775 // http://www.ashlux.com/wordpress/2009/08/14/hudson-and-the-sonar-plugin-fail-maveninstallation-nosuchmethoderror/
776 // indicates that this kind of inconsistency can happen. So let's be defensive
777 throw new IOException("Inconsistent file length: expected "+total+" but only got "+tmp.length());
778 }
779
780 return tmp;
781 } catch (IOException e) {
782 throw new IOException2("Failed to download from "+src,e);
783 }
784 }
785
786 /**
787 * Connects to the given URL for downloading the binary. Useful for tweaking
788 * how the connection gets established.
789 */
790 protected URLConnection connect(DownloadJob job, URL src) throws IOException {
791 return ProxyConfiguration.open(src);
792 }
793
794 /**
795 * Called after a plugin has been downloaded to move it into its final
796 * location. The default implementation is a file rename.
797 *
798 * @param job The install job that is invoking this strategy.
799 * @param src The temporary location of the plugin.
800 * @param dst The final destination to install the plugin to.
801 * @throws IOException if there are problems installing the resource.
802 */
803 public void install(DownloadJob job, File src, File dst) throws IOException {
804 job.replace(dst, src);
805 }
806
807 /**
808 * Called after an upgrade has been downloaded to move it into its final
809 * location. The default implementation is a file rename.
810 *
811 * @param job The upgrade job that is invoking this strategy.
812 * @param src The temporary location of the upgrade.
813 * @param dst The final destination to install the upgrade to.
814 * @throws IOException if there are problems installing the resource.
815 */
816 public void upgrade(DownloadJob job, File src, File dst) throws IOException {
817 job.replace(dst, src);
818 }
819
820 /**
821 * Returns an "always up" server for Internet connectivity testing.
822 *
823 * @deprecated as of 1.333
824 * With the introduction of multiple update center capability, this information
825 * is now a part of the <tt>update-center.json</tt> file. See
826 * <tt>http://jenkins-ci.org/update-center.json</tt> as an example.
827 */
828 public String getConnectionCheckUrl() {
829 return "http://www.google.com";
830 }
831
832 /**
833 * Returns the URL of the server that hosts the update-center.json
834 * file.
835 *
836 * @deprecated as of 1.333
837 * With the introduction of multiple update center capability, this information
838 * is now moved to {@link UpdateSite}.
839 * @return
840 * Absolute URL that ends with '/'.
841 */
842 public String getUpdateCenterUrl() {
843 return UPDATE_CENTER_URL;
844 }
845
846 /**
847 * Returns the URL of the server that hosts plugins and core updates.
848 *
849 * @deprecated as of 1.333
850 * <tt>update-center.json</tt> is now signed, so we don't have to further make sure that
851 * we aren't downloading from anywhere unsecure.
852 */
853 public String getPluginRepositoryBaseUrl() {
854 return "http://jenkins-ci.org/";
855 }
856
857
858 private void testConnection(URL url) throws IOException {
859 try {
860 Util.copyStreamAndClose(ProxyConfiguration.open(url).getInputStream(),new NullOutputStream());
861 } catch (SSLHandshakeException e) {
862 if (e.getMessage().contains("PKIX path building failed"))
863 // fix up this crappy error message from JDK
864 throw new IOException2("Failed to validate the SSL certificate of "+url,e);
865 }
866 }
867 }
868
869 /**
870 * Things that {@link UpdateCenter#installerService} executes.
871 *
872 * This object will have the <tt>row.jelly</tt> which renders the job on UI.
873 */
874 @ExportedBean
875 public abstract class UpdateCenterJob implements Runnable {
876 /**
877 * Unique ID that identifies this job.
878 *
879 * @see UpdateCenter#getJob(int)
880 */
881 @Exported
882 public final int id = iota.incrementAndGet();
883
884 /**
885 * Which {@link UpdateSite} does this belong to?
886 */
887 public final UpdateSite site;
888
889 /**
890 * If this job fails, set to the error.
891 */
892 protected Throwable error;
893
894 protected UpdateCenterJob(UpdateSite site) {
895 this.site = site;
896 }
897
898 public Api getApi() {
899 return new Api(this);
900 }
901
902 /**
903 * @deprecated as of 1.326
904 * Use {@link #submit()} instead.
905 */
906 public void schedule() {
907 submit();
908 }
909
910 @Exported
911 public String getType() {
912 return getClass().getSimpleName();
913 }
914
915 /**
916 * Schedules this job for an execution
917 * @return
918 * {@link Future} to keeps track of the status of the execution.
919 */
920 public Future<UpdateCenterJob> submit() {
921 LOGGER.fine("Scheduling "+this+" to installerService");
922 jobs.add(this);
923 return installerService.submit(this,this);
924 }
925
926 @Exported
927 public String getErrorMessage() {
928 return error != null ? error.getMessage() : null;
929 }
930
931 public Throwable getError() {
932 return error;
933 }
934 }
935
936 /**
937 * Restarts jenkins.
938 */
939 public class RestartJenkinsJob extends UpdateCenterJob {
940 /**
941 * Immutable state of this job.
942 */
943 @Exported(inline=true)
944 public volatile RestartJenkinsJobStatus status = new Pending();
945
946 /**
947 * Cancel job
948 */
949 public synchronized boolean cancel() {
950 if (status instanceof Pending) {
951 status = new Canceled();
952 return true;
953 }
954 return false;
955 }
956
957 public RestartJenkinsJob(UpdateSite site) {
958 super(site);
959 }
960
961 public synchronized void run() {
962 if (!(status instanceof Pending)) {
963 return;
964 }
965 status = new Running();
966 try {
967 Jenkins.getInstance().safeRestart();
968 } catch (RestartNotSupportedException exception) {
969 // ignore if restart is not allowed
970 status = new Failure();
971 error = exception;
972 }
973 }
974
975 @ExportedBean
976 public abstract class RestartJenkinsJobStatus {
977 @Exported
978 public final int id = iota.incrementAndGet();
979
980 }
981
982 public class Pending extends RestartJenkinsJobStatus {
983 @Exported
984 public String getType() {
985 return getClass().getSimpleName();
986 }
987 }
988
989 public class Running extends RestartJenkinsJobStatus {
990
991 }
992
993 public class Failure extends RestartJenkinsJobStatus {
994
995 }
996
997 public class Canceled extends RestartJenkinsJobStatus {
998
999 }
1000 }
1001
1002 /**
1003 * Tests the internet connectivity.
1004 */
1005 public final class ConnectionCheckJob extends UpdateCenterJob {
1006 private final Vector<String> statuses= new Vector<String>();
1007
1008 public ConnectionCheckJob(UpdateSite site) {
1009 super(site);
1010 }
1011
1012 public void run() {
1013 LOGGER.fine("Doing a connectivity check");
1014 try {
1015 String connectionCheckUrl = site.getConnectionCheckUrl();
1016 if (connectionCheckUrl!=null) {
1017 statuses.add(Messages.UpdateCenter_Status_CheckingInternet());
1018 try {
1019 config.checkConnection(this, connectionCheckUrl);
1020 } catch (IOException e) {
1021 if(e.getMessage().contains("Connection timed out")) {
1022 // Google can't be down, so this is probably a proxy issue
1023 statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl));
1024 return;
1025 }
1026 }
1027 }
1028
1029 statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet());
1030 config.checkUpdateCenter(this, site.getUrl());
1031
1032 statuses.add(Messages.UpdateCenter_Status_Success());
1033 } catch (UnknownHostException e) {
1034 statuses.add(Messages.UpdateCenter_Status_UnknownHostException(e.getMessage()));
1035 addStatus(e);
1036 error = e;
1037 } catch (IOException e) {
1038 statuses.add(Functions.printThrowable(e));
1039 error = e;
1040 }
1041 }
1042
1043 private void addStatus(UnknownHostException e) {
1044 statuses.add("<pre>"+ Functions.xmlEscape(Functions.printThrowable(e))+"</pre>");
1045 }
1046
1047 public String[] getStatuses() {
1048 synchronized (statuses) {
1049 return statuses.toArray(new String[statuses.size()]);
1050 }
1051 }
1052 }
1053
1054 /**
1055 * Base class for a job that downloads a file from the Jenkins project.
1056 */
1057 public abstract class DownloadJob extends UpdateCenterJob {
1058 /**
1059 * Immutable object representing the current state of this job.
1060 */
1061 @Exported(inline=true)
1062 public volatile InstallationStatus status = new Pending();
1063
1064 /**
1065 * Where to download the file from.
1066 */
1067 protected abstract URL getURL() throws MalformedURLException;
1068
1069 /**
1070 * Where to download the file to.
1071 */
1072 protected abstract File getDestination();
1073
1074 @Exported
1075 public abstract String getName();
1076
1077 /**
1078 * Called when the whole thing went successfully.
1079 */
1080 protected abstract void onSuccess();
1081
1082
1083 private Authentication authentication;
1084
1085 /**
1086 * Get the user that initiated this job
1087 */
1088 public Authentication getUser() {
1089 return this.authentication;
1090 }
1091
1092 protected DownloadJob(UpdateSite site, Authentication authentication) {
1093 super(site);
1094 this.authentication = authentication;
1095 }
1096
1097 public void run() {
1098 try {
1099 LOGGER.info("Starting the installation of "+getName()+" on behalf of "+getUser().getName());
1100
1101 _run();
1102
1103 LOGGER.info("Installation successful: "+getName());
1104 status = new Success();
1105 onSuccess();
1106 } catch (InstallationStatus e) {
1107 status = e;
1108 if (status.isSuccess()) onSuccess();
1109 requiresRestart |= status.requiresRestart();
1110 } catch (Throwable e) {
1111 LOGGER.log(Level.SEVERE, "Failed to install "+getName(),e);
1112 status = new Failure(e);
1113 error = e;
1114 }
1115 }
1116
1117 protected void _run() throws IOException, InstallationStatus {
1118 URL src = getURL();
1119
1120 config.preValidate(this, src);
1121
1122 File dst = getDestination();
1123 File tmp = config.download(this, src);
1124
1125 config.postValidate(this, tmp);
1126 config.install(this, tmp, dst);
1127 }
1128
1129 /**
1130 * Called when the download is completed to overwrite
1131 * the old file with the new file.
1132 */
1133 protected void replace(File dst, File src) throws IOException {
1134 File bak = Util.changeExtension(dst,".bak");
1135 bak.delete();
1136 dst.renameTo(bak);
1137 dst.delete(); // any failure up to here is no big deal
1138 if(!src.renameTo(dst)) {
1139 throw new IOException("Failed to rename "+src+" to "+dst);
1140 }
1141 }
1142
1143 /**
1144 * Indicates the status or the result of a plugin installation.
1145 * <p>
1146 * Instances of this class is immutable.
1147 */
1148 @ExportedBean
1149 public abstract class InstallationStatus extends Throwable {
1150 public final int id = iota.incrementAndGet();
1151 @Exported
1152 public boolean isSuccess() {
1153 return false;
1154 }
1155 @Exported
1156 public final String getType() {
1157 return getClass().getSimpleName();
1158 }
1159
1160 /**
1161 * Indicates that a restart is needed to complete the tasks.
1162 */
1163 public boolean requiresRestart() {
1164 return false;
1165 }
1166 }
1167
1168 /**
1169 * Indicates that the installation of a plugin failed.
1170 */
1171 public class Failure extends InstallationStatus {
1172 public final Throwable problem;
1173
1174 public Failure(Throwable problem) {
1175 this.problem = problem;
1176 }
1177
1178 public String getProblemStackTrace() {
1179 return Functions.printThrowable(problem);
1180 }
1181 }
1182
1183 /**
1184 * Indicates that the installation was successful but a restart is needed.
1185 *
1186 * @see
1187 */
1188 public class SuccessButRequiresRestart extends Success {
1189 private final Localizable message;
1190
1191 public SuccessButRequiresRestart(Localizable message) {
1192 this.message = message;
1193 }
1194
1195 public String getMessage() {
1196 return message.toString();
1197 }
1198
1199 @Override
1200 public boolean requiresRestart() {
1201 return true;
1202 }
1203 }
1204
1205 /**
1206 * Indicates that the plugin was successfully installed.
1207 */
1208 public class Success extends InstallationStatus {
1209 @Override public boolean isSuccess() {
1210 return true;
1211 }
1212 }
1213
1214 /**
1215 * Indicates that the plugin is waiting for its turn for installation.
1216 */
1217 public class Pending extends InstallationStatus {
1218 }
1219
1220 /**
1221 * Installation of a plugin is in progress.
1222 */
1223 public class Installing extends InstallationStatus {
1224 /**
1225 * % completed download, or -1 if the percentage is not known.
1226 */
1227 public final int percentage;
1228
1229 public Installing(int percentage) {
1230 this.percentage = percentage;
1231 }
1232 }
1233 }
1234
1235 /**
1236 * Represents the state of the installation activity of one plugin.
1237 */
1238 public final class InstallationJob extends DownloadJob {
1239 /**
1240 * What plugin are we trying to install?
1241 */
1242 @Exported
1243 public final Plugin plugin;
1244
1245 private final PluginManager pm = Jenkins.getInstance().getPluginManager();
1246
1247 /**
1248 * True to load the plugin into this Jenkins, false to wait until restart.
1249 */
1250 private final boolean dynamicLoad;
1251
1252 /**
1253 * @deprecated as of 1.442
1254 */
1255 public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth) {
1256 this(plugin,site,auth,false);
1257 }
1258
1259 public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth, boolean dynamicLoad) {
1260 super(site, auth);
1261 this.plugin = plugin;
1262 this.dynamicLoad = dynamicLoad;
1263 }
1264
1265 protected URL getURL() throws MalformedURLException {
1266 return new URL(plugin.url);
1267 }
1268
1269 protected File getDestination() {
1270 File baseDir = pm.rootDir;
1271 return new File(baseDir, plugin.name + ".jpi");
1272 }
1273
1274 private File getLegacyDestination() {
1275 File baseDir = pm.rootDir;
1276 return new File(baseDir, plugin.name + ".hpi");
1277 }
1278
1279 public String getName() {
1280 return plugin.getDisplayName();
1281 }
1282
1283 @Override
1284 public void _run() throws IOException, InstallationStatus {
1285 super._run();
1286
1287 // if this is a bundled plugin, make sure it won't get overwritten
1288 PluginWrapper pw = plugin.getInstalled();
1289 if (pw!=null && pw.isBundled()) {
1290 SecurityContext oldContext = ACL.impersonate(ACL.SYSTEM);
1291 try {
1292 pw.doPin();
1293 } finally {
1294 SecurityContextHolder.setContext(oldContext);
1295 }
1296 }
1297
1298 if (dynamicLoad) {
1299 try {
1300 pm.dynamicLoad(getDestination());
1301 } catch (RestartRequiredException e) {
1302 throw new SuccessButRequiresRestart(e.message);
1303 } catch (Exception e) {
1304 throw new IOException2("Failed to dynamically deploy this plugin",e);
1305 }
1306 } else {
1307 throw new SuccessButRequiresRestart(Messages._UpdateCenter_DownloadButNotActivated());
1308 }
1309 }
1310
1311 protected void onSuccess() {
1312 pm.pluginUploaded = true;
1313 }
1314
1315 @Override
1316 public String toString() {
1317 return super.toString()+"[plugin="+plugin.title+"]";
1318 }
1319
1320 /**
1321 * Called when the download is completed to overwrite
1322 * the old file with the new file.
1323 */
1324 @Override
1325 protected void replace(File dst, File src) throws IOException {
1326 File bak = Util.changeExtension(dst,".bak");
1327
1328 bak.delete();
1329 final File legacy = getLegacyDestination();
1330 if(legacy.exists()){
1331 legacy.renameTo(bak);
1332 }else{
1333 dst.renameTo(bak);
1334 }
1335 legacy.delete();
1336 dst.delete(); // any failure up to here is no big deal
1337
1338 if(!src.renameTo(dst)) {
1339 throw new IOException("Failed to rename "+src+" to "+dst);
1340 }
1341 }
1342 }
1343
1344 /**
1345 * Represents the state of the downgrading activity of plugin.
1346 */
1347 public final class PluginDowngradeJob extends DownloadJob {
1348 /**
1349 * What plugin are we trying to install?
1350 */
1351 public final Plugin plugin;
1352
1353 private final PluginManager pm = Jenkins.getInstance().getPluginManager();
1354
1355 public PluginDowngradeJob(Plugin plugin, UpdateSite site, Authentication auth) {
1356 super(site, auth);
1357 this.plugin = plugin;
1358 }
1359
1360 protected URL getURL() throws MalformedURLException {
1361 return new URL(plugin.url);
1362 }
1363
1364 protected File getDestination() {
1365 File baseDir = pm.rootDir;
1366 final File legacy = new File(baseDir, plugin.name + ".hpi");
1367 if(legacy.exists()){
1368 return legacy;
1369 }
1370 return new File(baseDir, plugin.name + ".jpi");
1371 }
1372
1373 protected File getBackup() {
1374 File baseDir = pm.rootDir;
1375 return new File(baseDir, plugin.name + ".bak");
1376 }
1377
1378 public String getName() {
1379 return plugin.getDisplayName();
1380 }
1381
1382 @Override
1383 public void run() {
1384 try {
1385 LOGGER.info("Starting the downgrade of "+getName()+" on behalf of "+getUser().getName());
1386
1387 _run();
1388
1389 LOGGER.info("Downgrade successful: "+getName());
1390 status = new Success();
1391 onSuccess();
1392 } catch (Throwable e) {
1393 LOGGER.log(Level.SEVERE, "Failed to downgrade "+getName(),e);
1394 status = new Failure(e);
1395 error = e;
1396 }
1397 }
1398
1399 @Override
1400 protected void _run() throws IOException {
1401 File dst = getDestination();
1402 File backup = getBackup();
1403
1404 config.install(this, backup, dst);
1405 }
1406
1407 /**
1408 * Called to overwrite
1409 * current version with backup file
1410 */
1411 @Override
1412 protected void replace(File dst, File backup) throws IOException {
1413 dst.delete(); // any failure up to here is no big deal
1414 if(!backup.renameTo(dst)) {
1415 throw new IOException("Failed to rename "+backup+" to "+dst);
1416 }
1417 }
1418
1419 protected void onSuccess() {
1420 pm.pluginUploaded = true;
1421 }
1422
1423 @Override
1424 public String toString() {
1425 return super.toString()+"[plugin="+plugin.title+"]";
1426 }
1427 }
1428
1429 /**
1430 * Represents the state of the upgrade activity of Jenkins core.
1431 */
1432 public final class HudsonUpgradeJob extends DownloadJob {
1433 public HudsonUpgradeJob(UpdateSite site, Authentication auth) {
1434 super(site, auth);
1435 }
1436
1437 protected URL getURL() throws MalformedURLException {
1438 return new URL(site.getData().core.url);
1439 }
1440
1441 protected File getDestination() {
1442 return Lifecycle.get().getHudsonWar();
1443 }
1444
1445 public String getName() {
1446 return "jenkins.war";
1447 }
1448
1449 protected void onSuccess() {
1450 status = new Success();
1451 }
1452
1453 @Override
1454 protected void replace(File dst, File src) throws IOException {
1455 Lifecycle.get().rewriteHudsonWar(src);
1456 }
1457 }
1458
1459 public final class HudsonDowngradeJob extends DownloadJob {
1460 public HudsonDowngradeJob(UpdateSite site, Authentication auth) {
1461 super(site, auth);
1462 }
1463
1464 protected URL getURL() throws MalformedURLException {
1465 return new URL(site.getData().core.url);
1466 }
1467
1468 protected File getDestination() {
1469 return Lifecycle.get().getHudsonWar();
1470 }
1471
1472 public String getName() {
1473 return "jenkins.war";
1474 }
1475 protected void onSuccess() {
1476 status = new Success();
1477 }
1478 @Override
1479 public void run() {
1480 try {
1481 LOGGER.info("Starting the downgrade of "+getName()+" on behalf of "+getUser().getName());
1482
1483 _run();
1484
1485 LOGGER.info("Downgrading successful: "+getName());
1486 status = new Success();
1487 onSuccess();
1488 } catch (Throwable e) {
1489 LOGGER.log(Level.SEVERE, "Failed to downgrade "+getName(),e);
1490 status = new Failure(e);
1491 error = e;
1492 }
1493 }
1494
1495 @Override
1496 protected void _run() throws IOException {
1497
1498 File backup = new File(Lifecycle.get().getHudsonWar() + ".bak");
1499 File dst = getDestination();
1500
1501 config.install(this, backup, dst);
1502 }
1503
1504 @Override
1505 protected void replace(File dst, File src) throws IOException {
1506 Lifecycle.get().rewriteHudsonWar(src);
1507 }
1508 }
1509
1510 public static final class PluginEntry implements Comparable<PluginEntry> {
1511 public Plugin plugin;
1512 public String category;
1513 private PluginEntry(Plugin p, String c) { plugin = p; category = c; }
1514
1515 public int compareTo(PluginEntry o) {
1516 int r = category.compareTo(o.category);
1517 if (r==0) r = plugin.name.compareToIgnoreCase(o.plugin.name);
1518 return r;
1519 }
1520 }
1521
1522 /**
1523 * Adds the update center data retriever to HTML.
1524 */
1525 @Extension
1526 public static class PageDecoratorImpl extends PageDecorator {
1527 }
1528
1529 /**
1530 * Initializes the update center.
1531 *
1532 * This has to wait until after all plugins load, to let custom UpdateCenterConfiguration take effect first.
1533 */
1534 @Initializer(after=PLUGINS_STARTED)
1535 public static void init(Jenkins h) throws IOException {
1536 h.getUpdateCenter().load();
1537 }
1538
1539 /**
1540 * Sequence number generator.
1541 */
1542 private static final AtomicInteger iota = new AtomicInteger();
1543
1544 private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName());
1545
1546 /**
1547 * @deprecated as of 1.333
1548 * Use {@link UpdateSite#neverUpdate}
1549 */
1550 public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never");
1551
1552 public static final XStream2 XSTREAM = new XStream2();
1553
1554 static {
1555 XSTREAM.alias("site",UpdateSite.class);
1556 XSTREAM.alias("sites",PersistedList.class);
1557 }
1558 }
1559