| // 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.exec; | |
| import build.bazel.remote.execution.v2.Platform; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.hash.HashCode; | |
| import com.google.devtools.build.lib.actions.ActionContext; | |
| import com.google.devtools.build.lib.actions.ActionInput; | |
| import com.google.devtools.build.lib.actions.ExecException; | |
| import com.google.devtools.build.lib.actions.ExecutionStrategy; | |
| import com.google.devtools.build.lib.actions.FileArtifactValue; | |
| import com.google.devtools.build.lib.actions.MetadataProvider; | |
| import com.google.devtools.build.lib.actions.Spawn; | |
| import com.google.devtools.build.lib.actions.SpawnResult; | |
| import com.google.devtools.build.lib.actions.Spawns; | |
| import com.google.devtools.build.lib.actions.cache.VirtualActionInput; | |
| import com.google.devtools.build.lib.analysis.platform.PlatformUtils; | |
| import com.google.devtools.build.lib.exec.Protos.Digest; | |
| import com.google.devtools.build.lib.exec.Protos.File; | |
| import com.google.devtools.build.lib.exec.Protos.SpawnExec; | |
| import com.google.devtools.build.lib.remote.options.RemoteOptions; | |
| import com.google.devtools.build.lib.util.io.MessageOutputStream; | |
| import com.google.devtools.build.lib.vfs.DigestHashFunction; | |
| import com.google.devtools.build.lib.vfs.Dirent; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.Symlinks; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.IOException; | |
| import java.time.Duration; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.Comparator; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.SortedMap; | |
| import java.util.TreeMap; | |
| import java.util.TreeSet; | |
| import java.util.function.Consumer; | |
| import java.util.logging.Level; | |
| import java.util.logging.Logger; | |
| import javax.annotation.Nullable; | |
| /** | |
| * A logging utility for spawns. | |
| */ | |
| @ExecutionStrategy( | |
| name = {"spawn-log"}, | |
| contextType = SpawnLogContext.class | |
| ) | |
| public class SpawnLogContext implements ActionContext { | |
| private static final Logger logger = Logger.getLogger(SpawnLogContext.class.getName()); | |
| private final Path execRoot; | |
| private final MessageOutputStream executionLog; | |
| @Nullable private final RemoteOptions remoteOptions; | |
| public SpawnLogContext( | |
| Path execRoot, MessageOutputStream executionLog, @Nullable RemoteOptions remoteOptions) { | |
| this.execRoot = execRoot; | |
| this.executionLog = executionLog; | |
| this.remoteOptions = remoteOptions; | |
| } | |
| /** Log the executed spawn to the output stream. */ | |
| public void logSpawn( | |
| Spawn spawn, | |
| MetadataProvider metadataProvider, | |
| SortedMap<PathFragment, ActionInput> inputMap, | |
| Duration timeout, | |
| SpawnResult result) | |
| throws IOException, ExecException { | |
| SortedMap<Path, ActionInput> existingOutputs = listExistingOutputs(spawn); | |
| SpawnExec.Builder builder = SpawnExec.newBuilder(); | |
| builder.addAllCommandArgs(spawn.getArguments()); | |
| Map<String, String> env = spawn.getEnvironment(); | |
| // Sorting the environment pairs by variable name. | |
| TreeSet<String> variables = new TreeSet<>(env.keySet()); | |
| for (String var : variables) { | |
| builder.addEnvironmentVariablesBuilder().setName(var).setValue(env.get(var)); | |
| } | |
| try { | |
| for (Map.Entry<PathFragment, ActionInput> e : inputMap.entrySet()) { | |
| ActionInput input = e.getValue(); | |
| Path inputPath = execRoot.getRelative(input.getExecPathString()); | |
| if (inputPath.isDirectory()) { | |
| listDirectoryContents(inputPath, (file) -> builder.addInputs(file), metadataProvider); | |
| } else { | |
| Digest digest = computeDigest(input, null, metadataProvider); | |
| builder.addInputsBuilder().setPath(input.getExecPathString()).setDigest(digest); | |
| } | |
| } | |
| } catch (IOException e) { | |
| logger.log(Level.WARNING, "Error computing spawn inputs", e); | |
| } | |
| ArrayList<String> outputPaths = new ArrayList<>(); | |
| for (ActionInput output : spawn.getOutputFiles()) { | |
| outputPaths.add(output.getExecPathString()); | |
| } | |
| Collections.sort(outputPaths); | |
| builder.addAllListedOutputs(outputPaths); | |
| for (Map.Entry<Path, ActionInput> e : existingOutputs.entrySet()) { | |
| Path path = e.getKey(); | |
| if (path.isDirectory()) { | |
| listDirectoryContents(path, (file) -> builder.addActualOutputs(file), metadataProvider); | |
| } else { | |
| File.Builder outputBuilder = builder.addActualOutputsBuilder(); | |
| outputBuilder.setPath(path.relativeTo(execRoot).toString()); | |
| try { | |
| outputBuilder.setDigest(computeDigest(e.getValue(), path, metadataProvider)); | |
| } catch (IOException ex) { | |
| logger.log(Level.WARNING, "Error computing spawn event output properties", ex); | |
| } | |
| } | |
| } | |
| builder.setRemotable(Spawns.mayBeExecutedRemotely(spawn)); | |
| Platform execPlatform = | |
| PlatformUtils.getPlatformProto(spawn.getExecutionPlatform(), remoteOptions); | |
| if (execPlatform != null) { | |
| builder.setPlatform(buildPlatform(execPlatform)); | |
| } | |
| if (result.status() != SpawnResult.Status.SUCCESS) { | |
| builder.setStatus(result.status().toString()); | |
| } | |
| if (!timeout.isZero()) { | |
| builder.setTimeoutMillis(timeout.toMillis()); | |
| } | |
| builder.setCacheable(Spawns.mayBeCached(spawn)); | |
| builder.setExitCode(result.exitCode()); | |
| builder.setRemoteCacheHit(result.isCacheHit()); | |
| builder.setRunner(result.getRunnerName()); | |
| String progressMessage = spawn.getResourceOwner().getProgressMessage(); | |
| if (progressMessage != null) { | |
| builder.setProgressMessage(progressMessage); | |
| } | |
| builder.setMnemonic(spawn.getMnemonic()); | |
| executionLog.write(builder.build()); | |
| } | |
| public void close() throws IOException { | |
| executionLog.close(); | |
| } | |
| private static Protos.Platform buildPlatform(Platform platform) { | |
| Protos.Platform.Builder platformBuilder = Protos.Platform.newBuilder(); | |
| for (Platform.Property p : platform.getPropertiesList()) { | |
| platformBuilder.addPropertiesBuilder().setName(p.getName()).setValue(p.getValue()); | |
| } | |
| return platformBuilder.build(); | |
| } | |
| private SortedMap<Path, ActionInput> listExistingOutputs(Spawn spawn) { | |
| TreeMap<Path, ActionInput> result = new TreeMap<>(); | |
| for (ActionInput output : spawn.getOutputFiles()) { | |
| Path outputPath = execRoot.getRelative(output.getExecPathString()); | |
| // TODO(olaola): once symlink API proposal is implemented, report symlinks here. | |
| if (outputPath.exists()) { | |
| result.put(outputPath, output); | |
| } | |
| } | |
| return result; | |
| } | |
| private void listDirectoryContents( | |
| Path path, Consumer<File> addFile, MetadataProvider metadataProvider) { | |
| try { | |
| // TODO(olaola): once symlink API proposal is implemented, report symlinks here. | |
| List<Dirent> sortedDirent = new ArrayList<>(path.readdir(Symlinks.NOFOLLOW)); | |
| sortedDirent.sort(Comparator.comparing(Dirent::getName)); | |
| for (Dirent dirent : sortedDirent) { | |
| String name = dirent.getName(); | |
| Path child = path.getRelative(name); | |
| if (dirent.getType() == Dirent.Type.DIRECTORY) { | |
| listDirectoryContents(child, addFile, metadataProvider); | |
| } else { | |
| addFile.accept( | |
| File.newBuilder() | |
| .setPath(child.relativeTo(execRoot).toString()) | |
| .setDigest(computeDigest(null, child, metadataProvider)) | |
| .build()); | |
| } | |
| } | |
| } catch (IOException e) { | |
| logger.log(Level.WARNING, "Error computing spawn event file properties", e); | |
| } | |
| } | |
| /** | |
| * Computes the digest of the given ActionInput or corresponding path. Will try to access the | |
| * Metadata cache first, if it is available, and fall back to digesting the contents manually. | |
| */ | |
| private Digest computeDigest( | |
| @Nullable ActionInput input, @Nullable Path path, MetadataProvider metadataProvider) | |
| throws IOException { | |
| Preconditions.checkArgument(input != null || path != null); | |
| DigestHashFunction hashFunction = execRoot.getFileSystem().getDigestFunction(); | |
| Digest.Builder digest = Digest.newBuilder().setHashFunctionName(hashFunction.toString()); | |
| if (input != null) { | |
| if (input instanceof VirtualActionInput) { | |
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | |
| ((VirtualActionInput) input).writeTo(buffer); | |
| byte[] blob = buffer.toByteArray(); | |
| return digest | |
| .setHash(hashFunction.getHashFunction().hashBytes(blob).toString()) | |
| .setSizeBytes(blob.length) | |
| .build(); | |
| } | |
| // Try to access the cached metadata, otherwise fall back to local computation. | |
| try { | |
| FileArtifactValue metadata = metadataProvider.getMetadata(input); | |
| if (metadata != null) { | |
| byte[] hash = metadata.getDigest(); | |
| if (hash != null) { | |
| return digest | |
| .setHash(HashCode.fromBytes(hash).toString()) | |
| .setSizeBytes(metadata.getSize()) | |
| .build(); | |
| } | |
| } | |
| } catch (IOException | IllegalStateException e) { | |
| // Pass through to local computation. | |
| } | |
| } | |
| if (path == null) { | |
| path = execRoot.getRelative(input.getExecPath()); | |
| } | |
| // Compute digest manually. | |
| return digest | |
| .setHash(HashCode.fromBytes(path.getDigest()).toString()) | |
| .setSizeBytes(path.getFileSize()) | |
| .build(); | |
| } | |
| } |