/******************************************************************************
* Copyright (C) 2011-2013 Fabio Zadrozny and others
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Fabio Zadrozny <fabiofz@gmail.com> - initial API and implementation
******************************************************************************/
package org.python.pydev.editor.codecompletion.revisited;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.preference.IPersistentPreferenceStore;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.graphics.Image;
import org.python.pydev.core.ExtensionHelper;
import org.python.pydev.core.IInterpreterInfo;
import org.python.pydev.core.IInterpreterManager;
import org.python.pydev.core.log.Log;
import org.python.pydev.plugin.PydevPlugin;
import org.python.pydev.plugin.preferences.PydevPrefs;
import org.python.pydev.shared_core.string.StringUtils;
import org.python.pydev.shared_core.structure.DataAndImageTreeNode;
import org.python.pydev.shared_core.structure.OrderedSet;
import org.python.pydev.shared_core.structure.TreeNode;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_core.utils.ThreadPriorityHelper;
import org.python.pydev.shared_ui.ImageCache;
import org.python.pydev.shared_ui.SharedUiPlugin;
import org.python.pydev.shared_ui.UIConstants;
import org.python.pydev.shared_ui.utils.RunInUiThread;
import org.python.pydev.ui.dialogs.SelectNDialog;
import org.python.pydev.ui.dialogs.TreeNodeLabelProvider;
import org.python.pydev.ui.pythonpathconf.DefaultPathsForInterpreterInfo;
import org.python.pydev.ui.pythonpathconf.IInterpreterInfoBuilder;
import org.python.pydev.ui.pythonpathconf.InterpreterInfo;
/**
* This is a helper class to keep the PYTHONPATH of interpreters configured inside Eclipse with the PYTHONPATH that
* the interpreter actually has currently.
*
* I.e.: doing on the command-line:
*
* d:\bin\Python265\Scripts\pip-2.6.exe install path.py --egg
* d:\bin\Python265\Scripts\pip-2.6.exe uninstall path.py
*
* which will change the pythonpath should actually request a pythonpath update
*
*
* Also, doing:
*
* d:\bin\Python265\Scripts\pip-2.6.exe install path.py
* d:\bin\Python265\Scripts\pip-2.6.exe uninstall path.py
*
* which will only download the path.py without actually changing the pythonpath must also work!
*
* @author Fabio
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class SynchSystemModulesManager {
public static final boolean DEBUG = false;
public static class PythonpathChange {
public final String path;
public final boolean add;
public PythonpathChange(String path, boolean add) {
this.path = path;
this.add = add;
}
@Override
public String toString() {
if (add) {
return "Add to PYTHONPATH: " + path;
}
return "Remove from PYTHONPATH: " + path;
}
public void apply(OrderedSet<String> newPythonPath) {
if (add) {
newPythonPath.add(path);
} else {
newPythonPath.remove(path);
}
}
}
private class JobApplyChanges extends Job {
private DataAndImageTreeNode root;
private List<TreeNode> selectElements;
private ManagerInfoToUpdate managerToNameToInfo;
private final Object lock = new Object();
public JobApplyChanges() {
super("Apply PYTHONPATH changes");
}
@Override
protected IStatus run(IProgressMonitor monitor) {
ThreadPriorityHelper priorityHelper = new ThreadPriorityHelper(this.getThread());
priorityHelper.setMinPriority();
try {
DataAndImageTreeNode localRoot;
List<TreeNode> localSelectElements;
ManagerInfoToUpdate localManagerToNameToInfo;
synchronized (lock) {
localRoot = this.root;
localSelectElements = this.selectElements;
localManagerToNameToInfo = this.managerToNameToInfo;
this.root = null;
this.selectElements = null;
this.managerToNameToInfo = null;
}
if (localRoot != null && localSelectElements != null) {
applySelectedChangesToInterpreterInfosPythonpath(localRoot, localSelectElements, monitor);
} else if (localManagerToNameToInfo != null) {
synchronizeManagerToNameToInfoPythonpath(monitor, localManagerToNameToInfo, null);
}
} finally {
priorityHelper.restoreInitialPriority();
}
return Status.OK_STATUS;
}
public void stack(DataAndImageTreeNode root, List<TreeNode> selectElements,
ManagerInfoToUpdate managerToNameToInfo) {
synchronized (lock) {
this.root = root;
this.selectElements = selectElements;
this.managerToNameToInfo = managerToNameToInfo; //important to check the initial time!
}
}
public void stack(ManagerInfoToUpdate managerToNameToInfo) {
synchronized (lock) {
this.root = null;
this.selectElements = null;
this.managerToNameToInfo = managerToNameToInfo;
}
}
}
private final JobApplyChanges jobApplyChanges = new JobApplyChanges();
public static class CreateInterpreterInfoCallback {
public IInterpreterInfo createInterpreterInfo(IInterpreterManager manager, String executable,
IProgressMonitor monitor) {
boolean askUser = false;
try {
return manager.createInterpreterInfo(executable, monitor, askUser);
} catch (Exception e) {
Log.log(e);
}
return null;
}
}
public void applySelectedChangesToInterpreterInfosPythonpath(
final DataAndImageTreeNode root, List<TreeNode> selectElements, IProgressMonitor monitor) {
List<IInterpreterInfo> changedInfos = computeChanges(root, selectElements);
if (changedInfos.size() > 0) {
IInterpreterManager[] allInterpreterManagers = PydevPlugin.getAllInterpreterManagers();
for (IInterpreterManager manager : allInterpreterManagers) {
if (manager == null) {
continue;
}
Map<String, IInterpreterInfo> changedInterpreterNameToInterpreter = new HashMap<>();
for (IInterpreterInfo info : changedInfos) {
changedInterpreterNameToInterpreter.put(info.getName(), info);
}
IInterpreterInfo[] allInfos = manager.getInterpreterInfos();
List<Object> newInfos = new ArrayList<>(allInfos.length);
Set<String> changedNames = new HashSet<>();
//Important: keep the order in which the user configured the interpreters.
for (IInterpreterInfo info : allInfos) {
IInterpreterInfo changedInfo = changedInterpreterNameToInterpreter.remove(info.getName());
if (changedInfo != null) {
//Override with the ones that should be changed.
newInfos.add(changedInfo);
changedNames.add(changedInfo.getExecutableOrJar());
} else {
newInfos.add(info);
}
}
if (changedNames.size() > 0) {
if (DEBUG) {
System.out.println("Updating interpreters: " + changedNames);
}
manager.setInfos(
newInfos.toArray(new IInterpreterInfo[newInfos.size()]),
changedNames,
monitor);
}
}
}
}
/**
* Here we'll update the tree structure to be shown to the user with the changes (root).
* The managerToNameToInfo structure has the information on the interpreter manager and related
* interpreter infos for which the changes should be checked.
*/
public void updateStructures(IProgressMonitor monitor, final DataAndImageTreeNode root,
ManagerInfoToUpdate managerToNameToInfo, CreateInterpreterInfoCallback callback) {
if (monitor == null) {
monitor = new NullProgressMonitor();
}
ImageCache imageCache = SharedUiPlugin.getImageCache();
if (imageCache == null) {
imageCache = new ImageCache(null) { //create dummy for tests
@Override
public Image get(String key) {
return null;
}
};
}
for (Tuple<IInterpreterManager, IInterpreterInfo> infos : managerToNameToInfo.getManagerAndInfos()) {
IInterpreterManager manager = infos.o1;
IInterpreterInfo internalInfo = infos.o2;
String executable = internalInfo.getExecutableOrJar();
IInterpreterInfo newInterpreterInfo = callback.createInterpreterInfo(manager, executable, monitor);
if (newInterpreterInfo == null) {
continue;
}
DefaultPathsForInterpreterInfo defaultPaths = new DefaultPathsForInterpreterInfo();
OrderedSet<String> newEntries = new OrderedSet<String>(newInterpreterInfo.getPythonPath());
newEntries.removeAll(internalInfo.getPythonPath());
//Iterate over the new entries to suggest what should be added (we already have only what's not there).
for (Iterator<String> it = newEntries.iterator(); it.hasNext();) {
String entryInPythonpath = it.next();
if (!defaultPaths.selectByDefault(entryInPythonpath) || !defaultPaths.exists(entryInPythonpath)) {
it.remove(); //Don't suggest the addition of entries in the workspace or entries which do not exist.
}
}
//Iterate over existing entries to suggest what should be removed.
OrderedSet<String> removedEntries = new OrderedSet<String>();
List<String> pythonPath = internalInfo.getPythonPath();
for (String string : pythonPath) {
if (!new File(string).exists()) {
//Only suggest a removal if it was removed from the filesystem.
removedEntries.add(string);
}
}
if (newEntries.size() > 0 || removedEntries.size() > 0) {
DataAndImageTreeNode<IInterpreterInfo> interpreterNode = new DataAndImageTreeNode<IInterpreterInfo>(
root,
internalInfo,
imageCache.get(
UIConstants.PY_INTERPRETER_ICON));
for (String s : newEntries) {
new DataAndImageTreeNode(interpreterNode, new PythonpathChange(s, true),
imageCache.get(UIConstants.LIB_SYSTEM));
}
for (String s : removedEntries) {
new DataAndImageTreeNode(interpreterNode, new PythonpathChange(s, false),
imageCache.get(UIConstants.REMOVE_LIB_SYSTEM));
}
}
}
}
/**
* Given a passed tree, selects the elements on the tree (and returns the selected elements in a flat list).
*/
private List<TreeNode> selectElementsInDialog(final DataAndImageTreeNode root, List<TreeNode> initialSelection) {
List<TreeNode> selectElements = SelectNDialog.selectElements(root,
new TreeNodeLabelProvider() {
@Override
public org.eclipse.swt.graphics.Image getImage(Object element) {
DataAndImageTreeNode n = (DataAndImageTreeNode) element;
return n.image;
};
@Override
public String getText(Object element) {
TreeNode n = (TreeNode) element;
Object data = n.getData();
if (data == null) {
return "null";
}
if (data instanceof IInterpreterInfo) {
IInterpreterInfo iInterpreterInfo = (IInterpreterInfo) data;
return iInterpreterInfo.getNameForUI();
}
return data.toString();
};
},
"System PYTHONPATH changes detected",
"Please check which interpreters and paths should be updated.",
true,
initialSelection);
return selectElements;
}
/**
* Given the tree structure we created initially with all the changes (root) and the elements
* that the user selected in the tree (selectElements), return a list of infos updated with the
* proper pythonpath.
*/
private List<IInterpreterInfo> computeChanges(final DataAndImageTreeNode root,
List<TreeNode> selectElements) {
List<IInterpreterInfo> changedInfos = new ArrayList<>();
HashSet<TreeNode> set = new HashSet<TreeNode>(selectElements.size());
set.addAll(selectElements);
for (Object n : root.getChildren()) {
DataAndImageTreeNode interpreterNode = (DataAndImageTreeNode) n;
if (set.contains(interpreterNode)) {
IInterpreterInfo info = (IInterpreterInfo) interpreterNode.getData();
List<String> pythonPath = info.getPythonPath();
boolean changed = false;
OrderedSet<String> newPythonPath = new OrderedSet<String>(pythonPath);
for (Object entryNode : interpreterNode.getChildren()) {
DataAndImageTreeNode pythonpathNode = (DataAndImageTreeNode) entryNode;
if (set.contains(pythonpathNode)) {
PythonpathChange change = (PythonpathChange) pythonpathNode.data;
change.apply(newPythonPath);
changed = true;
}
}
if (changed) {
InterpreterInfo copy = (InterpreterInfo) info.makeCopy();
copy.libs.clear();
copy.libs.addAll(newPythonPath);
changedInfos.add(copy);
}
}
}
return changedInfos;
}
public void synchronizeManagerToNameToInfoPythonpath(IProgressMonitor monitor,
ManagerInfoToUpdate localManagerToNameToInfo,
IInterpreterInfoBuilder builder) {
if (monitor == null) {
monitor = new NullProgressMonitor();
}
if (builder == null) {
builder = (IInterpreterInfoBuilder) ExtensionHelper
.getParticipant(ExtensionHelper.PYDEV_INTERPRETER_INFO_BUILDER, false);
}
//Ok, all is Ok in the PYTHONPATH, so, check if something changed inside the interpreter info
//and not on the PYTHONPATH.
Tuple<IInterpreterManager, IInterpreterInfo>[] managerAndInfos = localManagerToNameToInfo.getManagerAndInfos();
for (Tuple<IInterpreterManager, IInterpreterInfo> tuple : managerAndInfos) {
//If it was changed or not, we must check the internal structure too!
InterpreterInfo info = (InterpreterInfo) tuple.o2;
if (DEBUG) {
System.out.println("Synchronizing PYTHONPATH info: " + info.getNameForUI());
}
long initial = System.currentTimeMillis();
builder.syncInfoToPythonPath(monitor, info);
if (DEBUG) {
System.out.println("End Synchronizing PYTHONPATH info (" + (System.currentTimeMillis() - initial)
/ 1000.0 + " secs.)");
}
}
}
private boolean selectingElementsInDialog = false;
public boolean getSelectingElementsInDialog() {
synchronized (selectingElementsInDialogLock) {
return selectingElementsInDialog;
}
}
private final Object selectingElementsInDialogLock = new Object();
/**
* Asynchronously selects the elements in a dialog (i.e.: will execute in the UI thread) and then
* asynchronously again (in a non-ui thread) apply the changes selected.
* @param managerToNameToInfo
*/
/*default*/void asyncSelectAndScheduleElementsToChangePythonpath(final DataAndImageTreeNode root,
final ManagerInfoToUpdate managerToNameToInfo,
final List<TreeNode> initialSelection) {
RunInUiThread.async(new Runnable() {
@Override
public void run() {
synchronized (selectingElementsInDialogLock) {
if (selectingElementsInDialog) {
if (DEBUG) {
System.out.println("Bailing out: a dialog is already showing.");
}
return;
}
selectingElementsInDialog = true;
}
try {
if (managerToNameToInfo.somethingChanged()) {
if (DEBUG) {
System.out.println("Not asking anything because something changed in the meanwhile.");
}
return; //If something changed, don't do anything (we should automatically reschedule in this case).
}
List<TreeNode> selectedElements = selectElementsInDialog(root, initialSelection);
saveUnselected(root, selectedElements, PydevPrefs.getPreferences());
if (selectedElements != null && selectedElements.size() > 0) {
jobApplyChanges.stack(root, selectedElements, managerToNameToInfo);
} else {
jobApplyChanges.stack(managerToNameToInfo);
}
jobApplyChanges.schedule();
} finally {
synchronized (selectingElementsInDialogLock) {
selectingElementsInDialog = false;
}
}
}
});
}
/**
* When the user selects changes in selectElementsInDialog, it's possible that he doesn't check some of the
* proposed changes, thus, in this case, we should save the unselected items in the preferences and the next
* time such a change is proposed, it should appear unchecked (and if all changes are unchecked, we shouldn't
* present the user with a dialog).
*
* @param root this is the initial structure, containing all the proposed changes.
* @param selectedElements this is a structure which will hold only the selected changes.
* @param iPreferenceStore this is the store where we'll keep the selected changes.
*/
public void saveUnselected(DataAndImageTreeNode root, List<TreeNode> selectedElements,
IPreferenceStore iPreferenceStore) {
//root has null data, level 1 has IInterpreterInfo and level 2 has PythonpathChange.
HashSet<TreeNode> selectionSet = new HashSet<>();
if (selectedElements != null && selectedElements.size() > 0) {
selectionSet.addAll(selectedElements);
}
boolean changed = false;
for (DataAndImageTreeNode<IInterpreterInfo> interpreterNode : (List<DataAndImageTreeNode<IInterpreterInfo>>) root
.getChildren()) {
Set<TreeNode> addToIgnore = new HashSet<>();
if (!selectionSet.contains(interpreterNode)) {
//ignore all the entries below this interpreter.
addToIgnore.addAll(interpreterNode.getChildren());
} else {
//check each entry and only add the ones not selected.
for (TreeNode<PythonpathChange> pathNode : interpreterNode.getChildren()) {
if (!selectionSet.contains(pathNode)) {
addToIgnore.add(pathNode);
}
}
}
if (addToIgnore.size() > 0) {
IInterpreterInfo info = interpreterNode.getData();
String key = createKeyForInfo(info);
ArrayList<String> addToIgnorePaths = new ArrayList<String>(addToIgnore.size());
for (TreeNode<PythonpathChange> node : addToIgnore) {
PythonpathChange data = node.getData();
addToIgnorePaths.add(data.path);
}
if (DEBUG) {
System.out.println("Setting key: " + key);
System.out.println("Paths ignored: " + addToIgnorePaths);
}
changed = true;
iPreferenceStore.setValue(key, StringUtils.join("|||", addToIgnorePaths));
}
}
if (changed) {
if (iPreferenceStore instanceof IPersistentPreferenceStore) {
IPersistentPreferenceStore iPersistentPreferenceStore = (IPersistentPreferenceStore) iPreferenceStore;
try {
iPersistentPreferenceStore.save();
} catch (IOException e) {
Log.log(e);
}
}
}
}
public static String createKeyForInfo(IInterpreterInfo info) {
return "synch_ignore_entries_"
+ StringUtils.md5(info.getName() + "_" + info.getExecutableOrJar());
}
public List<TreeNode> createInitialSelectionForDialogConsideringPreviouslyIgnored(DataAndImageTreeNode root,
IPreferenceStore iPreferenceStore) {
List<TreeNode> initialSelection = new ArrayList<>();
for (DataAndImageTreeNode<IInterpreterInfo> interpreterNode : (List<DataAndImageTreeNode<IInterpreterInfo>>) root
.getChildren()) {
IInterpreterInfo info = interpreterNode.getData();
String key = createKeyForInfo(info);
String ignoredValue = iPreferenceStore.getString(key);
if (ignoredValue != null && ignoredValue.length() > 0) {
Set<String> previouslyIgnored = new HashSet(StringUtils.split(ignoredValue, "|||"));
boolean added = false;
for (TreeNode<PythonpathChange> pathNode : interpreterNode.getChildren()) {
if (!previouslyIgnored.contains(pathNode.data.path)) {
initialSelection.add(pathNode);
added = true;
} else {
if (SynchSystemModulesManager.DEBUG) {
System.out.println("Removed from initial selection: " + pathNode);
}
}
}
if (added) {
initialSelection.add(interpreterNode);
}
} else {
//Node and children all selected initially (nothing ignored).
initialSelection.add(interpreterNode);
initialSelection.addAll(interpreterNode.getChildren());
}
}
if (SynchSystemModulesManager.DEBUG) {
for (TreeNode treeNode : initialSelection) {
System.out.println("Initial selection: " + treeNode.getData());
}
}
return initialSelection;
}
/**
* Note: it's public mostly for tests. Should not be instanced when not in tests!
*/
public SynchSystemModulesManager() {
}
}