blob: d36a3b0aded2b9b1c5c8e41fced9f44acae22510 [file] [log] [blame]
/*
* 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);
}
}
}
}
}