/**
   * Determines if the given configuration is currently running. If any of the configurations are
   * currently stuck in the queue, it is logged.
   *
   * @param execution Contains information about the general build, including the listener used to
   *     log queue blockage.
   * @param configuration The configuration being checked to see if it's running.
   * @param mutableWhyMap Mutable map used to track the reasons a configuration is stuck in the
   *     queue. This prevents duplicate reasons from flooding the logs.
   * @return True if the build represented by the given configuration is currently running or stuck
   *     in the queue. False if the build has finished running.
   */
  private boolean isBuilding(
      MatrixBuild.MatrixBuildExecution execution,
      MatrixConfiguration configuration,
      Map<String, String> mutableWhyMap) {
    MatrixRun build = configuration.getBuildByNumber(execution.getBuild().getNumber());
    if (build != null) {
      return build.isBuilding();
    }

    Queue.Item queueItem = configuration.getQueueItem();
    if (queueItem != null) {
      String why = queueItem.getWhy();
      String key = queueItem.task.getFullDisplayName() + " " + queueItem.id;
      String oldWhy = mutableWhyMap.get(key);
      if (why == null) {
        mutableWhyMap.remove(key);
      }
      if (why != null && !why.equals(oldWhy)) {
        mutableWhyMap.put(key, why);
        BuildListener listener = execution.getListener();
        PrintStream logger = listener.getLogger();
        logger.print(
            "Configuration "
                + ModelHyperlinkNote.encodeTo(configuration)
                + " is still in the queue: ");
        queueItem.getCauseOfBlockage().print(listener); // this is still shown on the same line
      }
    }

    return true;
  }
  private void addTestflightLinks(
      AbstractBuild<?, ?> build, BuildListener listener, Map parsedMap) {
    TestflightBuildAction installAction = new TestflightBuildAction();
    String installUrl = (String) parsedMap.get("install_url");
    installAction.displayName = Messages.TestflightRecorder_InstallLinkText();
    installAction.iconFileName = "package.gif";
    installAction.urlName = installUrl;
    build.addAction(installAction);
    listener.getLogger().println(Messages.TestflightRecorder_InfoInstallLink(installUrl));

    TestflightBuildAction configureAction = new TestflightBuildAction();
    String configUrl = (String) parsedMap.get("config_url");
    configureAction.displayName = Messages.TestflightRecorder_ConfigurationLinkText();
    configureAction.iconFileName = "gear2.gif";
    configureAction.urlName = configUrl;
    build.addAction(configureAction);
    listener.getLogger().println(Messages.TestflightRecorder_InfoConfigurationLink(configUrl));

    build.addAction(new EnvAction());

    // Add info about the selected build into the environment
    EnvAction envData = build.getAction(EnvAction.class);
    if (envData != null) {
      envData.add("TESTFLIGHT_INSTALL_URL", installUrl);
      envData.add("TESTFLIGHT_CONFIG_URL", configUrl);
    }
  }
  @Override
  public boolean perform(
      AbstractBuild<?, ?> build, Launcher launcher, final BuildListener listener) {
    if (build.getResult().isWorseOrEqualTo(Result.FAILURE)) return false;

    listener.getLogger().println(Messages.TestflightRecorder_InfoUploading());

    try {
      EnvVars vars = build.getEnvironment(listener);

      String workspace = vars.expand("$WORKSPACE");

      List<TestflightUploader.UploadRequest> urList =
          new ArrayList<TestflightUploader.UploadRequest>();

      for (TestflightTeam team : createDefaultPlusAdditionalTeams()) {
        try {
          TestflightUploader.UploadRequest ur = createPartialUploadRequest(team, vars, build);
          urList.add(ur);
        } catch (MisconfiguredJobException mje) {
          listener.getLogger().println(mje.getConfigurationMessage());
          return false;
        }
      }

      for (TestflightUploader.UploadRequest ur : urList) {
        TestflightRemoteRecorder remoteRecorder =
            new TestflightRemoteRecorder(workspace, ur, listener);

        final List<Map> parsedMaps;

        try {
          Object result = launcher.getChannel().call(remoteRecorder);
          parsedMaps = (List<Map>) result;
        } catch (UploadException ue) {
          listener
              .getLogger()
              .println(Messages.TestflightRecorder_IncorrectResponseCode(ue.getStatusCode()));
          listener.getLogger().println(ue.getResponseBody());
          return false;
        }

        if (parsedMaps.size() == 0) {
          listener.getLogger().println(Messages.TestflightRecorder_NoUploadedFile(ur.filePaths));
          return false;
        }
        for (Map parsedMap : parsedMaps) {
          addTestflightLinks(build, listener, parsedMap);
        }
      }
    } catch (Throwable e) {
      listener.getLogger().println(e);
      e.printStackTrace(listener.getLogger());
      return false;
    }

    return true;
  }
 /**
  * Logic is more-or-less copied from {@link
  * DefaultMatrixExecutionStrategyImpl#notifyStartBuild(java.util.List)}
  *
  * <p>Triggers the startBuild event on all aggregators. This should be called before any run is
  * started.
  *
  * @param aggregators The aggregators to be notified.
  * @param listener Listener from parent build that can be logged to.
  * @return True if all aggregators return true. If any aggregator returns false, false is
  *     immediately returned and no new aggregators are called.
  * @throws IOException
  * @throws InterruptedException
  */
 private boolean notifyStartBuild(List<MatrixAggregator> aggregators, BuildListener listener)
     throws IOException, InterruptedException {
   for (MatrixAggregator aggregator : aggregators) {
     if (!aggregator.startBuild()) {
       listener.error("Aggregator terminated build: " + aggregator.toString());
       return false;
     }
   }
   return true;
 }
  /**
   * Schedules the given configuration.
   *
   * <p>Copied from the {@link
   * DefaultMatrixExecutionStrategyImpl#scheduleConfigurationBuild(hudson.matrix.MatrixBuild.MatrixBuildExecution,
   * hudson.matrix.MatrixConfiguration)}
   *
   * @param execution Contains information about the general build, including the listener used to
   *     log queue blockage.
   * @param configuration The configuration to schedule.
   * @param upstreamCause The cause of the build. Will either be an {@link
   *     hudson.model.Cause.UpstreamCause} or {@link
   *     com.attask.jenkins.healingmatrixproject.SelfHealingCause}.
   */
  private void scheduleConfigurationBuild(
      MatrixBuild.MatrixBuildExecution execution,
      MatrixConfiguration configuration,
      Cause.UpstreamCause upstreamCause)
      throws InterruptedException {
    MatrixBuild build = (MatrixBuild) execution.getBuild();
    execution
        .getListener()
        .getLogger()
        .println(Messages.MatrixBuild_Triggering(ModelHyperlinkNote.encodeTo(configuration)));

    // filter the parent actions for those that can be passed to the individual jobs.
    List<MatrixChildAction> childActions = Util.filter(build.getActions(), MatrixChildAction.class);

    BuildListener listener = execution.getListener();
    while (!configuration.scheduleBuild(childActions, upstreamCause)) {
      String msg = "Unable to schedule build " + configuration.getFullDisplayName() + ". Retrying.";
      listener.error(msg);
      Thread.sleep(500);
    }
  }
 /**
  * Logic is copied from {@link
  * DefaultMatrixExecutionStrategyImpl#notifyEndBuild(hudson.matrix.MatrixRun, java.util.List)}
  */
 private void notifyEndRun(
     MatrixRun run, List<MatrixAggregator> aggregators, BuildListener listener)
     throws InterruptedException, IOException {
   if (run == null)
     return; // can happen if the configuration run gets cancelled before it gets started.
   for (MatrixAggregator aggregator : aggregators) {
     if (!aggregator.endRun(run)) {
       listener.error("Aggregator terminated build: " + aggregator.toString());
       throw new AbortException();
     }
   }
 }
  /**
   * Waits for the given configurations to finish, retrying any that qualify to be rerun.
   *
   * @param execution Provided by the plugin.
   * @param patterns List of regular expression patterns used to scan the log to determine if a
   *     build should be rerun.
   * @param retries Mutable map that tracks the number of times a specific configuration has been
   *     retried.
   * @param configurations The configurations that have already been scheduled to run that should be
   *     waited for to finish.
   * @return The worst result of all the runs. If a build was rerun, only the result of the rerun is
   *     considered.
   * @throws InterruptedException
   * @throws IOException
   */
  private Result waitForMatrixRuns(
      MatrixBuild.MatrixBuildExecution execution,
      List<Pattern> patterns,
      Map<MatrixConfiguration, Integer> retries,
      LinkedList<MatrixConfiguration> configurations)
      throws InterruptedException, IOException {
    BuildListener listener = execution.getListener();
    PrintStream logger = listener.getLogger();

    Map<String, String> whyBlockedMap =
        new HashMap<
            String,
            String>(); // keep track of why builds are blocked so we can print unique messages when
    // they change.
    Result finalResult = Result.SUCCESS;
    int iteration = 0;
    boolean continueRetrying = true;
    while (!configurations.isEmpty()) {
      ++iteration;
      MatrixConfiguration configuration = configurations.removeFirst();
      if (isBuilding(execution, configuration, whyBlockedMap)) {
        if (iteration >= configurations.size()) {
          // Every time we loop through all the configurations, sleep for a bit.
          // This is to prevent polling too often while everything is still building.
          iteration = 0;
          Thread.sleep(1000);
        }
        configurations.add(configuration);
        continue;
      }

      Run parentBuild = execution.getBuild();
      MatrixRun matrixRun = configuration.getBuildByNumber(parentBuild.getNumber());
      Result runResult = matrixRun.getResult();
      if (continueRetrying
          && runResult.isWorseOrEqualTo(getWorseThanOrEqualTo())
          && runResult.isBetterOrEqualTo(getBetterThanOrEqualTo())) {
        if (matchesPattern(matrixRun, patterns)) {
          int retriedCount = retries.get(configuration);
          if (retriedCount < getMaxRetries()) {
            ++retriedCount;
            retries.put(configuration, retriedCount);
            // rerun
            String logMessage =
                String.format(
                    "%s was %s. Matched pattern to rerun. Rerunning (%d).",
                    matrixRun, runResult, retriedCount);
            listener.error(logMessage);

            HealedAction action = parentBuild.getAction(HealedAction.class);
            if (action == null) {
              //noinspection SynchronizationOnLocalVariableOrMethodParameter
              synchronized (parentBuild.getActions()) {
                action = parentBuild.getAction(HealedAction.class);
                if (action == null) {
                  action = new HealedAction(matrixRun.getCharset());
                  parentBuild.addAction(action);
                }
              }
            }
            action.addAutoHealedJob(matrixRun);

            MatrixConfiguration parent = matrixRun.getParent();
            if (parent != null) {
              // I'm paranoid about NPEs
              parent.removeRun(matrixRun);
              matrixRun.delete();
            } else {
              LOGGER.severe(
                  "couldn't remove old run, parent was null. This is a Jenkins core bug.");
            }
            scheduleConfigurationBuild(
                execution, configuration, new SelfHealingCause(parentBuild, retriedCount));
            configurations.add(configuration);
            continue;
          } else {
            String logMessage =
                String.format(
                    "%s was %s. Matched pattern to rerun, but the max number of retries (%d) has been met.",
                    matrixRun, runResult, getMaxRetries());
            listener.error(logMessage);
            if (getStopRetryingAfterOneFails()) {
              listener.error("Not retrying any more builds.");
              continueRetrying = false;
            }
          }
        } else {
          String logMessage =
              String.format(
                  "%s was %s. It did not match the pattern to rerun. Accepting result.",
                  matrixRun, runResult);
          logger.println(logMessage);
        }
      }
      notifyEndRun(matrixRun, execution.getAggregators(), execution.getListener());
      finalResult = finalResult.combine(runResult);
    }
    return finalResult;
  }