/* * 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.rage; import static com.facebook.buck.rage.AbstractRageConfig.RageProtocolVersion; import com.facebook.buck.cli.BuckConfig; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.log.LogConfigPaths; import com.facebook.buck.log.Logger; import com.facebook.buck.util.Console; import com.facebook.buck.util.OptionalCompat; import com.facebook.buck.util.Optionals; import com.facebook.buck.util.environment.BuildEnvironmentDescription; import com.facebook.buck.util.immutables.BuckStyleImmutable; import com.facebook.buck.util.versioncontrol.FullVersionControlStats; import com.facebook.buck.util.versioncontrol.VersionControlStatsGenerator; import com.google.common.base.Function; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import org.immutables.value.Value; /** Base class for gathering logs and other interesting information from buck. */ public abstract class AbstractReport { private static final Logger LOG = Logger.get(AbstractReport.class); private final ProjectFilesystem filesystem; private final DefectReporter defectReporter; private final BuildEnvironmentDescription buildEnvironmentDescription; private final VersionControlStatsGenerator versionControlStatsGenerator; private final Console output; private final RageConfig rageConfig; private final ExtraInfoCollector extraInfoCollector; private final Optional<WatchmanDiagReportCollector> watchmanDiagReportCollector; public AbstractReport( ProjectFilesystem filesystem, DefectReporter defectReporter, BuildEnvironmentDescription buildEnvironmentDescription, VersionControlStatsGenerator versionControlStatsGenerator, Console output, RageConfig rageBuckConfig, ExtraInfoCollector extraInfoCollector, Optional<WatchmanDiagReportCollector> watchmanDiagReportCollector) { this.filesystem = filesystem; this.defectReporter = defectReporter; this.buildEnvironmentDescription = buildEnvironmentDescription; this.versionControlStatsGenerator = versionControlStatsGenerator; this.output = output; this.rageConfig = rageBuckConfig; this.extraInfoCollector = extraInfoCollector; this.watchmanDiagReportCollector = watchmanDiagReportCollector; } protected abstract ImmutableSet<BuildLogEntry> promptForBuildSelection() throws IOException; protected Optional<SourceControlInfo> getSourceControlInfo() throws IOException, InterruptedException { Optional<FullVersionControlStats> versionControlStatsOptional = versionControlStatsGenerator.generateStats(VersionControlStatsGenerator.Mode.FULL); if (!versionControlStatsOptional.isPresent()) { return Optional.empty(); } FullVersionControlStats versionControlStats = versionControlStatsOptional.get(); return Optional.of( SourceControlInfo.of( versionControlStats.getCurrentRevisionId(), versionControlStats.getBaseBookmarks(), Optional.of(versionControlStats.getBranchedFromMasterRevisionId()), Optional.of(versionControlStats.getBranchedFromMasterTS()), versionControlStats.getDiff(), versionControlStats.getPathsChangedInWorkingDirectory())); } protected abstract Optional<UserReport> getUserReport() throws IOException; protected abstract Optional<FileChangesIgnoredReport> getFileChangesIgnoredReport() throws IOException, InterruptedException; public final Optional<DefectSubmitResult> collectAndSubmitResult() throws IOException, InterruptedException { ImmutableSet<BuildLogEntry> selectedBuilds = promptForBuildSelection(); if (selectedBuilds.isEmpty()) { return Optional.empty(); } Optional<UserReport> userReport = getUserReport(); Optional<SourceControlInfo> sourceControlInfo = getSourceControlInfo(); ImmutableSet<Path> extraInfoPaths = ImmutableSet.of(); Optional<String> extraInfo = Optional.empty(); try { Optional<ExtraInfoResult> extraInfoResultOptional = extraInfoCollector.run(); if (extraInfoResultOptional.isPresent()) { extraInfoPaths = extraInfoResultOptional.get().getExtraFiles(); extraInfo = Optional.of(extraInfoResultOptional.get().getOutput()); } } catch (DefaultExtraInfoCollector.ExtraInfoExecutionException e) { output.printErrorText( "There was a problem gathering additional information: %s. " + "The results will not be attached to the report.", e.getMessage()); } Optional<FileChangesIgnoredReport> fileChangesIgnoredReport = getFileChangesIgnoredReport(); UserLocalConfiguration userLocalConfiguration = UserLocalConfiguration.of(isNoBuckCheckPresent(), getLocalConfigs()); ImmutableSet<Path> includedPaths = FluentIterable.from(selectedBuilds) .transformAndConcat( new Function<BuildLogEntry, Iterable<Path>>() { @Override public Iterable<Path> apply(BuildLogEntry input) { ImmutableSet.Builder<Path> result = ImmutableSet.builder(); Optionals.addIfPresent(input.getRuleKeyLoggerLogFile(), result); Optionals.addIfPresent(input.getMachineReadableLogFile(), result); result.add(input.getRelativePath()); return result.build(); } }) .append(extraInfoPaths) .append(userLocalConfiguration.getLocalConfigsContents().keySet()) .append(getTracePathsOfBuilds(selectedBuilds)) .append( fileChangesIgnoredReport .flatMap(r -> r.getWatchmanDiagReport()) .map(ImmutableList::of) .orElse(ImmutableList.of())) .toSet(); DefectReport defectReport = DefectReport.builder() .setUserReport(userReport) .setHighlightedBuildIds( FluentIterable.from(selectedBuilds) .transformAndConcat((x) -> OptionalCompat.asSet(x.getBuildId()))) .setBuildEnvironmentDescription(buildEnvironmentDescription) .setSourceControlInfo(sourceControlInfo) .setIncludedPaths(includedPaths) .setExtraInfo(extraInfo) .setFileChangesIgnoredReport(fileChangesIgnoredReport) .setUserLocalConfiguration(userLocalConfiguration) .build(); output.getStdOut().println("Writing report, please wait..\n"); return Optional.of(defectReporter.submitReport(defectReport)); } public void presentDefectSubmitResult( Optional<DefectSubmitResult> defectSubmitResult, boolean showJson) { if (!defectSubmitResult.isPresent()) { output.printErrorText( "No logs of interesting commands were found. Check if buck-out/log " + "contains commands except buck launch & buck rage."); return; } DefectSubmitResult result = defectSubmitResult.get(); LOG.debug("Got defect submit result %s", result); // If request has an empty isRequestSuccessful, it means we did not try to upload it somewhere. if (!result.getIsRequestSuccessful().isPresent()) { if (result.getReportSubmitLocation().isPresent()) { output.printSuccess("Report saved at %s", result.getReportSubmitLocation().get()); } else { output.printErrorText( "=> Failed to save report locally. Reason: %s", result.getReportSubmitErrorMessage().orElse("Unknown")); } return; } if (result.getIsRequestSuccessful().get()) { if (result.getRequestProtocol().equals(RageProtocolVersion.SIMPLE)) { output.getStdOut().printf("%s", result.getReportSubmitMessage().get()); } else { String message = "=> Upload was successful.\n"; if (result.getReportSubmitLocation().isPresent()) { message += "=> Report was uploaded to " + result.getReportSubmitLocation().get() + "\n"; } if (result.getReportSubmitMessage().isPresent() && showJson) { message += "=> Full Response was: " + result.getReportSubmitMessage().get() + "\n"; } output.getStdOut().print(message); } } else { output.printErrorText( "=> Failed to upload report because of error: %s.\n=> Report was saved locally at %s", result.getReportSubmitErrorMessage().get(), result.getReportSubmitLocation()); } } @Value.Immutable @BuckStyleImmutable interface AbstractUserReport { String getUserIssueDescription(); } private ImmutableMap<Path, String> getLocalConfigs() { Path rootPath = filesystem.getRootPath(); ImmutableSet<Path> knownUserLocalConfigs = ImmutableSet.of( Paths.get(BuckConfig.BUCK_CONFIG_OVERRIDE_FILE_NAME), LogConfigPaths.LOCAL_PATH, Paths.get(".watchman.local"), Paths.get(".buckjavaargs.local"), Paths.get(".bucklogging.local.properties")); ImmutableMap.Builder<Path, String> localConfigs = ImmutableMap.builder(); for (Path localConfig : knownUserLocalConfigs) { try { localConfigs.put( localConfig, new String(Files.readAllBytes(rootPath.resolve(localConfig)), StandardCharsets.UTF_8)); } catch (FileNotFoundException e) { LOG.debug("%s was not found.", localConfig); } catch (IOException e) { LOG.warn("Failed to read contents of %s.", rootPath.resolve(localConfig).toString()); } } return localConfigs.build(); } /** * It returns a list of trace files that corresponds to builds while respecting the maximum size * of the final zip file. * * @param entries the highlighted builds * @return a set of paths that points to the corresponding traces. */ private ImmutableSet<Path> getTracePathsOfBuilds(ImmutableSet<BuildLogEntry> entries) { ImmutableSet.Builder<Path> tracePaths = new ImmutableSet.Builder<>(); long reportSizeBytes = 0; for (BuildLogEntry entry : entries) { reportSizeBytes += entry.getSize(); if (entry.getTraceFile().isPresent()) { try { Path traceFile = filesystem.getPathForRelativeExistingPath(entry.getTraceFile().get()); long traceFileSizeBytes = Files.size(traceFile); if (rageConfig.getReportMaxSizeBytes().isPresent()) { if (reportSizeBytes + traceFileSizeBytes < rageConfig.getReportMaxSizeBytes().get()) { tracePaths.add(entry.getTraceFile().get()); reportSizeBytes += traceFileSizeBytes; } } else { tracePaths.add(entry.getTraceFile().get()); reportSizeBytes += traceFileSizeBytes; } } catch (IOException e) { LOG.info("Trace path %s wasn't valid, skipping it.", entry.getTraceFile().get()); } } } return tracePaths.build(); } private boolean isNoBuckCheckPresent() { return Files.exists(filesystem.getRootPath().resolve(".nobuckcheck")); } protected Optional<FileChangesIgnoredReport> runWatchmanDiagReportCollector(UserInput input) throws IOException, InterruptedException { if (!watchmanDiagReportCollector.isPresent() || !input.confirm( "Is buck not picking up changes to files? " + "(saying 'yes' will run extra consistency checks)")) { return Optional.empty(); } Optional<Path> watchmanDiagReport = Optional.empty(); try { watchmanDiagReport = Optional.of(watchmanDiagReportCollector.get().run()); } catch (ExtraInfoCollector.ExtraInfoExecutionException e) { output.printErrorText( "There was a problem getting the watchman-diag report: %s. " + "The information will be omitted from the report.", e); } return Optional.of( FileChangesIgnoredReport.builder().setWatchmanDiagReport(watchmanDiagReport).build()); } }