/*
 * mod_debug - utilities to debug lighttpd
 *
 * Todo:
 *     - prettier output
 *     - more detailed output
 *     - more debug actions (for other stuff than connections)
 *
 * Author:
 *     Copyright (c) 2009-2011 Thomas Porzelt
 * License:
 *     MIT, see COPYING file in the lighttpd 2 tree
 */

#include <lighttpd/base.h>

#include <lighttpd/lighttpd-glue.h>

#ifdef WITH_PROFILER
# include <lighttpd/profiler.h>
#endif

LI_API gboolean mod_debug_init(liModules *mods, liModule *mod);
LI_API gboolean mod_debug_free(liModules *mods, liModule *mod);

struct mod_debug_detailed_t {
	liConnection con;
};
typedef struct mod_debug_detailed_t mod_debug_detailed_t;

struct mod_debug_data_t {
	guint wrk_ndx;
	guint con_ndx;
	liConnection *con;
	liWaitQueueElem io_timeout_elem;
	gint fd;
	liConnectionState state;
	GString *remote_addr_str, *local_addr_str;
	gboolean is_ssl, keep_alive;
	GString *host, *path, *query;
	liHttpMethod method;
	goffset request_size;
	goffset response_size;
	li_tstamp ts_started;
	guint64 bytes_in;
	guint64 bytes_out;
	guint64 bytes_in_5s_diff;
	guint64 bytes_out_5s_diff;
	GString *detailed;
};
typedef struct mod_debug_data_t mod_debug_data_t;

struct mod_debug_job_t {
	liVRequest *vr;
	gpointer *context;
	liPlugin *p;
	struct {
		guint wrk_ndx;
		guint con_ndx;
		gint fd;
		GString *remote_addr_str;
	} detailed;
};
typedef struct mod_debug_job_t mod_debug_job_t;

struct plugin_debug_worker_data {
	liWorker *wrk;
	struct plugin_debug_data *pd;
	liEventAsync stop_listen;
	liEventTimer stop_listen_timeout;
};
typedef struct plugin_debug_worker_data plugin_debug_worker_data;

struct plugin_debug_data {
	int stop_listen_timeout_seconds;
	int stop_listen_timeout_seconds_repeat;
	plugin_debug_worker_data *worker_data;
};
typedef struct plugin_debug_data plugin_debug_data;


/* the CollectFunc */
static gpointer debug_collect_func(liWorker *wrk, gpointer fdata) {
	GArray *cons;
	guint len;
	mod_debug_job_t *job = fdata;

	/* gather connection info */
	cons = g_array_sized_new(FALSE, TRUE, sizeof(mod_debug_data_t), wrk->connections_active);
	g_array_set_size(cons, wrk->connections_active);

	for (guint i = 0; i < wrk->connections_active; i++) {
		liConnection *c = g_array_index(wrk->connections, liConnection*, i);
		mod_debug_data_t *cd = &g_array_index(cons, mod_debug_data_t, i);
		cd->wrk_ndx = wrk->ndx;
		cd->con_ndx = i;
		cd->con = c;
		cd->io_timeout_elem = c->io_timeout_elem;
		cd->fd = -1;
		cd->is_ssl = c->info.is_ssl;
		cd->keep_alive = c->info.keep_alive;
		cd->remote_addr_str = g_string_new_len(GSTR_LEN(c->info.remote_addr_str));
		cd->local_addr_str = g_string_new_len(GSTR_LEN(c->info.local_addr_str));
		cd->host = g_string_new_len(GSTR_LEN(c->mainvr->request.uri.host));
		cd->path = g_string_new_len(GSTR_LEN(c->mainvr->request.uri.path));
		cd->query = g_string_new_len(GSTR_LEN(c->mainvr->request.uri.query));
		cd->method = c->mainvr->request.http_method;
		cd->request_size = c->mainvr->request.content_length;
		cd->response_size = (NULL != c->mainvr->backend_source) ? c->mainvr->backend_source->out->bytes_out : 0;
		cd->state = c->state;
		cd->ts_started = c->ts_started;
		cd->bytes_in = c->info.stats.bytes_in;
		cd->bytes_out = c->info.stats.bytes_out;
		cd->bytes_in_5s_diff = c->info.stats.bytes_in_5s_diff;
		cd->bytes_out_5s_diff = c->info.stats.bytes_out_5s_diff;

		if (job->detailed.remote_addr_str) {
			if (job->detailed.wrk_ndx == wrk->ndx && job->detailed.con_ndx == i &&
				job->detailed.fd == cd->fd && g_string_equal(job->detailed.remote_addr_str, c->info.remote_addr_str)) {

				cd->detailed = g_string_sized_new(1023);
				g_string_append_printf(cd->detailed, "<pre>connection* @ %p = {\n", (void*)cd->con);
				g_string_append_printf(cd->detailed, "	fd = %d,\n", cd->fd);
				g_string_append_printf(cd->detailed, "	remote_addr_str = \"%s\",\n", cd->remote_addr_str->str);
				g_string_append_printf(cd->detailed, "	local_addr_str = \"%s\",\n", cd->local_addr_str->str);
				g_string_append_printf(cd->detailed, "	is_ssl = \"%s\",\n", cd->is_ssl ? "true" : "false");
				g_string_append_printf(cd->detailed, "	keep_alive = \"%s\",\n", cd->keep_alive ? "true" : "false");
				g_string_append_printf(cd->detailed, "	state = \"%s\",\n", li_connection_state_str(cd->state));
				g_string_append_printf(cd->detailed, "	ts_started = %f,\n", cd->ts_started);
				g_string_append_printf(cd->detailed,
					"	io_timeout_elem = {\n"
					"		queued = %s,\n"
					"		ts = %f,\n"
					"		prev = %p,\n"
					"		next = %p,\n"
					"		data = %p,\n"
					"	}\n",
					cd->io_timeout_elem.queued ? "true":"false",
					cd->io_timeout_elem.ts,
					(void*)cd->io_timeout_elem.prev, (void*)cd->io_timeout_elem.next,
					cd->io_timeout_elem.data
				);
				g_string_append_printf(cd->detailed,
					"	stats = {\n"
					"		bytes_in = %"G_GUINT64_FORMAT",\n"
					"		bytes_out = %"G_GUINT64_FORMAT"\n"
					"	}\n",
					cd->bytes_in, cd->bytes_out
				);
				g_string_append_printf(cd->detailed,
					"	mainvr = {\n"
					"		request = {\n"
					"			method = \"%s\"\n"
					"			host = \"%s\"\n"
					"			path = \"%s\"\n"
					"			query = \"%s\"\n"
					"		}\n"
					"	}\n",
					li_http_method_string(cd->method, &len),
					cd->host->str,
					cd->path->str,
					cd->query->str
				);
				li_g_string_append_len(cd->detailed, CONST_STR_LEN("}</pre>"));
			}
		}
	}

	return cons;
}

/* the CollectCallback */
static void debug_collect_cb(liWorker *wrk, gpointer cbdata, gpointer fdata, GPtrArray *result, gboolean complete) {
	mod_debug_job_t *job = cbdata;
	liVRequest *vr;
	GString *html;

	UNUSED(wrk);
	UNUSED(fdata);

	if (!complete) {
		/* someone called li_collect_break, so we don't need any vrequest handling here. just free the result data */
		guint i, j;

		for (i = 0; i < result->len; i++) {
			GArray *cons = g_ptr_array_index(result, i);
			for (j = 0; j < cons->len; j++) {
				mod_debug_data_t *cd = &g_array_index(cons, mod_debug_data_t, j);

				g_string_free(cd->remote_addr_str, TRUE);
				g_string_free(cd->local_addr_str, TRUE);
				g_string_free(cd->host, TRUE);
				g_string_free(cd->path, TRUE);
				g_string_free(cd->query, TRUE);
				if (cd->detailed)
					g_string_free(cd->detailed, TRUE);
			}

			g_array_free(cons, TRUE);
		}

		if (job->detailed.remote_addr_str)
			g_string_free(job->detailed.remote_addr_str, TRUE);
		g_slice_free(mod_debug_job_t, job);

		return;
	}

	vr = job->vr;
	/* clear context so it doesn't get cleaned up anymore */
	*(job->context) = NULL;

	html = g_string_sized_new(2047);

	li_g_string_append_len(html, CONST_STR_LEN("<html>\n<head>\n<title>Lighttpd mod_debug</title>\n"
		"<style>a { color: blue; }</style>\n"
		"</head>\n<body>\n"));

	/* list connections */
	{
		guint i, j;
		GString *duration = g_string_sized_new(15);

		g_string_append_printf(html, "<p>now: %f<br>io timeout watcher active/repeat: %s/%f<br></p>\n",
			li_cur_ts(vr->wrk), li_event_active(&vr->wrk->io_timeout_queue.timer) ? "yes":"no",
			vr->wrk->io_timeout_queue.timer.libevmess.timer.repeat
		);

		li_g_string_append_len(html, CONST_STR_LEN("<table><tr><th>Client</th><th>Duration</th><th></th></tr>\n"));

		for (i = 0; i < result->len; i++) {
			GArray *cons = g_ptr_array_index(result, i);

			for (j = 0; j < cons->len; j++) {
				mod_debug_data_t *d = &g_array_index(cons, mod_debug_data_t, j);

				li_counter_format((guint64)(li_cur_ts(vr->wrk) - d->ts_started), COUNTER_TIME, duration);
				g_string_append_printf(html, "<tr><td>%s</td><td style=\"text-align:right;\">%s</td><td style=\"padding-left:10px;\"><a href=\"?%u_%u_%d_%s\">debug</a></td></tr>\n",
					d->remote_addr_str->str,
					duration->str,
					d->wrk_ndx, d->con_ndx, d->fd, d->remote_addr_str->str
				);

				if (d->detailed) {
					g_string_append_printf(html, "<tr><td colspan=\"3\">%s</td></tr>\n", d->detailed->str);
					g_string_free(d->detailed, TRUE);
				}

				g_string_free(d->remote_addr_str, TRUE);
				g_string_free(d->local_addr_str, TRUE);
				g_string_free(d->host, TRUE);
				g_string_free(d->path, TRUE);
				g_string_free(d->query, TRUE);
			}

			g_array_free(cons, TRUE);
		}

		li_g_string_append_len(html, CONST_STR_LEN("</table>\n"));
		g_string_free(duration, TRUE);
	}

	li_g_string_append_len(html, CONST_STR_LEN("</body>\n</html>\n"));

	if (li_vrequest_handle_direct(vr)) {
		li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/html; charset=utf-8"));
		vr->response.http_status = 200;

		li_chunkqueue_append_string(vr->direct_out, html);
		li_vrequest_joblist_append(vr);
	} else {
		g_string_free(html, TRUE);
	}

	if (job->detailed.remote_addr_str)
		g_string_free(job->detailed.remote_addr_str, TRUE);
	g_slice_free(mod_debug_job_t, job);
}

static liHandlerResult debug_show_connections_cleanup(liVRequest *vr, gpointer param, gpointer context) {
	liCollectInfo *ci = (liCollectInfo*) context;

	UNUSED(vr);
	UNUSED(param);

	li_collect_break(ci);

	return LI_HANDLER_GO_ON;
}

static liHandlerResult debug_show_connections(liVRequest *vr, gpointer param, gpointer *context) {
	liPlugin *p = (liPlugin*) param;

	switch (vr->request.http_method) {
	case LI_HTTP_METHOD_GET:
	case LI_HTTP_METHOD_HEAD:
		break;
	default:
		return LI_HANDLER_GO_ON;
	}

	if (NULL != *context)
		return LI_HANDLER_WAIT_FOR_EVENT;

	if (!li_vrequest_is_handled(vr)) {
		liCollectInfo *ci;
		mod_debug_job_t *j = g_slice_new0(mod_debug_job_t);
		j->vr = vr;
		j->context = context;
		j->p = p;

		if (vr->request.uri.query->len) {
			/* querystring = <wrk_ndx>_<con_ndx>_<con_fd>_<remote_addr_str> */
			j->detailed.remote_addr_str = g_string_sized_new(vr->request.uri.query->len);
			sscanf(vr->request.uri.query->str, "%u_%u_%i_%s", &(j->detailed.wrk_ndx),
				&(j->detailed.con_ndx), &(j->detailed.fd), j->detailed.remote_addr_str->str);
			g_string_set_size(j->detailed.remote_addr_str, strlen(j->detailed.remote_addr_str->str));
		}

		VR_DEBUG(vr, "%s", "collecting debug info...");

		ci = li_collect_start(vr->wrk, debug_collect_func, j, debug_collect_cb, j);
		*context = ci; /* may be NULL */
	}

	return (*context) ? LI_HANDLER_WAIT_FOR_EVENT : LI_HANDLER_GO_ON;
}

static liAction* debug_show_connections_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(userdata);

	if (!li_value_is_nothing(val)) {
		ERROR(srv, "%s", "debug.show_connections doesn't expect any parameters");
		return NULL;
	}

	return li_action_new_function(debug_show_connections, debug_show_connections_cleanup, NULL, p);
}

#ifdef WITH_PROFILER
static liHandlerResult debug_profiler_dump(liVRequest *vr, gpointer param, gpointer *context) {
	gint minsize = GPOINTER_TO_INT(param);

	UNUSED(vr); UNUSED(context);

	if (!getenv("LIGHTY_PROFILE_MEM"))
		return LI_HANDLER_GO_ON;

	li_profiler_dump(minsize);

	return LI_HANDLER_GO_ON;
}

static liAction* debug_profiler_dump_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	gpointer ptr;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_NONE == li_value_type(val)) {
		ptr = GINT_TO_POINTER(10240);
	} else if (LI_VALUE_NUMBER == li_value_type(val)) {
		ptr = GINT_TO_POINTER(val->data.number);
	} else {
		ERROR(srv, "%s", "debug.profiler_dump takes an optional integer (minsize) as parameter");
		return NULL;
	}

	return li_action_new_function(debug_profiler_dump, NULL, NULL, ptr);
}
#endif

static void format_event(GString *out, liWorker *wrk, liEventBase *base) {
	gboolean active = li_event_active_(base);

	g_string_append_printf(out,
		"Event listener for worker %i: '%s' (%s %s)%s",
		wrk->ndx,
		base->event_name,
		active ? "active" : "inactive",
		li_event_type_string(base->type),
		base->keep_loop_alive
			? (active
				? ""
				: " [doesn't keep loop alive]")
			: " [does never keep loop alive]");
	switch (base->type) {
	case LI_EVT_IO:
		{
			liEventIO *io = li_event_io_from(base);
			liSocketAddress local_addr = li_sockaddr_local_from_socket(li_event_io_fd(io));
			liSocketAddress remote_addr = li_sockaddr_remote_from_socket(li_event_io_fd(io));

			g_string_append_printf(out,
				": fd=%i, events=%i [%s%s]",
				li_event_io_fd(io),
				io->events,
				0 != (io->events & LI_EV_READ) ? "read," : "",
				0 != (io->events & LI_EV_WRITE) ? "write," : "");

			li_sockaddr_to_string(local_addr, wrk->tmp_str, TRUE);
			g_string_append_printf(out, ", local=%s", wrk->tmp_str->str);

			li_sockaddr_to_string(remote_addr, wrk->tmp_str, TRUE);
			g_string_append_printf(out, ", remote=%s", wrk->tmp_str->str);
		}
		break;
	case LI_EVT_TIMER:
		{
			liEventTimer *timer = li_event_timer_from(base);
			g_string_append_printf(out,
				": timeout=%i",
				(int)timer->libevmess.timer.repeat);
		}
		break;
	case LI_EVT_CHILD:
		{
			liEventChild *child = li_event_child_from(base);
			g_string_append_printf(out,
				": pid=%i",
				li_event_child_pid(child));
		}
		break;
	case LI_EVT_SIGNAL:
		{
			liEventSignal *signal = li_event_signal_from(base);
			g_string_append_printf(out,
				": signum=%i",
				li_event_signal_signum(signal));
		}
		break;
	default:
		break;
	}
	g_string_append_printf(out, "\n");
}

/* if show_all is FALSE only active events that keep the loop alive are shown */
static void log_events(liWorker *wrk, liLogContext *context, gboolean show_all) {
	GList *lnk;
	GString *out = g_string_sized_new(0);

	for (lnk = wrk->loop.watchers.head; NULL != lnk; lnk = lnk->next) {
		liEventBase *base = LI_CONTAINER_OF(lnk, liEventBase, link_watchers);
		gboolean active = li_event_active_(base);

		if (show_all || (active && base->keep_loop_alive)) {
			format_event(out, wrk, base);
			_ERROR(wrk->srv, wrk, context, "%s", out->str);
			g_string_truncate(out, 0);
		}
	}

	g_string_free(out, TRUE);
}

/* if show_all is FALSE only active events that keep the loop alive are shown */
static void format_events(GString *out, liWorker *wrk, gboolean show_all) {
	GList *lnk;
	g_string_truncate(out, 0);

	for (lnk = wrk->loop.watchers.head; NULL != lnk; lnk = lnk->next) {
		liEventBase *base = LI_CONTAINER_OF(lnk, liEventBase, link_watchers);
		gboolean active = li_event_active_(base);

		if (show_all || (active && base->keep_loop_alive)) {
			format_event(out, wrk, base);
		}
	}
}

struct collect_events_job {
	liVRequest *vr;
	gpointer *context;
	gboolean show_all;
};
typedef struct collect_events_job collect_events_job;

/* the CollectFunc */
static gpointer debug_show_events_func(liWorker *wrk, gpointer fdata) {
	collect_events_job *job = fdata;
	GString *out = g_string_sized_new(0);

	format_events(out, wrk, job->show_all);

	return out;
}

/* the CollectCallback */
static void debug_show_events_cb(liWorker *wrk, gpointer cbdata, gpointer fdata, GPtrArray *result, gboolean complete) {
	collect_events_job *job = fdata;
	liVRequest *vr;

	UNUSED(wrk);
	UNUSED(cbdata);

	if (complete) {
		vr = job->vr;
		/* clear context so it doesn't get cleaned up anymore */
		*(job->context) = NULL;

		if (li_vrequest_handle_direct(vr)) {
			guint i;

			li_http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/plain; charset=utf-8"));
			vr->response.http_status = 200;

			for (i = 0; i < result->len; i++) {
				GString *str = g_ptr_array_index(result, i);
				g_ptr_array_index(result, i) = NULL;
				li_chunkqueue_append_string(vr->direct_out, str);
			}

			li_vrequest_joblist_append(vr);
		}
	}

	{
		guint i;

		for (i = 0; i < result->len; i++) {
			GString *str = g_ptr_array_index(result, i);
			if (NULL != str) g_string_free(str, TRUE);
		}

		g_slice_free(collect_events_job, job);
	}
}

static liHandlerResult debug_show_events_cleanup(liVRequest *vr, gpointer param, gpointer context) {
	liCollectInfo *ci = (liCollectInfo*) context;

	UNUSED(vr);
	UNUSED(param);

	li_collect_break(ci);

	return LI_HANDLER_GO_ON;
}

static liHandlerResult debug_show_events(liVRequest *vr, gpointer param, gpointer *context) {
	UNUSED(param);

	switch (vr->request.http_method) {
	case LI_HTTP_METHOD_GET:
	case LI_HTTP_METHOD_HEAD:
		break;
	default:
		return LI_HANDLER_GO_ON;
	}

	if (NULL != *context)
		return LI_HANDLER_WAIT_FOR_EVENT;

	if (!li_vrequest_is_handled(vr)) {
		liCollectInfo *ci;
		collect_events_job *job = g_slice_new0(collect_events_job);
		job->vr = vr;
		job->context = context;
		job->show_all = TRUE;

		VR_DEBUG(vr, "%s", "collecting events info...");

		ci = li_collect_start(vr->wrk, debug_show_events_func, job, debug_show_events_cb, NULL);
		*context = ci; /* may be NULL */
	}

	return (*context) ? LI_HANDLER_WAIT_FOR_EVENT : LI_HANDLER_GO_ON;
}

static liAction* debug_show_events_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	UNUSED(wrk); UNUSED(userdata); UNUSED(p);

	if (!li_value_is_nothing(val)) {
		ERROR(srv, "%s", "debug.show_events doesn't expect any parameters");
		return NULL;
	}

	return li_action_new_function(debug_show_events, debug_show_events_cleanup, NULL, NULL);
}

static gboolean debug_show_events_after_shutdown(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {
	plugin_debug_data *pd = p->data;
	UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_NUMBER == li_value_type(val)) {
		pd->stop_listen_timeout_seconds = val->data.number;
		return TRUE;
	} else if (
		li_value_list_has_len(val, 2)
		&& LI_VALUE_NUMBER == li_value_list_type_at(val, 0)
		&& LI_VALUE_NUMBER == li_value_list_type_at(val, 0)
	) {
		pd->stop_listen_timeout_seconds = li_value_list_at(val, 0)->data.number;
		pd->stop_listen_timeout_seconds_repeat = li_value_list_at(val, 1)->data.number;
		return TRUE;
	}

	ERROR(srv, "debug.show_events_after_shutdown expects one or two numbers, got %s", li_value_type_string(val));
	return FALSE;
}

static const liPluginOption options[] = {
	{ NULL, 0, 0, NULL }
};

static const liPluginAction actions[] = {
	{ "debug.show_connections", debug_show_connections_create, NULL },
#ifdef WITH_PROFILER
	{ "debug.profiler_dump", debug_profiler_dump_create, NULL },
#endif
	{ "debug.show_events", debug_show_events_create, NULL },

	{ NULL, NULL, NULL }
};

static const liPluginSetup setups[] = {
	{ "debug.show_events_after_shutdown", debug_show_events_after_shutdown, NULL },

	{ NULL, NULL, NULL }
};

static void plugin_debug_stop_listen_timeout(liEventBase *watcher, int events) {
	plugin_debug_worker_data *pwd = LI_CONTAINER_OF(li_event_timer_from(watcher), plugin_debug_worker_data, stop_listen_timeout);
	plugin_debug_data *pd = pwd->pd;
	UNUSED(events);

	ERROR(pwd->wrk->srv, "Couldn't suspend yet, checking events for worker %i:", pwd->wrk->ndx);
	log_events(pwd->wrk, NULL, FALSE);

	if (pd->stop_listen_timeout_seconds_repeat >= 0) {
		li_event_timer_once(&pwd->stop_listen_timeout, pd->stop_listen_timeout_seconds_repeat);
	}
}

static void plugin_debug_worker_stop_listen(liEventBase *watcher, int events) {
	plugin_debug_worker_data *pwd = LI_CONTAINER_OF(li_event_async_from(watcher), plugin_debug_worker_data, stop_listen);
	plugin_debug_data *pd = pwd->pd;
	UNUSED(events);

	if (!li_event_attached(&pwd->stop_listen_timeout)) {
		li_event_attach(&pwd->wrk->loop, &pwd->stop_listen_timeout);
		li_event_timer_once(&pwd->stop_listen_timeout, pd->stop_listen_timeout_seconds);
	}
}

static void plugin_debug_prepare_worker(liServer *srv, liPlugin *p, liWorker *wrk) {
	plugin_debug_data *pd = p->data;
	plugin_debug_worker_data *pwd;
	UNUSED(srv);

	if (pd->stop_listen_timeout_seconds < 0) return;

	pwd = &pd->worker_data[wrk->ndx];
	pwd->pd = pd;
	pwd->wrk = wrk;

	li_event_async_init(&wrk->loop, "mod_debug stop_listen", &pwd->stop_listen, plugin_debug_worker_stop_listen);
	li_event_timer_init(NULL, "mod_debug stop_listen_timeout", &pwd->stop_listen_timeout, plugin_debug_stop_listen_timeout);
	li_event_set_keep_loop_alive(&pwd->stop_listen_timeout, FALSE);
}

static void plugin_debug_prepare(liServer *srv, liPlugin *p) {
	plugin_debug_data *pd = p->data;

	if (pd->stop_listen_timeout_seconds >= 0) {
		pd->worker_data = g_new0(plugin_debug_worker_data, srv->worker_count);
	}
}

static void plugin_debug_stop_listen(liServer *srv, liPlugin *p) {
	plugin_debug_data *pd = p->data;

	if (NULL != pd->worker_data) {
		unsigned int i;
		for (i = 0; i < srv->worker_count; ++i) {
			plugin_debug_worker_data *pwd = &pd->worker_data[i];
			if (NULL == pwd->wrk) continue;
			li_event_async_send(&pwd->stop_listen);
		}
	}
}

static void plugin_debug_free(liServer *srv, liPlugin *p) {
	plugin_debug_data *pd = p->data;

	if (NULL != pd->worker_data) {
		unsigned int i;
		for (i = 0; i < srv->worker_count; ++i) {
			plugin_debug_worker_data *pwd = &pd->worker_data[i];
			li_event_clear(&pwd->stop_listen);
			li_event_clear(&pwd->stop_listen_timeout);
		}
		g_free(pd->worker_data);
	}
	g_free(pd);
}

static void plugin_debug_init(liServer *srv, liPlugin *p, gpointer userdata) {
	plugin_debug_data *pd = g_malloc0(sizeof(plugin_debug_data));
	UNUSED(srv); UNUSED(userdata);

	p->options = options;
	p->actions = actions;
	p->setups = setups;

	p->data = pd;

	pd->stop_listen_timeout_seconds = -1; /* disabled by default */
	pd->stop_listen_timeout_seconds_repeat = -1; /* disabled by default */

	p->free = plugin_debug_free;
	p->handle_stop_listen = plugin_debug_stop_listen;
	p->handle_prepare = plugin_debug_prepare;
	p->handle_prepare_worker = plugin_debug_prepare_worker;
}


gboolean mod_debug_init(liModules *mods, liModule *mod) {
	UNUSED(mod);

	MODULE_VERSION_CHECK(mods);

	mod->config = li_plugin_register(mods->main, "mod_debug", plugin_debug_init, NULL);

	return mod->config != NULL;
}

gboolean mod_debug_free(liModules *mods, liModule *mod) {
	if (mod->config)
		li_plugin_free(mods->main, mod->config);

	return TRUE;
}
