X-Git-Url: https://code.citadel.org/?a=blobdiff_plain;f=citadel%2Fsysdep.c;h=655bb7249286fc26975c3acd563f76b0dbb5e663;hb=fe4a9ff9d1b33197fb5dfc45567b98c63049d7b6;hp=18e66b9f3c1cfb6b94cd703d93323701f41c1adf;hpb=9120a1b98dcb0d551b94d01b3353f24b53573491;p=citadel.git diff --git a/citadel/sysdep.c b/citadel/sysdep.c index 18e66b9f3..655bb7249 100644 --- a/citadel/sysdep.c +++ b/citadel/sysdep.c @@ -1,16 +1,15 @@ /* + * $Id$ + * * Citadel/UX "system dependent" stuff. * See copyright.txt for copyright information. * - * $Id$ - * - * Here's where we (hopefully) have all the parts of the Citadel server that + * Here's where we (hopefully) have most parts of the Citadel server that * would need to be altered to run the server in a non-POSIX environment. - * Wherever possible, we use function wrappers and type definitions to create - * abstractions that are platform-independent from the calling side. * - * Eventually we'll try porting to a different platform and either have - * multiple variants of this file or simply load it up with #ifdefs. + * If we ever port to a different platform and either have multiple + * variants of this file or simply load it up with #ifdefs. + * */ @@ -22,18 +21,23 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include #include +#ifdef __GNUC__ +#include +#endif #ifdef HAVE_PTHREAD_H #include #endif @@ -63,30 +67,56 @@ struct TheHeap *heap = NULL; pthread_mutex_t Critters[MAX_SEMAPHORES]; /* Things needing locking */ pthread_key_t MyConKey; /* TSD key for MyContext() */ -int msock; /* master listening socket */ -int verbosity = 3; /* Logging level */ +int verbosity = DEFAULT_VERBOSITY; /* Logging level */ struct CitContext masterCC; +int rescan[2]; /* The Rescan Pipe */ +time_t last_purge = 0; /* Last dead session purge */ +static int num_threads = 0; /* Current number of threads */ +int num_sessions = 0; /* Current number of sessions */ + +fd_set masterfds; /* Master sockets etc. */ +int masterhighest; + +time_t last_timer = 0L; /* Last timer hook processing */ + +static pthread_t initial_thread; /* tid for main() thread */ /* * lprintf() ... Write logging information + * + * Note: the variable "buf" below needs to be large enough to handle any + * log data sent through this function. BE CAREFUL! */ void lprintf(int loglevel, const char *format, ...) { va_list arg_ptr; - char buf[512]; + char buf[4096]; va_start(arg_ptr, format); vsprintf(buf, format, arg_ptr); va_end(arg_ptr); if (loglevel <= verbosity) { - fprintf(stderr, "%s", buf); + struct timeval tv; + struct tm *tim; + + gettimeofday(&tv, NULL); + tim = localtime(&(tv.tv_sec)); + /* + * Log provides millisecond accuracy. If you need + * microsecond accuracy and your OS supports it, change + * %03ld to %06ld and remove " / 1000" after tv.tv_usec. + */ + fprintf(stderr, "%04d/%02d/%02d %2d:%02d:%02d.%03ld %s", + tim->tm_year + 1900, tim->tm_mon + 1, tim->tm_mday, + tim->tm_hour, tim->tm_min, tim->tm_sec, + tv.tv_usec / 1000, buf); fflush(stderr); - } + } PerformLogHooks(loglevel, buf); - } +} @@ -96,7 +126,11 @@ void *tracked_malloc(size_t tsize, char *tfile, int tline) { struct TheHeap *hptr; ptr = malloc(tsize); - if (ptr == NULL) return(NULL); + if (ptr == NULL) { + lprintf(3, "DANGER! mallok(%d) at %s:%d failed!\n", + tsize, tfile, tline); + return(NULL); + } hptr = (struct TheHeap *) malloc(sizeof(struct TheHeap)); strcpy(hptr->h_file, tfile); @@ -105,8 +139,17 @@ void *tracked_malloc(size_t tsize, char *tfile, int tline) { hptr->h_ptr = ptr; heap = hptr; return ptr; - } +} + +char *tracked_strdup(const char *orig, char *tfile, int tline) { + char *s; + s = tracked_malloc( (strlen(orig)+1), tfile, tline); + if (s == NULL) return NULL; + + strcpy(s, orig); + return s; +} void tracked_free(void *ptr) { struct TheHeap *hptr, *freeme; @@ -115,19 +158,19 @@ void tracked_free(void *ptr) { hptr = heap->next; free(heap); heap = hptr; - } + } else { for (hptr=heap; hptr->next!=NULL; hptr=hptr->next) { if (hptr->next->h_ptr == ptr) { freeme = hptr->next; hptr->next = hptr->next->next; free(freeme); - } } } + } free(ptr); - } +} void *tracked_realloc(void *ptr, size_t size) { void *newptr; @@ -137,10 +180,10 @@ void *tracked_realloc(void *ptr, size_t size) { for (hptr=heap; hptr!=NULL; hptr=hptr->next) { if (hptr->h_ptr == ptr) hptr->h_ptr = newptr; - } + } return newptr; - } +} void dump_tracked() { @@ -150,26 +193,18 @@ void dump_tracked() { for (hptr=heap; hptr!=NULL; hptr=hptr->next) { cprintf("%20s %5d\n", hptr->h_file, hptr->h_line); - } - cprintf("000\n"); } +#ifdef __GNUC__ + malloc_stats(); #endif -#ifndef HAVE_PTHREAD_CANCEL -/* - * signal handler to fake thread cancellation; only required on BSDI as far - * as I know. - */ - -static pthread_t main_thread_id; - -static RETSIGTYPE cancel_thread(int signum) { - pthread_exit(NULL); - } + cprintf("000\n"); +} #endif + /* - * we used to use master_cleanup() as a signal handler to shut down the server. + * We used to use master_cleanup() as a signal handler to shut down the server. * however, master_cleanup() and the functions it calls do some things that * aren't such a good idea to do from a signal handler: acquiring mutexes, * playing with signal masks on BSDI systems, etc. so instead we install the @@ -177,11 +212,11 @@ static RETSIGTYPE cancel_thread(int signum) { * that it's time to call master_cleanup() and exit. */ -static volatile int time_to_die = 0; +volatile int time_to_die = 0; static RETSIGTYPE signal_cleanup(int signum) { time_to_die = 1; - } +} /* @@ -193,16 +228,17 @@ void init_sysdep(void) { /* Set up a bunch of semaphores to be used for critical sections */ for (a=0; ap_proto)); + s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s < 0) { lprintf(1, "citserver: Can't create a socket: %s\n", strerror(errno)); - exit(errno); - } + return(-1); + } - /* Set the SO_REUSEADDR socket option, because it makes sense. */ i = 1; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) { - lprintf(1, "citserver: Can't bind: %s\n", strerror(errno)); - exit(errno); - } + lprintf(1, "citserver: Can't bind: %s\n", + strerror(errno)); + close(s); + return(-1); + } - if (listen(s, queue_len) < 0) { + if (listen(s, actual_queue_len) < 0) { lprintf(1, "citserver: Can't listen: %s\n", strerror(errno)); - exit(errno); - } + close(s); + return(-1); + } return(s); - } +} + /* - * Return a pointer to a thread's own CitContext structure (old) - * NOTE: this version of MyContext() is commented out because it is no longer - * in use. It was written before I discovered TSD keys. This - * version pounds through the context list until it finds the one matching - * the currently running thread. It remains here, commented out, in case it - * is needed for future ports to threading libraries which have the equivalent - * of pthread_self() but not pthread_key_create() and its ilk. - * - * struct CitContext *MyContext() { - * struct CitContext *ptr; - * THREAD me; - * - * me = pthread_self(); - * for (ptr=ContextList; ptr!=NULL; ptr=ptr->next) { - * if (ptr->mythread == me) return(ptr); - * } - * return(NULL); - * } + * Create a Unix domain socket and listen on it */ +int ig_uds_server(char *sockpath, int queue_len) +{ + struct sockaddr_un addr; + int s; + int i; + int actual_queue_len; + + actual_queue_len = queue_len; + if (actual_queue_len < 5) actual_queue_len = 5; + + i = unlink(sockpath); + if (i != 0) if (errno != ENOENT) { + lprintf(1, "citserver: can't unlink %s: %s\n", + sockpath, strerror(errno)); + return(-1); + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + safestrncpy(addr.sun_path, sockpath, sizeof addr.sun_path); + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s < 0) { + lprintf(1, "citserver: Can't create a socket: %s\n", + strerror(errno)); + return(-1); + } + + if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + lprintf(1, "citserver: Can't bind: %s\n", + strerror(errno)); + return(-1); + } + + if (listen(s, actual_queue_len) < 0) { + lprintf(1, "citserver: Can't listen: %s\n", strerror(errno)); + return(-1); + } + + chmod(sockpath, 0777); + return(s); +} + + /* - * Return a pointer to a thread's own CitContext structure (new) + * Return a pointer to the CitContext structure bound to the thread which + * called this function. If there's no such binding (for example, if it's + * called by the housekeeper thread) then a generic 'master' CC is returned. */ struct CitContext *MyContext(void) { struct CitContext *retCC; retCC = (struct CitContext *) pthread_getspecific(MyConKey); if (retCC == NULL) retCC = &masterCC; return(retCC); - } +} /* - * Wedge our way into the context list. + * Initialize a new context and place it in the list. The session number + * used to be the PID (which is why it's called cs_pid), but that was when we + * had one process per session. Now we just assign them sequentially, starting + * at 1 (don't change it to 0 because masterCC uses 0) and re-using them when + * sessions terminate. */ struct CitContext *CreateNewContext(void) { - struct CitContext *me; + struct CitContext *me, *ptr; + int num = 1; + int startover = 0; - lprintf(9, "CreateNewContext: calling malloc()\n"); me = (struct CitContext *) mallok(sizeof(struct CitContext)); if (me == NULL) { lprintf(1, "citserver: can't allocate memory!!\n"); - pthread_exit(NULL); - } - memset(me, 0, sizeof(struct CitContext)); - - begin_critical_section(S_SESSION_TABLE); - me->next = ContextList; - ContextList = me; - end_critical_section(S_SESSION_TABLE); - return(me); - } - -/* - * Add a thread's thread ID to the context - */ -void InitMyContext(struct CitContext *con) -{ -#ifdef HAVE_PTHREAD_CANCEL - int oldval; -#endif - - con->mythread = pthread_self(); -#ifdef HAVE_PTHREAD_CANCEL - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldval); - pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldval); -#endif - if (pthread_setspecific(MyConKey, (void *)con) != 0) { - lprintf(1, "ERROR! pthread_setspecific() failed: %s\n", - strerror(errno)); - } + return NULL; } + memset(me, 0, sizeof(struct CitContext)); -/* - * Remove a context from the context list. - */ -void RemoveContext(struct CitContext *con) -{ - struct CitContext *ptr; - - lprintf(7, "Starting RemoveContext()\n"); - lprintf(9, "Session count before RemoveContext is %d\n", - session_count()); - if (con==NULL) { - lprintf(7, "WARNING: RemoveContext() called with null!\n"); - return; - } - - /* - * session_count() starts its own S_SESSION_TABLE critical section; - * so do not call it from within this loop. + /* The new context will be created already in the CON_EXECUTING state + * in order to prevent another thread from grabbing it while it's + * being set up. */ + me->state = CON_EXECUTING; + begin_critical_section(S_SESSION_TABLE); - lprintf(7, "Closing socket %d\n", con->client_socket); - close(con->client_socket); - lprintf(9, "Dereferencing session context\n"); - if (ContextList==con) { - ContextList = ContextList->next; - } - else { + /* obtain a unique session number */ + do { + startover = 0; for (ptr = ContextList; ptr != NULL; ptr = ptr->next) { - if (ptr->next == con) { - ptr->next = con->next; - } + if (ptr->cs_pid == num) { + ++num; + startover = 1; } } + } while (startover == 1); - lprintf(9, "Freeing session context...\n"); - phree(con); - lprintf(9, "...done.\n"); - end_critical_section(S_SESSION_TABLE); - - lprintf(9, "Session count after RemoveContext is %d\n", - session_count()); - - lprintf(7, "Done with RemoveContext\n"); - } - - -/* - * Return the number of sessions currently running. - * (This should probably be moved out of sysdep.c) - */ -int session_count(void) { - struct CitContext *ptr; - int TheCount = 0; + me->cs_pid = num; + me->next = ContextList; + ContextList = me; + ++num_sessions; - lprintf(9, "session_count() starting\n"); - begin_critical_section(S_SESSION_TABLE); - for (ptr = ContextList; ptr != NULL; ptr = ptr->next) { - ++TheCount; - lprintf(9, "Counted session %3d (%d)\n", ptr->cs_pid, TheCount); - } end_critical_section(S_SESSION_TABLE); + return(me); +} - lprintf(9, "session_count() finishing\n"); - return(TheCount); - } /* @@ -482,17 +446,33 @@ void client_write(char *buf, int nbytes) { int bytes_written = 0; int retval; + int sock; + + + if (CC->redirect_fp != NULL) { + fwrite(buf, nbytes, 1, CC->redirect_fp); + return; + } + + if (CC->redirect_sock > 0) { + sock = CC->redirect_sock; /* and continue below... */ + } + else { + sock = CC->client_socket; + } + while (bytes_written < nbytes) { - retval = write(CC->client_socket, &buf[bytes_written], + retval = write(sock, &buf[bytes_written], nbytes - bytes_written); if (retval < 1) { lprintf(2, "client_write() failed: %s\n", strerror(errno)); - cleanup(errno); - } - bytes_written = bytes_written + retval; + if (sock == CC->client_socket) CC->kill_me = 1; + return; } + bytes_written = bytes_written + retval; } +} /* @@ -502,14 +482,14 @@ void client_write(char *buf, int nbytes) */ void cprintf(const char *format, ...) { va_list arg_ptr; - char buf[256]; + char buf[SIZ]; va_start(arg_ptr, format); if (vsnprintf(buf, sizeof buf, format, arg_ptr) == -1) buf[sizeof buf - 2] = '\n'; client_write(buf, strlen(buf)); va_end(arg_ptr); - } +} /* @@ -517,7 +497,8 @@ void cprintf(const char *format, ...) { * Return values are: * 1 Requested number of bytes has been read. * 0 Request timed out. - * If the socket breaks, the session is immediately terminated. + * -1 The socket is broken. + * If the socket breaks, the session will be terminated. */ int client_read_to(char *buf, int bytes, int timeout) { @@ -535,20 +516,22 @@ int client_read_to(char *buf, int bytes, int timeout) retval = select( (CC->client_socket)+1, &rfds, NULL, NULL, &tv); + if (FD_ISSET(CC->client_socket, &rfds) == 0) { return(0); - } + } rlen = read(CC->client_socket, &buf[len], bytes-len); if (rlen<1) { lprintf(2, "client_read() failed: %s\n", strerror(errno)); - cleanup(errno); - } - len = len + rlen; + CC->kill_me = 1; + return(-1); } - return(1); + len = len + rlen; } + return(1); +} /* * Read data from the client socket with default timeout. @@ -558,7 +541,7 @@ int client_read_to(char *buf, int bytes, int timeout) int client_read(char *buf, int bytes) { return(client_read_to(buf, bytes, config.c_sleeping)); - } +} /* @@ -574,13 +557,13 @@ int client_gets(char *buf) */ for (i = 0;;i++) { retval = client_read(&buf[i], 1); - if (retval != 1 || buf[i] == '\n' || i == 255) + if (retval != 1 || buf[i] == '\n' || i == (SIZ-1)) break; - } + } /* If we got a long line, discard characters until the newline. */ - if (i == 255) + if (i == (SIZ-1)) while (buf[i] != '\n' && retval == 1) retval = client_read(&buf[i], 1); @@ -589,8 +572,9 @@ int client_gets(char *buf) buf[i] = 0; while ((strlen(buf)>0)&&(!isprint(buf[strlen(buf)-1]))) buf[strlen(buf)-1] = 0; + if (retval < 0) strcpy(buf, "000"); return(retval); - } +} @@ -598,59 +582,50 @@ int client_gets(char *buf) * The system-dependent part of master_cleanup() - close the master socket. */ void sysdep_master_cleanup(void) { - lprintf(7, "Closing master socket %d\n", msock); - close(msock); - } + struct ServiceFunctionHook *serviceptr; -/* - * Cleanup routine to be called when one thread is shutting down. - */ -void cleanup(int exit_code) -{ - /* Terminate the thread. - * Its cleanup handler will call cleanup_stuff() + /* + * close all protocol master sockets */ - lprintf(7, "Calling pthread_exit()\n"); - pthread_exit(NULL); + for (serviceptr = ServiceHookTable; serviceptr != NULL; + serviceptr = serviceptr->next ) { + + if (serviceptr->tcp_port > 0) + lprintf(3, "Closing listener on port %d\n", + serviceptr->tcp_port); + + if (serviceptr->sockpath != NULL) + lprintf(3, "Closing listener on '%s'\n", + serviceptr->sockpath); + + close(serviceptr->msock); + + /* If it's a Unix domain socket, remove the file. */ + if (serviceptr->sockpath != NULL) { + unlink(serviceptr->sockpath); + } } +} + /* * Terminate another session. + * (This could justifiably be moved out of sysdep.c because it + * no longer does anything that is system-dependent.) */ void kill_session(int session_to_kill) { struct CitContext *ptr; - THREAD killme = 0; - lprintf(9, "kill_session() scanning for thread to cancel...\n"); begin_critical_section(S_SESSION_TABLE); for (ptr = ContextList; ptr != NULL; ptr = ptr->next) { if (ptr->cs_pid == session_to_kill) { - killme = ptr->mythread; - } - } - end_critical_section(S_SESSION_TABLE); - lprintf(9, "kill_session() finished scanning.\n"); - - if (killme != 0) { -#ifdef HAVE_PTHREAD_CANCEL - lprintf(9, "calling pthread_cancel()\n"); - pthread_cancel(killme); -#else - pthread_kill(killme, SIGUSR1); -#endif + ptr->kill_me = 1; } } + end_critical_section(S_SESSION_TABLE); +} -/* - * The system-dependent wrapper around the main context loop. - */ -void *sd_context_loop(struct CitContext *con) { - pthread_cleanup_push(*cleanup_stuff, NULL); - context_loop(con); - pthread_cleanup_pop(0); - return NULL; - } /* @@ -661,12 +636,12 @@ void start_daemon(int do_close_stdio) { /* close(0); */ close(1); close(2); - } + } signal(SIGHUP,SIG_IGN); signal(SIGINT,SIG_IGN); signal(SIGQUIT,SIG_IGN); if (fork()!=0) exit(0); - } +} @@ -682,13 +657,13 @@ void cmd_nset(char *cmdbuf) FILE *netsetup; int ch; int a, b; - char netsetup_args[3][256]; + char netsetup_args[3][SIZ]; if (CC->usersupp.axlevel < 6) { cprintf("%d Higher access required.\n", ERROR + HIGHER_ACCESS_REQUIRED); return; - } + } for (a=1; a<=3; ++a) { if (num_parms(cmdbuf) >= a) { @@ -696,13 +671,13 @@ void cmd_nset(char *cmdbuf) for (b=0; b&1", netsetup_args[0], netsetup_args[1], netsetup_args[2]); @@ -710,30 +685,30 @@ void cmd_nset(char *cmdbuf) if (netsetup == NULL) { cprintf("%d %s\n", ERROR, strerror(errno)); return; - } + } fbuf[0] = 0; while (ch = getc(netsetup), (ch > 0)) { fbuf[strlen(fbuf)+1] = 0; fbuf[strlen(fbuf)] = ch; - } + } retcode = pclose(netsetup); if (retcode != 0) { for (a=0; apw_gecos); for (a=0; atid, NULL, worker_thread, NULL) != 0)) + { + lprintf(1, "Can't create worker thread: %s\n", + strerror(ret)); + } + n->next = worker_list; + worker_list = n; +} + + + +/* + * Purge all sessions which have the 'kill_me' flag set. + * This function has code to prevent it from running more than once every + * few seconds, because running it after every single unbind would waste a lot + * of CPU time and keep the context list locked too much. + * + * After that's done, we raise or lower the size of the worker thread pool + * if such an action is appropriate. + */ +void dead_session_purge(void) { + struct CitContext *ptr, *rem; + struct worker_node **node, *tmp; + pthread_t self; + + if ( (time(NULL) - last_purge) < 5 ) return; /* Too soon, go away */ + time(&last_purge); + + do { + rem = NULL; + begin_critical_section(S_SESSION_TABLE); + for (ptr = ContextList; ptr != NULL; ptr = ptr->next) { + if ( (ptr->state == CON_IDLE) && (ptr->kill_me) ) { + rem = ptr; + } + } + end_critical_section(S_SESSION_TABLE); + + /* RemoveContext() enters its own S_SESSION_TABLE critical + * section, so we have to do it like this. + */ + if (rem != NULL) { + lprintf(9, "Purging session %d\n", rem->cs_pid); + RemoveContext(rem); + } + + } while (rem != NULL); + + + /* Raise or lower the size of the worker thread pool if such + * an action is appropriate. + */ + + self = pthread_self(); + + if ( (num_sessions > num_threads) + && (num_threads < config.c_max_workers) ) { + begin_critical_section(S_WORKER_LIST); + create_worker(); + end_critical_section(S_WORKER_LIST); + } + /* don't let the initial thread die since it's responsible for + waiting for all the other threads to terminate. */ + else if ( (num_sessions < num_threads) + && (num_threads > config.c_min_workers) + && (self != initial_thread) ) { + cdb_free_tsd(); + begin_critical_section(S_WORKER_LIST); + --num_threads; + + /* we're exiting before server shutdown... unlink ourself from + the worker list and detach our thread to avoid memory leaks + */ + + for (node = &worker_list; *node != NULL; node = &(*node)->next) + if ((*node)->tid == self) { + tmp = *node; + *node = (*node)->next; + phree(tmp); + break; + } + + pthread_detach(self); + end_critical_section(S_WORKER_LIST); + pthread_exit(NULL); + } + +} + + + + + +/* + * Redirect a session's output to a file or socket. + * This function may be called with a file handle *or* a socket (but not + * both). Call with neither to return output to its normal client socket. + */ +void CtdlRedirectOutput(FILE *fp, int sock) { + + if (fp != NULL) CC->redirect_fp = fp; + else CC->redirect_fp = NULL; + + if (sock > 0) CC->redirect_sock = sock; + else CC->redirect_sock = (-1); + +} + + +/* + * masterCC is the context we use when not attached to a session. This + * function initializes it. + */ +void InitializeMasterCC(void) { + memset(&masterCC, 0, sizeof(struct CitContext)); + masterCC.internal_pgm = 1; + masterCC.cs_pid = 0; +} + + + +/* + * Set up a fd_set containing all the master sockets to which we + * always listen. It's computationally less expensive to just copy + * this to a local fd_set when starting a new select() and then add + * the client sockets than it is to initialize a new one and then + * figure out what to put there. + */ +void init_master_fdset(void) { + struct ServiceFunctionHook *serviceptr; + int m; + + lprintf(9, "Initializing master fdset\n"); + + FD_ZERO(&masterfds); + masterhighest = 0; + + lprintf(9, "Will listen on rescan pipe %d\n", rescan[0]); + FD_SET(rescan[0], &masterfds); + if (rescan[0] > masterhighest) masterhighest = rescan[0]; + + for (serviceptr = ServiceHookTable; serviceptr != NULL; + serviceptr = serviceptr->next ) { + m = serviceptr->msock; + lprintf(9, "Will listen on master socket %d\n", m); + FD_SET(m, &masterfds); + if (m > masterhighest) { + masterhighest = m; + } + } + lprintf(9, "masterhighest = %d\n", masterhighest); +} + + /* * Here's where it all begins. */ int main(int argc, char **argv) { - struct sockaddr_in fsin; /* Data for master socket */ - int alen; /* Data for master socket */ - int ssock; /* Descriptor for master socket */ - THREAD SessThread; /* Thread descriptor */ - pthread_attr_t attr; /* Thread attributes */ - struct CitContext *con; /* Temporary context pointer */ char tracefile[128]; /* Name of file to log traces to */ int a, i; /* General-purpose variables */ - char convbuf[128]; - fd_set readfds; - struct timeval tv; struct passwd *pw; int drop_root_perms = 1; + char *moddir; + struct worker_node *wnp; /* specify default port name and trace file */ strcpy(tracefile, ""); + /* initialize the master context */ + InitializeMasterCC(); + /* parse command-line arguments */ for (a=1; a 0) ? 0 : 1 ) ; - } + } /* -x specifies the desired logging level */ else if (!strncmp(argv[a], "-x", 2)) { - strcpy(convbuf, argv[a]); - verbosity = atoi(&convbuf[2]); - } + verbosity = atoi(&argv[a][2]); + } else if (!strncmp(argv[a], "-h", 2)) { - strcpy(convbuf, argv[a]); - strcpy(bbs_home_directory, &convbuf[2]); + safestrncpy(bbs_home_directory, &argv[a][2], + sizeof bbs_home_directory); home_specified = 1; - } + } + + else if (!strncmp(argv[a], "-f", 2)) { + do_defrag = 1; + } /* -r tells the server not to drop root permissions. don't use * this unless you know what you're doing. this should be @@ -822,37 +969,78 @@ int main(int argc, char **argv) /* any other parameter makes it crash and burn */ else { - lprintf(1, "citserver: usage: "); - lprintf(1, "citserver [-tTraceFile]"); - lprintf(1, " [-d] [-xLogLevel] [-hHomeDir]\n"); + lprintf(1, "citserver: usage: " + "citserver [-tTraceFile] [-d] [-f]" + " [-xLogLevel] [-hHomeDir]\n"); exit(1); - } - } + } + /* Tell 'em who's in da house */ - lprintf(1, "Multithreaded message server for %s\n", CITADEL); - lprintf(1, "Copyright (C) 1987-1999 by Art Cancro. "); - lprintf(1, "All rights reserved.\n\n"); + lprintf(1, +"\nMultithreaded message server for Citadel/UX\n" +"Copyright (C) 1987-2000 by the Citadel/UX development team.\n" +"Citadel/UX is free software, covered by the GNU General Public License, and\n" +"you are welcome to change it and/or distribute copies of it under certain\n" +"conditions. There is absolutely no warranty for this software. Please\n" +"read the 'COPYING.txt' file for details.\n\n"); /* Initialize... */ init_sysdep(); - openlog("citserver",LOG_PID,LOG_USER); + openlog("citserver", LOG_PID, LOG_USER); + /* Load site-specific parameters */ lprintf(7, "Loading citadel.config\n"); get_config(); + + /* + * Do non system dependent startup functions. + */ + master_startup(); + + /* + * Bind the server to a Unix-domain socket. + */ + CtdlRegisterServiceHook(0, + "citadel.socket", + citproto_begin_session, + do_command_loop); + + /* + * Bind the server to our favorite TCP port (usually 504). + */ + CtdlRegisterServiceHook(config.c_port_number, + NULL, + citproto_begin_session, + do_command_loop); + + /* + * Load any server-side modules (plugins) available here. + */ + lprintf(7, "Initializing loadable modules\n"); + if ((moddir = malloc(strlen(bbs_home_directory) + 9)) != NULL) { + sprintf(moddir, "%s/modules", bbs_home_directory); + DLoader_Init(moddir); + free(moddir); + } + /* - * Bind the server to our favourite port. - * There is no need to check for errors, because ig_tcp_server() - * exits if it doesn't succeed. + * The rescan pipe exists so that worker threads can be woken up and + * told to re-scan the context list for fd's to listen on. This is + * necessary, for example, when a context is about to go idle and needs + * to get back on that list. */ - lprintf(7, "Attempting to bind to port %d...\n", config.c_port_number); - msock = ig_tcp_server(config.c_port_number, 5); - lprintf(7, "Listening on socket %d\n", msock); + if (pipe(rescan)) { + lprintf(1, "Can't create rescan pipe!\n"); + exit(errno); + } + + init_master_fdset(); /* - * Now that we've bound the socket, change to the BBS user id and its + * Now that we've bound the sockets, change to the BBS user id and its * corresponding group ids */ if (drop_root_perms) { @@ -861,90 +1049,254 @@ int main(int argc, char **argv) "Group IDs will be incorrect.\n", BBSUID, strerror(errno)); else { - if (initgroups(pw->pw_name, pw->pw_gid)) - lprintf(3, "initgroups(): %s\n", - strerror(errno)); + initgroups(pw->pw_name, pw->pw_gid); if (setgid(pw->pw_gid)) lprintf(3, "setgid(%d): %s\n", pw->pw_gid, strerror(errno)); - } + } lprintf(7, "Changing uid to %d\n", BBSUID); if (setuid(BBSUID) != 0) { lprintf(3, "setuid() failed: %s\n", strerror(errno)); - } } + } - lprintf(7, "Initializing loadable modules\n"); - DLoader_Init("./modules"); - lprintf(9, "Modules done initializing.\n"); + /* We want to check for idle sessions once per minute */ + CtdlRegisterSessionHook(terminate_idle_sessions, EVT_TIMER); /* - * Do non system dependent startup functions. + * Now create a bunch of worker threads. */ - master_startup(); + lprintf(9, "Starting %d worker threads\n", config.c_min_workers-1); + begin_critical_section(S_WORKER_LIST); + for (i=0; i<(config.c_min_workers-1); ++i) { + create_worker(); + } + end_critical_section(S_WORKER_LIST); + + /* Now this thread can become a worker as well. */ + initial_thread = pthread_self(); + worker_thread(NULL); + + /* Server is exiting. Wait for workers to shutdown. */ + lprintf(7, "Waiting for worker threads to shut down\n"); + + begin_critical_section(S_WORKER_LIST); + while (worker_list != NULL) { + wnp = worker_list; + worker_list = wnp->next; + + /* avoid deadlock with an exiting thread */ + end_critical_section(S_WORKER_LIST); + if ((i = pthread_join(wnp->tid, NULL))) + lprintf(1, "pthread_join: %s\n", strerror(i)); + phree(wnp); + begin_critical_section(S_WORKER_LIST); + } + end_critical_section(S_WORKER_LIST); + + master_cleanup(); + + return(0); +} + + +/* + * Bind a thread to a context. (It's inline merely to speed things up.) + */ +inline void become_session(struct CitContext *which_con) { + pthread_setspecific(MyConKey, (void *)which_con ); +} + + + +/* + * This loop just keeps going and going and going... + */ +void *worker_thread(void *arg) { + int i; + char junk; + int highest; + struct CitContext *ptr; + struct CitContext *bind_me = NULL; + fd_set readfds; + int retval; + struct CitContext *con= NULL; /* Temporary context pointer */ + struct ServiceFunctionHook *serviceptr; + struct sockaddr_in fsin; /* Data for master socket */ + int alen; /* Data for master socket */ + int ssock; /* Descriptor for client socket */ + struct timeval tv; + + num_threads++; + + cdb_allocate_tsd(); - /* - * Endless loop. Listen on the master socket. When a connection - * comes in, create a socket, a context, and a thread. - */ while (!time_to_die) { - /* we need to check if a signal has been delivered. because - * syscalls may be restartable across signals, we call - * select with a timeout of 1 second and repeatedly check for - * time_to_die... */ - FD_ZERO(&readfds); - FD_SET(msock, &readfds); - tv.tv_sec = 1; + + /* + * A naive implementation would have all idle threads + * calling select() and then they'd all wake up at once. We + * solve this problem by putting the select() in a critical + * section, so only one thread has the opportunity to wake + * up. If we wake up on a master socket, create a new + * session context; otherwise, just bind the thread to the + * context we want and go on our merry way. + */ + + /* make doubly sure we're not holding any stale db handles + * which might cause a deadlock. + */ + cdb_check_handles(); + + begin_critical_section(S_I_WANNA_SELECT); +SETUP_FD: memcpy(&readfds, &masterfds, sizeof masterfds); + highest = masterhighest; + begin_critical_section(S_SESSION_TABLE); + for (ptr = ContextList; ptr != NULL; ptr = ptr->next) { + if (ptr->state == CON_IDLE) { + FD_SET(ptr->client_socket, &readfds); + if (ptr->client_socket > highest) + highest = ptr->client_socket; + } + } + end_critical_section(S_SESSION_TABLE); + + tv.tv_sec = 1; /* wake up every second if no input */ tv.tv_usec = 0; - if (select(msock + 1, &readfds, NULL, NULL, &tv) <= 0) - continue; - alen = sizeof fsin; - ssock = accept(msock, (struct sockaddr *)&fsin, &alen); - if (ssock < 0) { - lprintf(2, "citserver: accept() failed: %s\n", - strerror(errno)); + + do_select: + if (!time_to_die) + retval = select(highest + 1, &readfds, NULL, NULL, &tv); + else { + end_critical_section(S_I_WANNA_SELECT); + break; + } + + /* Now figure out who made this select() unblock. + * First, check for an error or exit condition. + */ + if (retval < 0) { + if (errno != EINTR) { + lprintf(9, "Exiting (%s)\n", strerror(errno)); + time_to_die = 1; + } else if (!time_to_die) + goto do_select; + } + + /* Next, check to see if it's a new client connecting + * on a master socket. + */ + else for (serviceptr = ServiceHookTable; serviceptr != NULL; + serviceptr = serviceptr->next ) { + + if (FD_ISSET(serviceptr->msock, &readfds)) { + alen = sizeof fsin; + ssock = accept(serviceptr->msock, + (struct sockaddr *)&fsin, &alen); + if (ssock < 0) { + lprintf(2, "citserver: accept(): %s\n", + strerror(errno)); + } + else { + lprintf(7, "citserver: " + "New client socket %d\n", + ssock); + + /* New context will be created already + * set up in the CON_EXECUTING state. + */ + con = CreateNewContext(); + + /* Assign new socket number to it. */ + con->client_socket = ssock; + con->h_command_function = + serviceptr->h_command_function; + + /* Determine whether local socket */ + if (serviceptr->sockpath != NULL) + con->is_local_socket = 1; + + /* Set the SO_REUSEADDR socket option */ + i = 1; + setsockopt(ssock, SOL_SOCKET, + SO_REUSEADDR, + &i, sizeof(i)); + + become_session(con); + begin_session(con); + serviceptr->h_greeting_function(); + become_session(NULL); + con->state = CON_IDLE; + goto SETUP_FD; + } } + } + + /* If the rescan pipe went active, someone is telling this + * thread that the &readfds needs to be refreshed with more + * current data. + */ + if (time_to_die) { + end_critical_section(S_I_WANNA_SELECT); + break; + } + + if (FD_ISSET(rescan[0], &readfds)) { + read(rescan[0], &junk, 1); + goto SETUP_FD; + } + + /* It must be a client socket. Find a context that has data + * waiting on its socket *and* is in the CON_IDLE state. + */ else { - lprintf(7, "citserver: Client socket %d\n", ssock); - lprintf(9, "creating context\n"); - con = CreateNewContext(); - con->client_socket = ssock; - - /* Set the SO_REUSEADDR socket option */ - lprintf(9, "setting socket options\n"); - i = 1; - setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, - &i, sizeof(i)); - - /* set attributes for the new thread */ - lprintf(9, "setting thread attributes\n"); - pthread_attr_init(&attr); - pthread_attr_setdetachstate(&attr, - PTHREAD_CREATE_DETACHED); - - /* now create the thread */ - lprintf(9, "creating thread\n"); - if (pthread_create(&SessThread, &attr, - (void* (*)(void*)) sd_context_loop, - con) - != 0) { - lprintf(1, - "citserver: can't create thread: %s\n", - strerror(errno)); + bind_me = NULL; + begin_critical_section(S_SESSION_TABLE); + for (ptr = ContextList; + ( (ptr != NULL) && (bind_me == NULL) ); + ptr = ptr->next) { + if ( (FD_ISSET(ptr->client_socket, &readfds)) + && (ptr->state == CON_IDLE) ) { + bind_me = ptr; } + } + if (bind_me != NULL) { + /* Found one. Stake a claim to it before + * letting anyone else touch the context list. + */ + bind_me->state = CON_EXECUTING; + } - /* detach the thread - * (defunct -- now done at thread creation time) - * if (pthread_detach(&SessThread) != 0) { - * lprintf(1, - * "citserver: can't detach thread: %s\n", - * strerror(errno)); - * } - */ - lprintf(9, "done!\n"); + end_critical_section(S_SESSION_TABLE); + end_critical_section(S_I_WANNA_SELECT); + + /* We're bound to a session, now do *one* command */ + if (bind_me != NULL) { + become_session(bind_me); + CC->h_command_function(); + become_session(NULL); + bind_me->state = CON_IDLE; + if (bind_me->kill_me == 1) { + RemoveContext(bind_me); + } + write(rescan[1], &junk, 1); } + } - master_cleanup(); - return 0; + dead_session_purge(); + if ((time(NULL) - last_timer) > 60L) { + last_timer = time(NULL); + cdb_check_handles(); /* suggested by Justin Case */ + PerformSessionHooks(EVT_TIMER); + } + + check_sched_shutdown(); } + /* If control reaches this point, the server is shutting down */ + --num_threads; + return NULL; +} + + +