From 4b922c4d6262b1b1cc13f13b7e43b8b6ae19751f Mon Sep 17 00:00:00 2001 From: Dave West Date: Wed, 21 Nov 2007 02:48:48 +0000 Subject: [PATCH] Begun implimentation of a really good thread control interface. As it turns out it impliments a lot of what eCrash does to track threads though I didn't know it at the time. To check it out look in sysdep.c and search on CtdlThread. That will get you close. At the moment nothing makes use of this code. A handy side effect is a thread safe sleep mechanism that will sleep the current thread untill some other thread sleeps or it times out, needs a bit more work to make it sleep any thread without waking when another thread sleeps. Also fixed a potential memory leak or two in the old thread code. --- citadel/housekeeping.c | 2 + citadel/parsedate.c | 187 ++++++++++++-------------- citadel/server.h | 1 + citadel/sysdep.c | 294 ++++++++++++++++++++++++++++++++++++++++- citadel/sysdep_decls.h | 15 +++ 5 files changed, 393 insertions(+), 106 deletions(-) diff --git a/citadel/housekeeping.c b/citadel/housekeeping.c index 3a02ef569..cbc8a1c42 100644 --- a/citadel/housekeeping.c +++ b/citadel/housekeeping.c @@ -168,6 +168,8 @@ void do_housekeeping(void) { JournalRunQueue(); PerformSessionHooks(EVT_HOUSE); /* perform as needed housekeeping */ + + ctdl_internal_thread_gc(0); /* Then, do the "once per minute" stuff... */ if (do_perminute_housekeeping_now) { diff --git a/citadel/parsedate.c b/citadel/parsedate.c index 39a3651a8..5a355fd15 100644 --- a/citadel/parsedate.c +++ b/citadel/parsedate.c @@ -1,7 +1,7 @@ -/* A Bison parser, made by GNU Bison 1.875c. */ +/* A Bison parser, made by GNU Bison 1.875. */ /* Skeleton parser for Yacc-like parsing with Bison, - Copyright (C) 1984, 1989, 1990, 2000, 2001, 2002, 2003 Free Software Foundation, Inc. + Copyright (C) 1984, 1989, 1990, 2000, 2001, 2002 Free Software Foundation, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -223,7 +223,7 @@ typedef union YYSTYPE { enum _MERIDIAN Meridian; } YYSTYPE; /* Line 191 of yacc.c. */ -#line 227 "y.tab.c" +#line 226 "y.tab.c" # define yystype YYSTYPE /* obsolescent; will be withdrawn */ # define YYSTYPE_IS_DECLARED 1 # define YYSTYPE_IS_TRIVIAL 1 @@ -235,29 +235,22 @@ typedef union YYSTYPE { /* Line 214 of yacc.c. */ -#line 239 "y.tab.c" +#line 238 "y.tab.c" #if ! defined (yyoverflow) || YYERROR_VERBOSE -# ifndef YYFREE -# define YYFREE free -# endif -# ifndef YYMALLOC -# define YYMALLOC malloc -# endif - /* The parser invokes alloca or malloc; define the necessary symbols. */ -# ifdef YYSTACK_USE_ALLOCA -# if YYSTACK_USE_ALLOCA -# define YYSTACK_ALLOC alloca -# endif +# if YYSTACK_USE_ALLOCA +# define YYSTACK_ALLOC alloca # else -# if defined (alloca) || defined (_ALLOCA_H) -# define YYSTACK_ALLOC alloca -# else -# ifdef __GNUC__ -# define YYSTACK_ALLOC __builtin_alloca +# ifndef YYSTACK_USE_ALLOCA +# if defined (alloca) || defined (_ALLOCA_H) +# define YYSTACK_ALLOC alloca +# else +# ifdef __GNUC__ +# define YYSTACK_ALLOC __builtin_alloca +# endif # endif # endif # endif @@ -270,15 +263,15 @@ typedef union YYSTYPE { # include /* INFRINGES ON USER NAME SPACE */ # define YYSIZE_T size_t # endif -# define YYSTACK_ALLOC YYMALLOC -# define YYSTACK_FREE YYFREE +# define YYSTACK_ALLOC malloc +# define YYSTACK_FREE free # endif #endif /* ! defined (yyoverflow) || YYERROR_VERBOSE */ #if (! defined (yyoverflow) \ && (! defined (__cplusplus) \ - || (defined (YYSTYPE_IS_TRIVIAL) && YYSTYPE_IS_TRIVIAL))) + || (YYSTYPE_IS_TRIVIAL))) /* A type that is properly aligned for any stack member. */ union yyalloc @@ -299,7 +292,7 @@ union yyalloc /* Copy COUNT objects from FROM to TO. The source and destination do not overlap. */ # ifndef YYCOPY -# if defined (__GNUC__) && 1 < __GNUC__ +# if 1 < __GNUC__ # define YYCOPY(To, From, Count) \ __builtin_memcpy (To, From, (Count) * sizeof (*(From))) # else @@ -432,10 +425,10 @@ static const unsigned short yyrline[] = First, the terminals, then, starting at YYNTOKENS, nonterminals. */ static const char *const yytname[] = { - "$end", "error", "$undefined", "tDAY", "tDAYZONE", "tMERIDIAN", - "tMONTH", "tMONTH_UNIT", "tSEC_UNIT", "tSNUMBER", "tUNUMBER", "tZONE", - "':'", "'/'", "','", "$accept", "spec", "item", "time", "zone", - "numzone", "date", "rel", "o_merid", 0 + "$end", "error", "$undefined", "tDAY", "tDAYZONE", "tMERIDIAN", "tMONTH", + "tMONTH_UNIT", "tSEC_UNIT", "tSNUMBER", "tUNUMBER", "tZONE", "':'", + "'/'", "','", "$accept", "spec", "item", "time", "zone", "numzone", + "date", "rel", "o_merid", 0 }; #endif @@ -560,8 +553,7 @@ static const unsigned char yystos[] = #define YYACCEPT goto yyacceptlab #define YYABORT goto yyabortlab -#define YYERROR goto yyerrorlab - +#define YYERROR goto yyerrlab1 /* Like YYERROR except do call yyerror. This remains here temporarily to ease the transition to the new meaning of YYERROR, for GCC. @@ -595,11 +587,11 @@ while (0) are run). */ #ifndef YYLLOC_DEFAULT -# define YYLLOC_DEFAULT(Current, Rhs, N) \ - ((Current).first_line = (Rhs)[1].first_line, \ - (Current).first_column = (Rhs)[1].first_column, \ - (Current).last_line = (Rhs)[N].last_line, \ - (Current).last_column = (Rhs)[N].last_column) +# define YYLLOC_DEFAULT(Current, Rhs, N) \ + Current.first_line = Rhs[1].first_line; \ + Current.first_column = Rhs[1].first_column; \ + Current.last_line = Rhs[N].last_line; \ + Current.last_column = Rhs[N].last_column; #endif /* YYLEX -- calling `yylex' with the right arguments. */ @@ -643,7 +635,7 @@ do { \ /*------------------------------------------------------------------. | yy_stack_print -- Print the state stack from its BOTTOM up to its | -| TOP (included). | +| TOP (cinluded). | `------------------------------------------------------------------*/ #if defined (__STDC__) || defined (__cplusplus) @@ -683,9 +675,9 @@ yy_reduce_print (yyrule) #endif { int yyi; - unsigned int yylno = yyrline[yyrule]; + unsigned int yylineno = yyrline[yyrule]; YYFPRINTF (stderr, "Reducing stack by rule %d (line %u), ", - yyrule - 1, yylno); + yyrule - 1, yylineno); /* Print the symbols being reduced, and their result. */ for (yyi = yyprhs[yyrule]; 0 <= yyrhs[yyi]; yyi++) YYFPRINTF (stderr, "%s ", yytname [yyrhs[yyi]]); @@ -722,7 +714,7 @@ int yydebug; SIZE_MAX < YYSTACK_BYTES (YYMAXDEPTH) evaluated with infinite-precision integer arithmetic. */ -#if defined (YYMAXDEPTH) && YYMAXDEPTH == 0 +#if YYMAXDEPTH == 0 # undef YYMAXDEPTH #endif @@ -1409,8 +1401,8 @@ yyreduce: } -/* Line 1000 of yacc.c. */ -#line 1414 "y.tab.c" +/* Line 991 of yacc.c. */ +#line 1405 "y.tab.c" yyvsp -= yylen; yyssp -= yylen; @@ -1451,33 +1443,18 @@ yyerrlab: { YYSIZE_T yysize = 0; int yytype = YYTRANSLATE (yychar); - const char* yyprefix; char *yymsg; - int yyx; + int yyx, yycount; + yycount = 0; /* Start YYX at -YYN if negative to avoid negative indexes in YYCHECK. */ - int yyxbegin = yyn < 0 ? -yyn : 0; - - /* Stay within bounds of both yycheck and yytname. */ - int yychecklim = YYLAST - yyn; - int yyxend = yychecklim < YYNTOKENS ? yychecklim : YYNTOKENS; - int yycount = 0; - - yyprefix = ", expecting "; - for (yyx = yyxbegin; yyx < yyxend; ++yyx) + for (yyx = yyn < 0 ? -yyn : 0; + yyx < (int) (sizeof (yytname) / sizeof (char *)); yyx++) if (yycheck[yyx + yyn] == yyx && yyx != YYTERROR) - { - yysize += yystrlen (yyprefix) + yystrlen (yytname [yyx]); - yycount += 1; - if (yycount == 5) - { - yysize = 0; - break; - } - } - yysize += (sizeof ("syntax error, unexpected ") - + yystrlen (yytname[yytype])); + yysize += yystrlen (yytname[yyx]) + 15, yycount++; + yysize += yystrlen ("syntax error, unexpected ") + 1; + yysize += yystrlen (yytname[yytype]); yymsg = (char *) YYSTACK_ALLOC (yysize); if (yymsg != 0) { @@ -1486,13 +1463,16 @@ yyerrlab: if (yycount < 5) { - yyprefix = ", expecting "; - for (yyx = yyxbegin; yyx < yyxend; ++yyx) + yycount = 0; + for (yyx = yyn < 0 ? -yyn : 0; + yyx < (int) (sizeof (yytname) / sizeof (char *)); + yyx++) if (yycheck[yyx + yyn] == yyx && yyx != YYTERROR) { - yyp = yystpcpy (yyp, yyprefix); + const char *yyq = ! yycount ? ", expecting " : " or "; + yyp = yystpcpy (yyp, yyq); yyp = yystpcpy (yyp, yytname[yyx]); - yyprefix = " or "; + yycount++; } } yyerror (yymsg); @@ -1513,56 +1493,52 @@ yyerrlab: /* If just tried and failed to reuse lookahead token after an error, discard it. */ - if (yychar <= YYEOF) + /* Return failure if at end of input. */ + if (yychar == YYEOF) { - /* If at end of input, pop the error token, - then the rest of the stack, then return failure. */ - if (yychar == YYEOF) - for (;;) - { - YYPOPSTACK; - if (yyssp == yyss) - YYABORT; - YYDSYMPRINTF ("Error: popping", yystos[*yyssp], yyvsp, yylsp); - yydestruct (yystos[*yyssp], yyvsp); - } + /* Pop the error token. */ + YYPOPSTACK; + /* Pop the rest of the stack. */ + while (yyss < yyssp) + { + YYDSYMPRINTF ("Error: popping", yystos[*yyssp], yyvsp, yylsp); + yydestruct (yystos[*yyssp], yyvsp); + YYPOPSTACK; + } + YYABORT; } - else - { - YYDSYMPRINTF ("Error: discarding", yytoken, &yylval, &yylloc); - yydestruct (yytoken, &yylval); - yychar = YYEMPTY; - } + YYDSYMPRINTF ("Error: discarding", yytoken, &yylval, &yylloc); + yydestruct (yytoken, &yylval); + yychar = YYEMPTY; + } /* Else will try to reuse lookahead token after shifting the error token. */ - goto yyerrlab1; + goto yyerrlab2; -/*---------------------------------------------------. -| yyerrorlab -- error raised explicitly by YYERROR. | -`---------------------------------------------------*/ -yyerrorlab: +/*----------------------------------------------------. +| yyerrlab1 -- error raised explicitly by an action. | +`----------------------------------------------------*/ +yyerrlab1: -#ifdef __GNUC__ - /* Pacify GCC when the user code never invokes YYERROR and the label - yyerrorlab therefore never appears in user code. */ - if (0) - goto yyerrorlab; + /* Suppress GCC warning that yyerrlab1 is unused when no action + invokes YYERROR. */ +#if defined (__GNUC_MINOR__) && 2093 <= (__GNUC__ * 1000 + __GNUC_MINOR__) \ + && !defined __cplusplus + __attribute__ ((__unused__)) #endif - yyvsp -= yylen; - yyssp -= yylen; - yystate = *yyssp; - goto yyerrlab1; + goto yyerrlab2; -/*-------------------------------------------------------------. -| yyerrlab1 -- common code for both syntax error and YYERROR. | -`-------------------------------------------------------------*/ -yyerrlab1: + +/*---------------------------------------------------------------. +| yyerrlab2 -- pop states until the error token can be shifted. | +`---------------------------------------------------------------*/ +yyerrlab2: yyerrstatus = 3; /* Each real token shifted decrements this. */ for (;;) @@ -1585,8 +1561,9 @@ yyerrlab1: YYDSYMPRINTF ("Error: popping", yystos[*yyssp], yyvsp, yylsp); yydestruct (yystos[yystate], yyvsp); - YYPOPSTACK; - yystate = *yyssp; + yyvsp--; + yystate = *--yyssp; + YY_STACK_PRINT (yyss, yyssp); } diff --git a/citadel/server.h b/citadel/server.h index 02b0de9c7..95c820ef8 100644 --- a/citadel/server.h +++ b/citadel/server.h @@ -242,6 +242,7 @@ enum { S_CHKPWD, S_LOG, S_NETSPOOL, + S_THREAD_LIST, MAX_SEMAPHORES }; diff --git a/citadel/sysdep.c b/citadel/sysdep.c index 14676acfb..43a4bdec2 100644 --- a/citadel/sysdep.c +++ b/citadel/sysdep.c @@ -786,7 +786,7 @@ void sysdep_master_cleanup(void) { #ifdef HAVE_OPENSSL destruct_ssl(); #endif - serv_calendar_destroy(); + serv_calendar_destroy(); // FIXME: Shouldn't be here, should be by a cleanup hook surely. CtdlDestroyProtoHooks(); CtdlDestroyDeleteHooks(); CtdlDestroyXmsgHooks(); @@ -944,6 +944,292 @@ int convert_login(char NameToConvert[]) { } } + + +/* + * New thread interface. + * To create a thread you must call one of the create thread functions. + * You must pass it the address of (a pointer to a CtdlThreadNode initialised to NULL) like this + * struct CtdlThreadNode *node = NULL; + * pass in &node + * If the thread is created *node will point to the thread control structure for the created thread. + * If the thread creation fails *node remains NULL + * Do not free the memory pointed to by *node, it doesn't belong to you. + * If your thread function returns it will be started again without creating a new thread. + * If your thread function wants to exit it should call CtdlThreadExit(ret_code); + * This new interface duplicates much of the eCrash stuff. We should go for closer integration since that would + * remove the need for the calls to eCrashRegisterThread and friends + */ + +// FIXME: these defines should be else where +#define CTDLTHREAD_BIGSTACK 0x0001 + +struct CtdlThreadNode *CtdlThreadList = NULL; +static pthread_mutex_t ThreadWaiterMutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t ThreadWaiterCond = PTHREAD_COND_INITIALIZER; + + +/* + * So we now have a sleep command that works with threads but it is in seconds + */ +void CtdlThreadSleep(int secs) +{ + + struct timespec wake_time; + struct timeval time_now; + + + memset (&wake_time, 0, sizeof(struct timespec)); + gettimeofday(&time_now, NULL); + wake_time.tv_sec = time_now.tv_sec + secs; + pthread_cond_timedwait(&ThreadWaiterCond, &ThreadWaiterMutex, &wake_time); +} + + +/* + * Routine to clean up our thread function on exit + */ +void ctdl_internal_thread_cleanup(void *arg) +{ + struct CtdlThreadNode *this_thread; + // arg is a pointer to our thread structure + this_thread = (struct CtdlThreadNode *) arg; + if (this_thread->valid) + { + /* + * In here we were called by the current thread because it is exiting + * NB. WE ARE THE CURRENT THREAD + */ + CtdlLogPrintf(CTDL_NOTICE, "Thread \"%s\" (%ld) exited.\n", this_thread->name, this_thread->tid); + this_thread->running = FALSE; + this_thread->valid = FALSE; // needs to be last thing else house keeping will unlink us too early + /* + * Our thread is exiting either because it wanted to end or because the server is stopping + * We need to clean up + */ + #ifdef HAVE_BACKTRACE + eCrash_UnregisterThread(); + #endif + } + else + { + if (this_thread->tid == pthread_self()) + { + CtdlLogPrintf(CTDL_EMERG, "Thread system PANIC a thread is trying to clean up after itself.\n"); + time_to_die = -1; + return; + } + /* + * In here we were called by some other thread that wants to clean up any dead threads + * NB. WE ARE NOT THE THREAD BEING CLEANED + */ + // We probably got called by house keeping or master shutdown so we unlink the dead threads here + num_threads--; + + begin_critical_section(S_THREAD_LIST); + if(this_thread->name) + free(this_thread->name); + if(this_thread->prev) + this_thread->prev->next = this_thread->next; + if(this_thread->next) + this_thread->next->prev = this_thread->next; + end_critical_section(S_THREAD_LIST); + free(this_thread); + } +} + + +/* + * Garbage collection routine. + * Gets called by do_housekeeping() and in master_cleanup() to clean up the thread list + */ +void ctdl_internal_thread_gc (int shutdown) +{ + struct CtdlThreadNode *this_thread, *that_thread; + + this_thread = CtdlThreadList; + while(this_thread) + { + that_thread = this_thread; + this_thread = this_thread->next; + + if(shutdown && that_thread->valid) + { // We want the threads to shutdown so first ask it nicely + that_thread->running = FALSE; + // Wait for it to exit + CtdlThreadSleep(1); + + if(that_thread->valid) // Be more brutal about it + pthread_cancel (that_thread->tid); + // Wait for it to exit + CtdlThreadSleep(1); + } + + if (that_thread->valid == FALSE) + { + CtdlLogPrintf(CTDL_NOTICE, "Joining thread \"%s\" (%ld)\n", that_thread->name, that_thread->tid); + pthread_join(that_thread->tid, NULL); + ctdl_internal_thread_cleanup(that_thread); + } + } +} + +/* + * Runtime function for a Citadel Thread. + * This initialises the threads environment and then calls the user supplied thread function + * Note that this is the REAL thread function and wraps the users thread function. + */ +void *ctdl_internal_thread_func (void *arg) +{ + struct CtdlThreadNode *this_thread; + void *ret = NULL; + + // Get our thread data structure + this_thread = (struct CtdlThreadNode *) arg; + // Tell the world we are here + CtdlLogPrintf(CTDL_NOTICE, "Spawned a new thread \"%s\" (%ld). \n", this_thread->name, this_thread->tid); + + num_threads++; // Increase the count of threads in the system. + + // Register for tracing + #ifdef HAVE_BACKTRACE + eCrash_RegisterThread(this_thread->name, 0); + #endif + + // Register the cleanup function to take care of when we exit. + pthread_cleanup_push(ctdl_internal_thread_cleanup, arg); + + this_thread->running = TRUE; + + while ((!time_to_die) && (this_thread->running)) + { // Call the users thread function + ret = (this_thread->thread_func)(this_thread->user_args); + } + + /* + * Our thread is exiting either because it wanted to end or because the server is stopping + * We need to clean up + */ + #ifdef HAVE_BACKTRACE + eCrash_UnregisterThread(); + #endif + + pthread_cleanup_pop(1); // Execute our cleanup routine and remove it + + return(ret); +} + + + +/* + * Internal function to create a thread. + * Must be called from within a S_THREAD_LIST critical section + */ +int ctdl_internal_create_thread(char *name, int flags, void *(*thread_func) (void *arg), void *arg, struct CtdlThreadNode **new_thread) +{ + int ret = 0; + pthread_attr_t attr; + struct CtdlThreadNode *this_thread; + + if (*new_thread) + { + lprintf(CTDL_EMERG, "Possible attempt to overwrite an existing thread!!!\n"); + return -1; + } + + this_thread = malloc(sizeof(struct CtdlThreadNode)); + if (this_thread == NULL) { + lprintf(CTDL_EMERG, "can't allocate CtdlThreadNode, exiting\n"); + return ret; + } + // Ensuring this is zero'd means we make sure the thread doesn't start doing its thing until we are ready. + memset (this_thread, 0, sizeof(struct CtdlThreadNode)); + + if ((ret = pthread_attr_init(&attr))) { + lprintf(CTDL_EMERG, "pthread_attr_init: %s\n", strerror(ret)); + free(this_thread); + return ret; + } + + /* Our per-thread stacks need to be bigger than the default size, + * otherwise the MIME parser crashes on FreeBSD, and the IMAP service + * crashes on 64-bit Linux. + */ + if (flags & CTDLTHREAD_BIGSTACK) + { + if ((ret = pthread_attr_setstacksize(&attr, THREADSTACKSIZE))) { + lprintf(CTDL_EMERG, "pthread_attr_setstacksize: %s\n", + strerror(ret)); + pthread_attr_destroy(&attr); + free(this_thread); + return ret; + } + } + + /* + * If we got here we are going to create the thread so we must initilise the structure + * first because most implimentations of threading can't create it in a stopped state + * and it might want to do things with its structure that aren't initialised otherwise. + */ + if(name) + { + this_thread->name = strdup(name); + } + else + { + this_thread->name = strdup("Unknown Thread"); + } + this_thread->flags = flags; + this_thread->thread_func = thread_func; + this_thread->user_args = arg; + this_thread->valid = 1; // Need this to prevent house keeping unlinking us from the list + /* + * We pass this_thread into the thread as its args so that it can find out information + * about itself and it has a bit of storage space for itself, not to mention that the REAL + * thread function needs to finish off the setup of the structure + */ + if ((ret = pthread_create(&this_thread->tid, &attr, ctdl_internal_thread_func, this_thread) != 0)) + { + + lprintf(CTDL_ALERT, "Can't create thread: %s\n", + strerror(ret)); + if (this_thread->name) + free (this_thread->name); + free(this_thread); + pthread_attr_destroy(&attr); + return ret; + } + + this_thread->next = CtdlThreadList; + CtdlThreadList = this_thread; + *new_thread = this_thread; + pthread_attr_destroy(&attr); + return 0; +} + +/* + * Wrapper function to create a thread + * ensures the critical section and other protections are in place. + * char *name = name to give to thread, if NULL, use generic name + * int flags = flags to determine type of thread and standard facilities + */ +int CtdlCreateThread(char *name, int flags, void *(*thread_func) (void *arg), void *arg, struct CtdlThreadNode **new_thread) +{ + int ret; + + begin_critical_section(S_THREAD_LIST); + ret = ctdl_internal_create_thread(name, flags, thread_func, arg, new_thread); + end_critical_section(S_THREAD_LIST); + return ret; +} + + + +/* + * Old thread interface. + */ + + struct worker_node *worker_list = NULL; @@ -966,6 +1252,7 @@ void create_worker(void) { if ((ret = pthread_attr_init(&attr))) { lprintf(CTDL_EMERG, "pthread_attr_init: %s\n", strerror(ret)); time_to_die = -1; + free(n); return; } @@ -978,6 +1265,7 @@ void create_worker(void) { strerror(ret)); time_to_die = -1; pthread_attr_destroy(&attr); + free(n); return; } @@ -986,6 +1274,10 @@ void create_worker(void) { lprintf(CTDL_ALERT, "Can't create worker thread: %s\n", strerror(ret)); + time_to_die = -1; + pthread_attr_destroy(&attr); + free(n); + return; } n->next = worker_list; diff --git a/citadel/sysdep_decls.h b/citadel/sysdep_decls.h index 630d117e3..ab8745c79 100644 --- a/citadel/sysdep_decls.h +++ b/citadel/sysdep_decls.h @@ -84,6 +84,8 @@ void InitializeMasterCC(void); void init_master_fdset(void); void create_worker(void); void InitialiseSemaphores(void); +void ctdl_internal_thread_gc (int shutdown); + extern int num_sessions; extern volatile int time_to_die; @@ -99,6 +101,19 @@ extern struct worker_node { struct worker_node *next; } *worker_list; + +extern struct CtdlThreadNode { + pthread_t tid; + char *name; + void *(*thread_func) (void *arg); + void *user_args; + int flags; + int running; + int valid; + struct CtdlThreadNode *prev; + struct CtdlThreadNode *next; +} *CtdlThreadList; + extern int SyslogFacility(char *name); extern int syslog_facility; -- 2.30.2