/**
* Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
/*
* Created on August 20, 2013
*
* @author Andrew Ferrazzutti
*/
package org.python.pydev.ui.pythonpathconf;
import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
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.MultiStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.dialogs.ListDialog;
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.shared_core.structure.LinkedListWarningOnSlowOperations;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_core.structure.Tuple3;
import org.python.pydev.shared_ui.EditorUtils;
import org.python.pydev.shared_ui.UIConstants;
import org.python.pydev.shared_ui.utils.AsynchronousProgressMonitorWrapper;
import org.python.pydev.ui.dialogs.PyDialogHelpers;
import org.python.pydev.ui.pythonpathconf.IInterpreterProviderFactory.InterpreterType;
/**
* This class uses code based from {@link AbstractInterpreterEditor} and
* {@link AbstractInterpreterPreferencesPage} to form a somewhat lighter utility for auto-
* configuring a PyDev interpreter (without having to rely on the Preferences dialog). Also
* contains an interpreter auto-searching method {@link AutoConfigMaker#autoConfig(InterpreterType)}
* implemented statically, for use by other dialogs (particularly {@link AbstractInterpreterEditor}.
*
* @author Andrew Ferrazzutti
*/
public class AutoConfigMaker {
private InterpreterType interpreterType;
private IInterpreterManager interpreterManager;
private boolean advanced;
private PrintWriter logger;
private CharArrayWriter charWriter;
private Map<String, IInterpreterInfo> nameToInfo;
/**
* Create a new AutoConfigMaker, which will hold all passed settings for automatically
* creating a new interpreter configuration. Must call {@link AutoConfigMaker#autoConfigAttempt}
* to actually create the configuration.
* @param interpreterType The interpreter's Python type: Python, Jython, or IronPython.
* @param advanced Set to true if advanced auto-config is to be used, which allows users to choose
* an interpreter out of the ones found.
* @param logger May be set to null to use a new logger.
* @param nameToInfo A map of names as keys to the IInterpreterInfos of existing interpreters. Set
* to null if no other interpreters exist at the time of the configuration attempt.
*/
public AutoConfigMaker(InterpreterType interpreterType, boolean advanced,
PrintWriter logger, Map<String, IInterpreterInfo> nameToInfo) {
this.interpreterType = interpreterType;
this.nameToInfo = nameToInfo;
switch (interpreterType) {
case JYTHON:
interpreterManager = PydevPlugin.getJythonInterpreterManager(true);
break;
case IRONPYTHON:
interpreterManager = PydevPlugin.getIronpythonInterpreterManager(true);
break;
default:
interpreterManager = PydevPlugin.getPythonInterpreterManager(true);
}
this.advanced = advanced;
if (logger != null) {
this.charWriter = null;
this.logger = logger;
} else {
//Use a new logger if one wasn't provided.
this.charWriter = new CharArrayWriter();
this.logger = new PrintWriter(this.charWriter);
}
}
/**
* Attempts to automatically find and apply an interpreter of the interpreter type specified
* in the constructor, in cases when no interpreters of that type are yet configured.
* @param onConfigComplete An optional JobChangeAdapter to be associated with the configure operation.
*/
public boolean autoConfigSingleApply(JobChangeAdapter onConfigComplete) {
if (interpreterManager.getInterpreterInfos().length != 0) {
return false;
}
ObtainInterpreterInfoOperation operation = autoConfigSearch();
//autoConfigSearch displays an error dialog if an interpreter couldn't be found, so don't display errors for null cases here.
if (operation == null) {
return false;
}
try {
final IInterpreterInfo interpreterInfo = operation.result.makeCopy();
final Set<String> interpreterNamesToRestore = new HashSet<String>(
Arrays.asList(operation.result.executableOrJar));
//------------- Now, actually prepare the interpreter.
Job applyOperationJob = new Job("Configure Interpreter") {
@Override
protected IStatus run(IProgressMonitor monitor) {
monitor = new AsynchronousProgressMonitorWrapper(monitor);
PyDialogHelpers.enableAskInterpreterStep(false);
monitor.beginTask("Restoring PYTHONPATH", IProgressMonitor.UNKNOWN);
try {
//set this interpreter as the only interpreter, since none existed before this one
interpreterManager.setInfos(new IInterpreterInfo[] { interpreterInfo },
interpreterNamesToRestore, monitor);
} catch (Exception e) {
Log.log(e);
//show the user a message (so that it does not fail silently)...
String errorMsg = "Error configuring the chosen interpreter.\n"
+ "Make sure the file containing the interpreter did not get corrupted during the configuration process.";
ErrorDialog.openError(EditorUtils.getShell(), "Interpreter configuration failure",
errorMsg, PydevPlugin.makeStatus(IStatus.ERROR, "See error log for details.", e));
return Status.CANCEL_STATUS;
} finally {
monitor.done();
PyDialogHelpers.enableAskInterpreterStep(true);
}
return Status.OK_STATUS;
}
};
applyOperationJob.setUser(true);
if (onConfigComplete != null) {
applyOperationJob.addJobChangeListener(onConfigComplete);
}
applyOperationJob.schedule();
return true;
} catch (Exception e) {
Log.log(e);
String errorMsg = "Error getting info on the interpreter selected by the auto-configurer.\n"
+ "Try manual configuration instead.\n\n"
+ "Common reasons include:\n\n" + "- Using an unsupported version\n"
+ " (Python and Jython require at least version 2.1 and IronPython 2.6).\n"
+ "\n" + "- Specifying an invalid interpreter\n"
+ " (usually a link to the actual interpreter on Mac or Linux)";
//show the user a message (so that it does not fail silently)...
ErrorDialog.openError(EditorUtils.getShell(), "Unable to get info on the interpreter.",
errorMsg, PydevPlugin.makeStatus(IStatus.ERROR, "See error log for details.", e));
return false;
} finally {
if (charWriter != null) {
Log.logInfo(charWriter.toString());
}
}
}
/**
* Searches for a valid interpreter.
* If quick auto-config, returns the first non-failing interpreter found.
* If advanced, allows the user to choose from a list of verified interpreters, and returns the chosen one.
*
* @return The interpreter found by quick auto-config, or the one chosen by the user for advanced auto-config.
*/
public ObtainInterpreterInfoOperation autoConfigSearch() {
// get the possible interpreters
final List<PossibleInterpreter> possibleInterpreters = getPossibleInterpreters();
// keep track of the selected item
PossibleInterpreter selectedFromPossible = null;
// Query them for validity. If advanced or none installed, nothing will be selected.
// If using quick config, choose the first installed interpreter (possibly none).
Tuple3<PossibleInterpreter, Boolean, List<Exception>> r = removeInvalidPossibles(possibleInterpreters);
selectedFromPossible = r.o1;
boolean foundDuplicate = r.o2;
List<Exception> exceptions = r.o3;
// If we don't have anything we can add, exit now with an error message
if (possibleInterpreters.size() > 0) {
if (selectedFromPossible == null) {
if (advanced && possibleInterpreters.size() > 1) {
// if using advanced config & we have more than 1 to choose from, ask the user
selectedFromPossible = promptToSelectInterpreter(possibleInterpreters);
// if selectedFromPossible is still null, user must have cancelled
if (selectedFromPossible == null) {
return null;
}
} else {
// else, we can just auto-select for them
selectedFromPossible = possibleInterpreters.get(0);
}
}
return selectedFromPossible.getOperation();
} else {
showNothingToConfigureError(foundDuplicate, exceptions);
}
return null;
}
private class PossibleInterpreter {
private IInterpreterProvider provider;
private ObtainInterpreterInfoOperation quickOperation;
private Tuple<String, String> interpreterNameAndExecutable;
public PossibleInterpreter(IInterpreterProvider provider) {
this.provider = provider;
}
/**
* Indicates whether or not an interpreter is valid for use.
* @return <code>false</code> if the interpreter is a duplicate of a configured one,
* or <code>true</code> if it is valid for use, or is yet to be installed.
* @throws Exception An exception is thrown if the interpreter cannot be configured at all.
*/
public boolean isValid() throws Exception {
if (needInstall()) {
return true;
}
// Try a quick config of the provider.
// If getNameAndExecutable is successful, the interpreter won't be null & will have a unique name,
// but it may a duplicate of something already configured.
if (InterpreterConfigHelpers.getDuplicatedMessageError(null, getNameAndExecutable().o2,
nameToInfo) != null) {
return false;
}
this.quickOperation = createOperation(false, false);
return true;
}
private ObtainInterpreterInfoOperation createOperation(boolean advanced, boolean showErrors) throws Exception {
return InterpreterConfigHelpers.tryInterpreter(getNameAndExecutable(), interpreterManager, !advanced,
showErrors, logger, EditorUtils.getShell());
}
private Tuple<String, String> getNameAndExecutable() throws Exception {
if (interpreterNameAndExecutable != null) {
return interpreterNameAndExecutable;
}
String executable = provider.getExecutableOrJar();
if (executable != null && executable.trim().length() > 0) {
String name = provider.getName();
if (name == null) {
name = executable;
}
if (nameToInfo != null) {
name = InterpreterConfigHelpers.getUniqueInterpreterName(name, nameToInfo);
}
interpreterNameAndExecutable = new Tuple<String, String>(name, executable);
} else {
throw new Exception("Provider is invalid because it returned null from getExecutableOrJar()");
}
return interpreterNameAndExecutable;
}
public ObtainInterpreterInfoOperation getOperation() {
if (needInstall()) {
SafeRunner.run(new SafeRunnable() {
@Override
public void run() throws Exception {
provider.runInstall();
}
});
if (needInstall()) {
// runInstall failed, nothing else we can do (SafeRunnable or
// the installer itself will have displayed an error)
return null;
}
// name & executable may have changed, so set it to null to mark it for update the next time it's needed
interpreterNameAndExecutable = null;
}
if (!advanced && quickOperation != null) {
return quickOperation;
} else {
// Re-run an operation if user has to select folders (if advanced),
// if the provider was uninstalled (if quickOperation is null),
// or if the interpreter was missing libs (advanced & null quickOperation).
try {
return createOperation(advanced, true);
} catch (Exception e) {
// Failed to create operation, as we did "showErrors=true" we don't
// need to display them again though, so simply log them and exit
Log.log(e);
return null;
}
}
}
public boolean needInstall() {
return !provider.isInstalled();
}
public String getExecutableOrJar() {
return provider.getExecutableOrJar();
}
}
private PossibleInterpreter promptToSelectInterpreter(final List<PossibleInterpreter> possibleInterpreters) {
// Now we need to prompt the user to choose.
ListDialog listDialog = new ListDialog(EditorUtils.getShell());
listDialog.setContentProvider(new ArrayContentProvider());
listDialog.setLabelProvider(new LabelProvider() {
@Override
public Image getImage(Object element) {
return PydevPlugin.getImageCache().get(UIConstants.PY_INTERPRETER_ICON);
}
@Override
public String getText(Object element) {
PossibleInterpreter possible = (PossibleInterpreter) element;
return possible.getExecutableOrJar();
}
});
listDialog.setInput(possibleInterpreters.toArray());
listDialog.setMessage("Multiple possible interpreters are available.\n"
+ "Please select which one you want to install and configure.");
if (listDialog.open() == ListDialog.OK) {
Object[] result = listDialog.getResult();
return (PossibleInterpreter) result[0];
} else {
return null;
}
}
private Tuple3<PossibleInterpreter, Boolean, List<Exception>> removeInvalidPossibles(
final List<PossibleInterpreter> possibleInterpreters) {
boolean foundDuplicate = false;
List<Exception> exceptions = new LinkedListWarningOnSlowOperations<Exception>();
// Iterate through the interpreters, removing the invalid ones
for (Iterator<PossibleInterpreter> iterator = possibleInterpreters.iterator(); iterator.hasNext();) {
PossibleInterpreter possibleInterpreter = iterator.next();
Boolean validStatus = null;
// Calling isValid may be a lengthy operation
try {
validStatus = possibleInterpreter.isValid();
if (!validStatus) {
foundDuplicate = true;
throw new Exception("Duplicate interpreter.");
}
} catch (Exception e) {
// If validStatus is null, an exception was thrown by isValid; save the first error found.
if (validStatus == null) {
exceptions.add(e);
}
// Remove the interpreter if it is invalid or a duplicate.
// Exception to this rule: if using advanced config, allow interpreters with no lib folders.
if (!advanced || !e.getMessage().startsWith(InterpreterConfigHelpers.ERMSG_NOLIBS)) {
iterator.remove();
}
continue;
}
// We want to early exit for quick config
if (!advanced && !possibleInterpreter.needInstall()) {
// Early exit
return new Tuple3<>(possibleInterpreter, foundDuplicate, exceptions);
}
}
// keep going
return new Tuple3<>(null, foundDuplicate, exceptions);
}
private void showNothingToConfigureError(boolean foundDuplicate, List<Exception> exceptions) {
String errorMsg = "Auto-configurer could not find a valid interpreter"
+ (foundDuplicate ? " that has not already been configured" : "") + ".\n"
+ "Please manually configure a new interpreter instead.";
String typeSpecificMessage;
switch (interpreterType) {
case PYTHON:
typeSpecificMessage = "\n\nNote: the system environment variables that are used "
+ "when auto-searching for a Jython interpreter are the following:\n"
+ "- PATH\n"
+ "- PYTHONHOME / PYTHON_HOME";
break;
case JYTHON:
typeSpecificMessage = "\n\nNote: the system environment variables that are used "
+ "when auto-searching for a Jython interpreter are the following:\n"
+ "- PATH\n"
+ "- PYTHONHOME / PYTHON_HOME\n"
+ "- JYTHONHOME / JYTHON_HOME";
break;
default:
typeSpecificMessage = "";
}
String message;
if (exceptions.size() > 0) {
message = "Errors getting info on discovered interpreter(s).\n"
+ "See error log for details.";
} else if (foundDuplicate) {
message = "All interpreters found are already being used.";
} else {
// If there are no duplicates nor an interpreter error, nothing was found at all.
message = "No interpreters were found.\n"
+ "Make sure an interpreter is in the system PATH.";
}
String dialogTitle = "Unable to auto-configure.";
if (exceptions.size() > 0) {
IStatus[] children = new IStatus[exceptions.size()];
for (int i = 0; i < exceptions.size(); i++) {
Exception exception = exceptions.get(i);
children[i] = PydevPlugin.makeStatus(IStatus.ERROR, null, exception);
}
MultiStatus multiStatus = new MultiStatus(PydevPlugin.getPluginID(), IStatus.ERROR, children, message,
null);
ErrorDialog.openError(EditorUtils.getShell(), dialogTitle, errorMsg + typeSpecificMessage, multiStatus);
} else {
Status status = PydevPlugin.makeStatus(IStatus.ERROR, message, null);
ErrorDialog.openError(EditorUtils.getShell(), dialogTitle, errorMsg + typeSpecificMessage, status);
}
}
private List<PossibleInterpreter> getPossibleInterpreters() {
final List<IInterpreterProvider> providers = getAllProviders();
final List<PossibleInterpreter> possibleInterpreters = new ArrayList<>(providers.size());
for (IInterpreterProvider provider : providers) {
PossibleInterpreter possibleInterpreter = new PossibleInterpreter(provider);
possibleInterpreters.add(possibleInterpreter);
}
return possibleInterpreters;
}
private List<IInterpreterProvider> getAllProviders() {
final List<IInterpreterProvider> providers = new ArrayList<IInterpreterProvider>();
@SuppressWarnings("unchecked")
List<IInterpreterProviderFactory> participants = ExtensionHelper
.getParticipants(ExtensionHelper.PYDEV_INTERPRETER_PROVIDER);
for (final IInterpreterProviderFactory providerFactory : participants) {
SafeRunner.run(new SafeRunnable() {
@Override
public void run() throws Exception {
IInterpreterProvider[] ips = providerFactory.getInterpreterProviders(interpreterType);
if (ips != null) {
providers.addAll(Arrays.asList(ips));
}
}
});
}
return providers;
}
}