public void run(GrinderProperties grinderProperties) throws GrinderException {
		StartGrinderMessage startMessage = null;
		ConsoleCommunication consoleCommunication = null;
		m_fanOutStreamSender = new FanOutStreamSender(GrinderConstants.AGENT_FANOUT_STREAM_THREAD_COUNT);
		m_timer = new Timer(false);
		final int connectionPort = grinderProperties.getInt(GrinderProperties.CONSOLE_PORT, 0);
		try {
			while (true) {
				m_logger.info(GrinderBuild.getName());
				ScriptLocation script = null;
				GrinderProperties properties;

				do {
					properties = createAndMergeProperties(grinderProperties,
							startMessage != null ? startMessage.getProperties() : null);
					if (m_agentConfig.isConnectionMode()) {
						properties.setProperty(GrinderProperties.CONSOLE_HOST, NetworkUtils.getLocalHostAddress());
						properties.setInt(GrinderProperties.CONSOLE_PORT, connectionPort);
					} else {
						properties.setProperty(GrinderProperties.CONSOLE_HOST, m_agentConfig.getControllerIP());
					}
					m_agentIdentity.setName(m_agentConfig.getAgentHostID());
					final Connector connector = m_connectorFactory.create(properties);
					// 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, grinderProperties.getProperty(
									"grinder.user", "_default"));
							consoleCommunication.start();
							m_logger.info("Connect 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);
					}
					m_logger.debug("The script location is {}", script.getFile().getAbsolutePath());
					if (!script.getFile().canRead()) {
						m_logger.error("The script file '{}' does not exist or is not readable.", script);
						script = null;
						break;
					}
				} while (script == null);

				if (script != null) {
					// Set up log directory.
					if (!properties.containsKey(GrinderProperties.LOG_DIRECTORY)) {
						properties.setFile(GrinderProperties.LOG_DIRECTORY, new File(m_agentConfig.getHome()
								.getLogDirectory(), properties.getProperty(GRINDER_PROP_TEST_ID, "default")));
					}
					File logFile = new File(properties.getFile(GrinderProperties.LOG_DIRECTORY, new File(".")),
						m_agentIdentity.getName() + "-0.log");
					m_logger.info("log file : {}", logFile);
					AbstractLanguageHandler handler = Lang.getByFileName(script.getFile()).getHandler();
					final WorkerFactory workerFactory;
					Properties rebasedSystemProperty = rebaseSystemClassPath(System.getProperties(), m_agentConfig.getCurrentDirectory());

					String jvmArguments = buildTestRunProperties(script, handler, rebasedSystemProperty, properties);

					if (!properties.getBoolean("grinder.debug.singleprocess", false)) {
						// Fix to provide empty system classpath to speed up
						final WorkerProcessCommandLine workerCommandLine = new WorkerProcessCommandLine(properties,
								filterSystemClassPath(rebasedSystemProperty, handler, m_logger), jvmArguments,
								script.getDirectory());

						m_logger.info("Worker process command line: {}", workerCommandLine);
						FileUtils.writeStringToFile(logFile, workerCommandLine.toString() + "\n\n");
						workerFactory = new ProcessWorkerFactory(workerCommandLine, m_agentIdentity,
								m_fanOutStreamSender, consoleCommunication != null, script, properties);
					} else {
						m_logger.info("DEBUG MODE. Spawning threads rather than processes");
						m_logger.warn("grinder.jvm.arguments ({}) ignored in single process mode", jvmArguments);

						workerFactory = new DebugThreadWorkerFactory(m_agentIdentity, m_fanOutStreamSender,
								consoleCommunication != null, script, properties);
					}
					m_logger.debug("Worker launcher is prepared.");
					final WorkerLauncher workerLauncher = new WorkerLauncher(properties.getInt("grinder.processes", 1),
							workerFactory, m_eventSynchronisation, m_logger);
					m_workerLauncherForShutdown = workerLauncher;
					final boolean threadRampUp = properties.getBoolean("grinder.threadRampUp", false);
					final int increment = properties.getInt("grinder.processIncrement", 0);
					if (!threadRampUp) {
						m_logger.debug("'Ramp Up' mode by {}.", increment);
					}
					if (!threadRampUp && 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 {
						m_logger.debug("start all workers");
						workerLauncher.startAllWorkers();
					}

					// Wait for a termination event.
					synchronized (m_eventSynchronisation) {
						final long maximumShutdownTime = 5000;
						long consoleSignalTime = -1;
						while (!workerLauncher.allFinished()) {
							m_logger.debug("Waiting until all workers are finished");
							if (consoleSignalTime == -1
									&& m_consoleListener.checkForMessage(ConsoleListener.ANY
									^ ConsoleListener.START)) {
								m_logger.info("Don't start anymore by message from controller.");
								workerLauncher.dontStartAnyMore();
								consoleSignalTime = System.currentTimeMillis();
							}
							if (consoleSignalTime >= 0
									&& System.currentTimeMillis() - consoleSignalTime > maximumShutdownTime) {

								m_logger.info("Terminating unresponsive processes by force");

								// destroyAllWorkers() prevents further workers
								// from starting.
								workerLauncher.destroyAllWorkers();
							}
							m_eventSynchronisation.waitNoInterrruptException(maximumShutdownTime);
						}
						m_logger.info("All workers are finished");
					}
					m_logger.debug("Normal shutdown");
					workerLauncher.shutdown();
					break;
				}

				if (consoleCommunication == null) {
					m_logger.debug("Console communication death");
					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.debug("Test is finished, wait 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)) {
						m_logger.debug("Got shutdown message");
						break;
					} else {
						m_logger.debug("Natural death");
						// ConsoleListener.RESET or natural death.
						startMessage = null;
					}
				}
			}
		} catch (Exception e) {
			m_logger.error("Exception occurred in the agent message loop", e);
		} finally {
			if (m_timer != null) {
				m_timer.cancel();
				m_timer = null;
			}
			shutdownConsoleCommunication(consoleCommunication);
			if (m_fanOutStreamSender != null) {
				m_fanOutStreamSender.shutdown();
				m_fanOutStreamSender = null;
			}
			m_consoleListener.shutdown();
			m_logger.info("Test shuts down.");
		}
	}
Run the Grinder agent process.

@param grinderProperties {@link GrinderProperties} which contains grinder agent base configuration.
@throws GrinderException If an error occurs.
ngrinder-core/src/main/java/net/grinder/engine/agent/AgentImplementationEx.java