/* * 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.doctor; import com.facebook.buck.doctor.config.DoctorConfig; import com.facebook.buck.doctor.config.DoctorEndpointRequest; import com.facebook.buck.doctor.config.DoctorEndpointResponse; import com.facebook.buck.doctor.config.DoctorSuggestion; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.log.Logger; import com.facebook.buck.model.Pair; import com.facebook.buck.rage.BuildLogEntry; import com.facebook.buck.rage.DefectSubmitResult; import com.facebook.buck.rage.RageConfig; import com.facebook.buck.rage.UserInput; import com.facebook.buck.util.Console; import com.facebook.buck.util.DirtyPrintStreamDecorator; import com.facebook.buck.util.ObjectMappers; import com.facebook.buck.util.unit.SizeUnit; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.concurrent.TimeUnit; import okhttp3.FormBody; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class DoctorReportHelper { private static final Logger LOG = Logger.get(DoctorReportHelper.class); private static final int ARGS_MAX_CHARS = 60; private static final String WARNING_FILE_TEMPLATE = "Command %s does not contain a %s. Some " + "information will not be available"; private static final String DECODE_FAIL_TEMPLATE = "Decoding remote response failed. Reason: %s"; public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); private ProjectFilesystem filesystem; private UserInput input; private Console console; private DoctorConfig doctorConfig; public DoctorReportHelper( ProjectFilesystem filesystem, UserInput input, Console console, DoctorConfig doctorConfig) { this.filesystem = filesystem; this.input = input; this.console = console; this.doctorConfig = doctorConfig; } public Optional<BuildLogEntry> promptForBuild(List<BuildLogEntry> buildLogs) throws IOException { if (buildLogs.isEmpty()) { return Optional.empty(); } return input.selectOne( "Which buck invocation would you like to report?", buildLogs, entry -> { Pair<Double, SizeUnit> humanReadableSize = SizeUnit.getHumanReadableSize(entry.getSize(), SizeUnit.BYTES); String cmdArgs = entry.getCommandArgs().orElse("unknown command"); cmdArgs = cmdArgs.substring(0, Math.min(cmdArgs.length(), ARGS_MAX_CHARS)); return String.format( "\t%s\tbuck [%s] %s (%.2f %s)", entry.getLastModifiedTime(), cmdArgs, prettyPrintExitCode(entry.getExitCode()), humanReadableSize.getFirst(), humanReadableSize.getSecond().getAbbreviation()); }); } public DoctorEndpointRequest generateEndpointRequest( BuildLogEntry entry, DefectSubmitResult rageResult) throws IOException { Optional<String> machineLog; if (entry.getMachineReadableLogFile().isPresent()) { machineLog = Optional.of( Files.toString( filesystem.resolve(entry.getMachineReadableLogFile().get()).toFile(), Charsets.UTF_8)); } else { LOG.warn(String.format(WARNING_FILE_TEMPLATE, entry.toString(), "machine readable log")); machineLog = Optional.empty(); } return DoctorEndpointRequest.of( entry.getBuildId(), entry.getRelativePath().toString(), machineLog, rageResult.getReportSubmitMessage(), rageResult.getReportSubmitLocation()); } public DoctorEndpointResponse uploadRequest(DoctorEndpointRequest request) { if (!doctorConfig.getEndpointUrl().isPresent()) { String errorMsg = String.format( "Doctor endpoint URL is not set. Please set [%s] %s on your .buckconfig", DoctorConfig.DOCTOR_SECTION, DoctorConfig.URL_FIELD); return createErrorDoctorEndpointResponse(errorMsg); } byte[] requestJson; try { requestJson = ObjectMappers.WRITER.writeValueAsBytes(request); } catch (JsonProcessingException e) { return createErrorDoctorEndpointResponse( "Failed to encode request to JSON. " + "Reason: " + e.getMessage()); } OkHttpClient httpClient = new OkHttpClient.Builder() .connectTimeout(doctorConfig.getHttpTimeoutMs(), TimeUnit.MILLISECONDS) .readTimeout(doctorConfig.getHttpTimeoutMs(), TimeUnit.MILLISECONDS) .writeTimeout(doctorConfig.getHttpTimeoutMs(), TimeUnit.MILLISECONDS) .build(); Response httpResponse; try { RequestBody requestBody; ImmutableMap<String, String> extraArgs = doctorConfig.getExtraRequestArgs(); if (extraArgs.isEmpty()) { requestBody = RequestBody.create(JSON, requestJson); } else { FormBody.Builder formBody = new FormBody.Builder(); formBody.add("data", new String(requestJson)); for (Map.Entry<String, String> entry : extraArgs.entrySet()) { formBody.add(entry.getKey(), entry.getValue()); } requestBody = formBody.build(); } Request httpRequest = new Request.Builder().url(doctorConfig.getEndpointUrl().get()).post(requestBody).build(); httpResponse = httpClient.newCall(httpRequest).execute(); } catch (IOException e) { return createErrorDoctorEndpointResponse( "Failed to perform the request to " + doctorConfig.getEndpointUrl().get() + ". Reason: " + e.getMessage()); } try { if (httpResponse.isSuccessful()) { String body = new String(httpResponse.body().bytes(), Charsets.UTF_8); return ObjectMappers.readValue(body, DoctorEndpointResponse.class); } return createErrorDoctorEndpointResponse("Request was not successful."); } catch (IOException e) { return createErrorDoctorEndpointResponse(String.format(DECODE_FAIL_TEMPLATE, e.getMessage())); } } public final void presentResponse(DoctorEndpointResponse response) { DirtyPrintStreamDecorator output = console.getStdOut(); if (response.getErrorMessage().isPresent() && !response.getErrorMessage().get().isEmpty()) { LOG.warn(response.getErrorMessage().get()); output.println("=> " + response.getErrorMessage().get()); return; } if (response.getSuggestions().isEmpty()) { output.println(console.getAnsi().asWarningText("\n:: No available suggestions right now.")); } else { output.println(console.getAnsi().asInformationText("\n:: Suggestions")); response.getSuggestions().forEach(this::prettyPrintSuggestion); } output.println(); } public final void presentRageResult(Optional<DefectSubmitResult> result) { if (!result.isPresent()) { console.getStdOut().println("=> Failed to generate a rage DefectSubmitResult."); return; } DefectSubmitResult submitResult = result.get(); if (submitResult.getIsRequestSuccessful().isPresent()) { if (submitResult.getReportSubmitLocation().isPresent()) { if (submitResult.getRequestProtocol().equals(RageConfig.RageProtocolVersion.JSON)) { console .getStdOut() .printf( "=> Report was uploaded to %s.\n\n", submitResult.getReportSubmitLocation().get()); } else { console.getStdOut().printf("%s", submitResult.getReportSubmitLocation().get()); } } } else { console .getStdOut() .printf("=> Report saved at %s\n", submitResult.getReportSubmitLocation().get()); } } private void prettyPrintSuggestion(DoctorSuggestion suggestion) { console .getStdOut() .println( String.format( "- [%s]%s %s", console.getAnsi().isAnsiTerminal() ? suggestion.getStatus().getEmoji() + " " : suggestion.getStatus().getText(), suggestion.getArea().isPresent() ? ("[" + suggestion.getArea().get() + "]") : "", suggestion.getSuggestion())); } private String prettyPrintExitCode(OptionalInt exitCode) { String result = "Exit code: " + (exitCode.isPresent() ? Integer.toString(exitCode.getAsInt()) : "Unknown"); if (exitCode.isPresent() && console.getAnsi().isAnsiTerminal()) { if (exitCode.getAsInt() == 0) { return console.getAnsi().asGreenText(result); } else { return console.getAnsi().asRedText(result); } } return result; } private DoctorEndpointResponse createErrorDoctorEndpointResponse(String errorMessage) { console.printErrorText(errorMessage); LOG.error(errorMessage); return DoctorEndpointResponse.of(Optional.of(errorMessage), ImmutableList.of()); } @VisibleForTesting Console getConsole() { return console; } }