class AgentImplementation implements Agent {

  @Override
  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 (final 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();

            if (messageProperties.getAssociatedFile() != null) {
              // If the properties is associated with a file in the file store
              // we rebase it so the script location is calculated relative to
              // the file.

              messageProperties.setAssociatedFile(
                fileStoreDirectory.getFile(
                  messageProperties.getAssociatedFile()));

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

              // We 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);
    }
  }

  public  AgentImplementation(Logger logger,
                             File alternateFile,
                             boolean proceedWithoutConsole);

}