/*
 *  Phusion Passenger - https://www.phusionpassenger.com/
 *  Copyright (c) 2010-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.
 */

#include <ios>
#include <algorithm>
#include <stdexcept>
#include <cstdio>
#include <cstddef>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <queue>
#include <sys/time.h>
#include <fcntl.h>
#include <utility>
#include <unistd.h>
#include <time.h>
#include <pthread.h>

#include <boost/cstdint.hpp>
#include <boost/circular_buffer.hpp>
#include <boost/foreach.hpp>
#include <oxt/thread.hpp>
#include <oxt/detail/context.hpp>

#include <Constants.h>
#include <StaticString.h>
#include <Exceptions.h>
#include <LoggingKit/Logging.h>
#include <LoggingKit/Assert.h>
#include <LoggingKit/Config.h>
#include <LoggingKit/Context.h>
#include <ConfigKit/ConfigKit.h>
#include <FileTools/PathManip.h>
#include <Utils.h>
#include <StrIntTools/StrIntUtils.h>
#include <SystemTools/SystemTime.h>

namespace Passenger {
namespace LoggingKit {

using namespace std;


#define TRUNCATE_LOGPATHS_TO_MAXCHARS 3 // set to 0 to disable truncation

Context *context = NULL;
AssertionFailureInfo lastAssertionFailure;

void
initialize(const Json::Value &initialConfig, const ConfigKit::Translator &translator) {
	context = new Context(initialConfig, translator);
}

void
shutdown() {
	delete context;
	context = NULL;
}

Level getLevel() {
	if (OXT_LIKELY(context != NULL)) {
		return context->getConfigRealization()->level;
	} else {
		return Level(DEFAULT_LOG_LEVEL);
	}
}

void
setLevel(Level level) {
	Json::Value config;
	vector<ConfigKit::Error> errors;
	ConfigChangeRequest req;

	config["level"] = levelToString(level).toString();
	if (context->prepareConfigChange(config, errors, req)) {
		context->commitConfigChange(req);
	} else {
		P_BUG("Error setting log level: " << ConfigKit::toString(errors));
	}
}

Level
parseLevel(const StaticString &name) {
	if (name == "crit" || name == "0") {
		return CRIT;
	} else if (name == "error" || name == "1") {
		return ERROR;
	} else if (name == "warn" || name == "2") {
		return WARN;
	} else if (name == "notice" || name == "3") {
		return NOTICE;
	} else if (name == "info" || name == "4") {
		return INFO;
	} else if (name == "debug" || name == "5") {
		return DEBUG;
	} else if (name == "debug2" || name == "6") {
		return DEBUG2;
	} else if (name == "debug3" || name == "7") {
		return DEBUG3;
	} else {
		return UNKNOWN_LEVEL;
	}
}

StaticString
levelToString(Level level) {
	switch (level) {
	case CRIT:
		return P_STATIC_STRING("crit");
	case ERROR:
		return P_STATIC_STRING("error");
	case WARN:
		return P_STATIC_STRING("warn");
	case NOTICE:
		return P_STATIC_STRING("notice");
	case INFO:
		return P_STATIC_STRING("info");
	case DEBUG:
		return P_STATIC_STRING("debug");
	case DEBUG2:
		return P_STATIC_STRING("debug2");
	case DEBUG3:
		return P_STATIC_STRING("debug3");
	default:
		return P_STATIC_STRING("unknown");
	}
}


const char *
_strdupFastStringStream(const FastStringStream<> &stream) {
	char *buf = (char *) malloc(stream.size() + 1);
	memcpy(buf, stream.data(), stream.size());
	buf[stream.size()] = '\0';
	return buf;
}

bool
_passesLogLevel(const Context *context, Level level, const ConfigRealization **outputConfigRlz) {
	if (OXT_UNLIKELY(context == NULL)) {
		*outputConfigRlz = NULL;
		return Level(DEFAULT_LOG_LEVEL) >= level;
	} else {
		const ConfigRealization *configRlz = context->getConfigRealization();
		*outputConfigRlz = configRlz;
		return configRlz->level >= level;
	}
}

bool
_shouldLogFileDescriptors(const Context *context, const ConfigRealization **outputConfigRlz) {
	if (OXT_UNLIKELY(context == NULL)) {
		return false;
	} else {
		const ConfigRealization *configRlz = context->getConfigRealization();
		*outputConfigRlz = configRlz;
		return configRlz->fileDescriptorLogTargetType != NO_TARGET;
	}
}

void
_prepareLogEntry(FastStringStream<> &sstream, Level level, const char *file, unsigned int line) {
	struct tm the_tm;
	char datetime_buf[32];
	char threadIdBuf[std::max<unsigned int>(
		std::max<unsigned int>(
			2 * sizeof(boost::uintptr_t) + 1,
			2 * sizeof(unsigned int) + 1
		),
		32
	)];
	int datetime_size;
	unsigned int threadIdSize;
	struct timeval tv;
	StaticString logLevelMarkers[] = {
		P_STATIC_STRING("C"),
		P_STATIC_STRING("E"),
		P_STATIC_STRING("W"),
		P_STATIC_STRING("N"),
		P_STATIC_STRING("I"),
		P_STATIC_STRING("D"),
		P_STATIC_STRING("D2"),
		P_STATIC_STRING("D3")
	};

	gettimeofday(&tv, NULL);
	localtime_r(&tv.tv_sec, &the_tm);
	datetime_size = snprintf(datetime_buf, sizeof(datetime_buf),
		"%d-%02d-%02d %02d:%02d:%02d.%04llu",
		the_tm.tm_year + 1900, the_tm.tm_mon + 1, the_tm.tm_mday,
		the_tm.tm_hour, the_tm.tm_min, the_tm.tm_sec,
		(unsigned long long) tv.tv_usec / 100);

	#ifdef OXT_THREAD_LOCAL_KEYWORD_SUPPORTED
		// We only use oxt::get_thread_local_context() if it is fast enough.
		oxt::thread_local_context *ctx = oxt::get_thread_local_context();
		if (OXT_LIKELY(ctx != NULL)) {
			threadIdSize = integerToHexatri(ctx->thread_number,
				threadIdBuf);
		} else {
			threadIdSize = integerToHexatri((boost::uintptr_t) pthread_self(),
				threadIdBuf);
		}
	#else
		threadIdSize = integerToHexatri((boost::uintptr_t) pthread_self(),
			threadIdBuf);
	#endif

	sstream <<
		P_STATIC_STRING("[ ") <<
		logLevelMarkers[int(level)] <<
		P_STATIC_STRING(" ") <<
		StaticString(datetime_buf, datetime_size) <<
		P_STATIC_STRING(" ") <<
		std::dec << getpid() <<
		P_STATIC_STRING("/T") <<
		StaticString(threadIdBuf, threadIdSize) <<
		P_STATIC_STRING(" ");

	if (startsWith(file, P_STATIC_STRING("src/"))) { // special reduncancy filter because most code resides in these paths
		file += sizeof("src/") - 1;
		if (startsWith(file, P_STATIC_STRING("cxx_supportlib/"))) {
			file += sizeof("cxx_supportlib/") - 1;
		}
	}

	if (TRUNCATE_LOGPATHS_TO_MAXCHARS > 0) {
		truncateBeforeTokens(file, P_STATIC_STRING("/\\"), TRUNCATE_LOGPATHS_TO_MAXCHARS, sstream);
	} else {
		sstream << file;
	}

	sstream << P_STATIC_STRING(":") <<
		line << P_STATIC_STRING(" ]: ");
}

static void
writeExactWithoutOXT(int fd, const char *str, unsigned int size) {
	/* We do not use writeExact() here because writeExact()
	 * uses oxt::syscalls::write(), which is an interruption point and
	 * which is slightly more expensive than a plain write().
	 * Logging may block, but in most cases not indefinitely,
	 * so we don't care if the write() here is not an interruption
	 * point. If the write does block indefinitely then it's
	 * probably a FIFO that is not opened on the other side.
	 * In that case we can blame the user.
	 */
	ssize_t ret;
	unsigned int written = 0;
	while (written < size) {
		do {
			ret = write(fd, str + written, size - written);
		} while (ret == -1 && errno == EINTR);
		if (ret == -1) {
			/* The most likely reason why this fails is when the user has setup
			 * Apache to log to a pipe (e.g. to a log rotation script). Upon
			 * restarting the web server, the process that reads from the pipe
			 * shuts down, so we can't write to it anymore. That's why we
			 * just ignore write errors. It doesn't make sense to abort for
			 * something like this.
			 */
			break;
		} else {
			written += ret;
		}
	}
}

void
_writeLogEntry(const ConfigRealization *configRealization, const char *str, unsigned int size) {
	if (OXT_LIKELY(configRealization != NULL)) {
		writeExactWithoutOXT(configRealization->targetFd, str, size);
	} else {
		writeExactWithoutOXT(STDERR_FILENO, str, size);
	}
}

void
_writeFileDescriptorLogEntry(const ConfigRealization *configRealization,
	const char *str, unsigned int size)
{
	assert(configRealization != NULL);
	assert(configRealization->fileDescriptorLogTargetType != UNKNOWN_TARGET);
	assert(configRealization->fileDescriptorLogTargetFd != -1);
	writeExactWithoutOXT(configRealization->fileDescriptorLogTargetFd, str, size);
}

void
Context::saveNewLog(const HashedStaticString &groupName, const char *sourceStr, unsigned int sourceStrLen, const char *message, unsigned int messageLen) {
	boost::lock_guard<boost::mutex> l(syncher); //lock

	unsigned long long timestamp = SystemTime::getUsec();

	LogStore::Cell *c = logStore.lookupCell(groupName);
	if (c == NULL) {
		AppGroupLog appGroupLog;
		appGroupLog.pidLog = TimestampedLogBuffer(LOG_MONITORING_MAX_LINES * 5);
		c = logStore.insert(groupName, appGroupLog);
	}
	AppGroupLog &rec = c->value;

	TimestampedLog ll;
	ll.timestamp = timestamp;
	ll.sourceId = string(sourceStr, sourceStrLen);
	ll.lineText = string(message, messageLen);
	rec.pidLog.push_back(ll);
	//unlock
}

void
Context::saveMonitoredFileLog(const HashedStaticString &groupName,
	const char *sourceStr, unsigned int sourceStrLen,
	const char *content, unsigned int contentLen)
{
	vector<StaticString> lines;
	split(StaticString(content, contentLen), '\n', lines);

	boost::lock_guard<boost::mutex> l(syncher); //lock

	LogStore::Cell *c = logStore.lookupCell(groupName);
	if (c == NULL) {
		AppGroupLog appGroupLog;
		appGroupLog.pidLog = TimestampedLogBuffer(LOG_MONITORING_MAX_LINES * 5);
		c = logStore.insert(groupName, appGroupLog);
	}
	AppGroupLog &rec = c->value;

	HashedStaticString source(sourceStr, sourceStrLen);
	SimpleLogMap::Cell *c2 = rec.watchFileLog.lookupCell(source);
	if (c2 == NULL) {
		SimpleLogBuffer logBuffer(LOG_MONITORING_MAX_LINES);
		c2 = rec.watchFileLog.insert(source, logBuffer);
	}
	c2->value.clear();
	foreach (StaticString line, lines) {
		c2->value.push_back(string(line.data(), line.size()));
	}
	//unlock
}

Json::Value
Context::convertLog(){
	boost::lock_guard<boost::mutex> l(syncher); //lock
	Json::Value reply = Json::objectValue;

	if (!logStore.empty()) {
		Context::LogStore::ConstIterator appGroupIter(logStore);
		while (*appGroupIter != NULL) {
			reply[appGroupIter.getKey()] = Json::objectValue;

			Json::Value &processLog = reply[appGroupIter.getKey()]["Application process log (combined)"];
			if (processLog.isNull()) {
				processLog = Json::arrayValue;
			}
			foreach (TimestampedLog logLine, appGroupIter->value.pidLog) {
				Json::Value logLineJson = Json::objectValue;
				logLineJson["source_id"] = logLine.sourceId;
				logLineJson["timestamp"] = (Json::UInt64) logLine.timestamp;
				logLineJson["line"] = logLine.lineText;
				processLog.append(logLineJson);
			}

			Context::SimpleLogMap::ConstIterator watchFileLogIter(appGroupIter->value.watchFileLog);
			while (*watchFileLogIter != NULL) {
				if (!reply[appGroupIter.getKey()].isMember(watchFileLogIter.getKey())){
					reply[appGroupIter.getKey()][watchFileLogIter.getKey()] = Json::arrayValue;
				}
				foreach (string line, watchFileLogIter->value) {
					reply[appGroupIter.getKey()][watchFileLogIter.getKey()].append(line);
				}
				watchFileLogIter.next();
			}

			appGroupIter.next();
		}
	}

	return reply;
	//unlock
}

static void
realLogAppOutput(const HashedStaticString &groupName, int targetFd,
    char *buf, unsigned int bufSize,
	const char *pidStr, unsigned int pidStrLen,
	const char *channelName, unsigned int channelNameLen,
	const char *message, unsigned int messageLen, int appLogFile,
	bool saveLog, bool prefixLogs)
{
	char *pos = buf;
	char *end = buf + bufSize;

	if (prefixLogs) {
		pos = appendData(pos, end, "App ");
		pos = appendData(pos, end, pidStr, pidStrLen);
		pos = appendData(pos, end, " ");
		pos = appendData(pos, end, channelName, channelNameLen);
		pos = appendData(pos, end, ": ");
	}
	pos = appendData(pos, end, message, messageLen);
	pos = appendData(pos, end, "\n");

	if (OXT_UNLIKELY(context != NULL && saveLog)) {
		context->saveNewLog(groupName, pidStr, pidStrLen, message, messageLen);
	}
	if (appLogFile > -1) {
		writeExactWithoutOXT(appLogFile, buf, pos - buf);
	}
	writeExactWithoutOXT(targetFd, buf, pos - buf);
}

void
logAppOutput(const HashedStaticString &groupName, pid_t pid, const StaticString &channelName,
	const char *message, unsigned int size, const StaticString &appLogFile)
{
	int targetFd;
	bool saveLog = false;
	bool prefixLogs = true;

	if (OXT_LIKELY(context != NULL)) {
		const ConfigRealization *configRealization = context->getConfigRealization();
		if (configRealization->level < configRealization->appOutputLogLevel) {
			return;
		}

		targetFd = configRealization->targetFd;
		saveLog = configRealization->saveLog;
		prefixLogs = !configRealization->disableLogPrefix;
	} else {
		targetFd = STDERR_FILENO;
	}

	int fd = -1;
	if (!appLogFile.empty()) {
		fd = open(appLogFile.data(), O_WRONLY | O_APPEND | O_CREAT, 0640);
		if (fd == -1) {
			int e = errno;
			P_ERROR("opening file: " << appLogFile << " for logging " << groupName << " failed. Error: " << strerror(e));
		}
	}
	char pidStr[sizeof("4294967295")];
	unsigned int pidStrLen, totalLen;

	try {
		pidStrLen = integerToOtherBase<pid_t, 10>(pid, pidStr, sizeof(pidStr));
	} catch (const std::length_error &) {
		pidStr[0] = '?';
		pidStr[1] = '\0';
		pidStrLen = 1;
	}

	totalLen = (sizeof("App X Y: \n") - 2) + pidStrLen + channelName.size() + size;
	if (totalLen < 1024) {
		char buf[1024];
		realLogAppOutput(groupName, targetFd,
			buf, sizeof(buf),
			pidStr, pidStrLen,
			channelName.data(), channelName.size(),
			message, size, fd, saveLog, prefixLogs);
	} else {
		DynamicBuffer buf(totalLen);
		realLogAppOutput(groupName, targetFd,
			buf.data, totalLen,
			pidStr, pidStrLen,
			channelName.data(), channelName.size(),
			message, size, fd, saveLog, prefixLogs);
	}
	if(fd > -1){close(fd);}
}


static Json::Value
normalizeConfig(const Json::Value &effectiveValues) {
	Json::Value updates(Json::objectValue);

	updates["level"] = levelToString(parseLevel(
		effectiveValues["level"].asString())).toString();
	updates["app_output_log_level"] = levelToString(parseLevel(
		effectiveValues["app_output_log_level"].asString())).toString();

	if (effectiveValues["target"].isString()) {
		updates["target"]["path"] = absolutizePath(effectiveValues["target"].asString());
	} else if (!effectiveValues["target"]["path"].isNull()) {
		updates["target"] = effectiveValues["target"];
		updates["target"]["path"] = absolutizePath(effectiveValues["target"]["path"].asString());
	}

	if (effectiveValues["file_descriptor_log_target"].isString()) {
		updates["file_descriptor_log_target"]["path"] =
			absolutizePath(effectiveValues["file_descriptor_log_target"].asString());
	} else if (effectiveValues["file_descriptor_log_target"].isObject()
		&& !effectiveValues["file_descriptor_log_target"]["path"].isNull())
	{
		updates["file_descriptor_log_target"] = effectiveValues["file_descriptor_log_target"];
		updates["file_descriptor_log_target"]["path"] =
			absolutizePath(effectiveValues["file_descriptor_log_target"]["path"].asString());
	}

	return updates;
}


Context::Context(const Json::Value &initialConfig,
	const ConfigKit::Translator &translator)
	: config(schema, initialConfig, translator),
	  gcShuttingDown(false)
{
	configRlz.store(new ConfigRealization(config));
	configRlz.load()->apply(config, NULL);
	configRlz.load()->finalize();
	gcThread = new oxt::thread(boost::bind(&Context::gcThreadMain, this),
		"LoggingKit config garbage collector thread",
		128 * 1024);
}

Context::~Context() {
	{
		boost::unique_lock<boost::mutex> l(gcSyncher);
		gcShuttingDown = true;
		gcSyncherCond.notify_one();
	}
	gcThread->join();

	delete gcThread;
	delete configRlz.load();

	while (!oldConfigRlzs.empty()) {
		delete oldConfigRlzs.front().first;
		oldConfigRlzs.pop();
	}
}

ConfigKit::Store
Context::getConfig() const {
	boost::lock_guard<boost::mutex> l(syncher);
	return config;
}

bool
Context::prepareConfigChange(const Json::Value &updates,
	vector<ConfigKit::Error> &errors, LoggingKit::ConfigChangeRequest &req)
{
	{
		boost::lock_guard<boost::mutex> l(syncher);
		req.config.reset(new ConfigKit::Store(config, updates, errors));
	}
	if (!errors.empty()) {
		return false;
	}

	req.configRlz = new ConfigRealization(*req.config);
	return true;
}

void
Context::commitConfigChange(LoggingKit::ConfigChangeRequest &req) BOOST_NOEXCEPT_OR_NOTHROW {
	boost::lock_guard<boost::mutex> l(syncher);
	ConfigRealization *oldConfigRlz = configRlz.load();
	ConfigRealization *newConfigRlz = req.configRlz;

	req.configRlz->apply(*req.config, oldConfigRlz);

	config.swap(*req.config);

	configRlz.store(newConfigRlz, boost::memory_order_release);
	req.configRlz = NULL; // oldConfigRlz will be garbage collected by apply()

	newConfigRlz->finalize();
}

Json::Value
Context::inspectConfig() const {
	boost::lock_guard<boost::mutex> l(syncher);
	return config.inspect();
}

void
Context::freeOldConfigRlzLater(ConfigRealization *oldConfigRlz, MonotonicTimeUsec monotonicNow) {
	// Garbage collect old config realization in 5 minutes.
	// There is no way to cheaply find out whether oldConfigRlz
	// is still being used (we don't want to resort to more atomic
	// operations, or conservative garbage collection) but
	// waiting 5 minutes should be good enough.
	MonotonicTimeUsec gcTime = monotonicNow + 5llu * 60llu * 1000000llu;
	boost::unique_lock<boost::mutex> l(gcSyncher);
	oldConfigRlzs.push(make_pair(oldConfigRlz, gcTime));
	gcSyncherCond.notify_one();
}

void
Context::gcThreadMain() {
	boost::unique_lock<boost::mutex> l(gcSyncher);
	while (true) {
		while (oldConfigRlzs.empty() && !gcShuttingDown) {
			gcSyncherCond.wait(l);
		}
		if (gcShuttingDown) {
			break;
		}

		pair<ConfigRealization *, MonotonicTimeUsec> p = oldConfigRlzs.front();
		MonotonicTimeUsec now = SystemTime::getMonotonicUsecWithGranularity<SystemTime::GRAN_1SEC>();
		// Wait until it's time to GC this config object,
		// or until the destructor tells us that we're shutting down.
		while (!gcShuttingDown && now < p.second) {
			gcSyncherCond.timed_wait(l, boost::posix_time::microseconds(p.second - now));
			now = SystemTime::getMonotonicUsecWithGranularity<SystemTime::GRAN_1SEC>();
		}
		if (!gcShuttingDown) {
			delete p.first;
			oldConfigRlzs.pop();
		}
	}
}

Json::Value
Schema::createStderrTarget() {
	Json::Value doc;
	doc["stderr"] = true;
	return doc;
}

void
Schema::validateLogLevel(const string &key, const ConfigKit::Store &store,
	vector<ConfigKit::Error> &errors)
{
	typedef ConfigKit::Error Error;
	Level level = parseLevel(store[key].asString());
	if (level == UNKNOWN_LEVEL) {
		errors.push_back(Error("'{{" + key + "}}' must be one of"
			" 'crit', 'error', 'warn', 'notice', 'info', 'debug', 'debug2' or 'debug3'"));
	}
}

void
Schema::validateTarget(const string &key, const ConfigKit::Store &store,
	vector<ConfigKit::Error> &errors)
{
	typedef ConfigKit::Error Error;
	Json::Value value = store[key];
	string keyQuote = "'{{" + key + "}}'";

	if (value.isNull()) {
		return;
	}

	// Allowed formats:
	// "/path-to-file"
	// { "stderr": true }
	// { "path": "/path" }
	// { "path": "/path", "fd": 123 }
	// { "path": "/path", "stderr": true }

	if (value.isObject()) {
		if (value.isMember("stderr")) {
			if (!value["stderr"].isBool() || !value["stderr"].asBool()) {
				errors.push_back(Error("When " + keyQuote
					+ " is an object containing the 'stderr' key,"
					" it must have the 'true' value"));
				return;
			}
		}

		if (value.isMember("path")) {
			if (!value["path"].isString()) {
				errors.push_back(Error("When " + keyQuote
					+ " is an object containing the 'path' key,"
					" it must be a string"));
			}
			if (value.isMember("fd")) {
				if (!value["fd"].isInt()) {
					errors.push_back(Error("When " + keyQuote
						+ " is an object containing the 'fd' key,"
						" it must be an integer"));
				} else if (value["fd"].asInt() < 0) {
					errors.push_back(Error("When " + keyQuote
						+ " is an object containing the 'fd' key,"
						" it must be 0 or greater"));
				}
			}
			if (value.isMember("fd") && value.isMember("stderr")) {
				errors.push_back(Error(keyQuote
					+ " may contain either the 'fd' or the"
					" 'stderr' key, but not both"));
			}
		} else if (value.isMember("stderr")) {
			if (value.size() > 1) {
				errors.push_back(Error("When " + keyQuote
					+ " is an object containing the 'stderr' key,"
					" it may not contain any other keys"));
			} else if (!value["stderr"].asBool()) {
				errors.push_back(Error("When " + keyQuote
					+ " is an object containing the 'stderr' key,"
					" it must have the 'true' value"));
			}
		} else {
			errors.push_back(Error("When " + keyQuote
				+ " is an object, it must contain either"
				" the 'stderr' or 'path' key"));
		}
	} else if (!value.isString()) {
		errors.push_back(Error(keyQuote
			+ " must be either a string or an object"));
	}
}

static Json::Value
filterTargetFd(const Json::Value &value) {
	Json::Value result = value;
	result.removeMember("fd");
	return result;
}

Schema::Schema() {
	using namespace ConfigKit;

	add("level", STRING_TYPE, OPTIONAL, DEFAULT_LOG_LEVEL_NAME);
	add("target", ANY_TYPE, OPTIONAL, createStderrTarget())
		.setInspectFilter(filterTargetFd);
	add("file_descriptor_log_target", ANY_TYPE, OPTIONAL)
		.setInspectFilter(filterTargetFd);
	add("redirect_stderr", BOOL_TYPE, OPTIONAL, true);
	add("app_output_log_level", STRING_TYPE, OPTIONAL, DEFAULT_APP_OUTPUT_LOG_LEVEL_NAME);
	add("buffer_logs", BOOL_TYPE, OPTIONAL, false);
	add("disable_log_prefix", BOOL_TYPE, OPTIONAL, false);

	addValidator(boost::bind(validateLogLevel, "level",
		boost::placeholders::_1, boost::placeholders::_2));
	addValidator(boost::bind(validateLogLevel, "app_output_log_level",
		boost::placeholders::_1, boost::placeholders::_2));
	addValidator(boost::bind(validateTarget, "target",
		boost::placeholders::_1, boost::placeholders::_2));
	addValidator(boost::bind(validateTarget, "file_descriptor_log_target",
		boost::placeholders::_1, boost::placeholders::_2));

	addNormalizer(normalizeConfig);

	finalize();
}


ConfigRealization::ConfigRealization(const ConfigKit::Store &store)
	: level(parseLevel(store["level"].asString())),
	  appOutputLogLevel(parseLevel(store["app_output_log_level"].asString())),
	  saveLog(store["buffer_logs"].asBool()),
	  finalized(false),
	  disableLogPrefix(store["disable_log_prefix"].asBool())
{
	if (store["target"].isMember("stderr")) {
		targetType = STDERR_TARGET;
		targetFd = STDERR_FILENO;
		targetFdClosePolicy = NEVER_CLOSE;
	} else if (store["target"]["fd"].isNull()) {
		string path = store["target"]["path"].asString();
		targetType = FILE_TARGET;
		if (store["target"]["stderr"].asBool()) {
			targetFd = STDERR_FILENO;
			targetFdClosePolicy = NEVER_CLOSE;
		} else {
			targetFd = syscalls::open(path.c_str(),
				O_WRONLY | O_APPEND | O_CREAT, 0644);
			if (targetFd == -1) {
				int e = errno;
				throw FileSystemException(
					"Cannot open " + path + " for writing",
					e, path);
			}
			targetFdClosePolicy = ALWAYS_CLOSE;
		}
	} else {
		targetType = FILE_TARGET;
		targetFd = store["target"]["fd"].asInt();
		// If anything goes wrong before finalization, then
		// the caller is responsible for cleaning up the fd.
		// See the Context class description.
		targetFdClosePolicy = CLOSE_WHEN_FINALIZED;
	}

	if (store["file_descriptor_log_target"].isNull()) {
		fileDescriptorLogTargetType = NO_TARGET;
		fileDescriptorLogTargetFd = -1;
		fileDescriptorLogTargetFdClosePolicy = NEVER_CLOSE;
	} else if (store["file_descriptor_log_target"].isMember("stderr")) {
		fileDescriptorLogTargetType = STDERR_TARGET;
		fileDescriptorLogTargetFd = STDERR_FILENO;
		fileDescriptorLogTargetFdClosePolicy = NEVER_CLOSE;
	} else if (store["file_descriptor_log_target"]["fd"].isNull()) {
		string path = store["file_descriptor_log_target"]["path"].asString();
		fileDescriptorLogTargetType = FILE_TARGET;
		if (store["file_descriptor_log_target"]["stderr"].asBool()) {
			fileDescriptorLogTargetFd = STDERR_FILENO;
			fileDescriptorLogTargetFdClosePolicy = NEVER_CLOSE;
		} else {
			fileDescriptorLogTargetFd = syscalls::open(path.c_str(),
				O_WRONLY | O_APPEND | O_CREAT, 0644);
			if (fileDescriptorLogTargetFd == -1) {
				int e = errno;
				throw FileSystemException(
					"Cannot open " + path + " for writing",
					e, path);
			}
			fileDescriptorLogTargetFdClosePolicy = ALWAYS_CLOSE;
		}
	} else {
		fileDescriptorLogTargetType = FILE_TARGET;
		fileDescriptorLogTargetFd = store["file_descriptor_log_target"]["fd"].asInt();
		// If anything goes wrong before finalization, then
		// the caller is responsible for cleaning up the fd.
		// See the Context class description.
		fileDescriptorLogTargetFdClosePolicy = CLOSE_WHEN_FINALIZED;
	}
}

ConfigRealization::~ConfigRealization() {
	switch (targetFdClosePolicy) {
	case NEVER_CLOSE:
		// Do nothing.
		break;
	case ALWAYS_CLOSE:
		syscalls::close(targetFd);
		break;
	case CLOSE_WHEN_FINALIZED:
		if (finalized) {
			syscalls::close(targetFd);
		}
		break;
	}

	switch (fileDescriptorLogTargetFdClosePolicy) {
	case NEVER_CLOSE:
		// Do nothing.
		break;
	case ALWAYS_CLOSE:
		syscalls::close(fileDescriptorLogTargetFd);
		break;
	case CLOSE_WHEN_FINALIZED:
		if (finalized) {
			syscalls::close(fileDescriptorLogTargetFd);
		}
		break;
	}
}

void
ConfigRealization::apply(const ConfigKit::Store &config, ConfigRealization *oldConfigRlz)
	BOOST_NOEXCEPT_OR_NOTHROW
{
	if (config["redirect_stderr"].asBool()) {
		int ret = syscalls::dup2(targetFd, STDERR_FILENO);
		if (ret == -1) {
			int e = errno;
			P_ERROR("Error redirecting logging target to stderr: "
				<< strerror(e) << " (errno=" << e << ")");
		}
	}

	if (oldConfigRlz != NULL) {
		MonotonicTimeUsec monotonicNow = SystemTime::getMonotonicUsecWithGranularity<SystemTime::GRAN_1SEC>();
		context->freeOldConfigRlzLater(oldConfigRlz, monotonicNow);
	}
}

void
ConfigRealization::finalize() {
	finalized = true;
}


ConfigChangeRequest::ConfigChangeRequest()
	: configRlz(NULL)
{
	// Do nothing.
}

ConfigChangeRequest::~ConfigChangeRequest() {
	delete configRlz;
}


} // namespace LoggingKit
} // namespace Passenger
