PY-19923 Don't log stacktrace of ExecutionException so as not to pollute log files
[idea/community.git] / python / src / com / jetbrains / python / sdk / PythonSdkUpdater.java
1 /*
2  * Copyright 2000-2016 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.jetbrains.python.sdk;
17
18 import com.google.common.collect.ImmutableList;
19 import com.google.common.collect.Lists;
20 import com.google.common.collect.Sets;
21 import com.intellij.execution.ExecutionException;
22 import com.intellij.openapi.application.Application;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.application.ModalityState;
25 import com.intellij.openapi.application.PathManager;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.module.Module;
28 import com.intellij.openapi.module.ModuleManager;
29 import com.intellij.openapi.progress.ProgressIndicator;
30 import com.intellij.openapi.progress.ProgressManager;
31 import com.intellij.openapi.progress.Task;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.projectRoots.Sdk;
34 import com.intellij.openapi.projectRoots.SdkAdditionalData;
35 import com.intellij.openapi.projectRoots.SdkModificator;
36 import com.intellij.openapi.roots.OrderRootType;
37 import com.intellij.openapi.startup.StartupActivity;
38 import com.intellij.openapi.ui.Messages;
39 import com.intellij.openapi.util.io.FileUtil;
40 import com.intellij.openapi.util.io.FileUtilRt;
41 import com.intellij.openapi.vfs.StandardFileSystems;
42 import com.intellij.openapi.vfs.VirtualFile;
43 import com.intellij.util.PathMappingSettings;
44 import com.intellij.util.concurrency.BlockingSet;
45 import com.intellij.util.concurrency.EdtExecutorService;
46 import com.jetbrains.python.PyBundle;
47 import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil;
48 import com.jetbrains.python.packaging.PyPackageManager;
49 import com.jetbrains.python.psi.PyUtil;
50 import com.jetbrains.python.remote.PyCredentialsContribution;
51 import com.jetbrains.python.remote.PyRemoteSdkAdditionalDataBase;
52 import com.jetbrains.python.sdk.skeletons.PySkeletonRefresher;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
55
56 import java.awt.*;
57 import java.io.File;
58 import java.util.*;
59 import java.util.List;
60 import java.util.concurrent.TimeUnit;
61
62 /**
63  * Refreshes all project's Python SDKs.
64  *
65  * @author vlan
66  * @author yole
67  */
68 public class PythonSdkUpdater implements StartupActivity {
69   private static final Logger LOG = Logger.getInstance("#com.jetbrains.python.sdk.PythonSdkUpdater");
70   public static final int INITIAL_ACTIVITY_DELAY = 7000;
71
72   private static final Object ourLock = new Object();
73   private static final Set<String> ourScheduledToRefresh = Sets.newHashSet();
74   private static final BlockingSet<String> ourUnderRefresh = new BlockingSet<String>();
75
76   /**
77    * Refreshes the SDKs of the modules for the open project after some delay.
78    */
79   @Override
80   public void runActivity(@NotNull final Project project) {
81     final Application application = ApplicationManager.getApplication();
82     if (application.isUnitTestMode()) {
83       return;
84     }
85     EdtExecutorService.getScheduledExecutorInstance().schedule(() -> ProgressManager.getInstance().run(new Task.Backgroundable(project, "Updating Python Paths", false) {
86       @Override
87       public void run(@NotNull ProgressIndicator indicator) {
88         final Project project = getProject();
89         if (project.isDisposed()) {
90           return;
91         }
92         for (final Sdk sdk : getPythonSdks(project)) {
93           update(sdk, null, project, null);
94         }
95       }
96     }), INITIAL_ACTIVITY_DELAY, TimeUnit.MILLISECONDS);
97   }
98
99   /**
100    * Updates the paths of an SDK and regenerates its skeletons as a background task.
101    *
102    * May be invoked from any thread. May freeze the current thread while evaluating sys.path.
103    *
104    * For a local SDK it commits all the SDK paths and runs a background task for updating skeletons. For a remote SDK it runs a background
105    * task for updating skeletons that saves path mappings in the additional SDK data and then commits all the SDK paths.
106    *
107    * The commit of the changes in the SDK happens in the AWT thread while the current thread is waiting the result.
108    *
109    * @param sdkModificator if null then it tries to get an SDK modifier from the SDK table, falling back to the modifier of the SDK
110    *                       passed as an argument accessed from the AWT thread
111    * @return false if there was an immediate problem updating the SDK. Other problems are reported as log entries and balloons.
112    */
113   public static boolean update(@NotNull Sdk sdk, @Nullable SdkModificator sdkModificator, @Nullable final Project project,
114                                @Nullable final Component ownerComponent) {
115     final String homePath = sdk.getHomePath();
116     synchronized (ourLock) {
117       ourScheduledToRefresh.add(homePath);
118     }
119     if (!updateLocalSdkPaths(sdk, sdkModificator)) {
120       return false;
121     }
122
123
124     final Application application = ApplicationManager.getApplication();
125
126     if (application.isUnitTestMode()) {
127       /**
128        * All actions we take after this line are dedicated to skeleton update process.
129        * Not all tests do need them.
130        * To find test API that updates skeleton, find usage of following method:
131        * {@link PySkeletonRefresher#refreshSkeletonsOfSdk(Project, Component, String, Sdk)}
132        */
133       return true;
134     }
135
136     @SuppressWarnings("ThrowableInstanceNeverThrown") final Throwable methodCallStacktrace = new Throwable();
137     application.invokeLater(() -> {
138       synchronized (ourLock) {
139         if (!ourScheduledToRefresh.contains(homePath)) {
140           return;
141         }
142         ourScheduledToRefresh.remove(homePath);
143       }
144       ProgressManager.getInstance().run(new Task.Backgroundable(project, PyBundle.message("sdk.gen.updating.interpreter"), false) {
145         @Override
146         public void run(@NotNull ProgressIndicator indicator) {
147           final Project project1 = getProject();
148           final Sdk sdk12 = PythonSdkType.findSdkByPath(homePath);
149           if (sdk12 != null) {
150             ourUnderRefresh.put(homePath);
151             try {
152               final String skeletonsPath = getBinarySkeletonsPath(homePath);
153               try {
154                 if (PythonSdkType.isRemote(sdk12) && project1 == null && ownerComponent == null) {
155                   LOG.error("For refreshing skeletons of remote SDK, either project or owner component must be specified");
156                 }
157                 LOG.info("Performing background update of skeletons for SDK " + sdk12.getHomePath());
158                 indicator.setText("Updating skeletons...");
159                 PySkeletonRefresher.refreshSkeletonsOfSdk(project1, ownerComponent, skeletonsPath, sdk12);
160                 updateRemoteSdkPaths(sdk12);
161                 indicator.setIndeterminate(true);
162                 indicator.setText("Scanning installed packages...");
163                 indicator.setText2("");
164                 LOG.info("Performing background scan of packages for SDK " + sdk12.getHomePath());
165                 try {
166                   PyPackageManager.getInstance(sdk12).refreshAndGetPackages(true);
167                 }
168                 catch (ExecutionException e) {
169                   if (LOG.isDebugEnabled()) {
170                     e.initCause(methodCallStacktrace);
171                     LOG.debug(e);
172                   }
173                   else {
174                     LOG.warn(e.getMessage());
175                   }
176                 }
177               }
178               catch (InvalidSdkException e) {
179                 if (PythonSdkType.isVagrant(sdk12)
180                     || new CredentialsTypeExChecker() {
181                   @Override
182                   protected boolean checkLanguageContribution(PyCredentialsContribution languageContribution) {
183                     return languageContribution.shouldNotifySdkSkeletonFail();
184                   }
185                 }.check(sdk12)) {
186                   PythonSdkType.notifyRemoteSdkSkeletonsFail(e, () -> {
187                     final Sdk sdk1 = PythonSdkType.findSdkByPath(homePath);
188                     if (sdk1 != null) {
189                       update(sdk1, null, project1, ownerComponent);
190                     }
191                   });
192                 }
193                 else if (!PythonSdkType.isInvalid(sdk12)) {
194                   LOG.error(e);
195                 }
196               }
197             }
198             finally {
199               try {
200                 ourUnderRefresh.remove(homePath);
201               }
202               catch (IllegalStateException e) {
203                 LOG.error(e);
204               }
205             }
206           }
207         }
208       });
209     }, ModalityState.NON_MODAL);
210     return true;
211   }
212
213   /**
214    * Updates the paths of an SDK and regenerates its skeletons as a background task. Shows an error message if the update fails.
215    *
216    * @see {@link #update(Sdk, SdkModificator, Project, Component)}
217    */
218   public static void updateOrShowError(@NotNull Sdk sdk, @Nullable SdkModificator sdkModificator, @Nullable Project project,
219                                        @Nullable Component ownerComponent) {
220     final boolean success = update(sdk, sdkModificator, project, ownerComponent);
221     if (!success) {
222       final String homePath = sdk.getHomePath();
223       final String sdkName = homePath != null ? homePath : sdk.getName();
224       Messages.showErrorDialog(project,
225                                PyBundle.message("MSG.cant.setup.sdk.$0", FileUtil.toSystemDependentName(sdkName)),
226                                PyBundle.message("MSG.title.bad.sdk"));
227     }
228   }
229
230   /**
231    * Updates the paths of a local SDK.
232    *
233    * May be invoked from any thread. May freeze the current thread while evaluating sys.path.
234    */
235   private static boolean updateLocalSdkPaths(@NotNull Sdk sdk, @Nullable SdkModificator sdkModificator) {
236     if (!PythonSdkType.isRemote(sdk)) {
237       final List<VirtualFile> localSdkPaths;
238       final boolean forceCommit = ensureBinarySkeletonsDirectoryExists(sdk);
239       try {
240         localSdkPaths = getLocalSdkPaths(sdk);
241       }
242       catch (InvalidSdkException e) {
243         if (!PythonSdkType.isInvalid(sdk)) {
244           LOG.error(e);
245         }
246         return false;
247       }
248       commitSdkPathsIfChanged(sdk, sdkModificator, localSdkPaths, forceCommit);
249     }
250     return true;
251   }
252
253   /**
254    * Updates the paths of a remote SDK.
255    *
256    * Requires the skeletons refresh steps to be run before it in order to get remote paths mappings in the additional SDK data.
257    *
258    * You may invoke it from any thread. Blocks until the commit is done in the AWT thread.
259    */
260   private static void updateRemoteSdkPaths(Sdk sdk) {
261     if (PythonSdkType.isRemote(sdk)) {
262       final boolean forceCommit = ensureBinarySkeletonsDirectoryExists(sdk);
263       final List<VirtualFile> remoteSdkPaths = getRemoteSdkPaths(sdk);
264       commitSdkPathsIfChanged(sdk, null, remoteSdkPaths, forceCommit);
265     }
266   }
267
268   private static boolean ensureBinarySkeletonsDirectoryExists(Sdk sdk) {
269     final String skeletonsPath = getBinarySkeletonsPath(sdk.getHomePath());
270     if (skeletonsPath != null) {
271       if (new File(skeletonsPath).mkdirs()) {
272         return true;
273       }
274     }
275     return false;
276   }
277
278   /**
279    * Returns all the paths for a local SDK.
280    */
281   @NotNull
282   private static List<VirtualFile> getLocalSdkPaths(@NotNull Sdk sdk) throws InvalidSdkException {
283     return ImmutableList.<VirtualFile>builder()
284       .addAll(evaluateSysPath(sdk))
285       .addAll(getSkeletonsPaths(sdk))
286       .addAll(getUserAddedPaths(sdk))
287       .build();
288   }
289
290   /**
291    * Returns all the paths for a remote SDK.
292    *
293    * Requires the skeletons refresh steps to be run before it in order to get remote paths mappings in the additional SDK data.
294    */
295   @NotNull
296   private static List<VirtualFile> getRemoteSdkPaths(@NotNull Sdk sdk) {
297     return ImmutableList.<VirtualFile>builder()
298       .addAll(getRemoteSdkMappedPaths(sdk))
299       .addAll(getSkeletonsPaths(sdk))
300       .addAll(getUserAddedPaths(sdk))
301       .build();
302   }
303
304   /**
305    * Returns all the paths manually added to an SDK by the user.
306    */
307   @NotNull
308   private static List<VirtualFile> getUserAddedPaths(@NotNull Sdk sdk) {
309     final SdkAdditionalData additionalData = sdk.getSdkAdditionalData();
310     final PythonSdkAdditionalData pythonAdditionalData = PyUtil.as(additionalData, PythonSdkAdditionalData.class);
311     return pythonAdditionalData != null ? Lists.newArrayList(pythonAdditionalData.getAddedPathFiles()) :
312            Collections.<VirtualFile>emptyList();
313   }
314
315   /**
316    * Returns local paths for a remote SDK that have been mapped to remote paths during the skeleton refresh step.
317    *
318    * Returns all the existing paths except those manually excluded by the user.
319    */
320   @NotNull
321   private static List<VirtualFile> getRemoteSdkMappedPaths(@NotNull Sdk sdk) {
322     final SdkAdditionalData additionalData = sdk.getSdkAdditionalData();
323     if (additionalData instanceof PyRemoteSdkAdditionalDataBase) {
324       final PyRemoteSdkAdditionalDataBase remoteSdkData = (PyRemoteSdkAdditionalDataBase)additionalData;
325       final List<String> paths = Lists.newArrayList();
326       for (PathMappingSettings.PathMapping mapping : remoteSdkData.getPathMappings().getPathMappings()) {
327         paths.add(mapping.getLocalRoot());
328       }
329       return filterRootPaths(sdk, paths);
330     }
331     return Collections.emptyList();
332   }
333
334   /**
335    * Filters valid paths from an initial set of Python paths and returns them as virtual files.
336    */
337   @NotNull
338   private static List<VirtualFile> filterRootPaths(@NotNull Sdk sdk, @NotNull List<String> paths) {
339     final PythonSdkAdditionalData pythonAdditionalData = PyUtil.as(sdk.getSdkAdditionalData(), PythonSdkAdditionalData.class);
340     final Collection<VirtualFile> excludedPaths = pythonAdditionalData != null ? pythonAdditionalData.getExcludedPathFiles() :
341                                                   Collections.<VirtualFile>emptyList();
342     final List<VirtualFile> results = Lists.newArrayList();
343     for (String path : paths) {
344       if (path != null && !FileUtilRt.extensionEquals(path, "egg-info")) {
345         final VirtualFile virtualFile = StandardFileSystems.local().refreshAndFindFileByPath(path);
346         if (virtualFile != null) {
347           final VirtualFile rootFile = PythonSdkType.getSdkRootVirtualFile(virtualFile);
348           if (!excludedPaths.contains(rootFile)) {
349             results.add(virtualFile);
350             continue;
351           }
352         }
353       }
354       LOG.info("Bogus sys.path entry " + path);
355     }
356     return results;
357   }
358
359   /**
360    * Returns the paths of the binary skeletons and user skeletons for an SDK.
361    */
362   @NotNull
363   private static List<VirtualFile> getSkeletonsPaths(@NotNull Sdk sdk) {
364     final List<VirtualFile> results = Lists.newArrayList();
365     final String skeletonsPath = getBinarySkeletonsPath(sdk.getHomePath());
366     if (skeletonsPath != null) {
367       final VirtualFile skeletonsDir = StandardFileSystems.local().refreshAndFindFileByPath(skeletonsPath);
368       if (skeletonsDir != null) {
369         results.add(skeletonsDir);
370         LOG.info("Binary skeletons directory for SDK \"" + sdk.getName() + "\" (" + sdk.getHomePath() + "): " +
371                  skeletonsDir.getPath());
372       }
373     }
374     final VirtualFile userSkeletonsDir = PyUserSkeletonsUtil.getUserSkeletonsDirectory();
375     if (userSkeletonsDir != null) {
376       results.add(userSkeletonsDir);
377       LOG.info("User skeletons directory for SDK \"" + sdk.getName() + "\" (" + sdk.getHomePath() + "): " +
378                userSkeletonsDir.getPath());
379     }
380     return results;
381   }
382
383   @Nullable
384   private static String getBinarySkeletonsPath(@Nullable String path) {
385     return path != null ? PythonSdkType.getSkeletonsPath(PathManager.getSystemPath(), path) : null;
386   }
387
388   /**
389    * Evaluates sys.path by running the Python interpreter from a local SDK.
390    *
391    * Returns all the existing paths except those manually excluded by the user.
392    */
393   @NotNull
394   private static List<VirtualFile> evaluateSysPath(@NotNull Sdk sdk) throws InvalidSdkException {
395     if (PythonSdkType.isRemote(sdk)) {
396       throw new IllegalArgumentException("Cannot evaluate sys.path for remote Python interpreter " + sdk);
397     }
398     final long startTime = System.currentTimeMillis();
399     final List<String> sysPath = PythonSdkType.getSysPath(sdk.getHomePath());
400     LOG.info("Updating sys.path took " + (System.currentTimeMillis() - startTime) + " ms");
401     return filterRootPaths(sdk, sysPath);
402   }
403
404   /**
405    * Commits new SDK paths using an SDK modificator if the paths have been changed.
406    *
407    * You may invoke it from any thread. Blocks until the commit is done in the AWT thread.
408    */
409   private static void commitSdkPathsIfChanged(@NotNull Sdk sdk,
410                                               @Nullable final SdkModificator sdkModificator,
411                                               @NotNull final List<VirtualFile> sdkPaths,
412                                               boolean forceCommit) {
413     final String homePath = sdk.getHomePath();
414     final SdkModificator modificatorToGetRoots = sdkModificator != null ? sdkModificator : sdk.getSdkModificator();
415     final List<VirtualFile> currentSdkPaths = Arrays.asList(modificatorToGetRoots.getRoots(OrderRootType.CLASSES));
416     if (forceCommit || !Sets.newHashSet(sdkPaths).equals(Sets.newHashSet(currentSdkPaths))) {
417       ApplicationManager.getApplication().invokeAndWait(() -> {
418         final Sdk sdk1 = PythonSdkType.findSdkByPath(homePath);
419         final SdkModificator modificatorToCommit = sdkModificator != null ? sdkModificator :
420                                                    sdk1 != null ? sdk1.getSdkModificator() : modificatorToGetRoots;
421         modificatorToCommit.removeAllRoots();
422         for (VirtualFile sdkPath : sdkPaths) {
423           modificatorToCommit.addRoot(PythonSdkType.getSdkRootVirtualFile(sdkPath), OrderRootType.CLASSES);
424         }
425         modificatorToCommit.commitChanges();
426       }, ModalityState.defaultModalityState());
427     }
428   }
429
430   /**
431    * Returns unique Python SDKs for the open modules of the project.
432    */
433   @NotNull
434   private static Set<Sdk> getPythonSdks(@NotNull Project project) {
435     final Set<Sdk> pythonSdks = Sets.newLinkedHashSet();
436     for (Module module : ModuleManager.getInstance(project).getModules()) {
437       final Sdk sdk = PythonSdkType.findPythonSdk(module);
438       if (sdk != null && sdk.getSdkType() instanceof PythonSdkType) {
439         pythonSdks.add(sdk);
440       }
441     }
442     return pythonSdks;
443   }
444 }