Merge remote-tracking branch 'origin/master' into prendota/plugin-manager-new-protocol
[idea/community.git] / platform / platform-impl / src / com / intellij / ide / plugins / PluginInstaller.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.ide.plugins;
3
4 import com.intellij.CommonBundle;
5 import com.intellij.ide.IdeBundle;
6 import com.intellij.ide.startup.StartupActionScriptManager;
7 import com.intellij.ide.util.PropertiesComponent;
8 import com.intellij.openapi.application.ApplicationManager;
9 import com.intellij.openapi.application.ApplicationNamesInfo;
10 import com.intellij.openapi.application.PathManager;
11 import com.intellij.openapi.application.ex.ApplicationInfoEx;
12 import com.intellij.openapi.diagnostic.Logger;
13 import com.intellij.openapi.extensions.PluginId;
14 import com.intellij.openapi.fileChooser.FileChooser;
15 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
16 import com.intellij.openapi.progress.ProgressIndicator;
17 import com.intellij.openapi.progress.ProgressManager;
18 import com.intellij.openapi.ui.Messages;
19 import com.intellij.openapi.ui.ex.MessagesEx;
20 import com.intellij.openapi.util.Comparing;
21 import com.intellij.openapi.util.Ref;
22 import com.intellij.openapi.util.io.FileUtil;
23 import com.intellij.openapi.util.io.FileUtilRt;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.openapi.vfs.VfsUtil;
26 import com.intellij.openapi.vfs.VfsUtilCore;
27 import com.intellij.openapi.vfs.VirtualFile;
28 import com.intellij.util.ArrayUtilRt;
29 import com.intellij.util.Consumer;
30 import com.intellij.util.io.Decompressor;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33
34 import javax.swing.*;
35 import java.awt.*;
36 import java.io.File;
37 import java.io.IOException;
38 import java.util.List;
39 import java.util.*;
40 import java.util.zip.ZipEntry;
41 import java.util.zip.ZipFile;
42
43 /**
44  * @author stathik
45  */
46 public final class PluginInstaller {
47   private static final Logger LOG = Logger.getInstance(PluginInstaller.class);
48
49   public static final String UNKNOWN_HOST_MARKER = "__unknown_repository__";
50
51   static final Object ourLock = new Object();
52   private static final String PLUGINS_PRESELECTION_PATH = "plugins.preselection.path";
53
54   private PluginInstaller() { }
55
56   public static boolean prepareToInstall(List<PluginNode> pluginsToInstall,
57                                          List<? extends IdeaPluginDescriptor> customOrAllPlugins,
58                                          boolean allowInstallWithoutRestart,
59                                          PluginManagerMain.PluginEnabler pluginEnabler,
60                                          Runnable onSuccess,
61                                          @NotNull ProgressIndicator indicator) {
62     //TODO: `PluginInstallOperation` expects only `customPlugins`, but it can take `allPlugins` too
63     PluginInstallOperation operation = new PluginInstallOperation(pluginsToInstall, customOrAllPlugins, pluginEnabler, indicator);
64     operation.setAllowInstallWithoutRestart(allowInstallWithoutRestart);
65     operation.run();
66     boolean success = operation.isSuccess();
67     if (success) {
68       ApplicationManager.getApplication().invokeLater(() -> {
69         if (allowInstallWithoutRestart) {
70           for (PendingDynamicPluginInstall install : operation.getPendingDynamicPluginInstalls()) {
71             installAndLoadDynamicPlugin(install.getFile(), null, install.getPluginDescriptor());
72           }
73         }
74         if (onSuccess != null) {
75           onSuccess.run();
76         }
77       });
78     }
79     return success;
80   }
81
82   /**
83    * @return true if restart is needed
84    */
85   public static boolean prepareToUninstall(@NotNull IdeaPluginDescriptor pluginDescriptor) throws IOException {
86     synchronized (ourLock) {
87       if (PluginManagerCore.isPluginInstalled(pluginDescriptor.getPluginId())) {
88         if (pluginDescriptor.isBundled()) {
89           LOG.error("Plugin is bundled: " + pluginDescriptor.getPluginId());
90         }
91         else {
92           boolean needRestart = !DynamicPlugins.allowLoadUnloadWithoutRestart((IdeaPluginDescriptorImpl)pluginDescriptor);
93           if (needRestart) {
94             uninstallAfterRestart(pluginDescriptor);
95           }
96
97           PluginStateManager.fireState(pluginDescriptor, false);
98           return needRestart;
99         }
100       }
101     }
102     return false;
103   }
104
105   private static void uninstallAfterRestart(IdeaPluginDescriptor pluginDescriptor) throws IOException {
106     StartupActionScriptManager.addActionCommand(new StartupActionScriptManager.DeleteCommand(pluginDescriptor.getPath()));
107   }
108
109   public static boolean uninstallDynamicPlugin(@Nullable JComponent parentComponent, IdeaPluginDescriptor pluginDescriptor, boolean isUpdate) {
110     boolean uninstalledWithoutRestart = parentComponent != null
111       ? DynamicPlugins.unloadPluginWithProgress(parentComponent, (IdeaPluginDescriptorImpl)pluginDescriptor, false, isUpdate)
112       : DynamicPlugins.unloadPlugin((IdeaPluginDescriptorImpl)pluginDescriptor, false, isUpdate);
113
114     if (uninstalledWithoutRestart) {
115       FileUtil.delete(pluginDescriptor.getPath());
116     }
117     else {
118       try {
119         uninstallAfterRestart(pluginDescriptor);
120       }
121       catch (IOException e) {
122         LOG.error(e);
123       }
124     }
125     return uninstalledWithoutRestart;
126   }
127
128   public static void installAfterRestart(@NotNull File sourceFile,
129                                          boolean deleteSourceFile,
130                                          @Nullable File existingPlugin,
131                                          @NotNull IdeaPluginDescriptor descriptor) throws IOException {
132     List<StartupActionScriptManager.ActionCommand> commands = new ArrayList<>();
133
134     if (existingPlugin != null) {
135       commands.add(new StartupActionScriptManager.DeleteCommand(existingPlugin));
136     }
137
138     String pluginsPath = PathManager.getPluginsPath();
139     if (sourceFile.getName().endsWith(".jar")) {
140       commands.add(new StartupActionScriptManager.CopyCommand(sourceFile, new File(pluginsPath, sourceFile.getName())));
141     }
142     else {
143       commands.add(new StartupActionScriptManager.DeleteCommand(new File(pluginsPath, rootEntryName(sourceFile))));  // drops stale directory
144       commands.add(new StartupActionScriptManager.UnzipCommand(sourceFile, new File(pluginsPath)));
145     }
146
147     if (deleteSourceFile) {
148       commands.add(new StartupActionScriptManager.DeleteCommand(sourceFile));
149     }
150
151     StartupActionScriptManager.addActionCommands(commands);
152
153     PluginStateManager.fireState(descriptor, true);
154   }
155
156   @Nullable
157   public static File installWithoutRestart(File sourceFile, IdeaPluginDescriptorImpl descriptor, Component parent) {
158     Ref<IOException> ref = new Ref<>();
159     Ref<File> refTarget = new Ref<>();
160     ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
161       String pluginsPath = PathManager.getPluginsPath();
162       try {
163         File target;
164         if (sourceFile.getName().endsWith(".jar")) {
165           target = new File(pluginsPath, sourceFile.getName());
166           FileUtilRt.copy(sourceFile, target);
167         }
168         else {
169           target = new File(pluginsPath, rootEntryName(sourceFile));
170           FileUtil.delete(target);
171           new Decompressor.Zip(sourceFile).extract(new File(pluginsPath));
172         }
173         refTarget.set(target);
174       }
175       catch (IOException e) {
176         ref.set(e);
177       }
178     }, IdeBundle.message("progress.title.installing.plugin"), false, null, parent instanceof JComponent ? (JComponent)parent : null);
179     IOException exception = ref.get();
180     if (exception != null) {
181       Messages.showErrorDialog(parent, IdeBundle.message("message.plugin.installation.failed.0", exception.getMessage()));
182     }
183     PluginStateManager.fireState(descriptor, true);
184     return exception != null ? null : refTarget.get();
185   }
186
187   private static String rootEntryName(File zip) throws IOException {
188     try (ZipFile zipFile = new ZipFile(zip)) {
189       Enumeration<? extends ZipEntry> entries = zipFile.entries();
190       while (entries.hasMoreElements()) {
191         ZipEntry zipEntry = entries.nextElement();
192         // we do not necessarily get a separate entry for the subdirectory when the file
193         // in the ZIP archive is placed in a subdirectory, so we need to check if the slash
194         // is found anywhere in the path
195         String name = zipEntry.getName();
196         int i = name.indexOf('/');
197         if (i > 0) return name.substring(0, i);
198       }
199     }
200
201     throw new IOException("Corrupted archive (no file entries): " + zip);
202   }
203
204   public static void addStateListener(@NotNull PluginStateListener listener) {
205     PluginStateManager.addStateListener(listener);
206   }
207
208   public static boolean install(@NotNull InstalledPluginsTableModel model,
209                                 @NotNull File file,
210                                 @NotNull Consumer<? super PluginInstallCallbackData> callback,
211                                 @Nullable Component parent) {
212     try {
213       IdeaPluginDescriptorImpl pluginDescriptor = PluginManager.loadDescriptorFromArtifact(file.toPath(), null);
214       if (pluginDescriptor == null) {
215         MessagesEx.showErrorDialog(parent, "Fail to load plugin descriptor from file " + file.getName(), CommonBundle.getErrorTitle());
216         return false;
217       }
218
219       InstalledPluginsState ourState = InstalledPluginsState.getInstance();
220
221       if (ourState.wasInstalled(pluginDescriptor.getPluginId())) {
222         String message = "Plugin '" + pluginDescriptor.getName() + "' was already installed";
223         MessagesEx.showWarningDialog(parent, message, "Install Plugin");
224         return false;
225       }
226
227       String incompatibleMessage = PluginManagerCore.getIncompatibleMessage(PluginManagerCore.getBuildNumber(),
228                                                                             pluginDescriptor.getSinceBuild(),
229                                                                             pluginDescriptor.getUntilBuild());
230       if (incompatibleMessage != null || PluginManagerCore.isBrokenPlugin(pluginDescriptor)) {
231         StringBuilder builder = new StringBuilder().append("Plugin '").append(pluginDescriptor.getName()).append("'");
232         if (pluginDescriptor.getVersion() != null) {
233           builder.append(" version ").append(pluginDescriptor.getVersion());
234         }
235         builder.append(" is incompatible with this installation");
236         if (incompatibleMessage != null) {
237           builder.append(": ").append(incompatibleMessage);
238         }
239         MessagesEx.showErrorDialog(parent, builder.toString(), CommonBundle.getErrorTitle());
240         return false;
241       }
242
243       IdeaPluginDescriptor installedPlugin = PluginManagerCore.getPlugin(pluginDescriptor.getPluginId());
244       if (installedPlugin != null && ApplicationInfoEx.getInstanceEx().isEssentialPlugin(installedPlugin.getPluginId())) {
245         String message = "Plugin '" + pluginDescriptor.getName() + "' is a core part of " + ApplicationNamesInfo.getInstance().getFullProductName()
246                          + ". In order to update it to a newer version you should update the IDE.";
247         MessagesEx.showErrorDialog(parent, message, CommonBundle.getErrorTitle());
248         return false;
249       }
250
251       File oldFile = null;
252       if (installedPlugin != null && !installedPlugin.isBundled()) {
253         oldFile = installedPlugin.getPath();
254       }
255
256       boolean installWithoutRestart = oldFile == null && DynamicPlugins.allowLoadUnloadWithoutRestart(pluginDescriptor);
257       if (!installWithoutRestart) {
258         installAfterRestart(file, false, oldFile, pluginDescriptor);
259       }
260
261       ourState.onPluginInstall(pluginDescriptor, installedPlugin != null, !installWithoutRestart);
262       checkInstalledPluginDependencies(model, pluginDescriptor, parent);
263       callback.consume(new PluginInstallCallbackData(file, pluginDescriptor, !installWithoutRestart));
264       return true;
265     }
266     catch (IOException ex) {
267       MessagesEx.showErrorDialog(parent, ex.getMessage(), CommonBundle.getErrorTitle());
268     }
269     return false;
270   }
271
272   @Nullable
273   public static IdeaPluginDescriptorImpl installAndLoadDynamicPlugin(@NotNull File file,
274                                                                      @Nullable Component parent,
275                                                                      IdeaPluginDescriptorImpl pluginDescriptor) {
276     File targetFile = installWithoutRestart(file, pluginDescriptor, parent);
277     if (targetFile != null) {
278       IdeaPluginDescriptorImpl targetDescriptor = PluginManager.loadDescriptor(targetFile.toPath(), PluginManagerCore.PLUGIN_XML);
279       if (targetDescriptor != null) {
280         DynamicPlugins.loadPlugin(targetDescriptor, false);
281         return targetDescriptor;
282       }
283     }
284     return null;
285   }
286
287   private static void checkInstalledPluginDependencies(@NotNull InstalledPluginsTableModel model,
288                                                        @NotNull IdeaPluginDescriptorImpl pluginDescriptor,
289                                                        @Nullable Component parent) {
290     final Set<PluginId> notInstalled = new HashSet<>();
291     final Set<PluginId> disabledIds = new HashSet<>();
292     final PluginId[] dependentPluginIds = pluginDescriptor.getDependentPluginIds();
293     final PluginId[] optionalDependentPluginIds = pluginDescriptor.getOptionalDependentPluginIds();
294     for (PluginId id : dependentPluginIds) {
295       if (ArrayUtilRt.find(optionalDependentPluginIds, id) > -1) continue;
296       final boolean disabled = model.isDisabled(id);
297       final boolean enabled = model.isEnabled(id);
298       if (!enabled && !disabled && !PluginManagerCore.isModuleDependency(id)) {
299         notInstalled.add(id);
300       }
301       else if (disabled) {
302         disabledIds.add(id);
303       }
304     }
305     if (!notInstalled.isEmpty()) {
306       String deps = StringUtil.join(notInstalled, PluginId::toString, ", ");
307       String message =
308         "Plugin " + pluginDescriptor.getName() + " depends on unknown plugin" + (notInstalled.size() > 1 ? "s " : " ") + deps;
309       MessagesEx.showWarningDialog(parent, message, "Install Plugin");
310     }
311     if (!disabledIds.isEmpty()) {
312       final Set<IdeaPluginDescriptor> dependencies = new HashSet<>();
313       for (IdeaPluginDescriptor ideaPluginDescriptor : model.getAllPlugins()) {
314         if (disabledIds.contains(ideaPluginDescriptor.getPluginId())) {
315           dependencies.add(ideaPluginDescriptor);
316         }
317       }
318       String part = "disabled plugin" + (dependencies.size() > 1 ? "s " : " ");
319       String deps = StringUtil.join(dependencies, IdeaPluginDescriptor::getName, ", ");
320       String message = "Plugin " + pluginDescriptor.getName() + " depends on " + part + deps + ". Enable " + part.trim() + "?";
321       if (Messages
322             .showOkCancelDialog(message, IdeBundle.message("dialog.title.install.plugin"), IdeBundle.message("button.install"), CommonBundle.getCancelButtonText(), Messages.getWarningIcon()) ==
323           Messages.OK) {
324         model.enableRows(dependencies.toArray(new IdeaPluginDescriptor[0]), Boolean.TRUE);
325       }
326     }
327   }
328
329   static void chooseAndInstall(@NotNull final InstalledPluginsTableModel model,
330                                @Nullable final Component parent, @NotNull final Consumer<? super PluginInstallCallbackData> callback) {
331     final FileChooserDescriptor descriptor = new FileChooserDescriptor(false, false, true, true, false, false) {
332       @Override
333       public boolean isFileSelectable(VirtualFile file) {
334         final String extension = file.getExtension();
335         return Comparing.strEqual(extension, "jar") || Comparing.strEqual(extension, "zip");
336       }
337     };
338     descriptor.setTitle(IdeBundle.message("chooser.title.plugin.file"));
339     descriptor.setDescription(IdeBundle.message("chooser.description.jar.and.zip.archives.are.accepted"));
340     final String oldPath = PropertiesComponent.getInstance().getValue(PLUGINS_PRESELECTION_PATH);
341     final VirtualFile toSelect =
342       oldPath == null ? null : VfsUtil.findFileByIoFile(new File(FileUtil.toSystemDependentName(oldPath)), false);
343     FileChooser.chooseFile(descriptor, null, parent, toSelect, virtualFile -> {
344       File file = VfsUtilCore.virtualToIoFile(virtualFile);
345       PropertiesComponent.getInstance().setValue(PLUGINS_PRESELECTION_PATH, FileUtil.toSystemIndependentName(file.getParent()));
346       install(model, file, callback, parent);
347     });
348   }
349 }