/*
 *  Phusion Passenger - https://www.phusionpassenger.com/
 *  Copyright (c) 2016-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_HANDSHAKE_PERFORM_H_
#define _PASSENGER_SPAWNING_KIT_HANDSHAKE_PERFORM_H_

#include <boost/thread.hpp>
#include <boost/make_shared.hpp>
#include <boost/bind/bind.hpp>
#include <oxt/thread.hpp>
#include <oxt/system_calls.hpp>
#include <oxt/backtrace.hpp>
#include <string>
#include <vector>
#include <exception>
#include <cstddef>
#include <cstdlib>
#include <cerrno>
#include <cassert>

#include <sys/types.h>
#include <dirent.h>

#include <jsoncpp/json.h>

#include <Constants.h>
#include <Exceptions.h>
#include <FileDescriptor.h>
#include <FileTools/FileManip.h>
#include <FileTools/PathManip.h>
#include <IOTools/IOUtils.h>
#include <Utils.h>
#include <Utils/ScopeGuard.h>
#include <SystemTools/SystemTime.h>
#include <StrIntTools/StrIntUtils.h>
#include <Core/SpawningKit/Config.h>
#include <Core/SpawningKit/Exceptions.h>
#include <Core/SpawningKit/Handshake/BackgroundIOCapturer.h>
#include <Core/SpawningKit/Handshake/Session.h>

namespace Passenger {
namespace SpawningKit {

using namespace std;
using namespace oxt;


/**
 * For an introduction see README.md, section
 * "The handshake and the HandshakePerform class".
 */
class HandshakePerform {
private:
	enum FinishState {
		// The app hasn't finished spawning yet.
		NOT_FINISHED,
		// The app has successfully finished spawning.
		FINISH_SUCCESS,
		// The app has finished spawning with an error.
		FINISH_ERROR,
		// An internal error occurred in watchFinishSignal().
		FINISH_INTERNAL_ERROR
	};

	HandshakeSession &session;
	Config * const config;
	const pid_t pid;
	const FileDescriptor stdinFd;
	const FileDescriptor stdoutAndErrFd;
	const string alreadyReadStdoutAndErrData;


	/**
	 * These objects captures the process's stdout and stderr while handshake is
	 * in progress. If handshaking fails, then any output captured by these objects
	 * will be stored into the resulting SpawnException's error page.
	 */
	BackgroundIOCapturerPtr stdoutAndErrCapturer;

	boost::mutex syncher;
	boost::condition_variable cond;

	oxt::thread *processExitWatcher;
	oxt::thread *finishSignalWatcher;
	bool processExited;
	FinishState finishState;
	string finishSignalWatcherErrorMessage;
	ErrorCategory finishSignalWatcherErrorCategory;

	oxt::thread *socketPingabilityWatcher;
	bool socketIsNowPingable;


	void initializeStdchannelsCapturing() {
		if (stdoutAndErrFd != -1) {
			stdoutAndErrCapturer = boost::make_shared<BackgroundIOCapturer>(
				stdoutAndErrFd, pid, "output", alreadyReadStdoutAndErrData);
			stdoutAndErrCapturer->setEndReachedCallback(boost::bind(
				&HandshakePerform::wakeupEventLoop, this));
			stdoutAndErrCapturer->start();
		}
	}

	void startWatchingProcessExit() {
		processExitWatcher = new oxt::thread(
			boost::bind(&HandshakePerform::watchProcessExit, this),
			"SpawningKit: process exit watcher", 64 * 1024);
	}

	void watchProcessExit() {
		TRACE_POINT();
		int ret = syscalls::waitpid(pid, NULL, 0);
		if (ret >= 0 || errno == EPERM) {
			boost::lock_guard<boost::mutex> l(syncher);
			processExited = true;
			wakeupEventLoop();
		}
	}

	void startWatchingFinishSignal() {
		finishSignalWatcher = new oxt::thread(
			boost::bind(&HandshakePerform::watchFinishSignal, this),
			"SpawningKit: finish signal watcher", 64 * 1024);
	}

	void watchFinishSignal() {
		TRACE_POINT();
		try {
			string path = session.responseDir + "/finish";
			int fd = syscalls::openat(session.responseDirFd, "finish",
				O_RDONLY | O_NOFOLLOW);
			if (fd == -1) {
				int e = errno;
				throw FileSystemException("Error opening FIFO " + path,
					e, path);
			}
			FdGuard guard(fd, __FILE__, __LINE__);

			char buf = '0';
			ssize_t ret = syscalls::read(fd, &buf, 1);
			if (ret == -1) {
				int e = errno;
				throw FileSystemException("Error reading from FIFO " + path,
					e, path);
			}

			guard.runNow();

			boost::lock_guard<boost::mutex> l(syncher);
			if (buf == '1') {
				finishState = FINISH_SUCCESS;
			} else {
				finishState = FINISH_ERROR;
			}
			wakeupEventLoop();
		} catch (const std::exception &e) {
			boost::lock_guard<boost::mutex> l(syncher);
			finishState = FINISH_INTERNAL_ERROR;
			finishSignalWatcherErrorMessage = e.what();
			finishSignalWatcherErrorCategory =
				inferErrorCategoryFromAnotherException(e,
					SPAWNING_KIT_HANDSHAKE_PERFORM);
			wakeupEventLoop();
		}
	}

	void startWatchingSocketPingability() {
		socketPingabilityWatcher = new oxt::thread(
			boost::bind(&HandshakePerform::watchSocketPingability, this),
			"SpawningKit: socket pingability watcher", 64 * 1024);
	}

	void watchSocketPingability() {
		TRACE_POINT();

		while (true) {
			unsigned long long timeout = 100000;
			bool pingable;

			try {
				pingable = pingTcpServer("127.0.0.1", session.expectedStartPort, &timeout);
			} catch (const std::exception &e) {
				P_WARN("Error checking whether 127.0.0.1:" << session.expectedStartPort
					<< " is connectable: " << e.what());
				syscalls::usleep(50000);
				continue;
			}

			if (pingable) {
				boost::lock_guard<boost::mutex> l(syncher);
				socketIsNowPingable = true;
				finishState = FINISH_SUCCESS;
				wakeupEventLoop();
				break;
			} else {
				syscalls::usleep(50000);
			}
		}
	}

	void waitUntilSpawningFinished(boost::unique_lock<boost::mutex> &l) {
		TRACE_POINT();
		bool done;

		do {
			boost::this_thread::interruption_point();
			done = checkCurrentState();
			if (!done) {
				MonotonicTimeUsec begin = SystemTime::getMonotonicUsec();
				cond.timed_wait(l, posix_time::microseconds(session.timeoutUsec));
				MonotonicTimeUsec end = SystemTime::getMonotonicUsec();
				if (end - begin > session.timeoutUsec) {
					session.timeoutUsec = 0;
				} else {
					session.timeoutUsec -= end - begin;
				}
			}
		} while (!done);
	}

	bool checkCurrentState() {
		TRACE_POINT();

		if ((stdoutAndErrCapturer != NULL && stdoutAndErrCapturer->isStopped())
		 || processExited)
		{
			UPDATE_TRACE_POINT();
			sleepShortlyToCaptureMoreStdoutStderr();
			loadJourneyStateFromResponseDir();
			if (session.journey.getFirstFailedStep() == UNKNOWN_JOURNEY_STEP) {
				session.journey.setStepErrored(bestGuessSubprocessFailedStep(), true);
			}

			SpawnException e(
				inferErrorCategoryFromResponseDir(INTERNAL_ERROR),
				session.journey,
				config);
			e.setSummary("The application process exited prematurely.");
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			loadSubprocessErrorMessagesAndEnvDump(e);
			throw e.finalize();
		}

		if (session.timeoutUsec == 0) {
			UPDATE_TRACE_POINT();
			sleepShortlyToCaptureMoreStdoutStderr();

			loadJourneyStateFromResponseDir();
			session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM);

			SpawnException e(TIMEOUT_ERROR, session.journey, config);
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			loadSubprocessErrorMessagesAndEnvDump(e);
			throw e.finalize();
		}

		return (config->genericApp && socketIsNowPingable)
			|| (!config->genericApp && finishState != NOT_FINISHED);
	}

	Result handleResponse() {
		TRACE_POINT();
		switch (finishState) {
		case FINISH_SUCCESS:
			return handleSuccessResponse();
		case FINISH_ERROR:
			handleErrorResponse();
			return Result(); // Never reached, shut up compiler warning.
		case FINISH_INTERNAL_ERROR:
			handleInternalError();
			return Result(); // Never reached, shut up compiler warning.
		default:
			P_BUG("Unknown finishState " + toString((int) finishState));
			return Result(); // Never reached, shut up compiler warning.
		}
	}

	Result handleSuccessResponse() {
		TRACE_POINT();
		Result &result = session.result;
		vector<StaticString> internalFieldErrors, appSuppliedFieldErrors;

		result.pid = pid;
		result.stdinFd = stdinFd;
		result.stdoutAndErrFd = stdoutAndErrFd;
		result.spawnEndTime = SystemTime::getUsec();
		result.spawnEndTimeMonotonic = SystemTime::getMonotonicUsec();
		setResultType(result);

		if (socketIsNowPingable) {
			assert(config->genericApp || config->findFreePort);
			result.sockets.push_back(Result::Socket());
			Result::Socket &socket = result.sockets.back();
			socket.address = "tcp://127.0.0.1:" + toString(session.expectedStartPort);
			socket.protocol = "http";
			socket.concurrency = -1;
			socket.acceptHttpRequests = true;
		}

		UPDATE_TRACE_POINT();
		if (fileExists(session.responseDir + "/properties.json")) {
			loadResultPropertiesFromResponseDir(!socketIsNowPingable);

			UPDATE_TRACE_POINT();
			if (session.journey.getType() == START_PRELOADER
				&& !resultHasSocketWithPreloaderProtocol())
			{
				throwSpawnExceptionBecauseAppDidNotProvidePreloaderProtocolSockets();
			} else if (session.journey.getType() != START_PRELOADER
				&& !resultHasSocketThatAcceptsHttpRequests())
			{
				throwSpawnExceptionBecauseAppDidNotProvideSocketsThatAcceptRequests();
			}
		}

		UPDATE_TRACE_POINT();
		if (result.validate(internalFieldErrors, appSuppliedFieldErrors)) {
			return result;
		} else {
			throwSpawnExceptionBecauseOfResultValidationErrors(internalFieldErrors,
				appSuppliedFieldErrors);
			abort(); // never reached, shut up compiler warning
		}
	}

	void handleErrorResponse() {
		TRACE_POINT();
		sleepShortlyToCaptureMoreStdoutStderr();
		loadJourneyStateFromResponseDir();
		if (session.journey.getFirstFailedStep() == UNKNOWN_JOURNEY_STEP) {
			session.journey.setStepErrored(bestGuessSubprocessFailedStep(), true);
		}

		SpawnException e(
			inferErrorCategoryFromResponseDir(INTERNAL_ERROR),
			session.journey,
			config);
		e.setSummary("The web application aborted with an error during startup.");
		e.setSubprocessPid(pid);
		e.setStdoutAndErrData(getStdoutErrData());
		loadSubprocessErrorMessagesAndEnvDump(e);
		throw e.finalize();
	}

	void handleInternalError() {
		TRACE_POINT();
		sleepShortlyToCaptureMoreStdoutStderr();

		loadJourneyStateFromResponseDir();
		session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM);

		SpawnException e(
			finishSignalWatcherErrorCategory,
			session.journey,
			config);
		e.setSummary("An internal error occurred while spawning an application process: "
			+ finishSignalWatcherErrorMessage);
		e.setAdvancedProblemDetails(finishSignalWatcherErrorMessage);
		e.setSubprocessPid(pid);
		e.setStdoutAndErrData(getStdoutErrData());
		throw e.finalize();
	}

	void loadResultPropertiesFromResponseDir(bool socketsRequired) {
		TRACE_POINT();
		Result &result = session.result;
		string path = session.responseDir + "/properties.json";
		Json::Reader reader;
		Json::Value doc;
		vector<string> errors;

		// We already checked whether properties.json exists before invoking
		// this method, so if safeReadFile() fails then we can't be sure that
		// it's an application problem. This is why we want the SystemException
		// to propagate to higher layers so that there it can be turned into
		// a generic filesystem-related or IO-related SpawnException, as opposed
		// to one about this problem specifically.

		UPDATE_TRACE_POINT();
		pair<string, bool> jsonContent = safeReadFile(session.responseDirFd, "properties.json",
			SPAWNINGKIT_MAX_PROPERTIES_JSON_SIZE);
		if (!jsonContent.second) {
			errors.push_back("Error parsing " + path + ": file bigger than "
				+ toString(SPAWNINGKIT_MAX_PROPERTIES_JSON_SIZE) + " bytes");
			throwSpawnExceptionBecauseOfResultValidationErrors(vector<string>(),
				errors);
		}
		if (!reader.parse(jsonContent.first, doc)) {
			errors.push_back("Error parsing " + path + ": " +
				reader.getFormattedErrorMessages());
			throwSpawnExceptionBecauseOfResultValidationErrors(vector<string>(),
				errors);
		}

		UPDATE_TRACE_POINT();
		validateResultPropertiesFile(doc, socketsRequired, errors);
		if (!errors.empty()) {
			errors.insert(errors.begin(), "The following errors were detected in "
				+ path + ":");
			throwSpawnExceptionBecauseOfResultValidationErrors(vector<string>(),
				errors);
		}

		if (!socketsRequired && (!doc.isMember("sockets") || doc["sockets"].empty())) {
			return;
		}

		UPDATE_TRACE_POINT();
		Json::Value::iterator it, end = doc["sockets"].end();
		for (it = doc["sockets"].begin(); it != end; it++) {
			const Json::Value &socketDoc = *it;
			result.sockets.push_back(Result::Socket());
			Result::Socket &socket = result.sockets.back();

			socket.address = socketDoc["address"].asString();
			socket.protocol = socketDoc["protocol"].asString();
			socket.concurrency = socketDoc["concurrency"].asInt();
			if (socketDoc.isMember("accept_http_requests")) {
				socket.acceptHttpRequests = socketDoc["accept_http_requests"].asBool();
			}
			if (socketDoc.isMember("description")) {
				socket.description = socketDoc["description"].asString();
			}
		}
	}

	void validateResultPropertiesFile(const Json::Value &doc, bool socketsRequired,
		vector<string> &errors) const
	{
		TRACE_POINT();
		if (!doc.isMember("sockets")) {
			if (socketsRequired) {
				errors.push_back("'sockets' must be specified");
			}
			return;
		}
		if (!doc["sockets"].isArray()) {
			errors.push_back("'sockets' must be an array");
			return;
		}
		if (socketsRequired && doc["sockets"].empty()) {
			errors.push_back("'sockets' must be non-empty");
			return;
		}

		UPDATE_TRACE_POINT();
		Json::Value::const_iterator it, end = doc["sockets"].end();
		for (it = doc["sockets"].begin(); it != end; it++) {
			const Json::Value &socketDoc = *it;

			if (!socketDoc.isObject()) {
				errors.push_back("'sockets[" + toString(it.index())
					+ "]' must be an object");
				continue;
			}

			validateResultPropertiesFileSocketField(socketDoc,
				"address", Json::stringValue, it.index(),
				true, true, errors);
			validateResultPropertiesFileSocketField(socketDoc,
				"protocol", Json::stringValue, it.index(),
				true, true, errors);
			validateResultPropertiesFileSocketField(socketDoc,
				"description", Json::stringValue, it.index(),
				false, true, errors);
			validateResultPropertiesFileSocketField(socketDoc,
				"concurrency", Json::intValue, it.index(),
				true, false, errors);
			validateResultPropertiesFileSocketField(socketDoc,
				"accept_http_requests", Json::booleanValue, it.index(),
				false, false, errors);
			validateResultPropertiesFileSocketAddress(socketDoc,
				it.index(), errors);
		}
	}

	void validateResultPropertiesFileSocketField(const Json::Value &doc,
		const char *key, Json::ValueType type, unsigned int index, bool required,
		bool requireNonEmpty, vector<string> &errors) const
	{
		if (!doc.isMember(key)) {
			if (required) {
				errors.push_back("'sockets[" + toString(index)
					+ "]." + key + "' must be specified");
			}
		} else if (doc[key].type() != type) {
			const char *typeDesc;
			switch (type) {
			case Json::stringValue:
				typeDesc = "a string";
				break;
			case Json::intValue:
				typeDesc = "an integer";
				break;
			case Json::booleanValue:
				typeDesc = "a boolean";
				break;
			default:
				typeDesc = "(unknown type)";
				break;
			}
			errors.push_back("'sockets[" + toString(index)
				+ "]." + key + "' must be " + typeDesc);
		} else if (requireNonEmpty && doc[key].asString().empty()) {
			errors.push_back("'sockets[" + toString(index)
				+ "]." + key + "' must be non-empty");
		}
	}

	void validateResultPropertiesFileSocketAddress(const Json::Value &doc,
		unsigned int index, vector<string> &errors) const
	{
		TRACE_POINT();
		if (!doc["address"].isString()
		 || getSocketAddressType(doc["address"].asString()) != SAT_UNIX)
		{
			return;
		}

		string filename = parseUnixSocketAddress(doc["address"].asString());

		if (filename.empty()) {
			errors.push_back("'sockets[" + toString(index)
				+ "].address' contains an empty Unix domain socket filename");
			return;
		}

		if (filename[0] != '/') {
			errors.push_back("'sockets[" + toString(index)
				+ "].address' when referring to a Unix domain socket, must be"
				" an absolute path (given path: " + filename + ")");
			return;
		}

		// If any of the parent directories is writable by a normal user
		// (Joe) that is not the app's user (Jane), then Joe can swap that
		// directory with something else, with contents controlled by Joe.
		// That way, Joe can cause Passenger to connect to (and forward
		// Jane's traffic to) a process that does not actually belong to
		// Jane.
		//
		// To mitigate this risk, we insist that the socket be placed in a
		// directory that we know is safe (instanceDir + "/apps.s").
		// We don't rely on isPathProbablySecureForRootUse() because that
		// function cannot be 100% sure that it is correct.

		UPDATE_TRACE_POINT();
		// instanceDir is only empty in tests
		if (!session.context->instanceDir.empty()) {
			StaticString actualDir = extractDirNameStatic(filename);
			string expectedDir = session.context->instanceDir + "/apps.s";
			if (actualDir != expectedDir) {
				errors.push_back("'sockets[" + toString(index)
					+ "].address', when referring to a Unix domain socket,"
					" must be an absolute path to a file in '"
					+ expectedDir + "' (given path: " + filename + ")");
				return;
			}
		}

		UPDATE_TRACE_POINT();
		struct stat s;
		int ret;
		do {
			ret = lstat(filename.c_str(), &s);
		} while (ret == -1 && errno == EAGAIN);

		if (ret == -1) {
			int e = errno;
			if (e == EEXIST) {
				errors.push_back("'sockets[" + toString(index)
					+ "].address' refers to a non-existant Unix domain"
					" socket file (given path: " + filename + ")");
				return;
			} else {
				throw FileSystemException("Cannot stat " + filename,
					e, filename);
			}
		}

		// We only check the UID, not the GID, because the socket
		// may be automatically made with a different GID than
		// the creating process's due to the setgid bit being set
		// the directory that contains the socket. Furthermore,
		// on macOS it seems that all directories behave as if
		// they have the setgid bit set.

		UPDATE_TRACE_POINT();
		if (s.st_uid != session.uid) {
			errors.push_back("'sockets[" + toString(index)
				+ "].address', when referring to a Unix domain socket file,"
				" must be owned by user " + lookupSystemUsernameByUid(session.uid)
				+ " (actual owner: " + lookupSystemUsernameByUid(s.st_uid) + ")");
		}
	}

	bool resultHasSocketWithPreloaderProtocol() const {
		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 true;
			}
		}
		return false;
	}

	bool resultHasSocketThatAcceptsHttpRequests() const {
		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->acceptHttpRequests) {
				return true;
			}
		}
		return false;
	}

	void wakeupEventLoop() {
		cond.notify_all();
	}

	string getStdoutErrData() const {
		return getStdoutErrData(stdoutAndErrCapturer);
	}

	static string getStdoutErrData(const BackgroundIOCapturerPtr &stdoutAndErrCapturer) {
		if (stdoutAndErrCapturer != NULL) {
			return stdoutAndErrCapturer->getData();
		} else {
			return "(not available)";
		}
	}

	void sleepShortlyToCaptureMoreStdoutStderr() const {
		syscalls::usleep(50000);
	}

	void throwSpawnExceptionBecauseAppDidNotProvidePreloaderProtocolSockets() {
		TRACE_POINT();
		assert(!config->genericApp);

		sleepShortlyToCaptureMoreStdoutStderr();

		if (!config->genericApp && config->startsUsingWrapper) {
			UPDATE_TRACE_POINT();
			loadJourneyStateFromResponseDir();
			session.journey.setStepErrored(SUBPROCESS_WRAPPER_PREPARATION, true);

			SpawnException e(INTERNAL_ERROR, session.journey, config);
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			loadBasicInfoFromEnvDumpDir(e);
			loadAnnotationsFromEnvDumpDir(e);

			if (config->wrapperSuppliedByThirdParty) {
				e.setSummary("Error spawning the web application:"
					" a third-party application wrapper did not"
					" report any sockets to receive preloader commands on.");
			} else {
				e.setSummary("Error spawning the web application:"
					" a " SHORT_PROGRAM_NAME "-internal application"
					" wrapper did not report any sockets to receive"
					" preloader commands on.");
			}

			if (config->wrapperSuppliedByThirdParty) {
				e.setProblemDescriptionHTML(
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application through a helper tool "
					" called the \"wrapper\". This helper tool is not part of "
					SHORT_PROGRAM_NAME ". " SHORT_PROGRAM_NAME " expected"
					" the helper tool to report a socket to receive preloader"
					" commands on, but the helper tool finished its startup"
					" procedure without reporting such a socket.</p>");
			} else {
				e.setProblemDescriptionHTML(
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application through a " SHORT_PROGRAM_NAME
					"-internal helper tool called the \"wrapper\","
					" but " SHORT_PROGRAM_NAME " encountered a bug"
					" in this helper tool. " SHORT_PROGRAM_NAME " expected"
					" the helper tool to report a socket to receive preloader"
					" commands on, but the helper tool finished its startup"
					" procedure without reporting such a socket.</p>");
			}

			if (config->wrapperSuppliedByThirdParty) {
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"This is a bug in the wrapper, so please contact the author of"
					" the wrapper. This problem is outside " SHORT_PROGRAM_NAME
					"'s control. Below follows the command that "
					SHORT_PROGRAM_NAME " tried to execute, so that you can infer"
					" which wrapper was used:</p>"
					"<pre>" + escapeHTML(config->startCommand) + "</pre>");
			} else {
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"This is a bug in " SHORT_PROGRAM_NAME "."
					" <a href=\"" SUPPORT_URL "\">Please report this bug</a>"
					" to the " SHORT_PROGRAM_NAME " authors.</p>");
			}

			throw e.finalize();

		} else {
			UPDATE_TRACE_POINT();
			loadJourneyStateFromResponseDir();
			session.journey.setStepErrored(SUBPROCESS_APP_LOAD_OR_EXEC, true);

			SpawnException e(INTERNAL_ERROR, session.journey, config);
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			loadBasicInfoFromEnvDumpDir(e);
			loadAnnotationsFromEnvDumpDir(e);

			e.setSummary("Error spawning the web application: the application"
				" did not report any sockets to receive preloader commands on.");
			e.setProblemDescriptionHTML(
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application, but encountered a bug"
				" in the application. " SHORT_PROGRAM_NAME " expected"
				" the application to report a socket to receive preloader"
				" commands on, but the application finished its startup"
				" procedure without reporting such a socket.</p>");
			e.setSolutionDescriptionHTML(
				"<p class=\"sole-solution\">"
				"Since this is a bug in the web application, please "
				"report this problem to the application's developer. "
				"This problem is outside " SHORT_PROGRAM_NAME "'s "
				"control.</p>");

			throw e.finalize();
		}
	}

	void throwSpawnExceptionBecauseAppDidNotProvideSocketsThatAcceptRequests() {
		TRACE_POINT();
		assert(!config->genericApp);

		sleepShortlyToCaptureMoreStdoutStderr();

		if (!config->genericApp && config->startsUsingWrapper) {
			UPDATE_TRACE_POINT();
			loadJourneyStateFromResponseDir();
			switch (session.journey.getType()) {
			case SPAWN_DIRECTLY:
			case START_PRELOADER:
				session.journey.setStepErrored(SUBPROCESS_WRAPPER_PREPARATION, true);
				break;
			case SPAWN_THROUGH_PRELOADER:
				session.journey.setStepErrored(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER, true);
				break;
			default:
				P_BUG("Unknown journey type " << (int) session.journey.getType());
			}

			SpawnException e(INTERNAL_ERROR, session.journey, config);
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			loadBasicInfoFromEnvDumpDir(e);
			loadAnnotationsFromEnvDumpDir(e);

			if (config->wrapperSuppliedByThirdParty) {
				e.setSummary("Error spawning the web application:"
					" a third-party application wrapper did not"
					" report any sockets to receive requests on.");
			} else {
				e.setSummary("Error spawning the web application:"
					" a " SHORT_PROGRAM_NAME "-internal application"
					" wrapper did not report any sockets to receive"
					" requests on.");
			}

			if (config->wrapperSuppliedByThirdParty) {
				e.setProblemDescriptionHTML(
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application through a helper tool"
					" called the \"wrapper\". This helper tool is not part of "
					SHORT_PROGRAM_NAME ". " SHORT_PROGRAM_NAME " expected"
					" the helper tool to report a socket to receive requests"
					" on, but the helper tool finished its startup procedure"
					" without reporting such a socket.</p>");
			} else {
				e.setProblemDescriptionHTML(
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application through a " SHORT_PROGRAM_NAME
					"-internal helper tool called the \"wrapper\","
					" but " SHORT_PROGRAM_NAME " encountered a bug"
					" in this helper tool. " SHORT_PROGRAM_NAME " expected"
					" the helper tool to report a socket to receive requests"
					" on, but the helper tool finished its startup procedure"
					" without reporting such a socket.</p>");
			}

			if (config->wrapperSuppliedByThirdParty) {
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"This is a bug in the wrapper, so please contact the author of"
					" the wrapper. This problem is outside " SHORT_PROGRAM_NAME
					"'s control. Below follows the command that "
					SHORT_PROGRAM_NAME " tried to execute, so that you can infer"
					" which wrapper was used:</p>"
					"<pre>" + escapeHTML(config->startCommand) + "</pre>");
			} else {
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"This is a bug in " SHORT_PROGRAM_NAME "."
					" <a href=\"" SUPPORT_URL "\">Please report this bug</a>"
					" to the " SHORT_PROGRAM_NAME " authors.</p>");
			}

			throw e.finalize();

		} else {
			UPDATE_TRACE_POINT();
			loadJourneyStateFromResponseDir();
			switch (session.journey.getType()) {
			case SPAWN_DIRECTLY:
			case START_PRELOADER:
				session.journey.setStepErrored(SUBPROCESS_APP_LOAD_OR_EXEC, true);
				break;
			case SPAWN_THROUGH_PRELOADER:
				session.journey.setStepErrored(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER, true);
				break;
			default:
				P_BUG("Unknown journey type " << (int) session.journey.getType());
			}

			SpawnException e(INTERNAL_ERROR, session.journey, config);
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			loadBasicInfoFromEnvDumpDir(e);
			loadAnnotationsFromEnvDumpDir(e);

			e.setSummary("Error spawning the web application: the application"
				" did not report any sockets to receive requests on.");
			e.setProblemDescriptionHTML(
				"<p>The " PROGRAM_NAME " application server tried"
				" to start the web application, but encountered a bug"
				" in the application. " SHORT_PROGRAM_NAME " expected"
				" the application to report a socket to receive requests"
				" on, but the application finished its startup procedure"
				" without reporting such a socket.</p>");
			e.setSolutionDescriptionHTML(
				"<p class=\"sole-solution\">"
				"Since this is a bug in the web application, please "
				"report this problem to the application's developer. "
				"This problem is outside " SHORT_PROGRAM_NAME "'s "
				"control.</p>");

			throw e.finalize();
		}
	}

	template<typename StringType>
	void throwSpawnExceptionBecauseOfResultValidationErrors(
		const vector<StringType> &internalFieldErrors,
		const vector<StringType> &appSuppliedFieldErrors)
	{
		TRACE_POINT();
		string message;
		typename vector<StringType>::const_iterator it, end;

		sleepShortlyToCaptureMoreStdoutStderr();

		if (!internalFieldErrors.empty()) {
			UPDATE_TRACE_POINT();
			loadJourneyStateFromResponseDir();
			session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM, true);

			SpawnException e(INTERNAL_ERROR, session.journey, config);
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			e.setAdvancedProblemDetails(toString(internalFieldErrors));
			loadBasicInfoFromEnvDumpDir(e);
			loadAnnotationsFromEnvDumpDir(e);

			e.setSummary("Error spawning the web application:"
				" a bug in " SHORT_PROGRAM_NAME " caused the"
				" spawn result to be invalid: "
				+ toString(internalFieldErrors));

			message = "<p>The " PROGRAM_NAME " application server tried"
				" to start the web application, but encountered a bug"
				" in " SHORT_PROGRAM_NAME " itself. The errors are as"
				" follows:</p>"
				"<ul>";
			end = internalFieldErrors.end();
			for (it = internalFieldErrors.begin(); it != end; it++) {
				message.append("<li>" + escapeHTML(*it) + "</li>");
			}
			message.append("</ul>");
			e.setProblemDescriptionHTML(message);

			e.setSolutionDescriptionHTML(
				"<p class=\"sole-solution\">"
				"This is a bug in " SHORT_PROGRAM_NAME "."
				" <a href=\"" SUPPORT_URL "\">Please report this bug</a>"
				" to the " SHORT_PROGRAM_NAME " authors.</p>");

			throw e.finalize();

		} else if (!config->genericApp && config->startsUsingWrapper) {
			UPDATE_TRACE_POINT();
			loadJourneyStateFromResponseDir();
			switch (session.journey.getType()) {
			case SPAWN_DIRECTLY:
			case START_PRELOADER:
				session.journey.setStepErrored(SUBPROCESS_WRAPPER_PREPARATION, true);
				break;
			case SPAWN_THROUGH_PRELOADER:
				session.journey.setStepErrored(SUBPROCESS_PREPARE_AFTER_FORKING_FROM_PRELOADER, true);
				break;
			default:
				P_BUG("Unknown journey type " << (int) session.journey.getType());
			}

			SpawnException e(INTERNAL_ERROR, session.journey, config);
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			e.setAdvancedProblemDetails(toString(appSuppliedFieldErrors));
			loadBasicInfoFromEnvDumpDir(e);
			loadAnnotationsFromEnvDumpDir(e);

			if (config->wrapperSuppliedByThirdParty) {
				e.setSummary("Error spawning the web application:"
					" a bug in a third-party application wrapper caused"
					" the spawn result to be invalid: "
					+ toString(appSuppliedFieldErrors));
			} else {
				e.setSummary("Error spawning the web application:"
					" a bug in a " SHORT_PROGRAM_NAME "-internal"
					" application wrapper caused the"
					" spawn result to be invalid: "
					+ toString(appSuppliedFieldErrors));
			}

			if (config->wrapperSuppliedByThirdParty) {
				message = "<p>The " PROGRAM_NAME " application server tried"
					" to start the web application through a helper tool"
					" called the \"wrapper\". This helper tool is not part of "
					SHORT_PROGRAM_NAME ". " SHORT_PROGRAM_NAME " expected"
					" the helper tool to communicate back various information"
					" about the application's startup procedure, but the tool"
					" did not communicate back correctly."
					" The errors are as follows:</p>"
					"<ul>";
			} else {
				message = "<p>The " PROGRAM_NAME " application server tried"
					" to start the web application through a " SHORT_PROGRAM_NAME
					"-internal helper tool (called the \"wrapper\"),"
					" but " SHORT_PROGRAM_NAME " encountered a bug"
					" in this helper tool. " SHORT_PROGRAM_NAME " expected"
					" the helper tool to communicate back various information"
					" about the application's startup procedure, but the tool"
					" did not communicate back correctly."
					" The errors are as follows:</p>"
					"<ul>";
			}
			end = appSuppliedFieldErrors.end();
			for (it = appSuppliedFieldErrors.begin(); it != end; it++) {
				message.append("<li>" + escapeHTML(*it) + "</li>");
			}
			message.append("</ul>");
			e.setProblemDescriptionHTML(message);

			if (config->wrapperSuppliedByThirdParty) {
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"This is a bug in the wrapper, so please contact the author of"
					" the wrapper. This problem is outside " SHORT_PROGRAM_NAME
					"'s control. Below follows the command that "
					SHORT_PROGRAM_NAME " tried to execute, so that you can infer"
					" which wrapper was used:</p>"
					"<pre>" + escapeHTML(config->startCommand) + "</pre>");
			} else {
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"This is a bug in " SHORT_PROGRAM_NAME "."
					" <a href=\"" SUPPORT_URL "\">Please report this bug</a>"
					" to the " SHORT_PROGRAM_NAME " authors.</p>");
			}

			throw e.finalize();

		} else {
			UPDATE_TRACE_POINT();
			loadJourneyStateFromResponseDir();
			session.journey.setStepErrored(SUBPROCESS_APP_LOAD_OR_EXEC, true);

			SpawnException e(INTERNAL_ERROR, session.journey, config);
			e.setSummary("Error spawning the web application:"
				" the application's spawn response is invalid: "
				+ toString(appSuppliedFieldErrors));
			e.setAdvancedProblemDetails(toString(appSuppliedFieldErrors));
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			loadBasicInfoFromEnvDumpDir(e);
			loadAnnotationsFromEnvDumpDir(e);

			message = "<p>The " PROGRAM_NAME " application server tried"
				" to start the web application, but encountered a bug"
				" in the application. " SHORT_PROGRAM_NAME " expected"
				" the application to communicate back various information"
				" about its startup procedure, but the application"
				" did not communicate back that correctly."
				" The errors are as follows:</p>"
				"<ul>";
			end = appSuppliedFieldErrors.end();
			for (it = appSuppliedFieldErrors.begin(); it != end; it++) {
				message.append("<li>" + escapeHTML(*it) + "</li>");
			}
			message.append("</ul>");
			e.setProblemDescriptionHTML(message);

			if (config->genericApp) {
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"Since this is a bug in the web application, please "
					"report this problem to the application's developer. "
					"This problem is outside " SHORT_PROGRAM_NAME "'s "
					"control.</p>");
			} else {
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"This is a bug in " SHORT_PROGRAM_NAME "."
					" <a href=\"" SUPPORT_URL "\">Please report this bug</a>"
					" to the " SHORT_PROGRAM_NAME " authors.</p>");
			}

			throw e.finalize();
		}
	}

	ErrorCategory inferErrorCategoryFromResponseDir(ErrorCategory defaultValue) const {
		TRACE_POINT();
		if (fileExists(session.responseDir + "/error/category")) {
			string value = strip(safeReadFile(session.responseErrorDirFd,
				"category", SPAWNINGKIT_MAX_ERROR_CATEGORY_SIZE).first);
			ErrorCategory category = stringToErrorCategory(value);

			if (category == UNKNOWN_ERROR_CATEGORY) {
				SpawnException e(INTERNAL_ERROR, session.journey, config);
				e.setStdoutAndErrData(getStdoutErrData());
				e.setSubprocessPid(pid);
				loadBasicInfoFromEnvDumpDir(e);
				loadAnnotationsFromEnvDumpDir(e);

				if (!config->genericApp && config->startsUsingWrapper) {
					if (config->wrapperSuppliedByThirdParty) {
						e.setSummary(
							"An error occurred while spawning an application process: "
							"the application wrapper (which is not part of "
							SHORT_PROGRAM_NAME
							") reported an invalid error category: "
							+ value);
					} else {
						e.setSummary(
							"An error occurred while spawning an application process: "
							"the application wrapper (which is internal to "
							SHORT_PROGRAM_NAME
							") reported an invalid error category: "
							+ value);
					}
				} else {
					e.setSummary(
						"An error occurred while spawning an application process: "
						"the application reported an invalid error category: "
						+ value);
				}

				if (!config->genericApp && config->startsUsingWrapper) {
					if (config->wrapperSuppliedByThirdParty) {
						e.setProblemDescriptionHTML(
							"<p>The " PROGRAM_NAME " application server tried"
							" to start the web application through a"
							" helper tool called the \"wrapper\". This helper tool "
							" is not part of " SHORT_PROGRAM_NAME ". The tool "
							" encountered an error, so " SHORT_PROGRAM_NAME
							" expected the tool to report details about that error."
							" But the tool communicated back in an invalid format:</p>"
							"<ul>"
							"<li>In file: " + escapeHTML(session.responseDir) + "/error/category</li>"
							"<li>Content: <code>" + escapeHTML(value) + "</code></li>"
							"</ul>");
						e.setSolutionDescriptionHTML(
							"<p class=\"sole-solution\">"
							"This is a bug in the wrapper, so please contact the author of"
							" the wrapper. This problem is outside " SHORT_PROGRAM_NAME
							"'s control. Below follows the command that "
							SHORT_PROGRAM_NAME " tried to execute, so that you can infer"
							" which wrapper was used:</p>"
							"<pre>" + escapeHTML(config->startCommand) + "</pre>");
					} else {
						e.setProblemDescriptionHTML(
							"<p>The " PROGRAM_NAME " application server tried"
							" to start the web application through a " SHORT_PROGRAM_NAME
							"-internal helper tool called the \"wrapper\"."
							" The tool encountered an error, so "
							SHORT_PROGRAM_NAME " expected the tool to report"
							" details about that error. But the tool communicated back"
							" in an invalid format:</p>"
							"<ul>"
							"<li>In file: " + escapeHTML(session.responseDir) + "/error/category</li>"
							"<li>Content: <code>" + escapeHTML(value) + "</code></li>"
							"</ul>");
						e.setSolutionDescriptionHTML(
							"<p class=\"sole-solution\">"
							"This is a bug in " SHORT_PROGRAM_NAME "."
							" <a href=\"" SUPPORT_URL "\">Please report this bug</a>"
							" to the " SHORT_PROGRAM_NAME " authors.</p>");
					}
				} else {
					e.setProblemDescriptionHTML(
						"<p>The " PROGRAM_NAME " application server tried"
						" to start the web application. The application encountered "
						" an error and tried to report details about the error back to "
						SHORT_PROGRAM_NAME ". But the application communicated back"
						" in an invalid format:</p>"
						"<ul>"
						"<li>In file: " + escapeHTML(session.responseDir) + "/error/category</li>"
							"<li>Content: <code>" + escapeHTML(value) + "</code></li>"
						"</ul>");
					e.setSolutionDescriptionHTML(
						"<p class=\"sole-solution\">"
						"This is a bug in the web application, please "
						"report this problem to the application's developer. "
						"This problem is outside " SHORT_PROGRAM_NAME "'s "
						"control.</p>");
				}

				throw e.finalize();
			} else {
				return category;
			}
		} else {
			return defaultValue;
		}
	}

	void loadJourneyStateFromResponseDir() {
		loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer);
	}

	static void loadJourneyStateFromResponseDir(HandshakeSession &session, pid_t pid,
		const BackgroundIOCapturerPtr &stdoutAndErrCapturer,
		JourneyStep firstStep, JourneyStep lastStep)
	{
		TRACE_POINT();
		JourneyStep step;

		for (step = firstStep; step < lastStep; step = JourneyStep((int) step + 1)) {
			if (!session.journey.hasStep(step)) {
				continue;
			}

			string stepString = journeyStepToStringLowerCase(step);
			string stepDir = session.responseDir + "/steps/" + stepString;
			if (!fileExists(stepDir + "/state")) {
				P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
					<< ": state file does not exist");
				continue;
			}

			map<JourneyStep, int>::const_iterator it = session.stepDirFds.find(step);
			if (it == session.stepDirFds.end()) {
				P_BUG("No fd opened for step " << stepString);
			}

			loadJourneyStateFromResponseDirForSpecificStep(
				session, pid, stdoutAndErrCapturer, step, stepDir, it->second);
		}
	}

	static void loadJourneyStateFromResponseDirForSpecificStep(HandshakeSession &session,
		pid_t pid, const BackgroundIOCapturerPtr &stdoutAndErrCapturer,
		JourneyStep step, const string &stepDir, int stepDirFd)
	{
		TRACE_POINT_WITH_DATA(journeyStepToString(step).data());
		string summary;
		string value = strip(safeReadFile(stepDirFd, "state",
			SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first);
		JourneyStepState state = stringToJourneyStepState(value);
		const Config *config = session.config;

		if (value.empty()) {
			P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
				<< ": state file is empty");
			return;
		}

		P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
			<< ": setting state to " << value);

		try {
			UPDATE_TRACE_POINT();
			switch (state) {
			case STEP_NOT_STARTED:
				// SpawnEnvSetupper explicitly sets the SUBPROCESS_OS_SHELL
				// step state to STEP_NOT_STARTED if it determines that it
				// should not execute the next command through the shell.
				session.journey.setStepNotStarted(step, true);
				break;
			case STEP_IN_PROGRESS:
				session.journey.setStepInProgress(step, true);
				break;
			case STEP_PERFORMED:
				session.journey.setStepPerformed(step, true);
				break;
			case STEP_ERRORED:
				session.journey.setStepErrored(step, true);
				break;
			default:
				session.journey.setStepErrored(step, true);

				SpawnException e(INTERNAL_ERROR, session.journey, config);
				e.setStdoutAndErrData(getStdoutErrData(stdoutAndErrCapturer));
				e.setSubprocessPid(pid);
				loadBasicInfoFromEnvDumpDir(e, session);
				loadAnnotationsFromEnvDumpDir(e, session);

				if (!config->genericApp && config->startsUsingWrapper) {
					if (config->wrapperSuppliedByThirdParty) {
						e.setSummary(
							"An error occurred while spawning an application process: "
							"the application wrapper (which is not part of " SHORT_PROGRAM_NAME
							") reported an invalid progress step state for step "
							+ journeyStepToString(step) + ": " + value);
					} else {
						e.setSummary(
							"An error occurred while spawning an application process: "
							"the application wrapper (which is internal to " SHORT_PROGRAM_NAME
							") reported an invalid progress step state for step "
							+ journeyStepToString(step) + ": " + value);
					}
				} else {
					e.setSummary(
						"An error occurred while spawning an application process: "
						"the application reported an invalid progress step state for step "
						+ journeyStepToString(step) + ": " + value);
				}

				if (!config->genericApp && config->startsUsingWrapper) {
					if (config->wrapperSuppliedByThirdParty) {
						e.setProblemDescriptionHTML(
							"<p>The " PROGRAM_NAME " application server tried"
							" to start the web application through a"
							" helper tool called the \"wrapper\". This helper tool"
							" is not part of " SHORT_PROGRAM_NAME ". "
							SHORT_PROGRAM_NAME " expected the helper tool to"
							" report about its startup progress, but the tool"
							" communicated back an invalid answer:</p>"
							"<ul>"
							"<li>In file: " + escapeHTML(stepDir) + "/state</li>"
							"<li>Content: <code>" + escapeHTML(value) + "</code></li>"
							"</ul>");
						e.setSolutionDescriptionHTML(
							"<p class=\"sole-solution\">"
							"This is a bug in the wrapper, so please contact the author of"
							" the wrapper. This problem is outside " SHORT_PROGRAM_NAME
							"'s control. Below follows the command that "
							SHORT_PROGRAM_NAME " tried to execute, so that you can infer"
							" which wrapper was used:</p>"
							"<pre>" + escapeHTML(config->startCommand) + "</pre>");
					} else {
						e.setProblemDescriptionHTML(
							"<p>The " PROGRAM_NAME " application server tried"
							" to start the web application through a " SHORT_PROGRAM_NAME
							"-internal helper tool called the \"wrapper\","
							" but " SHORT_PROGRAM_NAME " encountered a bug"
							" in this helper tool. " SHORT_PROGRAM_NAME " expected"
							" the helper tool to report about its startup progress,"
							" but the tool communicated back an invalid answer:</p>"
							"<ul>"
							"<li>In file: " + escapeHTML(stepDir) + "/state</li>"
							"<li>Content: <code>" + escapeHTML(value) + "</code></li>"
							"</ul>");
						e.setSolutionDescriptionHTML(
							"<p class=\"sole-solution\">"
							"This is a bug in " SHORT_PROGRAM_NAME "."
							" <a href=\"" SUPPORT_URL "\">Please report this bug</a>"
							" to the " SHORT_PROGRAM_NAME " authors.</p>");
					}
				} else {
					e.setProblemDescriptionHTML(
						"<p>The " PROGRAM_NAME " application server tried"
						" to start the web application, and expected the application"
						" to report about its startup progress. But the application"
						" communicated back an invalid answer:</p>"
						"<ul>"
						"<li>In file: " + escapeHTML(stepDir) + "/state</li>"
						"<li>Content: <code>" + escapeHTML(value) + "</code></li>"
						"</ul>");
					e.setSolutionDescriptionHTML(
						"<p class=\"sole-solution\">"
						"This is a bug in the web application, please "
						"report this problem to the application's developer. "
						"This problem is outside " SHORT_PROGRAM_NAME "'s "
						"control.</p>");
				}

				throw e.finalize();
				break;
			};
		} catch (const RuntimeException &originalException) {
			UPDATE_TRACE_POINT();
			session.journey.setStepErrored(step, true);

			SpawnException e(INTERNAL_ERROR, session.journey, config);
			e.setStdoutAndErrData(getStdoutErrData(stdoutAndErrCapturer));
			e.setSubprocessPid(pid);
			loadBasicInfoFromEnvDumpDir(e, session);
			loadAnnotationsFromEnvDumpDir(e, session);

			if (!config->genericApp && config->startsUsingWrapper) {
				if (config->wrapperSuppliedByThirdParty) {
					e.setSummary("An error occurred while spawning an application process: "
						"the application wrapper (which is not part of " SHORT_PROGRAM_NAME
						") reported an invalid progress step state for step "
						+ journeyStepToString(step) + ": "
						+ StaticString(originalException.what()));
				} else {
					e.setSummary("An error occurred while spawning an application process: "
						"the application wrapper (which is internal to " SHORT_PROGRAM_NAME
						") reported an invalid progress step state for step "
						+ journeyStepToString(step) + ": "
						+ StaticString(originalException.what()));
				}
			} else {
				e.setSummary("An error occurred while spawning an application process: "
					"the application reported an invalid progress step state for step "
					+ journeyStepToString(step) + ": "
					+ StaticString(originalException.what()));
			}

			if (!config->genericApp && config->startsUsingWrapper) {
				if (config->wrapperSuppliedByThirdParty) {
					e.setProblemDescriptionHTML(
						"<p>The " PROGRAM_NAME " application server tried"
						" to start the web application through a "
						" helper tool called the \"wrapper\". This helper tool"
						" is not part of " SHORT_PROGRAM_NAME ". "
						SHORT_PROGRAM_NAME " expected the helper tool to"
						" report about its startup progress, but the tool"
						" communicated back an invalid answer:</p>"
						"<ul>"
						"<li>In file: " + escapeHTML(stepDir) + "/state</li>"
						"<li>Error: " + escapeHTML(originalException.what()) + "</li>"
						"</ul>");
					e.setSolutionDescriptionHTML(
						"<p class=\"sole-solution\">"
						"This is a bug in the wrapper, so please contact the author of"
						" the wrapper. This problem is outside " SHORT_PROGRAM_NAME
						"'s control. Below follows the command that "
						SHORT_PROGRAM_NAME " tried to execute, so that you can infer"
						" which wrapper was used:</p>"
						"<pre>" + escapeHTML(config->startCommand) + "</pre>");
				} else {
					e.setProblemDescriptionHTML(
						"<p>The " PROGRAM_NAME " application server tried"
						" to start the web application through a " SHORT_PROGRAM_NAME
						"-internal helper tool called the \"wrapper\","
						" but " SHORT_PROGRAM_NAME " encountered a bug"
						" in this helper tool. " SHORT_PROGRAM_NAME " expected"
						" the helper tool to report about its startup progress,"
						" but the tool communicated back an invalid answer:</p>"
						"<ul>"
						"<li>In file: " + escapeHTML(stepDir) + "/state</li>"
						"<li>Error: " + escapeHTML(originalException.what()) + "</li>"
						"</ul>");
					e.setSolutionDescriptionHTML(
						"<p class=\"sole-solution\">"
						"This is a bug in " SHORT_PROGRAM_NAME "."
						" <a href=\"" SUPPORT_URL "\">Please report this bug</a>"
						" to the " SHORT_PROGRAM_NAME " authors.</p>");
				}
			} else {
				e.setProblemDescriptionHTML(
					"<p>The " PROGRAM_NAME " application server tried"
					" to start the web application, and expected the application"
					" to report about its startup progress. But the application"
					" communicated back an invalid answer:</p>"
					"<ul>"
					"<li>In file: " + escapeHTML(stepDir) + "/state</li>"
					"<li>Error: " + escapeHTML(originalException.what()) + "</li>"
					"</ul>");
				e.setSolutionDescriptionHTML(
					"<p class=\"sole-solution\">"
					"This is a bug in the web application, please "
					"report this problem to the application's developer. "
					"This problem is outside " SHORT_PROGRAM_NAME "'s "
					"control.</p>");
			}

			throw e.finalize();
		}

		UPDATE_TRACE_POINT();
		if (fileExists(stepDir + "/begin_time_monotonic")) {
			value = safeReadFile(stepDirFd, "begin_time_monotonic",
				SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first;
			MonotonicTimeUsec beginTimeMonotonic = llround(atof(value.c_str()) * 1000000);
			P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
				<< ": monotonic begin time is \"" << cEscapeString(value) << "\"");
			session.journey.setStepBeginTime(step, beginTimeMonotonic);
		} else if (fileExists(stepDir + "/begin_time")) {
			value = safeReadFile(stepDirFd, "begin_time",
				SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first;
			unsigned long long beginTime = llround(atof(value.c_str()) * 1000000);
			MonotonicTimeUsec beginTimeMonotonic = usecTimestampToMonoTime(beginTime);
			P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
				<< ": begin time is \"" << cEscapeString(value) << "\", monotonic conversion is "
				<< doubleToString(beginTimeMonotonic / 1000000.0));
			session.journey.setStepBeginTime(step, beginTimeMonotonic);
		} else {
			P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
				<< ": no begin time known");
		}

		UPDATE_TRACE_POINT();
		if (fileExists(stepDir + "/end_time_monotonic")) {
			value = safeReadFile(stepDirFd, "end_time_monotonic",
				SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first;
			MonotonicTimeUsec endTimeMonotonic = llround(atof(value.c_str()) * 1000000);
			P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
				<< ": monotonic end time is \"" << cEscapeString(value) << "\"");
			session.journey.setStepEndTime(step, endTimeMonotonic);
		} else if (fileExists(stepDir + "/end_time")) {
			value = safeReadFile(stepDirFd, "end_time",
				SPAWNINGKIT_MAX_JOURNEY_STEP_FILE_SIZE).first;
			unsigned long long endTime = llround(atof(value.c_str()) * 1000000);
			MonotonicTimeUsec endTimeMonotonic = usecTimestampToMonoTime(endTime);
			P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
				<< ": end time is \"" << cEscapeString(value) << "\", monotonic conversion is "
				<< doubleToString(endTimeMonotonic / 1000000.0));
			session.journey.setStepEndTime(step, endTimeMonotonic);
		} else {
			P_DEBUG("[App " << pid << " journey] Step " << journeyStepToString(step)
				<< ": no end time known");
		}
	}

	static MonotonicTimeUsec usecTimestampToMonoTime(unsigned long long timestamp) {
		unsigned long long now = SystemTime::getUsec();
		MonotonicTimeUsec nowMono = SystemTime::getMonotonicUsec();
		unsigned long long diff;

		if (now > nowMono) {
			diff = now - nowMono;
			return timestamp - diff;
		} else {
			diff = nowMono - now;
			return timestamp + diff;
		}
	}

	void loadSubprocessErrorMessagesAndEnvDump(SpawnException &e) const {
		TRACE_POINT();
		const string &responseDir = session.responseDir;

		if (fileExists(responseDir + "/error/summary")) {
			e.setSummary(strip(safeReadFile(session.responseErrorDirFd, "summary",
				SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first));
		}

		if (e.getAdvancedProblemDetails().empty()
		 && fileExists(responseDir + "/error/advanced_problem_details"))
		{
			e.setAdvancedProblemDetails(strip(safeReadFile(session.responseErrorDirFd,
				"advanced_problem_details", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first));
		}

		if (fileExists(responseDir + "/error/problem_description.html")) {
			e.setProblemDescriptionHTML(safeReadFile(session.responseErrorDirFd,
				"problem_description.html", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first);
		} else if (fileExists(responseDir + "/error/problem_description.txt")) {
			e.setProblemDescriptionHTML(escapeHTML(strip(safeReadFile(session.responseErrorDirFd,
				"problem_description.txt", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first)));
		}

		if (fileExists(responseDir + "/error/solution_description.html")) {
			e.setSolutionDescriptionHTML(safeReadFile(session.responseErrorDirFd,
				"solution_description.html", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first);
		} else if (fileExists(responseDir + "/error/solution_description.txt")) {
			e.setSolutionDescriptionHTML(escapeHTML(strip(safeReadFile(session.responseErrorDirFd,
				"solution_description.txt", SPAWNINGKIT_MAX_SUBPROCESS_ERROR_MESSAGE_SIZE).first)));
		}

		loadBasicInfoFromEnvDumpDir(e);
		loadAnnotationsFromEnvDumpDir(e);
	}

	void loadBasicInfoFromEnvDumpDir(SpawnException &e) const {
		loadBasicInfoFromEnvDumpDir(e, session);
	}

	static void loadBasicInfoFromEnvDumpDir(SpawnException &e, HandshakeSession &session) {
		string envvars, userInfo, ulimits;

		loadBasicInfoFromEnvDumpDir(session.envDumpDir, session.envDumpDirFd,
			envvars, userInfo, ulimits);
		e.setSubprocessEnvvars(envvars);
		e.setSubprocessUserInfo(userInfo);
		e.setSubprocessUlimits(ulimits);
	}

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

	void loadAnnotationsFromEnvDumpDir(SpawnException &e) const {
		loadAnnotationsFromEnvDumpDir(e, session);
	}

	static void loadAnnotationsFromEnvDumpDir(SpawnException &e, HandshakeSession &session) {
		TRACE_POINT();
		string path = session.envDumpDir + "/annotations";
		DIR *dir = opendir(path.c_str());
		if (dir == NULL) {
			return;
		}

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

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

		if (processExitWatcher != NULL) {
			processExitWatcher->interrupt_and_join();
			delete processExitWatcher;
			processExitWatcher = NULL;
		}
		if (finishSignalWatcher != NULL) {
			finishSignalWatcher->interrupt_and_join();
			delete finishSignalWatcher;
			finishSignalWatcher = NULL;
		}
		if (socketPingabilityWatcher != NULL) {
			socketPingabilityWatcher->interrupt_and_join();
			delete socketPingabilityWatcher;
			socketPingabilityWatcher = NULL;
		}
		if (stdoutAndErrCapturer != NULL) {
			stdoutAndErrCapturer->stop();
		}
	}

	JourneyStep bestGuessSubprocessFailedStep() const {
		JourneyStep step = getFirstSubprocessJourneyStepWithState(STEP_IN_PROGRESS);
		if (step != UNKNOWN_JOURNEY_STEP) {
			return step;
		}

		if (allSubprocessJourneyStepsHaveState(STEP_PERFORMED)) {
			return getLastSubprocessJourneyStepFrom(session.journey);
		} else {
			JourneyStep step = getLastSubprocessJourneyStepWithState(STEP_PERFORMED);
			if (step == UNKNOWN_JOURNEY_STEP) {
				return getFirstSubprocessJourneyStepFrom(session.journey);
			} else {
				assert(step != getLastSubprocessJourneyStepFrom(session.journey));
				return JourneyStep((int) step + 1);
			}
		}
	}

	JourneyStep getFirstSubprocessJourneyStepFrom(const Journey &journey) const {
		JourneyStep firstStep = getFirstSubprocessJourneyStep();
		JourneyStep lastStep = getLastSubprocessJourneyStep();
		JourneyStep step;

		for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) {
			if (session.journey.hasStep(step)) {
				return step;
			}
		}

		P_BUG("Never reached");
		return UNKNOWN_JOURNEY_STEP;
	}

	JourneyStep getLastSubprocessJourneyStepFrom(const Journey &journey) const {
		JourneyStep firstStep = getFirstSubprocessJourneyStep();
		JourneyStep lastStep = getLastSubprocessJourneyStep();
		JourneyStep result = UNKNOWN_JOURNEY_STEP;
		JourneyStep step;

		for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) {
			if (session.journey.hasStep(step)) {
				result = step;
			}
		}

		return result;
	}

	bool allSubprocessJourneyStepsHaveState(JourneyStepState state) const {
		JourneyStep firstStep = getFirstSubprocessJourneyStep();
		JourneyStep lastStep = getLastSubprocessJourneyStep();
		JourneyStep step;

		for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) {
			if (!session.journey.hasStep(step)) {
				continue;
			}

			if (session.journey.getStepInfo(step).state != state) {
				return false;
			}
		}

		return true;
	}

	JourneyStep getFirstSubprocessJourneyStepWithState(JourneyStepState state) const {
		JourneyStep firstStep = getFirstSubprocessJourneyStep();
		JourneyStep lastStep = getLastSubprocessJourneyStep();
		JourneyStep step;

		for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) {
			if (!session.journey.hasStep(step)) {
				continue;
			}

			if (session.journey.getStepInfo(step).state == state) {
				return step;
			}
		}

		return UNKNOWN_JOURNEY_STEP;
	}

	JourneyStep getLastSubprocessJourneyStepWithState(JourneyStepState state) const {
		JourneyStep firstStep = getFirstSubprocessJourneyStep();
		JourneyStep lastStep = getLastSubprocessJourneyStep();
		JourneyStep step;
		JourneyStep result = UNKNOWN_JOURNEY_STEP;

		for (step = firstStep; step <= lastStep; step = JourneyStep((int) step + 1)) {
			if (!session.journey.hasStep(step)) {
				continue;
			}

			if (session.journey.getStepInfo(step).state == state) {
				result = step;
			}
		}

		return result;
	}

	void setResultType(Result &result) const {
		if (config->genericApp) {
			result.type = Result::GENERIC;
		} else if (config->startsUsingWrapper) {
			result.type = Result::AUTO_SUPPORTED;
		} else {
			result.type = Result::KURIA;
		}
	}

public:
	struct DebugSupport {
		virtual ~DebugSupport() { }
		virtual void beginWaitUntilSpawningFinished() { }
	};

	DebugSupport *debugSupport;


	HandshakePerform(HandshakeSession &_session, pid_t _pid,
		const FileDescriptor &_stdinFd = FileDescriptor(),
		const FileDescriptor &_stdoutAndErrFd = FileDescriptor(),
		const string &_alreadyReadStdoutAndErrData = string())
		: session(_session),
		  config(session.config),
		  pid(_pid),
		  stdinFd(_stdinFd),
		  stdoutAndErrFd(_stdoutAndErrFd),
		  alreadyReadStdoutAndErrData(_alreadyReadStdoutAndErrData),
		  processExitWatcher(NULL),
		  finishSignalWatcher(NULL),
		  processExited(false),
		  finishState(NOT_FINISHED),
		  socketPingabilityWatcher(NULL),
		  socketIsNowPingable(false),
		  debugSupport(NULL)
	{
		assert(_session.context != NULL);
		assert(_session.context->isFinalized());
		assert(_session.config != NULL);
	}

	Result execute() {
		TRACE_POINT();
		ScopeGuard guard(boost::bind(&HandshakePerform::cleanup, this));

		// We do not set SPAWNING_KIT_HANDSHAKE_PERFORM to the IN_PROGRESS or
		// PERFORMED state here. That will be done by the caller because
		// it may want to perform additional preparation.

		try {
			initializeStdchannelsCapturing();
			startWatchingProcessExit();
			if (config->genericApp || config->findFreePort) {
				startWatchingSocketPingability();
			}
			if (!config->genericApp) {
				startWatchingFinishSignal();
			}
		} catch (const SpawnException &) {
			throw;
		} catch (const std::exception &originalException) {
			sleepShortlyToCaptureMoreStdoutStderr();

			loadJourneyStateFromResponseDir();
			session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM);

			SpawnException e(originalException, session.journey, config);
			e.setStdoutAndErrData(getStdoutErrData());
			e.setSubprocessPid(pid);
			throw e.finalize();
		}

		UPDATE_TRACE_POINT();
		try {
			boost::unique_lock<boost::mutex> l(syncher);
			if (debugSupport != NULL) {
				debugSupport->beginWaitUntilSpawningFinished();
			}
			waitUntilSpawningFinished(l);
			Result result = handleResponse();
			loadJourneyStateFromResponseDir();
			return result;
		} catch (const SpawnException &) {
			throw;
		} catch (const std::exception &originalException) {
			sleepShortlyToCaptureMoreStdoutStderr();

			loadJourneyStateFromResponseDir();
			session.journey.setStepErrored(SPAWNING_KIT_HANDSHAKE_PERFORM);

			SpawnException e(originalException, session.journey, config);
			e.setSubprocessPid(pid);
			e.setStdoutAndErrData(getStdoutErrData());
			throw e.finalize();
		}
	}

	static void loadJourneyStateFromResponseDir(HandshakeSession &session, pid_t pid,
		const BackgroundIOCapturerPtr &stdoutAndErrCapturer)
	{
		TRACE_POINT();

		P_DEBUG("[App " << pid << " journey] Loading state from " << session.responseDir);

		loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer,
			getFirstSubprocessJourneyStep(),
			getLastSubprocessJourneyStep());

		UPDATE_TRACE_POINT();
		loadJourneyStateFromResponseDir(session, pid, stdoutAndErrCapturer,
			getFirstPreloaderJourneyStep(),
			// Also load state from PRELOADER_FINISH since the
			// preloader writes there.
			JourneyStep((int) getLastPreloaderJourneyStep() + 1));
	}

	static void loadBasicInfoFromEnvDumpDir(const string &envDumpDir,
		int envDumpDirFd, string &envvars, string &userInfo, string &ulimits)
	{
		if (fileExists(envDumpDir + "/envvars")) {
			envvars = safeReadFile(envDumpDirFd, "envvars",
				SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first;
		}
		if (fileExists(envDumpDir + "/user_info")) {
			userInfo = safeReadFile(envDumpDirFd, "user_info",
				SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first;
		}
		if (fileExists(envDumpDir + "/ulimits")) {
			ulimits = safeReadFile(envDumpDirFd, "ulimits",
				SPAWNINGKIT_MAX_SUBPROCESS_ENVDUMP_SIZE).first;
		}
	}
};


} // namespace SpawningKit
} // namespace Passenger

#endif /* _PASSENGER_SPAWNING_KIT_HANDSHAKE_PERFORM_H_ */
