edca9e70620fb6a96051c859f10787a1c92bd723
[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                   e.initCause(methodCallStacktrace);
170                   LOG.warn(e);
171                 }
172               }
173               catch (InvalidSdkException e) {
174                 if (PythonSdkType.isVagrant(sdk12)
175                     || new CredentialsTypeExChecker() {
176                   @Override
177                   protected boolean checkLanguageContribution(PyCredentialsContribution languageContribution) {
178                     return languageContribution.shouldNotifySdkSkeletonFail();
179                   }
180                 }.check(sdk12)) {
181                   PythonSdkType.notifyRemoteSdkSkeletonsFail(e, () -> {
182                     final Sdk sdk1 = PythonSdkType.findSdkByPath(homePath);
183                     if (sdk1 != null) {
184                       update(sdk1, null, project1, ownerComponent);
185                     }
186                   });
187                 }
188                 else if (!PythonSdkType.isInvalid(sdk12)) {
189                   LOG.error(e);
190                 }
191               }
192             }
193             finally {
194               try {
195                 ourUnderRefresh.remove(homePath);
196               }
197               catch (IllegalStateException e) {
198                 LOG.error(e);
199               }
200             }
201           }
202         }
203       });
204     }, ModalityState.NON_MODAL);
205     return true;
206   }
207
208   /**
209    * Updates the paths of an SDK and regenerates its skeletons as a background task. Shows an error message if the update fails.
210    *
211    * @see {@link #update(Sdk, SdkModificator, Project, Component)}
212    */
213   public static void updateOrShowError(@NotNull Sdk sdk, @Nullable SdkModificator sdkModificator, @Nullable Project project,
214                                        @Nullable Component ownerComponent) {
215     final boolean success = update(sdk, sdkModificator, project, ownerComponent);
216     if (!success) {
217       final String homePath = sdk.getHomePath();
218       final String sdkName = homePath != null ? homePath : sdk.getName();
219       Messages.showErrorDialog(project,
220                                PyBundle.message("MSG.cant.setup.sdk.$0", FileUtil.toSystemDependentName(sdkName)),
221                                PyBundle.message("MSG.title.bad.sdk"));
222     }
223   }
224
225   /**
226    * Updates the paths of a local SDK.
227    *
228    * May be invoked from any thread. May freeze the current thread while evaluating sys.path.
229    */
230   private static boolean updateLocalSdkPaths(@NotNull Sdk sdk, @Nullable SdkModificator sdkModificator) {
231     if (!PythonSdkType.isRemote(sdk)) {
232       final List<VirtualFile> localSdkPaths;
233       final boolean forceCommit = ensureBinarySkeletonsDirectoryExists(sdk);
234       try {
235         localSdkPaths = getLocalSdkPaths(sdk);
236       }
237       catch (InvalidSdkException e) {
238         if (!PythonSdkType.isInvalid(sdk)) {
239           LOG.error(e);
240         }
241         return false;
242       }
243       commitSdkPathsIfChanged(sdk, sdkModificator, localSdkPaths, forceCommit);
244     }
245     return true;
246   }
247
248   /**
249    * Updates the paths of a remote SDK.
250    *
251    * Requires the skeletons refresh steps to be run before it in order to get remote paths mappings in the additional SDK data.
252    *
253    * You may invoke it from any thread. Blocks until the commit is done in the AWT thread.
254    */
255   private static void updateRemoteSdkPaths(Sdk sdk) {
256     if (PythonSdkType.isRemote(sdk)) {
257       final boolean forceCommit = ensureBinarySkeletonsDirectoryExists(sdk);
258       final List<VirtualFile> remoteSdkPaths = getRemoteSdkPaths(sdk);
259       commitSdkPathsIfChanged(sdk, null, remoteSdkPaths, forceCommit);
260     }
261   }
262
263   private static boolean ensureBinarySkeletonsDirectoryExists(Sdk sdk) {
264     final String skeletonsPath = getBinarySkeletonsPath(sdk.getHomePath());
265     if (skeletonsPath != null) {
266       if (new File(skeletonsPath).mkdirs()) {
267         return true;
268       }
269     }
270     return false;
271   }
272
273   /**
274    * Returns all the paths for a local SDK.
275    */
276   @NotNull
277   private static List<VirtualFile> getLocalSdkPaths(@NotNull Sdk sdk) throws InvalidSdkException {
278     return ImmutableList.<VirtualFile>builder()
279       .addAll(evaluateSysPath(sdk))
280       .addAll(getSkeletonsPaths(sdk))
281       .addAll(getUserAddedPaths(sdk))
282       .build();
283   }
284
285   /**
286    * Returns all the paths for a remote SDK.
287    *
288    * Requires the skeletons refresh steps to be run before it in order to get remote paths mappings in the additional SDK data.
289    */
290   @NotNull
291   private static List<VirtualFile> getRemoteSdkPaths(@NotNull Sdk sdk) {
292     return ImmutableList.<VirtualFile>builder()
293       .addAll(getRemoteSdkMappedPaths(sdk))
294       .addAll(getSkeletonsPaths(sdk))
295       .addAll(getUserAddedPaths(sdk))
296       .build();
297   }
298
299   /**
300    * Returns all the paths manually added to an SDK by the user.
301    */
302   @NotNull
303   private static List<VirtualFile> getUserAddedPaths(@NotNull Sdk sdk) {
304     final SdkAdditionalData additionalData = sdk.getSdkAdditionalData();
305     final PythonSdkAdditionalData pythonAdditionalData = PyUtil.as(additionalData, PythonSdkAdditionalData.class);
306     return pythonAdditionalData != null ? Lists.newArrayList(pythonAdditionalData.getAddedPathFiles()) :
307            Collections.<VirtualFile>emptyList();
308   }
309
310   /**
311    * Returns local paths for a remote SDK that have been mapped to remote paths during the skeleton refresh step.
312    *
313    * Returns all the existing paths except those manually excluded by the user.
314    */
315   @NotNull
316   private static List<VirtualFile> getRemoteSdkMappedPaths(@NotNull Sdk sdk) {
317     final SdkAdditionalData additionalData = sdk.getSdkAdditionalData();
318     if (additionalData instanceof PyRemoteSdkAdditionalDataBase) {
319       final PyRemoteSdkAdditionalDataBase remoteSdkData = (PyRemoteSdkAdditionalDataBase)additionalData;
320       final List<String> paths = Lists.newArrayList();
321       for (PathMappingSettings.PathMapping mapping : remoteSdkData.getPathMappings().getPathMappings()) {
322         paths.add(mapping.getLocalRoot());
323       }
324       return filterRootPaths(sdk, paths);
325     }
326     return Collections.emptyList();
327   }
328
329   /**
330    * Filters valid paths from an initial set of Python paths and returns them as virtual files.
331    */
332   @NotNull
333   private static List<VirtualFile> filterRootPaths(@NotNull Sdk sdk, @NotNull List<String> paths) {
334     final PythonSdkAdditionalData pythonAdditionalData = PyUtil.as(sdk.getSdkAdditionalData(), PythonSdkAdditionalData.class);
335     final Collection<VirtualFile> excludedPaths = pythonAdditionalData != null ? pythonAdditionalData.getExcludedPathFiles() :
336                                                   Collections.<VirtualFile>emptyList();
337     final List<VirtualFile> results = Lists.newArrayList();
338     for (String path : paths) {
339       if (path != null && !FileUtilRt.extensionEquals(path, "egg-info")) {
340         final VirtualFile virtualFile = StandardFileSystems.local().refreshAndFindFileByPath(path);
341         if (virtualFile != null) {
342           final VirtualFile rootFile = PythonSdkType.getSdkRootVirtualFile(virtualFile);
343           if (!excludedPaths.contains(rootFile)) {
344             results.add(virtualFile);
345             continue;
346           }
347         }
348       }
349       LOG.info("Bogus sys.path entry " + path);
350     }
351     return results;
352   }
353
354   /**
355    * Returns the paths of the binary skeletons and user skeletons for an SDK.
356    */
357   @NotNull
358   private static List<VirtualFile> getSkeletonsPaths(@NotNull Sdk sdk) {
359     final List<VirtualFile> results = Lists.newArrayList();
360     final String skeletonsPath = getBinarySkeletonsPath(sdk.getHomePath());
361     if (skeletonsPath != null) {
362       final VirtualFile skeletonsDir = StandardFileSystems.local().refreshAndFindFileByPath(skeletonsPath);
363       if (skeletonsDir != null) {
364         results.add(skeletonsDir);
365         LOG.info("Binary skeletons directory for SDK \"" + sdk.getName() + "\" (" + sdk.getHomePath() + "): " +
366                  skeletonsDir.getPath());
367       }
368     }
369     final VirtualFile userSkeletonsDir = PyUserSkeletonsUtil.getUserSkeletonsDirectory();
370     if (userSkeletonsDir != null) {
371       results.add(userSkeletonsDir);
372       LOG.info("User skeletons directory for SDK \"" + sdk.getName() + "\" (" + sdk.getHomePath() + "): " +
373                userSkeletonsDir.getPath());
374     }
375     return results;
376   }
377
378   @Nullable
379   private static String getBinarySkeletonsPath(@Nullable String path) {
380     return path != null ? PythonSdkType.getSkeletonsPath(PathManager.getSystemPath(), path) : null;
381   }
382
383   /**
384    * Evaluates sys.path by running the Python interpreter from a local SDK.
385    *
386    * Returns all the existing paths except those manually excluded by the user.
387    */
388   @NotNull
389   private static List<VirtualFile> evaluateSysPath(@NotNull Sdk sdk) throws InvalidSdkException {
390     if (PythonSdkType.isRemote(sdk)) {
391       throw new IllegalArgumentException("Cannot evaluate sys.path for remote Python interpreter " + sdk);
392     }
393     final long startTime = System.currentTimeMillis();
394     final List<String> sysPath = PythonSdkType.getSysPath(sdk.getHomePath());
395     LOG.info("Updating sys.path took " + (System.currentTimeMillis() - startTime) + " ms");
396     return filterRootPaths(sdk, sysPath);
397   }
398
399   /**
400    * Commits new SDK paths using an SDK modificator if the paths have been changed.
401    *
402    * You may invoke it from any thread. Blocks until the commit is done in the AWT thread.
403    */
404   private static void commitSdkPathsIfChanged(@NotNull Sdk sdk,
405                                               @Nullable final SdkModificator sdkModificator,
406                                               @NotNull final List<VirtualFile> sdkPaths,
407                                               boolean forceCommit) {
408     final String homePath = sdk.getHomePath();
409     final SdkModificator modificatorToGetRoots = sdkModificator != null ? sdkModificator : sdk.getSdkModificator();
410     final List<VirtualFile> currentSdkPaths = Arrays.asList(modificatorToGetRoots.getRoots(OrderRootType.CLASSES));
411     if (forceCommit || !Sets.newHashSet(sdkPaths).equals(Sets.newHashSet(currentSdkPaths))) {
412       ApplicationManager.getApplication().invokeAndWait(() -> {
413         final Sdk sdk1 = PythonSdkType.findSdkByPath(homePath);
414         final SdkModificator modificatorToCommit = sdkModificator != null ? sdkModificator :
415                                                    sdk1 != null ? sdk1.getSdkModificator() : modificatorToGetRoots;
416         modificatorToCommit.removeAllRoots();
417         for (VirtualFile sdkPath : sdkPaths) {
418           modificatorToCommit.addRoot(PythonSdkType.getSdkRootVirtualFile(sdkPath), OrderRootType.CLASSES);
419         }
420         modificatorToCommit.commitChanges();
421       }, ModalityState.defaultModalityState());
422     }
423   }
424
425   /**
426    * Returns unique Python SDKs for the open modules of the project.
427    */
428   @NotNull
429   private static Set<Sdk> getPythonSdks(@NotNull Project project) {
430     final Set<Sdk> pythonSdks = Sets.newLinkedHashSet();
431     for (Module module : ModuleManager.getInstance(project).getModules()) {
432       final Sdk sdk = PythonSdkType.findPythonSdk(module);
433       if (sdk != null && sdk.getSdkType() instanceof PythonSdkType) {
434         pythonSdks.add(sdk);
435       }
436     }
437     return pythonSdks;
438   }
439 }