/** * Copyright (c) 2012 by JP Moresmau * This code is made available under the terms of the Eclipse Public License, * version 1.0 (EPL). See http://www.eclipse.org/legal/epl-v10.html */ package net.sf.eclipsefp.haskell.buildwrapper; import java.io.File; import java.io.Writer; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import net.sf.eclipsefp.haskell.buildwrapper.types.BuildFlags; import net.sf.eclipsefp.haskell.buildwrapper.types.CabalImplDetails; import net.sf.eclipsefp.haskell.buildwrapper.usage.UsageAPI; import net.sf.eclipsefp.haskell.buildwrapper.usage.UsageThread; import net.sf.eclipsefp.haskell.buildwrapper.util.BWText; import net.sf.eclipsefp.haskell.util.FileUtil; import net.sf.eclipsefp.haskell.util.OutputWriter; import net.sf.eclipsefp.haskell.util.SingleJobQueue; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.osgi.util.NLS; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.eclipse.ui.statushandlers.StatusManager; import org.osgi.framework.BundleContext; /** * The plugin class for buildwrapper operations, providing utility methods and ways to obtain a Build Wrapper Facade for a given Haskell project * @author JPMoresmau */ public class BuildWrapperPlugin extends AbstractUIPlugin { final public static String PROBLEM_MARKER_ID = "net.sf.eclipsefp.haskell.core.problem"; // The plug-in ID public static final String PLUGIN_ID = "net.sf.eclipsefp.haskell.buildwrapper"; public static QualifiedName USERFLAGS_PROPERTY=new QualifiedName("EclipseFP", "UserFlags"); public static QualifiedName EXTRAOPTS_PROPERTY=new QualifiedName("EclipseFP", "ExtraOpts"); public static QualifiedName EDITORSTANZA_PROPERTY=new QualifiedName("EclipseFP", "EditorStanza"); // The shared instance private static BuildWrapperPlugin plugin; private static Map<IProject, BWFacade> facades=new HashMap<>(); private static Set<IProject> addSourceProjects = new HashSet<>(); private static String bwPath; private static int maxConfigureFailures=10; private static int maxEvalTime=30; public static boolean logAnswers=false; private UsageAPI usageAPI; private UsageThread usageThread=new UsageThread(); private IResourceChangeListener sandboxListener=new SandboxHelper.ProjectReferencesChangeListener(); private IResourceChangeListener nonHaskellListener=new NonHaskellResourceChangeListener(); private IResourceChangeListener preDeleteListener = new IResourceChangeListener() { @Override public void resourceChanged(IResourceChangeEvent event) { IProject project = (IProject) event.getResource(); // close all processes to prevent file-locking issues BWFacade f=getFacade(project); if (f!=null){ f.closeAllProcesses(); } } }; /** * The constructor */ public BuildWrapperPlugin() { } /* * (non-Javadoc) * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) */ @Override public void start(BundleContext context) throws Exception { super.start(context); plugin = this; usageThread.start(); ResourcesPlugin.getWorkspace().addResourceChangeListener(sandboxListener,IResourceChangeEvent.POST_CHANGE); ResourcesPlugin.getWorkspace().addResourceChangeListener(preDeleteListener, IResourceChangeEvent.PRE_DELETE); ResourcesPlugin.getWorkspace().addResourceChangeListener(nonHaskellListener,IResourceChangeEvent.POST_CHANGE); } /** * @param usageAPI the usageAPI to set */ public void setUsageAPI(UsageAPI usageAPI) { this.usageAPI = usageAPI; } /* * (non-Javadoc) * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) */ @Override public void stop(BundleContext context) throws Exception { plugin = null; ResourcesPlugin.getWorkspace().removeResourceChangeListener(sandboxListener); ResourcesPlugin.getWorkspace().removeResourceChangeListener(preDeleteListener); ResourcesPlugin.getWorkspace().removeResourceChangeListener(nonHaskellListener); // facades are removed and stopped by ScionManager usageThread.setShouldStop(); // wait for all pending writes for 10 secs usageThread.join(10000); // then close api and db usageAPI.close(); super.stop(context); } /** * Returns the shared instance * * @return the shared instance */ public static BuildWrapperPlugin getDefault() { return plugin; } /** * @return the usageAPI */ public UsageAPI getUsageAPI() { return usageAPI; } /** * @return the usageThread */ public UsageThread getUsageThread() { return usageThread; } /** * create a facade * @param p the project * @param impl the cabal invocations details * @param outStream the writer to log to * @return the created facade if the project has a cabal file, null otherwise */ public static BWFacade createFacade(IProject p,CabalImplDetails impl,Writer outStream){ IFile cf=getCabalFile(p); if (cf!=null){ BWFacade f=new BWFacade(); f.setBwPath(bwPath); f.setCabalImplDetails(impl); f.setCabalFile(cf.getLocation().toOSString()); f.setWorkingDir(new File(p.getLocation().toOSString())); f.setProject(p); f.setOutStream(outStream); facades.put(p, f); // why? build will do that for us // well if we don't build automatically we DO need it! if (!ResourcesPlugin.getWorkspace().isAutoBuilding()){ new JobFacade(f).synchronize(false); } return f; } return null; } /** * set cabal impl details on all known facades * @param impl */ public static void setCabalImplDetails(CabalImplDetails impl){ for (BWFacade f:facades.values()){ f.setCabalImplDetails(impl); } } public static BWFacade getFacade(IProject p){ return facades.get(p); } public static BWFacade removeFacade(IProject p){ BWFacade f=facades.remove(p); if (f!=null){ OutputWriter ow=f.getOutputWriter(); if (ow!=null){ ow.setTerminate(); } f.getBuildJobQueue().close(); f.getSynchronizeJobQueue().close(); for (SingleJobQueue q:f.getThingAtPointJobQueues()){ q.close(); } for (SingleJobQueue q:f.getEditorSynchronizeJobQueues()){ q.close(); } f.closeAllProcesses(); } return f; } public static JobFacade getJobFacade(IProject p){ BWFacade realF=getFacade(p); if (realF!=null){ return new JobFacade(realF); } return null; } public static WorkspaceFacade getWorkspaceFacade(IProject p,IProgressMonitor monitor){ BWFacade realF=getFacade(p); if (realF!=null){ return new WorkspaceFacade(realF,monitor); } return null; } public static void logInfo(String message) { log(IStatus.INFO, message, null); } public static void logDebug(String message) { // log(Status.INFO, message, null); } public static void logWarning(String message, Throwable cause) { log(IStatus.WARNING, message, cause); } public static void logError(String message, Throwable cause) { log(IStatus.ERROR, message, cause); } public static void log(int severity, String message, Throwable cause) { Status status = new Status(severity, BuildWrapperPlugin.PLUGIN_ID, severity, message, cause); logStatus(status); } public static void logStatus(IStatus status) { StatusManager.getManager().handle(status); } /** * Delete all problem markers for a given file. * * @param r A resource that could be a file or a project. */ public static void deleteProblems(IResource r) { deleteProblems(r, IResource.DEPTH_ZERO); } private static void deleteProblems(IResource r,int depth){ if (!r.getWorkspace().isTreeLocked() && r.exists() && r.getProject().isOpen()) { try { // if (r instanceof IFile) { // r.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); // } //org.eclipse.core.resources.problemmarker r.deleteMarkers(PROBLEM_MARKER_ID, true, depth); r.deleteMarkers("net.sf.eclipsefp.haskell.scion.client.ScionPlugin.projectProblem", true, depth); r.deleteMarkers("net.sf.eclipsefp.haskell.core.scionProblem", true,depth ); r.deleteMarkers(IMarker.PROBLEM, false, depth); // delete problems but not subtypes (HLint, etc are not managed by us) } catch (CoreException ex) { BuildWrapperPlugin.logError(BWText.error_deleteMarkers, ex); ex.printStackTrace(); } } } public static void deleteAllProblems(IProject p) { deleteProblems(p, IResource.DEPTH_INFINITE); } /** * cache project cabal file if cabal file doesn't have same name than project */ private static Map<IProject,IFile> cabalFileCache=new HashMap<>(); /** * get the cabal file path * this is similar to getCabalFile below but does not cache the result and works with Path * @param projectPath * @param name * @return */ public static IPath getCabalFile(final IPath projectPath, final String name) { IPath f=projectPath.append(name).addFileExtension( FileUtil.EXTENSION_CABAL) ; if (f==null || !f.toFile().exists()){ // oh oh // find a cabal file File[] children=projectPath.toFile().listFiles(); int cnt=0; for (File child:children){ if (child.isFile() && !child.isHidden()){ IPath pchild=projectPath.append(child.getName()); if ( pchild.getFileExtension() != null && pchild.getFileExtension().equalsIgnoreCase(FileUtil.EXTENSION_CABAL)){ f=pchild; cnt++; } } } // cnt=1 would mean only one file, we can live with that if (cnt!=1){ f=null; } } return f; } /** * Generate the Cabal project file's name from the Eclipse project's name. * * @param project The Eclipse project * @return The "<project>.cabal" string. */ public static IFile getCabalFile(final IProject project) { IFile f=project.getFile(new Path(project.getName()).addFileExtension(FileUtil.EXTENSION_CABAL)); if (f==null || !f.exists()){ // oh oh IFile f2=cabalFileCache.get(project); if (f2==null || !f2.exists()){ try { // find a cabal file IResource[] children=project.members(); int cnt=0; for (IResource child:children){ if (child instanceof IFile){ if ( child.getFileExtension() != null && child.getFileExtension().equalsIgnoreCase(FileUtil.EXTENSION_CABAL)){ f=(IFile)child; cnt++; } } } // cnt=1 would mean only one file, we can live with that if (cnt>1){ // log error, we've taken a random cabal file logError(NLS.bind(BWText.project_cabal_duplicate, project.getName()),null); } cabalFileCache.put(project, f); } catch (CoreException ce){ logError(NLS.bind(BWText.project_members_list_error, project.getName()), ce); } } else { f=f2; } } return f; } public static String getBwPath() { return bwPath; } public static void setBwPath(String bwPath) { BuildWrapperPlugin.bwPath = bwPath; for (BWFacade f:facades.values()){ if (f!=null){ f.setBwPath(bwPath); } } } public static int getMaxConfigureFailures() { return maxConfigureFailures; } public static void setMaxConfigureFailures(int maxConfigureFailures) { BuildWrapperPlugin.maxConfigureFailures = maxConfigureFailures; } /** * get location of cabal-dev global sandbox * @return */ public static IPath getDefaultUniqueCabalDevSandboxLocation(){ return getDefault().getStateLocation().append( ".cabal-dev" ); } /** * get location of cabal global sandbox * @return */ public static IPath getDefaultUniqueCabalSandboxLocation(){ return getDefault().getStateLocation().append( "sandbox" ); } public static int getMaxEvalTime() { return maxEvalTime; } public static void setMaxEvalTime(int maxEvalTime) { BuildWrapperPlugin.maxEvalTime = maxEvalTime; } /** * get extensions used in a file * @param file * @return */ public static Set<String> getExtensions(IFile file){ IProject p=file.getProject(); BWFacade bwf=BuildWrapperPlugin.getFacade( p ); Set<String> extensions=new HashSet<>(); if (bwf !=null){ BuildFlags bf=bwf.getBuildFlags( file ); if (bf!=null){ for (String s:bf.getGhcFlags()){ if (s.startsWith( "-X" )){ extensions.add(s.substring( 2 )); } } } } return extensions; } /** * @return the addSourceProjects */ public static Set<IProject> getAddSourceProjects() { return addSourceProjects; } }