/**
   * Equality.
   *
   * @param other Object to compare.
   * @return <code>true</code> if and only if we're equal to <code>other</code>.
   */
  public boolean equals(Object other) {
    if (this == other) {
      return true;
    }

    if (other == null || other.getClass() != ScriptLocation.class) {
      return false;
    }

    final ScriptLocation otherScriptLocation = (ScriptLocation) other;

    return getDirectory().equals(otherScriptLocation.getDirectory())
        && getFile().equals(otherScriptLocation.getFile());
  }
  /** {@inheritDoc} */
  @Override
  public ScriptEngine createScriptEngine(ScriptLocation script) throws EngineException {

    if (m_groovyFileMatcher.accept(script.getFile())) {
      return new GroovyScriptEngine(script);
    }

    return null;
  }
  /**
   * Constructor.
   *
   * @param properties Properties.
   * @param dcrContext DCR context.
   * @param scriptLocation Script location.
   */
  public GroovyScriptEngineService(
      GrinderProperties properties, //
      DCRContext dcrContext,
      ScriptLocation scriptLocation) {

    // This property name is poor, since it really means "If DCR
    // instrumentation is available, avoid the traditional Jython
    // instrumenter". I'm not renaming it, since I expect it only to last
    // a few releases, until DCR becomes the default.
    m_forceDCRInstrumentation =
        properties.getBoolean("grinder.dcrinstrumentation", false)
            // Hack: force DCR instrumentation for non-Jython scripts.
            || m_groovyFileMatcher.accept(scriptLocation.getFile());

    m_dcrContext = dcrContext;
  }
  /**
   * 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);
    }
  }
  /**
   * Constructor for JythonScriptEngine.
   *
   * @param pySystemState Python system state.
   * @throws EngineException If the script engine could not be created.
   */
  public JythonScriptEngine(final ScriptLocation script) throws EngineException {

    // Work around Jython issue 1894900.
    // If the python.cachedir has not been specified, and Jython is loaded
    // via the manifest classpath or the jar in the lib directory is
    // explicitly mentioned in the CLASSPATH, then set the cache directory to
    // be alongside jython.jar.
    if (System.getProperty(PYTHON_HOME) == null && System.getProperty(PYTHON_CACHEDIR) == null) {
      final String classpath = System.getProperty("java.class.path");

      final File grinderJar = findFileInPath(classpath, "grinder.jar");
      final File grinderJarDirectory =
          grinderJar != null ? grinderJar.getParentFile() : new File(".");

      final File jythonJar = findFileInPath(classpath, "jython.jar");
      final File jythonHome = jythonJar != null ? jythonJar.getParentFile() : grinderJarDirectory;

      if (grinderJarDirectory == null && jythonJar == null
          || grinderJarDirectory != null && grinderJarDirectory.equals(jythonHome)) {
        final File cacheDir = new File(jythonHome, CACHEDIR_DEFAULT_NAME);
        System.setProperty("python.cachedir", cacheDir.getAbsolutePath());
      }
    }

    m_systemState = new PySystemState();
    m_interpreter = new PythonInterpreter(null, m_systemState);

    m_interpreter.exec("class ___DieQuietly___: pass");
    m_dieQuietly = (PyClass) m_interpreter.get("___DieQuietly___");

    String version;

    try {
      version = PySystemState.class.getField("version").get(null).toString();
    } catch (final Exception e) {
      version = "Unknown";
    }

    m_version = version;

    // Prepend the script directory to the Python path. This matches the
    // behaviour of the Jython interpreter.
    m_systemState.path.insert(0, new PyString(script.getFile().getParent()));

    // Additionally, add the working directory to the Python path. I think
    // this will always be the same as the worker's CWD. Users expect to be
    // able to import from the directory the agent is running in or (when the
    // script has been distributed), the distribution directory.
    m_systemState.path.insert(1, new PyString(script.getDirectory().getFile().getPath()));

    try {
      // Run the test script, script does global set up here.
      m_interpreter.execfile(script.getFile().getPath());
    } catch (final PyException e) {
      throw new JythonScriptExecutionException("initialising test script", e);
    }

    // Find the callable that acts as a factory for test runner instances.
    m_testRunnerFactory = m_interpreter.get(TEST_RUNNER_CALLABLE_NAME);

    if (m_testRunnerFactory == null || !m_testRunnerFactory.isCallable()) {
      throw new JythonScriptExecutionException(
          "There is no callable (class or function) named '"
              + TEST_RUNNER_CALLABLE_NAME
              + "' in "
              + script);
    }
  }