// SPDX-License-Identifier: GPL-2.0-only WITH linking exception
// SPDX-FileCopyrightText: 2021–2026 grommunio GmbH
// This file is part of Gromox.
#include <algorithm>
#include <cassert>
#include <cerrno>
#include <climits>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <future>
#include <memory>
#include <mutex>
#include <netdb.h>
#include <poll.h>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <unordered_set>
#include <utility>
#include <vector>
#include <sys/socket.h>
#include <sys/types.h>
#include <libHX/io.h>
#include <libHX/socket.h>
#include <libHX/string.h>
#include <gromox/atomic.hpp>
#include <gromox/clock.hpp>
#include <gromox/config_file.hpp>
#include <gromox/defs.h>
#include <gromox/exmdb_common_util.hpp>
#include <gromox/exmdb_ext.hpp>
#include <gromox/exmdb_rpc.hpp>
#include <gromox/exmdb_server.hpp>
#include <gromox/list_file.hpp>
#include <gromox/listener_ctx.hpp>
#include <gromox/mapi_types.hpp>
#include <gromox/mysql_adaptor.hpp>
#include <gromox/paths.h>
#include <gromox/process.hpp>
#include <gromox/socketpass.hpp>
#include <gromox/svc_loader.hpp>
#include <gromox/util.hpp>
#include "db_engine.hpp"
#include "notification_agent.hpp"
#include "parser.hpp"
#ifndef AI_V4MAPPED
#	define AI_V4MAPPED 0
#endif

using namespace gromox;

struct parser_params {
	/* worker state */
	std::shared_ptr<exmdb_connection> conn;
	std::string single_user, injected_pkt;
	bool is_connected = false, b_private = false;

	/* set only when director role */
	bool use_workers = false;
	std::string_view input_buf;
};

struct pickup_params {
	wrapfd fd;
};

static size_t g_max_threads, g_max_routers;
static std::unordered_set<std::shared_ptr<router_connection>> g_router_list;
/* used for counting and for setting c->b_stop on exit from main thread */
static std::unordered_set<std::shared_ptr<exmdb_connection>> g_connection_list;
static std::mutex g_router_lock, g_connection_lock;
static gromox::atomic_bool g_exmdblisten_stop, g_exmdbpickup_running;
static std::vector<std::string> g_acl_list;
static listener_ctx exmdb_listen_ctx;
std::atomic<unsigned int> g_enable_dam, g_istore_standalone, g_dbengine_wanttoend;
std::string g_host_id;
pthread_t g_exmdbpickup_tid;
std::mutex g_exmdbpickup_tlock;
static pthread_t g_spzclean_tid;

parser_thread::parser_thread(generic_connection &&co) :
	generic_connection(std::move(co))
{}

parser_thread::parser_thread(parser_thread &&o) :
	generic_connection(std::move(o))
{
	thr_id = std::move(o.thr_id);
	o.thr_id = {};
}

parser_thread::~parser_thread()
{
	/*
	 * When the dtor runs, no thread should be in the work function's loop
	 * anymore
	 */
	if (pthread_equal(thr_id, {}))
		return;
	/*
	 * XXX: This is not great, but the logic has existed for
	 * practically forever (it used to test b_stop rather than
	 * thr_id, but that's even worse).
	 */
	if (pthread_equal(thr_id, pthread_self()))
		pthread_detach(thr_id);
	else
		pthread_join(thr_id, nullptr);
}

router_connection::router_connection(parser_thread &&o, std::string_view rid) :
	parser_thread(std::move(o)), last_time(time(nullptr))
{
	remote_id = rid;
}

void router_connection::push_and_wake(BINARY &&b) try
{
	std::unique_ptr<uint8_t[], stdlib_delete> blob(std::move(b.pb));
	b.pb = nullptr;
	{
		std::lock_guard dg_hold(dg_lock);
		datagram_list.emplace_back(std::move(blob), b.cb);
	}
	waken_cond.notify_one();
} catch (const std::bad_alloc &) {
}

void parser_thread::signal_stop()
{
	b_stop = true;
	{
		std::lock_guard lk(fd_lock);
		if (sockd >= 0)
			shutdown(sockd, SHUT_RDWR);
	}
	std::lock_guard lk(thr_lock);
	if (!pthread_equal(thr_id, {}))
		/* kick calls like read,write */
		pthread_kill(thr_id, SIGALRM);
}

void router_connection::signal_stop()
{
	parser_thread::signal_stop();
	/* but for condition_variables we really need this instead */
	waken_cond.notify_one();
}

void parser_thread::close_fd()
{
	std::lock_guard lk(fd_lock);
	generic_connection::reset();
}

void parser_thread::join()
{
	/*
	 * When T1 issues join, T1 has a shared_ptr reference, therefore T2
	 * cannot be executing in ~exmdb_connection, so pthread_detach won't
	 * interfere.
	 */
	std::lock_guard lk(thr_lock);
	if (!pthread_equal(thr_id, {})) {
		pthread_join(thr_id, nullptr);
		thr_id = {};
	}
}

void exmdb_parser_init(size_t max_threads, size_t max_routers)
{
	g_max_threads = max_threads;
	g_max_routers = max_routers;
}

/**
 * Indicate whether this machine is responsible for serving a mailbox
 *
 * @prefix:  a mailbox directory
 * @pvt:     returns whether the directory refers to a private or public store
 *
 * Returns 0 if local, ENOENT if (known to be) served elsewhere, or another
 * errno for transient infrastructure failures (e.g. SQL unavailable), which
 * callers should not present as a permanent misconfiguration.
 */
static errno_t exmdb_parser_is_local(const char *prefix, bool *pvt)
{
	if (*prefix == '\0')
		return 0;
	std::string hostname;
	auto err = mysql_adaptor_get_homeserver_for_dir(prefix, pvt, hostname);
	if (err == ENOENT)
		return ENOENT;
	if (err != 0) {
		mlog(LV_ERR, "%s: %s: %s", __func__, prefix, strerror(err));
		return err;
	}
	if (hostname == g_host_id || hostname.empty())
		return 0;
	mlog(LV_ERR, "exmdb: is_local: %s not served here (%s) (but by %s)",
		prefix, g_host_id.c_str(), hostname.c_str());
	return ENOENT;
}

static BOOL exmdb_parser_dispatch3(const exreq *q0, std::unique_ptr<exresp> &r0)
{
	switch (q0->call_id) {
#include <exmdb_dispatch.cpp>
	default:
		return FALSE;
	}
}

static BOOL exmdb_parser_dispatch2(const exreq *prequest, std::unique_ptr<exresp> &r0) try
{
	/*
	 * Special handling for a few RPCs in lieu of the default code provided
	 * for these callids in dispatch3.
	 */
	switch (prequest->call_id) {
	case exmdb_callid::get_content_sync: {
		auto &q = *static_cast<const exreq_get_content_sync *>(prequest);
		auto r1 = std::make_unique<exresp_get_content_sync>();
		auto &r = *r1;
		auto b_return = exmdb_server::get_content_sync(prequest->dir,
		           q.folder_id, q.username, q.pgiven, q.pseen,
		           q.pseen_fai, q.pread, q.cpid, q.prestriction,
		           q.b_ordered, &r.fai_count, &r.fai_total,
		           &r.normal_count, &r.normal_total, &r.updated_mids,
		           &r.chg_mids, &r.last_cn, &r.given_mids,
		           &r.deleted_mids, &r.nolonger_mids, &r.read_mids,
		           &r.unread_mids, &r.last_readcn);
		delete q.pgiven;
		delete q.pseen;
		delete q.pseen_fai;
		delete q.pread;
		r0 = std::move(r1);
		return b_return;
	}
	case exmdb_callid::get_hierarchy_sync: {
		auto &q = *static_cast<const exreq_get_hierarchy_sync *>(prequest);
		auto r1 = std::make_unique<exresp_get_hierarchy_sync>();
		auto &r = *r1;
		auto b_return = exmdb_server::get_hierarchy_sync(prequest->dir,
		           q.folder_id, q.username, q.pgiven, q.pseen,
		           &r.fldchgs, &r.last_cn, &r.given_fids,
		           &r.deleted_fids);
		delete q.pgiven;
		delete q.pseen;
		r0 = std::move(r1);
		return b_return;
	}
	default:
		return exmdb_parser_dispatch3(prequest, r0);
	}
} catch (const std::bad_alloc &) {
	return false;
}

static BOOL exmdb_parser_dispatch(const exreq *prequest, std::unique_ptr<exresp> &presponse)
{
	auto tstart = tp_now();
	exmdb_server::set_dir(prequest->dir);
	auto ret = exmdb_parser_dispatch2(prequest, presponse);
	if (ret)
		presponse->call_id = prequest->call_id;
	auto dbg = g_exrpc_debug.load();
	if (dbg == 0)
		return ret;
	auto tend = tp_now();
	if (!ret || dbg == 2)
		mlog(LV_DEBUG, "EXRPC %s %s %5luµs %s", znul(prequest->dir),
		        ret == 0 ? "ERR" : "ok ",
		        static_cast<unsigned long>(std::chrono::duration_cast<std::chrono::microseconds>(tend - tstart).count()),
		        exmdb_rpc_idtoname(prequest->call_id));
	return ret;
}

static inline void stripslash(char *s)
{
	for (auto z = strlen(s); z > 1 && s[z-1] == '/'; --z)
		s[z-1] = '\0';
}

static bool max_routers_reached()
{
	std::unique_lock r_hold(g_router_lock);
	return g_router_list.size() >= g_max_routers;
}

static std::mutex spwork_lock;
static std::map<std::string, socketpass_worker> spworkers;
static bool rqi_handoff(exmdb_connection &conn, const char *dir,
    std::string_view input_buf)
{
	if (*dir == '\0') {
		mlog(LV_ERR, "Tried to call rqi_handoff with empty dir\n");
		raise(SIGABRT);
	}
	/* End the connection in any case once we return */
	conn.b_stop = true;

	std::string prog;
	if (auto s = getenv("ISTORE_WORKER")) {
		prog = s;
	} else {
		prog = znul(service_get_prog_arg0());
		if (!prog.empty())
			prog = std::unique_ptr<char[], stdlib_delete>(HX_dirname(prog.c_str())).get();
		prog += "/istore";
	}
	const char *args[] = {prog.c_str(), "-x", dir, nullptr};

	std::lock_guard lk(spwork_lock);
	auto [iter, added] = spworkers.try_emplace(dir);
	auto &worker = iter->second;
	auto err = worker.start(prog.c_str(), const_cast<char **>(args));
	if (err < 0) {
		mlog(LV_ERR, "spworker_start %s: %s", prog.c_str(), strerror(-err));
		return false;
	}
	auto pass_err = worker.pass(input_buf, conn.sockd);
	if (pass_err == 0)
		return true;
	if (err > 0) {
		/* Newly-launched process failed outright */
		mlog(LV_ERR, "spworker_pass %s: %s", prog.c_str(), strerror(pass_err));
		return false;
	}
	/*
	 * Process is a bit "older" and might have shutdown due to inactivity
	 * timer, try to revive and retry once. We still hold spwork_lock, so
	 * no other thread can create a third spawn for this dir between these
	 * calls.
	 */
	if (pass_err == EPIPE || pass_err == ECONNRESET) {
		err = worker.restart(prog.c_str(), const_cast<char **>(args));
		if (err < 0) {
			mlog(LV_ERR, "spworker_start (retry) %s: %s",
				prog.c_str(), strerror(-err));
			return false;
		}
		pass_err = worker.pass(input_buf, conn.sockd);
		if (pass_err == 0)
			return true;
	}
	mlog(LV_ERR, "spworker_pass %s: %s", prog.c_str(), strerror(pass_err));
	return false;
}

static int rqi_terminate(exmdb_connection &conn, exmdb_response resp_code)
{
	if (HXio_fullwrite(conn.sockd, &resp_code, 1) != 1)
		/* ignore */;
	return -1;
}

static bool handoff_just_one(const char *rq_dir)
{
	auto allow_dir = getenv("ISTORE_JUST_ONE");
	if (allow_dir == nullptr)
		return true; /* split all */

	/* split just this one */
	return rq_dir != nullptr && strcmp(rq_dir, allow_dir) == 0;
}

static int rqi_connect(parser_params &param, const exreq_connect &q,
    std::string_view input_buf, BINARY &output_buf)
{
	auto &conn = *param.conn;

	if (!param.single_user.empty() &&
	    strcmp(param.single_user.c_str(), q.dir) != 0)
		return rqi_terminate(conn, exmdb_response::misconfig_prefix);
	auto lcl = exmdb_parser_is_local(q.dir, &param.b_private);
	if (lcl == ENOENT)
		return rqi_terminate(conn, exmdb_response::misconfig_prefix);
	else if (lcl != 0)
		return rqi_terminate(conn, exmdb_response::service_unavailable);
	else if (!!param.b_private != !!q.b_private)
		return rqi_terminate(conn, exmdb_response::misconfig_mode);
	if (param.use_workers && handoff_just_one(q.dir))
		return rqi_handoff(conn, q.dir, input_buf);

	/* q.remote_id is going away, copy it */
	conn.remote_id = q.remote_id;
	exmdb_server::set_remote_id(conn.remote_id.c_str());
	param.is_connected = true;
	free(output_buf.pb);
	output_buf.pb = static_cast<uint8_t *>(calloc(1, 5));
	if (output_buf.pb == nullptr)
		return rqi_terminate(conn, exmdb_response::lack_memory);
	output_buf.pb[0] = static_cast<uint8_t>(exmdb_response::success);
	output_buf.cb = 5;
	return 0;
}

static int rqi_listen(parser_params &param, const exreq_listen_notification &q,
    std::string_view input_buf) try
{
	auto &conn = *param.conn;
	if (!param.single_user.empty() &&
	    strcmp(param.single_user.c_str(), q.dir) != 0)
		return rqi_terminate(conn, exmdb_response::misconfig_prefix);
	if (g_max_routers != 0 && max_routers_reached())
		return rqi_terminate(conn, exmdb_response::max_reached);
	if (param.use_workers && handoff_just_one(q.dir))
		return rqi_handoff(conn, q.dir, input_buf);

	auto router = std::make_shared<router_connection>(std::move(static_cast<parser_thread &>(conn)), q.remote_id);
	static constexpr char success[5]{};
	auto wrret = write(router->sockd, success, std::size(success));
	if (wrret < 0 || static_cast<size_t>(wrret) != std::size(success))
		return -1; /* OS error */
	router->last_time = time(nullptr);
	{
		std::unique_lock r_hold(g_router_lock);
		g_router_list.insert(router);
	}
	{
		std::unique_lock chold(g_connection_lock);
		g_connection_list.erase(param.conn);
	}
	/*
	 * This function runs practically forever;
	 * thus we close the connection when that is all done.
	 */
	return notification_agent_thread_work(std::move(router));
} catch (const std::bad_alloc &) {
	return rqi_terminate(*param.conn, exmdb_response::lack_memory);
}

static int rqi_unconnected(parser_params &param, const exreq &request,
    std::string_view input_buf, BINARY &output_buf)
{
	switch (request.call_id) {
	case exmdb_callid::connect:
		return rqi_connect(param, static_cast<const exreq_connect &>(request),
		       input_buf, output_buf);
	case exmdb_callid::listen_notification:
		return rqi_listen(param, static_cast<const exreq_listen_notification &>(request),
		       input_buf);
	default:
		return rqi_terminate(*param.conn, exmdb_response::connect_incomplete);
	}
}

/**
 * On success, sets output_buf to something to send to the network and returns
 * 0. On error when the connection should be immediately closed, -1 is
 * returned. (In the error case, writing the error packet to the network is
 * done by rqp_io itself.)
 */
static int rqi_handle_buffer(parser_params &param, std::string_view input_buf,
    BINARY &output_buf)
{
	auto &conn = *param.conn;
	exmdb_server::build_env(param.b_private ? EM_PRIVATE : 0, nullptr);
	auto cl_env = HX::make_scope_exit(exmdb_server::free_env);

	std::unique_ptr<exreq> request;
	auto status = exmdb_ext_pull_request(input_buf, request);
	if (status != pack_result::ok)
		return rqi_terminate(conn, exmdb_response::pull_error);
	if (request == nullptr)
		return rqi_terminate(conn, exmdb_response::lack_memory);
	if (request->dir != nullptr)
		stripslash(request->dir);

	std::unique_ptr<exresp> response;
	if (!param.is_connected)
		return rqi_unconnected(param, *request, input_buf, output_buf);
	if (request->dir != nullptr && !param.single_user.empty() &&
	    param.single_user != request->dir)
		return rqi_terminate(conn, exmdb_response::misconfig_prefix);
	if (!exmdb_parser_dispatch(request.get(), response))
		return rqi_terminate(conn, exmdb_response::dispatch_error);
	if (exmdb_ext_push_response(response.get(), &output_buf) != pack_result::success)
		return rqi_terminate(conn, exmdb_response::push_error);
	return 0;
}

static void *request_parser_thread(void *pparam)
{
	uint8_t resp_buff[5]{};
	struct pollfd pfd_read;
	
	std::unique_ptr<parser_params> param(static_cast<parser_params *>(pparam));
	auto &pconnection = param->conn;
	auto cl_conn = HX::make_scope_exit([&]() {
		{ /* No new holders */
			std::lock_guard lk(g_connection_lock);
			g_connection_list.erase(pconnection);
		}
		/* Force remaining holders into a wall */
		pconnection->close_fd();
	});
	param->use_workers = strcmp(service_get_prog_id(), "istore-director") == 0 &&
	                     g_istore_standalone & ISTORE_SPLIT_WORKERS;
	try {
		char txt[16];
		snprintf(txt, std::size(txt), "exrq/%hu", pconnection->client_port);
		pthread_setname_np(pthread_self(), txt);
		std::unique_lock chold(g_connection_lock);
		g_connection_list.insert(pconnection);
	} catch (...) {
		return nullptr;
	}
	size_t offset = 0;
	bool is_writing = false, is_connected = false;
	BINARY output_buf{};
	auto cl_0 = HX::make_scope_exit([&]() { free(output_buf.pb); });
	std::string input_buf;

	while (!pconnection->b_stop) {
		if (is_writing) {
			auto wlen = write(pconnection->sockd, &output_buf.pb[offset],
			            output_buf.cb - offset);
			if (wlen <= 0)
				break;
			offset += wlen;
			if (offset < output_buf.cb)
				continue; /* keep writing if necessary */
			free(output_buf.pb);
			output_buf.pb = nullptr;
			output_buf.cb = 0;
			offset = 0;
			is_writing = false;
			continue;
		}
		if (!param->injected_pkt.empty()) {
			if (rqi_handle_buffer(*param, param->injected_pkt, output_buf) < 0)
				break;
			param->injected_pkt.clear();
			offset = 0;
			is_writing = true;
			continue;
		}
		pfd_read.fd = pconnection->sockd;
		pfd_read.events = POLLIN|POLLPRI;
		if (poll(&pfd_read, 1, SOCKET_TIMEOUT_MS) != 1)
			break;
		if (input_buf.empty()) {
			uint32_t buff_len = 0;
			auto read_len = read(pconnection->sockd,
					&buff_len, sizeof(uint32_t));
			if (read_len != sizeof(uint32_t))
				break;
			/* ping packet */
			if (0 == buff_len) {
				if (HXio_fullwrite(pconnection->sockd, resp_buff, 1) != 1)
					break;
				continue;
			} else if (buff_len >= UINT_MAX) {
				/* make cov-scan happy that we tested for buff_len */
				break;
			}
			try {
				input_buf.resize(buff_len);
			} catch (const std::bad_alloc &) {
				auto tmp_byte = exmdb_response::lack_memory;
				if (HXio_fullwrite(pconnection->sockd, &tmp_byte, 1) != 1 ||
				    !is_connected)
					break;
			}
			offset = 0;
			continue;
		}
		auto read_len = read(pconnection->sockd, &input_buf[offset],
		                input_buf.size() - offset);
		if (read_len <= 0)
			break;
		offset += read_len;
		if (offset < input_buf.size())
			continue; /* keep reading as necessary */
		if (rqi_handle_buffer(*param, input_buf, output_buf) < 0)
			break;
		input_buf.clear();
		offset = 0;
		is_writing = true;
	}
	return nullptr;
}

/*
 * Returns whether thread creation succeeded (and that co->sockd is now under
 * control of the new thread).
 */
bool exmdb_parser_insert_conn(std::shared_ptr<exmdb_connection> co)
{
	if (g_max_threads != 0) {
		std::lock_guard lk(g_connection_lock);
		if (g_connection_list.size() >= g_max_threads)
			return false;
	}

	auto par = std::make_unique<parser_params>();
	par->conn = std::move(co);
	auto ret = pthread_create4(&par->conn->thr_id, nullptr,
	           request_parser_thread, par.get());
	if (ret != 0) {
		mlog(LV_WARN, "W-2323: pthread_create: %s", strerror(ret));
		return false;
	} else {
		par.release(); /* thread should be vivid now */
		return true;
	}
}

std::shared_ptr<router_connection> exmdb_parser_get_router(const char *remote_id)
{
	std::lock_guard rhold(g_router_lock);
	auto it = std::find_if(g_router_list.begin(), g_router_list.end(),
	          [&](const auto &r) { return r->remote_id == remote_id; });
	return it != g_router_list.end() ? *it : nullptr;
}

void exmdb_parser_insert_router(std::shared_ptr<router_connection> &&pconnection)
{
	std::lock_guard rhold(g_router_lock);
	try {
		g_router_list.insert(std::move(pconnection));
	} catch (const std::bad_alloc &) {
	}
}

BOOL exmdb_parser_erase_router(const std::shared_ptr<router_connection> &pconnection)
{
	std::lock_guard rhold(g_router_lock);
	auto it = g_router_list.find(pconnection);
	if (it == g_router_list.cend())
		return false;
	g_router_list.erase(it);
	return TRUE;
}

void exmdb_parser_stop()
{
	/*
	 * At the end of this function, we want all threads be gone for sure,
	 * so they must be joined and cannot be let to detach themselves.
	 * Therefore, this function needs to be the last holder of the
	 * shared_ptr<>s.
	 *
	 * To do that, the connection lists are copied. [Detail: it can be moved,
	 * which will make rqi_listen :: `g_connection_list.erase(param.conn)`
	 * a no-op, which is good enough.].
	 */
	decltype(g_connection_list) conn_list;
	decltype(g_router_list) rt_list;
	{
		std::lock_guard chold(g_connection_lock);
		conn_list = std::move(g_connection_list);
		g_connection_list.clear();
	}
	{
		std::lock_guard rhold(g_router_lock);
		rt_list = std::move(g_router_list);
		g_router_list.clear();
	}
	for (auto &c : conn_list)
		c->signal_stop();
	for (auto &rt : rt_list)
		rt->signal_stop();
	for (auto &c : conn_list)
		c->join();
	for (auto &rt : rt_list)
		rt->join();
}

static int sockaccept_thread(generic_connection &&conn) try
{
	if (std::find(g_acl_list.cbegin(), g_acl_list.cend(),
	    conn.client_addr) == g_acl_list.cend()) {
		static std::atomic<time_t> g_lastwarn_time;
		auto prev = g_lastwarn_time.load();
		auto next = prev + 60;
		auto now = time(nullptr);
		if (next <= now && g_lastwarn_time.compare_exchange_strong(prev, now))
			mlog(LV_INFO, "I-1666: Rejecting %s: not allowed by exmdb_acl", conn.client_addr);
		auto tmp_byte = exmdb_response::access_deny;
		if (HXio_fullwrite(conn.sockd, &tmp_byte, 1) != 1)
			/* ignore */;
		return 0;
	}
	auto sp = std::make_shared<exmdb_connection>(std::move(conn));
	if (!exmdb_parser_insert_conn(sp)) {
		auto tmp_byte = exmdb_response::max_reached;
		if (HXio_fullwrite(sp->sockd, &tmp_byte, 1) != 1)
			/* ignore */;
		return 0;
	}
	return 0;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "%s: ENOMEM", __func__);
	return 0;
}

static int exmdb_acl_read(const char *config_path, const char *hosts_allow)
{
	auto &acl = g_acl_list;
	if (hosts_allow != nullptr)
		acl = gx_split(hosts_allow, ' ');
	auto ret = read_file_by_line("exmdb_acl.txt", config_path, acl);
	if (ret == ENOENT) {
	} else if (ret != 0) {
		mlog(LV_ERR, "exmdb_provider: Failed to read ACLs from exmdb_acl.txt: %s", strerror(errno));
		return -5;
	}
	std::sort(acl.begin(), acl.end());
	std::erase(acl, "");
	acl.erase(std::unique(acl.begin(), acl.end()), acl.end());
	if (acl.size() == 0) {
		mlog(LV_NOTICE, "exmdb_provider: defaulting to implicit access ACL containing ::1.");
		acl = {"::1"};
	}
	return 0;
}

int exmdb_listener_init(const config_file &gxcfg, const config_file &oldcfg)
{
	auto &ctx = exmdb_listen_ctx;
	ctx.m_thread_name = "exmdb_accept";
	auto line = gxcfg.get_value("exmdb_listen");
	if (line != nullptr)
		return ctx.add_bunch(line);
	auto host = oldcfg.get_value("listen_ip");
	if (host != nullptr)
		mlog(LV_NOTICE, "%s:listen_ip is deprecated in favor of %s:exmdb_listen",
			oldcfg.m_filename.c_str(), gxcfg.m_filename.c_str());
	else
		host = "::1";
	auto ps = oldcfg.get_value("exmdb_listen_port");
	uint16_t port = 5000;
	if (ps != nullptr) {
		mlog(LV_NOTICE, "%s:exmdb_listen_port is deprecated in favor of %s:exmdb_listen",
			oldcfg.m_filename.c_str(), gxcfg.m_filename.c_str());
		port = strtoul(znul(ps), nullptr, 0);
	}
	if (port != 0 && ctx.add_inet(host, port) != 0)
		return -1;
	return 0;
}

static void *spwz_cleaner(void *)
{
	/*
	 * SIGCHLD is not guaranteed to be queued; i.e. there may be coalescing
	 * and multiple children exiting might only lead to generation of a
	 * single signal event. One approach would be to drain the waiting
	 * queue `while(waitpid(-1, nullptr, WNOHANG) > 0){}`, but that runs
	 * afoul of collecting unrelated children [e.g. from rtftohtml]. So we
	 * will have to periodically loop over a list of PIDs known to us.
	 */
	pthread_setname_np(pthread_self(), "spwz_clean");
	while (!g_exmdblisten_stop) {
		sleep(60);
		std::lock_guard lk(spwork_lock);
		for (auto &e : spworkers)
			e.second.maybe_reap();
	}
	return nullptr;
}

int exmdb_listener_run(const char *config_path, const config_file &oldcfg)
{
	auto ret = exmdb_acl_read(config_path, oldcfg.get_value("exmdb_hosts_allow"));
	if (ret != 0)
		return ret;
	if (exmdb_listen_ctx.empty())
		return 0;
	g_exmdblisten_stop = false;
	auto err = exmdb_listen_ctx.watch_start(g_exmdblisten_stop, sockaccept_thread);
	if (err != 0) {
		mlog(LV_ERR, "exmdb_provider: failed to create exmdb listener thread: %s", strerror(err));
		return -1;
	}
	err = pthread_create(&g_spzclean_tid, nullptr, spwz_cleaner, nullptr);
	if (err != 0) {
		mlog(LV_ERR, "pthread_create spzclean: %s", strerror(err));
		exmdb_listen_ctx.reset();
		return -1;
	}
	return 0;
}

static void spw_clean()
{
	while (true) {
		decltype(spworkers.extract(spworkers.begin())) wnode;
		std::lock_guard lk(spwork_lock);
		if (spworkers.empty())
			return;
		wnode = spworkers.extract(spworkers.begin());
	}
}

void exmdb_listener_stop()
{
	g_exmdblisten_stop = true;
	if (!pthread_equal(g_spzclean_tid, {})) {
		pthread_kill(g_spzclean_tid, SIGALRM);
		pthread_join(g_spzclean_tid, nullptr);
	}
	exmdb_listen_ctx.reset();
	std::vector<std::future<void>> futs;
	for (size_t i = 0; i < g_exmdb_par_shutdown; ++i)
		futs.emplace_back(std::async(spw_clean));
}

/**
 * Returns a negative value to indicate that the caller (exmdb_pickup_loop)
 * should cease operation (e.g. because the socket peer has ended
 * transmissions), or 0 to continue.
 */
static int exmdb_pickup_one(int control_fd)
{
	auto par = std::make_unique<parser_params>();
	int client_fd = -1;
	auto ern = socketpass_receive(control_fd, par->injected_pkt, client_fd);
	if (ern == EINTR || ern == EAGAIN)
		/*
		 * Possibly as a result of Ctrl-C-ing the daemon or thread kick
		 * (SIGALRM)
		 */
		return 0;
	if (ern != 0)
		return -1;
	if (client_fd < 0)
		return 0;
	g_dbengine_wanttoend = false;
	/* Bring the fd under management (wrapfd) */
	par->conn = std::make_shared<exmdb_connection>(generic_connection::takeover(std::move(client_fd)));
	if (par->conn->sockd < 0)
		return 0;
	if (par->injected_pkt.empty())
		return 0;

	/* reprise of exmdb_parser_insert_conn */
	par->single_user = znul(getenv("ISTORE_USER"));
	auto ret = pthread_create4(&par->conn->thr_id, nullptr,
	           request_parser_thread, par.get());
	if (ret != 0)
		mlog(LV_WARN, "W-2324: pthread_create: %s", strerror(ret));
	else
		par.release(); /* thread should be vivid now */
	return 0;
}

static size_t routers_active()
{
	std::lock_guard lk(g_router_lock);
	return g_router_list.size();
}

static void *exmdb_pickup_loop(void *arg)
{
	pthread_setname_np(pthread_self(), "exmdb_pickup");
	std::unique_ptr<pickup_params> par(static_cast<pickup_params *>(arg));
	while (!g_exmdblisten_stop) {
		auto status = exmdb_pickup_one(STDIN_FILENO);
		if (status < 0)
			break; /* havetoend */
		if (status == 0 && g_dbengine_wanttoend && !routers_active())
			break;
	}
	g_exmdbpickup_running = false;
	return nullptr;
}

bool exmdb_pickup_running()
{
	return g_exmdbpickup_running.load();
}

int exmdb_pickup_run(int fd) try
{
	g_exmdblisten_stop = false;
	wrapfd wfd = fd;
	auto par = std::make_unique<pickup_params>(std::move(wfd));
	g_exmdbpickup_running = true;
	int ret = 0;
	{
		std::unique_lock lk(g_exmdbpickup_tlock);
		ret = pthread_create(&g_exmdbpickup_tid, nullptr,
		      exmdb_pickup_loop, par.get());
	}
	if (ret != 0) {
		mlog(LV_WARN, "W-1440: pthread_create: %s", strerror(ret));
		g_exmdbpickup_running = false;
		return -1;
	}
	par.release(); /* thread should be vivid now */
	return 0;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "%s: ENOMEM", __PRETTY_FUNCTION__);
	g_exmdbpickup_running = false;
	return ENOMEM;
}

void exmdb_pickup_stop()
{
	g_exmdblisten_stop = true;
	std::unique_lock lk(g_exmdbpickup_tlock);
	if (!pthread_equal(g_exmdbpickup_tid, {})) {
		pthread_kill(g_exmdbpickup_tid, SIGALRM);
		pthread_join(g_exmdbpickup_tid, nullptr);
	}
}
