| // Copyright 2018 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.skyframe; |
| |
| import static java.nio.charset.StandardCharsets.US_ASCII; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.cache.CacheBuilder; |
| import com.google.common.cache.CacheLoader; |
| import com.google.common.cache.LoadingCache; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Streams; |
| import com.google.common.hash.Hashing; |
| import com.google.common.io.BaseEncoding; |
| import com.google.devtools.build.lib.actions.ActionInput; |
| import com.google.devtools.build.lib.actions.ActionInputMap; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.FileArtifactValue; |
| import com.google.devtools.build.lib.actions.FileArtifactValue.InlineFileArtifactValue; |
| import com.google.devtools.build.lib.actions.FileArtifactValue.RemoteFileArtifactValue; |
| import com.google.devtools.build.lib.actions.FileArtifactValue.SourceFileArtifactValue; |
| import com.google.devtools.build.lib.actions.FileStateType; |
| import com.google.devtools.build.lib.actions.MetadataProvider; |
| import com.google.devtools.build.lib.profiler.Profiler; |
| import com.google.devtools.build.lib.profiler.ProfilerTask; |
| import com.google.devtools.build.lib.vfs.FileStatus; |
| import com.google.devtools.build.lib.vfs.FileSystem; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.lib.vfs.Root; |
| import com.google.devtools.build.skyframe.SkyFunction; |
| import com.google.protobuf.ByteString; |
| import java.io.ByteArrayOutputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InterruptedIOException; |
| import java.io.OutputStream; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.logging.Logger; |
| import javax.annotation.Nullable; |
| |
| /** |
| * File system for actions. |
| * |
| * <p>This class is thread-safe except that |
| * |
| * <ul> |
| * <li>{@link updateContext} must be called exclusively of any other methods. |
| * <li>This class relies on synchronized access to {@link env}. If there are other threads, that |
| * access {@link env}, they must also used synchronized access. |
| * </ul> |
| */ |
| final class ActionFileSystem extends FileSystem implements MetadataProvider, InjectionListener { |
| private static final Logger LOGGER = Logger.getLogger(ActionFileSystem.class.getName()); |
| public static final BaseEncoding LOWER_CASE_HEX = BaseEncoding.base16().lowerCase(); |
| |
| /** Actual underlying filesystem. */ |
| private final FileSystem delegate; |
| |
| private final PathFragment execRootFragment; |
| private final ImmutableList<PathFragment> sourceRoots; |
| |
| private final ActionInputMap inputArtifactData; |
| |
| /** exec path → artifact and metadata */ |
| private final HashMap<PathFragment, OptionalInputMetadata> optionalInputs; |
| |
| /** exec path → artifact and metadata */ |
| private final LoadingCache<PathFragment, OutputMetadata> outputs; |
| |
| /** Used to lookup metadata for optional inputs. */ |
| private SkyFunction.Environment env = null; |
| |
| /** |
| * Called whenever there is new metadata for an output. |
| * |
| * <p>This is backed by injection into an {@link ActionMetadataHandler} instance so should only be |
| * called once per artifact. |
| */ |
| private MetadataConsumer metadataConsumer = null; |
| |
| ActionFileSystem( |
| FileSystem delegate, |
| Path execRoot, |
| ImmutableList<Root> sourceRoots, |
| ActionInputMap inputArtifactData, |
| Iterable<Artifact> allowedInputs, |
| Iterable<Artifact> outputArtifacts) { |
| try { |
| Profiler.instance().startTask(ProfilerTask.ACTION_FS_STAGING, "staging"); |
| this.delegate = delegate; |
| |
| this.execRootFragment = execRoot.asFragment(); |
| this.sourceRoots = |
| sourceRoots |
| .stream() |
| .map(root -> root.asPath().asFragment()) |
| .collect(ImmutableList.toImmutableList()); |
| |
| validateRoots(); |
| |
| this.inputArtifactData = inputArtifactData; |
| |
| this.optionalInputs = new HashMap<>(); |
| for (Artifact input : allowedInputs) { |
| // Skips staging source artifacts as a performance optimization. We may want to stage them |
| // if we want stricter enforcement of source sandboxing. |
| // |
| // TODO(shahan): there are no currently known cases where metadata is requested for an |
| // optional source input. If there are any, we may want to stage those. |
| if (input.isSourceArtifact() || inputArtifactData.getMetadata(input) != null) { |
| continue; |
| } |
| optionalInputs.computeIfAbsent( |
| input.getExecPath(), unused -> new OptionalInputMetadata(input)); |
| } |
| |
| ImmutableMap<PathFragment, Artifact> outputsMapping = Streams.stream(outputArtifacts) |
| .collect(ImmutableMap.toImmutableMap(Artifact::getExecPath, a -> a)); |
| this.outputs = CacheBuilder.newBuilder().build( |
| CacheLoader.from(path -> new OutputMetadata(outputsMapping.get(path)))); |
| } finally { |
| Profiler.instance().completeTask(ProfilerTask.ACTION_FS_STAGING); |
| } |
| } |
| |
| /** |
| * Must be called prior to access and updated as needed. |
| * |
| * <p>These cannot be passed into the constructor because while {@link ActionFileSystem} is |
| * action-scoped, the environment and metadata consumer change multiple times, at well defined |
| * points, during the lifetime of an action. |
| */ |
| public void updateContext(SkyFunction.Environment env, MetadataConsumer metadataConsumer) { |
| this.env = env; |
| this.metadataConsumer = metadataConsumer; |
| } |
| |
| // -------------------- MetadataProvider implementation -------------------- |
| |
| @Override |
| @Nullable |
| public FileArtifactValue getMetadata(ActionInput actionInput) throws IOException { |
| return getMetadataChecked(actionInput.getExecPath()); |
| } |
| |
| @Override |
| @Nullable |
| public ActionInput getInput(String execPath) { |
| ActionInput input = inputArtifactData.getInput(execPath); |
| if (input != null) { |
| return input; |
| } |
| OptionalInputMetadata metadata = |
| optionalInputs.get(PathFragment.createAlreadyNormalized(execPath)); |
| return metadata == null ? null : metadata.getArtifact(); |
| } |
| |
| // -------------------- InjectionListener Implementation -------------------- |
| |
| @Override |
| public void onInsert(ActionInput dest, byte[] digest, long size, int backendIndex) |
| throws IOException { |
| OutputMetadata output = outputs.getUnchecked(dest.getExecPath()); |
| if (output != null) { |
| output.set(new RemoteFileArtifactValue(digest, size, backendIndex), |
| /*notifyConsumer=*/ false); |
| } |
| } |
| |
| // -------------------- FileSystem implementation -------------------- |
| |
| @Override |
| public boolean supportsModifications(Path path) { |
| return isOutput(path); |
| } |
| |
| @Override |
| public boolean supportsSymbolicLinksNatively(Path path) { |
| return isOutput(path); |
| } |
| |
| @Override |
| protected boolean supportsHardLinksNatively(Path path) { |
| return isOutput(path); |
| } |
| |
| @Override |
| public boolean isFilePathCaseSensitive() { |
| return true; |
| } |
| |
| @Override |
| protected FileStatus stat(Path path, boolean followSymlinks) throws IOException { |
| FileArtifactValue metadata = getMetadataOrThrowFileNotFound(path); |
| return new FileStatus() { |
| @Override |
| public boolean isFile() { |
| return metadata.getType() == FileStateType.REGULAR_FILE; |
| } |
| |
| @Override |
| public boolean isDirectory() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSymbolicLink() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSpecialFile() { |
| return metadata.getType() == FileStateType.SPECIAL_FILE; |
| } |
| |
| @Override |
| public long getSize() { |
| return metadata.getSize(); |
| } |
| |
| @Override |
| public long getLastModifiedTime() { |
| return metadata.getModifiedTime(); |
| } |
| |
| @Override |
| public long getLastChangeTime() { |
| return metadata.getModifiedTime(); |
| } |
| |
| @Override |
| public long getNodeId() { |
| throw new UnsupportedOperationException(); |
| } |
| }; |
| } |
| |
| /** ActionFileSystem currently doesn't track directories. */ |
| @Override |
| public boolean createDirectory(Path path) throws IOException { |
| return true; |
| } |
| |
| @Override |
| public void createDirectoryAndParents(Path path) throws IOException {} |
| |
| @Override |
| protected long getFileSize(Path path, boolean followSymlinks) throws IOException { |
| return getMetadataOrThrowFileNotFound(path).getSize(); |
| } |
| |
| @Override |
| public boolean delete(Path path) throws IOException { |
| // TODO(felly): Support file deletion. |
| return false; |
| } |
| |
| @Override |
| protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException { |
| return getMetadataOrThrowFileNotFound(path).getModifiedTime(); |
| } |
| |
| @Override |
| public void setLastModifiedTime(Path path, long newTime) throws IOException { |
| throw new UnsupportedOperationException(path.getPathString()); |
| } |
| |
| @Override |
| public byte[] getxattr(Path path, String name) throws IOException { |
| FileArtifactValue metadata = getMetadataChecked(asExecPath(path)); |
| if (metadata instanceof RemoteFileArtifactValue) { |
| RemoteFileArtifactValue remote = (RemoteFileArtifactValue) metadata; |
| // TODO(b/80244718): inject ActionFileSystem from elsewhere and replace with correct metadata |
| return ("/CENSORED_BY_LEAKR/" |
| + remote.getLocationIndex() |
| + "/" |
| + LOWER_CASE_HEX.encode(remote.getDigest())) |
| .getBytes(US_ASCII); |
| } |
| if (metadata instanceof SourceFileArtifactValue) { |
| return resolveSourcePath((SourceFileArtifactValue) metadata).getxattr(name); |
| } |
| return delegate.getPath(path.asFragment()).getxattr(name); |
| } |
| |
| @Override |
| protected byte[] getFastDigest(Path path, HashFunction hash) throws IOException { |
| if (hash != HashFunction.MD5) { |
| return null; |
| } |
| return getMetadataOrThrowFileNotFound(path).getDigest(); |
| } |
| |
| @Override |
| protected boolean isSymbolicLink(Path path) { |
| // TODO(felly): We should have minimal support for symlink awareness when looking at |
| // output --> src and src --> src symlinks. |
| return false; |
| } |
| |
| @Override |
| protected boolean isDirectory(Path path, boolean followSymlinks) { |
| // TODO(felly): Support directory awareness. |
| return true; |
| } |
| |
| @Override |
| protected Collection<String> getDirectoryEntries(Path path) throws IOException { |
| // TODO(felly): Support directory traversal. |
| return ImmutableList.of(); |
| } |
| |
| @Override |
| protected boolean isFile(Path path, boolean followSymlinks) { |
| // TODO(felly): Unify is* methods with the stat() operation. |
| FileArtifactValue metadata = getMetadataUnchecked(path); |
| return metadata == null ? false : metadata.getType() == FileStateType.REGULAR_FILE; |
| } |
| |
| @Override |
| protected boolean isSpecialFile(Path path, boolean followSymlinks) { |
| FileArtifactValue metadata = getMetadataUnchecked(path); |
| return metadata == null ? false : metadata.getType() == FileStateType.SPECIAL_FILE; |
| } |
| |
| private static String createSymbolicLinkErrorMessage( |
| Path linkPath, PathFragment targetFragment, String message) { |
| return "createSymbolicLink(" + linkPath + ", " + targetFragment + "): " + message; |
| } |
| |
| @Override |
| protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException { |
| // TODO(shahan): this might need to be loosened, but will require more information |
| Preconditions.checkArgument( |
| targetFragment.isAbsolute(), |
| "ActionFileSystem requires symlink targets to be absolute: %s -> %s", |
| linkPath, |
| targetFragment); |
| |
| // When creating symbolic links, it matters whether target is a source path or not because |
| // the metadata needs to be handled differently in that case. |
| PathFragment targetExecPath = null; |
| int sourceRootIndex = -1; // index into sourceRoots or -1 if not a source |
| if (targetFragment.startsWith(execRootFragment)) { |
| targetExecPath = targetFragment.relativeTo(execRootFragment); |
| } else { |
| for (int i = 0; i < sourceRoots.size(); ++i) { |
| if (targetFragment.startsWith(sourceRoots.get(i))) { |
| targetExecPath = targetFragment.relativeTo(sourceRoots.get(i)); |
| sourceRootIndex = i; |
| break; |
| } |
| } |
| if (sourceRootIndex == -1) { |
| throw new IllegalArgumentException( |
| linkPath |
| + " was not found under any known root: " |
| + execRootFragment |
| + ", " |
| + sourceRoots); |
| } |
| } |
| |
| FileArtifactValue inputMetadata = inputArtifactData.getMetadata(targetExecPath.getPathString()); |
| if (inputMetadata == null) { |
| OptionalInputMetadata metadataHolder = optionalInputs.get(targetExecPath); |
| if (metadataHolder != null) { |
| inputMetadata = metadataHolder.get(); |
| } |
| } |
| if (inputMetadata == null) { |
| throw new FileNotFoundException( |
| createSymbolicLinkErrorMessage( |
| linkPath, targetFragment, targetFragment + " is not an input.")); |
| } |
| OutputMetadata outputHolder = outputs.getUnchecked(asExecPath(linkPath)); |
| if (outputHolder == null) { |
| throw new FileNotFoundException( |
| createSymbolicLinkErrorMessage( |
| linkPath, targetFragment, linkPath + " is not an output.")); |
| } |
| if (sourceRootIndex >= 0) { |
| outputHolder.set( |
| new SourceFileArtifactValue( |
| targetExecPath, sourceRootIndex, inputMetadata.getDigest(), inputMetadata.getSize()), |
| true); |
| } else { |
| outputHolder.set(inputMetadata, /*notifyConsumer=*/ true); |
| } |
| } |
| |
| @Override |
| protected PathFragment readSymbolicLink(Path path) throws IOException { |
| throw new UnsupportedOperationException(path.getPathString()); |
| } |
| |
| @Override |
| protected boolean exists(Path path, boolean followSymlinks) { |
| Preconditions.checkArgument( |
| followSymlinks, "ActionFileSystem doesn't support no-follow: %s", path); |
| return getMetadataUnchecked(path) != null; |
| } |
| |
| |
| @Override |
| protected boolean isReadable(Path path) throws IOException { |
| return exists(path, true); |
| } |
| |
| @Override |
| protected void setReadable(Path path, boolean readable) throws IOException {} |
| |
| @Override |
| protected boolean isWritable(Path path) throws IOException { |
| return isOutput(path); |
| } |
| |
| @Override |
| public void setWritable(Path path, boolean writable) throws IOException {} |
| |
| @Override |
| protected boolean isExecutable(Path path) throws IOException { |
| return true; |
| } |
| |
| @Override |
| protected void setExecutable(Path path, boolean executable) throws IOException {} |
| |
| @Override |
| protected InputStream getInputStream(Path path) throws IOException { |
| FileArtifactValue metadata = getMetadataChecked(asExecPath(path)); |
| if (metadata instanceof InlineFileArtifactValue) { |
| return ((InlineFileArtifactValue) metadata).getInputStream(); |
| } |
| if (metadata instanceof SourceFileArtifactValue) { |
| return resolveSourcePath((SourceFileArtifactValue) metadata).getInputStream(); |
| } |
| Preconditions.checkArgument( |
| !(metadata instanceof RemoteFileArtifactValue), |
| "getInputStream called for remote file: %s", |
| path); |
| return delegate.getPath(path.asFragment()).getInputStream(); |
| } |
| |
| @Override |
| protected OutputStream getOutputStream(Path path, boolean append) { |
| Preconditions.checkArgument(!append, "ActionFileSystem doesn't support append."); |
| return outputs.getUnchecked(asExecPath(path)).getOutputStream(); |
| } |
| |
| @Override |
| public void renameTo(Path sourcePath, Path targetPath) throws IOException { |
| PathFragment sourceExecPath = asExecPath(sourcePath); |
| OutputMetadata sourceMetadata = outputs.getIfPresent(sourceExecPath); |
| if (sourceMetadata == null) { |
| throw new IOException("No output file at " + sourcePath + " to move to " + targetPath); |
| } |
| outputs.put(asExecPath(targetPath), sourceMetadata); |
| outputs.invalidate(sourceExecPath); |
| } |
| |
| @Override |
| protected void createFSDependentHardLink(Path linkPath, Path originalPath) throws IOException { |
| throw new UnsupportedOperationException( |
| "createFSDependendHardLink(" + linkPath + ", " + originalPath + ")"); |
| } |
| |
| // -------------------- Implementation Helpers -------------------- |
| |
| private PathFragment asExecPath(Path path) { |
| return asExecPath(path.asFragment()); |
| } |
| |
| private PathFragment asExecPath(PathFragment fragment) { |
| if (fragment.startsWith(execRootFragment)) { |
| return fragment.relativeTo(execRootFragment); |
| } |
| for (PathFragment root : sourceRoots) { |
| if (fragment.startsWith(root)) { |
| return fragment.relativeTo(root); |
| } |
| } |
| throw new IllegalArgumentException( |
| fragment + " was not found under any known root: " + execRootFragment + ", " + sourceRoots); |
| } |
| |
| @Nullable |
| private FileArtifactValue getMetadataChecked(PathFragment execPath) throws IOException { |
| { |
| FileArtifactValue metadata = inputArtifactData.getMetadata(execPath.getPathString()); |
| if (metadata != null) { |
| return metadata; |
| } |
| } |
| { |
| OptionalInputMetadata metadataHolder = optionalInputs.get(execPath); |
| if (metadataHolder != null) { |
| return metadataHolder.get(); |
| } |
| } |
| { |
| OutputMetadata metadataHolder = outputs.getIfPresent(execPath); |
| if (metadataHolder != null) { |
| FileArtifactValue metadata = metadataHolder.get(); |
| if (metadata != null) { |
| return metadata; |
| } |
| } |
| } |
| return null; |
| } |
| |
| private FileArtifactValue getMetadataOrThrowFileNotFound(Path path) throws IOException { |
| FileArtifactValue metadata = getMetadataChecked(asExecPath(path)); |
| if (metadata == null) { |
| throw new FileNotFoundException(path.getPathString() + " was not found"); |
| } |
| return metadata; |
| } |
| |
| @Nullable |
| private FileArtifactValue getMetadataUnchecked(Path path) { |
| try { |
| return getMetadataChecked(asExecPath(path)); |
| } catch (IOException e) { |
| throw new IllegalStateException( |
| "Error getting metadata for " + path.getPathString() + ": " + e.getMessage(), e); |
| } |
| } |
| |
| private boolean isOutput(Path path) { |
| // TODO(felly): This method should instead just refer to potential output paths, which are |
| // anything under the output tree. |
| PathFragment fragment = path.asFragment(); |
| if (!fragment.startsWith(execRootFragment)) { |
| return false; |
| } |
| return outputs.getIfPresent(fragment.relativeTo(execRootFragment)) != null; |
| } |
| |
| /** |
| * Verifies that no root is the prefix of any other root. |
| * |
| * <p>TODO(shahan): if this is insufficiently general, we can topologically order on the prefix |
| * relation between roots. |
| */ |
| private void validateRoots() { |
| for (PathFragment root1 : sourceRoots) { |
| Preconditions.checkState( |
| !root1.startsWith(execRootFragment), "%s starts with %s", root1, execRootFragment); |
| Preconditions.checkState( |
| !execRootFragment.startsWith(root1), "%s starts with %s", execRootFragment, root1); |
| for (PathFragment root2 : sourceRoots) { |
| if (root1 == root2) { |
| continue; |
| } |
| Preconditions.checkState(!root1.startsWith(root2), "%s starts with %s", root1, root2); |
| } |
| } |
| } |
| |
| private static ByteString toByteString(byte[] digest) { |
| return ByteString.copyFrom(BaseEncoding.base16().lowerCase().encode(digest).getBytes(US_ASCII)); |
| } |
| |
| /** NB: resolves to the underlying filesytem instead of this one. */ |
| private Path resolveSourcePath(SourceFileArtifactValue metadata) { |
| return delegate |
| .getPath(sourceRoots.get(metadata.getSourceRootIndex())) |
| .getRelative(metadata.getExecPath()); |
| } |
| |
| @FunctionalInterface |
| public interface MetadataConsumer { |
| void accept(Artifact artifact, FileArtifactValue value) throws IOException; |
| } |
| |
| private class OptionalInputMetadata { |
| private final Artifact artifact; |
| private volatile FileArtifactValue metadata = null; |
| |
| private OptionalInputMetadata(Artifact artifact) { |
| this.artifact = artifact; |
| } |
| |
| public Artifact getArtifact() { |
| return artifact; |
| } |
| |
| public FileArtifactValue get() throws IOException { |
| if (metadata == null) { |
| synchronized (this) { |
| if (metadata == null) { |
| try { |
| // TODO(shahan): {@link SkyFunction.Environment} requires single-threaded access so |
| // we enforce that here by making these (multithreaded) calls synchronized. It might |
| // be better to make the underlying methods synchronized to avoid having another |
| // caller unintentionally calling into the environment without locking. |
| // |
| // This is currently known to be reached from the distributor during remote include |
| // scanning which we expect to propagate exceptions up for skyframe restarts. |
| synchronized (env) { |
| metadata = (FileArtifactValue) env.getValue(ArtifactSkyKey.key(artifact, false)); |
| } |
| } catch (InterruptedException e) { |
| throw new InterruptedIOException(e.getMessage()); |
| } |
| if (metadata == null) { |
| throw new ActionExecutionFunction.MissingDepException(); |
| } |
| } |
| } |
| } |
| return metadata; |
| } |
| } |
| |
| private class OutputMetadata { |
| private final @Nullable Artifact artifact; |
| @Nullable private volatile FileArtifactValue metadata = null; |
| |
| private OutputMetadata(Artifact artifact) { |
| this.artifact = artifact; |
| } |
| |
| @Nullable |
| public FileArtifactValue get() { |
| return metadata; |
| } |
| |
| /** |
| * Sets the output metadata, and maybe notify the metadataConsumer. |
| * |
| * @param metadata the metadata to write |
| * @param notifyConsumer whether to notify metadataConsumer. Callers should not notify the |
| * metadataConsumer if it will be notified separately at the Spawn level. |
| */ |
| public void set(FileArtifactValue metadata, boolean notifyConsumer) throws IOException { |
| if (notifyConsumer && artifact != null) { |
| metadataConsumer.accept(artifact, metadata); |
| } |
| this.metadata = metadata; |
| } |
| |
| /** Callers are expected to close the returned stream. */ |
| public ByteArrayOutputStream getOutputStream() { |
| Preconditions.checkState(metadata == null, "getOutputStream called twice for: %s", artifact); |
| return new ByteArrayOutputStream() { |
| @Override |
| public void close() throws IOException { |
| super.close(); |
| byte[] data = toByteArray(); |
| set( |
| new InlineFileArtifactValue(data, Hashing.md5().hashBytes(data).asBytes()), |
| /*notifyConsumer=*/ true); |
| } |
| }; |
| } |
| } |
| } |