2 * Copyright 2000-2016 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.jetbrains.python.sdk;
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;
59 import java.util.List;
60 import java.util.concurrent.TimeUnit;
63 * Refreshes all project's Python SDKs.
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;
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>();
77 * Refreshes the SDKs of the modules for the open project after some delay.
80 public void runActivity(@NotNull final Project project) {
81 final Application application = ApplicationManager.getApplication();
82 if (application.isUnitTestMode()) {
85 EdtExecutorService.getScheduledExecutorInstance().schedule(() -> ProgressManager.getInstance().run(new Task.Backgroundable(project, "Updating Python Paths", false) {
87 public void run(@NotNull ProgressIndicator indicator) {
88 final Project project = getProject();
89 if (project.isDisposed()) {
92 for (final Sdk sdk : getPythonSdks(project)) {
93 update(sdk, null, project, null);
96 }), INITIAL_ACTIVITY_DELAY, TimeUnit.MILLISECONDS);
100 * Updates the paths of an SDK and regenerates its skeletons as a background task.
102 * May be invoked from any thread. May freeze the current thread while evaluating sys.path.
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.
107 * The commit of the changes in the SDK happens in the AWT thread while the current thread is waiting the result.
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.
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);
119 if (!updateLocalSdkPaths(sdk, sdkModificator)) {
124 final Application application = ApplicationManager.getApplication();
126 if (application.isUnitTestMode()) {
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)}
136 @SuppressWarnings("ThrowableInstanceNeverThrown") final Throwable methodCallStacktrace = new Throwable();
137 application.invokeLater(() -> {
138 synchronized (ourLock) {
139 if (!ourScheduledToRefresh.contains(homePath)) {
142 ourScheduledToRefresh.remove(homePath);
144 ProgressManager.getInstance().run(new Task.Backgroundable(project, PyBundle.message("sdk.gen.updating.interpreter"), false) {
146 public void run(@NotNull ProgressIndicator indicator) {
147 final Project project1 = getProject();
148 final Sdk sdk12 = PythonSdkType.findSdkByPath(homePath);
150 ourUnderRefresh.put(homePath);
152 final String skeletonsPath = getBinarySkeletonsPath(homePath);
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");
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());
166 PyPackageManager.getInstance(sdk12).refreshAndGetPackages(true);
168 catch (ExecutionException e) {
169 if (LOG.isDebugEnabled()) {
170 e.initCause(methodCallStacktrace);
174 LOG.warn(e.getMessage());
178 catch (InvalidSdkException e) {
179 if (PythonSdkType.isVagrant(sdk12)
180 || new CredentialsTypeExChecker() {
182 protected boolean checkLanguageContribution(PyCredentialsContribution languageContribution) {
183 return languageContribution.shouldNotifySdkSkeletonFail();
186 PythonSdkType.notifyRemoteSdkSkeletonsFail(e, () -> {
187 final Sdk sdk1 = PythonSdkType.findSdkByPath(homePath);
189 update(sdk1, null, project1, ownerComponent);
193 else if (!PythonSdkType.isInvalid(sdk12)) {
200 ourUnderRefresh.remove(homePath);
202 catch (IllegalStateException e) {
209 }, ModalityState.NON_MODAL);
214 * Updates the paths of an SDK and regenerates its skeletons as a background task. Shows an error message if the update fails.
216 * @see {@link #update(Sdk, SdkModificator, Project, Component)}
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);
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"));
231 * Updates the paths of a local SDK.
233 * May be invoked from any thread. May freeze the current thread while evaluating sys.path.
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);
240 localSdkPaths = getLocalSdkPaths(sdk);
242 catch (InvalidSdkException e) {
243 if (!PythonSdkType.isInvalid(sdk)) {
248 commitSdkPathsIfChanged(sdk, sdkModificator, localSdkPaths, forceCommit);
254 * Updates the paths of a remote SDK.
256 * Requires the skeletons refresh steps to be run before it in order to get remote paths mappings in the additional SDK data.
258 * You may invoke it from any thread. Blocks until the commit is done in the AWT thread.
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);
268 private static boolean ensureBinarySkeletonsDirectoryExists(Sdk sdk) {
269 final String skeletonsPath = getBinarySkeletonsPath(sdk.getHomePath());
270 if (skeletonsPath != null) {
271 if (new File(skeletonsPath).mkdirs()) {
279 * Returns all the paths for a local SDK.
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))
291 * Returns all the paths for a remote SDK.
293 * Requires the skeletons refresh steps to be run before it in order to get remote paths mappings in the additional SDK data.
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))
305 * Returns all the paths manually added to an SDK by the user.
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();
316 * Returns local paths for a remote SDK that have been mapped to remote paths during the skeleton refresh step.
318 * Returns all the existing paths except those manually excluded by the user.
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());
329 return filterRootPaths(sdk, paths);
331 return Collections.emptyList();
335 * Filters valid paths from an initial set of Python paths and returns them as virtual files.
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);
354 LOG.info("Bogus sys.path entry " + path);
360 * Returns the paths of the binary skeletons and user skeletons for an SDK.
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());
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());
384 private static String getBinarySkeletonsPath(@Nullable String path) {
385 return path != null ? PythonSdkType.getSkeletonsPath(PathManager.getSystemPath(), path) : null;
389 * Evaluates sys.path by running the Python interpreter from a local SDK.
391 * Returns all the existing paths except those manually excluded by the user.
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);
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);
405 * Commits new SDK paths using an SDK modificator if the paths have been changed.
407 * You may invoke it from any thread. Blocks until the commit is done in the AWT thread.
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);
425 modificatorToCommit.commitChanges();
426 }, ModalityState.defaultModalityState());
431 * Returns unique Python SDKs for the open modules of the project.
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) {