diff options
Diffstat (limited to 'src/main/java/com/google/devtools')
8 files changed, 307 insertions, 535 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java index bae4ec9d45..06c84de165 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxRunner.java @@ -15,7 +15,6 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import com.google.common.io.Files; import com.google.devtools.build.lib.actions.ExecException; @@ -27,6 +26,7 @@ import com.google.devtools.build.lib.shell.CommandException; import com.google.devtools.build.lib.shell.TerminationStatus; import com.google.devtools.build.lib.util.CommandFailureUtils; import com.google.devtools.build.lib.util.OsUtils; +import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.io.FileOutErr; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; @@ -36,7 +36,11 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; /** * Helper class for running the Linux sandbox. This runner prepares environment inside the sandbox, @@ -44,34 +48,27 @@ import java.util.List; */ public class LinuxSandboxRunner { private static final String LINUX_SANDBOX = "linux-sandbox" + OsUtils.executableExtension(); - private static final String SANDBOX_TIP = - "\n\nSandboxed execution failed, which may be legitimate (e.g. a compiler error), " - + "or due to missing dependencies. To enter the sandbox environment for easier debugging," - + " run the following command in parentheses. On command failure, " - + "a bash shell running inside the sandbox will then automatically be spawned:\n\n"; private final Path execRoot; - private final Path sandboxPath; private final Path sandboxExecRoot; private final Path argumentsFilePath; - private final ImmutableMap<Path, Path> mounts; - private final ImmutableSet<Path> createDirs; + private final Set<Path> writablePaths; + private final List<Path> inaccessiblePaths; private final boolean verboseFailures; private final boolean sandboxDebug; - public LinuxSandboxRunner( + LinuxSandboxRunner( Path execRoot, - Path sandboxPath, - ImmutableMap<Path, Path> mounts, - ImmutableSet<Path> createDirs, + Path sandboxExecRoot, + Set<Path> writablePaths, + List<Path> inaccessiblePaths, boolean verboseFailures, boolean sandboxDebug) { this.execRoot = execRoot; - this.sandboxPath = sandboxPath; - this.sandboxExecRoot = sandboxPath.getRelative(execRoot.asFragment().relativeTo("/")); + this.sandboxExecRoot = sandboxExecRoot; this.argumentsFilePath = - sandboxPath.getParentDirectory().getRelative(sandboxPath.getBaseName() + ".params"); - this.mounts = mounts; - this.createDirs = createDirs; + sandboxExecRoot.getParentDirectory().getRelative(sandboxExecRoot.getBaseName() + ".params"); + this.writablePaths = writablePaths; + this.inaccessiblePaths = inaccessiblePaths; this.verboseFailures = verboseFailures; this.sandboxDebug = sandboxDebug; } @@ -114,21 +111,20 @@ public class LinuxSandboxRunner { * * @param spawnArguments - arguments of spawn to run inside the sandbox * @param env - environment to run sandbox in - * @param cwd - current working directory * @param outErr - error output to capture sandbox's and command's stderr - * @param outputs - files to extract from the sandbox, paths are relative to the exec root - * @throws ExecException + * @param outputs - files to extract from the sandbox, paths are relative to the exec root @throws + * ExecException */ public void run( List<String> spawnArguments, - ImmutableMap<String, String> env, - File cwd, + Map<String, String> env, FileOutErr outErr, + Map<PathFragment, Path> inputs, Collection<PathFragment> outputs, int timeout, boolean blockNetwork) throws IOException, ExecException { - createFileSystem(outputs); + createFileSystem(inputs, outputs); List<String> fileArgs = new ArrayList<>(); List<String> commandLineArgs = new ArrayList<>(); @@ -139,13 +135,9 @@ public class LinuxSandboxRunner { fileArgs.add("-D"); } - // Sandbox directory. - fileArgs.add("-S"); - fileArgs.add(sandboxPath.getPathString()); - // Working directory of the spawn. fileArgs.add("-W"); - fileArgs.add(cwd.toString()); + fileArgs.add(sandboxExecRoot.toString()); // Kill the process after a timeout. if (timeout != -1) { @@ -154,26 +146,22 @@ public class LinuxSandboxRunner { } // Create all needed directories. - for (Path createDir : createDirs) { - fileArgs.add("-d"); - fileArgs.add(createDir.getPathString()); + for (Path writablePath : writablePaths) { + fileArgs.add("-w"); + fileArgs.add(writablePath.getPathString()); + if (writablePath.startsWith(sandboxExecRoot)) { + FileSystemUtils.createDirectoryAndParents(writablePath); + } } - if (blockNetwork) { - // Block network access out of the namespace. - fileArgs.add("-n"); + for (Path inaccessiblePath : inaccessiblePaths) { + fileArgs.add("-i"); + fileArgs.add(inaccessiblePath.getPathString()); } - // Mount all the inputs. - for (ImmutableMap.Entry<Path, Path> mount : mounts.entrySet()) { - fileArgs.add("-M"); - fileArgs.add(mount.getValue().getPathString()); - - // The file is mounted in a custom location inside the sandbox. - if (!mount.getValue().equals(mount.getKey())) { - fileArgs.add("-m"); - fileArgs.add(mount.getKey().getPathString()); - } + if (blockNetwork) { + // Block network access out of the namespace. + fileArgs.add("-N"); } FileSystemUtils.writeLinesAs(argumentsFilePath, StandardCharsets.ISO_8859_1, fileArgs); @@ -182,7 +170,8 @@ public class LinuxSandboxRunner { commandLineArgs.add("--"); commandLineArgs.addAll(spawnArguments); - Command cmd = new Command(commandLineArgs.toArray(new String[0]), env, cwd); + Command cmd = + new Command(commandLineArgs.toArray(new String[0]), env, sandboxExecRoot.getPathFile()); try { cmd.execute( @@ -200,40 +189,77 @@ public class LinuxSandboxRunner { } String message = CommandFailureUtils.describeCommandFailure( - verboseFailures, commandLineArgs, env, cwd.getPath()); - String finalMsg = (sandboxDebug && verboseFailures) ? SANDBOX_TIP + message : message; - throw new UserExecException(finalMsg, e, timedOut); + verboseFailures, commandLineArgs, env, sandboxExecRoot.getPathString()); + throw new UserExecException(message, e, timedOut); } finally { copyOutputs(outputs); } } - private void createFileSystem(Collection<PathFragment> outputs) throws IOException { - FileSystemUtils.createDirectoryAndParents(sandboxPath); + private void createFileSystem(Map<PathFragment, Path> inputs, Collection<PathFragment> outputs) + throws IOException { + Set<Path> createdDirs = new HashSet<>(); + FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, sandboxExecRoot); + createParentDirectoriesForInputs(createdDirs, inputs.keySet()); + createSymlinksForInputs(inputs); + createDirectoriesForOutputs(createdDirs, outputs); + } + + /** + * No input can be a child of another input, because otherwise we might try to create a symlink + * below another symlink we created earlier - which means we'd actually end up writing somewhere + * in the workspace. + * + * <p>If all inputs were regular files, this situation could naturally not happen - but + * unfortunately, we might get the occasional action that has directories in its inputs. + * + * <p>Creating all parent directories first ensures that we can safely create symlinks to + * directories, too, because we'll get an IOException with EEXIST if inputs happen to be nested + * once we start creating the symlinks for all inputs. + */ + private void createParentDirectoriesForInputs(Set<Path> createdDirs, Set<PathFragment> inputs) + throws IOException { + for (PathFragment inputPath : inputs) { + Path dir = sandboxExecRoot.getRelative(inputPath).getParentDirectory(); + Preconditions.checkArgument(dir.startsWith(sandboxExecRoot)); + FileSystemUtils.createDirectoryAndParentsWithCache(createdDirs, dir); + } + } + + private void createSymlinksForInputs(Map<PathFragment, Path> inputs) throws IOException { + // All input files are relative to the execroot. + for (Entry<PathFragment, Path> entry : inputs.entrySet()) { + Path key = sandboxExecRoot.getRelative(entry.getKey()); + key.createSymbolicLink(entry.getValue()); + } + } - // Prepare the output directories in the sandbox. + /** Prepare the output directories in the sandbox. */ + private void createDirectoriesForOutputs(Set<Path> createdDirs, Collection<PathFragment> outputs) + throws IOException { for (PathFragment output : outputs) { - FileSystemUtils.createDirectoryAndParents( - sandboxExecRoot.getRelative(output.getParentDirectory())); + FileSystemUtils.createDirectoryAndParentsWithCache( + createdDirs, sandboxExecRoot.getRelative(output.getParentDirectory())); + FileSystemUtils.createDirectoryAndParentsWithCache( + createdDirs, execRoot.getRelative(output.getParentDirectory())); } } private void copyOutputs(Collection<PathFragment> outputs) throws IOException { for (PathFragment output : outputs) { Path source = sandboxExecRoot.getRelative(output); - Path target = execRoot.getRelative(output); - FileSystemUtils.createDirectoryAndParents(target.getParentDirectory()); if (source.isFile() || source.isSymbolicLink()) { + Path target = execRoot.getRelative(output); Files.move(source.getPathFile(), target.getPathFile()); } } } public void cleanup() throws IOException { - if (sandboxPath.exists()) { - FileSystemUtils.deleteTree(sandboxPath); + if (sandboxExecRoot.exists()) { + FileSystemUtils.deleteTree(sandboxExecRoot); } - if (!sandboxDebug && argumentsFilePath.exists()) { + if (argumentsFilePath.exists()) { argumentsFilePath.delete(); } } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java index 9a04ed870c..4ee34e891e 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedStrategy.java @@ -13,10 +13,7 @@ // limitations under the License. package com.google.devtools.build.lib.sandbox; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.Files; import com.google.devtools.build.lib.actions.ActionExecutionContext; @@ -33,28 +30,22 @@ import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.actions.UserExecException; import com.google.devtools.build.lib.analysis.AnalysisUtils; import com.google.devtools.build.lib.analysis.BlazeDirectories; -import com.google.devtools.build.lib.analysis.config.RunUnder; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.rules.cpp.CppCompileAction; import com.google.devtools.build.lib.rules.fileset.FilesetActionContext; -import com.google.devtools.build.lib.rules.test.TestRunnerAction; +import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy; -import com.google.devtools.build.lib.unix.NativePosixFiles; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.io.FileOutErr; -import com.google.devtools.build.lib.vfs.FileStatus; import com.google.devtools.build.lib.vfs.FileSystem; -import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; -import com.google.devtools.build.lib.vfs.SearchPath; -import com.google.devtools.build.lib.vfs.Symlinks; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; @@ -67,10 +58,18 @@ import java.util.concurrent.atomic.AtomicInteger; contextType = SpawnActionContext.class ) public class LinuxSandboxedStrategy implements SpawnActionContext { + private static Boolean sandboxingSupported = null; + + public static boolean isSupported(CommandEnvironment env) { + if (sandboxingSupported == null) { + sandboxingSupported = LinuxSandboxRunner.isSupported(env); + } + return sandboxingSupported.booleanValue(); + } + private final ExecutorService backgroundWorkers; private final SandboxOptions sandboxOptions; - private final ImmutableMap<String, String> clientEnv; private final BlazeDirectories blazeDirs; private final Path execRoot; private final boolean verboseFailures; @@ -79,16 +78,14 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { private final AtomicInteger execCounter = new AtomicInteger(); private final String productName; - public LinuxSandboxedStrategy( + LinuxSandboxedStrategy( SandboxOptions options, - Map<String, String> clientEnv, BlazeDirectories blazeDirs, ExecutorService backgroundWorkers, boolean verboseFailures, boolean unblockNetwork, String productName) { this.sandboxOptions = options; - this.clientEnv = ImmutableMap.copyOf(clientEnv); this.blazeDirs = blazeDirs; this.execRoot = blazeDirs.getExecRoot(); this.backgroundWorkers = Preconditions.checkNotNull(backgroundWorkers); @@ -129,18 +126,21 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { String execId = uuid + "-" + execCounter.getAndIncrement(); // Each invocation of "exec" gets its own sandbox. - Path sandboxPath = - execRoot.getRelative(productName + "-sandbox").getRelative(execId); + Path sandboxExecRoot = + blazeDirs.getOutputBase().getRelative(productName + "-sandbox").getRelative(execId); // Gather all necessary mounts for the sandbox. - ImmutableMap<Path, Path> mounts; + Map<PathFragment, Path> mounts; try { mounts = getMounts(spawn, actionExecutionContext); } catch (IllegalArgumentException | IOException e) { throw new EnvironmentalExecException("Could not prepare mounts for sandbox execution", e); } - ImmutableSet<Path> createDirs = createImportantDirs(spawn.getEnvironment()); + Map<String, String> env = new HashMap<>(spawn.getEnvironment()); + + ImmutableSet<Path> writablePaths = getWritablePaths(sandboxExecRoot, env); + ImmutableList<Path> inaccessiblePaths = getInaccessiblePaths(); int timeout = getTimeout(spawn); @@ -157,17 +157,17 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { final LinuxSandboxRunner runner = new LinuxSandboxRunner( execRoot, - sandboxPath, - mounts, - createDirs, + sandboxExecRoot, + writablePaths, + inaccessiblePaths, verboseFailures, sandboxOptions.sandboxDebug); try { runner.run( spawn.getArguments(), - spawn.getEnvironment(), - execRoot.getPathFile(), + env, outErr, + mounts, outputFiles.build(), timeout, !this.unblockNetwork && !spawn.getExecutionInfo().containsKey("requires-network")); @@ -213,191 +213,86 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { return -1; } - /** - * Most programs expect certain directories to be present, e.g. /tmp. Make sure they are. - * - * <p>Note that $HOME is handled by linux-sandbox.c, because it changes user to nobody and the - * home directory of that user is not known by us. - */ - private ImmutableSet<Path> createImportantDirs(Map<String, String> env) { - ImmutableSet.Builder<Path> dirs = ImmutableSet.builder(); - FileSystem fs = blazeDirs.getFileSystem(); + /** Gets the list of directories that the spawn will assume to be writable. */ + private ImmutableSet<Path> getWritablePaths(Path sandboxExecRoot, Map<String, String> env) { + ImmutableSet.Builder<Path> writablePaths = ImmutableSet.builder(); + // We have to make the TEST_TMPDIR directory writable if it is specified. if (env.containsKey("TEST_TMPDIR")) { - PathFragment testTmpDir = new PathFragment(env.get("TEST_TMPDIR")); - if (testTmpDir.isAbsolute()) { - dirs.add(fs.getPath(testTmpDir)); - } else { - dirs.add(execRoot.getRelative(testTmpDir)); - } - } - dirs.add(fs.getPath("/tmp")); - return dirs.build(); - } - - private ImmutableMap<Path, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext) - throws IOException, ExecException { - ImmutableMap.Builder<Path, Path> result = new ImmutableMap.Builder<>(); - result.putAll(mountUsualUnixDirs()); - result.putAll(mountUserDefinedPath()); - - MountMap mounts = new MountMap(); - mounts.putAll(setupBlazeUtils()); - mounts.putAll(mountRunfilesFromManifests(spawn)); - mounts.putAll(mountRunfilesFromSuppliers(spawn)); - mounts.putAll(mountFilesFromFilesetManifests(spawn, executionContext)); - mounts.putAll(mountInputs(spawn, executionContext)); - mounts.putAll(mountRunUnderCommand(spawn)); - result.putAll(finalizeMounts(mounts)); - return result.build(); - } - - /** - * Helper method of {@link #finalizeMounts}. This method handles adding a single path - * to the output map, including making sure it exists and adding the target of a - * symbolic link if necessary. - * - * @param finalizedMounts the map to add the mapping(s) to - * @param target the key to add to the map - * @param source the value to add to the map - * @param stat information about source (passed in to avoid fetching it twice) - */ - private static void finalizeMountPath( - MountMap finalizedMounts, Path target, Path source, FileStatus stat) throws IOException { - // The source must exist. - Preconditions.checkArgument(stat != null, "%s does not exist", source.toString()); - finalizedMounts.put(target, source); - - if (stat.isSymbolicLink()) { - Path symlinkTarget = source.resolveSymbolicLinks(); - Preconditions.checkArgument( - symlinkTarget.exists(), "%s does not exist", symlinkTarget.toString()); - finalizedMounts.put(symlinkTarget, symlinkTarget); + Path testTmpDir = sandboxExecRoot.getRelative(env.get("TEST_TMPDIR")); + writablePaths.add(testTmpDir); + env.put("TEST_TMPDIR", testTmpDir.getPathString()); } + return writablePaths.build(); } - /** - * Performs various checks on each mounted file which require stating each one. - * Contained in one function to allow minimizing the number of syscalls involved. - * - * Checks for each mount if the source refers to a symbolic link and if yes, adds another mount - * for the target of that symlink to ensure that it keeps working inside the sandbox. - * - * Checks for each mount if the source refers to a directory and if yes, replaces that mount with - * mounts of all files inside that directory. - * - * Validates all mounts against a set of criteria and throws an exception on error. - * - * @return a new mounts multimap with all mounts and the added mounts. - */ - @VisibleForTesting - static MountMap finalizeMounts(Map<Path, Path> mounts) throws IOException { - MountMap finalizedMounts = new MountMap(); - for (Entry<Path, Path> mount : mounts.entrySet()) { - Path target = mount.getKey(); - Path source = mount.getValue(); - - FileStatus stat = source.statNullable(Symlinks.NOFOLLOW); - - if (stat != null && stat.isDirectory()) { - for (Path subSource : FileSystemUtils.traverseTree(source, Predicates.alwaysTrue())) { - Path subTarget = target.getRelative(subSource.relativeTo(source)); - finalizeMountPath( - finalizedMounts, subTarget, subSource, subSource.statNullable(Symlinks.NOFOLLOW)); - } - } else { - finalizeMountPath(finalizedMounts, target, source, stat); - } - } - return finalizedMounts; - } - - /** - * Mount a certain set of unix directories to make the usual tools and libraries available to the - * spawn that runs. - * - * Throws an exception if any of them do not exist. - */ - private MountMap mountUsualUnixDirs() throws IOException { - MountMap mounts = new MountMap(); - FileSystem fs = blazeDirs.getFileSystem(); - mounts.put(fs.getPath("/bin"), fs.getPath("/bin")); - mounts.put(fs.getPath("/sbin"), fs.getPath("/sbin")); - mounts.put(fs.getPath("/etc"), fs.getPath("/etc")); - - // Check if /etc/resolv.conf is a symlink and mount its target - // Fix #738 - Path resolv = fs.getPath("/etc/resolv.conf"); - if (resolv.exists() && resolv.isSymbolicLink()) { - mounts.put(resolv.resolveSymbolicLinks(), resolv.resolveSymbolicLinks()); - } - - for (String entry : NativePosixFiles.readdir("/")) { - if (entry.startsWith("lib")) { - Path libDir = fs.getRootDirectory().getRelative(entry); - mounts.put(libDir, libDir); - } - } - for (String entry : NativePosixFiles.readdir("/usr")) { - if (!entry.equals("local")) { - Path usrDir = fs.getPath("/usr").getRelative(entry); - mounts.put(usrDir, usrDir); - } - } - for (Path path : mounts.values()) { - Preconditions.checkArgument(path.exists(), "%s does not exist", path.toString()); + private ImmutableList<Path> getInaccessiblePaths() { + ImmutableList.Builder<Path> inaccessiblePaths = ImmutableList.builder(); + for (String path : sandboxOptions.sandboxBlockPath) { + inaccessiblePaths.add(blazeDirs.getFileSystem().getPath(path)); } - return mounts; + return inaccessiblePaths.build(); } - /** - * Mount the embedded tools. - */ - private MountMap setupBlazeUtils() { - MountMap mounts = new MountMap(); - Path mount = blazeDirs.getEmbeddedBinariesRoot().getRelative("build-runfiles"); - mounts.put(mount, mount); + private Map<PathFragment, Path> getMounts(Spawn spawn, ActionExecutionContext executionContext) + throws IOException, ExecException { + Map<PathFragment, Path> mounts = new HashMap<>(); + mountRunfilesFromManifests(mounts, spawn); + mountRunfilesFromSuppliers(mounts, spawn); + mountFilesFromFilesetManifests(mounts, spawn, executionContext); + mountInputs(mounts, spawn, executionContext); return mounts; } - /** - * Mount all runfiles that the spawn needs as specified in its runfiles manifests. - */ - private MountMap mountRunfilesFromManifests(Spawn spawn) throws IOException, ExecException { - MountMap mounts = new MountMap(); - for (Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) { + /** Mount all runfiles that the spawn needs as specified in its runfiles manifests. */ + private void mountRunfilesFromManifests(Map<PathFragment, Path> mounts, Spawn spawn) + throws IOException, ExecException { + for (Map.Entry<PathFragment, Artifact> manifest : spawn.getRunfilesManifests().entrySet()) { String manifestFilePath = manifest.getValue().getPath().getPathString(); Preconditions.checkState(!manifest.getKey().isAbsolute()); - Path targetDirectory = execRoot.getRelative(manifest.getKey()); - - mounts.putAll(parseManifestFile(targetDirectory, new File(manifestFilePath), false, "")); + PathFragment targetDirectory = manifest.getKey(); + + parseManifestFile( + blazeDirs.getFileSystem(), + mounts, + targetDirectory, + new File(manifestFilePath), + false, + ""); } - return mounts; } - /** - * Mount all files that the spawn needs as specified in its fileset manifests. - */ - private MountMap mountFilesFromFilesetManifests( - Spawn spawn, ActionExecutionContext executionContext) throws IOException, ExecException { + /** Mount all files that the spawn needs as specified in its fileset manifests. */ + private void mountFilesFromFilesetManifests( + Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext executionContext) + throws IOException, ExecException { final FilesetActionContext filesetContext = executionContext.getExecutor().getContext(FilesetActionContext.class); - MountMap mounts = new MountMap(); for (Artifact fileset : spawn.getFilesetManifests()) { - Path manifest = - execRoot.getRelative(AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath())); - Path targetDirectory = execRoot.getRelative(fileset.getExecPathString()); - - mounts.putAll( - parseManifestFile( - targetDirectory, manifest.getPathFile(), true, filesetContext.getWorkspaceName())); + File manifestFile = + new File( + execRoot.getPathString(), + AnalysisUtils.getManifestPathFromFilesetPath(fileset.getExecPath()).getPathString()); + PathFragment targetDirectory = fileset.getExecPath(); + + parseManifestFile( + blazeDirs.getFileSystem(), + mounts, + targetDirectory, + manifestFile, + true, + filesetContext.getWorkspaceName()); } - return mounts; } - static MountMap parseManifestFile( - Path targetDirectory, File manifestFile, boolean isFilesetManifest, String workspaceName) + /** A parser for the MANIFEST files used by Filesets and runfiles. */ + static void parseManifestFile( + FileSystem fs, + Map<PathFragment, Path> mounts, + PathFragment targetDirectory, + File manifestFile, + boolean isFilesetManifest, + String workspaceName) throws IOException, ExecException { - MountMap mounts = new MountMap(); int lineNum = 0; for (String line : Files.readLines(manifestFile, StandardCharsets.UTF_8)) { if (isFilesetManifest && (++lineNum % 2 == 0)) { @@ -409,7 +304,14 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { String[] fields = line.trim().split(" "); - Path targetPath; + // The "target" field is always a relative path that is to be interpreted in this way: + // (1) If this is a fileset manifest and our workspace name is not empty, the first segment + // of each "target" path must be the workspace name, which is then stripped before further + // processing. + // (2) The "target" path is then appended to the "targetDirectory", which is a path relative + // to the execRoot. Together, this results in the full path in the execRoot in which place a + // symlink referring to "source" has to be created (see below). + PathFragment targetPath; if (isFilesetManifest) { PathFragment targetPathFragment = new PathFragment(fields[0]); if (!workspaceName.isEmpty()) { @@ -424,13 +326,15 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { targetPath = targetDirectory.getRelative(fields[0]); } + // The "source" field, if it exists, is always an absolute path and may point to any file in + // the filesystem (it is not limited to files in the workspace or execroot). Path source; switch (fields.length) { case 1: - source = targetDirectory.getFileSystem().getPath("/dev/null"); + source = fs.getPath("/dev/null"); break; case 2: - source = targetDirectory.getFileSystem().getPath(fields[1]); + source = fs.getPath(fields[1]); break; default: throw new IllegalStateException("'" + line + "' splits into more than 2 parts"); @@ -438,38 +342,34 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { mounts.put(targetPath, source); } - return mounts; } - /** - * Mount all runfiles that the spawn needs as specified via its runfiles suppliers. - */ - private MountMap mountRunfilesFromSuppliers(Spawn spawn) throws IOException { - MountMap mounts = new MountMap(); - FileSystem fs = blazeDirs.getFileSystem(); + /** Mount all runfiles that the spawn needs as specified via its runfiles suppliers. */ + private void mountRunfilesFromSuppliers(Map<PathFragment, Path> mounts, Spawn spawn) + throws IOException { Map<PathFragment, Map<PathFragment, Artifact>> rootsAndMappings = spawn.getRunfilesSupplier().getMappings(); - for (Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings : + for (Map.Entry<PathFragment, Map<PathFragment, Artifact>> rootAndMappings : rootsAndMappings.entrySet()) { - Path root = fs.getRootDirectory().getRelative(rootAndMappings.getKey()); - for (Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) { + PathFragment root = rootAndMappings.getKey(); + if (root.isAbsolute()) { + root = root.relativeTo(execRoot.asFragment()); + } + for (Map.Entry<PathFragment, Artifact> mapping : rootAndMappings.getValue().entrySet()) { Artifact sourceArtifact = mapping.getValue(); - Path source = (sourceArtifact != null) ? sourceArtifact.getPath() : fs.getPath("/dev/null"); + PathFragment source = + (sourceArtifact != null) ? sourceArtifact.getExecPath() : new PathFragment("/dev/null"); Preconditions.checkArgument(!mapping.getKey().isAbsolute()); - Path target = root.getRelative(mapping.getKey()); - mounts.put(target, source); + PathFragment target = root.getRelative(mapping.getKey()); + mounts.put(target, execRoot.getRelative(source)); } } - return mounts; } - /** - * Mount all inputs of the spawn. - */ - private MountMap mountInputs(Spawn spawn, ActionExecutionContext actionExecutionContext) { - MountMap mounts = new MountMap(); - + /** Mount all inputs of the spawn. */ + private void mountInputs( + Map<PathFragment, Path> mounts, Spawn spawn, ActionExecutionContext actionExecutionContext) { List<ActionInput> inputs = ActionInputHelper.expandArtifacts( spawn.getInputFiles(), actionExecutionContext.getArtifactExpander()); @@ -485,124 +385,9 @@ public class LinuxSandboxedStrategy implements SpawnActionContext { if (input.getExecPathString().contains("internal/_middlemen/")) { continue; } - Path mount = execRoot.getRelative(input.getExecPathString()); - mounts.put(mount, mount); - } - return mounts; - } - - /** - * If a --run_under= option is set and refers to a command via its path (as opposed to via its - * label), we have to mount this. Note that this is best effort and works fine for shell scripts - * and small binaries, but we can't track any further dependencies of this command. - * - * <p>If --run_under= refers to a label, it is automatically provided in the spawn's input files, - * so mountInputs() will catch that case. - */ - private MountMap mountRunUnderCommand(Spawn spawn) { - MountMap mounts = new MountMap(); - - if (spawn.getResourceOwner() instanceof TestRunnerAction) { - TestRunnerAction testRunnerAction = ((TestRunnerAction) spawn.getResourceOwner()); - RunUnder runUnder = testRunnerAction.getExecutionSettings().getRunUnder(); - if (runUnder != null && runUnder.getCommand() != null) { - PathFragment sourceFragment = new PathFragment(runUnder.getCommand()); - Path mount; - if (sourceFragment.isAbsolute()) { - mount = blazeDirs.getFileSystem().getPath(sourceFragment); - } else if (blazeDirs.getExecRoot().getRelative(sourceFragment).exists()) { - mount = blazeDirs.getExecRoot().getRelative(sourceFragment); - } else { - List<Path> searchPath = - SearchPath.parse(blazeDirs.getFileSystem(), clientEnv.get("PATH")); - mount = SearchPath.which(searchPath, runUnder.getCommand()); - } - if (mount != null) { - mounts.put(mount, mount); - } - } - } - return mounts; - } - - /** - * Mount all user defined path in --sandbox_add_path. - */ - private MountMap mountUserDefinedPath() throws IOException { - MountMap mounts = new MountMap(); - FileSystem fs = blazeDirs.getFileSystem(); - - ImmutableList<Path> exclude = - ImmutableList.of(blazeDirs.getWorkspace(), blazeDirs.getOutputBase()); - - for (String pathStr : sandboxOptions.sandboxAddPath) { - Path path = fs.getPath(pathStr); - - // Check if path is in {workspace, outputBase} - for (Path exc : exclude) { - if (path.startsWith(exc)) { - throw new IllegalArgumentException( - "Mounting subdirectory of WORKSPACE or OUTPUTBASE to sandbox is not allowed."); - } - } - - // Check if path is ancestor of {workspace, outputBase} - // Mount subdirectory of path except {workspace, outputBase} - mounts.putAll(mountChildDirExclude(path, exclude)); + PathFragment mount = new PathFragment(input.getExecPathString()); + mounts.put(mount, execRoot.getRelative(mount)); } - - return mounts; - } - - /** - * Mount all subdirectories recursively except some paths - */ - private MountMap mountDirExclude(Path path, List<Path> exclude) throws IOException { - MountMap mounts = new MountMap(); - - if (!path.isDirectory(Symlinks.NOFOLLOW)) { - if (!exclude.contains(path)) { - mounts.put(path, path); - } - return mounts; - } - - try { - for (Path child : path.getDirectoryEntries()) { - // Ignore broken symlink - if (!child.exists()) { - continue; - } - - mounts.putAll(mountChildDirExclude(child, exclude)); - } - } catch (IOException e) { - throw new IOException("Illegal additional path for mount", e); - } - - return mounts; - } - - /** - * Helper function of mountDirExclude and mountUserDefinedPath - */ - private MountMap mountChildDirExclude(Path child, List<Path> exclude) throws IOException { - MountMap mounts = new MountMap(); - - boolean startsWithFlag = false; - for (Path exc : exclude) { - if (exc.startsWith(child)) { - startsWithFlag = true; - break; - } - } - if (!startsWithFlag) { - mounts.put(child, child); - } else if (!exclude.contains(child)) { - mounts.putAll(mountDirExclude(child, exclude)); - } - - return mounts; } @Override diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java b/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java deleted file mode 100644 index 5051d27df3..0000000000 --- a/src/main/java/com/google/devtools/build/lib/sandbox/MountMap.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2015 The Bazel Authors. All rights reserved. -// -// 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.google.devtools.build.lib.sandbox; - -import com.google.common.collect.ForwardingSortedMap; -import com.google.devtools.build.lib.vfs.Path; - -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -/** - * A map that throws an exception when trying to replace a key (i.e. once a key gets a value, - * any additional attempt of putting a value on the same key will throw an exception). - * - * <p>Returns entries sorted by path depth (shorter paths first) and in lexicographical order. - */ -final class MountMap extends ForwardingSortedMap<Path, Path> { - final TreeMap<Path, Path> delegate = new TreeMap<>(); - - @Override - protected SortedMap<Path, Path> delegate() { - return delegate; - } - - @Override - public Path put(Path key, Path value) { - Path previousValue = get(key); - if (previousValue == null) { - return super.put(key, value); - } else if (previousValue.equals(value)) { - return value; - } else { - throw new IllegalArgumentException( - String.format("Cannot mount both '%s' and '%s' onto '%s'", previousValue, value, key)); - } - } - - @Override - public void putAll(Map<? extends Path, ? extends Path> map) { - for (Entry<? extends Path, ? extends Path> entry : map.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - } -} diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java index 2d6e28e562..7b4e30218a 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextConsumer.java @@ -15,12 +15,11 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.ImmutableMultimap.Builder; import com.google.common.collect.Multimap; import com.google.devtools.build.lib.actions.ActionContextConsumer; import com.google.devtools.build.lib.actions.Executor.ActionContext; import com.google.devtools.build.lib.actions.SpawnActionContext; -import com.google.devtools.build.lib.util.OS; +import com.google.devtools.build.lib.runtime.CommandEnvironment; /** * {@link ActionContextConsumer} that requests the action contexts necessary for sandboxed @@ -28,6 +27,19 @@ import com.google.devtools.build.lib.util.OS; */ public class SandboxActionContextConsumer implements ActionContextConsumer { + private final ImmutableMultimap<Class<? extends ActionContext>, String> contexts; + + public SandboxActionContextConsumer(CommandEnvironment env) { + ImmutableMultimap.Builder<Class<? extends ActionContext>, String> contexts = + ImmutableMultimap.builder(); + + if (LinuxSandboxedStrategy.isSupported(env)) { + contexts.put(SpawnActionContext.class, "sandboxed"); + } + + this.contexts = contexts.build(); + } + @Override public ImmutableMap<String, String> getSpawnActionContexts() { return ImmutableMap.of(); @@ -35,13 +47,6 @@ public class SandboxActionContextConsumer implements ActionContextConsumer { @Override public Multimap<Class<? extends ActionContext>, String> getActionContexts() { - Builder<Class<? extends ActionContext>, String> contexts = ImmutableMultimap.builder(); - - if (OS.getCurrent() == OS.LINUX) { - contexts.put(SpawnActionContext.class, "sandboxed"); - } - - return contexts.build(); + return contexts; } - } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java index 6bf355610e..7bf38c901b 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java @@ -14,11 +14,11 @@ package com.google.devtools.build.lib.sandbox; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableList.Builder; import com.google.devtools.build.lib.actions.ActionContextProvider; import com.google.devtools.build.lib.actions.Executor.ActionContext; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.buildtool.BuildRequest; +import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.exec.ExecutionOptions; import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.util.OS; @@ -30,11 +30,16 @@ import java.util.concurrent.ExecutorService; */ public class SandboxActionContextProvider extends ActionContextProvider { + public static final String SANDBOX_NOT_SUPPORTED_MESSAGE = + "Sandboxed execution is not supported on your system and thus hermeticity of actions cannot " + + "be guaranteed. See http://bazel.io/docs/bazel-user-manual.html#sandboxing for more " + + "information. You can turn off this warning via --ignore_unsupported_sandboxing"; + @SuppressWarnings("unchecked") - private final ImmutableList<ActionContext> strategies; + private final ImmutableList<ActionContext> contexts; - private SandboxActionContextProvider(ImmutableList<ActionContext> strategies) { - this.strategies = strategies; + private SandboxActionContextProvider(ImmutableList<ActionContext> contexts) { + this.contexts = contexts; } public static SandboxActionContextProvider create( @@ -46,36 +51,45 @@ public class SandboxActionContextProvider extends ActionContextProvider { .getOptions(BuildConfiguration.Options.class) .testArguments .contains("--wrapper_script_flag=--debug"); - Builder<ActionContext> strategies = ImmutableList.builder(); + ImmutableList.Builder<ActionContext> contexts = ImmutableList.builder(); - if (OS.getCurrent() == OS.LINUX) { - strategies.add( - new LinuxSandboxedStrategy( - buildRequest.getOptions(SandboxOptions.class), - env.getClientEnv(), - env.getDirectories(), - backgroundWorkers, - verboseFailures, - unblockNetwork, - env.getRuntime().getProductName())); - } else if (OS.getCurrent() == OS.DARWIN) { - strategies.add( - DarwinSandboxedStrategy.create( - buildRequest.getOptions(SandboxOptions.class), - env.getClientEnv(), - env.getDirectories(), - backgroundWorkers, - verboseFailures, - unblockNetwork, - env.getRuntime().getProductName())); + switch (OS.getCurrent()) { + case LINUX: + if (LinuxSandboxedStrategy.isSupported(env)) { + contexts.add( + new LinuxSandboxedStrategy( + buildRequest.getOptions(SandboxOptions.class), + env.getDirectories(), + backgroundWorkers, + verboseFailures, + unblockNetwork, + env.getRuntime().getProductName())); + } else if (!buildRequest.getOptions(SandboxOptions.class).ignoreUnsupportedSandboxing) { + env.getReporter().handle(Event.warn(SANDBOX_NOT_SUPPORTED_MESSAGE)); + } + break; + case DARWIN: + if (DarwinSandboxRunner.isSupported()) { + contexts.add( + DarwinSandboxedStrategy.create( + buildRequest.getOptions(SandboxOptions.class), + env.getClientEnv(), + env.getDirectories(), + backgroundWorkers, + verboseFailures, + unblockNetwork, + env.getRuntime().getProductName())); + } + break; + default: + // No sandboxing available. } - return new SandboxActionContextProvider(strategies.build()); + return new SandboxActionContextProvider(contexts.build()); } @Override public Iterable<ActionContext> getActionContexts() { - return strategies; + return contexts; } - } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java index 5b9ca58931..8a20cb4f16 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java @@ -20,11 +20,9 @@ import com.google.devtools.build.lib.actions.ActionContextProvider; import com.google.devtools.build.lib.buildtool.BuildRequest; import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent; import com.google.devtools.build.lib.concurrent.ExecutorUtil; -import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.runtime.BlazeModule; import com.google.devtools.build.lib.runtime.Command; import com.google.devtools.build.lib.runtime.CommandEnvironment; -import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.common.options.OptionsBase; import java.io.IOException; @@ -35,66 +33,30 @@ import java.util.concurrent.Executors; * This module provides the Sandbox spawn strategy. */ public class SandboxModule extends BlazeModule { - public static final String SANDBOX_NOT_SUPPORTED_MESSAGE = - "Sandboxed execution is not supported on your system and thus hermeticity of actions cannot " - + "be guaranteed. See http://bazel.io/docs/bazel-user-manual.html#sandboxing for more " - + "information. You can turn off this warning via --ignore_unsupported_sandboxing"; - // Per-server state private ExecutorService backgroundWorkers; - private Boolean sandboxingSupported = null; // Per-command state private CommandEnvironment env; private BuildRequest buildRequest; - private synchronized boolean isSandboxingSupported(CommandEnvironment env) { - switch (OS.getCurrent()) { - case LINUX: - sandboxingSupported = LinuxSandboxRunner.isSupported(env); - break; - case DARWIN: - sandboxingSupported = DarwinSandboxRunner.isSupported(); - break; - default: - sandboxingSupported = false; - } - return sandboxingSupported.booleanValue(); - } - @Override public Iterable<ActionContextProvider> getActionContextProviders() { - Preconditions.checkNotNull(buildRequest); Preconditions.checkNotNull(env); - if (isSandboxingSupported(env)) { - Iterable<ActionContextProvider> ret; - try { - ret = - ImmutableList.<ActionContextProvider>of( - SandboxActionContextProvider.create(env, buildRequest, backgroundWorkers)); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - return ret; - } - - // For now, sandboxing is only supported on Linux and there's not much point in showing a scary - // warning to the user if they can't do anything about it. - if (!buildRequest.getOptions(SandboxOptions.class).ignoreUnsupportedSandboxing - && OS.getCurrent() == OS.LINUX) { - env.getReporter().handle(Event.warn(SANDBOX_NOT_SUPPORTED_MESSAGE)); + Preconditions.checkNotNull(buildRequest); + Preconditions.checkNotNull(backgroundWorkers); + try { + return ImmutableList.<ActionContextProvider>of( + SandboxActionContextProvider.create(env, buildRequest, backgroundWorkers)); + } catch (IOException e) { + throw new IllegalStateException(e); } - - return ImmutableList.of(); } @Override public Iterable<ActionContextConsumer> getActionContextConsumers() { Preconditions.checkNotNull(env); - if (isSandboxingSupported(env)) { - return ImmutableList.<ActionContextConsumer>of(new SandboxActionContextConsumer()); - } - return ImmutableList.of(); + return ImmutableList.<ActionContextConsumer>of(new SandboxActionContextConsumer(env)); } @Override diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java index f1291d2cbb..f18792f606 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxOptions.java @@ -15,7 +15,6 @@ package com.google.devtools.build.lib.sandbox; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionsBase; - import java.util.List; /** @@ -42,11 +41,11 @@ public class SandboxOptions extends OptionsBase { public boolean sandboxDebug; @Option( - name = "sandbox_add_path", + name = "sandbox_block_path", allowMultiple = true, defaultValue = "", category = "config", - help = "Add additional path to mount to sandbox. Path including workspace is not allowed." + help = "For sandboxed actions, disallow access to this path." ) - public List<String> sandboxAddPath; + public List<String> sandboxBlockPath; } diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java index 8c864547ff..cf12b50f21 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java @@ -22,7 +22,6 @@ import com.google.common.io.ByteStreams; import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.util.Preconditions; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -32,6 +31,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Set; /** * Helper functions that implement often-used complex operations on file @@ -582,10 +582,36 @@ public class FileSystemUtils { */ @ThreadSafe public static boolean createDirectoryAndParents(Path dir) throws IOException { + return createDirectoryAndParentsWithCache(null, dir); + } + + /** + * Attempts to create a directory with the name of the given path, creating ancestors as + * necessary. Only creates directories or their parents if they are not contained in the set + * {@code createdDirs} and instead assumes that they already exist. This saves a round-trip to the + * kernel, but is only safe when no one deletes directories that have been created by this method. + * + * <p>Postcondition: completes normally iff {@code dir} denotes an existing directory (not + * necessarily canonical); completes abruptly otherwise. + * + * @return true if the directory was successfully created anew, false if it already existed + * (including the case where {@code dir} denotes a symlink to an existing directory) + * @throws IOException if the directory could not be created + */ + @ThreadSafe + public static boolean createDirectoryAndParentsWithCache(Set<Path> createdDirs, Path dir) + throws IOException { // Optimised for minimal number of I/O calls. // Don't attempt to create the root directory. - if (dir.getParentDirectory() == null) { return false; } + if (dir.getParentDirectory() == null) { + return false; + } + + // We already created that directory. + if (createdDirs != null && createdDirs.contains(dir)) { + return false; + } FileSystem filesystem = dir.getFileSystem(); if (filesystem instanceof UnionFileSystem) { @@ -596,12 +622,23 @@ public class FileSystemUtils { } try { - return dir.createDirectory(); + boolean result = dir.createDirectory(); + if (createdDirs != null) { + createdDirs.add(dir); + } + return result; } catch (IOException e) { if (e.getMessage().endsWith(" (No such file or directory)")) { // ENOENT - createDirectoryAndParents(dir.getParentDirectory()); - return dir.createDirectory(); + createDirectoryAndParentsWithCache(createdDirs, dir.getParentDirectory()); + boolean result = dir.createDirectory(); + if (createdDirs != null) { + createdDirs.add(dir); + } + return result; } else if (e.getMessage().endsWith(" (File exists)") && dir.isDirectory()) { // EEXIST + if (createdDirs != null) { + createdDirs.add(dir); + } return false; } else { throw e; // some other error (e.g. ENOTDIR, EACCES, etc.) |