| /* |
| * 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.CompilerConfiguration; |
| import com.intellij.compiler.CompilerIOUtil; |
| import com.intellij.compiler.CompilerWorkspaceConfiguration; |
| import com.intellij.compiler.make.MakeUtil; |
| import com.intellij.compiler.server.BuildManager; |
| 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.openapi.vfs.newvfs.persistent.PersistentFS; |
| 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.indexing.IndexInfrastructure; |
| import com.intellij.util.io.*; |
| import com.intellij.util.io.DataOutputStream; |
| import com.intellij.util.messages.MessageBusConnection; |
| 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 |
| * |
| * 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 |
| * |
| * 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 TranslatingCompilerFilesMonitor implements ApplicationComponent { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.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) { |
| @NotNull |
| public File createValue(final Project project) { |
| Disposer.register(project, new Disposable() { |
| 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) { |
| protected void onDropFromCache(Integer key, TIntObjectHashMap<Pair<Integer, Integer>> value) { |
| try { |
| myOutputRootsStorage.put(key, value); |
| } |
| catch (IOException e) { |
| LOG.info(e); |
| } |
| } |
| |
| @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(); |
| |
| public TranslatingCompilerFilesMonitor(VirtualFileManager vfsManager, ProjectManager projectManager, Application application) { |
| myProjectManager = projectManager; |
| |
| projectManager.addProjectManagerListener(new MyProjectManagerListener()); |
| vfsManager.addVirtualFileListener(new MyVfsListener(), application); |
| } |
| |
| public static TranslatingCompilerFilesMonitor getInstance() { |
| return ApplicationManager.getApplication().getComponent(TranslatingCompilerFilesMonitor.class); |
| } |
| |
| 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); |
| } |
| } |
| } |
| |
| public void watchProject(Project project) { |
| synchronized (myDataLock) { |
| mySuspendedProjects.remove(getProjectId(project)); |
| } |
| } |
| |
| public boolean isSuspended(Project project) { |
| return isSuspended(getProjectId(project)); |
| } |
| |
| 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; |
| } |
| |
| 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 CompilerConfiguration configuration = CompilerConfiguration.getInstance(project); |
| final boolean _forceCompile = forceCompile || isRebuild; |
| 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 IndexInfrastructure.findFileById((PersistentFS)ManagingFS.getInstance(), id); |
| } |
| |
| public void update(final CompileContext context, @Nullable final String outputRoot, final Collection<TranslatingCompiler.OutputItem> successfullyCompiled, final VirtualFile[] filesToRecompile) |
| throws IOException { |
| final Project project = context.getProject(); |
| 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() { |
| 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 : MakeUtil.relativeClassPathToQName(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() { |
| public void run() { |
| for (VirtualFile file : filesToRecompile) { |
| if (file.isValid()) { |
| loadInfoAndAddSourceForRecompilation(projectId, file); |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| 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); |
| } |
| } |
| |
| @NotNull |
| public String getComponentName() { |
| return "TranslatingCompilerFilesMonitor"; |
| } |
| |
| 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()) { |
| final CompilerModuleExtension manager = CompilerModuleExtension.getInstance(module); |
| if (manager != null) { |
| final VirtualFile output = manager.getCompilerOutputPath(); |
| final int first = output != null? Math.abs(getFileId(output)) : -1; |
| final VirtualFile testsOutput = manager.getCompilerOutputPathForTests(); |
| 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>>>() { |
| 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); |
| } |
| } |
| |
| 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; |
| } |
| }); |
| } |
| |
| 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() { |
| 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() { |
| 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; |
| } |
| } |
| |
| 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; |
| } |
| }); |
| } |
| |
| // made public for tests |
| 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() { |
| 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; |
| } |
| }); |
| } |
| } |
| } |
| |
| 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; |
| } |
| }); |
| } |
| |
| public void scanSourcesForCompilableFiles(final Project project) { |
| final int projectId = getProjectId(project); |
| if (isSuspended(projectId)) { |
| return; |
| } |
| startAsyncScan(projectId); |
| StartupManager.getInstance(project).runWhenProjectIsInitialized(new Runnable() { |
| public void run() { |
| new Task.Backgroundable(project, CompilerBundle.message("compiler.initial.scanning.progress.text"), false) { |
| 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()) { |
| 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(ProjectRootManager.getInstance(projRef.get()).getContentSourceRoots()); |
| final int totalRootsCount = projectRoots.size() + intermediateRoots.size(); |
| scanSourceContent(projRef, projectRoots, totalRootsCount, true); |
| |
| if (!intermediateRoots.isEmpty()) { |
| final FileProcessor processor = new FileProcessor() { |
| 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>(); |
| |
| 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); |
| |
| if (CompilerWorkspaceConfiguration.getInstance(project).useOutOfProcessBuild()) { |
| suspendProject(project); |
| } |
| else { |
| watchProject(project); |
| } |
| |
| conn.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() { |
| private VirtualFile[] myRootsBefore; |
| private Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, project); |
| |
| public void beforeRootsChange(final ModuleRootEvent event) { |
| if (isSuspended(projectId)) { |
| return; |
| } |
| try { |
| myRootsBefore = ProjectRootManager.getInstance(projRef.get()).getContentSourceRoots(); |
| } |
| catch (ProjectRef.ProjectClosedException e) { |
| myRootsBefore = null; |
| } |
| } |
| |
| 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 = ProjectRootManager.getInstance(projRef.get()).getContentSourceRoots(); |
| 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() { |
| public void run() { |
| startAsyncScan(projectId); |
| new Task.Backgroundable(project, CompilerBundle.message("compiler.initial.scanning.progress.text"), false) { |
| 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); |
| } |
| |
| 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 { |
| public void propertyChanged(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); |
| } |
| } |
| |
| public void contentsChanged(final VirtualFileEvent event) { |
| markDirtyIfSource(event.getFile(), false); |
| } |
| |
| public void fileCreated(final VirtualFileEvent event) { |
| processNewFile(event.getFile(), true); |
| } |
| |
| public void fileCopied(final VirtualFileCopyEvent event) { |
| processNewFile(event.getFile(), true); |
| } |
| |
| public void fileMoved(VirtualFileMoveEvent event) { |
| processNewFile(event.getFile(), true); |
| } |
| |
| public void beforeFileDeletion(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(); |
| |
| 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() { |
| public boolean execute(int projectId) { |
| unmarkOutputPathForDeletion(projectId, filePath); |
| return true; |
| } |
| }); |
| } |
| } |
| }); |
| |
| notifyFilesDeleted(pathsToMark); |
| } |
| |
| public void beforeFileMovement(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() { |
| 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 |
| 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() { |
| 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() { |
| 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); |
| } |
| } |
| } |
| } |
| |
| public boolean isMarkedForCompilation(Project project, VirtualFile file) { |
| if (CompilerWorkspaceConfiguration.getInstance(project).useOutOfProcessBuild()) { |
| final CompilerManager compilerManager = CompilerManager.getInstance(project); |
| return !compilerManager.isUpToDate(compilerManager.createFilesCompileScope(new 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; |
| } |
| |
| 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); |
| } |
| |
| 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); |
| } |
| } |
| } |
| } |
| |
| } |