/*
 * Copyright 2025 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "config.h"
#include "rspamd.h"
#include "rspamd_control.h"
#include "worker_util.h"
#include "libserver/http/http_connection.h"
#include "libserver/http/http_private.h"
#include "libutil/libev_helper.h"
#include "unix-std.h"
#include "utlist.h"
#include "khash.h"
#include "composites/composites.h"
#include "memory_stat.h"

#ifdef HAVE_SYS_RESOURCE_H
#include <sys/resource.h>
#endif

#ifdef WITH_HYPERSCAN
#include "hyperscan_tools.h"
#endif

#define msg_debug_control(...) rspamd_conditional_debug_fast(NULL, NULL,                             \
															 rspamd_control_log_id, "control", NULL, \
															 RSPAMD_LOG_FUNC,                        \
															 __VA_ARGS__)

static ev_tstamp io_timeout = 30.0;

struct rspamd_control_session;

struct rspamd_control_reply_elt {
	struct rspamd_control_reply reply;
	struct rspamd_worker *worker;
	GQuark wrk_type;
	pid_t wrk_pid;
	rspamd_ev_cb handler;
	gpointer ud;
	int attached_fd;
	struct rspamd_control_command cmd;
	struct rspamd_control_reply_elt *prev, *next;
};

/* Hash table keyed by command id for pending control requests */
KHASH_INIT(rspamd_control_requests, uint64_t, struct rspamd_control_reply_elt *, 1,
		   kh_int64_hash_func, kh_int64_hash_equal);

struct rspamd_control_session {
	int fd;
	struct ev_loop *event_loop;
	struct rspamd_main *rspamd_main;
	struct rspamd_http_connection *conn;
	struct rspamd_control_command cmd;
	struct rspamd_control_reply_elt *replies;
	rspamd_inet_addr_t *addr;
	unsigned int replies_remain;
	gboolean is_reply;
};

static const struct rspamd_control_cmd_match {
	rspamd_ftok_t name;
	enum rspamd_control_type type;
} cmd_matches[] = {
	{.name = {
		 .begin = "/stat",
		 .len = sizeof("/stat") - 1},
	 .type = RSPAMD_CONTROL_STAT},
	{.name = {.begin = "/reload", .len = sizeof("/reload") - 1}, .type = RSPAMD_CONTROL_RELOAD},
	{.name = {.begin = "/reresolve", .len = sizeof("/reresolve") - 1}, .type = RSPAMD_CONTROL_RERESOLVE},
	{.name = {.begin = "/recompile", .len = sizeof("/recompile") - 1}, .type = RSPAMD_CONTROL_RECOMPILE},
	{.name = {.begin = "/fuzzystat", .len = sizeof("/fuzzystat") - 1}, .type = RSPAMD_CONTROL_FUZZY_STAT},
	{.name = {.begin = "/fuzzysync", .len = sizeof("/fuzzysync") - 1}, .type = RSPAMD_CONTROL_FUZZY_SYNC},
	{.name = {.begin = "/fuzzyhash", .len = sizeof("/fuzzyhash") - 1}, .type = RSPAMD_CONTROL_FUZZY_HASH},
	{.name = {.begin = "/compositesstats", .len = sizeof("/compositesstats") - 1}, .type = RSPAMD_CONTROL_COMPOSITES_STATS},
	{.name = {.begin = "/memstat", .len = sizeof("/memstat") - 1}, .type = RSPAMD_CONTROL_MEMORY_STAT},
};

static void rspamd_control_ignore_io_handler(int fd, short what, void *ud);

INIT_LOG_MODULE(control)

void rspamd_control_send_error(struct rspamd_control_session *session,
							   int code, const char *error_msg, ...)
{
	struct rspamd_http_message *msg;
	rspamd_fstring_t *reply;
	va_list args;

	msg = rspamd_http_new_message(HTTP_RESPONSE);

	va_start(args, error_msg);
	msg->status = rspamd_fstring_new();
	rspamd_vprintf_fstring(&msg->status, error_msg, args);
	va_end(args);

	msg->date = time(NULL);
	msg->code = code;
	reply = rspamd_fstring_sized_new(msg->status->len + 16);
	rspamd_printf_fstring(&reply, "{\"error\":\"%V\"}", msg->status);
	rspamd_http_message_set_body_from_fstring_steal(msg, reply);
	rspamd_http_connection_reset(session->conn);
	rspamd_http_connection_write_message(session->conn,
										 msg,
										 NULL,
										 "application/json",
										 session,
										 io_timeout);
}

static void
rspamd_control_send_ucl(struct rspamd_control_session *session,
						ucl_object_t *obj)
{
	struct rspamd_http_message *msg;
	rspamd_fstring_t *reply;

	msg = rspamd_http_new_message(HTTP_RESPONSE);
	msg->date = time(NULL);
	msg->code = 200;
	msg->status = rspamd_fstring_new_init("OK", 2);
	reply = rspamd_fstring_sized_new(BUFSIZ);
	rspamd_ucl_emit_fstring(obj, UCL_EMIT_JSON_COMPACT, &reply);
	rspamd_http_message_set_body_from_fstring_steal(msg, reply);
	rspamd_http_connection_reset(session->conn);
	rspamd_http_connection_write_message(session->conn,
										 msg,
										 NULL,
										 "application/json",
										 session,
										 io_timeout);
}

static void
rspamd_control_connection_close(struct rspamd_control_session *session)
{
	struct rspamd_control_reply_elt *elt, *telt;
	struct rspamd_main *rspamd_main;

	rspamd_main = session->rspamd_main;
	msg_info_main("finished connection from %s",
				  rspamd_inet_address_to_string(session->addr));

	DL_FOREACH_SAFE(session->replies, elt, telt)
	{
		/* Remove from worker's pending hash if still there */
		khash_t(rspamd_control_requests) *h =
			(khash_t(rspamd_control_requests) *) elt->worker->control_events_pending;
		khiter_t k = kh_get(rspamd_control_requests, h, elt->cmd.id);
		if (k != kh_end(h)) {
			kh_del(rspamd_control_requests, h, k);
		}
		g_free(elt);
	}

	rspamd_inet_address_free(session->addr);
	/*
	 * Stop any pending IO watcher before closing the socket; otherwise libev can
	 * abort on ev_io_stop() during final unref in some reload/startup races.
	 */
	rspamd_http_connection_reset(session->conn);
	rspamd_http_connection_unref(session->conn);
	close(session->fd);
	g_free(session);
}

static void
rspamd_control_write_reply(struct rspamd_control_session *session)
{
	ucl_object_t *rep, *cur, *workers;
	struct rspamd_control_reply_elt *elt;
	char tmpbuf[64];
	double total_utime = 0, total_systime = 0;
	struct ucl_parser *parser;
	unsigned int total_conns = 0;
	/* Composites stats aggregation */
	uint64_t total_checked_slow = 0, total_checked_fast = 0, total_matched = 0;
	/* Memory stat aggregation */
	uint64_t total_rss_kb = 0, total_lua_kb = 0, total_mempool_bytes = 0,
			 total_jemalloc_allocated = 0;

	rep = ucl_object_typed_new(UCL_OBJECT);
	workers = ucl_object_typed_new(UCL_OBJECT);

	DL_FOREACH(session->replies, elt)
	{
		/* Skip incompatible worker for fuzzy_stat */
		if ((session->cmd.type == RSPAMD_CONTROL_FUZZY_STAT ||
			 session->cmd.type == RSPAMD_CONTROL_FUZZY_SYNC ||
			 session->cmd.type == RSPAMD_CONTROL_FUZZY_HASH) &&
			elt->wrk_type != g_quark_from_static_string("fuzzy")) {
			continue;
		}

		rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "%P", elt->wrk_pid);
		cur = ucl_object_typed_new(UCL_OBJECT);

		ucl_object_insert_key(cur, ucl_object_fromstring(g_quark_to_string(elt->wrk_type)), "type", 0, false);

		switch (session->cmd.type) {
		case RSPAMD_CONTROL_STAT:
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.stat.conns), "conns", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.stat.utime), "utime", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.stat.systime), "systime", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.stat.uptime), "uptime", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.stat.maxrss), "maxrss", 0, false);

			total_utime += elt->reply.reply.stat.utime;
			total_systime += elt->reply.reply.stat.systime;
			total_conns += elt->reply.reply.stat.conns;

			break;

		case RSPAMD_CONTROL_RELOAD:
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.reload.status), "status", 0, false);
			break;
		case RSPAMD_CONTROL_RECOMPILE:
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.recompile.status), "status", 0, false);
			break;
		case RSPAMD_CONTROL_RERESOLVE:
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.reresolve.status), "status", 0, false);
			break;
		case RSPAMD_CONTROL_FUZZY_STAT:
			if (elt->attached_fd != -1) {
				/* We have some data to parse */
				parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);
				ucl_object_insert_key(cur,
									  ucl_object_fromint(
										  elt->reply.reply.fuzzy_stat.status),
									  "status",
									  0,
									  false);

				if (ucl_parser_add_fd(parser, elt->attached_fd)) {
					ucl_object_insert_key(cur, ucl_parser_get_object(parser),
										  "data", 0, false);
					ucl_parser_free(parser);
				}
				else {

					ucl_object_insert_key(cur, ucl_object_fromstring(ucl_parser_get_error(parser)), "error", 0, false);

					ucl_parser_free(parser);
				}

				ucl_object_insert_key(cur,
									  ucl_object_fromlstring(
										  elt->reply.reply.fuzzy_stat.storage_id,
										  MEMPOOL_UID_LEN - 1),
									  "id",
									  0,
									  false);
			}
			else {
				ucl_object_insert_key(cur,
									  ucl_object_fromstring("missing file"),
									  "error",
									  0,
									  false);
				ucl_object_insert_key(cur,
									  ucl_object_fromint(
										  elt->reply.reply.fuzzy_stat.status),
									  "status",
									  0,
									  false);
			}
			break;
		case RSPAMD_CONTROL_FUZZY_SYNC:
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.fuzzy_sync.status), "status", 0, false);
			break;
		case RSPAMD_CONTROL_FUZZY_HASH:
			ucl_object_insert_key(cur,
								  ucl_object_fromint(elt->reply.reply.fuzzy_hash.status),
								  "status", 0, false);

			if (elt->attached_fd != -1) {
				parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);

				if (ucl_parser_add_fd(parser, elt->attached_fd)) {
					ucl_object_insert_key(cur, ucl_parser_get_object(parser),
										  "data", 0, false);
				}
				else {
					ucl_object_insert_key(cur,
										  ucl_object_fromstring(ucl_parser_get_error(parser)),
										  "error", 0, false);
				}

				ucl_parser_free(parser);
				ucl_object_insert_key(cur,
									  ucl_object_fromlstring(
										  elt->reply.reply.fuzzy_hash.storage_id,
										  MEMPOOL_UID_LEN - 1),
									  "id",
									  0,
									  false);
			}
			break;
		case RSPAMD_CONTROL_COMPOSITES_STATS:
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.composites_stats.checked_slow),
								  "checked_slow", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.composites_stats.checked_fast),
								  "checked_fast", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromint(elt->reply.reply.composites_stats.matched),
								  "matched", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.composites_stats.time_slow_mean),
								  "time_slow_mean", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.composites_stats.time_slow_stddev),
								  "time_slow_stddev", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.composites_stats.time_fast_mean),
								  "time_fast_mean", 0, false);
			ucl_object_insert_key(cur, ucl_object_fromdouble(elt->reply.reply.composites_stats.time_fast_stddev),
								  "time_fast_stddev", 0, false);

			total_checked_slow += elt->reply.reply.composites_stats.checked_slow;
			total_checked_fast += elt->reply.reply.composites_stats.checked_fast;
			total_matched += elt->reply.reply.composites_stats.matched;
			break;
		case RSPAMD_CONTROL_MEMORY_STAT:
			ucl_object_insert_key(cur,
								  ucl_object_fromint(elt->reply.reply.memory_stat.status),
								  "status", 0, false);
			ucl_object_insert_key(cur,
								  ucl_object_fromint(elt->reply.reply.memory_stat.rss_kb),
								  "rss_kb", 0, false);
			ucl_object_insert_key(cur,
								  ucl_object_fromint(elt->reply.reply.memory_stat.lua_kb),
								  "lua_kb", 0, false);
			ucl_object_insert_key(cur,
								  ucl_object_fromint(elt->reply.reply.memory_stat.mempool_bytes),
								  "mempool_bytes", 0, false);
			ucl_object_insert_key(cur,
								  ucl_object_fromint(elt->reply.reply.memory_stat.jemalloc_allocated),
								  "jemalloc_allocated", 0, false);

			if (elt->attached_fd != -1) {
				parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);

				if (ucl_parser_add_fd(parser, elt->attached_fd)) {
					ucl_object_insert_key(cur, ucl_parser_get_object(parser),
										  "data", 0, false);
				}
				else {
					ucl_object_insert_key(cur,
										  ucl_object_fromstring(ucl_parser_get_error(parser)),
										  "error", 0, false);
				}
				ucl_parser_free(parser);
			}

			total_rss_kb += elt->reply.reply.memory_stat.rss_kb;
			total_lua_kb += elt->reply.reply.memory_stat.lua_kb;
			total_mempool_bytes += elt->reply.reply.memory_stat.mempool_bytes;
			total_jemalloc_allocated += elt->reply.reply.memory_stat.jemalloc_allocated;
			break;
		default:
			break;
		}

		if (elt->attached_fd != -1) {
			close(elt->attached_fd);
			elt->attached_fd = -1;
		}

		ucl_object_insert_key(workers, cur, tmpbuf, 0, true);
	}

	ucl_object_insert_key(rep, workers, "workers", 0, false);

	if (session->cmd.type == RSPAMD_CONTROL_STAT) {
		/* Total stats */
		cur = ucl_object_typed_new(UCL_OBJECT);
		ucl_object_insert_key(cur, ucl_object_fromint(total_conns), "conns", 0, false);
		ucl_object_insert_key(cur, ucl_object_fromdouble(total_utime), "utime", 0, false);
		ucl_object_insert_key(cur, ucl_object_fromdouble(total_systime), "systime", 0, false);

		ucl_object_insert_key(rep, cur, "total", 0, false);
	}
	else if (session->cmd.type == RSPAMD_CONTROL_COMPOSITES_STATS) {
		/* Total composites stats */
		cur = ucl_object_typed_new(UCL_OBJECT);
		ucl_object_insert_key(cur, ucl_object_fromint(total_checked_slow), "checked_slow", 0, false);
		ucl_object_insert_key(cur, ucl_object_fromint(total_checked_fast), "checked_fast", 0, false);
		ucl_object_insert_key(cur, ucl_object_fromint(total_matched), "matched", 0, false);

		ucl_object_insert_key(rep, cur, "total", 0, false);
	}
	else if (session->cmd.type == RSPAMD_CONTROL_MEMORY_STAT) {
		/* Total memory stats */
		cur = ucl_object_typed_new(UCL_OBJECT);
		ucl_object_insert_key(cur, ucl_object_fromint(total_rss_kb), "rss_kb", 0, false);
		ucl_object_insert_key(cur, ucl_object_fromint(total_lua_kb), "lua_kb", 0, false);
		ucl_object_insert_key(cur, ucl_object_fromint(total_mempool_bytes),
							  "mempool_bytes", 0, false);
		ucl_object_insert_key(cur, ucl_object_fromint(total_jemalloc_allocated),
							  "jemalloc_allocated", 0, false);

		ucl_object_insert_key(rep, cur, "total", 0, false);
	}

	rspamd_control_send_ucl(session, rep);
	ucl_object_unref(rep);
}

static void
rspamd_control_wrk_io(int fd, short what, gpointer ud)
{
	struct rspamd_control_reply_elt *elt = ud;
	struct rspamd_control_session *session;

	session = elt->ud;

	/*
	 * Note: In the new design, the reply is already read by
	 * rspamd_control_reply_handler() and stored in elt->reply.
	 * We just need to process it here.
	 */
	if (what != EV_READ) {
		/* Timeout waiting */
		msg_warn("timeout waiting reply from %P (%s)",
				 elt->wrk_pid, g_quark_to_string(elt->wrk_type));
	}

	session->replies_remain--;

	if (session->replies_remain == 0) {
		rspamd_control_write_reply(session);
	}
}

static void
rspamd_control_error_handler(struct rspamd_http_connection *conn, GError *err)
{
	struct rspamd_control_session *session = conn->ud;
	struct rspamd_main *rspamd_main;

	rspamd_main = session->rspamd_main;

	if (!session->is_reply) {
		msg_info_main("abnormally closing control connection: %e", err);
		session->is_reply = TRUE;
		rspamd_control_send_error(session, err->code, "%s", err->message);
	}
	else {
		rspamd_control_connection_close(session);
	}
}

static void
rspamd_pending_control_free(struct rspamd_control_reply_elt *rep_elt)
{
	if (rep_elt->attached_fd != -1) {
		close(rep_elt->attached_fd);
	}

	g_free(rep_elt);
}

void *
rspamd_control_pending_new(void)
{
	return kh_init(rspamd_control_requests);
}

void rspamd_control_pending_destroy(void *p)
{
	khash_t(rspamd_control_requests) *h = (khash_t(rspamd_control_requests) *) p;
	khiter_t k;

	if (h == NULL) {
		return;
	}

	for (k = kh_begin(h); k != kh_end(h); ++k) {
		if (kh_exist(h, k)) {
			rspamd_pending_control_free(kh_val(h, k));
		}
	}

	kh_destroy(rspamd_control_requests, h);
}

void rspamd_control_pending_remove_all(void *p)
{
	khash_t(rspamd_control_requests) *h = (khash_t(rspamd_control_requests) *) p;
	khiter_t k;

	if (h == NULL) {
		return;
	}

	for (k = kh_begin(h); k != kh_end(h); ++k) {
		if (kh_exist(h, k)) {
			rspamd_pending_control_free(kh_val(h, k));
		}
	}

	kh_clear(rspamd_control_requests, h);
}

static inline void
rspamd_control_fill_msghdr(struct rspamd_control_command *cmd,
						   int attached_fd, struct msghdr *msg,
						   struct iovec *iov,
						   void *control_buf,
						   size_t control_len)
{
	struct cmsghdr *cmsg;

	memset(msg, 0, sizeof(*msg));

	/* Attach fd to the message */
	if (attached_fd != -1) {
		memset(control_buf, 0, control_len);
		msg->msg_control = control_buf;
		msg->msg_controllen = control_len;
		cmsg = CMSG_FIRSTHDR(msg);
		cmsg->cmsg_level = SOL_SOCKET;
		cmsg->cmsg_type = SCM_RIGHTS;
		cmsg->cmsg_len = CMSG_LEN(sizeof(int));
		memcpy(CMSG_DATA(cmsg), &attached_fd, sizeof(int));
	}

	iov->iov_base = cmd;
	iov->iov_len = sizeof(*cmd);
	msg->msg_iov = iov;
	msg->msg_iovlen = 1;
}

/* Handler for control replies on main side - dispatches by ID */
static void
rspamd_control_reply_handler(EV_P_ ev_io *w, int revents)
{
	struct rspamd_worker *wrk = (struct rspamd_worker *) w->data;
	khash_t(rspamd_control_requests) *h =
		(khash_t(rspamd_control_requests) *) wrk->control_events_pending;
	struct rspamd_control_reply rep;
	struct rspamd_control_reply_elt *elt;
	struct rspamd_main *rspamd_main = wrk->srv;
	struct msghdr msg;
	struct iovec iov;
	unsigned char fdspace[CMSG_SPACE(sizeof(int))];
	int rfd;
	gssize r;
	khiter_t k;

	for (;;) {
		rfd = -1;
		memset(&msg, 0, sizeof(msg));
		msg.msg_control = fdspace;
		msg.msg_controllen = sizeof(fdspace);
		iov.iov_base = &rep;
		iov.iov_len = sizeof(rep);
		msg.msg_iov = &iov;
		msg.msg_iovlen = 1;
		r = recvmsg(w->fd, &msg, 0);

		if (r > 0 && msg.msg_controllen >= CMSG_LEN(sizeof(int))) {
			rfd = *(int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
		}

		if (r == -1) {
			if (errno == EAGAIN || errno == EWOULDBLOCK) {
				/* No more data */
				break;
			}
			msg_err_main("cannot read control reply from worker %P: %s",
						 wrk->pid, strerror(errno));
			break;
		}

		if (r == 0) {
			/* Connection closed */
			msg_debug_control("control pipe closed for worker %P", wrk->pid);
			ev_io_stop(EV_A_ w);
			if (rfd != -1) {
				close(rfd);
			}
			break;
		}

		if (r != (gssize) sizeof(rep)) {
			msg_err_main("incomplete control reply from worker %P: %d != %d",
						 wrk->pid, (int) r, (int) sizeof(rep));
			if (rfd != -1) {
				close(rfd);
			}
			continue;
		}

		/* Look up request by ID */
		k = kh_get(rspamd_control_requests, h, rep.id);
		if (k == kh_end(h)) {
			msg_warn_main("received control reply for unknown request id %uL from worker %P",
						  rep.id, wrk->pid);
			if (rfd != -1) {
				close(rfd);
			}
			continue;
		}

		elt = kh_val(h, k);
		kh_del(rspamd_control_requests, h, k);

		msg_debug_control("received reply for command %d id %uL from worker %P(%s)",
						  (int) rep.type, rep.id, wrk->pid, g_quark_to_string(wrk->type));

		if (rfd != -1) {
			elt->attached_fd = rfd;
		}

		/* Copy reply to element and call handler */
		memcpy(&elt->reply, &rep, sizeof(rep));
		if (elt->handler) {
			elt->handler(w->fd, EV_READ, elt);
		}
	}

	/* Stop watcher if no more pending requests */
	if (kh_size(h) == 0) {
		ev_io_stop(EV_A_ w);
	}
}

static struct rspamd_control_reply_elt *
rspamd_control_broadcast_cmd(struct rspamd_main *rspamd_main,
							 struct rspamd_control_command *cmd,
							 int attached_fd,
							 rspamd_ev_cb handler,
							 gpointer ud,
							 pid_t except_pid)
{
	GHashTableIter it;
	struct rspamd_worker *wrk;
	struct rspamd_control_reply_elt *rep_elt, *res = NULL;
	khash_t(rspamd_control_requests) * h;
	gpointer k, v;
	gssize r;
	khiter_t kh;
	int ret;

	g_hash_table_iter_init(&it, rspamd_main->workers);

	while (g_hash_table_iter_next(&it, &k, &v)) {
		wrk = v;

		/* No control pipe */
		if (wrk->control_pipe[0] == -1) {
			continue;
		}

		if (except_pid != 0 && wrk->pid == except_pid) {
			continue;
		}

		/* Worker is terminating, do not bother sending stuff */
		if (wrk->state == rspamd_worker_state_terminating) {
			continue;
		}

		h = (khash_t(rspamd_control_requests) *) wrk->control_events_pending;

		/* Generate unique ID for this command */
		cmd->id = ottery_rand_uint64();

		rep_elt = g_malloc0(sizeof(*rep_elt));
		rep_elt->worker = wrk;
		rep_elt->wrk_pid = wrk->pid;
		rep_elt->wrk_type = wrk->type;
		rep_elt->ud = ud;
		rep_elt->handler = handler;
		rep_elt->attached_fd = -1;
		memcpy(&rep_elt->cmd, cmd, sizeof(*cmd));

		/* Send command immediately */
		struct msghdr msg;
		struct iovec iov;
		unsigned char fdspace[CMSG_SPACE(sizeof(int))];

		rspamd_control_fill_msghdr(cmd, attached_fd, &msg, &iov, fdspace, sizeof(fdspace));
		r = sendmsg(wrk->control_pipe[0], &msg, 0);

		if (r == sizeof(*cmd)) {
			/* Add to hash for reply matching */
			kh = kh_put(rspamd_control_requests, h, cmd->id, &ret);
			if (ret < 0) {
				msg_err_main("cannot add control request to hash table");
				g_free(rep_elt);
				continue;
			}
			if (ret == 0) {
				/* Key already exists - ID collision (extremely unlikely with 64-bit random) */
				msg_warn_main("control command ID collision for %uL, previous request will be orphaned",
							  cmd->id);
				/* Free the old entry to prevent memory leak */
				struct rspamd_control_reply_elt *old_elt = kh_val(h, kh);
				if (old_elt) {
					g_free(old_elt);
				}
			}
			kh_val(h, kh) = rep_elt;

			/* Start control reply watcher if not already active */
			if (!ev_is_active(&wrk->control_ev)) {
				wrk->control_ev.data = wrk;
				ev_io_init(&wrk->control_ev, rspamd_control_reply_handler,
						   wrk->control_pipe[0], EV_READ);
				ev_io_start(rspamd_main->event_loop, &wrk->control_ev);
			}

			DL_APPEND(res, rep_elt);
			msg_debug_control("sent command %d id %uL to worker %P(%s), fd: %d",
							  (int) cmd->type, cmd->id,
							  wrk->pid,
							  g_quark_to_string(wrk->type),
							  wrk->control_pipe[0]);
		}
		else {
			msg_err_main("cannot write command %d to the worker %P(%s), fd: %d: %s",
						 (int) cmd->type,
						 wrk->pid,
						 g_quark_to_string(wrk->type),
						 wrk->control_pipe[0],
						 strerror(errno));
			g_free(rep_elt);
		}
	}

	return res;
}

void rspamd_control_broadcast_srv_cmd(struct rspamd_main *rspamd_main,
									  struct rspamd_control_command *cmd,
									  pid_t except_pid)
{
	rspamd_control_broadcast_cmd(rspamd_main, cmd, -1,
								 rspamd_control_ignore_io_handler, NULL, except_pid);
}

static int
rspamd_control_finish_handler(struct rspamd_http_connection *conn,
							  struct rspamd_http_message *msg)
{
	struct rspamd_control_session *session = conn->ud;
	rspamd_ftok_t srch;
	unsigned int i;
	gboolean found = FALSE;
	struct rspamd_control_reply_elt *cur;


	if (!session->is_reply) {
		if (msg->url == NULL) {
			rspamd_control_connection_close(session);

			return 0;
		}

		srch.begin = msg->url->str;
		srch.len = msg->url->len;

		/* Some commands carry arguments in the query string */
		const char *qpos = memchr(msg->url->str, '?', msg->url->len);
		rspamd_ftok_t query = {.begin = NULL, .len = 0};

		if (qpos != NULL) {
			query.begin = qpos + 1;
			query.len = msg->url->len - (qpos - msg->url->str) - 1;
			srch.len = qpos - msg->url->str;
		}

		session->is_reply = TRUE;

		for (i = 0; i < G_N_ELEMENTS(cmd_matches); i++) {
			if (rspamd_ftok_casecmp(&srch, &cmd_matches[i].name) == 0) {
				session->cmd.type = cmd_matches[i].type;
				found = TRUE;
				break;
			}
		}

		if (found && session->cmd.type == RSPAMD_CONTROL_FUZZY_HASH) {
			/* Parse digest=<128 hex chars> */
			static const char digest_pfx[] = "digest=";
			gsize hexlen = sizeof(session->cmd.cmd.fuzzy_hash.digest) * 2;

			if (query.len == sizeof(digest_pfx) - 1 + hexlen &&
				memcmp(query.begin, digest_pfx, sizeof(digest_pfx) - 1) == 0 &&
				rspamd_decode_hex_buf(query.begin + sizeof(digest_pfx) - 1, hexlen,
									  session->cmd.cmd.fuzzy_hash.digest,
									  sizeof(session->cmd.cmd.fuzzy_hash.digest)) != -1) {
				/* Parsed fine */
			}
			else {
				rspamd_control_send_error(session, 400,
										  "fuzzyhash requires ?digest=<%z hex characters>", hexlen);
				return 0;
			}
		}

		if (!found) {
			rspamd_control_send_error(session, 404, "Command not defined");
		}
		else {
			/* Send command to all workers */
			session->replies = rspamd_control_broadcast_cmd(
				session->rspamd_main, &session->cmd, -1,
				rspamd_control_wrk_io, session, 0);

			DL_FOREACH(session->replies, cur)
			{
				session->replies_remain++;
			}
		}
	}
	else {
		rspamd_control_connection_close(session);
	}


	return 0;
}

void rspamd_control_process_client_socket(struct rspamd_main *rspamd_main,
										  int fd, rspamd_inet_addr_t *addr)
{
	struct rspamd_control_session *session;

	session = g_malloc0(sizeof(*session));

	session->fd = fd;
	session->conn = rspamd_http_connection_new_server(rspamd_main->http_ctx,
													  fd,
													  NULL,
													  rspamd_control_error_handler,
													  rspamd_control_finish_handler,
													  0);
	/* Control commands are URL based, so the body is not used at all */
	rspamd_http_connection_set_max_size(session->conn,
										rspamd_main->cfg->max_message);
	session->rspamd_main = rspamd_main;
	session->addr = addr;
	session->event_loop = rspamd_main->event_loop;
	rspamd_http_connection_read_message(session->conn, session,
										io_timeout);
}

struct rspamd_worker_control_data {
	ev_io io_ev;
	struct rspamd_worker *worker;
	struct ev_loop *ev_base;
	struct {
		rspamd_worker_control_handler handler;
		gpointer ud;
	} handlers[RSPAMD_CONTROL_MAX];
};

static void
rspamd_control_default_cmd_handler(int fd,
								   int attached_fd,
								   struct rspamd_worker_control_data *cd,
								   struct rspamd_control_command *cmd)
{
	struct rspamd_control_reply rep;
	gssize r;
	struct rusage rusg;
	struct rspamd_config *cfg;
	struct rspamd_main *rspamd_main;

	memset(&rep, 0, sizeof(rep));
	rep.type = cmd->type;
	rep.id = cmd->id;
	rspamd_main = cd->worker->srv;

	switch (cmd->type) {
	case RSPAMD_CONTROL_MEMORY_STAT:
		/*
		 * Memory stat needs to attach a file descriptor with the full UCL
		 * dump, so it has its own send path and bypasses the trailing
		 * write() below.
		 */
		rspamd_memory_stat_collect_and_send(rspamd_main, cd->worker, fd, cmd);
		if (attached_fd != -1) {
			close(attached_fd);
		}
		return;
	case RSPAMD_CONTROL_STAT:
		if (getrusage(RUSAGE_SELF, &rusg) == -1) {
			msg_err_main("cannot get rusage stats: %s",
						 strerror(errno));
		}
		else {
			rep.reply.stat.utime = tv_to_double(&rusg.ru_utime);
			rep.reply.stat.systime = tv_to_double(&rusg.ru_stime);
			rep.reply.stat.maxrss = rusg.ru_maxrss;
		}

		rep.reply.stat.conns = cd->worker->nconns;
		rep.reply.stat.uptime = rspamd_get_calendar_ticks() - cd->worker->start_time;
		break;
	case RSPAMD_CONTROL_RELOAD:
	case RSPAMD_CONTROL_RECOMPILE:
	case RSPAMD_CONTROL_HYPERSCAN_LOADED:
	case RSPAMD_CONTROL_MONITORED_CHANGE:
	case RSPAMD_CONTROL_FUZZY_STAT:
	case RSPAMD_CONTROL_FUZZY_SYNC:
	case RSPAMD_CONTROL_FUZZY_HASH:
	case RSPAMD_CONTROL_LOG_PIPE:
	case RSPAMD_CONTROL_CHILD_CHANGE:
	case RSPAMD_CONTROL_FUZZY_BLOCKED:
		break;
	case RSPAMD_CONTROL_WORKERS_SPAWNED:
		rep.reply.workers_spawned.status = 0;
		break;
	case RSPAMD_CONTROL_COMPOSITES_STATS:
		if (cd->worker->srv->cfg && cd->worker->srv->cfg->composites_manager) {
			struct rspamd_composites_stats_export comp_stats;
			rspamd_composites_get_stats(cd->worker->srv->cfg->composites_manager, &comp_stats);
			rep.reply.composites_stats.checked_slow = comp_stats.checked_slow;
			rep.reply.composites_stats.checked_fast = comp_stats.checked_fast;
			rep.reply.composites_stats.matched = comp_stats.matched;
			rep.reply.composites_stats.time_slow_mean = comp_stats.time_slow_mean;
			rep.reply.composites_stats.time_slow_stddev = comp_stats.time_slow_stddev;
			rep.reply.composites_stats.time_fast_mean = comp_stats.time_fast_mean;
			rep.reply.composites_stats.time_fast_stddev = comp_stats.time_fast_stddev;
		}
		break;
	case RSPAMD_CONTROL_RERESOLVE:
		if (cd->worker->srv->cfg) {
			REF_RETAIN(cd->worker->srv->cfg);
			cfg = cd->worker->srv->cfg;

			if (cfg->ups_ctx) {
				msg_info_config("reresolving upstreams");
				rspamd_upstream_reresolve(cfg->ups_ctx);
			}

			rep.reply.reresolve.status = 0;
			CFG_REF_RELEASE(cfg);
		}
		else {
			rep.reply.reresolve.status = EINVAL;
		}
		break;
	default:
		break;
	}

	r = write(fd, &rep, sizeof(rep));

	if (r != sizeof(rep)) {
		msg_err_main("cannot write reply to the control socket: %s",
					 strerror(errno));
	}

	if (attached_fd != -1) {
		close(attached_fd);
	}
}

static void
rspamd_control_default_worker_handler(EV_P_ ev_io *w, int revents)
{
	struct rspamd_worker_control_data *cd =
		(struct rspamd_worker_control_data *) w->data;
	static struct rspamd_control_command cmd;
	static struct msghdr msg;
	static struct iovec iov;
	unsigned char fdspace[CMSG_SPACE(sizeof(int))];
	int rfd = -1;
	gssize r;

	iov.iov_base = &cmd;
	iov.iov_len = sizeof(cmd);
	memset(&msg, 0, sizeof(msg));
	msg.msg_control = fdspace;
	msg.msg_controllen = sizeof(fdspace);
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

	r = recvmsg(w->fd, &msg, 0);

	if (r == -1) {
		if (errno != EAGAIN && errno != EINTR) {
			if (errno != ECONNRESET) {
				/*
				 * In case of connection reset it means that main process
				 * has died, so do not pollute logs
				 */
				msg_err("cannot read request from the control socket: %s",
						strerror(errno));
			}
			ev_io_stop(cd->ev_base, &cd->io_ev);
			close(w->fd);
		}
	}
	else if (r < (int) sizeof(cmd)) {
		msg_err("short read of control command: %d of %d", (int) r,
				(int) sizeof(cmd));

		if (r == 0) {
			ev_io_stop(cd->ev_base, &cd->io_ev);
			close(w->fd);
		}
	}
	else if ((int) cmd.type >= 0 && cmd.type < RSPAMD_CONTROL_MAX) {

		if (msg.msg_controllen >= CMSG_LEN(sizeof(int))) {
			rfd = *(int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
		}

		if (cd->handlers[cmd.type].handler) {
			cd->handlers[cmd.type].handler(cd->worker->srv,
										   cd->worker,
										   w->fd,
										   rfd,
										   &cmd,
										   cd->handlers[cmd.type].ud);
		}
		else {
			rspamd_control_default_cmd_handler(w->fd, rfd, cd, &cmd);
		}
	}
	else {
		msg_err("unknown command: %d", (int) cmd.type);
	}
}

void rspamd_control_worker_add_default_cmd_handlers(struct rspamd_worker *worker,
													struct ev_loop *ev_base)
{
	struct rspamd_worker_control_data *cd;

	cd = g_malloc0(sizeof(*cd));
	cd->worker = worker;
	cd->ev_base = ev_base;

	cd->io_ev.data = cd;
	ev_io_init(&cd->io_ev, rspamd_control_default_worker_handler,
			   worker->control_pipe[1], EV_READ);
	ev_io_start(ev_base, &cd->io_ev);

	worker->control_data = cd;
}

/**
 * Register custom handler for a specific control command for this worker
 */
void rspamd_control_worker_add_cmd_handler(struct rspamd_worker *worker,
										   enum rspamd_control_type type,
										   rspamd_worker_control_handler handler,
										   gpointer ud)
{
	struct rspamd_worker_control_data *cd;

	g_assert(type >= 0 && type < RSPAMD_CONTROL_MAX);
	g_assert(handler != NULL);
	g_assert(worker->control_data != NULL);

	cd = worker->control_data;
	cd->handlers[type].handler = handler;
	cd->handlers[type].ud = ud;
}

struct rspamd_srv_reply_data {
	struct rspamd_worker *worker;
	struct rspamd_main *srv;
	int fd;
	struct rspamd_srv_reply rep;
};

static void
rspamd_control_ignore_io_handler(int fd, short what, void *ud)
{
	struct rspamd_control_reply_elt *elt =
		(struct rspamd_control_reply_elt *) ud;

	/* Reply already read by rspamd_control_reply_handler and stored in elt->reply */
	/* Just free the element - nothing to do with the reply */
	g_free(elt);
}

static void
rspamd_control_log_pipe_io_handler(int fd, short what, void *ud)
{
	struct rspamd_control_reply_elt *elt =
		(struct rspamd_control_reply_elt *) ud;

	/* Reply already read by rspamd_control_reply_handler and stored in elt->reply */
	/* Just free the element - nothing to do with the reply */
	g_free(elt);
}

static void
rspamd_control_handle_on_fork(struct rspamd_srv_command *cmd,
							  struct rspamd_main *srv)
{
	struct rspamd_worker *parent, *child;

	parent = g_hash_table_lookup(srv->workers,
								 GSIZE_TO_POINTER(cmd->cmd.on_fork.ppid));

	if (parent == NULL) {
		msg_err("cannot find parent for a forked process %P (%P child)",
				cmd->cmd.on_fork.ppid, cmd->cmd.on_fork.cpid);

		return;
	}

	if (cmd->cmd.on_fork.state == child_dead) {
		/* We need to remove stale worker */
		child = g_hash_table_lookup(srv->workers,
									GSIZE_TO_POINTER(cmd->cmd.on_fork.cpid));

		if (child == NULL) {
			msg_err("cannot find child for a forked process %P (%P parent)",
					cmd->cmd.on_fork.cpid, cmd->cmd.on_fork.ppid);

			return;
		}

		REF_RELEASE(child->cf);
		g_hash_table_remove(srv->workers,
							GSIZE_TO_POINTER(cmd->cmd.on_fork.cpid));
		rspamd_control_pending_destroy(child->control_events_pending);
		g_free(child);
	}
	else {
		child = g_malloc0(sizeof(struct rspamd_worker));
		child->srv = srv;
		child->type = parent->type;
		child->pid = cmd->cmd.on_fork.cpid;
		child->srv_pipe[0] = -1;
		child->srv_pipe[1] = -1;
		child->control_pipe[0] = -1;
		child->control_pipe[1] = -1;
		child->cf = parent->cf;
		child->ppid = parent->pid;
		REF_RETAIN(child->cf);
		child->control_events_pending = rspamd_control_pending_new();
		g_hash_table_insert(srv->workers,
							GSIZE_TO_POINTER(cmd->cmd.on_fork.cpid), child);
	}
}

static void
rspamd_fill_health_reply(struct rspamd_main *srv, struct rspamd_srv_reply *rep)
{
	GHashTableIter it;
	gpointer k, v;

	memset(&rep->reply.health, 0, sizeof(rep->reply));
	g_hash_table_iter_init(&it, srv->workers);

	while (g_hash_table_iter_next(&it, &k, &v)) {
		struct rspamd_worker *wrk = (struct rspamd_worker *) v;

		if (wrk->hb.nbeats < 0) {
			rep->reply.health.workers_hb_lost++;
		}
		else if (rspamd_worker_is_scanner(wrk)) {
			rep->reply.health.scanners_count++;
		}

		rep->reply.health.workers_count++;
	}

	rep->reply.status = (g_hash_table_size(srv->workers) > 0);
}


static void
rspamd_srv_handler(EV_P_ ev_io *w, int revents)
{
	struct rspamd_worker *worker;
	static struct rspamd_srv_command cmd;
	struct rspamd_main *rspamd_main;
	struct rspamd_srv_reply_data *rdata;
	struct msghdr msg;
	struct cmsghdr *cmsg;
	static struct iovec iov;
	unsigned char fdspace[CMSG_SPACE(sizeof(int))];
	int *spair, rfd = -1;
	char *nid;
	struct rspamd_control_command wcmd;
	gssize r;

	if (revents == EV_READ) {
		worker = (struct rspamd_worker *) w->data;
		rspamd_main = worker->srv;
		iov.iov_base = &cmd;
		iov.iov_len = sizeof(cmd);
		memset(&msg, 0, sizeof(msg));
		msg.msg_control = fdspace;
		msg.msg_controllen = sizeof(fdspace);
		msg.msg_iov = &iov;
		msg.msg_iovlen = 1;

		r = recvmsg(w->fd, &msg, 0);

		if (r == -1) {
			if (errno != EAGAIN) {
				msg_err_main("cannot read from worker's srv pipe: %s",
							 strerror(errno));
			}
			else {
				return;
			}
		}
		else if (r == 0) {
			/*
			 * Usually this means that a worker is dead, so do not try to read
			 * anything
			 */
			msg_err_main("cannot read from worker's srv pipe connection closed; command = %s",
						 rspamd_srv_command_to_string(cmd.type));
			ev_io_stop(EV_A_ w);
		}
		else if (r != sizeof(cmd)) {
			msg_err_main("cannot read from worker's srv pipe incomplete command: %d != %d; command = %s",
						 (int) r, (int) sizeof(cmd), rspamd_srv_command_to_string(cmd.type));
		}
		else {
			rdata = g_malloc0(sizeof(*rdata));
			rdata->worker = worker;
			rdata->srv = rspamd_main;
			rdata->rep.id = cmd.id;
			rdata->rep.type = cmd.type;
			rdata->fd = -1;
			worker->tmp_data = rdata;

			if (msg.msg_controllen >= CMSG_LEN(sizeof(int))) {
				rfd = *(int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
			}

			switch (cmd.type) {
			case RSPAMD_SRV_SOCKETPAIR:
				spair = g_hash_table_lookup(rspamd_main->spairs, cmd.cmd.spair.pair_id);
				if (spair == NULL) {
					spair = g_malloc(sizeof(int) * 2);

					if (rspamd_socketpair(spair, cmd.cmd.spair.af) == -1) {
						rdata->rep.reply.spair.code = errno;
						msg_err_main("cannot create socket pair: %s", strerror(errno));
					}
					else {
						nid = g_malloc(sizeof(cmd.cmd.spair.pair_id));
						memcpy(nid, cmd.cmd.spair.pair_id,
							   sizeof(cmd.cmd.spair.pair_id));
						g_hash_table_insert(rspamd_main->spairs, nid, spair);
						rdata->rep.reply.spair.code = 0;
						rdata->fd = cmd.cmd.spair.pair_num ? spair[1] : spair[0];
					}
				}
				else {
					rdata->rep.reply.spair.code = 0;
					rdata->fd = cmd.cmd.spair.pair_num ? spair[1] : spair[0];
				}
				break;
			case RSPAMD_SRV_HYPERSCAN_LOADED:
#ifdef WITH_HYPERSCAN
				/* Main process just broadcasts cache update events to workers */
				if (cmd.cmd.hs_loaded.scope[0] != '\0') {
					/* Scoped loading */
					const char *scope = cmd.cmd.hs_loaded.scope;
					msg_info_main("received scoped hyperscan cache loaded for scope: %s", scope);

					/* Broadcast scoped command to all workers */
					memset(&wcmd, 0, sizeof(wcmd));
					wcmd.type = RSPAMD_CONTROL_HYPERSCAN_LOADED;
					rspamd_strlcpy(wcmd.cmd.hs_loaded.scope,
								   cmd.cmd.hs_loaded.scope,
								   sizeof(wcmd.cmd.hs_loaded.scope));
					wcmd.cmd.hs_loaded.forced = cmd.cmd.hs_loaded.forced;
					rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
												 rspamd_control_ignore_io_handler, NULL, worker->pid);
				}
				else {
					/* Legacy full cache update */

					/*
					 * Only enable cleanup if hyperscan is not disabled.
					 * When disable_hyperscan is set, workers won't load databases
					 * and thus won't notify main about known files via
					 * RSPAMD_SRV_NOTICE_HYPERSCAN_CACHE. Enabling cleanup in that
					 * case would cause main to delete all cached files on exit.
					 */
					if (!rspamd_main->cfg->disable_hyperscan) {
						rspamd_hyperscan_notice_loaded();
					}

					msg_info_main("received hyperscan cache loaded");

					/* Broadcast command to all workers */
					memset(&wcmd, 0, sizeof(wcmd));
					wcmd.type = RSPAMD_CONTROL_HYPERSCAN_LOADED;
					wcmd.cmd.hs_loaded.forced = cmd.cmd.hs_loaded.forced;
					wcmd.cmd.hs_loaded.scope[0] = '\0'; /* Empty scope for legacy */
					rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
												 rspamd_control_ignore_io_handler, NULL, worker->pid);
				}
#endif
				break;
			case RSPAMD_SRV_MULTIPATTERN_LOADED:
#ifdef WITH_HYPERSCAN
				msg_info_main("received multipattern loaded notification for '%s'",
							  cmd.cmd.mp_loaded.name);

				/* Broadcast command to all workers */
				memset(&wcmd, 0, sizeof(wcmd));
				wcmd.type = RSPAMD_CONTROL_MULTIPATTERN_LOADED;
				rspamd_strlcpy(wcmd.cmd.mp_loaded.name,
							   cmd.cmd.mp_loaded.name,
							   sizeof(wcmd.cmd.mp_loaded.name));
				rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
											 rspamd_control_ignore_io_handler, NULL, worker->pid);
#endif
				break;
			case RSPAMD_SRV_REGEXP_MAP_LOADED:
#ifdef WITH_HYPERSCAN
				msg_info_main("received regexp map loaded notification for '%s'",
							  cmd.cmd.re_map_loaded.name);

				/* Broadcast command to all workers */
				memset(&wcmd, 0, sizeof(wcmd));
				wcmd.type = RSPAMD_CONTROL_REGEXP_MAP_LOADED;
				memcpy(wcmd.cmd.re_map_loaded.digest,
					   cmd.cmd.re_map_loaded.digest,
					   sizeof(wcmd.cmd.re_map_loaded.digest));
				rspamd_strlcpy(wcmd.cmd.re_map_loaded.name,
							   cmd.cmd.re_map_loaded.name,
							   sizeof(wcmd.cmd.re_map_loaded.name));
				rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
											 rspamd_control_ignore_io_handler, NULL, worker->pid);
#endif
				break;
			case RSPAMD_SRV_MONITORED_CHANGE:
				/* Broadcast command to all workers */
				memset(&wcmd, 0, sizeof(wcmd));
				wcmd.type = RSPAMD_CONTROL_MONITORED_CHANGE;
				rspamd_strlcpy(wcmd.cmd.monitored_change.tag,
							   cmd.cmd.monitored_change.tag,
							   sizeof(wcmd.cmd.monitored_change.tag));
				wcmd.cmd.monitored_change.alive = cmd.cmd.monitored_change.alive;
				wcmd.cmd.monitored_change.sender = cmd.cmd.monitored_change.sender;
				rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
											 rspamd_control_ignore_io_handler, NULL, 0);
				break;
			case RSPAMD_SRV_LOG_PIPE:
				memset(&wcmd, 0, sizeof(wcmd));
				wcmd.type = RSPAMD_CONTROL_LOG_PIPE;
				wcmd.cmd.log_pipe.type = cmd.cmd.log_pipe.type;
				rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
											 rspamd_control_log_pipe_io_handler, NULL, 0);
				break;
			case RSPAMD_SRV_ON_FORK:
				rdata->rep.reply.on_fork.status = 0;
				rspamd_control_handle_on_fork(&cmd, rspamd_main);
				break;
			case RSPAMD_SRV_HEARTBEAT:
				worker->hb.last_event = ev_time();
				rdata->rep.reply.heartbeat.status = 0;
				break;
			case RSPAMD_SRV_BUSY:
				worker->hb.is_busy = cmd.cmd.busy.is_busy;
				if (cmd.cmd.busy.is_busy) {
					rspamd_strlcpy(worker->hb.busy_reason, cmd.cmd.busy.reason,
								   sizeof(worker->hb.busy_reason));
					rspamd_default_log_function(G_LOG_LEVEL_DEBUG,
												rspamd_main->server_pool->tag.tagname,
												rspamd_main->server_pool->tag.uid,
												RSPAMD_LOG_FUNC,
												"worker type %s with pid %P marked as busy: %s",
												g_quark_to_string(worker->type), worker->pid,
												worker->hb.busy_reason);
				}
				else {
					rspamd_default_log_function(G_LOG_LEVEL_DEBUG,
												rspamd_main->server_pool->tag.tagname,
												rspamd_main->server_pool->tag.uid,
												RSPAMD_LOG_FUNC,
												"worker type %s with pid %P finished: %s",
												g_quark_to_string(worker->type), worker->pid,
												worker->hb.busy_reason);
					worker->hb.busy_reason[0] = '\0';
				}
				break;
			case RSPAMD_SRV_RECOMPILE_REQUEST:
				msg_info_main("received hyperscan recompile request from worker %P",
							  worker->pid);
				rdata->rep.reply.recompile_request.status = 0;
				memset(&wcmd, 0, sizeof(wcmd));
				wcmd.type = RSPAMD_CONTROL_RECOMPILE;
				rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
											 rspamd_control_ignore_io_handler, NULL, 0);
				break;
			case RSPAMD_SRV_HEALTH:
				rspamd_fill_health_reply(rspamd_main, &rdata->rep);
				break;
			case RSPAMD_SRV_NOTICE_HYPERSCAN_CACHE:
#ifdef WITH_HYPERSCAN
				if (rspamd_main->cfg->hs_cache_dir != NULL &&
					cmd.cmd.hyperscan_cache_file.filename[0] != '\0') {
					char full_path[PATH_MAX];
					rspamd_snprintf(full_path, sizeof(full_path), "%s/%s",
									rspamd_main->cfg->hs_cache_dir,
									cmd.cmd.hyperscan_cache_file.filename);
					rspamd_hyperscan_notice_known(full_path);
				}
#endif
				rdata->rep.reply.hyperscan_cache_file.unused = 0;
				break;
			case RSPAMD_SRV_FUZZY_BLOCKED:
				/* Broadcast command to all workers */
				memset(&wcmd, 0, sizeof(wcmd));
				wcmd.type = RSPAMD_CONTROL_FUZZY_BLOCKED;
				/* Ensure that memcpy is safe */
				G_STATIC_ASSERT(sizeof(wcmd.cmd.fuzzy_blocked) == sizeof(cmd.cmd.fuzzy_blocked));
				memcpy(&wcmd.cmd.fuzzy_blocked, &cmd.cmd.fuzzy_blocked, sizeof(wcmd.cmd.fuzzy_blocked));
				rspamd_control_broadcast_cmd(rspamd_main, &wcmd, rfd,
											 rspamd_control_ignore_io_handler, NULL, worker->pid);
				break;
			case RSPAMD_SRV_WORKERS_SPAWNED:
				/* No need to broadcast, this is just a notification from main to specific workers */
				rdata->rep.reply.workers_spawned.status = 0;
				break;
			default:
				msg_err_main("unknown command type: %d", cmd.type);
				break;
			}

			if (rfd != -1) {
				/* Close our copy to avoid descriptors leak */
				close(rfd);
			}

			/* Now plan write event and send data back */
			w->data = rdata;
			ev_io_stop(EV_A_ w);
			ev_io_set(w, worker->srv_pipe[0], EV_WRITE);
			ev_io_start(EV_A_ w);
		}
	}
	else if (revents == EV_WRITE) {
		rdata = (struct rspamd_srv_reply_data *) w->data;
		worker = rdata->worker;
		worker->tmp_data = NULL; /* Avoid race */
		rspamd_main = rdata->srv;

		memset(&msg, 0, sizeof(msg));

		/* Attach fd to the message */
		if (rdata->fd != -1) {
			memset(fdspace, 0, sizeof(fdspace));
			msg.msg_control = fdspace;
			msg.msg_controllen = sizeof(fdspace);
			cmsg = CMSG_FIRSTHDR(&msg);
			cmsg->cmsg_level = SOL_SOCKET;
			cmsg->cmsg_type = SCM_RIGHTS;
			cmsg->cmsg_len = CMSG_LEN(sizeof(int));
			memcpy(CMSG_DATA(cmsg), &rdata->fd, sizeof(int));
		}

		iov.iov_base = &rdata->rep;
		iov.iov_len = sizeof(rdata->rep);
		msg.msg_iov = &iov;
		msg.msg_iovlen = 1;

		r = sendmsg(w->fd, &msg, 0);

		if (r == -1) {
			if (errno == EAGAIN || errno == EWOULDBLOCK) {
				/* Socket buffer full, retry on next event loop iteration.
				 * Keep watcher on EV_WRITE and keep rdata intact.
				 */
				return;
			}
			msg_err_main("cannot write to worker's srv pipe when writing reply: %s; command = %s",
						 strerror(errno), rspamd_srv_command_to_string(rdata->rep.type));
		}
		else if (r != sizeof(rdata->rep)) {
			msg_err_main("cannot write to worker's srv pipe: %d != %d; command = %s",
						 (int) r, (int) sizeof(rdata->rep),
						 rspamd_srv_command_to_string(rdata->rep.type));
		}

		g_free(rdata);
		w->data = worker;
		ev_io_stop(EV_A_ w);
		ev_io_set(w, worker->srv_pipe[0], EV_READ);
		ev_io_start(EV_A_ w);
	}
}

void rspamd_srv_start_watching(struct rspamd_main *srv,
							   struct rspamd_worker *worker,
							   struct ev_loop *ev_base)
{
	g_assert(worker != NULL);

	worker->tmp_data = NULL;
	worker->srv_ev.data = worker;
	ev_io_init(&worker->srv_ev, rspamd_srv_handler, worker->srv_pipe[0], EV_READ);
	ev_io_start(ev_base, &worker->srv_ev);
}

/*
 * Worker-side srv_pipe infrastructure.
 * Uses a single watcher with ID-keyed hash for proper reply matching.
 */

struct rspamd_srv_request_data {
	uint64_t id;
	struct rspamd_srv_command cmd;
	int attached_fd;
	rspamd_srv_reply_handler handler;
	gpointer ud;
	gboolean sent;                        /* true if command has been sent */
	struct rspamd_srv_request_data *next; /* for send queue */
};

/* Hash table keyed by command id */
KHASH_INIT(rspamd_srv_requests, uint64_t, struct rspamd_srv_request_data *, 1,
		   kh_int64_hash_func, kh_int64_hash_equal);

struct rspamd_srv_pipe_ctx {
	ev_io io_ev;
	struct rspamd_worker *worker;
	struct ev_loop *ev_base;
	khash_t(rspamd_srv_requests) * requests;    /* pending requests by id */
	struct rspamd_srv_request_data *send_queue; /* queue of requests to send */
};

static void rspamd_srv_pipe_handler(EV_P_ ev_io *w, int revents);

static void
rspamd_srv_pipe_update_watcher(struct rspamd_srv_pipe_ctx *ctx)
{
	int events = 0;

	/* Need to write if we have unsent requests in the queue */
	if (ctx->send_queue != NULL) {
		events |= EV_WRITE;
	}

	/* Need to read if we have any pending requests awaiting replies */
	if (kh_size(ctx->requests) > 0) {
		events |= EV_READ;
	}

	if (events == 0) {
		/* No pending work, stop watcher */
		if (ev_is_active(&ctx->io_ev)) {
			ev_io_stop(ctx->ev_base, &ctx->io_ev);
		}
	}
	else {
		/* Update watcher events if needed */
		if (!ev_is_active(&ctx->io_ev) || (ctx->io_ev.events & (EV_READ | EV_WRITE)) != events) {
			ev_io_stop(ctx->ev_base, &ctx->io_ev);
			ev_io_set(&ctx->io_ev, ctx->worker->srv_pipe[1], events);
			ev_io_start(ctx->ev_base, &ctx->io_ev);
		}
	}
}

static void
rspamd_srv_pipe_handler(EV_P_ ev_io *w, int revents)
{
	struct rspamd_srv_pipe_ctx *ctx = (struct rspamd_srv_pipe_ctx *) w->data;
	struct rspamd_srv_request_data *rd;
	struct msghdr msg;
	struct iovec iov;
	unsigned char fdspace[CMSG_SPACE(sizeof(int))];
	struct cmsghdr *cmsg;
	struct rspamd_srv_reply rep;
	gssize r;
	khiter_t k;
	int rfd = -1;

	if (revents & EV_WRITE) {
		/* Send queued requests */
		while (ctx->send_queue != NULL) {
			rd = ctx->send_queue;

			memset(&msg, 0, sizeof(msg));

			/* Attach fd to the message */
			if (rd->attached_fd != -1) {
				memset(fdspace, 0, sizeof(fdspace));
				msg.msg_control = fdspace;
				msg.msg_controllen = sizeof(fdspace);
				cmsg = CMSG_FIRSTHDR(&msg);
				cmsg->cmsg_level = SOL_SOCKET;
				cmsg->cmsg_type = SCM_RIGHTS;
				cmsg->cmsg_len = CMSG_LEN(sizeof(int));
				memcpy(CMSG_DATA(cmsg), &rd->attached_fd, sizeof(int));
			}

			iov.iov_base = &rd->cmd;
			iov.iov_len = sizeof(rd->cmd);
			msg.msg_iov = &iov;
			msg.msg_iovlen = 1;

			r = sendmsg(w->fd, &msg, 0);

			if (r == -1) {
				if (errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOBUFS) {
					/* Socket buffer full, try again later */
					break;
				}
				msg_err("cannot write to server pipe: %s; command = %s",
						strerror(errno), rspamd_srv_command_to_string(rd->cmd.type));
				/* Remove from queue and hash, close fd if not transferred, free */
				LL_DELETE(ctx->send_queue, rd);
				k = kh_get(rspamd_srv_requests, ctx->requests, rd->id);
				if (k != kh_end(ctx->requests)) {
					kh_del(rspamd_srv_requests, ctx->requests, k);
				}
				if (rd->attached_fd != -1) {
					close(rd->attached_fd);
				}
				g_free(rd);
				continue;
			}
			else if (r != (gssize) sizeof(rd->cmd)) {
				msg_err("incomplete write to server pipe: %d != %d; command = %s",
						(int) r, (int) sizeof(rd->cmd),
						rspamd_srv_command_to_string(rd->cmd.type));
				LL_DELETE(ctx->send_queue, rd);
				k = kh_get(rspamd_srv_requests, ctx->requests, rd->id);
				if (k != kh_end(ctx->requests)) {
					kh_del(rspamd_srv_requests, ctx->requests, k);
				}
				if (rd->attached_fd != -1) {
					close(rd->attached_fd);
				}
				g_free(rd);
				continue;
			}

			/* Successfully sent, remove from send queue but keep in hash */
			LL_DELETE(ctx->send_queue, rd);
			rd->sent = TRUE;
		}
	}

	if (revents & EV_READ) {
		/* Read replies */
		for (;;) {
			iov.iov_base = &rep;
			iov.iov_len = sizeof(rep);
			memset(&msg, 0, sizeof(msg));
			msg.msg_control = fdspace;
			msg.msg_controllen = sizeof(fdspace);
			msg.msg_iov = &iov;
			msg.msg_iovlen = 1;

			r = recvmsg(w->fd, &msg, 0);

			if (r == -1) {
				if (errno == EAGAIN || errno == EWOULDBLOCK) {
					/* No more data */
					break;
				}
				msg_err("cannot read from server pipe: %s", strerror(errno));
				break;
			}

			if (r == 0) {
				/* Connection closed */
				msg_err("server pipe closed unexpectedly");
				break;
			}

			if (r != (gssize) sizeof(rep)) {
				msg_err("incomplete read from server pipe: %d != %d",
						(int) r, (int) sizeof(rep));
				continue;
			}

			/* Look up request by ID */
			k = kh_get(rspamd_srv_requests, ctx->requests, rep.id);
			if (k == kh_end(ctx->requests)) {
				msg_warn("received reply for unknown request id %uL",
						 rep.id);
				continue;
			}

			rd = kh_val(ctx->requests, k);
			kh_del(rspamd_srv_requests, ctx->requests, k);

			/* Extract attached fd if present */
			rfd = -1;
			if (msg.msg_controllen >= CMSG_LEN(sizeof(int))) {
				rfd = *(int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));
			}

			/* Call handler */
			if (rd->handler) {
				rd->handler(ctx->worker, &rep, rfd, rd->ud);
			}

			g_free(rd);
		}
	}

	rspamd_srv_pipe_update_watcher(ctx);
}

static struct rspamd_srv_pipe_ctx *
rspamd_srv_pipe_ctx_create(struct rspamd_worker *worker, struct ev_loop *ev_base)
{
	struct rspamd_srv_pipe_ctx *ctx;

	ctx = g_malloc0(sizeof(*ctx));
	ctx->worker = worker;
	ctx->ev_base = ev_base;
	ctx->requests = kh_init(rspamd_srv_requests);
	ctx->send_queue = NULL;

	ctx->io_ev.data = ctx;
	ev_io_init(&ctx->io_ev, rspamd_srv_pipe_handler, worker->srv_pipe[1], 0);
	/* Watcher starts inactive, will be activated when requests are added */

	return ctx;
}

static void
rspamd_srv_pipe_ctx_destroy(struct rspamd_srv_pipe_ctx *ctx)
{
	struct rspamd_srv_request_data *rd;
	khiter_t k;

	if (ctx == NULL) {
		return;
	}

	ev_io_stop(ctx->ev_base, &ctx->io_ev);

	/*
	 * Free all pending requests from hash table.
	 * Note: items in send_queue are also in the hash, so we only iterate
	 * the hash to avoid double-free. For unsent items, close attached_fd
	 * since it was never transferred to main process.
	 */
	for (k = kh_begin(ctx->requests); k != kh_end(ctx->requests); ++k) {
		if (kh_exist(ctx->requests, k)) {
			rd = kh_val(ctx->requests, k);
			if (!rd->sent && rd->attached_fd != -1) {
				close(rd->attached_fd);
			}
			g_free(rd);
		}
	}
	kh_destroy(rspamd_srv_requests, ctx->requests);

	g_free(ctx);
}

/* Per-worker srv_pipe context (each worker process has its own copy after fork) */
static struct rspamd_srv_pipe_ctx *srv_pipe_ctx = NULL;

void rspamd_srv_pipe_init(struct rspamd_worker *worker, struct ev_loop *ev_base)
{
	if (srv_pipe_ctx != NULL) {
		/* Already initialized */
		return;
	}
	srv_pipe_ctx = rspamd_srv_pipe_ctx_create(worker, ev_base);
}

void rspamd_srv_pipe_cleanup(void)
{
	rspamd_srv_pipe_ctx_destroy(srv_pipe_ctx);
	srv_pipe_ctx = NULL;
}

void rspamd_srv_send_command(struct rspamd_worker *worker,
							 struct ev_loop *ev_base,
							 struct rspamd_srv_command *cmd,
							 int attached_fd,
							 rspamd_srv_reply_handler handler,
							 gpointer ud)
{
	struct rspamd_srv_request_data *rd;
	int ret;
	khiter_t k;

	g_assert(cmd != NULL);
	g_assert(worker != NULL);

	/* Lazy initialization of srv_pipe context */
	if (srv_pipe_ctx == NULL) {
		rspamd_srv_pipe_init(worker, ev_base);
	}

	rd = g_malloc0(sizeof(*rd));
	cmd->id = ottery_rand_uint64();
	rd->id = cmd->id;
	memcpy(&rd->cmd, cmd, sizeof(rd->cmd));
	rd->handler = handler;
	rd->ud = ud;
	rd->attached_fd = attached_fd;
	rd->sent = FALSE;
	rd->next = NULL;

	/* Add to hash for reply matching */
	k = kh_put(rspamd_srv_requests, srv_pipe_ctx->requests, rd->id, &ret);
	if (ret < 0) {
		msg_err("cannot add request to hash table");
		g_free(rd);
		return;
	}
	kh_val(srv_pipe_ctx->requests, k) = rd;

	/* Add to send queue */
	LL_APPEND(srv_pipe_ctx->send_queue, rd);

	/* Activate watcher */
	rspamd_srv_pipe_update_watcher(srv_pipe_ctx);
}

enum rspamd_control_type
rspamd_control_command_from_string(const char *str)
{
	enum rspamd_control_type ret = RSPAMD_CONTROL_MAX;

	if (!str) {
		return ret;
	}

	if (g_ascii_strcasecmp(str, "hyperscan_loaded") == 0) {
		ret = RSPAMD_CONTROL_HYPERSCAN_LOADED;
	}
	else if (g_ascii_strcasecmp(str, "stat") == 0) {
		ret = RSPAMD_CONTROL_STAT;
	}
	else if (g_ascii_strcasecmp(str, "reload") == 0) {
		ret = RSPAMD_CONTROL_RELOAD;
	}
	else if (g_ascii_strcasecmp(str, "reresolve") == 0) {
		ret = RSPAMD_CONTROL_RERESOLVE;
	}
	else if (g_ascii_strcasecmp(str, "recompile") == 0) {
		ret = RSPAMD_CONTROL_RECOMPILE;
	}
	else if (g_ascii_strcasecmp(str, "log_pipe") == 0) {
		ret = RSPAMD_CONTROL_LOG_PIPE;
	}
	else if (g_ascii_strcasecmp(str, "fuzzy_stat") == 0) {
		ret = RSPAMD_CONTROL_FUZZY_STAT;
	}
	else if (g_ascii_strcasecmp(str, "fuzzy_sync") == 0) {
		ret = RSPAMD_CONTROL_FUZZY_SYNC;
	}
	else if (g_ascii_strcasecmp(str, "fuzzy_hash") == 0) {
		ret = RSPAMD_CONTROL_FUZZY_HASH;
	}
	else if (g_ascii_strcasecmp(str, "monitored_change") == 0) {
		ret = RSPAMD_CONTROL_MONITORED_CHANGE;
	}
	else if (g_ascii_strcasecmp(str, "child_change") == 0) {
		ret = RSPAMD_CONTROL_CHILD_CHANGE;
	}
	else if (g_ascii_strcasecmp(str, "workers_spawned") == 0) {
		ret = RSPAMD_CONTROL_WORKERS_SPAWNED;
	}
	else if (g_ascii_strcasecmp(str, "composites_stats") == 0) {
		ret = RSPAMD_CONTROL_COMPOSITES_STATS;
	}

	return ret;
}

const char *
rspamd_control_command_to_string(enum rspamd_control_type cmd)
{
	const char *reply = "unknown";

	switch (cmd) {
	case RSPAMD_CONTROL_STAT:
		reply = "stat";
		break;
	case RSPAMD_CONTROL_RELOAD:
		reply = "reload";
		break;
	case RSPAMD_CONTROL_RERESOLVE:
		reply = "reresolve";
		break;
	case RSPAMD_CONTROL_RECOMPILE:
		reply = "recompile";
		break;
	case RSPAMD_CONTROL_HYPERSCAN_LOADED:
		reply = "hyperscan_loaded";
		break;
	case RSPAMD_CONTROL_LOG_PIPE:
		reply = "log_pipe";
		break;
	case RSPAMD_CONTROL_FUZZY_STAT:
		reply = "fuzzy_stat";
		break;
	case RSPAMD_CONTROL_FUZZY_SYNC:
		reply = "fuzzy_sync";
		break;
	case RSPAMD_CONTROL_FUZZY_HASH:
		reply = "fuzzy_hash";
		break;
	case RSPAMD_CONTROL_MONITORED_CHANGE:
		reply = "monitored_change";
		break;
	case RSPAMD_CONTROL_CHILD_CHANGE:
		reply = "child_change";
		break;
	case RSPAMD_CONTROL_WORKERS_SPAWNED:
		reply = "workers_spawned";
		break;
	case RSPAMD_CONTROL_COMPOSITES_STATS:
		reply = "composites_stats";
		break;
	case RSPAMD_CONTROL_FUZZY_BLOCKED:
		reply = "fuzzy_blocked";
		break;
	case RSPAMD_CONTROL_MULTIPATTERN_LOADED:
		reply = "multipattern_loaded";
		break;
	case RSPAMD_CONTROL_REGEXP_MAP_LOADED:
		reply = "regexp_map_loaded";
		break;
	default:
		break;
	}

	return reply;
}

const char *rspamd_srv_command_to_string(enum rspamd_srv_type cmd)
{
	const char *reply = "unknown";

	switch (cmd) {
	case RSPAMD_SRV_SOCKETPAIR:
		reply = "socketpair";
		break;
	case RSPAMD_SRV_HYPERSCAN_LOADED:
		reply = "hyperscan_loaded";
		break;
	case RSPAMD_SRV_MONITORED_CHANGE:
		reply = "monitored_change";
		break;
	case RSPAMD_SRV_LOG_PIPE:
		reply = "log_pipe";
		break;
	case RSPAMD_SRV_ON_FORK:
		reply = "on_fork";
		break;
	case RSPAMD_SRV_HEARTBEAT:
		reply = "heartbeat";
		break;
	case RSPAMD_SRV_HEALTH:
		reply = "health";
		break;
	case RSPAMD_SRV_NOTICE_HYPERSCAN_CACHE:
		reply = "notice_hyperscan_cache";
		break;
	case RSPAMD_SRV_FUZZY_BLOCKED:
		reply = "fuzzy_blocked";
		break;
	case RSPAMD_SRV_WORKERS_SPAWNED:
		reply = "workers_spawned";
		break;
	case RSPAMD_SRV_MULTIPATTERN_LOADED:
		reply = "multipattern_loaded";
		break;
	case RSPAMD_SRV_REGEXP_MAP_LOADED:
		reply = "regexp_map_loaded";
		break;
	case RSPAMD_SRV_BUSY:
		reply = "busy";
		break;
	case RSPAMD_SRV_RECOMPILE_REQUEST:
		reply = "recompile_request";
		break;
	default:
		break;
	}

	return reply;
}

void rspamd_worker_set_busy(struct rspamd_worker *worker,
							struct ev_loop *event_loop,
							const char *reason)
{
	struct rspamd_srv_command srv_cmd;

	if (worker->state != rspamd_worker_state_running) {
		return;
	}

	memset(&srv_cmd, 0, sizeof(srv_cmd));
	srv_cmd.type = RSPAMD_SRV_BUSY;
	srv_cmd.cmd.busy.is_busy = (reason != NULL);

	if (reason != NULL) {
		rspamd_strlcpy(srv_cmd.cmd.busy.reason, reason,
					   sizeof(srv_cmd.cmd.busy.reason));
	}
	else {
		srv_cmd.cmd.busy.reason[0] = '\0';
	}

	rspamd_srv_send_command(worker, event_loop, &srv_cmd, -1, NULL, NULL);
}
