/* sys.c, liboop, copyright 1999 Dan Egnor

   This is free software; you can redistribute it and/or modify it under the
   terms of the GNU Lesser General Public License, version 2.1 or later.
   See the file COPYING for details. */

#include "oop.h"

#include <errno.h>
#include <assert.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <string.h>

#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif

#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif

#ifdef HAVE_STRING_H
#include <string.h>   /* Needed on NetBSD1.1/SPARC due to bzero/FD_ZERO. */
#endif

#ifdef HAVE_STRINGS_H
#include <strings.h>  /* Needed on AIX 4.2 due to bzero/FD_ZERO. */
#endif

#define MAGIC 0x9643

struct sys_time {
	struct sys_time *next;
	struct timeval tv;
	oop_call_time *f;
	void *v;
};

struct sys_signal_handler {
	struct sys_signal_handler *next;
	oop_call_signal *f;
	void *v;
};

struct sys_signal {
	struct sys_signal_handler *list,*ptr;
	struct sigaction old;
	volatile sig_atomic_t active;
};

struct sys_file_handler {
	oop_call_fd *f;
	void *v;
};

typedef struct sys_file_handler sys_file[OOP_NUM_EVENTS];

struct oop_source_sys {
	oop_source oop;
	int magic;
	int in_run;
	int num_events;

	/* Timeout queue */
	struct sys_time *time_queue,*time_run;

	/* Signal handling */
	struct sys_signal sig[OOP_NUM_SIGNALS];
	sigjmp_buf env;
	int do_jmp,sig_active;

	/* File descriptors */
	int num_files;
	sys_file *files;
};

struct oop_source_sys *sys_sig_owner[OOP_NUM_SIGNALS];

static oop_source_sys *verify_source(oop_source *source) {
	oop_source_sys *sys = (oop_source_sys *) source;
	assert(MAGIC == sys->magic && "corrupt oop_source structure");
	return sys;
}

static void sys_on_fd(oop_source *source,int fd,oop_event ev,
                      oop_call_fd *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	assert(NULL != f && "callback must be non-NULL");
	if (fd >= sys->num_files) {
		int i,j,num_files = 1 + fd;
		sys_file *files = oop_malloc(num_files * sizeof(sys_file));
		if (NULL == files) return; /* ugh */

		memcpy(files,sys->files,sizeof(sys_file) * sys->num_files);
		for (i = sys->num_files; i < num_files; ++i)
			for (j = 0; j < OOP_NUM_EVENTS; ++j)
				files[i][j].f = NULL;

		if (NULL != sys->files) oop_free(sys->files);
		sys->files = files;
		sys->num_files = num_files;
	}

	assert(NULL == sys->files[fd][ev].f && "multiple handlers registered for a file event");
	sys->files[fd][ev].f = f;
	sys->files[fd][ev].v = v;
	++sys->num_events;
}

static void sys_cancel_fd(oop_source *source,int fd,oop_event ev) {
	oop_source_sys *sys = verify_source(source);
	if (fd < sys->num_files && NULL != sys->files[fd][ev].f) {
		sys->files[fd][ev].f = NULL;
		sys->files[fd][ev].v = NULL;
		--sys->num_events;
	}
}

static void sys_on_time(oop_source *source,struct timeval tv,
                        oop_call_time *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	struct sys_time **p = &sys->time_queue;
	struct sys_time *time = oop_malloc(sizeof(struct sys_time));
	assert(tv.tv_usec >= 0 && "tv_usec must be positive");
	assert(tv.tv_usec < 1000000 && "tv_usec measures microseconds");
	assert(NULL != f && "callback must be non-NULL");
	if (NULL == time) return; /* ugh */
	time->tv = tv;
	time->f = f;
	time->v = v;

	while (NULL != *p
	&&    ((*p)->tv.tv_sec < tv.tv_sec
	||    ((*p)->tv.tv_sec == tv.tv_sec 
	&&     (*p)->tv.tv_usec <= tv.tv_usec))) p = &(*p)->next;
	time->next = *p;
	*p = time;

	++sys->num_events;
}

static int sys_remove_time(oop_source_sys *sys,
                           struct sys_time **p,struct timeval tv,
                           oop_call_time *f,void *v) {
	while (NULL != *p
	&&    ((*p)->tv.tv_sec < tv.tv_sec
	||    ((*p)->tv.tv_sec == tv.tv_sec 
	&&     (*p)->tv.tv_usec < tv.tv_usec))) p = &(*p)->next;
	while (NULL != *p
	&&     (*p)->tv.tv_sec == tv.tv_sec
	&&     (*p)->tv.tv_usec == tv.tv_usec
	&&    ((*p)->f != f || (*p)->v != v)) p = &(*p)->next;
	if (NULL != *p 
	&& (*p)->tv.tv_sec == tv.tv_sec && (*p)->tv.tv_usec == tv.tv_usec) {
		struct sys_time *time = *p;
		assert(f == time->f);
		assert(v == time->v);
		*p = time->next;
		oop_free(time);
		--sys->num_events;
		return 1;
	}
	return 0;
}

static void sys_cancel_time(oop_source *source,struct timeval tv,
                            oop_call_time *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	if (!sys_remove_time(sys,&sys->time_run,tv,f,v))
		sys_remove_time(sys,&sys->time_queue,tv,f,v);
}

static void sys_signal_handler(int sig) {
	oop_source_sys *sys = sys_sig_owner[sig];
	struct sigaction act;
	assert(NULL != sys);

	/* Reset the handler, in case this is needed. */
	sigaction(sig,NULL,&act);
	act.sa_handler = sys_signal_handler;
	sigaction(sig,&act,NULL);

	assert(NULL != sys->sig[sig].list);
	sys->sig[sig].active = 1;
	sys->sig_active = 1;

	/* Break out of select() loop, if necessary. */
	if (sys->do_jmp) siglongjmp(sys->env,1);
}

static void sys_on_signal(oop_source *source,int sig,
                          oop_call_signal *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	struct sys_signal_handler *handler = oop_malloc(sizeof(*handler));
	assert(NULL != f && "callback must be non-NULL");
	if (NULL == handler) return; /* ugh */

	assert(sig > 0 && sig < OOP_NUM_SIGNALS && "invalid signal number");

	handler->f = f;
	handler->v = v;
	handler->next = sys->sig[sig].list;
	sys->sig[sig].list = handler;
	++sys->num_events;

	if (NULL == handler->next) {
		struct sigaction act;

		assert(NULL == sys_sig_owner[sig]);
		sys_sig_owner[sig] = sys;

		assert(0 == sys->sig[sig].active);
		sigaction(sig,NULL,&act);
		sys->sig[sig].old = act;
		act.sa_handler = sys_signal_handler;
#ifdef SA_NODEFER /* BSD/OS doesn't have this, for one. */
		act.sa_flags &= ~SA_NODEFER;
#endif
		sigaction(sig,&act,NULL);
	}
}

static void sys_cancel_signal(oop_source *source,int sig,
                              oop_call_signal *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	struct sys_signal_handler **pp = &sys->sig[sig].list;

	assert(sig > 0 && sig < OOP_NUM_SIGNALS && "invalid signal number");

	while (NULL != *pp && ((*pp)->f != f || (*pp)->v != v))
		pp = &(*pp)->next;

	if (NULL != *pp) {
		struct sys_signal_handler *p = *pp;

		if (NULL == p->next && &sys->sig[sig].list == pp) {
			sigaction(sig,&sys->sig[sig].old,NULL);
			sys->sig[sig].active = 0;
			sys_sig_owner[sig] = NULL;
		}

		*pp = p->next;
		if (sys->sig[sig].ptr == p) sys->sig[sig].ptr = *pp;
		--sys->num_events;
		oop_free(p);
	}
}

oop_source_sys *oop_sys_new(void) {
	oop_source_sys *source = oop_malloc(sizeof(oop_source_sys));
	int i;
	if (NULL == source) return NULL;
	source->oop.on_fd = sys_on_fd;
	source->oop.cancel_fd = sys_cancel_fd;
	source->oop.on_time = sys_on_time;
	source->oop.cancel_time = sys_cancel_time;
	source->oop.on_signal = sys_on_signal;
	source->oop.cancel_signal = sys_cancel_signal;
	source->magic = MAGIC;
	source->in_run = 0;
	source->num_events = 0;
	source->time_queue = source->time_run = NULL;

	source->do_jmp = 0;
	source->sig_active = 0;
	for (i = 0; i < OOP_NUM_SIGNALS; ++i) {
		source->sig[i].list = NULL;
		source->sig[i].ptr = NULL;
		source->sig[i].active = 0;
	}

	source->num_files = 0;
	source->files = NULL;

	return source;
}

static void *sys_time_run(oop_source_sys *sys) {
	void *ret = OOP_CONTINUE;
	while (OOP_CONTINUE == ret && NULL != sys->time_run) {
		struct sys_time *p = sys->time_run;
		sys->time_run = sys->time_run->next;
		--sys->num_events;
		ret = p->f(&sys->oop,p->tv,p->v); /* reenter! */
		oop_free(p);
	}
	return ret;
}

void *oop_sys_run(oop_source_sys *sys) {
	void *ret = OOP_CONTINUE;
	assert(!sys->in_run && "oop_sys_run is not reentrant");
	while (0 != sys->num_events && OOP_CONTINUE == ret)
		ret = oop_sys_run_once(sys);
	return ret;
}

void *oop_sys_run_once(oop_source_sys *sys) {
	void * volatile ret = OOP_CONTINUE;
	struct timeval * volatile ptv = NULL;
	struct timeval tv;
	fd_set rfd,wfd,xfd;
	int i,rv;

	assert(!sys->in_run && "oop_sys_run_once is not reentrant");
	sys->in_run = 1;

	if (NULL != sys->time_run) {
		/* interrupted, restart */
		ptv = &tv;
		tv.tv_sec = 0;
		tv.tv_usec = 0;
	} else if (NULL != sys->time_queue) {
		ptv = &tv;
		gettimeofday(ptv,NULL);
		if (sys->time_queue->tv.tv_usec < tv.tv_usec) {
			tv.tv_usec -= 1000000;
			tv.tv_sec ++;
		}
		tv.tv_sec = sys->time_queue->tv.tv_sec - tv.tv_sec;
		tv.tv_usec = sys->time_queue->tv.tv_usec - tv.tv_usec;
		if (tv.tv_sec < 0) {
			tv.tv_sec = 0;
			tv.tv_usec = 0;
		}
	}

	if (!sys->sig_active) sys->do_jmp = !sigsetjmp(sys->env,1);
	if (sys->sig_active) {
		/* Still perform select(), but don't block. */
		ptv = &tv;
		tv.tv_sec = 0;
		tv.tv_usec = 0;
	}

	/* select() fails on FreeBSD with EINVAL if tv_sec > 1000000000.
           The manual specifies the error code but not the limit.  We limit
	   the select() timeout to one hour for portability. */
	if (NULL != ptv && ptv->tv_sec >= 3600) ptv->tv_sec = 3599;
	assert(NULL == ptv 
	   || (ptv->tv_sec >= 0 && ptv->tv_sec < 3600
           &&  ptv->tv_usec >= 0 && ptv->tv_usec < 1000000));

	FD_ZERO(&rfd);
	FD_ZERO(&wfd);
	FD_ZERO(&xfd);
	for (i = 0; i < sys->num_files; ++i) {
		if (NULL != sys->files[i][OOP_READ].f) FD_SET(i,&rfd);
		if (NULL != sys->files[i][OOP_WRITE].f) FD_SET(i,&wfd);
		if (NULL != sys->files[i][OOP_EXCEPTION].f) FD_SET(i,&xfd);
	}

	do
		rv = select(sys->num_files,&rfd,&wfd,&xfd,ptv);
	while (0 > rv && EINTR == errno);

	sys->do_jmp = 0;

	if (0 > rv) { /* Error in select(). */
		ret = OOP_ERROR;
		goto done; 
	}

	if (sys->sig_active) {
		sys->sig_active = 0;
		for (i = 0; OOP_CONTINUE == ret && i < OOP_NUM_SIGNALS; ++i) {
			if (sys->sig[i].active) {
				sys->sig[i].active = 0;
				sys->sig[i].ptr = sys->sig[i].list;
			}
			while (OOP_CONTINUE == ret && NULL != sys->sig[i].ptr) {
				struct sys_signal_handler *h;
				h = sys->sig[i].ptr;
				sys->sig[i].ptr = h->next;
				ret = h->f(&sys->oop,i,h->v);
			}
		}
		if (OOP_CONTINUE != ret) {
			sys->sig_active = 1; /* come back */
			goto done;
		}
	}

	if (0 < rv) {
		for (i = 0; OOP_CONTINUE == ret && i < sys->num_files; ++i)
			if (FD_ISSET(i,&xfd) 
			&&  NULL != sys->files[i][OOP_EXCEPTION].f)
				ret = sys->files[i][OOP_EXCEPTION].f(
					&sys->oop,i,OOP_EXCEPTION,
					 sys->files[i][OOP_EXCEPTION].v);
		for (i = 0; OOP_CONTINUE == ret && i < sys->num_files; ++i)
			if (FD_ISSET(i,&wfd) 
			&&  NULL != sys->files[i][OOP_WRITE].f)
				ret = sys->files[i][OOP_WRITE].f(
					&sys->oop,i,OOP_WRITE,
					 sys->files[i][OOP_WRITE].v);
		for (i = 0; OOP_CONTINUE == ret && i < sys->num_files; ++i)
			if (FD_ISSET(i,&rfd) 
			&&  NULL != sys->files[i][OOP_READ].f)
				ret = sys->files[i][OOP_READ].f(
					&sys->oop,i,OOP_READ,
					 sys->files[i][OOP_READ].v);
		if (OOP_CONTINUE != ret) goto done;
	}

	/* Catch any leftover timeout events. */
	ret = sys_time_run(sys);
	if (OOP_CONTINUE != ret) goto done;

	if (NULL != sys->time_queue) {
		struct sys_time *p,**pp = &sys->time_queue;
		gettimeofday(&tv,NULL);
		while (NULL != *pp 
		   && (tv.tv_sec > (*pp)->tv.tv_sec
		   || (tv.tv_sec == (*pp)->tv.tv_sec
		   &&  tv.tv_usec >= (*pp)->tv.tv_usec)))
			pp = &(*pp)->next;
		p = *pp;
		*pp = NULL;
		sys->time_run = sys->time_queue;
		sys->time_queue = p;
	}

	ret = sys_time_run(sys);

done:
	sys->in_run = 0;
	return ret;
}

void oop_sys_delete(oop_source_sys *sys) {
	int i,j;

	assert(!sys->in_run && "cannot delete while in oop_sys_run");
	assert(NULL == sys->time_queue 
	&&     NULL == sys->time_run
	&&     "cannot delete with timeout");

	for (i = 0; i < OOP_NUM_SIGNALS; ++i)
		assert(NULL == sys->sig[i].list && "cannot delete with signal handler");

	for (i = 0; i < sys->num_files; ++i)
		for (j = 0; j < OOP_NUM_EVENTS; ++j)
			assert(NULL == sys->files[i][j].f && "cannot delete with file handler");

	assert(0 == sys->num_events);
	if (NULL != sys->files) oop_free(sys->files);
	oop_free(sys);
}

oop_source *oop_sys_source(oop_source_sys *sys) {
	assert(&sys->oop == (oop_source *) sys);
	return &sys->oop;
}


syntax highlighted by Code2HTML, v. 0.9.1