/****************************************************************************** * Copyright (C) 2013 Fabio Zadrozny * * 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.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; 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.python.pydev.core.IInterpreterInfo; import org.python.pydev.core.IInterpreterManager; import org.python.pydev.core.IInterpreterManagerListener; import org.python.pydev.core.log.Log; import org.python.pydev.editor.codecompletion.revisited.SynchSystemModulesManager.CreateInterpreterInfoCallback; import org.python.pydev.plugin.PydevPlugin; import org.python.pydev.plugin.preferences.PydevPrefs; import org.python.pydev.shared_core.io.FileUtils; import org.python.pydev.shared_core.path_watch.EventsStackerRunnable; import org.python.pydev.shared_core.path_watch.IFilesystemChangesListener; import org.python.pydev.shared_core.path_watch.IPathWatch; import org.python.pydev.shared_core.path_watch.PathWatch; import org.python.pydev.shared_core.structure.DataAndImageTreeNode; import org.python.pydev.shared_core.structure.TreeNode; import org.python.pydev.shared_core.utils.ThreadPriorityHelper; import org.python.pydev.ui.interpreters.AbstractInterpreterManager; import org.python.pydev.ui.pythonpathconf.InterpreterGeneralPreferencesPage; @SuppressWarnings({ "unchecked", "rawtypes" }) public class SyncSystemModulesManagerScheduler implements IInterpreterManagerListener { private final IPathWatch pathWatch = new PathWatch(); public SyncSystemModulesManagerScheduler() { pathWatch.setDirectoryFileFilter(filter, dirFilter); } private final SynchJob job = new SynchJob("Sync System PYTHONPATH"); /** * Registers some interpreter manager to be tracked for changes. * @param interpreterInfos * @return true if there are infos to be tracked and false otherwise. */ private void registerInterpreterManager(IInterpreterManager iInterpreterManager, IInterpreterInfo[] interpreterInfos) { //This will make us start tracking changes in the filesystem. afterSetInfos(iInterpreterManager, interpreterInfos); //Make sure we're called again when the paths in the interpreter manager change. iInterpreterManager.addListener(this); } /** * To be called when we start the plugin. * * Should be called only once (when we'll make a full check for the current integrity of the information) * Later on, we'll start to check if things change in the PYTHONPATH based on changes in the filesystem. */ public void start() { boolean scheduleInitially = false; boolean reCheckOnFilesystemChanges = InterpreterGeneralPreferencesPage.getReCheckOnFilesystemChanges(); IInterpreterManager[] managers = PydevPlugin.getAllInterpreterManagers(); for (IInterpreterManager iInterpreterManager : managers) { if (iInterpreterManager != null) { IInterpreterInfo[] interpreterInfos = iInterpreterManager.getInterpreterInfos(); if (reCheckOnFilesystemChanges) { this.registerInterpreterManager(iInterpreterManager, interpreterInfos); } scheduleInitially = scheduleInitially || (interpreterInfos != null && interpreterInfos.length > 0); } } int timeout = 1000 * 30; //Default is waiting 30 seconds after startup IPreferenceStore preferences = PydevPrefs.getPreferences(); boolean alreadyChecked = preferences.getBoolean("INTERPRETERS_CHECKED_ONCE"); //Now we add builtin indexing on our checks (so, force it at least once). boolean force = false; if (!alreadyChecked) { preferences.setValue("INTERPRETERS_CHECKED_ONCE", true); //Now we add builtin indexing on our checks (so, force it at least once). force = true; timeout = 1000 * 7; //In this case, wait only 7 seconds after startup } if (force || InterpreterGeneralPreferencesPage.getCheckConsistentOnStartup()) { if (scheduleInitially) { //Only do the initial schedule if there's something to be tracked (otherwise, wait for some interpreter //to be configured and work only on deltas already). //The initial job will do a full check on what's available and if it's synched with the filesystem. job.addAllToTrack(); job.scheduleLater(timeout); //Wait a minute before starting our sync process. } } } public void addToCheck(AbstractInterpreterManager manager, IInterpreterInfo[] infos) { for (IInterpreterInfo info : infos) { job.addToTrack(manager, info); } //Give some seconds for it to start... job.scheduleLater(4 * 1000); } public void checkAllNow() { //Add all to be tracked Map<IInterpreterManager, Map<String, IInterpreterInfo>> addedToTrack = job.addAllToTrack(); //remove from the preferences any ignore the user had set previously Set<Entry<IInterpreterManager, Map<String, IInterpreterInfo>>> entrySet = addedToTrack.entrySet(); IPreferenceStore preferences = PydevPrefs.getPreferences(); for (Entry<IInterpreterManager, Map<String, IInterpreterInfo>> entry : entrySet) { Set<Entry<String, IInterpreterInfo>> entrySet2 = entry.getValue().entrySet(); for (Entry<String, IInterpreterInfo> entry2 : entrySet2) { String key = SynchSystemModulesManager.createKeyForInfo(entry2.getValue()); preferences.setValue(key, ""); } } if (preferences instanceof IPersistentPreferenceStore) { IPersistentPreferenceStore iPersistentPreferenceStore = (IPersistentPreferenceStore) preferences; try { iPersistentPreferenceStore.save(); } catch (IOException e) { Log.log(e); } } //schedule changes to be executed. job.scheduleLater(0); } /** * Stops the synchronization. */ public void stop() { job.cancel(); IInterpreterManager[] managers = PydevPlugin.getAllInterpreterManagers(); synchronized (lockSetInfos) { for (IInterpreterManager iInterpreterManager : managers) { if (iInterpreterManager != null) { stopTrack(iInterpreterManager, pathWatch); } } } pathWatch.dispose(); } private static final class PyFilesFilter implements FileFilter { @Override public boolean accept(File pathname) { //Only consider python files String name = pathname.getName(); return PythonPathHelper.isValidFileMod(name) || name.endsWith(".pth"); } } private static final class PyDirFilter implements FileFilter { @Override public boolean accept(File pathname) { return PythonPathHelper.isFolderWithInit(pathname); } } private static final FileFilter filter = new PyFilesFilter(); private static final FileFilter dirFilter = new PyDirFilter(); private static final class SynchJob extends Job { private SynchJob(String name) { super(name); setPriority(Job.BUILD); } private final SynchSystemModulesManager fSynchManager = new SynchSystemModulesManager(); private Object fManagerToNameToInfoLock = new Object(); private Map<IInterpreterManager, Map<String, IInterpreterInfo>> fManagerToNameToInfo = null; @Override protected IStatus run(IProgressMonitor monitor) { boolean selectingElementsInDialog = fSynchManager.getSelectingElementsInDialog(); if (selectingElementsInDialog) { //No point in starting a process if the user already has a dialog related to this process open. if (SynchSystemModulesManager.DEBUG) { System.out.println("Dialog already showing: rescheduling new check for later."); } this.scheduleLater(20000); return Status.OK_STATUS; } if (SynchSystemModulesManager.DEBUG) { System.out.println("Running SynchJob!"); } if (monitor == null) { monitor = new NullProgressMonitor(); } ManagerInfoToUpdate managerToNameToInfo; synchronized (fManagerToNameToInfoLock) { if (this.fManagerToNameToInfo == null || this.fManagerToNameToInfo.size() == 0) { return Status.OK_STATUS; //nothing to do if there's nothing there... } managerToNameToInfo = new ManagerInfoToUpdate(this.fManagerToNameToInfo); this.fManagerToNameToInfo = null; } long initialTime = System.currentTimeMillis(); ThreadPriorityHelper priorityHelper = new ThreadPriorityHelper(this.getThread()); priorityHelper.setMinPriority(); try { final DataAndImageTreeNode root = new DataAndImageTreeNode(null, null, null); if (monitor.isCanceled()) { return Status.OK_STATUS; } fSynchManager.updateStructures(monitor, root, managerToNameToInfo, new CreateInterpreterInfoCallback()); long delta = System.currentTimeMillis() - initialTime; if (SynchSystemModulesManager.DEBUG) { System.out.println("Time to check polling for changes in interpreters: " + delta / 1000.0 + " secs."); } List<TreeNode> initialSelection = new ArrayList<>(0); if (root.hasChildren()) { initialSelection = fSynchManager.createInitialSelectionForDialogConsideringPreviouslyIgnored(root, PydevPrefs.getPreferences()); } if (root.hasChildren() && initialSelection.size() > 0) { if (SynchSystemModulesManager.DEBUG) { System.out.println("Changes found in PYTHONPATH."); } fSynchManager.asyncSelectAndScheduleElementsToChangePythonpath(root, managerToNameToInfo, initialSelection); } else { if (SynchSystemModulesManager.DEBUG) { System.out.println("PYTHONPATH remained the same."); } fSynchManager.synchronizeManagerToNameToInfoPythonpath(monitor, managerToNameToInfo, null); } } finally { //As jobs are from a thread pool, restore the priority afterwards priorityHelper.restoreInitialPriority(); } return Status.OK_STATUS; } /** * @return a (shallow) copy of the elements added to track. */ public Map<IInterpreterManager, Map<String, IInterpreterInfo>> addAllToTrack() { synchronized (fManagerToNameToInfoLock) { fManagerToNameToInfo = PydevPlugin .getInterpreterManagerToInterpreterNameToInfo(); Map<IInterpreterManager, Map<String, IInterpreterInfo>> copy = new HashMap<IInterpreterManager, Map<String, IInterpreterInfo>>(); Set<Entry<IInterpreterManager, Map<String, IInterpreterInfo>>> entrySet = fManagerToNameToInfo .entrySet(); for (Entry<IInterpreterManager, Map<String, IInterpreterInfo>> entry : entrySet) { HashMap<String, IInterpreterInfo> value = new HashMap<>(entry.getValue()); copy.put(entry.getKey(), value); } return copy; } } public void addToTrack(IInterpreterManager manager, IInterpreterInfo info) { synchronized (fManagerToNameToInfoLock) { if (fManagerToNameToInfo == null) { fManagerToNameToInfo = new HashMap<>(); } Map<String, IInterpreterInfo> map = fManagerToNameToInfo.get(manager); if (map == null) { map = new HashMap<>(); fManagerToNameToInfo.put(manager, map); } map.put(info.getName(), info); } } private Thread scheduleThread; private volatile long runAt; private final Object scheduleThreadLock = new Object(); /** * Differently from the regular schedule, this will create a thread which will * call the actual schedule() only after the given amount of time passes, but if it's * already scheduled, it'll only make it execute after more time passes. */ public void scheduleLater(long millis) { if (SynchSystemModulesManager.DEBUG) { System.out.println("(Re)Scheduling change for: " + millis / 1000.0 + " secs."); } runAt = System.currentTimeMillis() + millis; synchronized (scheduleThreadLock) { if (scheduleThread == null) { scheduleThread = new Thread() { @Override public void run() { try { long currentTimeMillis = System.currentTimeMillis(); long delta = currentTimeMillis - runAt; while (delta < 0) { try { //Don't sleep too much as the time can be changed to lower by a new call. sleep(250); } catch (InterruptedException e) { } currentTimeMillis = System.currentTimeMillis(); delta = currentTimeMillis - runAt; } synchronized (scheduleThreadLock) { if (SynchSystemModulesManager.DEBUG) { System.out.println("Actually schedulling job!"); } SynchJob.this.schedule(); scheduleThread = null; } } catch (Exception e) { Log.log(e); } }; }; scheduleThread.start(); } } } } public static interface IInfoTrackerListener { void onChangedIInterpreterInfo(InfoTracker infoTracker, File file); } /** * Helper class: when a path in the IInterpreterInfo changes some content (or actual path), * it calls a listener to take action (i.e.: validate contents). */ public static final class InfoTracker implements IFilesystemChangesListener { public final IInterpreterInfo info; public final IInterpreterManager manager; public final List<File> filepathsTracked = new ArrayList<>(); private final IInfoTrackerListener listener; public InfoTracker(IInterpreterManager manager, IInterpreterInfo info, IInfoTrackerListener listener) { this.manager = manager; this.info = info; this.listener = listener; } @Override public void added(File file) { //Note: report directly as we should be only listening to what we want with the passed filter. listener.onChangedIInterpreterInfo(this, file); } @Override public void removed(File file) { //Note: report directly as we should be only listening to what we want with the passed filter. listener.onChangedIInterpreterInfo(this, file); } public void registerTracking(File f) { filepathsTracked.add(f); } } private final Map<IInterpreterManager, List<InfoTracker>> managerToPathsTracker = new HashMap<>(); private final IInfoTrackerListener fListener = new IInfoTrackerListener() { @Override public void onChangedIInterpreterInfo(InfoTracker infoTracker, File file) { if (SynchSystemModulesManager.DEBUG) { System.out.println("File changed :" + file + " starting track of: " + infoTracker.info.getNameForUI()); } job.addToTrack(infoTracker.manager, infoTracker.info); if (file.exists() && file.isDirectory()) { //If it's a directory, it may be a copy operation, so, check until the copy finishes (i.e.: //poll for changes and when there are no changes anymore scheduleLater it right away). job.scheduleLater(1000); long lastFound = 0; while (true) { long lastModified = FileUtils.getLastModifiedTimeFromDir(file, filter, dirFilter, EventsStackerRunnable.LEVELS_TO_GET_MODIFIED_TIME); if (lastFound == lastModified) { break; } lastFound = lastModified; //If we don't have a change in the directory structure for 500 millis, stop it. synchronized (this) { try { this.wait(500); } catch (InterruptedException e) { //Ignore } } if (lastFound == 0) { return; //I.e.: found no interesting file. } job.scheduleLater(1000); } } else { job.scheduleLater(5 * 1000); // 5 seconds } } }; private final Object lockSetInfos = new Object(); @Override public void afterSetInfos(IInterpreterManager manager, IInterpreterInfo[] interpreterInfos) { this.afterSetInfos(manager, interpreterInfos, fListener); } public void afterSetInfos(IInterpreterManager manager, IInterpreterInfo[] interpreterInfos, IInfoTrackerListener listener) { synchronized (lockSetInfos) { stopTrack(manager, pathWatch); List<InfoTracker> currTrackers = new ArrayList<>(); managerToPathsTracker.put(manager, currTrackers); for (IInterpreterInfo info : interpreterInfos) { List<String> pythonPath = info.getPythonPath(); InfoTracker tracker = new InfoTracker(manager, info, listener); for (String string : pythonPath) { File f = new File(string); if (SynchSystemModulesManager.DEBUG) { System.out.println("Tracking file: " + f + " for: " + info.getNameForUI()); } tracker.registerTracking(f); pathWatch.track(f, tracker); currTrackers.add(tracker); } } } } /** * Must be synchronized (lockSetInfos). */ private void stopTrack(IInterpreterManager manager, IPathWatch pathWatch) { List<InfoTracker> currTrackers = managerToPathsTracker.remove(manager); if (currTrackers != null) { for (InfoTracker infoTracker : currTrackers) { for (File f : infoTracker.filepathsTracked) { pathWatch.stopTrack(f, infoTracker); } } } } }