/* * 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.zip.ZipOutputStreams.HandleDuplicates.APPEND_TO_ZIP; import com.facebook.buck.event.BuckEventBus; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.log.Logger; import com.facebook.buck.slb.ClientSideSlb; import com.facebook.buck.slb.HttpResponse; import com.facebook.buck.slb.HttpService; import com.facebook.buck.slb.LoadBalancedService; import com.facebook.buck.slb.RetryingHttpService; import com.facebook.buck.slb.SlbBuckConfig; import com.facebook.buck.timing.Clock; import com.facebook.buck.util.ObjectMappers; import com.facebook.buck.zip.CustomZipEntry; import com.facebook.buck.zip.CustomZipOutputStream; import com.facebook.buck.zip.ZipOutputStreams; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okio.BufferedSink; /** Takes care of actually writing out the report. */ public class DefaultDefectReporter implements DefectReporter { private static final Logger LOG = Logger.get(AbstractReport.class); private static final String REPORT_FILE_NAME = "report.json"; private static final String DIFF_FILE_NAME = "changes.diff"; private static final int HTTP_SUCCESS_CODE = 200; private static final String REQUEST_PROTOCOL_VERSION = "x-buck-protocol-version"; private final ProjectFilesystem filesystem; private final RageConfig rageConfig; private final BuckEventBus buckEventBus; private final Clock clock; public DefaultDefectReporter( ProjectFilesystem filesystem, RageConfig rageConfig, BuckEventBus buckEventBus, Clock clock) { this.filesystem = filesystem; this.rageConfig = rageConfig; this.buckEventBus = buckEventBus; this.clock = clock; } private void addFilesToArchive(CustomZipOutputStream out, ImmutableSet<Path> paths) throws IOException { for (Path logFile : paths) { Preconditions.checkArgument(!logFile.isAbsolute(), "Should be a relative Path.", logFile); // If the file is hidden(UNIX terms) save it as normal file. if (logFile.getFileName().toString().startsWith(".")) { out.putNextEntry( new CustomZipEntry(Paths.get(logFile.getFileName().toString().substring(1)))); } else { out.putNextEntry(new CustomZipEntry(logFile)); } try (InputStream input = filesystem.newFileInputStream(logFile)) { ByteStreams.copy(input, out); } out.closeEntry(); } } private void addStringsAsFilesToArchive( CustomZipOutputStream out, ImmutableMap<String, String> files) throws IOException { for (Map.Entry<String, String> file : files.entrySet()) { out.putNextEntry(new CustomZipEntry(file.getKey())); out.write(file.getValue().getBytes(Charsets.UTF_8)); out.closeEntry(); } } @Override public DefectSubmitResult submitReport(DefectReport defectReport) throws IOException { DefectSubmitResult.Builder defectSubmitResult = DefectSubmitResult.builder(); defectSubmitResult.setRequestProtocol(rageConfig.getProtocolVersion()); Optional<SlbBuckConfig> frontendConfig = rageConfig.getFrontendConfig(); if (frontendConfig.isPresent()) { Optional<ClientSideSlb> slb = frontendConfig.get().tryCreatingClientSideSlb(clock, buckEventBus); if (slb.isPresent()) { try { return uploadReport(defectReport, defectSubmitResult, slb.get()); } catch (IOException e) { LOG.debug(e, "Failed uploading report to server."); defectSubmitResult.setIsRequestSuccessful(false); defectSubmitResult.setReportSubmitErrorMessage(e.getMessage()); } } } filesystem.mkdirs(filesystem.getBuckPaths().getBuckOut()); Path defectReportPath = filesystem.createTempFile(filesystem.getBuckPaths().getBuckOut(), "defect_report", ".zip"); try (OutputStream outputStream = filesystem.newFileOutputStream(defectReportPath)) { writeReport(defectReport, outputStream); } return defectSubmitResult .setIsRequestSuccessful(Optional.empty()) .setReportSubmitLocation(defectReportPath.toString()) .build(); } private void writeReport(DefectReport defectReport, OutputStream outputStream) throws IOException { try (BufferedOutputStream baseOut = new BufferedOutputStream(outputStream); CustomZipOutputStream out = ZipOutputStreams.newOutputStream(baseOut, APPEND_TO_ZIP)) { if (defectReport.getSourceControlInfo().isPresent() && defectReport.getSourceControlInfo().get().getDiff().isPresent()) { addStringsAsFilesToArchive( out, ImmutableMap.of( DIFF_FILE_NAME, defectReport.getSourceControlInfo().get().getDiff().get())); } addFilesToArchive(out, defectReport.getIncludedPaths()); out.putNextEntry(new CustomZipEntry(REPORT_FILE_NAME)); ObjectMappers.WRITER.writeValue(out, defectReport); } } private DefectSubmitResult uploadReport( final DefectReport defectReport, DefectSubmitResult.Builder defectSubmitResult, ClientSideSlb slb) throws IOException { long timeout = rageConfig.getHttpTimeout(); OkHttpClient httpClient = new OkHttpClient.Builder() .connectTimeout(timeout, TimeUnit.MILLISECONDS) .readTimeout(timeout, TimeUnit.MILLISECONDS) .writeTimeout(timeout, TimeUnit.MILLISECONDS) .build(); HttpService httpService = new RetryingHttpService( buckEventBus, new LoadBalancedService(slb, httpClient, buckEventBus), rageConfig.getMaxUploadRetries()); try { Request.Builder requestBuilder = new Request.Builder(); requestBuilder.addHeader( REQUEST_PROTOCOL_VERSION, rageConfig.getProtocolVersion().name().toLowerCase()); requestBuilder.post( new RequestBody() { @Override public MediaType contentType() { return MediaType.parse("application/x-www-form-urlencoded"); } @Override public void writeTo(BufferedSink bufferedSink) throws IOException { writeReport(defectReport, bufferedSink.outputStream()); } }); HttpResponse response = httpService.makeRequest(rageConfig.getReportUploadPath(), requestBuilder); String responseBody; try (InputStream inputStream = response.getBody()) { responseBody = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); } if (response.statusCode() == HTTP_SUCCESS_CODE) { defectSubmitResult.setIsRequestSuccessful(true); if (rageConfig.getProtocolVersion().equals(AbstractRageConfig.RageProtocolVersion.SIMPLE)) { return defectSubmitResult .setReportSubmitMessage(responseBody) .setReportSubmitLocation(responseBody) .build(); } else { // Decode Json response. RageJsonResponse json = ObjectMappers.READER.readValue( ObjectMappers.createParser(responseBody.getBytes(Charsets.UTF_8)), RageJsonResponse.class); return defectSubmitResult .setIsRequestSuccessful(json.getRequestSuccessful()) .setReportSubmitErrorMessage(json.getErrorMessage()) .setReportSubmitMessage(json.getMessage()) .setReportSubmitLocation(json.getRageUrl()) .build(); } } else { throw new IOException( String.format( "Connection to %s returned code %d and message: %s", response.requestUrl(), response.statusCode(), responseBody)); } } catch (IOException e) { throw new IOException(String.format("Failed uploading report because [%s].", e.getMessage())); } finally { httpService.close(); } } }