/* * Copyright 2000-2012 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.compiler.impl; import com.intellij.ProjectTopics; import com.intellij.compiler.CompilerIOUtil; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.compiler.*; import com.intellij.openapi.compiler.ex.CompileContextEx; import com.intellij.openapi.components.ApplicationComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.project.ProjectManagerAdapter; import com.intellij.openapi.roots.*; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.*; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.*; import com.intellij.openapi.vfs.newvfs.FileAttribute; import com.intellij.openapi.vfs.newvfs.ManagingFS; import com.intellij.openapi.vfs.newvfs.NewVirtualFile; import com.intellij.openapi.vfs.newvfs.persistent.FSRecords; import com.intellij.util.Alarm; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.SLRUCache; import com.intellij.util.indexing.FileBasedIndex; import com.intellij.util.io.*; import com.intellij.util.io.DataOutputStream; import com.intellij.util.messages.MessageBusConnection; import consulo.compiler.ModuleCompilerPathsManager; import consulo.compiler.impl.TranslatingCompilerFilesMonitor; import consulo.compiler.impl.TranslatingCompilerFilesMonitorHelper; import consulo.compiler.make.DependencyCache; import consulo.compiler.server.BuildManager; import consulo.module.extension.ModuleExtension; import consulo.module.extension.ModuleExtensionChangeListener; import consulo.roots.ContentFolderScopes; import consulo.roots.impl.ProductionContentFolderTypeProvider; import consulo.roots.impl.TestContentFolderTypeProvider; import gnu.trove.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.*; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; /** * @author Eugene Zhuravlev * @since Jun 3, 2008 * <p> * A source file is scheduled for recompilation if * 1. its timestamp has changed * 2. one of its corresponding output files was deleted * 3. output root of containing module has changed * <p> * An output file is scheduled for deletion if: * 1. corresponding source file has been scheduled for recompilation (see above) * 2. corresponding source file has been deleted */ public class TranslatingCompilerFilesMonitorImpl extends TranslatingCompilerFilesMonitor implements ApplicationComponent { private static final Logger LOG = Logger.getInstance("#consulo.compiler.impl.TranslatingCompilerFilesMonitor"); private static final boolean ourDebugMode = false; private static final FileAttribute ourSourceFileAttribute = new FileAttribute("_make_source_file_info_", 3); private static final FileAttribute ourOutputFileAttribute = new FileAttribute("_make_output_file_info_", 3); private static final Key<Map<String, VirtualFile>> SOURCE_FILES_CACHE = Key.create("_source_url_to_vfile_cache_"); private final Object myDataLock = new Object(); private final TIntHashSet mySuspendedProjects = new TIntHashSet(); // projectId for all projects that should not be monitored private final TIntObjectHashMap<TIntHashSet> mySourcesToRecompile = new TIntObjectHashMap<TIntHashSet>(); // ProjectId->set of source file paths private PersistentHashMap<Integer, TIntObjectHashMap<Pair<Integer, Integer>>> myOutputRootsStorage; // ProjectId->map[moduleId->Pair(outputDirId, testOutputDirId)] // Map: projectId -> Map{output path -> [sourceUrl; className]} private final SLRUCache<Integer, Outputs> myOutputsToDelete = new SLRUCache<Integer, Outputs>(3, 3) { @Override public Outputs getIfCached(Integer key) { final Outputs value = super.getIfCached(key); if (value != null) { value.allocate(); } return value; } @NotNull @Override public Outputs get(Integer key) { final Outputs value = super.get(key); value.allocate(); return value; } @NotNull @Override public Outputs createValue(Integer key) { try { final String dirName = FSRecords.getNames().valueOf(key); final File storeFile; if (StringUtil.isEmpty(dirName)) { storeFile = null; } else { final File compilerCacheDir = CompilerPaths.getCacheStoreDirectory(dirName); storeFile = compilerCacheDir.exists() ? new File(compilerCacheDir, "paths_to_delete.dat") : null; } return new Outputs(storeFile, loadPathsToDelete(storeFile)); } catch (IOException e) { LOG.info(e); return new Outputs(null, new HashMap<String, SourceUrlClassNamePair>()); } } @Override protected void onDropFromCache(Integer key, Outputs value) { value.release(); } }; private final SLRUCache<Project, File> myGeneratedDataPaths = new SLRUCache<Project, File>(8, 8) { @Override @NotNull public File createValue(final Project project) { Disposer.register(project, new Disposable() { @Override public void dispose() { myGeneratedDataPaths.remove(project); } }); return CompilerPaths.getGeneratedDataDirectory(project); } }; private final SLRUCache<Integer, TIntObjectHashMap<Pair<Integer, Integer>>> myProjectOutputRoots = new SLRUCache<Integer, TIntObjectHashMap<Pair<Integer, Integer>>>(2, 2) { @Override protected void onDropFromCache(Integer key, TIntObjectHashMap<Pair<Integer, Integer>> value) { try { myOutputRootsStorage.put(key, value); } catch (IOException e) { LOG.info(e); } } @Override @NotNull public TIntObjectHashMap<Pair<Integer, Integer>> createValue(Integer key) { TIntObjectHashMap<Pair<Integer, Integer>> map = null; try { ensureOutputStorageInitialized(); map = myOutputRootsStorage.get(key); } catch (IOException e) { LOG.info(e); } return map != null ? map : new TIntObjectHashMap<Pair<Integer, Integer>>(); } }; private final ProjectManager myProjectManager; private final TIntIntHashMap myInitInProgress = new TIntIntHashMap(); // projectId for successfully initialized projects private final Object myAsyncScanLock = new Object(); private boolean myForceCompiling; public TranslatingCompilerFilesMonitorImpl(VirtualFileManager vfsManager, ProjectManager projectManager, Application application) { myProjectManager = projectManager; projectManager.addProjectManagerListener(new MyProjectManagerListener()); vfsManager.addVirtualFileListener(new MyVfsListener(), application); } @Override public void suspendProject(Project project) { final int projectId = getProjectId(project); synchronized (myDataLock) { if (!mySuspendedProjects.add(projectId)) { return; } FileUtil.createIfDoesntExist(CompilerPaths.getRebuildMarkerFile(project)); // cleanup internal structures to free memory mySourcesToRecompile.remove(projectId); myOutputsToDelete.remove(projectId); myGeneratedDataPaths.remove(project); } synchronized (myProjectOutputRoots) { ensureOutputStorageInitialized(); myProjectOutputRoots.remove(projectId); try { myOutputRootsStorage.remove(projectId); } catch (IOException e) { LOG.info(e); } } } @Override public void watchProject(Project project) { synchronized (myDataLock) { mySuspendedProjects.remove(getProjectId(project)); } } @Override public boolean isSuspended(Project project) { return isSuspended(getProjectId(project)); } @Override public boolean isSuspended(int projectId) { synchronized (myDataLock) { return mySuspendedProjects.contains(projectId); } } @Nullable public static VirtualFile getSourceFileByOutput(VirtualFile outputFile) { final OutputFileInfo outputFileInfo = loadOutputInfo(outputFile); if (outputFileInfo != null) { final String path = outputFileInfo.getSourceFilePath(); if (path != null) { return LocalFileSystem.getInstance().findFileByPath(path); } } return null; } @Override public void collectFiles(CompileContext context, final TranslatingCompiler compiler, Iterator<VirtualFile> scopeSrcIterator, boolean forceCompile, final boolean isRebuild, Collection<VirtualFile> toCompile, Collection<Trinity<File, String, Boolean>> toDelete) { final Project project = context.getProject(); final int projectId = getProjectId(project); final CompilerManager configuration = CompilerManager.getInstance(project); final boolean _forceCompile = forceCompile || isRebuild || myForceCompiling; final Set<VirtualFile> selectedForRecompilation = new HashSet<VirtualFile>(); synchronized (myDataLock) { final TIntHashSet pathsToRecompile = mySourcesToRecompile.get(projectId); if (_forceCompile || pathsToRecompile != null && !pathsToRecompile.isEmpty()) { if (ourDebugMode) { System.out.println("Analysing potentially recompilable files for " + compiler.getDescription()); } while (scopeSrcIterator.hasNext()) { final VirtualFile file = scopeSrcIterator.next(); if (!file.isValid()) { if (LOG.isDebugEnabled() || ourDebugMode) { LOG.debug("Skipping invalid file " + file.getPresentableUrl()); if (ourDebugMode) { System.out.println("\t SKIPPED(INVALID) " + file.getPresentableUrl()); } } continue; } final int fileId = getFileId(file); if (_forceCompile) { if (compiler.isCompilableFile(file, context) && !configuration.isExcludedFromCompilation(file)) { toCompile.add(file); if (ourDebugMode) { System.out.println("\t INCLUDED " + file.getPresentableUrl()); } selectedForRecompilation.add(file); if (pathsToRecompile == null || !pathsToRecompile.contains(fileId)) { loadInfoAndAddSourceForRecompilation(projectId, file); } } else { if (ourDebugMode) { System.out.println("\t NOT COMPILABLE OR EXCLUDED " + file.getPresentableUrl()); } } } else if (pathsToRecompile.contains(fileId)) { if (compiler.isCompilableFile(file, context) && !configuration.isExcludedFromCompilation(file)) { toCompile.add(file); if (ourDebugMode) { System.out.println("\t INCLUDED " + file.getPresentableUrl()); } selectedForRecompilation.add(file); } else { if (ourDebugMode) { System.out.println("\t NOT COMPILABLE OR EXCLUDED " + file.getPresentableUrl()); } } } else { if (ourDebugMode) { System.out.println("\t NOT INCLUDED " + file.getPresentableUrl()); } } } } // it is important that files to delete are collected after the files to compile (see what happens if forceCompile == true) if (!isRebuild) { final Outputs outputs = myOutputsToDelete.get(projectId); try { final VirtualFileManager vfm = VirtualFileManager.getInstance(); final LocalFileSystem lfs = LocalFileSystem.getInstance(); final List<String> zombieEntries = new ArrayList<String>(); final Map<String, VirtualFile> srcFileCache = getFileCache(context); for (Map.Entry<String, SourceUrlClassNamePair> entry : outputs.getEntries()) { final String outputPath = entry.getKey(); final SourceUrlClassNamePair classNamePair = entry.getValue(); final String sourceUrl = classNamePair.getSourceUrl(); final VirtualFile srcFile; if (srcFileCache.containsKey(sourceUrl)) { srcFile = srcFileCache.get(sourceUrl); } else { srcFile = vfm.findFileByUrl(sourceUrl); srcFileCache.put(sourceUrl, srcFile); } final boolean sourcePresent = srcFile != null; if (sourcePresent) { if (!compiler.isCompilableFile(srcFile, context)) { continue; // do not collect files that were compiled by another compiler } if (!selectedForRecompilation.contains(srcFile)) { if (!isMarkedForRecompilation(projectId, getFileId(srcFile))) { if (LOG.isDebugEnabled() || ourDebugMode) { final String message = "Found zombie entry (output is marked, but source is present and up-to-date): " + outputPath; LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } zombieEntries.add(outputPath); } continue; } } if (lfs.findFileByPath(outputPath) != null) { //noinspection UnnecessaryBoxing final File file = new File(outputPath); toDelete.add(new Trinity<File, String, Boolean>(file, classNamePair.getClassName(), Boolean.valueOf(sourcePresent))); if (LOG.isDebugEnabled() || ourDebugMode) { final String message = "Found file to delete: " + file; LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } } else { if (LOG.isDebugEnabled() || ourDebugMode) { final String message = "Found zombie entry marked for deletion: " + outputPath; LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } // must be gagbage entry, should cleanup zombieEntries.add(outputPath); } } for (String path : zombieEntries) { unmarkOutputPathForDeletion(projectId, path); } } finally { outputs.release(); } } } } private static Map<String, VirtualFile> getFileCache(CompileContext context) { Map<String, VirtualFile> cache = context.getUserData(SOURCE_FILES_CACHE); if (cache == null) { context.putUserData(SOURCE_FILES_CACHE, cache = new HashMap<String, VirtualFile>()); } return cache; } private static int getFileId(final VirtualFile file) { return FileBasedIndex.getFileId(file); } private static VirtualFile findFileById(int id) { return ManagingFS.getInstance().findFileById(id); } @Override public void update(final CompileContext context, @Nullable final String outputRoot, final Collection<TranslatingCompiler.OutputItem> successfullyCompiled, final VirtualFile[] filesToRecompile) throws IOException { myForceCompiling = false; final Project project = context.getProject(); final DependencyCache dependencyCache = ((CompileContextEx)context).getDependencyCache(); final int projectId = getProjectId(project); if (!successfullyCompiled.isEmpty()) { final LocalFileSystem lfs = LocalFileSystem.getInstance(); final IOException[] exceptions = {null}; // need read action here to ensure that no modifications were made to VFS while updating file attributes ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { try { final Map<VirtualFile, SourceFileInfo> compiledSources = new HashMap<VirtualFile, SourceFileInfo>(); final Set<VirtualFile> forceRecompile = new HashSet<VirtualFile>(); for (TranslatingCompiler.OutputItem item : successfullyCompiled) { final VirtualFile sourceFile = item.getSourceFile(); final boolean isSourceValid = sourceFile.isValid(); SourceFileInfo srcInfo = compiledSources.get(sourceFile); if (isSourceValid && srcInfo == null) { srcInfo = loadSourceInfo(sourceFile); if (srcInfo != null) { srcInfo.clearPaths(projectId); } else { srcInfo = new SourceFileInfo(); } compiledSources.put(sourceFile, srcInfo); } final String outputPath = item.getOutputPath(); if (outputPath != null) { // can be null for packageinfo final VirtualFile outputFile = lfs.findFileByPath(outputPath); //assert outputFile != null : "Virtual file was not found for \"" + outputPath + "\""; if (outputFile != null) { if (!sourceFile.equals(outputFile)) { final String className = outputRoot == null ? null : dependencyCache.relativePathToQName(outputPath.substring(outputRoot.length()), '/'); if (isSourceValid) { srcInfo.addOutputPath(projectId, outputPath); saveOutputInfo(outputFile, new OutputFileInfo(sourceFile.getPath(), className)); } else { markOutputPathForDeletion(projectId, outputPath, className, sourceFile.getUrl()); } } } else { // output file was not found LOG.warn("TranslatingCompilerFilesMonitor.update(): Virtual file was not found for \"" + outputPath + "\""); if (isSourceValid) { forceRecompile.add(sourceFile); } } } } final long compilationStartStamp = ((CompileContextEx)context).getStartCompilationStamp(); for (Map.Entry<VirtualFile, SourceFileInfo> entry : compiledSources.entrySet()) { final SourceFileInfo info = entry.getValue(); final VirtualFile file = entry.getKey(); final long fileStamp = file.getTimeStamp(); info.updateTimestamp(projectId, fileStamp); saveSourceInfo(file, info); if (LOG.isDebugEnabled() || ourDebugMode) { final String message = "Unschedule recompilation (successfully compiled) " + file.getPresentableUrl(); LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } removeSourceForRecompilation(projectId, Math.abs(getFileId(file))); if (fileStamp > compilationStartStamp && !((CompileContextEx)context).isGenerated(file) || forceRecompile.contains(file)) { // changes were made during compilation, need to re-schedule compilation // it is important to invoke removeSourceForRecompilation() before this call to make sure // the corresponding output paths will be scheduled for deletion addSourceForRecompilation(projectId, file, info); } } } catch (IOException e) { exceptions[0] = e; } } }); if (exceptions[0] != null) { throw exceptions[0]; } } if (filesToRecompile.length > 0) { ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { for (VirtualFile file : filesToRecompile) { if (file.isValid()) { loadInfoAndAddSourceForRecompilation(projectId, file); } } } }); } } @Override public void updateOutputRootsLayout(Project project) { final TIntObjectHashMap<Pair<Integer, Integer>> map = buildOutputRootsLayout(new ProjectRef(project)); final int projectId = getProjectId(project); synchronized (myProjectOutputRoots) { myProjectOutputRoots.put(projectId, map); } } @Override @NotNull public String getComponentName() { return "TranslatingCompilerFilesMonitor"; } @Override public void initComponent() { ensureOutputStorageInitialized(); } private static File getOutputRootsFile() { return new File(CompilerPaths.getCompilerSystemDirectory(), "output_roots.dat"); } private static void deleteStorageFiles(File tableFile) { final File[] files = tableFile.getParentFile().listFiles(); if (files != null) { final String name = tableFile.getName(); for (File file : files) { if (file.getName().startsWith(name)) { FileUtil.delete(file); } } } } private static Map<String, SourceUrlClassNamePair> loadPathsToDelete(@Nullable final File file) { final Map<String, SourceUrlClassNamePair> map = new HashMap<String, SourceUrlClassNamePair>(); try { if (file != null && file.length() > 0) { final DataInputStream is = new DataInputStream(new BufferedInputStream(new FileInputStream(file))); try { final int size = is.readInt(); for (int i = 0; i < size; i++) { final String _outputPath = CompilerIOUtil.readString(is); final String srcUrl = CompilerIOUtil.readString(is); final String className = CompilerIOUtil.readString(is); map.put(FileUtil.toSystemIndependentName(_outputPath), new SourceUrlClassNamePair(srcUrl, className)); } } finally { is.close(); } } } catch (FileNotFoundException ignored) { } catch (IOException e) { LOG.info(e); } return map; } private void ensureOutputStorageInitialized() { if (myOutputRootsStorage != null) { return; } final File rootsFile = getOutputRootsFile(); try { initOutputRootsFile(rootsFile); } catch (IOException e) { LOG.info(e); deleteStorageFiles(rootsFile); try { initOutputRootsFile(rootsFile); } catch (IOException e1) { LOG.error(e1); } } } private TIntObjectHashMap<Pair<Integer, Integer>> buildOutputRootsLayout(ProjectRef projRef) { final TIntObjectHashMap<Pair<Integer, Integer>> map = new TIntObjectHashMap<Pair<Integer, Integer>>(); for (Module module : ModuleManager.getInstance(projRef.get()).getModules()) { ModuleCompilerPathsManager moduleCompilerPathsManager = ModuleCompilerPathsManager.getInstance(module); final VirtualFile output = moduleCompilerPathsManager.getCompilerOutput(ProductionContentFolderTypeProvider.getInstance()); final int first = output != null ? Math.abs(getFileId(output)) : -1; final VirtualFile testsOutput = moduleCompilerPathsManager.getCompilerOutput(TestContentFolderTypeProvider.getInstance()); final int second = testsOutput != null ? Math.abs(getFileId(testsOutput)) : -1; map.put(getModuleId(module), new Pair<Integer, Integer>(first, second)); } return map; } private void initOutputRootsFile(File rootsFile) throws IOException { myOutputRootsStorage = new PersistentHashMap<Integer, TIntObjectHashMap<Pair<Integer, Integer>>>(rootsFile, EnumeratorIntegerDescriptor.INSTANCE, new DataExternalizer<TIntObjectHashMap<Pair<Integer, Integer>>>() { @Override public void save(DataOutput out, TIntObjectHashMap<Pair<Integer, Integer>> value) throws IOException { for (final TIntObjectIterator<Pair<Integer, Integer>> it = value.iterator(); it.hasNext(); ) { it.advance(); DataInputOutputUtil.writeINT(out, it.key()); final Pair<Integer, Integer> pair = it.value(); DataInputOutputUtil.writeINT(out, pair.first); DataInputOutputUtil.writeINT(out, pair.second); } } @Override public TIntObjectHashMap<Pair<Integer, Integer>> read( DataInput in) throws IOException { final DataInputStream _in = (DataInputStream)in; final TIntObjectHashMap<Pair<Integer, Integer>> map = new TIntObjectHashMap<Pair<Integer, Integer>>(); while (_in.available() > 0) { final int key = DataInputOutputUtil.readINT(_in); final int first = DataInputOutputUtil.readINT(_in); final int second = DataInputOutputUtil.readINT(_in); map.put(key, new Pair<Integer, Integer>(first, second)); } return map; } }); } @Override public void disposeComponent() { try { synchronized (myProjectOutputRoots) { myProjectOutputRoots.clear(); } } finally { synchronized (myDataLock) { myOutputsToDelete.clear(); } } try { final PersistentHashMap<Integer, TIntObjectHashMap<Pair<Integer, Integer>>> storage = myOutputRootsStorage; if (storage != null) { storage.close(); } } catch (IOException e) { LOG.info(e); deleteStorageFiles(getOutputRootsFile()); } } private static void savePathsToDelete(final File file, final Map<String, SourceUrlClassNamePair> outputs) { try { FileUtil.createParentDirs(file); final DataOutputStream os = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file))); try { if (outputs != null) { os.writeInt(outputs.size()); for (Map.Entry<String, SourceUrlClassNamePair> entry : outputs.entrySet()) { CompilerIOUtil.writeString(entry.getKey(), os); final SourceUrlClassNamePair pair = entry.getValue(); CompilerIOUtil.writeString(pair.getSourceUrl(), os); CompilerIOUtil.writeString(pair.getClassName(), os); } } else { os.writeInt(0); } } finally { os.close(); } } catch (IOException e) { LOG.error(e); } } @Nullable private static SourceFileInfo loadSourceInfo(final VirtualFile file) { try { final DataInputStream is = ourSourceFileAttribute.readAttribute(file); if (is != null) { try { return new SourceFileInfo(is); } finally { is.close(); } } } catch (RuntimeException e) { final Throwable cause = e.getCause(); if (cause instanceof IOException) { LOG.info(e); // ignore IOExceptions } else { throw e; } } catch (IOException ignored) { LOG.info(ignored); } return null; } public static void removeSourceInfo(VirtualFile file) { saveSourceInfo(file, new SourceFileInfo()); } private static void saveSourceInfo(VirtualFile file, SourceFileInfo descriptor) { final java.io.DataOutputStream out = ourSourceFileAttribute.writeAttribute(file); try { try { descriptor.save(out); } finally { out.close(); } } catch (IOException ignored) { LOG.info(ignored); } } @Nullable private static OutputFileInfo loadOutputInfo(final VirtualFile file) { try { final DataInputStream is = ourOutputFileAttribute.readAttribute(file); if (is != null) { try { return new OutputFileInfo(is); } finally { is.close(); } } } catch (RuntimeException e) { final Throwable cause = e.getCause(); if (cause instanceof IOException) { LOG.info(e); // ignore IO exceptions } else { throw e; } } catch (IOException ignored) { LOG.info(ignored); } return null; } private static void saveOutputInfo(VirtualFile file, OutputFileInfo descriptor) { final java.io.DataOutputStream out = ourOutputFileAttribute.writeAttribute(file); try { try { descriptor.save(out); } finally { out.close(); } } catch (IOException ignored) { LOG.info(ignored); } } private int getProjectId(Project project) { try { return FSRecords.getNames().enumerate(CompilerPaths.getCompilerSystemDirectoryName(project)); } catch (IOException e) { LOG.info(e); } return -1; } private int getModuleId(Module module) { try { return FSRecords.getNames().enumerate(module.getName().toLowerCase(Locale.US)); } catch (IOException e) { LOG.info(e); } return -1; } private static class OutputFileInfo { private final int mySourcePath; private final int myClassName; OutputFileInfo(final String sourcePath, @Nullable String className) throws IOException { final PersistentStringEnumerator symtable = FSRecords.getNames(); mySourcePath = symtable.enumerate(sourcePath); myClassName = className != null ? symtable.enumerate(className) : -1; } OutputFileInfo(final DataInput in) throws IOException { mySourcePath = in.readInt(); myClassName = in.readInt(); } String getSourceFilePath() { try { return FSRecords.getNames().valueOf(mySourcePath); } catch (IOException e) { LOG.info(e); } return null; } @Nullable public String getClassName() { try { return myClassName < 0 ? null : FSRecords.getNames().valueOf(myClassName); } catch (IOException e) { LOG.info(e); } return null; } public void save(final DataOutput out) throws IOException { out.writeInt(mySourcePath); out.writeInt(myClassName); } } private static class SourceFileInfo { private TIntLongHashMap myTimestamps; // ProjectId -> last compiled stamp private TIntObjectHashMap<Serializable> myProjectToOutputPathMap; // ProjectId -> either a single output path or a set of output paths private SourceFileInfo() { } private SourceFileInfo(@NotNull DataInput in) throws IOException { final int projCount = DataInputOutputUtil.readINT(in); for (int idx = 0; idx < projCount; idx++) { final int projectId = DataInputOutputUtil.readINT(in); final long stamp = DataInputOutputUtil.readTIME(in); updateTimestamp(projectId, stamp); final int pathsCount = DataInputOutputUtil.readINT(in); for (int i = 0; i < pathsCount; i++) { final int path = in.readInt(); addOutputPath(projectId, path); } } } public void save(@NotNull final DataOutput out) throws IOException { final int[] projects = getProjectIds().toArray(); DataInputOutputUtil.writeINT(out, projects.length); for (int projectId : projects) { DataInputOutputUtil.writeINT(out, projectId); DataInputOutputUtil.writeTIME(out, getTimestamp(projectId)); final Object value = myProjectToOutputPathMap != null ? myProjectToOutputPathMap.get(projectId) : null; if (value instanceof Integer) { DataInputOutputUtil.writeINT(out, 1); out.writeInt(((Integer)value).intValue()); } else if (value instanceof TIntHashSet) { final TIntHashSet set = (TIntHashSet)value; DataInputOutputUtil.writeINT(out, set.size()); final IOException[] ex = new IOException[]{null}; set.forEach(new TIntProcedure() { @Override public boolean execute(final int value) { try { out.writeInt(value); return true; } catch (IOException e) { ex[0] = e; return false; } } }); if (ex[0] != null) { throw ex[0]; } } else { DataInputOutputUtil.writeINT(out, 0); } } } private void updateTimestamp(final int projectId, final long stamp) { if (stamp > 0L) { if (myTimestamps == null) { myTimestamps = new TIntLongHashMap(1, 0.98f); } myTimestamps.put(projectId, stamp); } else { if (myTimestamps != null) { myTimestamps.remove(projectId); } } } TIntHashSet getProjectIds() { final TIntHashSet result = new TIntHashSet(); if (myTimestamps != null) { result.addAll(myTimestamps.keys()); } if (myProjectToOutputPathMap != null) { result.addAll(myProjectToOutputPathMap.keys()); } return result; } private void addOutputPath(final int projectId, String outputPath) { try { addOutputPath(projectId, FSRecords.getNames().enumerate(outputPath)); } catch (IOException e) { LOG.info(e); } } private void addOutputPath(final int projectId, final int outputPath) { if (myProjectToOutputPathMap == null) { myProjectToOutputPathMap = new TIntObjectHashMap<Serializable>(1, 0.98f); myProjectToOutputPathMap.put(projectId, outputPath); } else { final Object val = myProjectToOutputPathMap.get(projectId); if (val == null) { myProjectToOutputPathMap.put(projectId, outputPath); } else { TIntHashSet set; if (val instanceof Integer) { set = new TIntHashSet(); set.add(((Integer)val).intValue()); myProjectToOutputPathMap.put(projectId, set); } else { assert val instanceof TIntHashSet; set = (TIntHashSet)val; } set.add(outputPath); } } } public boolean clearPaths(final int projectId) { if (myProjectToOutputPathMap != null) { final Serializable removed = myProjectToOutputPathMap.remove(projectId); return removed != null; } return false; } long getTimestamp(final int projectId) { return myTimestamps == null ? -1L : myTimestamps.get(projectId); } void processOutputPaths(final int projectId, final Proc proc) { if (myProjectToOutputPathMap != null) { try { final PersistentStringEnumerator symtable = FSRecords.getNames(); final Object val = myProjectToOutputPathMap.get(projectId); if (val instanceof Integer) { proc.execute(projectId, symtable.valueOf(((Integer)val).intValue())); } else if (val instanceof TIntHashSet) { ((TIntHashSet)val).forEach(new TIntProcedure() { @Override public boolean execute(final int value) { try { proc.execute(projectId, symtable.valueOf(value)); return true; } catch (IOException e) { LOG.info(e); return false; } } }); } } catch (IOException e) { LOG.info(e); } } } boolean isAssociated(int projectId, String outputPath) { if (myProjectToOutputPathMap != null) { try { final Object val = myProjectToOutputPathMap.get(projectId); if (val instanceof Integer) { return FileUtil.pathsEqual(outputPath, FSRecords.getNames().valueOf(((Integer)val).intValue())); } if (val instanceof TIntHashSet) { final int _outputPath = FSRecords.getNames().enumerate(outputPath); return ((TIntHashSet)val).contains(_outputPath); } } catch (IOException e) { LOG.info(e); } } return false; } } @Override public List<String> getCompiledClassNames(VirtualFile srcFile, Project project) { final SourceFileInfo info = loadSourceInfo(srcFile); if (info == null) { return Collections.emptyList(); } final ArrayList<String> result = new ArrayList<String>(); info.processOutputPaths(getProjectId(project), new Proc() { @Override public boolean execute(int projectId, String outputPath) { VirtualFile clsFile = LocalFileSystem.getInstance().findFileByPath(outputPath); if (clsFile != null) { OutputFileInfo outputInfo = loadOutputInfo(clsFile); if (outputInfo != null) { ContainerUtil.addIfNotNull(result, outputInfo.getClassName()); } } return true; } }); return result; } private interface FileProcessor { void execute(VirtualFile file); } private static void processRecursively(VirtualFile file, final boolean dbOnly, final FileProcessor processor) { if (!(file.getFileSystem() instanceof LocalFileSystem)) { return; } final FileTypeManager fileTypeManager = FileTypeManager.getInstance(); VfsUtilCore.visitChildrenRecursively(file, new VirtualFileVisitor() { @NotNull @Override public Result visitFileEx(@NotNull VirtualFile file) { if (fileTypeManager.isFileIgnored(file)) { return SKIP_CHILDREN; } if (!file.isDirectory()) { processor.execute(file); } return CONTINUE; } @Nullable @Override public Iterable<VirtualFile> getChildrenIterable(@NotNull VirtualFile file) { return file.isDirectory() && dbOnly ? ((NewVirtualFile)file).iterInDbChildren() : null; } }); } @Override public void scanSourceContent(final ProjectRef projRef, final Collection<VirtualFile> roots, final int totalRootCount, final boolean isNewRoots) { if (roots.isEmpty()) { return; } final int projectId = getProjectId(projRef.get()); if (LOG.isDebugEnabled()) { LOG.debug("Scanning source content for project projectId=" + projectId + "; url=" + projRef.get().getPresentableUrl()); } final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(projRef.get()).getFileIndex(); final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); int processed = 0; for (VirtualFile srcRoot : roots) { if (indicator != null) { projRef.get(); indicator.setText2(srcRoot.getPresentableUrl()); indicator.setFraction(++processed / (double)totalRootCount); } if (isNewRoots) { fileIndex.iterateContentUnderDirectory(srcRoot, new ContentIterator() { @Override public boolean processFile(final VirtualFile file) { if (!file.isDirectory()) { if (!isMarkedForRecompilation(projectId, Math.abs(getFileId(file)))) { final SourceFileInfo srcInfo = loadSourceInfo(file); if (srcInfo == null || srcInfo.getTimestamp(projectId) != file.getTimeStamp()) { addSourceForRecompilation(projectId, file, srcInfo); } } } else { projRef.get(); } return true; } }); } else { final FileTypeManager fileTypeManager = FileTypeManager.getInstance(); VfsUtilCore.visitChildrenRecursively(srcRoot, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { if (fileTypeManager.isFileIgnored(file)) { return false; } final int fileId = getFileId(file); if (fileId > 0 /*file is valid*/) { if (file.isDirectory()) { projRef.get(); } else if (!isMarkedForRecompilation(projectId, fileId)) { final SourceFileInfo srcInfo = loadSourceInfo(file); if (srcInfo != null) { addSourceForRecompilation(projectId, file, srcInfo); } } } return true; } }); } } } @Override public void ensureInitializationCompleted(Project project, ProgressIndicator indicator) { final int id = getProjectId(project); synchronized (myAsyncScanLock) { while (myInitInProgress.containsKey(id)) { if (!project.isOpen() || project.isDisposed() || (indicator != null && indicator.isCanceled())) { // makes no sense to continue waiting break; } try { myAsyncScanLock.wait(500); } catch (InterruptedException ignored) { break; } } } } private void markOldOutputRoots(final ProjectRef projRef, final TIntObjectHashMap<Pair<Integer, Integer>> currentLayout) { final int projectId = getProjectId(projRef.get()); final TIntHashSet rootsToMark = new TIntHashSet(); synchronized (myProjectOutputRoots) { final TIntObjectHashMap<Pair<Integer, Integer>> oldLayout = myProjectOutputRoots.get(projectId); for (final TIntObjectIterator<Pair<Integer, Integer>> it = oldLayout.iterator(); it.hasNext(); ) { it.advance(); final Pair<Integer, Integer> currentRoots = currentLayout.get(it.key()); final Pair<Integer, Integer> oldRoots = it.value(); if (shouldMark(oldRoots.first, currentRoots != null ? currentRoots.first : -1)) { rootsToMark.add(oldRoots.first); } if (shouldMark(oldRoots.second, currentRoots != null ? currentRoots.second : -1)) { rootsToMark.add(oldRoots.second); } } } for (TIntIterator it = rootsToMark.iterator(); it.hasNext(); ) { final int id = it.next(); final VirtualFile outputRoot = findFileById(id); if (outputRoot != null) { processOldOutputRoot(projectId, outputRoot); } } } private static boolean shouldMark(Integer oldOutputRoot, Integer currentOutputRoot) { return oldOutputRoot != null && oldOutputRoot.intValue() > 0 && !Comparing.equal(oldOutputRoot, currentOutputRoot); } private void processOldOutputRoot(final int projectId, VirtualFile outputRoot) { // recursively mark all corresponding sources for recompilation VfsUtilCore.visitChildrenRecursively(outputRoot, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { if (!file.isDirectory()) { // todo: possible optimization - process only those outputs that are not marked for deletion yet final OutputFileInfo outputInfo = loadOutputInfo(file); if (outputInfo != null) { final String srcPath = outputInfo.getSourceFilePath(); final VirtualFile srcFile = srcPath != null ? LocalFileSystem.getInstance().findFileByPath(srcPath) : null; if (srcFile != null) { loadInfoAndAddSourceForRecompilation(projectId, srcFile); } } } return true; } }); } @Override public void scanSourcesForCompilableFiles(final Project project) { final int projectId = getProjectId(project); if (isSuspended(projectId)) { return; } startAsyncScan(projectId); StartupManager.getInstance(project).runWhenProjectIsInitialized(new Runnable() { @Override public void run() { new Task.Backgroundable(project, CompilerBundle.message("compiler.initial.scanning.progress.text"), false) { @Override public void run(@NotNull final ProgressIndicator indicator) { final ProjectRef projRef = new ProjectRef(project); if (LOG.isDebugEnabled()) { LOG.debug("Initial sources scan for project hash=" + projectId + "; url=" + projRef.get().getPresentableUrl()); } try { final IntermediateOutputCompiler[] compilers = CompilerManager.getInstance(projRef.get()).getCompilers(IntermediateOutputCompiler.class); final Set<VirtualFile> intermediateRoots = new HashSet<VirtualFile>(); if (compilers.length > 0) { final Module[] modules = ModuleManager.getInstance(projRef.get()).getModules(); for (IntermediateOutputCompiler compiler : compilers) { for (Module module : modules) { if (module.isDisposed() || module.getModuleDirUrl() == null) { continue; } final VirtualFile outputRoot = LocalFileSystem.getInstance().refreshAndFindFileByPath(CompilerPaths.getGenerationOutputPath(compiler, module, false)); if (outputRoot != null) { intermediateRoots.add(outputRoot); } final VirtualFile testsOutputRoot = LocalFileSystem.getInstance().refreshAndFindFileByPath(CompilerPaths.getGenerationOutputPath(compiler, module, true)); if (testsOutputRoot != null) { intermediateRoots.add(testsOutputRoot); } } } } final List<VirtualFile> projectRoots = Arrays.asList(getRootsForScan(projRef.get())); final int totalRootsCount = projectRoots.size() + intermediateRoots.size(); scanSourceContent(projRef, projectRoots, totalRootsCount, true); if (!intermediateRoots.isEmpty()) { final FileProcessor processor = new FileProcessor() { @Override public void execute(final VirtualFile file) { if (!isMarkedForRecompilation(projectId, Math.abs(getFileId(file)))) { final SourceFileInfo srcInfo = loadSourceInfo(file); if (srcInfo == null || srcInfo.getTimestamp(projectId) != file.getTimeStamp()) { addSourceForRecompilation(projectId, file, srcInfo); } } } }; int processed = projectRoots.size(); for (VirtualFile root : intermediateRoots) { projRef.get(); indicator.setText2(root.getPresentableUrl()); indicator.setFraction(++processed / (double)totalRootsCount); processRecursively(root, false, processor); } } markOldOutputRoots(projRef, buildOutputRootsLayout(projRef)); } catch (ProjectRef.ProjectClosedException swallowed) { } finally { terminateAsyncScan(projectId, false); } } }.queue(); } }); } private void terminateAsyncScan(int projectId, final boolean clearCounter) { synchronized (myAsyncScanLock) { int counter = myInitInProgress.remove(projectId); if (clearCounter) { myAsyncScanLock.notifyAll(); } else { if (--counter > 0) { myInitInProgress.put(projectId, counter); } else { myAsyncScanLock.notifyAll(); } } } } private void startAsyncScan(final int projectId) { synchronized (myAsyncScanLock) { int counter = myInitInProgress.get(projectId); counter = (counter > 0) ? counter + 1 : 1; myInitInProgress.put(projectId, counter); myAsyncScanLock.notifyAll(); } } private class MyProjectManagerListener extends ProjectManagerAdapter { final Map<Project, MessageBusConnection> myConnections = new HashMap<Project, MessageBusConnection>(); @Override public void projectOpened(final Project project) { final MessageBusConnection conn = project.getMessageBus().connect(); myConnections.put(project, conn); final ProjectRef projRef = new ProjectRef(project); final int projectId = getProjectId(project); watchProject(project); conn.subscribe(ModuleExtension.CHANGE_TOPIC, new ModuleExtensionChangeListener() { @Override public void beforeExtensionChanged(@NotNull ModuleExtension<?> oldExtension, @NotNull ModuleExtension<?> newExtension) { for (TranslatingCompilerFilesMonitorHelper helper : TranslatingCompilerFilesMonitorHelper.EP_NAME.getExtensions()) { if (helper.isModuleExtensionAffectToCompilation(newExtension)) { myForceCompiling = true; break; } } } }); conn.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() { private VirtualFile[] myRootsBefore; private Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, project); @Override public void beforeRootsChange(final ModuleRootEvent event) { if (isSuspended(projectId)) { return; } try { myRootsBefore = getRootsForScan(projRef.get()); } catch (ProjectRef.ProjectClosedException e) { myRootsBefore = null; } } @Override public void rootsChanged(final ModuleRootEvent event) { if (isSuspended(projectId)) { return; } if (LOG.isDebugEnabled()) { LOG.debug("Before roots changed for projectId=" + projectId + "; url=" + project.getPresentableUrl()); } try { final VirtualFile[] rootsBefore = myRootsBefore; myRootsBefore = null; final VirtualFile[] rootsAfter = getRootsForScan(projRef.get()); final Set<VirtualFile> newRoots = new HashSet<VirtualFile>(); final Set<VirtualFile> oldRoots = new HashSet<VirtualFile>(); { if (rootsAfter.length > 0) { ContainerUtil.addAll(newRoots, rootsAfter); } if (rootsBefore != null) { newRoots.removeAll(Arrays.asList(rootsBefore)); } } { if (rootsBefore != null) { ContainerUtil.addAll(oldRoots, rootsBefore); } if (!oldRoots.isEmpty() && rootsAfter.length > 0) { oldRoots.removeAll(Arrays.asList(rootsAfter)); } } myAlarm.cancelAllRequests(); // need alarm to deal with multiple rootsChanged events myAlarm.addRequest(new Runnable() { @Override public void run() { startAsyncScan(projectId); new Task.Backgroundable(project, CompilerBundle.message("compiler.initial.scanning.progress.text"), false) { @Override public void run(@NotNull final ProgressIndicator indicator) { try { if (newRoots.size() > 0) { scanSourceContent(projRef, newRoots, newRoots.size(), true); } if (oldRoots.size() > 0) { scanSourceContent(projRef, oldRoots, oldRoots.size(), false); } markOldOutputRoots(projRef, buildOutputRootsLayout(projRef)); } catch (ProjectRef.ProjectClosedException swallowed) { // ignored } finally { terminateAsyncScan(projectId, false); } } }.queue(); } }, 500, ModalityState.NON_MODAL); } catch (ProjectRef.ProjectClosedException e) { LOG.info(e); } } }); scanSourcesForCompilableFiles(project); } @Override public void projectClosed(final Project project) { final int projectId = getProjectId(project); terminateAsyncScan(projectId, true); myConnections.remove(project).disconnect(); synchronized (myDataLock) { mySourcesToRecompile.remove(projectId); myOutputsToDelete.remove(projectId); // drop cache to save memory } } } private class MyVfsListener extends VirtualFileAdapter { @Override public void propertyChanged(@NotNull final VirtualFilePropertyEvent event) { if (VirtualFile.PROP_NAME.equals(event.getPropertyName())) { final VirtualFile eventFile = event.getFile(); final VirtualFile parent = event.getParent(); if (parent != null) { final String oldName = (String)event.getOldValue(); final String root = parent.getPath() + "/" + oldName; final Set<File> toMark = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY); if (eventFile.isDirectory()) { VfsUtilCore.visitChildrenRecursively(eventFile, new VirtualFileVisitor() { private StringBuilder filePath = new StringBuilder(root); @Override public boolean visitFile(@NotNull VirtualFile child) { if (child.isDirectory()) { if (!Comparing.equal(child, eventFile)) { filePath.append("/").append(child.getName()); } } else { String childPath = filePath.toString(); if (!Comparing.equal(child, eventFile)) { childPath += "/" + child.getName(); } toMark.add(new File(childPath)); } return true; } @Override public void afterChildrenVisited(@NotNull VirtualFile file) { if (file.isDirectory() && !Comparing.equal(file, eventFile)) { filePath.delete(filePath.length() - file.getName().length() - 1, filePath.length()); } } }); } else { toMark.add(new File(root)); } notifyFilesDeleted(toMark); } markDirtyIfSource(eventFile, false); } } @Override public void contentsChanged(@NotNull final VirtualFileEvent event) { markDirtyIfSource(event.getFile(), false); } @Override public void fileCreated(@NotNull final VirtualFileEvent event) { processNewFile(event.getFile(), true); } @Override public void fileCopied(@NotNull final VirtualFileCopyEvent event) { processNewFile(event.getFile(), true); } @Override public void fileMoved(@NotNull VirtualFileMoveEvent event) { processNewFile(event.getFile(), true); } @Override public void beforeFileDeletion(@NotNull final VirtualFileEvent event) { final VirtualFile eventFile = event.getFile(); if ((LOG.isDebugEnabled() && eventFile.isDirectory()) || ourDebugMode) { final String message = "Processing file deletion: " + eventFile.getPresentableUrl(); LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } final Set<File> pathsToMark = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY); processRecursively(eventFile, true, new FileProcessor() { private final TIntArrayList myAssociatedProjectIds = new TIntArrayList(); @Override public void execute(final VirtualFile file) { final String filePath = file.getPath(); pathsToMark.add(new File(filePath)); myAssociatedProjectIds.clear(); try { final OutputFileInfo outputInfo = loadOutputInfo(file); if (outputInfo != null) { final String srcPath = outputInfo.getSourceFilePath(); final VirtualFile srcFile = srcPath != null ? LocalFileSystem.getInstance().findFileByPath(srcPath) : null; if (srcFile != null) { final SourceFileInfo srcInfo = loadSourceInfo(srcFile); if (srcInfo != null) { final boolean srcWillBeDeleted = VfsUtil.isAncestor(eventFile, srcFile, false); for (int projectId : srcInfo.getProjectIds().toArray()) { if (isSuspended(projectId)) { continue; } if (srcInfo.isAssociated(projectId, filePath)) { myAssociatedProjectIds.add(projectId); if (srcWillBeDeleted) { if (LOG.isDebugEnabled() || ourDebugMode) { final String message = "Unschedule recompilation because of deletion " + srcFile.getPresentableUrl(); LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } removeSourceForRecompilation(projectId, Math.abs(getFileId(srcFile))); } else { addSourceForRecompilation(projectId, srcFile, srcInfo); } } } } } } final SourceFileInfo srcInfo = loadSourceInfo(file); if (srcInfo != null) { final TIntHashSet projects = srcInfo.getProjectIds(); if (!projects.isEmpty()) { final ScheduleOutputsForDeletionProc deletionProc = new ScheduleOutputsForDeletionProc(file.getUrl()); deletionProc.setRootBeingDeleted(eventFile); final int sourceFileId = Math.abs(getFileId(file)); for (int projectId : projects.toArray()) { if (isSuspended(projectId)) { continue; } if (srcInfo.isAssociated(projectId, filePath)) { myAssociatedProjectIds.add(projectId); } // mark associated outputs for deletion srcInfo.processOutputPaths(projectId, deletionProc); if (LOG.isDebugEnabled() || ourDebugMode) { final String message = "Unschedule recompilation because of deletion " + file.getPresentableUrl(); LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } removeSourceForRecompilation(projectId, sourceFileId); } } } } finally { // it is important that update of myOutputsToDelete is done at the end // otherwise the filePath of the file that is about to be deleted may be re-scheduled for deletion in addSourceForRecompilation() myAssociatedProjectIds.forEach(new TIntProcedure() { @Override public boolean execute(int projectId) { unmarkOutputPathForDeletion(projectId, filePath); return true; } }); } } }); notifyFilesDeleted(pathsToMark); } @Override public void beforeFileMovement(@NotNull final VirtualFileMoveEvent event) { markDirtyIfSource(event.getFile(), true); } private void markDirtyIfSource(final VirtualFile file, final boolean fromMove) { final Set<File> pathsToMark = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY); processRecursively(file, false, new FileProcessor() { @Override public void execute(final VirtualFile file) { pathsToMark.add(new File(file.getPath())); final SourceFileInfo srcInfo = file.isValid() ? loadSourceInfo(file) : null; if (srcInfo != null) { for (int projectId : srcInfo.getProjectIds().toArray()) { if (isSuspended(projectId)) { if (srcInfo.clearPaths(projectId)) { srcInfo.updateTimestamp(projectId, -1L); saveSourceInfo(file, srcInfo); } } else { addSourceForRecompilation(projectId, file, srcInfo); // when the file is moved to a new location, we should 'forget' previous associations if (fromMove) { if (srcInfo.clearPaths(projectId)) { saveSourceInfo(file, srcInfo); } } } } } else { processNewFile(file, false); } } }); if (fromMove) { notifyFilesDeleted(pathsToMark); } else if (!isIgnoredOrUnderIgnoredDirectory(file)) { notifyFilesChanged(pathsToMark); } } private void processNewFile(final VirtualFile file, final boolean notifyServer) { final Ref<Boolean> isInContent = Ref.create(false); ApplicationManager.getApplication().runReadAction(new Runnable() { // need read action to ensure that the project was not disposed during the iteration over the project list @Override public void run() { for (final Project project : myProjectManager.getOpenProjects()) { if (!project.isInitialized()) { continue; // the content of this project will be scanned during its post-startup activities } final int projectId = getProjectId(project); final boolean projectSuspended = isSuspended(projectId); final ProjectRootManager rootManager = ProjectRootManager.getInstance(project); ProjectFileIndex fileIndex = rootManager.getFileIndex(); if (fileIndex.isInContent(file)) { isInContent.set(true); } if (fileIndex.isInSourceContent(file)) { final TranslatingCompiler[] translators = CompilerManager.getInstance(project).getCompilers(TranslatingCompiler.class); processRecursively(file, false, new FileProcessor() { @Override public void execute(final VirtualFile file) { if (!projectSuspended && isCompilable(file)) { loadInfoAndAddSourceForRecompilation(projectId, file); } } boolean isCompilable(VirtualFile file) { for (TranslatingCompiler translator : translators) { if (translator.isCompilableFile(file, DummyCompileContext.getInstance())) { return true; } } return false; } }); } else { if (!projectSuspended && belongsToIntermediateSources(file, project)) { processRecursively(file, false, new FileProcessor() { @Override public void execute(final VirtualFile file) { loadInfoAndAddSourceForRecompilation(projectId, file); } }); } } } } }); if (notifyServer && !isIgnoredOrUnderIgnoredDirectory(file)) { final Set<File> pathsToMark = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY); boolean dbOnly = !isInContent.get(); processRecursively(file, dbOnly, new FileProcessor() { @Override public void execute(VirtualFile file) { pathsToMark.add(new File(file.getPath())); } }); notifyFilesChanged(pathsToMark); } } } private boolean isIgnoredOrUnderIgnoredDirectory(final VirtualFile file) { FileTypeManager fileTypeManager = FileTypeManager.getInstance(); if (fileTypeManager.isFileIgnored(file)) { return true; } //optimization: if file is in content of some project it's definitely not ignored boolean isInContent = ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() { @Override public Boolean compute() { for (Project project : myProjectManager.getOpenProjects()) { if (project.isInitialized() && ProjectRootManager.getInstance(project).getFileIndex().isInContent(file)) { return true; } } return false; } }); if (isInContent) { return false; } VirtualFile current = file.getParent(); while (current != null) { if (fileTypeManager.isFileIgnored(current)) { return true; } current = current.getParent(); } return false; } private static void notifyFilesChanged(Collection<File> paths) { if (!paths.isEmpty()) { BuildManager.getInstance().notifyFilesChanged(paths); } } private static void notifyFilesDeleted(Collection<File> paths) { if (!paths.isEmpty()) { BuildManager.getInstance().notifyFilesDeleted(paths); } } private boolean belongsToIntermediateSources(VirtualFile file, Project project) { return FileUtil.isAncestor(myGeneratedDataPaths.get(project), new File(file.getPath()), true); } private void loadInfoAndAddSourceForRecompilation(final int projectId, final VirtualFile srcFile) { addSourceForRecompilation(projectId, srcFile, loadSourceInfo(srcFile)); } private void addSourceForRecompilation(final int projectId, final VirtualFile srcFile, @Nullable final SourceFileInfo srcInfo) { final boolean alreadyMarked; synchronized (myDataLock) { TIntHashSet set = mySourcesToRecompile.get(projectId); if (set == null) { set = new TIntHashSet(); mySourcesToRecompile.put(projectId, set); } alreadyMarked = !set.add(Math.abs(getFileId(srcFile))); if (!alreadyMarked && (LOG.isDebugEnabled() || ourDebugMode)) { final String message = "Scheduled recompilation " + srcFile.getPresentableUrl(); LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } } if (!alreadyMarked && srcInfo != null) { srcInfo.updateTimestamp(projectId, -1L); srcInfo.processOutputPaths(projectId, new ScheduleOutputsForDeletionProc(srcFile.getUrl())); saveSourceInfo(srcFile, srcInfo); } } private void removeSourceForRecompilation(final int projectId, final int srcId) { synchronized (myDataLock) { TIntHashSet set = mySourcesToRecompile.get(projectId); if (set != null) { set.remove(srcId); if (set.isEmpty()) { mySourcesToRecompile.remove(projectId); } } } } private VirtualFile[] getRootsForScan(Project project) { List<VirtualFile> list = new ArrayList<VirtualFile>(); Module[] modules = ModuleManager.getInstance(project).getModules(); TranslatingCompilerFilesMonitorHelper[] extensions = TranslatingCompilerFilesMonitorHelper.EP_NAME.getExtensions(); for (Module module : modules) { for (TranslatingCompilerFilesMonitorHelper extension : extensions) { VirtualFile[] rootsForModule = extension.getRootsForModule(module); if (rootsForModule != null) { Collections.addAll(list, rootsForModule); } } VirtualFile[] contentFolderFiles = ModuleRootManager.getInstance(module).getContentFolderFiles(ContentFolderScopes.all(false)); Collections.addAll(list, contentFolderFiles); } return VfsUtil.toVirtualFileArray(list); } @Override public boolean isMarkedForCompilation(Project project, VirtualFile file) { return isMarkedForRecompilation(getProjectId(project), getFileId(file)); } private boolean isMarkedForRecompilation(int projectId, final int srcId) { synchronized (myDataLock) { final TIntHashSet set = mySourcesToRecompile.get(projectId); return set != null && set.contains(srcId); } } private interface Proc { boolean execute(final int projectId, String outputPath); } private class ScheduleOutputsForDeletionProc implements Proc { private final String mySrcUrl; private final LocalFileSystem myFileSystem; @Nullable private VirtualFile myRootBeingDeleted; private ScheduleOutputsForDeletionProc(final String srcUrl) { mySrcUrl = srcUrl; myFileSystem = LocalFileSystem.getInstance(); } public void setRootBeingDeleted(@Nullable VirtualFile rootBeingDeleted) { myRootBeingDeleted = rootBeingDeleted; } @Override public boolean execute(final int projectId, String outputPath) { final VirtualFile outFile = myFileSystem.findFileByPath(outputPath); if (outFile != null) { // not deleted yet if (myRootBeingDeleted != null && VfsUtil.isAncestor(myRootBeingDeleted, outFile, false)) { unmarkOutputPathForDeletion(projectId, outputPath); } else { final OutputFileInfo outputInfo = loadOutputInfo(outFile); final String classname = outputInfo != null ? outputInfo.getClassName() : null; markOutputPathForDeletion(projectId, outputPath, classname, mySrcUrl); } } return true; } } private void markOutputPathForDeletion(final int projectId, final String outputPath, final String classname, final String srcUrl) { final SourceUrlClassNamePair pair = new SourceUrlClassNamePair(srcUrl, classname); synchronized (myDataLock) { final Outputs outputs = myOutputsToDelete.get(projectId); try { outputs.put(outputPath, pair); if (LOG.isDebugEnabled() || ourDebugMode) { final String message = "ADD path to delete: " + outputPath + "; source: " + srcUrl; LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } } finally { outputs.release(); } } } private void unmarkOutputPathForDeletion(final int projectId, String outputPath) { synchronized (myDataLock) { final Outputs outputs = myOutputsToDelete.get(projectId); try { final SourceUrlClassNamePair val = outputs.remove(outputPath); if (val != null) { if (LOG.isDebugEnabled() || ourDebugMode) { final String message = "REMOVE path to delete: " + outputPath; LOG.debug(message); if (ourDebugMode) { System.out.println(message); } } } } finally { outputs.release(); } } } public static final class ProjectRef extends Ref<Project> { static class ProjectClosedException extends RuntimeException { } public ProjectRef(Project project) { super(project); } @Override public Project get() { final Project project = super.get(); if (project != null && project.isDisposed()) { throw new ProjectClosedException(); } return project; } } private static class Outputs { private boolean myIsDirty = false; @Nullable private final File myStoreFile; private final Map<String, SourceUrlClassNamePair> myMap; private final AtomicInteger myRefCount = new AtomicInteger(1); Outputs(@Nullable File storeFile, Map<String, SourceUrlClassNamePair> map) { myStoreFile = storeFile; myMap = map; } public Set<Map.Entry<String, SourceUrlClassNamePair>> getEntries() { return Collections.unmodifiableSet(myMap.entrySet()); } public void put(String outputPath, SourceUrlClassNamePair pair) { if (myStoreFile == null) { return; } if (pair == null) { remove(outputPath); } else { myMap.put(outputPath, pair); myIsDirty = true; } } public SourceUrlClassNamePair remove(String outputPath) { if (myStoreFile == null) { return null; } final SourceUrlClassNamePair removed = myMap.remove(outputPath); myIsDirty |= removed != null; return removed; } void allocate() { myRefCount.incrementAndGet(); } public void release() { if (myRefCount.decrementAndGet() == 0) { if (myIsDirty && myStoreFile != null) { savePathsToDelete(myStoreFile, myMap); } } } } }