/* * Copyright 2016-present Facebook, Inc. * * 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.facebook.buck.ide.intellij; import com.facebook.buck.artifact_cache.ArtifactCacheBuckConfig; import com.facebook.buck.artifact_cache.DirCacheEntry; import com.facebook.buck.cli.BuckConfig; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.log.Logger; import java.io.File; import java.io.FileFilter; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveAction; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** Cleans out any unwanted IntelliJ IDEA project files. */ public class IJProjectCleaner { private static final Logger LOG = Logger.get(IJProjectCleaner.class); private static final int EXECUTOR_SHUTDOWN_TIMEOUT = 1; private static final TimeUnit EXECUTOR_SHUTDOWN_TIME_UNIT = TimeUnit.MINUTES; private static final FilenameFilter IML_FILENAME_FILTER = (dir, name) -> name.endsWith(".iml"); private static final FilenameFilter XML_FILENAME_FILTER = (dir, name) -> name.endsWith(".xml"); private static final FileFilter SUBDIRECTORY_FILTER = File::isDirectory; private final ProjectFilesystem projectFilesystem; private final Set<File> filesToKeep = new HashSet<>(); public IJProjectCleaner(ProjectFilesystem projectFilesystem) { this.projectFilesystem = projectFilesystem; } /** @param path The path to not include in the cleaning operation. */ public void doNotDelete(Path path) { filesToKeep.add(convertPathToFile(path)); } public void writeFilesToKeepToFile(String filename) throws IOException { Files.write( projectFilesystem.resolve(filename), filesToKeep .stream() .map(File::getAbsolutePath) .map(Paths::get) .map(projectFilesystem::relativize) .map(Path::toString) .collect(Collectors.toSet())); } private File convertPathToFile(Path path) { if (!path.isAbsolute()) { path = projectFilesystem.resolve(path); } try { return path.toRealPath().toFile(); } catch (IOException e) { LOG.warn("Problem resolving " + path, e); return path.toAbsolutePath().toFile(); } } /** * We shouldn't use all the processors available because this can bog down the system, but using * n/2 with an upper limit of 4 as the thread limit should give us enough threads to keep SSDs * busy whilst not bogging down systems with slow rotating disks. */ private int getParallelismLimit() { int limit = Math.max(Runtime.getRuntime().availableProcessors() / 2, 4); return limit > 0 ? limit : 1; } @SuppressWarnings("serial") public void clean( final BuckConfig buckConfig, final Path librariesXmlBase, final boolean runPostGenerationCleaner, final boolean removeOldLibraries) { if (!runPostGenerationCleaner && !removeOldLibraries) { return; } final Set<File> buckDirectories = new HashSet<>(); buckDirectories.add( convertPathToFile( projectFilesystem.resolve(projectFilesystem.getBuckPaths().getBuckOut()))); ArtifactCacheBuckConfig cacheBuckConfig = new ArtifactCacheBuckConfig(buckConfig); for (DirCacheEntry entry : cacheBuckConfig.getCacheEntries().getDirCacheEntries()) { buckDirectories.add(convertPathToFile(entry.getCacheDir())); } ForkJoinPool cleanExecutor = new ForkJoinPool(getParallelismLimit()); try { cleanExecutor.invoke( new RecursiveAction() { @Override protected void compute() { List<RecursiveAction> topLevelTasks = new ArrayList<>(2); if (runPostGenerationCleaner) { topLevelTasks.add( new CandidateFinderWithExclusions( convertPathToFile(projectFilesystem.resolve("")), IML_FILENAME_FILTER, buckDirectories)); } topLevelTasks.add( new CandidateFinder(convertPathToFile(librariesXmlBase), XML_FILENAME_FILTER)); invokeAll(topLevelTasks); } }); } finally { cleanExecutor.shutdown(); try { cleanExecutor.awaitTermination(EXECUTOR_SHUTDOWN_TIMEOUT, EXECUTOR_SHUTDOWN_TIME_UNIT); } catch (InterruptedException e) { Logger.get(IJProjectCleaner.class).warn("Timeout during executor shutdown.", e); } } } @SuppressWarnings("serial") private class DirectoryCleaner extends RecursiveAction { private File directory; private FilenameFilter filenameFilter; DirectoryCleaner(File directory, FilenameFilter filenameFilter) { this.directory = directory; this.filenameFilter = filenameFilter; } @Override protected void compute() { File[] files = directory.listFiles(filenameFilter); if (files == null) { return; } for (File file : files) { file = file.getAbsoluteFile(); if (filesToKeep.contains(file)) { LOG.warn("Skipping " + file); continue; } LOG.warn("Deleting " + file); if (!file.delete()) { LOG.warn("Unable to delete " + file); } } } } @SuppressWarnings("serial") private class CandidateFinder extends RecursiveAction { private File directory; private FilenameFilter filenameFilter; CandidateFinder(File directory, FilenameFilter filenameFilter) { this.directory = directory; this.filenameFilter = filenameFilter; } protected void addCandidateDirectoryFinder(List<RecursiveAction> finders, File subdirectory) { finders.add(new CandidateFinder(subdirectory, filenameFilter)); } @Override public void compute() { File[] subdirectories = directory.listFiles(SUBDIRECTORY_FILTER); if (subdirectories == null) { return; } List<RecursiveAction> actions = new ArrayList<>(subdirectories.length + 1); actions.add(new DirectoryCleaner(directory, filenameFilter)); for (File subdirectory : subdirectories) { if (Files.isSymbolicLink(subdirectory.toPath())) { continue; } addCandidateDirectoryFinder(actions, subdirectory); } invokeAll(actions); } } @SuppressWarnings("serial") private class CandidateFinderWithExclusions extends CandidateFinder { Set<File> exclusions; CandidateFinderWithExclusions( File directory, FilenameFilter filenameFilter, Set<File> exclusions) { super(directory, filenameFilter); this.exclusions = exclusions; } @Override protected void addCandidateDirectoryFinder(List<RecursiveAction> actions, File subdirectory) { if (exclusions.contains(subdirectory)) { return; } super.addCandidateDirectoryFinder(actions, subdirectory); } } }