view mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java @ 930:1f19b1a10edb

fix TW-40598. Report HG stdout errors ony if error is not empty and not spaces (was only non-empty check)
author eugene.petrenko@jetbrains.com
date Fri, 27 Mar 2015 20:28:33 +0100
parents e467e67d5892
children 9ad288b299ea
line wrap: on
line source
/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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 jetbrains.buildServer.buildTriggers.vcs.mercurial.command;

import com.intellij.execution.process.ProcessNotCreatedException;
import com.intellij.openapi.diagnostic.Logger;
import jetbrains.buildServer.ExecResult;
import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.*;
import jetbrains.buildServer.util.StringUtil;
import jetbrains.buildServer.vcs.VcsException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import static com.intellij.openapi.util.text.StringUtil.isEmpty;
import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces;
import static java.util.Arrays.asList;
import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil.removePrivateData;

/**
 * Mercurial command result. Filters out private data from stdout and detects errors.
 *
 * @author dmitry.neverov
 */
public class CommandResult {

  //Mercurial returns -1 in the case of errors (see dispatch.py)
  //and on some shells (e.g. windows cmd) it is truncated to 255.
  //A non-zero exit code is not always an error:
  //http://mercurial.selenic.com/bts/issue186
  //http://mercurial.selenic.com/bts/issue2189
  //e.g. pull command in hg 2.1 exits with 1 if no new changes were pulled.
  private static final Set<Integer> ERROR_EXIT_CODES = setOf(-1, 255);

  private static final String MERCURIAL_NOT_FOUND_MESSAGE_PREFIX = "Cannot run program \"";
  private static final String MERCURIAL_NOT_FOUND_MESSAGE_SUFFIX1 = "No such file or directory";
  private static final String MERCURIAL_NOT_FOUND_MESSAGE_SUFFIX2 = "The system cannot find the file specified";

  private final Logger myLogger;
  private final String myCommand;
  private final ExecResult myDelegate;
  private final Set<String> myPrivateData;
  private final CommandSettings mySettings;
  private final String myCommandWorkingDir;

  public CommandResult(@NotNull Logger logger,
                       @NotNull String command,
                       @NotNull ExecResult execResult,
                       @NotNull Set<String> privateData,
                       @NotNull CommandSettings settings,
                       @Nullable String commandWorkingDir) {
    myLogger = logger;
    myCommand = command;
    myDelegate = execResult;
    myPrivateData = privateData;
    mySettings = settings;
    myCommandWorkingDir = commandWorkingDir;
  }

  public int getExitCode() {
    return myDelegate.getExitCode();
  }

  @NotNull
  public String getSecureStdout() {
    return removePrivateData(myDelegate.getStdout(), myPrivateData);
  }

  @NotNull
  public String getRawStdout() {
    return myDelegate.getStdout();
  }

  public void checkCommandFailed() throws VcsException {
    checkFailure(false);
  }

  public void checkFailure(boolean failWhenStderrIsNonEmpty) throws VcsException {
    rethrowDetectedError();
    if (isFailure())
      logAndThrowError();
    String stderr = getSecureStderr();
    if (!isEmptyOrSpaces(stderr)) {
      if (failWhenStderrIsNonEmpty)
        logAndThrowError();
      else
        logStderr(stderr);
    }
  }

  private void logAndThrowError() throws VcsException {
    String message = createCommandLogMessage();
    myLogger.warn(message);
    if (hasImportantException())
      myLogger.error("Error during executing '" + getCommand() + "'", getException());
    throwVcsException(getExceptionMessage());
  }

  private void throwVcsException(@NotNull String message) throws VcsException {
    //noinspection ThrowableResultOfMethodCallIgnored
    Throwable e = getException();
    if (isMercurialNotFoundException(e)) {
      assert e != null;
      throw new MercurialNotFoundException(getCommand(), e);
    }
    if (e != null)
      throw new VcsException(message, e);
    throw new VcsException(message);
  }

  private boolean isMercurialNotFoundException(@Nullable Throwable e) {
    return e instanceof ProcessNotCreatedException &&
           e.getCause() instanceof IOException &&
           isMercurialNotFoundErrorMessage(e.getMessage());
  }

  private boolean isMercurialNotFoundErrorMessage(@Nullable String message) {
    return message != null &&
           (message.startsWith(MERCURIAL_NOT_FOUND_MESSAGE_PREFIX) &&
            (message.endsWith(MERCURIAL_NOT_FOUND_MESSAGE_SUFFIX1) ||
             message.endsWith(MERCURIAL_NOT_FOUND_MESSAGE_SUFFIX2)) ||
           message.startsWith("CreateProcess") && message.endsWith("error=2"));
  }

  private void logStderr(String stderr) {
    myLogger.warn("Error output produced by: " + getCommand());
    myLogger.warn(stderr);
  }

  @NotNull
  private String getSecureStderr() {
    return removePrivateData(myDelegate.getStderr(), myPrivateData);
  }

  @NotNull
  private String getRawStderr() {
    return myDelegate.getStderr();
  }

  @Nullable
  private Throwable getException() {
    return myDelegate.getException();
  }

  private boolean isFailure() {
    //noinspection ThrowableResultOfMethodCallIgnored
    return getException() != null || isErrorExitCode();
  }

  private boolean isErrorExitCode() {
    int exitCode = myDelegate.getExitCode();
    return ERROR_EXIT_CODES.contains(exitCode);
  }

  private boolean shouldDetectErrors() {
    return isFailure() || myDelegate.getExitCode() != 0;
  }

  @NotNull
  private String getCommand() {
    return removePrivateData(myCommand, myPrivateData);
  }

  private boolean hasImportantException() {
    //noinspection ThrowableResultOfMethodCallIgnored
    Throwable exception = getException();
    return exception instanceof NullPointerException;
  }

  @NotNull
  private String createCommandLogMessage() {
    StringBuilder message = new StringBuilder();
    message.append("'");
    if (!isEmpty(myCommandWorkingDir))
      message.append("[").append(myCommandWorkingDir).append("] ");
    message.append(getCommand()).append("' command failed.");
    String stderr = getSecureStderr();
    if (!isEmptyOrSpaces(stderr)) {
      int logOutputLimit = mySettings.getLogOutputLimit();
      if (logOutputLimit == -1) {
        message.append("\nstderr:\n").append(stderr);
      } else {
        message.append(StringUtil.truncateStringValueWithDotsAtEnd(stderr, logOutputLimit));
      }
    }
    return message.toString();
  }

  private String getExceptionMessage() {
    StringBuilder message = new StringBuilder();
    message.append("'").append(getCommand()).append("' command failed.");
    String stderr = getSecureStderr();
    if (!isEmptyOrSpaces(stderr)) {
      message.append("\n");
      int limit = mySettings.getExceptionOutputLimit();
      if (stderr.length() < limit || limit == -1) {
        message.append("stderr: ").append(stderr);
      } else {
        if (limit > 4)
          message.append("stderr: ").append(StringUtil.truncateStringValueWithDotsAtEnd(stderr, limit));
        message.append("\nSee details in teamcity-vcs.log");
      }
    }
    return message.toString();
  }

  private void rethrowDetectedError() throws VcsException {
    if (!shouldDetectErrors())
      return;
    String stderr = getRawStderr().trim();
    checkUnrelatedRepository(stderr);
    checkUnknownRevision(stderr);
    checkFileNotUnderTheRoot(stderr);
    checkConnectionRefused(stderr);
    checkAbandonedTransaction(stderr);
    checkMergeWithWorkDirAncestor(stderr);
    checkNothingToMerge(stderr);
    checkUnknownException(stderr);
  }

  private void checkUnrelatedRepository(@NotNull final String stderr) throws UnrelatedRepositoryException {
    if (stderr.contains("abort: repository is unrelated"))
      throw new UnrelatedRepositoryException();
  }

  private void checkUnknownRevision(@NotNull final String stderr) throws UnknownRevisionException {
    final String message = "abort: unknown revision '";
    int idx = stderr.indexOf(message);
    if (idx != -1) {
      int startIdx = idx + message.length();
      int endIdx = stderr.indexOf("'", startIdx);
      String revision = stderr.substring(startIdx, endIdx);
      throw new UnknownRevisionException(revision);
    }
  }

  private void checkFileNotUnderTheRoot(@NotNull final String stderr) throws VcsException {
    final String prefix = "abort: ";
    int idx = stderr.indexOf("abort: ");
    if (idx != -1) {
      int startIdx = idx + prefix.length();
      int endIdx = stderr.indexOf(" not under root");
      if (endIdx != -1) {
        String path = stderr.substring(startIdx, endIdx);
        throw new UnknownFileException(path);
      }
    }
  }

  private void checkConnectionRefused(@NotNull final String stderr) throws ConnectionRefusedException {
    if (stderr.equals("abort: error: Connection refused"))
      throw new ConnectionRefusedException();
  }

  private void checkAbandonedTransaction(@NotNull final String stderr) throws AbandonedTransactionFound {
    if (stderr.contains("abort: abandoned transaction found - run hg recover"))
      throw new AbandonedTransactionFound();
  }

  private void checkMergeWithWorkDirAncestor(@NotNull final String stderr) throws MergeWithWorkingDirAncestor {
    if (stderr.equals("abort: merging with a working directory ancestor has no effect"))
      throw new MergeWithWorkingDirAncestor();
  }

  private void checkNothingToMerge(@NotNull final String stderr) throws NothingToMergeException {
    if (stderr.startsWith("abort: nothing to merge"))
      throw new NothingToMergeException();
  }

  private void checkUnknownException(@NotNull final String stderr) throws UnknownMercurialException {
    if (stderr.contains("unknown exception encountered"))
      throw new UnknownMercurialException(stderr);
  }

  private static Set<Integer> setOf(Integer... ints) {
    return new HashSet<Integer>(asList(ints));
  }
}