view mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommandResult.java @ 596:a37960e1f0a0

Log stderr when hg command fails to ease troubleshooting
author Dmitry Neverov <dmitry.neverov@jetbrains.com>
date Mon, 29 Apr 2013 14:23:20 +0400
parents a9fea2d1d6c8
children eadc7e5c2236
line wrap: on
line source
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 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;

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

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

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

  public void checkFailure(boolean failWhenStderrIsNonEmpty) throws VcsException {
    rethrowDetectedError();
    if (isFailure())
      logAndThrowError();
    String stderr = getStderr();
    if (!isEmpty(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(myCommand, 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 getStderr() {
    return removePrivateData(myDelegate.getStderr(), myPrivateData);
  }

  @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;
  }

  private String createCommandLogMessage() {
    StringBuilder message = new StringBuilder();
    message.append("'").append(getCommand()).append("' command failed.");
    String stderr = getStderr();
    if (!isEmpty(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.");
    return message.toString();
  }

  private void rethrowDetectedError() throws VcsException {
    if (!shouldDetectErrors())
      return;
    String stderr = getStderr().trim();
    checkUnrelatedRepository(stderr);
    checkUnknownRevision(stderr);
    checkFileNotUnderTheRoot(stderr);
    checkConnectionRefused(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 static Set<Integer> setOf(Integer... ints) {
    return new HashSet<Integer>(asList(ints));
  }
}