/*
 *  Copyright (c) 2006 Sendmail, Inc. and its suppliers.
 *	All rights reserved.
 *
 * By using this file, you agree to the terms and conditions set
 * forth in the LICENSE file which can be found at the top level of
 * the sendmail distribution.
 *
 */

#include <sm/gen.h>
SM_RCSID("@(#)$Id: monitor.c,v 8.7 2007/04/23 16:26:28 ca Exp $")
#include "libmilter.h"

#if _FFR_THREAD_MONITOR

/*
**  Thread Monitoring
**  Todo: more error checking (return code from function calls)
**  add comments.
*/

bool Monitor = false; /* use monitoring? */
static unsigned int Mon_exec_time = 0;

/* mutex protects Mon_cur_ctx, Mon_ctx_head, and ctx_start */
static smutex_t Mon_mutex;
static scond_t Mon_cv;

/*
**  Current ctx to monitor.
**  Invariant:
**  Mon_cur_ctx == NULL || Mon_cur_ctx is thread which was started the longest
**	time ago.
**
**  Basically the entries in the list are ordered by time because new
**	entries are appended at the end. However, due to the concurrent
**	execution (multi-threaded) and no guaranteed order of wakeups
**	after a mutex_lock() attempt, the order might not be strict,
**	i.e., if the list contains e1 and e2 (in that order) then
**	the the start time of e2 can be (slightly) smaller than that of e1.
**	However, this slight inaccurracy should not matter for the proper
**	working of this algorithm.
*/

static SMFICTX_PTR Mon_cur_ctx = NULL;
static smfi_hd_T Mon_ctx_head; /* head of the linked list of active contexts */

/*
**  SMFI_SET_MAX_EXEC_TIME -- set maximum execution time for a thread
**
**	Parameters:
**		tm -- maximum execution time for a thread
**
**	Returns:
**		MI_SUCCESS
*/

int
smfi_set_max_exec_time(tm)
	unsigned int tm;
{
	Mon_exec_time = tm;
	return MI_SUCCESS;
}

/*
**  MI_MONITOR_THREAD -- monitoring thread
**
**	Parameters:
**		arg -- ignored (required by pthread_create())
**
**	Returns:
**		NULL on termination.
*/

static void *
mi_monitor_thread(arg)
	void *arg;
{
	sthread_t tid;
	int r;
	time_t now, end;

	SM_ASSERT(Monitor);
	SM_ASSERT(Mon_exec_time > 0);
	tid = (sthread_t) sthread_get_id();
	if (pthread_detach(tid) != 0)
	{
		/* log an error */
		return (void *)1;
	}

/*
**  NOTE: this is "flow through" code,
**  do NOT use do { } while ("break" is used here!)
*/

#define MON_CHK_STOP							\
	now = time(NULL);						\
	end = Mon_cur_ctx->ctx_start + Mon_exec_time;			\
	if (now > end)							\
	{								\
		smi_log(SMI_LOG_ERR,					\
			"WARNING: monitor timeout triggered, now=%ld, end=%ld, tid=%ld, state=0x%x",\
			(long) now, (long) end,				\
			(long) Mon_cur_ctx->ctx_id, Mon_cur_ctx->ctx_state);\
		mi_stop_milters(MILTER_STOP);				\
		break;							\
	}

	(void) smutex_lock(&Mon_mutex);
	while (mi_stop() == MILTER_CONT)
	{
		if (Mon_cur_ctx != NULL && Mon_cur_ctx->ctx_start > 0)
		{
			struct timespec abstime;

			MON_CHK_STOP;
			abstime.tv_sec = end;
			abstime.tv_nsec = 0;
			r = pthread_cond_timedwait(&Mon_cv, &Mon_mutex,
					&abstime);
		}
		else
			r = pthread_cond_wait(&Mon_cv, &Mon_mutex);
		if (mi_stop() != MILTER_CONT)
			break;
		if (Mon_cur_ctx != NULL && Mon_cur_ctx->ctx_start > 0)
		{
			MON_CHK_STOP;
		}
	}
	(void) smutex_unlock(&Mon_mutex);

	return NULL;
}

/*
**  MI_MONITOR_INIT -- initialize monitoring thread
**
**	Parameters: none
**
**	Returns:
**		MI_SUCCESS/MI_FAILURE
*/

int
mi_monitor_init()
{
	int r;
	sthread_t tid;

	SM_ASSERT(!Monitor);
	if (Mon_exec_time <= 0)
		return MI_SUCCESS;
	Monitor = true;
	if (!smutex_init(&Mon_mutex))
		return MI_FAILURE;
	if (scond_init(&Mon_cv) != 0)
		return MI_FAILURE;
	SM_TAILQ_INIT(&Mon_ctx_head);

	r = thread_create(&tid, mi_monitor_thread, (void *)NULL);
	if (r != 0)
		return r;
	return MI_SUCCESS;
}

/*
**  MI_MONITOR_WORK_BEGIN -- record start of thread execution
**
**	Parameters:
**		ctx -- session context
**		cmd -- milter command char
**
**	Returns:
**		0
*/

int
mi_monitor_work_begin(ctx, cmd)
	SMFICTX_PTR ctx;
	int cmd;
{
	(void) smutex_lock(&Mon_mutex);
	if (NULL == Mon_cur_ctx)
	{
		Mon_cur_ctx = ctx;
		(void) scond_signal(&Mon_cv);
	}
	ctx->ctx_start = time(NULL);
	SM_TAILQ_INSERT_TAIL(&Mon_ctx_head, ctx, ctx_mon_link);
	(void) smutex_unlock(&Mon_mutex);
	return 0;
}

/*
**  MI_MONITOR_WORK_END -- record end of thread execution
**
**	Parameters:
**		ctx -- session context
**		cmd -- milter command char
**
**	Returns:
**		0
*/

int
mi_monitor_work_end(ctx, cmd)
	SMFICTX_PTR ctx;
	int cmd;
{
	(void) smutex_lock(&Mon_mutex);
	ctx->ctx_start = 0;
	SM_TAILQ_REMOVE(&Mon_ctx_head, ctx, ctx_mon_link);
	if (Mon_cur_ctx == ctx)
	{
		if (SM_TAILQ_EMPTY(&Mon_ctx_head))
			Mon_cur_ctx = NULL;
		else
			Mon_cur_ctx = SM_TAILQ_FIRST(&Mon_ctx_head);
	}
	(void) smutex_unlock(&Mon_mutex);
	return 0;
}
#endif /* _FFR_THREAD_MONITOR */