/** Clean up resources. */
  public void shutdown() {
    m_timer.cancel();
    m_fanOutStreamSender.shutdown();
    m_consoleListener.shutdown();

    m_logger.info("finished");
  }
  private void shutdownConsoleCommunication(ConsoleCommunication consoleCommunication) {

    if (consoleCommunication != null) {
      consoleCommunication.shutdown();
    }

    m_consoleListener.discardMessages(ConsoleListener.ANY);
  }
  /**
   * Run the Grinder agent process.
   *
   * @throws GrinderException If an error occurs.
   */
  public void run() throws GrinderException {

    StartGrinderMessage startMessage = null;
    ConsoleCommunication consoleCommunication = null;

    try {
      while (true) {
        m_logger.info(GrinderBuild.getName());

        ScriptLocation script = null;
        GrinderProperties properties;

        do {
          properties =
              createAndMergeProperties(startMessage != null ? startMessage.getProperties() : null);

          m_agentIdentity.setName(properties.getProperty("grinder.hostID", getHostName()));

          final Connector connector =
              properties.getBoolean("grinder.useConsole", true)
                  ? m_connectorFactory.create(properties)
                  : null;

          // We only reconnect if the connection details have changed.
          if (consoleCommunication != null
              && !consoleCommunication.getConnector().equals(connector)) {
            shutdownConsoleCommunication(consoleCommunication);
            consoleCommunication = null;
            // Accept any startMessage from previous console - see bug 2092881.
          }

          if (consoleCommunication == null && connector != null) {
            try {
              consoleCommunication = new ConsoleCommunication(connector);
              consoleCommunication.start();
              m_logger.info("connected to console at {}", connector.getEndpointAsString());
            } catch (CommunicationException e) {
              if (m_proceedWithoutConsole) {
                m_logger.warn(
                    "{}, proceeding without the console; set "
                        + "grinder.useConsole=false to disable this warning.",
                    e.getMessage());
              } else {
                m_logger.error(e.getMessage());
                return;
              }
            }
          }

          if (consoleCommunication != null && startMessage == null) {
            m_logger.info("waiting for console signal");
            m_consoleListener.waitForMessage();

            if (m_consoleListener.received(ConsoleListener.START)) {
              startMessage = m_consoleListener.getLastStartGrinderMessage();
              continue; // Loop to handle new properties.
            } else {
              break; // Another message, check at end of outer while loop.
            }
          }

          if (startMessage != null) {
            final GrinderProperties messageProperties = startMessage.getProperties();
            final Directory fileStoreDirectory = m_fileStore.getDirectory();

            // Convert relative path to absolute path.
            messageProperties.setAssociatedFile(
                fileStoreDirectory.getFile(messageProperties.getAssociatedFile()));

            final File consoleScript =
                messageProperties.resolveRelativeFile(
                    messageProperties.getFile(
                        GrinderProperties.SCRIPT, GrinderProperties.DEFAULT_SCRIPT));

            // We only fall back to the agent properties if the start message
            // doesn't specify a script and there is no default script.
            if (messageProperties.containsKey(GrinderProperties.SCRIPT)
                || consoleScript.canRead()) {
              // The script directory may not be the file's direct parent.
              script = new ScriptLocation(fileStoreDirectory, consoleScript);
            }

            m_agentIdentity.setNumber(startMessage.getAgentNumber());
          } else {
            m_agentIdentity.setNumber(-1);
          }

          if (script == null) {
            final File scriptFile =
                properties.resolveRelativeFile(
                    properties.getFile(GrinderProperties.SCRIPT, GrinderProperties.DEFAULT_SCRIPT));

            script = new ScriptLocation(scriptFile);
          }

          if (!script.getFile().canRead()) {
            m_logger.error("The script file '" + script + "' does not exist or is not readable.");
            script = null;
            break;
          }
        } while (script == null);

        if (script != null) {
          final String jvmArguments = properties.getProperty("grinder.jvm.arguments");

          final WorkerFactory workerFactory;

          if (!properties.getBoolean("grinder.debug.singleprocess", false)) {

            final WorkerProcessCommandLine workerCommandLine =
                new WorkerProcessCommandLine(
                    properties, System.getProperties(), jvmArguments, script.getDirectory());

            m_logger.info("Worker process command line: {}", workerCommandLine);

            workerFactory =
                new ProcessWorkerFactory(
                    workerCommandLine,
                    m_agentIdentity,
                    m_fanOutStreamSender,
                    consoleCommunication != null,
                    script,
                    properties);
          } else {
            m_logger.info("DEBUG MODE: Spawning threads rather than processes");

            if (jvmArguments != null) {
              m_logger.warn(
                  "grinder.jvm.arguments ({}) ignored in single process mode", jvmArguments);
            }

            workerFactory =
                new DebugThreadWorkerFactory(
                    m_agentIdentity,
                    m_fanOutStreamSender,
                    consoleCommunication != null,
                    script,
                    properties);
          }

          final WorkerLauncher workerLauncher =
              new WorkerLauncher(
                  properties.getInt("grinder.processes", 1),
                  workerFactory,
                  m_eventSynchronisation,
                  m_logger);

          final int increment = properties.getInt("grinder.processIncrement", 0);

          if (increment > 0) {
            final boolean moreProcessesToStart =
                workerLauncher.startSomeWorkers(
                    properties.getInt("grinder.initialProcesses", increment));

            if (moreProcessesToStart) {
              final int incrementInterval =
                  properties.getInt("grinder.processIncrementInterval", 60000);

              final RampUpTimerTask rampUpTimerTask =
                  new RampUpTimerTask(workerLauncher, increment);

              m_timer.scheduleAtFixedRate(rampUpTimerTask, incrementInterval, incrementInterval);
            }
          } else {
            workerLauncher.startAllWorkers();
          }

          // Wait for a termination event.
          synchronized (m_eventSynchronisation) {
            final long maximumShutdownTime = 20000;
            long consoleSignalTime = -1;

            while (!workerLauncher.allFinished()) {
              if (consoleSignalTime == -1
                  && m_consoleListener.checkForMessage(
                      ConsoleListener.ANY ^ ConsoleListener.START)) {
                workerLauncher.dontStartAnyMore();
                consoleSignalTime = System.currentTimeMillis();
              }

              if (consoleSignalTime >= 0
                  && System.currentTimeMillis() - consoleSignalTime > maximumShutdownTime) {

                m_logger.info("forcibly terminating unresponsive processes");

                // destroyAllWorkers() prevents further workers from starting.
                workerLauncher.destroyAllWorkers();
              }

              m_eventSynchronisation.waitNoInterrruptException(maximumShutdownTime);
            }
          }

          workerLauncher.shutdown();
        }

        if (consoleCommunication == null) {
          break;
        } else {
          // Ignore any pending start messages.
          m_consoleListener.discardMessages(ConsoleListener.START);

          if (!m_consoleListener.received(ConsoleListener.ANY)) {
            // We've got here naturally, without a console signal.
            m_logger.info("finished, waiting for console signal");
            m_consoleListener.waitForMessage();
          }

          if (m_consoleListener.received(ConsoleListener.START)) {
            startMessage = m_consoleListener.getLastStartGrinderMessage();
          } else if (m_consoleListener.received(ConsoleListener.STOP | ConsoleListener.SHUTDOWN)) {
            break;
          } else {
            // ConsoleListener.RESET or natural death.
            startMessage = null;
          }
        }
      }
    } finally {
      shutdownConsoleCommunication(consoleCommunication);
    }
  }