a2833e6e56271114bcff8289d256b6c0ce8f687c
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / updateSettings / impl / UpdateChecker.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.updateSettings.impl;
17
18 import com.intellij.ide.IdeBundle;
19 import com.intellij.ide.plugins.*;
20 import com.intellij.ide.reporter.ConnectionException;
21 import com.intellij.ide.util.PropertiesComponent;
22 import com.intellij.notification.Notification;
23 import com.intellij.notification.NotificationType;
24 import com.intellij.notification.Notifications;
25 import com.intellij.openapi.application.ApplicationInfo;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.application.PathManager;
28 import com.intellij.openapi.application.ex.ApplicationInfoEx;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.extensions.PluginId;
31 import com.intellij.openapi.progress.ProcessCanceledException;
32 import com.intellij.openapi.progress.ProgressIndicator;
33 import com.intellij.openapi.progress.ProgressManager;
34 import com.intellij.openapi.ui.Messages;
35 import com.intellij.openapi.util.BuildNumber;
36 import com.intellij.openapi.util.JDOMUtil;
37 import com.intellij.openapi.util.SystemInfo;
38 import com.intellij.openapi.util.io.FileUtil;
39 import com.intellij.openapi.util.text.StringUtil;
40 import com.intellij.openapi.vfs.VirtualFile;
41 import com.intellij.util.PlatformUtils;
42 import com.intellij.util.containers.ContainerUtil;
43 import com.intellij.util.io.UrlConnectionUtil;
44 import com.intellij.util.net.HttpConfigurable;
45 import com.intellij.util.text.DateFormatUtil;
46 import com.intellij.util.ui.UIUtil;
47 import org.jdom.Document;
48 import org.jdom.Element;
49 import org.jdom.JDOMException;
50 import org.jetbrains.annotations.NonNls;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53
54 import javax.swing.*;
55 import java.io.*;
56 import java.net.HttpURLConnection;
57 import java.net.URL;
58 import java.net.URLConnection;
59 import java.util.*;
60 import java.util.concurrent.Future;
61 import java.util.concurrent.TimeUnit;
62 import java.util.concurrent.TimeoutException;
63
64 /**
65  * XML sample:
66  * <pre>{@code
67  * <idea>
68  *   <build>456</build>
69  *   <version>4.5.2</version>
70  *   <title>New Intellij IDEA Version</title>
71  *   <message>
72  *     New version of IntelliJ IDEA is available.
73  *     Please visit http://www.intellij.com/ for more info.
74  *   </message>
75  * </idea>
76  * }</pre>
77  *
78  * @author mike
79  * Date: Oct 31, 2002
80  */
81 public final class UpdateChecker {
82   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.updateSettings.impl.UpdateChecker");
83
84   public static String ADDITIONAL_REQUEST_OPTIONS = "";
85   @NonNls private static final String INSTALLATION_UID = "installation.uid";
86
87   private UpdateChecker() {
88   }
89
90   public static void showConnectionErrorDialog() {
91     Messages.showErrorDialog(IdeBundle.message("error.checkforupdates.connection.failed"),
92                              IdeBundle.message("title.connection.error"));
93   }
94
95   public static enum DownloadPatchResult {
96     SUCCESS, FAILED, CANCELED
97   }
98
99   private static boolean myVeryFirstOpening = true;
100
101
102   @NonNls
103   private static final String DISABLED_UPDATE = "disabled_update.txt";
104   private static TreeSet<String> ourDisabledToUpdatePlugins;
105
106   private static class StringHolder {
107     private static final String UPDATE_URL = ApplicationInfoEx.getInstanceEx().getUpdateUrls().getCheckingUrl();
108     private static final String PATCHES_URL = ApplicationInfoEx.getInstanceEx().getUpdateUrls().getPatchesUrl();
109     private StringHolder() { }
110   }
111
112   private static String getUpdateUrl() {
113     String url = System.getProperty("idea.updates.url");
114     if (url != null) {
115       return url;
116     }
117     return StringHolder.UPDATE_URL;
118   }
119
120   private static String getPatchesUrl() {
121     String url = System.getProperty("idea.patches.url");
122     if (url != null) {
123       return url;
124     }
125     return StringHolder.PATCHES_URL;
126   }
127
128   public static boolean isMyVeryFirstOpening() {
129     return myVeryFirstOpening;
130   }
131
132   public static void setMyVeryFirstOpening(final boolean myVeryFirstProjectOpening) {
133     myVeryFirstOpening = myVeryFirstProjectOpening;
134   }
135
136   public static boolean checkNeeded() {
137     final UpdateSettings settings = UpdateSettings.getInstance();
138     if (settings == null || getUpdateUrl() == null) return false;
139
140     final long timeDelta = System.currentTimeMillis() - settings.LAST_TIME_CHECKED;
141     if (Math.abs(timeDelta) < DateFormatUtil.DAY) return false;
142
143     return settings.CHECK_NEEDED;
144   }
145
146   public static List<PluginDownloader> updatePlugins(final boolean showErrorDialog,
147                                                      final @Nullable PluginHostsConfigurable hostsConfigurable) {
148     final List<PluginDownloader> downloaded = new ArrayList<PluginDownloader>();
149     final Set<String> failed = new HashSet<String>();
150     for (String host : getPluginHosts(hostsConfigurable)) {
151       try {
152         checkPluginsHost(host, downloaded);
153       }
154       catch (Exception e) {
155         LOG.info(e);
156         failed.add(host);
157       }
158     }
159
160     final Map<String, IdeaPluginDescriptor> toUpdate = new HashMap<String, IdeaPluginDescriptor>();
161     final IdeaPluginDescriptor[] installedPlugins = PluginManager.getPlugins();
162     for (IdeaPluginDescriptor installedPlugin : installedPlugins) {
163       if (!installedPlugin.isBundled()) {
164         toUpdate.put(installedPlugin.getPluginId().getIdString(), installedPlugin);
165       }
166     }
167     final PluginManagerUISettings updateSettings = PluginManagerUISettings.getInstance();
168     updateSettings.myOutdatedPlugins.clear();
169     if (!toUpdate.isEmpty()) {
170       try {
171         final ArrayList<IdeaPluginDescriptor> process = RepositoryHelper.process(null);
172         for (IdeaPluginDescriptor loadedPlugin : process) {
173           final String idString = loadedPlugin.getPluginId().getIdString();
174           final IdeaPluginDescriptor installedPlugin = toUpdate.get(idString);
175           if (installedPlugin != null) {
176             if (StringUtil.compareVersionNumbers(loadedPlugin.getVersion(), installedPlugin.getVersion()) > 0) {
177               updateSettings.myOutdatedPlugins.add(idString);
178               if (((IdeaPluginDescriptorImpl)installedPlugin).isEnabled()) {
179                 final PluginDownloader downloader = PluginDownloader.createDownloader(loadedPlugin);
180                 if (downloader.prepareToInstall()) {
181                   downloaded.add(downloader);
182                 }
183               }
184             }
185           }
186         }
187       }
188       catch (Exception e) {
189         showErrorMessage(showErrorDialog, e.getMessage());
190       }
191     }
192
193     if (!failed.isEmpty()) {
194       showErrorMessage(showErrorDialog, IdeBundle.message("connection.failed.message", StringUtil.join(failed, ",")));
195     }
196     return downloaded.isEmpty() ? null : downloaded;
197   }
198
199   private static void showErrorMessage(boolean showErrorDialog, final String failedMessage) {
200     if (showErrorDialog) {
201       UIUtil.invokeLaterIfNeeded(new Runnable() {
202         @Override
203         public void run() {
204           Messages.showErrorDialog(failedMessage, IdeBundle.message("title.connection.error"));
205         }
206       });
207     }
208     else {
209       LOG.info(failedMessage);
210     }
211   }
212
213   private static List<String> getPluginHosts(@Nullable PluginHostsConfigurable hostsConfigurable) {
214     final ArrayList<String> hosts = new ArrayList<String>();
215     if (hostsConfigurable != null) {
216       hosts.addAll(hostsConfigurable.getPluginsHosts());
217     }
218     else {
219       hosts.addAll(UpdateSettings.getInstance().myPluginHosts);
220     }
221     final String pluginHosts = System.getProperty("idea.plugin.hosts");
222     if (pluginHosts != null) {
223       ContainerUtil.addAll(hosts, pluginHosts.split(";"));
224     }
225     return hosts;
226   }
227
228   public static boolean checkPluginsHost(final String host, final List<PluginDownloader> downloaded) throws Exception {
229     return checkPluginsHost(host, downloaded, true);
230   }
231
232   public static boolean checkPluginsHost(final String host,
233                                          final List<PluginDownloader> downloaded,
234                                          final boolean collectToUpdate) throws Exception {
235     InputStream inputStream = loadVersionInfo(host);
236     if (inputStream == null) return false;
237     final Document document;
238     try {
239       document = JDOMUtil.loadDocument(inputStream);
240     }
241     catch (JDOMException e) {
242       return false;
243     }
244
245     inputStream = loadVersionInfo(host);
246     if (inputStream == null) return false;
247     final ArrayList<IdeaPluginDescriptor> descriptors = RepositoryHelper.loadPluginsFromDescription(inputStream);
248     for (IdeaPluginDescriptor descriptor : descriptors) {
249       downloaded.add(PluginDownloader.createDownloader(descriptor));
250     }
251
252     boolean success = true;
253     for (Object plugin : document.getRootElement().getChildren("plugin")) {
254       final Element pluginElement = (Element)plugin;
255       final String pluginId = pluginElement.getAttributeValue("id");
256       final String pluginUrl = pluginElement.getAttributeValue("url");
257       final String pluginVersion = pluginElement.getAttributeValue("version");
258       final Element descriptionElement = pluginElement.getChild("description");
259       final String description;
260       if (descriptionElement != null) {
261         description = descriptionElement.getText();
262       } else {
263         description = null;
264       }
265       
266       final List<PluginId> dependsPlugins = new ArrayList<PluginId>();
267       final List depends = pluginElement.getChildren("depends");
268       for (Object depend : depends) {
269         dependsPlugins.add(PluginId.getId(((Element)depend).getText()));
270       }
271
272       if (pluginId == null) {
273         LOG.info("plugin id should not be null");
274         success = false;
275         continue;
276       }
277
278       if (pluginUrl == null) {
279         LOG.info("plugin url should not be null");
280         success = false;
281         continue;
282       }
283
284       final VirtualFile pluginFile = PluginDownloader.findPluginFile(pluginUrl, host);
285       if (pluginFile == null) continue;
286
287       if (collectToUpdate) {
288         final String finalPluginUrl = pluginFile.getUrl();
289         final Runnable updatePluginRunnable = new Runnable() {
290           public void run() {
291             try {
292               final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
293               if (progressIndicator != null) {
294                 progressIndicator.setText(finalPluginUrl);
295               }
296               final PluginDownloader uploader = new PluginDownloader(pluginId, finalPluginUrl, pluginVersion);
297               if (uploader.prepareToInstall()) {
298                 downloaded.add(uploader);
299               }
300             }
301             catch (IOException e) {
302               LOG.info(e);
303             }
304           }
305         };
306         if (ApplicationManager.getApplication().isDispatchThread()) {
307           ProgressManager.getInstance().runProcessWithProgressSynchronously(updatePluginRunnable, IdeBundle.message("update.uploading.plugin.progress.title"), true, null);
308         }
309         else {
310           updatePluginRunnable.run();
311         }
312       } else {
313         final PluginDownloader downloader = new PluginDownloader(pluginId, pluginUrl, pluginVersion);
314         downloader.setDescription(description);
315         downloader.setDepends(dependsPlugins);
316         downloaded.add(downloader);
317       }
318     }
319     return success;
320   }
321
322   @NotNull
323   public static CheckForUpdateResult doCheckForUpdates(final UpdateSettings settings) {
324     ApplicationInfo appInfo = ApplicationInfo.getInstance();
325     BuildNumber currentBuild = appInfo.getBuild();
326     int majorVersion = Integer.parseInt(appInfo.getMajorVersion());
327     final UpdatesXmlLoader loader = new UpdatesXmlLoader(getUpdateUrl(), getInstallationUID(), null);
328     final UpdatesInfo info;
329     try {
330       info = loader.loadUpdatesInfo();
331       if (info == null) {
332         return new CheckForUpdateResult(UpdateStrategy.State.NOTHING_LOADED);
333       }
334     }
335     catch (ConnectionException e) {
336       return new CheckForUpdateResult(UpdateStrategy.State.CONNECTION_ERROR, e);
337     }
338
339     UpdateStrategy strategy = new UpdateStrategy(majorVersion, currentBuild, info, settings);
340     return strategy.checkForUpdates();
341   }
342
343
344   @NotNull
345   public static CheckForUpdateResult checkForUpdates() {
346     return checkForUpdates(false);
347   }
348
349   @NotNull
350   public static CheckForUpdateResult checkForUpdates(final boolean disregardIgnoredBuilds) {
351     if (LOG.isDebugEnabled()) {
352       LOG.debug("enter: auto checkForUpdates()");
353     }
354
355     UserUpdateSettings settings = UpdateSettings.getInstance();
356     if (disregardIgnoredBuilds) {
357       settings = new UserUpdateSettings() {
358         @NotNull
359         @Override
360         public List<String> getKnownChannelsIds() {
361           return UpdateSettings.getInstance().getKnownChannelsIds();
362         }
363
364         @Override
365         public List<String> getIgnoredBuildNumbers() {
366           return Collections.emptyList();
367         }
368
369         @Override
370         public void setKnownChannelIds(List<String> ids) {
371           UpdateSettings.getInstance().setKnownChannelIds(ids);
372         }
373
374         @NotNull
375         @Override
376         public ChannelStatus getSelectedChannelStatus() {
377           return UpdateSettings.getInstance().getSelectedChannelStatus();
378         }
379       };
380     }
381
382     final CheckForUpdateResult result = doCheckForUpdates(UpdateSettings.getInstance());
383
384     if (result.getState() == UpdateStrategy.State.LOADED) {
385       UpdateSettings.getInstance().LAST_TIME_CHECKED = System.currentTimeMillis();
386       settings.setKnownChannelIds(result.getAllChannelsIds());
387     }
388
389     return result;
390   }
391
392   public static void showUpdateResult(CheckForUpdateResult checkForUpdateResult,
393                                       List<PluginDownloader> updatedPlugins,
394                                       boolean showConfirmation,
395                                       boolean enableLink,
396                                       final boolean alwaysShowResults) {
397     UpdateChannel channelToPropose = checkForUpdateResult.getChannelToPropose();
398     if (channelToPropose != null && channelToPropose.getLatestBuild() != null) {
399       NewChannelDialog dialog = new NewChannelDialog(channelToPropose);
400       dialog.setModal(alwaysShowResults);
401       dialog.show();
402     }
403     else if (checkForUpdateResult.hasNewBuildInSelectedChannel()) {
404       UpdateInfoDialog dialog = new UpdateInfoDialog(true, checkForUpdateResult.getUpdatedChannel(), updatedPlugins, enableLink);
405       dialog.setModal(alwaysShowResults);
406       dialog.show();
407     }
408     else if (updatedPlugins != null || alwaysShowResults) {
409       NoUpdatesDialog dialog = new NoUpdatesDialog(true, updatedPlugins, enableLink);
410       dialog.setShowConfirmation(showConfirmation);
411       dialog.show();
412     }
413 }
414
415   private static InputStream loadVersionInfo(final String url) throws Exception {
416     if (LOG.isDebugEnabled()) {
417       LOG.debug("enter: loadVersionInfo(UPDATE_URL='" + url + "' )");
418     }
419     final InputStream[] inputStreams = new InputStream[]{null};
420     final Exception[] exception = new Exception[]{null};
421     Future<?> downloadThreadFuture = ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
422       public void run() {
423         try {
424           HttpConfigurable.getInstance().prepareURL(url);
425
426           String uid = getInstallationUID();
427
428           final URL requestUrl =
429             new URL(url + "?build=" + ApplicationInfo.getInstance().getBuild().asString() + "&uid=" + uid + ADDITIONAL_REQUEST_OPTIONS);
430           inputStreams[0] = requestUrl.openStream();
431         }
432         catch (IOException e) {
433           exception[0] = e;
434         }
435       }
436     });
437
438     try {
439       downloadThreadFuture.get(5, TimeUnit.SECONDS);
440     }
441     catch (TimeoutException e) {
442       // ignore
443     }
444
445     if (!downloadThreadFuture.isDone()) {
446       downloadThreadFuture.cancel(true);
447       throw new ConnectionException(IdeBundle.message("updates.timeout.error"));
448     }
449
450     if (exception[0] != null) throw exception[0];
451     return inputStreams[0];
452   }
453
454   public static String getInstallationUID() {
455     final PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
456     String uid = "";
457     if (!propertiesComponent.isValueSet(INSTALLATION_UID)) {
458       try {
459         uid = UUID.randomUUID().toString();
460       }
461       catch (Exception ignored) {
462       }
463       catch (InternalError ignored) {
464       }
465       propertiesComponent.setValue(INSTALLATION_UID, uid);
466     }
467     else {
468       uid = propertiesComponent.getValue(INSTALLATION_UID);
469     }
470     return uid;
471   }
472
473   public static boolean install(List<PluginDownloader> downloaders) {
474     boolean installed = false;
475     for (PluginDownloader downloader : downloaders) {
476       if (getDisabledToUpdatePlugins().contains(downloader.getPluginId())) continue;
477       try {
478         downloader.install();
479         installed = true;
480       }
481       catch (IOException e) {
482         LOG.info(e);
483       }
484     }
485     return installed;
486   }
487
488
489   public static DownloadPatchResult downloadAndInstallPatch(final BuildInfo newVersion) {
490     final DownloadPatchResult[] result = new DownloadPatchResult[]{DownloadPatchResult.CANCELED};
491
492     if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
493       public void run() {
494         try {
495           doDownloadAndInstallPatch(newVersion, ProgressManager.getInstance().getProgressIndicator());
496           result[0] = DownloadPatchResult.SUCCESS;
497         }
498         catch (final IOException e) {
499           LOG.info(e);
500           result[0] = DownloadPatchResult.FAILED;
501
502           SwingUtilities.invokeLater(new Runnable() {
503             public void run() {
504               Notifications.Bus.notify(new Notification("Updater",
505                                                         "Failed to download patch file",
506                                                         e.getMessage(),
507                                                         NotificationType.ERROR));
508             }
509           });
510         }
511       }
512     }, IdeBundle.message("update.downloading.patch.progress.title"), true, null)) {
513       return DownloadPatchResult.CANCELED;
514     }
515
516     return result[0];
517   }
518
519   private static void doDownloadAndInstallPatch(BuildInfo newVersion, ProgressIndicator i) throws IOException {
520     PatchInfo patch = newVersion.findPatchForCurrentBuild();
521     if (patch == null) throw new IOException("No patch is available for current version");
522
523     String productCode = ApplicationInfo.getInstance().getBuild().getProductCode();
524
525     String osSuffix = "";
526     if (SystemInfo.isWindows) {
527       osSuffix = "-win";
528     }
529     else if (SystemInfo.isMac) {
530       osSuffix = "-mac";
531     }
532     else if (SystemInfo.isUnix) osSuffix = "-unix";
533
534     String fromBuildNumber = patch.getFromBuild().asStringWithoutProductCode();
535     String toBuildNumber = newVersion.getNumber().asStringWithoutProductCode();
536     String fileName = productCode + "-" + fromBuildNumber + "-" + toBuildNumber + "-patch" + osSuffix + ".jar";
537     URLConnection connection = null;
538     InputStream in = null;
539     OutputStream out = null;
540
541     String platform = PlatformUtils.getPlatformPrefix();
542     String patchFileName = ("jetbrains.patch.jar." + platform).toLowerCase();
543     File patchFile = new File(FileUtil.getTempDirectory(), patchFileName);
544
545     try {
546       connection = new URL(new URL(getPatchesUrl()), fileName).openConnection();
547       in = UrlConnectionUtil.getConnectionInputStreamWithException(connection, i);
548       out = new BufferedOutputStream(new FileOutputStream(patchFile));
549
550       i.setIndeterminate(false);
551
552       byte[] buffer = new byte[10 * 1024];
553       int total = connection.getContentLength();
554       int count;
555       int read = 0;
556
557       while ((count = in.read(buffer)) > 0) {
558         i.checkCanceled();
559         out.write(buffer, 0, count);
560         read += count;
561         i.setFraction(((double)read) / total);
562         i.setText2((read / 1024) + "/" + (total / 1024) + " KB");
563       }
564     }
565     catch (IOException e) {
566       patchFile.delete();
567       throw e;
568     }
569     catch (ProcessCanceledException e) {
570       patchFile.delete();
571       throw e;
572     }
573     catch (Throwable e) {
574       patchFile.delete();
575       throw new RuntimeException(e);
576     }
577     finally {
578       if (out != null) out.close();
579       if (in != null) in.close();
580       if (connection instanceof HttpURLConnection) ((HttpURLConnection)connection).disconnect();
581     }
582   }
583
584   public static Set<String> getDisabledToUpdatePlugins() {
585     if (ourDisabledToUpdatePlugins == null) {
586       ourDisabledToUpdatePlugins = new TreeSet<String>();
587       if (!ApplicationManager.getApplication().isUnitTestMode()) {
588         try {
589           final File file = new File(PathManager.getConfigPath(), DISABLED_UPDATE);
590           if (file.isFile()) {
591             final String[] ids = FileUtil.loadFile(file).split("[\\s]");
592             for (String id : ids) {
593               if (id != null && id.trim().length() > 0) {
594                 ourDisabledToUpdatePlugins.add(id.trim());
595               }
596             }
597           }
598         }
599         catch (IOException e) {
600           LOG.error(e);
601         }
602       }
603     }
604     return ourDisabledToUpdatePlugins;
605   }
606
607   public static void saveDisabledToUpdatePlugins() {
608     try {
609       File plugins = new File(PathManager.getConfigPath(), DISABLED_UPDATE);
610       FileUtil.ensureCanCreateFile(plugins);
611
612       PrintWriter printWriter = null;
613       try {
614         printWriter = new PrintWriter(new BufferedWriter(new FileWriter(plugins)));
615         for (String id : getDisabledToUpdatePlugins()) {
616           printWriter.println(id);
617         }
618         printWriter.flush();
619       }
620       finally {
621         if (printWriter != null) {
622           printWriter.close();
623         }
624       }
625     }
626     catch (IOException e) {
627       LOG.error(e);
628     }
629   }
630 }