/*
 *  Phusion Passenger - https://www.phusionpassenger.com/
 *  Copyright (c) 2011-2025 Asynchronous B.V.
 *
 *  "Passenger", "Phusion Passenger" and "Union Station" are registered
 *  trademarks of Asynchronous B.V.
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 */
#ifndef _PASSENGER_SPAWNING_KIT_SMART_SPAWNER_H_
#define _PASSENGER_SPAWNING_KIT_SMART_SPAWNER_H_

#include <oxt/thread.hpp>
#include <oxt/system_calls.hpp>
#include <boost/bind/bind.hpp>
#include <boost/make_shared.hpp>
#include <string>
#include <vector>
#include <map>
#include <exception>
#include <dirent.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h> // IWYU pragma: keep; for signal constants
#include <unistd.h>
#include <cstring>
#include <cassert>

#include <adhoc_lve.h>

#include <LoggingKit/Logging.h>
#include <Constants.h>
#include <Exceptions.h>
#include <DataStructures/StringKeyTable.h>
#include <ProcessManagement/Utils.h>
#include <SystemTools/ProcessMetricsCollector.h>
#include <SystemTools/SystemTime.h>
#include <FileTools/FileManip.h>
#include <IOTools/BufferedIO.h>
#include <JsonTools/JsonUtils.h>
#include <Utils/ScopeGuard.h>
#include <Utils/AsyncSignalSafeUtils.h>
#include <LveLoggingDecorator.h>
#include <Core/SpawningKit/Spawner.h>
#include <Core/SpawningKit/Exceptions.h>
#include <Core/SpawningKit/PipeWatcher.h>
#include <Core/SpawningKit/Handshake/Session.h>
#include <Core/SpawningKit/Handshake/Prepare.h>
#include <Core/SpawningKit/Handshake/Perform.h>
#include <Core/SpawningKit/Handshake/BackgroundIOCapturer.h>

#ifdef INTELLISENSE
	#include <Core/ApplicationPool/Options.h>
#endif

namespace Passenger {
namespace SpawningKit {

using namespace std;
using namespace boost;
using namespace oxt;


class SmartSpawner: public Spawner {
private:
	const string preloaderCommandString;
	string preloaderEnvvars;
	string preloaderUserInfo;
	string preloaderUlimits;
	StringKeyTable<string> preloaderAnnotations;
	AppPoolOptions options;

	// Protects m_lastUsed and pid.
	mutable boost::mutex simpleFieldSyncher;
	// Protects everything else.
	mutable boost::mutex syncher;

	// Preloader information.
	pid_t pid;
	FileDescriptor preloaderStdin;
	string socketAddress;
	unsigned long long m_lastUsed;


	/**
	 * Behaves like <tt>waitpid(pid, status, WNOHANG)</tt>, but waits at most
	 * <em>timeout</em> miliseconds for the process to exit.
	 */
	static int timedWaitpid(pid_t pid, int *status, unsigned long long timeout) {
		Timer<SystemTime::GRAN_10MSEC> timer;
		int ret;

		do {
			ret = syscalls::waitpid(pid, status, WNOHANG);
			if (ret > 0 || ret == -1) {
				return ret;
			} else {
				syscalls::usleep(10000);
			}
		} while (timer.elapsed() < timeout);
		return 0; // timed out
	}

	static bool osProcessExists(pid_t pid) {
		if (syscalls::kill(pid, 0) == 0) {
			/* On some environments, e.g. Heroku, the init process does
			 * not properly reap adopted zombie processes, which can interfere
			 * with our process existance check. To work around this, we
			 * explicitly check whether or not the process has become a zombie.
			 */
			return !isZombie(pid);
		} else {
			return errno != ESRCH;
		}
	}

	static bool isZombie(pid_t pid) {
		string filename = "/proc/" + toString(pid) + "/status";
		FILE *f = fopen(filename.c_str(), "r");
		if (f == NULL) {
			// Don't know.
			return false;
		}

		bool result = false;
		while (!feof(f)) {
			char buf[512];
			const char *line;

			line = fgets(buf, sizeof(buf), f);
			if (line == NULL) {
				break;
			}
			if (strcmp(line, "State:	Z (zombie)\n") == 0) {
				// Is a zombie.
				result = true;
				break;
			}
		}
		fclose(f);
		return result;
	}

	static string createCommandString(const vector<string> &command) {
		string result;
		vector<string>::const_iterator it;
		vector<string>::const_iterator begin = command.begin();
		vector<string>::const_iterator end = command.end();

		for (it = begin; it != end; it++) {
			if (it != begin) {
				result.append(1, ' ');
			}
			result.append(escapeShell(*it));
		}

		return result;
	}

	void setConfigFromAppPoolOptions(Config *config, Json::Value &extraArgs,
		const AppPoolOptions &options)
	{
		Spawner::setConfigFromAppPoolOptions(config, extraArgs, options);
		config->spawnMethod = P_STATIC_STRING("smart");
	}

	struct StdChannelsAsyncOpenState {
		const int workDirFd;

		oxt::thread *stdinOpenThread;
		FileDescriptor stdinFd;
		int stdinOpenErrno;

		oxt::thread *stdoutAndErrOpenThread;
		FileDescriptor stdoutAndErrFd;
		int stdoutAndErrOpenErrno;

		BackgroundIOCapturerPtr stdoutAndErrCapturer;

		StdChannelsAsyncOpenState(int _workDirFd)
			: workDirFd(_workDirFd),
			  stdinOpenThread(NULL),
			  stdoutAndErrOpenThread(NULL)
			{ }

		~StdChannelsAsyncOpenState() {
			boost::this_thread::disable_interruption di;
			boost::this_thread::disable_syscall_interruption dsi;
			if (stdinOpenThread != NULL) {
				stdinOpenThread->interrupt_and_join();
				delete stdinOpenThread;
			}
			if (stdoutAndErrOpenThread != NULL) {
				stdoutAndErrOpenThread->interrupt_and_join();
				delete stdoutAndErrOpenThread;
			}
		}
	};

	typedef boost::shared_ptr<StdChannelsAsyncOpenState> StdChannelsAsyncOpenStatePtr;

	StdChannelsAsyncOpenStatePtr openStdChannelsFifosAsynchronously(
		HandshakeSession &session)
	{
		StdChannelsAsyncOpenStatePtr state = boost::make_shared<StdChannelsAsyncOpenState>(
			session.workDirFd);
		state->stdinOpenThread = new oxt::thread(boost::bind(
			openStdinChannel, state, session.workDir->getPath()),
			"FIFO opener: " + session.workDir->getPath() + "/stdin", 1024 * 128);
		state->stdoutAndErrOpenThread = new oxt::thread(boost::bind(
			openStdoutAndErrChannel, state, session.workDir->getPath()),
			"FIFO opener: " + session.workDir->getPath() + "/stdout_and_err", 1024 * 128);
		return state;
	}

	void waitForStdChannelFifosToBeOpenedByPeer(const StdChannelsAsyncOpenStatePtr &state,
		HandshakeSession &session, pid_t pid)
	{
		TRACE_POINT();
		MonotonicTimeUsec startTime = SystemTime::getMonotonicUsec();
		ScopeGuard guard(boost::bind(adjustTimeout, startTime, &session.timeoutUsec));

		try {
			if (state->stdinOpenThread->try_join_for(
				boost::chrono::microseconds(session.timeoutUsec)))
			{
				delete state->stdinOpenThread;
				state->stdinOpenThread = NULL;
				if (state->stdinFd == -1) {
					throw SystemException("Error opening FIFO "
						+ session.workDir->getPath() + "/stdin",
						state->stdinOpenErrno);
				} else {
					P_LOG_FILE_DESCRIPTOR_PURPOSE(state->stdinFd,
						"App " << pid << " (" << options.appRoot
						<< ") stdin");
					adjustTimeout(startTime, &session.timeoutUsec);
					startTime = SystemTime::getMonotonicUsec();
				}
			} else {
				throw TimeoutException("Timeout opening FIFO "
					+ session.workDir->getPath() + "/stdin");
			}

			UPDATE_TRACE_POINT();
			if (state->stdoutAndErrOpenThread->try_join_for(
				boost::chrono::microseconds(session.timeoutUsec)))
			{
				delete state->stdoutAndErrOpenThread;
				state->stdoutAndErrOpenThread = NULL;
				if (state->stdoutAndErrFd == -1) {
					throw SystemException("Error opening FIFO "
						+ session.workDir->getPath() + "/stdout_and_err",
						state->stdoutAndErrOpenErrno);
				} else {
					P_LOG_FILE_DESCRIPTOR_PURPOSE(state->stdoutAndErrFd,
						"App " << pid << " (" << options.appRoot
						<< ") stdoutAndErr");
				}
			} else {
				throw TimeoutException("Timeout opening FIFO "
					+ session.workDir->getPath() + "/stdout_and_err");
			}

			state->stdoutAndErrCapturer = boost::make_shared<BackgroundIOCapturer>(
				state->stdoutAndErrFd, pid, session.config->appGroupName,
				session.config->logFile);
			state->stdoutAndErrCapturer->start();
		} catch (const boost::system::system_error &e) {
			throw SystemException(e.what(), e.code().value());
		}
	}

	static void openStdinChannel(StdChannelsAsyncOpenStatePtr state,
		const string &workDir)
	{
		int fd = syscalls::openat(state->workDirFd, "stdin", O_WRONLY | O_APPEND | O_NOFOLLOW);
		int e = errno;
		state->stdinFd.assign(fd, __FILE__, __LINE__);
		state->stdinOpenErrno = e;
	}

	static void openStdoutAndErrChannel(StdChannelsAsyncOpenStatePtr state,
		const string &workDir)
	{
		int fd = syscalls::openat(state->workDirFd, "stdout_and_err", O_RDONLY | O_NOFOLLOW);
		int e = errno;
		state->stdoutAndErrFd.assign(fd, __FILE__, __LINE__);
		state->stdoutAndErrOpenErrno = e;
	}

	bool preloaderStarted() const {
		return pid != -1;
	}

	void startPreloader() {
		TRACE_POINT();
		boost::this_thread::disable_interruption di;
		boost::this_thread::disable_syscall_interruption dsi;
		assert(!preloaderStarted());
		P_DEBUG("Spawning new preloader: appRoot=" << options.appRoot);

		Config config;
		Json::Value extraArgs;
		try {
			setConfigFromAppPoolOptions(&config, extraArgs, options);
			config.startCommand = preloaderCommandString;
		} catch (const std::exception &originalException) {
			Journey journey(SPAWN_THROUGH_PRELOADER, true);
			journey.setStepErrored(SPAWNING_KIT_PREPARATION, true);
			throw SpawnException(originalException, journey,
				&config).finalize();
		}

		HandshakeSession session(*context, config, START_PRELOADER);
		session.journey.setStepInProgress(SPAWNING_KIT_PREPARATION);

		try {
			internalStartPreloader(config, session, extraArgs);
		} catch (const SpawnException &) {
			throw;
		} catch (const std::exception &originalException) {
			session.journey.setStepErrored(SPAWNING_KIT_PREPARATION);
			throw SpawnException(originalException, session.journey,
				&config).finalize();
		}
	}

	void internalStartPreloader(Config &config, HandshakeSession &session,
		const Json::Value &extraArgs)
	{
		TRACE_POINT();
		HandshakePrepare(session, extraArgs).execute();
		Pipe stdinChannel = createPipe(__FILE__, __LINE__);
		Pipe stdoutAndErrChannel = createPipe(__FILE__, __LINE__);
		adhoc_lve::LveEnter scopedLveEnter(LveLoggingDecorator::lveInitOnce(),
			session.uid,
			config.lveMinUid,
			LveLoggingDecorator::lveExitCallback);
		LveLoggingDecorator::logLveEnter(scopedLveEnter,
			session.uid,
			config.lveMinUid);
		string agentFilename = context->resourceLocator
			->findSupportBinary(AGENT_EXE);

		session.journey.setStepPerformed(SPAWNING_KIT_PREPARATION);
		session.journey.setStepInProgress(SPAWNING_KIT_FORK_SUBPROCESS);
		session.journey.setStepInProgress(SUBPROCESS_BEFORE_FIRST_EXEC);

		pid_t pid = syscalls::fork();
		if (pid == 0) {
			int e;
			char buf[1024];
			const char *end = buf + sizeof(buf);
			namespace ASSU = AsyncSignalSafeUtils;

			resetSignalHandlersAndMask();
			disableMallocDebugging();
			int stdinCopy = dup2(stdinChannel.first, 3);
			int stdoutAndErrCopy = dup2(stdoutAndErrChannel.second, 4);
			dup2(stdinCopy, 0);
			dup2(stdoutAndErrCopy, 1);
			dup2(stdoutAndErrCopy, 2);
			closeAllFileDescriptors(2);

			execlp(agentFilename.c_str(),
				agentFilename.c_str(),
				"spawn-env-setupper",
				session.workDir->getPath().c_str(),
				"--before",
				(char *) 0);

			char *pos = buf;
			e = errno;
			pos = ASSU::appendData(pos, end, "Cannot execute \"");
			pos = ASSU::appendData(pos, end, agentFilename.data(), agentFilename.size());
			pos = ASSU::appendData(pos, end, "\": ");
			pos = ASSU::appendData(pos, end, ASSU::limitedStrerror(e));
			pos = ASSU::appendData(pos, end, " (errno=");
			pos = ASSU::appendInteger<int, 10>(pos, end, e);
			pos = ASSU::appendData(pos, end, ")\n");
			ASSU::printError(buf, pos - buf);
			_exit(1);

		} else if (pid == -1) {
			int e = errno;
			UPDATE_TRACE_POINT();
			session.journey.setStepErrored(SPAWNING_KIT_FORK_SUBPROCESS);
			SpawnException ex(OPERATING_SYSTEM_ERROR, session.journey, &config);
			ex.setSummary(StaticString("Cannot fork a new process: ") + strerror(e)
				+ " (errno=" + toString(e) + ")");
			ex.setAdvancedProblemDetails(StaticString("Cannot fork a new process: ")
				+ strerror(e) + " (errno=" + toString(e) + ")");
			throw ex.finalize();

		} else {
			UPDATE_TRACE_POINT();
			session.journey.setStepPerformed(SPAWNING_KIT_FORK_SUBPROCESS);
			session.journey.setStepInProgress(SPAWNING_KIT_HANDSHAKE_PERFORM);

			scopedLveEnter.exit();

			P_LOG_FILE_DESCRIPTOR_PURPOSE(stdinChannel.second,
				"Preloader " << pid << " (" << options.appRoot << ") stdin");
			P_LOG_FILE_DESCRIPTOR_PURPOSE(stdoutAndErrChannel.first,
				"Preloader " << pid << " (" << options.appRoot << ") stdoutAndErr");

			UPDATE_TRACE_POINT();
			ScopeGuard guard(boost::bind(nonInterruptableKillAndWaitpid, pid));
			P_DEBUG("Preloader process forked for appRoot=" << options.appRoot
				<< ": PID " << pid);
			stdinChannel.first.close();
			stdoutAndErrChannel.second.close();

			HandshakePerform(session, pid, stdinChannel.second,
				stdoutAndErrChannel.first).execute();
			string envvars, userInfo, ulimits;
			// If a new output variable was added to this function,
			// then don't forget to also update these locations:
			// - the critical section below
			// - bottom of stopPreloader()
			// - addPreloaderEnvDumps()
			HandshakePerform::loadBasicInfoFromEnvDumpDir(session.envDumpDir,
				session.envDumpDirFd, envvars, userInfo, ulimits);
			string socketAddress = findPreloaderCommandSocketAddress(session);

			{
				boost::lock_guard<boost::mutex> l(simpleFieldSyncher);
				this->pid = pid;
				this->socketAddress = socketAddress;
				this->preloaderStdin = stdinChannel.second;
				this->preloaderEnvvars = envvars;
				this->preloaderUserInfo = userInfo;
				this->preloaderUlimits = ulimits;
				this->preloaderAnnotations = loadAnnotationsFromEnvDumpDir(
					session.envDumpDir, session.envDumpAnnotationsDirFd);
			}

			PipeWatcherPtr watcher = boost::make_shared<PipeWatcher>(
				stdoutAndErrChannel.first, "output", config.appGroupName,
				config.logFile, pid);
			watcher->initialize();
			watcher->start();

			UPDATE_TRACE_POINT();
			guard.clear();
			session.journey.setStepPerformed(SPAWNING_KIT_HANDSHAKE_PERFORM);
			P_INFO("Preloader for " << options.appRoot <<
				" started on PID " << pid <<
				", listening on " << socketAddress);
		}
	}

	void stopPreloader() {
		TRACE_POINT();
		boost::this_thread::disable_interruption di;
		boost::this_thread::disable_syscall_interruption dsi;

		if (!preloaderStarted()) {
			return;
		}

		preloaderStdin.close(false);

		if (timedWaitpid(pid, NULL, 5000) == 0) {
			P_DEBUG("Preloader did not exit in time, killing it...");
			syscalls::kill(pid, SIGKILL);
			syscalls::waitpid(pid, NULL, 0);
		}

		// Delete socket after the process has exited so that it
		// doesn't crash upon deleting a nonexistant file.
		if (getSocketAddressType(socketAddress) == SAT_UNIX) {
			string filename = parseUnixSocketAddress(socketAddress);
			syscalls::unlink(filename.c_str());
		}

		{
			boost::lock_guard<boost::mutex> l(simpleFieldSyncher);
			pid = -1;
			socketAddress.clear();
			preloaderEnvvars.clear();
			preloaderUserInfo.clear();
			preloaderUlimits.clear();
			preloaderAnnotations.clear();
		}
	}

	FileDescriptor connectToPreloader(HandshakeSession &session) {
		TRACE_POINT();
		FileDescriptor fd(connectToServer(socketAddress, __FILE__, __LINE__), NULL, 0);
		P_LOG_FILE_DESCRIPTOR_PURPOSE(fd, "Preloader " << pid
			<< " (" << session.config->appRoot << ") connection");
		return fd;
	}

	struct ForkResult {
		pid_t pid;
		FileDescriptor stdinFd;
		FileDescriptor stdoutAndErrFd;
		string alreadyReadStdoutAndErrData;

		ForkResult()
			: pid(-1)
			{ }

		ForkResult(pid_t _pid, const FileDescriptor &_stdinFd,
			const FileDescriptor &_stdoutAndErrFd,
			const string &_alreadyReadStdoutAndErrData)
			: pid(_pid),
			  stdinFd(_stdinFd),
			  stdoutAndErrFd(_stdoutAndErrFd),
			  alreadyReadStdoutAndErrData(_alreadyReadStdoutAndErrData)
			{ }
	};

	struct PreloaderCrashed {
		SystemException *systemException;
		IOException *ioException;

		PreloaderCrashed(const SystemException &e)
			: systemException(new SystemException(e)),
			  ioException(NULL)
			{ }

		PreloaderCrashed(const IOException &e)
			: systemException(NULL),
			  ioException(new IOException(e))
			{ }

		~PreloaderCrashed() {
			delete systemException;
			delete ioException;
		}

		const oxt::tracable_exception &getException() const {
			if (systemException != NULL) {
				return *systemException;
			} else {
				return *ioException;
			}
		}
	};

	ForkResult invokeForkCommand(HandshakeSession &session, JourneyStep &stepToMarkAsErrored) {
		TRACE_POINT();

		P_ASSERT_EQ(session.journey.getStepInfo(SPAWNING_KIT_PREPARATION).state,
			STEP_PERFORMED);

		try {
			StdChannelsAsyncOpenStatePtr stdChannelsAsyncOpenState =
				openStdChannelsFifosAsynchronously(session);
			return internalInvokeForkCommand(session, stdChannelsAsyncOpenState,
				stepToMarkAsErrored);
		} catch (const PreloaderCrashed &crashException1) {
			UPDATE_TRACE_POINT();
			P_WARN("An error occurred while spawning an application process: "
				<< crashException1.getException().what());
			P_WARN("The application preloader seems to have crashed,"
				" restarting it and trying again...");

			session.journey.reset();

			try {
				stopPreloader();
			} catch (const SpawnException &) {
				throw;
			} catch (const std::exception &originalException) {
				session.journey.setStepErrored(SPAWNING_KIT_PREPARATION, true);

				SpawnException e(originalException, session.journey, session.config);
				e.setSummary(StaticString("Error stopping a crashed preloader: ")
					+ originalException.what());
				e.setProblemDescriptionHTML(
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application by communicating with a"
					" helper process that we call a \"preloader\". However,"
					" this helper process crashed unexpectedly. "
					SHORT_PROGRAM_NAME " then tried to restart it, but"
					" encountered the following error while trying to"
					" stop the preloader:</p>"
					"<pre>" + escapeHTML(originalException.what()) + "</pre>");
				throw e.finalize();
			}

			UPDATE_TRACE_POINT();
			startPreloader();
			session.journey.reset();
			session.journey.setStepPerformed(SPAWNING_KIT_PREPARATION, true);

			UPDATE_TRACE_POINT();
			try {
				StdChannelsAsyncOpenStatePtr stdChannelsAsyncOpenState =
					openStdChannelsFifosAsynchronously(session);
				return internalInvokeForkCommand(session, stdChannelsAsyncOpenState,
					stepToMarkAsErrored);
			} catch (const PreloaderCrashed &crashException2) {
				UPDATE_TRACE_POINT();

				session.journey.reset();
				session.journey.setStepErrored(SPAWNING_KIT_PREPARATION, true);

				try {
					stopPreloader();
				} catch (const SpawnException &) {
					throw;
				} catch (const std::exception &originalException) {
					SpawnException e(originalException, session.journey, session.config);
					e.setSummary(StaticString("Error stopping a crashed preloader: ")
						+ originalException.what());
					e.setProblemDescriptionHTML(
						"<p>The " PROGRAM_NAME " application server tried"
						" to start the web application by communicating with a"
						" helper process that we call a \"preloader\". However,"
						" this helper process crashed unexpectedly. "
						SHORT_PROGRAM_NAME " then tried to restart it, but"
						" encountered the following error while trying to"
						" stop the preloader:</p>"
						"<pre>" + escapeHTML(originalException.what()) + "</pre>");
					throw e.finalize();
				}

				SpawnException e(crashException2.getException(),
					session.journey, session.config);
				e.setSummary(StaticString("An application preloader crashed: ") +
					crashException2.getException().what());
				e.setProblemDescriptionHTML(
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application by communicating with a"
					" helper process that we call a \"preloader\". However,"
					" this helper process crashed unexpectedly:</p>"
					"<pre>" + escapeHTML(crashException2.getException().what())
					+ "</pre>");
				throw e.finalize();
			}
		}
	}

	ForkResult internalInvokeForkCommand(HandshakeSession &session,
		const StdChannelsAsyncOpenStatePtr &stdChannelsAsyncOpenState,
		JourneyStep &stepToMarkAsErrored)
	{
		TRACE_POINT();

		P_ASSERT_EQ(session.journey.getStepInfo(SPAWNING_KIT_PREPARATION).state,
			STEP_PERFORMED);

		session.journey.setStepInProgress(SPAWNING_KIT_CONNECT_TO_PRELOADER);
		stepToMarkAsErrored = SPAWNING_KIT_CONNECT_TO_PRELOADER;
		FileDescriptor fd;
		string line;
		Json::Value doc;
		try {
			fd = connectToPreloader(session);
		} catch (const SystemException &e) {
			throw PreloaderCrashed(e);
		} catch (const IOException &e) {
			throw PreloaderCrashed(e);
		}

		session.journey.setStepPerformed(SPAWNING_KIT_CONNECT_TO_PRELOADER);
		session.journey.setStepInProgress(SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER);
		stepToMarkAsErrored = SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER;
		try {
			sendForkCommand(session, fd);
		} catch (const SystemException &e) {
			throw PreloaderCrashed(e);
		} catch (const IOException &e) {
			throw PreloaderCrashed(e);
		}

		session.journey.setStepPerformed(SPAWNING_KIT_SEND_COMMAND_TO_PRELOADER);
		session.journey.setStepInProgress(SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER);
		stepToMarkAsErrored = SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER;
		try {
			line = readForkCommandResponse(session, fd);
		} catch (const SystemException &e) {
			throw PreloaderCrashed(e);
		} catch (const IOException &e) {
			throw PreloaderCrashed(e);
		}

		session.journey.setStepPerformed(SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER);
		session.journey.setStepInProgress(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER);
		stepToMarkAsErrored = SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER;
		doc = parseForkCommandResponse(session, line);

		session.journey.setStepPerformed(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER);
		session.journey.setStepInProgress(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);
		stepToMarkAsErrored = SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER;
		return handleForkCommandResponse(session, stdChannelsAsyncOpenState, doc);
	}

	void sendForkCommand(HandshakeSession &session, const FileDescriptor &fd) {
		TRACE_POINT();
		Json::Value doc;

		doc["command"] = "spawn";
		doc["work_dir"] = session.workDir->getPath();

		writeExact(fd, Json::FastWriter().write(doc), &session.timeoutUsec);
	}

	string readForkCommandResponse(HandshakeSession &session, const FileDescriptor &fd) {
		TRACE_POINT();
		BufferedIO io(fd);

		try {
			return io.readLine(10240, &session.timeoutUsec);
		} catch (const SecurityException &) {
			session.journey.setStepErrored(SPAWNING_KIT_READ_RESPONSE_FROM_PRELOADER);

			SpawnException e(INTERNAL_ERROR, session.journey, session.config);
			addPreloaderEnvDumps(e);
			e.setSummary("The preloader process sent a response that exceeds the maximum size limit.");
			e.setProblemDescriptionHTML(
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application by communicating with a"
				" helper process that we call a \"preloader\". However,"
				" this helper process sent a response that exceeded the"
				" internally-defined maximum size limit.</p>");
			e.setSolutionDescriptionHTML(
				"<p class=\"sole-solution\">"
				"This is probably a bug in the preloader process. Please "
				"<a href=\"" SUPPORT_URL "\">"
				"report this bug</a>."
				"</p>");
			throw e.finalize();
		}
	}

	Json::Value parseForkCommandResponse(HandshakeSession &session, const string &data) {
		TRACE_POINT();
		Json::Value doc;
		Json::Reader reader;

		if (!reader.parse(data, doc)) {
			session.journey.setStepErrored(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER);

			SpawnException e(INTERNAL_ERROR, session.journey, session.config);
			addPreloaderEnvDumps(e);
			e.setSummary("The preloader process sent an unparseable response: " + data);
			e.setProblemDescriptionHTML(
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application by communicating with a"
				" helper process that we call a \"preloader\". However,"
				" this helper process sent a response that looks like"
				" gibberish.</p>"
				"<p>The response is as follows:</p>"
				"<pre>" + escapeHTML(data) + "</pre>");
			e.setSolutionDescriptionHTML(
				"<p class=\"sole-solution\">"
				"This is probably a bug in the preloader process. Please "
				"<a href=\"" SUPPORT_URL "\">"
				"report this bug</a>."
				"</p>");
			throw e.finalize();
		}

		UPDATE_TRACE_POINT();
		if (!validateForkCommandResponse(doc)) {
			session.journey.setStepErrored(SPAWNING_KIT_PARSE_RESPONSE_FROM_PRELOADER);

			SpawnException e(INTERNAL_ERROR, session.journey, session.config);
			addPreloaderEnvDumps(e);
			e.setSummary("The preloader process sent a response that does not"
				" match the expected structure: " + stringifyJson(doc));
			e.setProblemDescriptionHTML(
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application by communicating with a"
				" helper process that we call a \"preloader\". However,"
				" this helper process sent a response that does not match"
				" the structure that " SHORT_PROGRAM_NAME " expects.</p>"
				"<p>The response is as follows:</p>"
				"<pre>" + escapeHTML(doc.toStyledString()) + "</pre>");
			e.setSolutionDescriptionHTML(
				"<p class=\"sole-solution\">"
				"This is probably a bug in the preloader process. Please "
				"<a href=\"" SUPPORT_URL "\">"
				"report this bug</a>."
				"</p>");
			throw e.finalize();
		}

		return doc;
	}

	bool validateForkCommandResponse(const Json::Value &doc) const {
		if (!doc.isObject()) {
			return false;
		}
		if (!doc.isMember("result") || !doc["result"].isString()) {
			return false;
		}
		if (doc["result"].asString() == "ok") {
			if (!doc.isMember("pid") || !doc["pid"].isInt()) {
				return false;
			}
			return true;
		} else if (doc["result"].asString() == "error") {
			if (!doc.isMember("message") || !doc["message"].isString()) {
				return false;
			}
			return true;
		} else {
			return false;
		}
	}

	ForkResult handleForkCommandResponse(HandshakeSession &session,
		const StdChannelsAsyncOpenStatePtr &stdChannelsAsyncOpenState,
		const Json::Value &doc)
	{
		TRACE_POINT();
		if (doc["result"].asString() == "ok") {
			return handleForkCommandResponseSuccess(session, stdChannelsAsyncOpenState,
				doc);
		} else {
			P_ASSERT_EQ(doc["result"].asString(), "error");
			return handleForkCommandResponseError(session, doc);
		}
	}

	ForkResult handleForkCommandResponseSuccess(HandshakeSession &session,
		const StdChannelsAsyncOpenStatePtr &stdChannelsAsyncOpenState, const Json::Value &doc)
	{
		TRACE_POINT();
		pid_t spawnedPid = doc["pid"].asInt();

		// How do we know the preloader actually forked a process
		// instead of reporting the PID of a random other existing process?
		// For security reasons we perform a bunch of sanity checks,
		// including checking the PID's UID.

		if (spawnedPid < 1) {
			UPDATE_TRACE_POINT();
			session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);

			SpawnException e(INTERNAL_ERROR, session.journey, session.config);
			addPreloaderEnvDumps(e);
			e.setSummary("The the preloader said it spawned a process with PID "
				+ toString(spawnedPid) + ", which is not allowed.");
			e.setSubprocessPid(spawnedPid);
			e.setStdoutAndErrData(getBackgroundIOCapturerData(
				stdChannelsAsyncOpenState->stdoutAndErrCapturer));
			e.setProblemDescriptionHTML(
				"<h2>Application process has unexpected PID</h2>"
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application by communicating with a"
				" helper process that we call a \"preloader\". However,"
				" the preloader reported that it started a process with"
				" a PID of " + toString(spawnedPid) + ", which is not"
				" allowed.</p>");
			if (!session.config->genericApp && session.config->startsUsingWrapper
				&& session.config->wrapperSuppliedByThirdParty)
			{
				e.setSolutionDescriptionHTML(
					"<h2>Please report this bug</h2>"
					"<p class=\"sole-solution\">"
					"This is probably a bug in the preloader process. The preloader "
					"wrapper program is not written by the " PROGRAM_NAME " authors, "
					"but by a third party. Please report this bug to the author of "
					"the preloader wrapper program."
					"</p>");
			} else {
				e.setSolutionDescriptionHTML(
					"<h2>Please report this bug</h2>"
					"<p class=\"sole-solution\">"
					"This is probably a bug in the preloader process. The preloader "
					"is an internal tool part of " PROGRAM_NAME ". Please "
					"<a href=\"" SUPPORT_URL "\">"
					"report this bug</a>."
					"</p>");
			}
			throw e.finalize();
		}

		UPDATE_TRACE_POINT();
		uid_t spawnedUid = getProcessUid(session, spawnedPid,
			stdChannelsAsyncOpenState->stdoutAndErrCapturer);
		if (spawnedUid != session.uid) {
			UPDATE_TRACE_POINT();
			session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);

			SpawnException e(INTERNAL_ERROR, session.journey, session.config);
			addPreloaderEnvDumps(e);
			e.setSummary("The process that the preloader said it spawned, PID "
				+ toString(spawnedPid) + ", has UID " + toString(spawnedUid)
				+ ", but the expected UID is " + toString(session.uid));
			e.setSubprocessPid(spawnedPid);
			e.setStdoutAndErrData(getBackgroundIOCapturerData(
				stdChannelsAsyncOpenState->stdoutAndErrCapturer));
			e.setProblemDescriptionHTML(
				"<h2>Application process has unexpected UID</h2>"
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application by communicating with a"
				" helper process that we call a \"preloader\". However,"
				" the web application process that the preloader started"
				" belongs to the wrong user. The UID of the web"
				" application process should be " + toString(session.uid)
				+ ", but is actually " + toString(session.uid) + ".</p>");
			if (!session.config->genericApp && session.config->startsUsingWrapper
				&& session.config->wrapperSuppliedByThirdParty)
			{
				e.setSolutionDescriptionHTML(
					"<h2>Please report this bug</h2>"
					"<p class=\"sole-solution\">"
					"This is probably a bug in the preloader process. The preloader "
					"wrapper program is not written by the " PROGRAM_NAME " authors, "
					"but by a third party. Please report this bug to the author of "
					"the preloader wrapper program."
					"</p>");
			} else {
				e.setSolutionDescriptionHTML(
					"<h2>Please report this bug</h2>"
					"<p class=\"sole-solution\">"
					"This is probably a bug in the preloader process. The preloader "
					"is an internal tool part of " PROGRAM_NAME ". Please "
					"<a href=\"" SUPPORT_URL "\">"
					"report this bug</a>."
					"</p>");
			}
			throw e.finalize();
		}

		UPDATE_TRACE_POINT();
		ScopeGuard guard(boost::bind(nonInterruptableKillAndWaitpid, spawnedPid));
		waitForStdChannelFifosToBeOpenedByPeer(stdChannelsAsyncOpenState,
			session, spawnedPid);

		UPDATE_TRACE_POINT();
		string alreadyReadStdoutAndErrData;
		if (stdChannelsAsyncOpenState->stdoutAndErrCapturer != NULL) {
			stdChannelsAsyncOpenState->stdoutAndErrCapturer->stop();
			alreadyReadStdoutAndErrData = stdChannelsAsyncOpenState->stdoutAndErrCapturer->getData();
		}
		guard.clear();
		return ForkResult(spawnedPid, stdChannelsAsyncOpenState->stdinFd,
			stdChannelsAsyncOpenState->stdoutAndErrFd,
			alreadyReadStdoutAndErrData);
	}

	ForkResult handleForkCommandResponseError(HandshakeSession &session,
		const Json::Value &doc)
	{
		session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);

		SpawnException e(INTERNAL_ERROR, session.journey, session.config);
		addPreloaderEnvDumps(e);
		e.setSummary("An error occured while starting the web application: "
			+ doc["message"].asString());
		e.setProblemDescriptionHTML(
			"<p>The " PROGRAM_NAME " application server tried to"
			" start the web application by communicating with a"
			" helper process that we call a \"preloader\". However, "
			" this helper process reported an error:</p>"
			"<pre>" + escapeHTML(doc["message"].asString()) + "</pre>");
		e.setSolutionDescriptionHTML(
			"<p class=\"sole-solution\">"
			"Please try troubleshooting the problem by studying the"
			" <strong>error message</strong> and the"
			" <strong>diagnostics</strong> reports. You can also"
			" consult <a href=\"" SUPPORT_URL "\">the " SHORT_PROGRAM_NAME
			" support resources</a> for help.</p>");
		throw e.finalize();
	}

	void createStdChannelFifos(const HandshakeSession &session) {
		const string &workDir = session.workDir->getPath();
		createFifo(session, workDir + "/stdin");
		createFifo(session, workDir + "/stdout_and_err");
	}

	void createFifo(const HandshakeSession &session, const string &path) {
		int ret;

		do {
			ret = mkfifo(path.c_str(), 0600);
		} while (ret == -1 && errno == EAGAIN);
		if (ret == -1) {
			int e = errno;
			throw FileSystemException("Cannot create FIFO file " + path,
				e, path);
		}

		ret = syscalls::chown(path.c_str(), session.uid, session.gid);
		if (ret == -1) {
			int e = errno;
			throw FileSystemException("Cannot change owner and group on FIFO file " + path,
				e, path);
		}
	}

	string getBackgroundIOCapturerData(const BackgroundIOCapturerPtr &capturer) const {
		if (capturer != NULL) {
			// Sleep shortly to allow the child process to finish writing logs.
			syscalls::usleep(50000);
			return capturer->getData();
		} else {
			return string();
		}
	}

	uid_t getProcessUid(HandshakeSession &session, pid_t pid,
		const BackgroundIOCapturerPtr &stdoutAndErrCapturer)
	{
		TRACE_POINT();
		uid_t uid = (uid_t) -1;

		try {
			vector<pid_t> pids;
			pids.push_back(pid);
			ProcessMetricMap result = ProcessMetricsCollector().collect(pids);
			uid = result[pid].uid;
		} catch (const ParseException &) {
			HandshakePerform::loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer);
			session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);

			SpawnException e(INTERNAL_ERROR, session.journey, session.config);
			addPreloaderEnvDumps(e);
			e.setSummary("Unable to query the UID of spawned application process "
				+ toString(pid) + ": error parsing 'ps' output");
			e.setSubprocessPid(pid);
			e.setProblemDescriptionHTML(
				"<h2>Unable to use 'ps' to query PID " + toString(pid) + "</h2>"
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application. As part of the starting"
				" procedure, " SHORT_PROGRAM_NAME " also tried to query"
				" the system user ID of the web application process"
				" using the operating system's \"ps\" tool. However,"
				" this tool returned output that " SHORT_PROGRAM_NAME
				" could not understand.</p>");
			e.setSolutionDescriptionHTML(
				createSolutionDescriptionForProcessMetricsCollectionError());
			throw e.finalize();
		} catch (const SystemException &originalException) {
			HandshakePerform::loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer);
			session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);

			SpawnException e(OPERATING_SYSTEM_ERROR, session.journey, session.config);
			addPreloaderEnvDumps(e);
			e.setSummary("Unable to query the UID of spawned application process "
				+ toString(pid) + "; error capturing 'ps' output: "
				+ originalException.what());
			e.setSubprocessPid(pid);
			e.setProblemDescriptionHTML(
				"<h2>Error capturing 'ps' output for PID " + toString(pid) + "</h2>"
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application. As part of the starting"
				" procedure, " SHORT_PROGRAM_NAME " also tried to query"
				" the system user ID of the web application process."
				" This is done by using the operating system's \"ps\""
				" tool and by querying operating system APIs and special"
				" files. However, an error was encountered while doing"
				" one of those things.</p>"
				"<p>The error returned by the operating system is as follows:</p>"
				"<pre>" + escapeHTML(originalException.what()) + "</pre>");
			e.setSolutionDescriptionHTML(
				createSolutionDescriptionForProcessMetricsCollectionError());
			throw e.finalize();
		}

		UPDATE_TRACE_POINT();
		if (uid == (uid_t) -1) {
			if (osProcessExists(pid)) {
				HandshakePerform::loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer);
				session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);

				SpawnException e(INTERNAL_ERROR, session.journey, session.config);
				addPreloaderEnvDumps(e);
				e.setSummary("Unable to query the UID of spawned application process "
					+ toString(pid) + ": 'ps' did not report information"
					" about this process");
				e.setSubprocessPid(pid);
				e.setProblemDescriptionHTML(
					"<h2>'ps' did not return any information about PID " + toString(pid) + "</h2>"
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application. As part of the starting"
					" procedure, " SHORT_PROGRAM_NAME " also tried to query"
					" the system user ID of the web application process"
					" using the operating system's \"ps\" tool. However,"
					" this tool did not return any information about"
					" the web application process.</p>");
				e.setSolutionDescriptionHTML(
					createSolutionDescriptionForProcessMetricsCollectionError());
				throw e.finalize();
			} else {
				HandshakePerform::loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer);
				session.journey.setStepErrored(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);

				SpawnException e(INTERNAL_ERROR, session.journey, session.config);
				addPreloaderEnvDumps(e);
				e.setSummary("The application process spawned from the preloader"
					" seems to have exited prematurely");
				e.setSubprocessPid(pid);
				e.setStdoutAndErrData(getBackgroundIOCapturerData(stdoutAndErrCapturer));
				e.setProblemDescriptionHTML(
					"<h2>Application process exited prematurely</h2>"
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application. As part of the starting"
					" procedure, " SHORT_PROGRAM_NAME " also tried to query"
					" the system user ID of the web application process"
					" using the operating system's \"ps\" tool. However,"
					" this tool did not return any information about"
					" the web application process.</p>");
				e.setSolutionDescriptionHTML(
					createSolutionDescriptionForProcessMetricsCollectionError());
				throw e.finalize();
			}
		} else {
			return uid;
		}
	}

	static string createSolutionDescriptionForProcessMetricsCollectionError() {
		const char *path = getenv("PATH");
		if (path == NULL || path[0] == '\0') {
			path = "(empty)";
		}
		return "<div class=\"multiple-solutions\">"

			"<h3>Check whether the \"ps\" tool is installed and accessible by "
			SHORT_PROGRAM_NAME "</h3>"
			"<p>Maybe \"ps\" is not installed. Or maybe it is installed, but "
			SHORT_PROGRAM_NAME " cannot find it inside its PATH. Or"
			" maybe filesystem permissions disallow " SHORT_PROGRAM_NAME
			" from accessing \"ps\". Please check all these factors and"
			" fix them if necessary.</p>"
			"<p>" SHORT_PROGRAM_NAME "'s PATH is:</p>"
			"<pre>" + escapeHTML(path) + "</pre>"

			"<h3>Check whether the server is low on resources</h3>"
			"<p>Maybe the server is currently low on resources. This would"
			" cause the \"ps\" tool to encounter errors. Please study the"
			" <em>error message</em> and the <em>diagnostics reports</em> to"
			" verify whether this is the case. Key things to check for:</p>"
			"<ul>"
			"<li>Excessive CPU usage</li>"
			"<li>Memory and swap</li>"
			"<li>Ulimits</li>"
			"</ul>"
			"<p>If the server is indeed low on resources, find a way to"
			" free up some resources.</p>"

			"<h3>Check whether /proc is mounted</h3>"
			"<p>On many operating systems including Linux and FreeBSD, \"ps\""
			" only works if /proc is mounted. Please check this.</p>"

			"<h3>Still no luck?</h3>"
			"<p>Please try troubleshooting the problem by studying the"
			" <em>diagnostics</em> reports.</p>"

			"</div>";
	}

	static void adjustTimeout(MonotonicTimeUsec startTime, unsigned long long *timeout) {
		boost::this_thread::disable_interruption di;
		boost::this_thread::disable_syscall_interruption dsi;
		MonotonicTimeUsec now = SystemTime::getMonotonicUsec();
		assert(now >= startTime);
		MonotonicTimeUsec diff = now - startTime;
		if (*timeout >= diff) {
			*timeout -= diff;
		} else {
			*timeout = 0;
		}
	}

	static void doClosedir(DIR *dir) {
		closedir(dir);
	}

	static string findPreloaderCommandSocketAddress(const HandshakeSession &session) {
		const vector<Result::Socket> &sockets = session.result.sockets;
		vector<Result::Socket>::const_iterator it, end = sockets.end();
		for (it = sockets.begin(); it != end; it++) {
			if (it->protocol == "preloader") {
				return it->address;
			}
		}
		return string();
	}

	static StringKeyTable<string> loadAnnotationsFromEnvDumpDir(const string &envDumpDir,
		int envDumpAnnotationsDirFd)
	{
		string path = envDumpDir + "/annotations";
		DIR *dir = opendir(path.c_str());
		if (dir == NULL) {
			return StringKeyTable<string>();
		}

		ScopeGuard guard(boost::bind(doClosedir, dir));
		StringKeyTable<string> result;
		struct dirent *ent;
		while ((ent = readdir(dir)) != NULL) {
			if (ent->d_name[0] != '.') {
				result.insert(ent->d_name, strip(safeReadFile(envDumpAnnotationsDirFd,
					ent->d_name, SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first), true);
			}
		}

		result.compact();

		return result;
	}

	void addPreloaderEnvDumps(SpawnException &e) const {
		e.setPreloaderPid(pid);
		e.setPreloaderEnvvars(preloaderEnvvars);
		e.setPreloaderUserInfo(preloaderUserInfo);
		e.setPreloaderUlimits(preloaderUlimits);

		if (e.getSubprocessEnvvars().empty()) {
			e.setSubprocessEnvvars(preloaderEnvvars);
		}
		if (e.getSubprocessUserInfo().empty()) {
			e.setSubprocessUserInfo(preloaderUserInfo);
		}
		if (e.getSubprocessUlimits().empty()) {
			e.setSubprocessUlimits(preloaderUlimits);
		}

		StringKeyTable<string>::ConstIterator it(preloaderAnnotations);
		while (*it != NULL) {
			e.setAnnotation(it.getKey(), it.getValue(), false);
			it.next();
		}
	}

public:
	SmartSpawner(Context *context,
		const vector<string> &preloaderCommand,
		const AppPoolOptions &_options)
		: Spawner(context),
		  preloaderCommandString(createCommandString(preloaderCommand))
	{
		if (preloaderCommand.size() < 2) {
			throw ArgumentException("preloaderCommand must have at least 2 elements");
		}

		options    = _options.copyAndPersist();
		pid        = -1;
		m_lastUsed = SystemTime::getUsec();
	}

	virtual ~SmartSpawner() {
		boost::lock_guard<boost::mutex> l(syncher);
		stopPreloader();
	}

	virtual Result spawn(const AppPoolOptions &options) {
		TRACE_POINT();
		P_ASSERT_EQ(options.appType, this->options.appType);
		P_ASSERT_EQ(options.appRoot, this->options.appRoot);

		P_DEBUG("Spawning new process: appRoot=" << options.appRoot);
		possiblyRaiseInternalError(options);

		{
			boost::lock_guard<boost::mutex> l(simpleFieldSyncher);
			m_lastUsed = SystemTime::getUsec();
		}
		UPDATE_TRACE_POINT();
		boost::lock_guard<boost::mutex> l(syncher);
		if (!preloaderStarted()) {
			UPDATE_TRACE_POINT();
			startPreloader();
		}

		UPDATE_TRACE_POINT();
		Config config;
		Json::Value extraArgs;
		try {
			setConfigFromAppPoolOptions(&config, extraArgs, options);
		} catch (const std::exception &originalException) {
			Journey journey(SPAWN_THROUGH_PRELOADER, true);
			journey.setStepErrored(SPAWNING_KIT_PREPARATION, true);
			SpawnException e(originalException, journey, &config);
			addPreloaderEnvDumps(e);
			throw e.finalize();
		}

		UPDATE_TRACE_POINT();
		HandshakeSession session(*context, config, SPAWN_THROUGH_PRELOADER);
		session.journey.setStepInProgress(SPAWNING_KIT_PREPARATION);
		JourneyStep stepToMarkAsErrored = SPAWNING_KIT_PREPARATION;

		try {
			UPDATE_TRACE_POINT();
			HandshakePrepare prepare(session, extraArgs);
			prepare.execute();
			createStdChannelFifos(session);
			prepare.finalize();
			session.journey.setStepPerformed(SPAWNING_KIT_PREPARATION, true);

			UPDATE_TRACE_POINT();
			ForkResult forkResult = invokeForkCommand(session, stepToMarkAsErrored);

			UPDATE_TRACE_POINT();
			ScopeGuard guard(boost::bind(nonInterruptableKillAndWaitpid, forkResult.pid));
			P_DEBUG("Process forked for appRoot=" << options.appRoot << ": PID " << forkResult.pid);

			UPDATE_TRACE_POINT();
			session.journey.setStepPerformed(SPAWNING_KIT_PROCESS_RESPONSE_FROM_PRELOADER);
			session.journey.setStepInProgress(PRELOADER_PREPARATION);
			session.journey.setStepInProgress(SPAWNING_KIT_HANDSHAKE_PERFORM);
			stepToMarkAsErrored = SPAWNING_KIT_HANDSHAKE_PERFORM;
			HandshakePerform(session, forkResult.pid, forkResult.stdinFd,
				forkResult.stdoutAndErrFd, forkResult.alreadyReadStdoutAndErrData).
				execute();
			guard.clear();
			session.journey.setStepPerformed(SPAWNING_KIT_HANDSHAKE_PERFORM);
			P_DEBUG("Process spawning done: appRoot=" << options.appRoot <<
				", pid=" << forkResult.pid);
			return session.result;
		} catch (SpawnException &e) {
			addPreloaderEnvDumps(e);
			throw e;
		} catch (const std::exception &originalException) {
			session.journey.setStepErrored(stepToMarkAsErrored, true);
			SpawnException e(originalException, session.journey,
				&config);
			addPreloaderEnvDumps(e);
			throw e.finalize();
		}
	}

	virtual bool cleanable() const {
		return true;
	}

	virtual void cleanup() {
		TRACE_POINT();
		{
			boost::lock_guard<boost::mutex> l(simpleFieldSyncher);
			m_lastUsed = SystemTime::getUsec();
		}
		boost::lock_guard<boost::mutex> lock(syncher);
		stopPreloader();
	}

	virtual unsigned long long lastUsed() const {
		boost::lock_guard<boost::mutex> lock(simpleFieldSyncher);
		return m_lastUsed;
	}

	pid_t getPreloaderPid() const {
		boost::lock_guard<boost::mutex> lock(simpleFieldSyncher);
		return pid;
	}
};


} // namespace SpawningKit
} // namespace Passenger

#endif /* _PASSENGER_SPAWNING_KIT_SMART_SPAWNER_H_ */
