59509ce99c9dd30b64786afa5ebd131d65a89913
[yaz-moved-to-github.git] / src / statserv.c
1 /* This file is part of the YAZ toolkit.
2  * Copyright (C) 1995-2008 Index Data
3  * See the file LICENSE for details.
4  */
5
6 /**
7  * \file statserv.c
8  * \brief Implements GFS logic
9  */
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <string.h>
14 #include <ctype.h>
15
16 #ifdef WIN32
17 #include <process.h>
18 #include <winsock.h>
19 #include <direct.h>
20 #endif
21
22 #include <yaz/sc.h>
23
24 #if HAVE_SYS_TYPES_H
25 #include <sys/types.h>
26 #endif
27 #if HAVE_SYS_WAIT_H
28 #include <sys/wait.h>
29 #endif
30 #if HAVE_UNISTD_H
31 #include <unistd.h>
32 #endif
33 #if HAVE_PWD_H
34 #include <pwd.h>
35 #endif
36
37 #if YAZ_HAVE_XML2
38 #include <libxml/parser.h>
39 #include <libxml/tree.h>
40 #include <libxml/xinclude.h>
41 #endif
42
43 #if YAZ_POSIX_THREADS
44 #include <pthread.h>
45 #elif YAZ_GNU_THREADS
46 #include <pth.h>
47 #endif
48
49 #include <fcntl.h>
50 #include <signal.h>
51 #include <errno.h>
52
53 #include <yaz/comstack.h>
54 #include <yaz/tcpip.h>
55 #include <yaz/options.h>
56 #ifdef USE_XTIMOSI
57 #include <yaz/xmosi.h>
58 #endif
59 #include <yaz/log.h>
60 #include "eventl.h"
61 #include "session.h"
62 #include <yaz/statserv.h>
63
64 static IOCHAN pListener = NULL;
65
66 static char gfs_root_dir[FILENAME_MAX+1];
67 static struct gfs_server *gfs_server_list = 0;
68 static struct gfs_listen *gfs_listen_list = 0;
69 static NMEM gfs_nmem = 0;
70
71 static char *me = "statserver"; /* log prefix */
72 static char *programname="statserver"; /* full program name */
73 #ifdef WIN32
74 DWORD current_control_tls;
75 static int init_control_tls = 0;
76 #elif YAZ_POSIX_THREADS
77 static pthread_key_t current_control_tls;
78 static int init_control_tls = 0;
79 #else
80 static statserv_options_block *current_control_block = 0;
81 #endif
82
83 /*
84  * default behavior.
85  */
86 #define STAT_DEFAULT_LOG_LEVEL "server,session,request"
87
88 int check_options(int argc, char **argv);
89 statserv_options_block control_block = {
90     1,                          /* dynamic mode */
91     0,                          /* threaded mode */
92     0,                          /* one shot (single session) */
93     "",                         /* no PDUs */
94     "",                         /* diagnostic output to stderr */
95     "tcp:@:9999",               /* default listener port */
96     PROTO_Z3950,                /* default application protocol */
97     15,                         /* idle timeout (minutes) */
98     1024*1024,                  /* maximum PDU size (approx.) to allow */
99     "default-config",           /* configuration name to pass to backend */
100     "",                         /* set user id */
101     0,                          /* bend_start handler */
102     0,                          /* bend_stop handler */
103     check_options,              /* Default routine, for checking the run-time arguments */
104     check_ip_tcpd,
105     "",
106     0,                          /* default value for inet deamon */
107     0,                          /* handle (for service, etc) */
108     0,                          /* bend_init handle */
109     0,                          /* bend_close handle */
110 #ifdef WIN32
111     "Z39.50 Server",            /* NT Service Name */
112     "Server",                   /* NT application Name */
113     "",                         /* NT Service Dependencies */
114     "Z39.50 Server",            /* NT Service Display Name */
115 #endif /* WIN32 */
116     0,                          /* SOAP handlers */
117     "",                         /* PID fname */
118     0,                          /* background daemon */
119     "",                         /* SSL certificate filename */
120     ""                          /* XML config filename */
121 };
122
123 static int max_sessions = 0;
124
125 static int logbits_set = 0;
126 static int log_session = 0; /* one-line logs for session */
127 static int log_sessiondetail = 0; /* more detailed stuff */
128 static int log_server = 0;
129
130 /** get_logbits sets global loglevel bits */
131 static void get_logbits(int force)
132 { /* needs to be called after parsing cmd-line args that can set loglevels!*/
133     if (force || !logbits_set)
134     {
135         logbits_set = 1;
136         log_session = yaz_log_module_level("session");
137         log_sessiondetail = yaz_log_module_level("sessiondetail");
138         log_server = yaz_log_module_level("server");
139     }
140 }
141
142
143 static int add_listener(char *where, int listen_id);
144
145 #if YAZ_HAVE_XML2
146 static xmlDocPtr xml_config_doc = 0;
147 #endif
148
149 #if YAZ_HAVE_XML2
150 static xmlNodePtr xml_config_get_root(void)
151 {
152     xmlNodePtr ptr = 0;
153     if (xml_config_doc)
154     {
155         ptr = xmlDocGetRootElement(xml_config_doc);
156         if (!ptr || ptr->type != XML_ELEMENT_NODE ||
157             strcmp((const char *) ptr->name, "yazgfs"))
158         {
159             yaz_log(YLOG_WARN, "Bad/missing root element for config %s",
160                     control_block.xml_config);
161             return 0;
162         
163         }
164     }
165     return ptr;
166 }
167 #endif
168
169 #if YAZ_HAVE_XML2
170 static char *nmem_dup_xml_content(NMEM n, xmlNodePtr ptr)
171 {
172     unsigned char *cp;
173     xmlNodePtr p;
174     int len = 1;  /* start with 1, because of trailing 0 */
175     unsigned char *str;
176     int first = 1; /* whitespace lead flag .. */
177     /* determine length */
178     for (p = ptr; p; p = p->next)
179     {
180         if (p->type == XML_TEXT_NODE)
181             len += xmlStrlen(p->content);
182     }
183     /* now allocate for the string */
184     str = (unsigned char *) nmem_malloc(n, len);
185     *str = '\0'; /* so we can use strcat */
186     for (p = ptr; p; p = p->next)
187     {
188         if (p->type == XML_TEXT_NODE)
189         {
190             cp = p->content;
191             if (first)
192             {
193                 while(*cp && isspace(*cp))
194                     cp++;
195                 if (*cp)
196                     first = 0;  /* reset if we got non-whitespace out */
197             }
198             strcat((char *)str, (const char *)cp); /* append */
199         }
200     }
201     /* remove trailing whitespace */
202     cp = strlen((const char *)str) + str;
203     while (cp != str && isspace(cp[-1]))
204         cp--;
205     *cp = '\0';
206     /* return resulting string */
207     return (char *) str;
208 }
209 #endif
210
211 #if YAZ_HAVE_XML2
212 static struct gfs_server * gfs_server_new(void)
213 {
214     struct gfs_server *n = (struct gfs_server *)
215         nmem_malloc(gfs_nmem, sizeof(*n));
216     memcpy(&n->cb, &control_block, sizeof(control_block));
217     n->next = 0;
218     n->host = 0;
219     n->listen_ref = 0;
220     n->cql_transform = 0;
221     n->ccl_transform = 0;
222     n->server_node_ptr = 0;
223     n->directory = 0;
224     n->docpath = 0;
225     n->stylesheet = 0;
226     n->retrieval = yaz_retrieval_create();
227     return n;
228 }
229 #endif
230
231 #if YAZ_HAVE_XML2
232 static struct gfs_listen * gfs_listen_new(const char *id, 
233                                           const char *address)
234 {
235     struct gfs_listen *n = (struct gfs_listen *)
236         nmem_malloc(gfs_nmem, sizeof(*n));
237     if (id)
238         n->id = nmem_strdup(gfs_nmem, id);
239     else
240         n->id = 0;
241     n->next = 0;
242     n->address = nmem_strdup(gfs_nmem, address);
243     return n;
244 }
245 #endif
246
247 static void gfs_server_chdir(struct gfs_server *gfs)
248 {
249     if (gfs_root_dir[0])
250     {
251         if (chdir(gfs_root_dir))
252             yaz_log(YLOG_WARN|YLOG_ERRNO, "chdir %s", gfs_root_dir);
253     }
254     if (gfs->directory)
255     {
256         if (chdir(gfs->directory))
257             yaz_log(YLOG_WARN|YLOG_ERRNO, "chdir %s",
258                     gfs->directory);
259     }
260 }
261
262 int control_association(association *assoc, const char *host, int force_open)
263 {
264     char vhost[128], *cp;
265     if (host)
266     {
267         strncpy(vhost, host, 127);
268         vhost[127] = '\0';
269         cp = strchr(vhost, ':');
270         if (cp)
271             *cp = '\0';
272         host = vhost;
273     }
274     assoc->server = 0;
275     if (control_block.xml_config[0])
276     {
277         struct gfs_server *gfs;
278         for (gfs = gfs_server_list; gfs; gfs = gfs->next)
279         {
280             int listen_match = 0;
281             int host_match = 0;
282             if ( !gfs->host || (host && gfs->host && !strcmp(host, gfs->host)))
283                 host_match = 1;
284             if (!gfs->listen_ref ||
285                 gfs->listen_ref == assoc->client_chan->chan_id)
286                 listen_match = 1;
287             if (listen_match && host_match)
288             {
289                 if (force_open ||
290                     (assoc->last_control != &gfs->cb && assoc->backend))
291                 {
292                     statserv_setcontrol(assoc->last_control);
293                     if (assoc->backend && assoc->init)
294                     {
295                         gfs_server_chdir(gfs);
296                         (assoc->last_control->bend_close)(assoc->backend);
297                     }
298                     assoc->backend = 0;
299                     xfree(assoc->init);
300                     assoc->init = 0;
301                 }
302                 assoc->server = gfs;
303                 assoc->last_control = &gfs->cb;
304                 statserv_setcontrol(&gfs->cb);
305                 
306                 gfs_server_chdir(gfs);
307                 break;
308             }
309         }
310         if (!gfs)
311         {
312             statserv_setcontrol(0);
313             assoc->last_control = 0;
314             return 0;
315         }
316     }
317     else
318     {
319         statserv_setcontrol(&control_block);
320         assoc->last_control = &control_block;
321     }
322     yaz_log(YLOG_DEBUG, "server select: config=%s", 
323             assoc->last_control->configname);
324
325     assoc->maximumRecordSize = assoc->last_control->maxrecordsize;
326     assoc->preferredMessageSize = assoc->last_control->maxrecordsize;
327     cs_set_max_recv_bytes(assoc->client_link, assoc->maximumRecordSize);
328     return 1;
329 }
330
331 #if YAZ_HAVE_XML2
332 static void xml_config_read(void)
333 {
334     struct gfs_server **gfsp = &gfs_server_list;
335     struct gfs_listen **gfslp = &gfs_listen_list;
336     xmlNodePtr ptr = xml_config_get_root();
337
338     if (!ptr)
339         return;
340     for (ptr = ptr->children; ptr; ptr = ptr->next)
341     {
342         struct _xmlAttr *attr;
343         if (ptr->type != XML_ELEMENT_NODE)
344             continue;
345         attr = ptr->properties;
346         if (!strcmp((const char *) ptr->name, "listen"))
347         {
348             /*
349               <listen id="listenerid">tcp:@:9999</listen>
350             */
351             const char *id = 0;
352             const char *address =
353                 nmem_dup_xml_content(gfs_nmem, ptr->children);
354             for ( ; attr; attr = attr->next)
355                 if (!xmlStrcmp(attr->name, BAD_CAST "id")
356                     && attr->children && attr->children->type == XML_TEXT_NODE)
357                     id = nmem_dup_xml_content(gfs_nmem, attr->children);
358             if (address)
359             {
360                 *gfslp = gfs_listen_new(id, address);
361                 gfslp = &(*gfslp)->next;
362                 *gfslp = 0; /* make listener list consistent for search */
363             }
364         }
365         else if (!strcmp((const char *) ptr->name, "server"))
366         {
367             xmlNodePtr ptr_server = ptr;
368             xmlNodePtr ptr;
369             const char *listenref = 0;
370             const char *id = 0;
371             struct gfs_server *gfs;
372
373             for ( ; attr; attr = attr->next)
374                 if (!xmlStrcmp(attr->name, BAD_CAST "listenref") 
375                     && attr->children && attr->children->type == XML_TEXT_NODE)
376                     listenref = nmem_dup_xml_content(gfs_nmem, attr->children);
377                 else if (!xmlStrcmp(attr->name, BAD_CAST "id")
378                          && attr->children
379                          && attr->children->type == XML_TEXT_NODE)
380                     id = nmem_dup_xml_content(gfs_nmem, attr->children);
381                 else
382                     yaz_log(YLOG_WARN, "Unknown attribute '%s' for server",
383                             attr->name);
384             gfs = *gfsp = gfs_server_new();
385             gfs->server_node_ptr = ptr_server;
386             if (listenref)
387             {
388                 int id_no;
389                 struct gfs_listen *gl = gfs_listen_list;
390                 for (id_no = 1; gl; gl = gl->next, id_no++)
391                     if (gl->id && !strcmp(gl->id, listenref))
392                     {
393                         gfs->listen_ref = id_no;
394                         break;
395                     }
396                 if (!gl)
397                     yaz_log(YLOG_WARN, "Non-existent listenref '%s' in server "
398                             "config element", listenref);
399             }
400             for (ptr = ptr_server->children; ptr; ptr = ptr->next)
401             {
402                 if (ptr->type != XML_ELEMENT_NODE)
403                     continue;
404                 if (!strcmp((const char *) ptr->name, "host"))
405                 {
406                     gfs->host = nmem_dup_xml_content(gfs_nmem,
407                                                      ptr->children);
408                 }
409                 else if (!strcmp((const char *) ptr->name, "config"))
410                 {
411                     strcpy(gfs->cb.configname,
412                            nmem_dup_xml_content(gfs_nmem, ptr->children));
413                 }
414                 else if (!strcmp((const char *) ptr->name, "cql2rpn"))
415                 {
416                     gfs->cql_transform = cql_transform_open_fname(
417                         nmem_dup_xml_content(gfs_nmem, ptr->children)
418                         );
419                 }
420                 else if (!strcmp((const char *) ptr->name, "ccl2rpn"))
421                 {
422                     char *name;
423                     FILE *f;
424
425                     name = nmem_dup_xml_content(gfs_nmem, ptr->children);
426                     if ((f = fopen(name, "r")) == 0) {
427                         yaz_log(YLOG_FATAL, "can't open CCL file '%s'", name);
428                         exit(1);
429                     }
430                     gfs->ccl_transform = ccl_qual_mk();
431                     ccl_qual_file (gfs->ccl_transform, f);
432                     fclose(f);
433                 }
434                 else if (!strcmp((const char *) ptr->name, "directory"))
435                 {
436                     gfs->directory = 
437                         nmem_dup_xml_content(gfs_nmem, ptr->children);
438                 }
439                 else if (!strcmp((const char *) ptr->name, "docpath"))
440                 {
441                     gfs->docpath = 
442                         nmem_dup_xml_content(gfs_nmem, ptr->children);
443                 }
444                 else if (!strcmp((const char *) ptr->name, "maximumrecordsize"))
445                 {
446                     gfs->cb.maxrecordsize = atoi(
447                         nmem_dup_xml_content(gfs_nmem, ptr->children));
448                 }
449                 else if (!strcmp((const char *) ptr->name, "stylesheet"))
450                 {
451                     char *s = nmem_dup_xml_content(gfs_nmem, ptr->children);
452                     gfs->stylesheet = (char *)
453                         nmem_malloc(gfs_nmem, strlen(s) + 2);
454                     sprintf(gfs->stylesheet, "/%s", s);
455                 }
456                 else if (!strcmp((const char *) ptr->name, "explain"))
457                 {
458                     ; /* being processed separately */
459                 }
460                 else if (!strcmp((const char *) ptr->name, "retrievalinfo"))
461                 {
462                     if (yaz_retrieval_configure(gfs->retrieval, ptr))
463                     {       
464                         yaz_log(YLOG_FATAL, "%s in config %s",
465                                 yaz_retrieval_get_error(gfs->retrieval),
466                                 control_block.xml_config);
467                         exit(1);
468                     }
469                 }
470                 else
471                 {
472                     yaz_log(YLOG_FATAL, "Unknown element '%s' in config %s",
473                             ptr->name, control_block.xml_config);
474                     exit(1);
475                 }
476             }
477             gfsp = &(*gfsp)->next;
478         }
479     }
480     *gfsp = 0;
481 }
482 #endif
483
484 static void xml_config_open(void)
485 {
486     if (!getcwd(gfs_root_dir, FILENAME_MAX))
487     {
488         yaz_log(YLOG_WARN|YLOG_ERRNO, "getcwd failed");
489         gfs_root_dir[0] = '\0';
490     }
491 #ifdef WIN32
492     init_control_tls = 1;
493     current_control_tls = TlsAlloc();
494 #elif YAZ_POSIX_THREADS
495     init_control_tls = 1;
496     pthread_key_create(&current_control_tls, 0);
497 #endif
498     
499     gfs_nmem = nmem_create();
500 #if YAZ_HAVE_XML2
501     if (control_block.xml_config[0] == '\0')
502         return;
503
504     if (!xml_config_doc)
505     {
506         xml_config_doc = xmlParseFile(control_block.xml_config);
507         if (!xml_config_doc)
508         {
509             yaz_log(YLOG_FATAL, "Could not parse %s", control_block.xml_config);
510             exit(1);
511         }
512         else
513         {
514             int noSubstitutions = xmlXIncludeProcess(xml_config_doc);
515             if (noSubstitutions == -1)
516             {
517                 yaz_log(YLOG_WARN, "XInclude processing failed for config %s",
518                         control_block.xml_config);
519                 exit(1);
520             }
521         }
522     }
523     xml_config_read();
524 #endif
525 }
526
527 static void xml_config_close(void)
528 {
529 #if YAZ_HAVE_XML2
530     if (xml_config_doc)
531     {
532         xmlFreeDoc(xml_config_doc);
533         xml_config_doc = 0;
534     }
535 #endif
536     gfs_server_list = 0;
537     nmem_destroy(gfs_nmem);
538 #ifdef WIN32
539     if (init_control_tls)
540         TlsFree(current_control_tls);
541 #elif YAZ_POSIX_THREADS
542     if (init_control_tls)
543         pthread_key_delete(current_control_tls);
544 #endif
545 }
546
547 static void xml_config_add_listeners(void)
548 {
549     struct gfs_listen *gfs = gfs_listen_list;
550     int id_no;
551
552     for (id_no = 1; gfs; gfs = gfs->next, id_no++)
553     {
554         if (gfs->address)
555             add_listener(gfs->address, id_no);
556     }
557 }
558
559 static void xml_config_bend_start(void)
560 {
561     if (control_block.xml_config[0])
562     {
563         struct gfs_server *gfs = gfs_server_list;
564         for (; gfs; gfs = gfs->next)
565         {
566             yaz_log(YLOG_DEBUG, "xml_config_bend_start config=%s",
567                     gfs->cb.configname);
568             statserv_setcontrol(&gfs->cb);
569             if (control_block.bend_start)
570             {
571                 gfs_server_chdir(gfs);
572                 (control_block.bend_start)(&gfs->cb);
573             }
574         }
575     }
576     else
577     {
578         yaz_log(YLOG_DEBUG, "xml_config_bend_start default config");
579         statserv_setcontrol(&control_block);
580         if (control_block.bend_start)
581             (*control_block.bend_start)(&control_block);
582     }
583 }
584
585 static void xml_config_bend_stop(void)
586 {
587     if (control_block.xml_config[0])
588     {
589         struct gfs_server *gfs = gfs_server_list;
590         for (; gfs; gfs = gfs->next)
591         {
592             yaz_log(YLOG_DEBUG, "xml_config_bend_stop config=%s",
593                     gfs->cb.configname);
594             statserv_setcontrol(&gfs->cb);
595             if (control_block.bend_stop)
596                 (control_block.bend_stop)(&gfs->cb);
597         }
598     }
599     else
600     {
601         yaz_log(YLOG_DEBUG, "xml_config_bend_stop default config");
602         statserv_setcontrol(&control_block);
603         if (control_block.bend_stop)
604             (*control_block.bend_stop)(&control_block);
605     }
606 }
607
608 /*
609  * handle incoming connect requests.
610  * The dynamic mode is a bit tricky mostly because we want to avoid
611  * doing all of the listening and accepting in the parent - it's
612  * safer that way.
613  */
614 #ifdef WIN32
615
616 typedef struct _ThreadList ThreadList;
617
618 struct _ThreadList
619 {
620     HANDLE hThread;
621     IOCHAN pIOChannel;
622     ThreadList *pNext;
623 };
624
625 static ThreadList *pFirstThread;
626 static CRITICAL_SECTION Thread_CritSect;
627 static BOOL bInitialized = FALSE;
628
629 static void ThreadList_Initialize()
630 {
631     /* Initialize the critical Sections */
632     InitializeCriticalSection(&Thread_CritSect);
633
634     /* Set the first thraed */
635     pFirstThread = NULL;
636
637     /* we have been initialized */
638     bInitialized = TRUE;
639 }
640
641 static void statserv_add(HANDLE hThread, IOCHAN pIOChannel)
642 {
643     /* Only one thread can go through this section at a time */
644     EnterCriticalSection(&Thread_CritSect);
645
646     {
647         /* Lets create our new object */
648         ThreadList *pNewThread = (ThreadList *)malloc(sizeof(ThreadList));
649         pNewThread->hThread = hThread;
650         pNewThread->pIOChannel = pIOChannel;
651         pNewThread->pNext = pFirstThread;
652         pFirstThread = pNewThread;
653
654         /* Lets let somebody else create a new object now */
655         LeaveCriticalSection(&Thread_CritSect);
656     }
657 }
658
659 void statserv_remove(IOCHAN pIOChannel)
660 {
661     /* Only one thread can go through this section at a time */
662     EnterCriticalSection(&Thread_CritSect);
663
664     {
665         ThreadList *pCurrentThread = pFirstThread;
666         ThreadList *pNextThread;
667         ThreadList *pPrevThread =NULL;
668
669         /* Step through all the threads */
670         for (; pCurrentThread != NULL; pCurrentThread = pNextThread)
671         {
672             /* We only need to compare on the IO Channel */
673             if (pCurrentThread->pIOChannel == pIOChannel)
674             {
675                 /* We have found the thread we want to delete */
676                 /* First of all reset the next pointers */
677                 if (pPrevThread == NULL)
678                     pFirstThread = pCurrentThread->pNext;
679                 else
680                     pPrevThread->pNext = pCurrentThread->pNext;
681
682                 /* All we need todo now is delete the memory */
683                 free(pCurrentThread);
684
685                 /* No need to look at any more threads */
686                 pNextThread = NULL;
687             }
688             else
689             {
690                 /* We need to look at another thread */
691                 pNextThread = pCurrentThread->pNext;
692                 pPrevThread = pCurrentThread;
693             }
694         }
695
696         /* Lets let somebody else remove an object now */
697         LeaveCriticalSection(&Thread_CritSect);
698     }
699 }
700
701 /* WIN32 statserv_closedown */
702 static void statserv_closedown()
703 {
704     /* Shouldn't do anything if we are not initialized */
705     if (bInitialized)
706     {
707         int iHandles = 0;
708         HANDLE *pThreadHandles = NULL;
709
710         /* We need to stop threads adding and removing while we */
711         /* start the closedown process */
712         EnterCriticalSection(&Thread_CritSect);
713
714         {
715             /* We have exclusive access to the thread stuff now */
716             /* Y didn't i use a semaphore - Oh well never mind */
717             ThreadList *pCurrentThread = pFirstThread;
718
719             /* Before we do anything else, we need to shutdown the listener */
720             if (pListener != NULL)
721                 iochan_destroy(pListener);
722
723             for (; pCurrentThread != NULL; pCurrentThread = pCurrentThread->pNext)
724             {
725                 /* Just destroy the IOCHAN, that should do the trick */
726                 iochan_destroy(pCurrentThread->pIOChannel);
727                 closesocket(pCurrentThread->pIOChannel->fd);
728
729                 /* Keep a running count of our handles */
730                 iHandles++;
731             }
732
733             if (iHandles > 0)
734             {
735                 HANDLE *pCurrentHandle ;
736
737                 /* Allocate the thread handle array */
738                 pThreadHandles = (HANDLE *)malloc(sizeof(HANDLE) * iHandles);
739                 pCurrentHandle = pThreadHandles; 
740
741                 for (pCurrentThread = pFirstThread;
742                      pCurrentThread != NULL;
743                      pCurrentThread = pCurrentThread->pNext, pCurrentHandle++)
744                 {
745                     /* Just the handle */
746                     *pCurrentHandle = pCurrentThread->hThread;
747                 }
748             }
749
750             /* We can now leave the critical section */
751             LeaveCriticalSection(&Thread_CritSect);
752         }
753
754         /* Now we can really do something */
755         if (iHandles > 0)
756         {
757             yaz_log(log_server, "waiting for %d to die", iHandles);
758             /* This will now wait, until all the threads close */
759             WaitForMultipleObjects(iHandles, pThreadHandles, TRUE, INFINITE);
760
761             /* Free the memory we allocated for the handle array */
762             free(pThreadHandles);
763         }
764
765         xml_config_bend_stop();
766         /* No longer require the critical section, since all threads are dead */
767         DeleteCriticalSection(&Thread_CritSect);
768     }
769     xml_config_close();
770 }
771
772 void __cdecl event_loop_thread(IOCHAN iochan)
773 {
774     iochan_event_loop(&iochan);
775 }
776
777 /* WIN32 listener */
778 static void listener(IOCHAN h, int event)   
779 {
780     COMSTACK line = (COMSTACK) iochan_getdata(h);
781     IOCHAN parent_chan = line->user;
782     association *newas;
783     int res;
784     HANDLE newHandle;
785
786     if (event == EVENT_INPUT)
787     {
788         COMSTACK new_line;
789         IOCHAN new_chan;
790
791         if ((res = cs_listen(line, 0, 0)) < 0)
792         {
793             yaz_log(YLOG_FATAL|YLOG_ERRNO, "cs_listen failed");
794             return;
795         }
796         else if (res == 1)
797             return; /* incomplete */
798         yaz_log(YLOG_DEBUG, "listen ok");
799         new_line = cs_accept(line);
800         if (!new_line)
801         {
802             yaz_log(YLOG_FATAL, "Accept failed.");
803             return;
804         }
805         yaz_log(YLOG_DEBUG, "Accept ok");
806
807         if (!(new_chan = iochan_create(cs_fileno(new_line), ir_session,
808                                        EVENT_INPUT, parent_chan->chan_id)))
809         {
810             yaz_log(YLOG_FATAL, "Failed to create iochan");
811             iochan_destroy(h);
812             return;
813         }
814
815         yaz_log(YLOG_DEBUG, "Creating association");
816         if (!(newas = create_association(new_chan, new_line,
817                                          control_block.apdufile)))
818         {
819             yaz_log(YLOG_FATAL, "Failed to create new assoc.");
820             iochan_destroy(h);
821             return;
822         }
823         newas->cs_get_mask = EVENT_INPUT;
824         newas->cs_put_mask = 0;
825         newas->cs_accept_mask = 0;
826
827         yaz_log(YLOG_DEBUG, "Setting timeout %d", control_block.idle_timeout);
828         iochan_setdata(new_chan, newas);
829         iochan_settimeout(new_chan, 60);
830
831         /* Now what we need todo is create a new thread with this iochan as
832            the parameter */
833         newHandle = (HANDLE) _beginthread(event_loop_thread, 0, new_chan);
834         if (newHandle == (HANDLE) -1)
835         {
836             
837             yaz_log(YLOG_FATAL|YLOG_ERRNO, "Failed to create new thread.");
838             iochan_destroy(h);
839             return;
840         }
841         /* We successfully created the thread, so add it to the list */
842         statserv_add(newHandle, new_chan);
843
844         yaz_log(YLOG_DEBUG, "Created new thread, id = %ld iochan %p",(long) newHandle, new_chan);
845         iochan_setflags(h, EVENT_INPUT | EVENT_EXCEPT); /* reset listener */
846     }
847     else
848     {
849         yaz_log(YLOG_FATAL, "Bad event on listener.");
850         iochan_destroy(h);
851         return;
852     }
853 }
854
855 int statserv_must_terminate(void)
856 {
857     return 0;
858 }
859
860 #else /* ! WIN32 */
861
862 static int term_flag = 0;
863 /* To save having an #ifdef in event_loop we need to
864    define this empty function 
865 */
866 int statserv_must_terminate(void)
867 {
868     return term_flag;
869 }
870
871 void statserv_remove(IOCHAN pIOChannel)
872 {
873 }
874
875 static void statserv_closedown(void)
876 {
877     IOCHAN p;
878
879     xml_config_bend_stop();
880     for (p = pListener; p; p = p->next)
881     {
882         iochan_destroy(p);
883     }
884     xml_config_close();
885 }
886
887 void sigterm(int sig)
888 {
889     term_flag = 1;
890 }
891
892 static void *new_session(void *vp);
893 static int no_sessions = 0;
894
895 /* UNIX listener */
896 static void listener(IOCHAN h, int event)
897 {
898     COMSTACK line = (COMSTACK) iochan_getdata(h);
899     int res;
900
901     if (event == EVENT_INPUT)
902     {
903         COMSTACK new_line;
904         if ((res = cs_listen_check(line, 0, 0, control_block.check_ip,
905                                    control_block.daemon_name)) < 0)
906         {
907             yaz_log(YLOG_WARN|YLOG_ERRNO, "cs_listen failed");
908             return;
909         }
910         else if (res == 1)
911         {
912             yaz_log(YLOG_WARN, "cs_listen incomplete");
913             return;
914         }
915         new_line = cs_accept(line);
916         if (!new_line)
917         {
918             yaz_log(YLOG_FATAL, "Accept failed.");
919             iochan_setflags(h, EVENT_INPUT | EVENT_EXCEPT); /* reset listener */
920             return;
921         }
922
923         yaz_log(log_sessiondetail, "Connect from %s", cs_addrstr(new_line));
924
925         no_sessions++;
926         if (control_block.dynamic)
927         {
928             if ((res = fork()) < 0)
929             {
930                 yaz_log(YLOG_FATAL|YLOG_ERRNO, "fork");
931                 iochan_destroy(h);
932                 return;
933             }
934             else if (res == 0) /* child */
935             {
936                 char nbuf[100];
937                 IOCHAN pp;
938
939                 for (pp = pListener; pp; pp = iochan_getnext(pp))
940                 {
941                     COMSTACK l = (COMSTACK)iochan_getdata(pp);
942                     cs_close(l);
943                     iochan_destroy(pp);
944                 }
945                 sprintf(nbuf, "%s(%d)", me, no_sessions);
946                 yaz_log_init_prefix(nbuf);
947                 /* ensure that bend_stop is not called when each child exits -
948                    only for the main process ..  */
949                 control_block.bend_stop = 0;
950             }
951             else /* parent */
952             {
953                 cs_close(new_line);
954                 return;
955             }
956         }
957
958         if (control_block.threads)
959         {
960 #if YAZ_POSIX_THREADS
961             pthread_t child_thread;
962             pthread_create(&child_thread, 0, new_session, new_line);
963             pthread_detach(child_thread);
964 #elif YAZ_GNU_THREADS
965             pth_attr_t attr;
966             pth_t child_thread;
967
968             attr = pth_attr_new();
969             pth_attr_set(attr, PTH_ATTR_JOINABLE, FALSE);
970             pth_attr_set(attr, PTH_ATTR_STACK_SIZE, 32*1024);
971             pth_attr_set(attr, PTH_ATTR_NAME, "session");
972             yaz_log(YLOG_DEBUG, "pth_spawn begin");
973             child_thread = pth_spawn(attr, new_session, new_line);
974             yaz_log(YLOG_DEBUG, "pth_spawn finish");
975             pth_attr_destroy(attr);
976 #else
977             new_session(new_line);
978 #endif
979         }
980         else
981             new_session(new_line);
982     }
983     else if (event == EVENT_TIMEOUT)
984     {
985         yaz_log(log_server, "Shutting down listener.");
986         iochan_destroy(h);
987     }
988     else
989     {
990         yaz_log(YLOG_FATAL, "Bad event on listener.");
991         iochan_destroy(h);
992     }
993 }
994
995 static void *new_session(void *vp)
996 {
997     char *a;
998     association *newas;
999     IOCHAN new_chan;
1000     COMSTACK new_line = (COMSTACK) vp;
1001     IOCHAN parent_chan = (IOCHAN) new_line->user;
1002
1003     unsigned cs_get_mask, cs_accept_mask, mask =  
1004         ((new_line->io_pending & CS_WANT_WRITE) ? EVENT_OUTPUT : 0) |
1005         ((new_line->io_pending & CS_WANT_READ) ? EVENT_INPUT : 0);
1006
1007     if (mask)
1008     {
1009         cs_accept_mask = mask;  /* accept didn't complete */
1010         cs_get_mask = 0;
1011     }
1012     else
1013     {
1014         cs_accept_mask = 0;     /* accept completed.  */
1015         cs_get_mask = mask = EVENT_INPUT;
1016     }
1017
1018     if (!(new_chan = iochan_create(cs_fileno(new_line), ir_session, mask,
1019                                    parent_chan->chan_id)))
1020     {
1021         yaz_log(YLOG_FATAL, "Failed to create iochan");
1022         return 0;
1023     }
1024     if (!(newas = create_association(new_chan, new_line,
1025                                      control_block.apdufile)))
1026     {
1027         yaz_log(YLOG_FATAL, "Failed to create new assoc.");
1028         return 0;
1029     }
1030     newas->cs_accept_mask = cs_accept_mask;
1031     newas->cs_get_mask = cs_get_mask;
1032
1033     iochan_setdata(new_chan, newas);
1034     iochan_settimeout(new_chan, 60);
1035 #if 1
1036     a = cs_addrstr(new_line);
1037 #else
1038     a = 0;
1039 #endif
1040     yaz_log_xml_errors(0, YLOG_WARN);
1041     yaz_log(log_session, "Session - OK %d %s %ld",
1042             no_sessions, a ? a : "[Unknown]", (long) getpid());
1043     if (max_sessions && no_sessions >= max_sessions)
1044         control_block.one_shot = 1;
1045     if (control_block.threads)
1046     {
1047         iochan_event_loop(&new_chan);
1048     }
1049     else
1050     {
1051         new_chan->next = pListener;
1052         pListener = new_chan;
1053     }
1054     return 0;
1055 }
1056
1057 /* UNIX */
1058 #endif
1059
1060 static void inetd_connection(int what)
1061 {
1062     COMSTACK line;
1063     IOCHAN chan;
1064     association *assoc;
1065     char *addr;
1066
1067     if ((line = cs_createbysocket(0, tcpip_type, 0, what)))
1068     {
1069         if ((chan = iochan_create(cs_fileno(line), ir_session, EVENT_INPUT,
1070                                   0)))
1071         {
1072             if ((assoc = create_association(chan, line,
1073                                             control_block.apdufile)))
1074             {
1075                 iochan_setdata(chan, assoc);
1076                 iochan_settimeout(chan, 60);
1077                 addr = cs_addrstr(line);
1078                 yaz_log(log_sessiondetail, "Inetd association from %s",
1079                         addr ? addr : "[UNKNOWN]");
1080                 assoc->cs_get_mask = EVENT_INPUT;
1081             }
1082             else
1083             {
1084                 yaz_log(YLOG_FATAL, "Failed to create association structure");
1085             }
1086             chan->next = pListener;
1087             pListener = chan;
1088         }
1089         else
1090         {
1091             yaz_log(YLOG_FATAL, "Failed to create iochan");
1092         }
1093     }
1094     else
1095     {
1096         yaz_log(YLOG_ERRNO|YLOG_FATAL, "Failed to create comstack on socket 0");
1097     }
1098 }
1099
1100 /*
1101  * Set up a listening endpoint, and give it to the event-handler.
1102  */
1103 static int add_listener(char *where, int listen_id)
1104 {
1105     COMSTACK l;
1106     void *ap;
1107     IOCHAN lst = NULL;
1108     const char *mode;
1109
1110     if (control_block.dynamic)
1111         mode = "dynamic";
1112     else if (control_block.threads)
1113         mode = "threaded";
1114     else
1115         mode = "static";
1116
1117     yaz_log(log_server, "Adding %s listener on %s id=%d", mode, where,
1118             listen_id);
1119
1120     l = cs_create_host(where, 2, &ap);
1121     if (!l)
1122     {
1123         yaz_log(YLOG_FATAL, "Failed to listen on %s", where);
1124         return -1;
1125     }
1126     if (*control_block.cert_fname)
1127         cs_set_ssl_certificate_file(l, control_block.cert_fname);
1128
1129     if (cs_bind(l, ap, CS_SERVER) < 0)
1130     {
1131         if (cs_errno(l) == CSYSERR)
1132             yaz_log(YLOG_FATAL|YLOG_ERRNO, "Failed to bind to %s", where);
1133         else
1134             yaz_log(YLOG_FATAL, "Failed to bind to %s: %s", where,
1135                     cs_strerror(l));
1136         cs_close(l);
1137         return -1;
1138     }
1139     if (!(lst = iochan_create(cs_fileno(l), listener, EVENT_INPUT |
1140                               EVENT_EXCEPT, listen_id)))
1141     {
1142         yaz_log(YLOG_FATAL|YLOG_ERRNO, "Failed to create IOCHAN-type");
1143         cs_close(l);
1144         return -1;
1145     }
1146     iochan_setdata(lst, l); /* user-defined data for listener is COMSTACK */
1147     l->user = lst;  /* user-defined data for COMSTACK is listener chan */
1148
1149     /* Add listener to chain */
1150     lst->next = pListener;
1151     pListener = lst;
1152     return 0; /* OK */
1153 }
1154
1155 #ifndef WIN32
1156 /* UNIX only (for windows we don't need to catch the signals) */
1157 static void catchchld(int num)
1158 {
1159     while (waitpid(-1, 0, WNOHANG) > 0)
1160         ;
1161     signal(SIGCHLD, catchchld);
1162 }
1163 #endif
1164
1165 statserv_options_block *statserv_getcontrol(void)
1166 {
1167 #ifdef WIN32
1168     if (init_control_tls)
1169         return (statserv_options_block *) TlsGetValue(current_control_tls);
1170     else
1171         return &control_block;
1172 #elif YAZ_POSIX_THREADS
1173     if (init_control_tls)
1174         return (statserv_options_block *)
1175             pthread_getspecific(current_control_tls);
1176     else
1177         return &control_block;
1178 #else
1179     if (current_control_block)
1180         return current_control_block;
1181     return &control_block;
1182 #endif
1183 }
1184
1185 void statserv_setcontrol(statserv_options_block *block)
1186 {
1187     if (gfs_root_dir[0])
1188     {
1189         if (chdir(gfs_root_dir))
1190             yaz_log(YLOG_WARN|YLOG_ERRNO, "chdir %s", gfs_root_dir);
1191     }
1192 #ifdef WIN32
1193     if (init_control_tls)
1194         TlsSetValue(current_control_tls, block);
1195 #elif YAZ_POSIX_THREADS
1196     if (init_control_tls)
1197         pthread_setspecific(current_control_tls, block);
1198 #else
1199     current_control_block = block;
1200 #endif
1201 }
1202
1203 static void statserv_reset(void)
1204 {
1205 }
1206
1207 static int statserv_sc_main(yaz_sc_t s, int argc, char **argv)
1208 {
1209     char sep;
1210 #ifdef WIN32
1211     /* We need to initialize the thread list */
1212     ThreadList_Initialize();
1213 /* WIN32 */
1214 #endif
1215
1216
1217 #ifdef WIN32
1218     sep = '\\';
1219 #else
1220     sep = '/';
1221 #endif
1222     if ((me = strrchr(argv[0], sep)))
1223         me++; /* get the basename */
1224     else
1225         me = argv[0];
1226     programname = argv[0];
1227
1228     if (control_block.options_func(argc, argv))
1229         return 1;
1230
1231     xml_config_open();
1232     
1233     xml_config_bend_start();
1234
1235 #ifdef WIN32
1236     xml_config_add_listeners();
1237
1238     yaz_log(log_server, "Starting server %s", me);
1239     if (!pListener && *control_block.default_listen)
1240         add_listener(control_block.default_listen, 0);
1241 #else
1242 /* UNIX */
1243     if (control_block.inetd)
1244         inetd_connection(control_block.default_proto);
1245     else
1246     {
1247         static int hand[2];
1248         if (control_block.background)
1249         {
1250             /* create pipe so that parent waits until child has created
1251                PID (or failed) */
1252             if (pipe(hand) < 0)
1253             {
1254                 yaz_log(YLOG_FATAL|YLOG_ERRNO, "pipe");
1255                 return 1;
1256             }
1257             switch (fork())
1258             {
1259             case 0: 
1260                 break;
1261             case -1:
1262                 return 1;
1263             default:
1264                 close(hand[1]);
1265                 while(1)
1266                 {
1267                     char dummy[1];
1268                     int res = read(hand[0], dummy, 1);
1269                     if (res < 0 && yaz_errno() != EINTR)
1270                     {
1271                         yaz_log(YLOG_FATAL|YLOG_ERRNO, "read fork handshake");
1272                         break;
1273                     }
1274                     else if (res >= 0)
1275                         break;
1276                 }
1277                 close(hand[0]);
1278                 _exit(0);
1279             }
1280             /* child */
1281             close(hand[0]);
1282             if (setsid() < 0)
1283                 return 1;
1284             
1285             close(0);
1286             close(1);
1287             close(2);
1288             open("/dev/null", O_RDWR);
1289             if (dup(0) == -1)
1290                 return 1;
1291             if (dup(0) == -1)
1292                 return 1;
1293         }
1294         xml_config_add_listeners();
1295
1296         if (!pListener && *control_block.default_listen)
1297             add_listener(control_block.default_listen, 0);
1298         
1299         if (!pListener)
1300             return 1;
1301
1302         if (*control_block.pid_fname)
1303         {
1304             FILE *f = fopen(control_block.pid_fname, "w");
1305             if (!f)
1306             {
1307                 yaz_log(YLOG_FATAL|YLOG_ERRNO, "Couldn't create %s", 
1308                         control_block.pid_fname);
1309                 exit(0);
1310             }
1311             fprintf(f, "%ld", (long) getpid());
1312             fclose(f);
1313         }
1314         
1315         if (control_block.background)
1316             close(hand[1]);
1317
1318
1319         yaz_log(log_server, "Starting server %s pid=%ld", programname, 
1320                 (long) getpid());
1321 #if 0
1322         sigset_t sigs_to_block;
1323         
1324         sigemptyset(&sigs_to_block);
1325         sigaddset(&sigs_to_block, SIGTERM);
1326         pthread_sigmask(SIG_BLOCK, &sigs_to_block, 0);
1327         /* missing... */
1328 #endif
1329         if (control_block.dynamic)
1330             signal(SIGCHLD, catchchld);
1331     }
1332     signal(SIGPIPE, SIG_IGN);
1333     signal(SIGTERM, sigterm);
1334     if (*control_block.setuid)
1335     {
1336         struct passwd *pw;
1337         
1338         if (!(pw = getpwnam(control_block.setuid)))
1339         {
1340             yaz_log(YLOG_FATAL, "%s: Unknown user", control_block.setuid);
1341             return(1);
1342         }
1343         if (setuid(pw->pw_uid) < 0)
1344         {
1345             yaz_log(YLOG_FATAL|YLOG_ERRNO, "setuid");
1346             exit(1);
1347         }
1348     }
1349 /* UNIX */
1350 #endif
1351     if (pListener == NULL)
1352         return 1;
1353     if (s)
1354         yaz_sc_running(s);
1355     yaz_log(YLOG_DEBUG, "Entering event loop.");
1356     return iochan_event_loop(&pListener);
1357 }
1358
1359 static void option_copy(char *dst, const char *src)
1360 {
1361     strncpy(dst, src ? src : "", 127);
1362     dst[127] = '\0';
1363 }
1364
1365 int check_options(int argc, char **argv)
1366 {
1367     int ret = 0, r;
1368     char *arg;
1369
1370     yaz_log_init_level(yaz_log_mask_str(STAT_DEFAULT_LOG_LEVEL)); 
1371
1372     get_logbits(1); 
1373
1374     while ((ret = options("1a:iszSTl:v:u:c:w:t:k:d:A:p:DC:f:m:r:",
1375                           argv, argc, &arg)) != -2)
1376     {
1377         switch (ret)
1378         {
1379         case 0:
1380             if (add_listener(arg, 0))
1381                 return 1;  /* failed to create listener */
1382             break;
1383         case '1':        
1384             control_block.one_shot = 1;
1385             control_block.dynamic = 0;
1386             break;
1387         case 'z':
1388             control_block.default_proto = PROTO_Z3950;
1389             break;
1390         case 's':
1391             fprintf(stderr, "%s: SR protocol no longer supported\n", me);
1392             exit(1);
1393             break;
1394         case 'S':
1395             control_block.dynamic = 0;
1396             break;
1397         case 'T':
1398 #if YAZ_POSIX_THREADS
1399             control_block.dynamic = 0;
1400             control_block.threads = 1;
1401 #elif YAZ_GNU_THREADS
1402             control_block.dynamic = 0;
1403             control_block.threads = 1;
1404 #else
1405             fprintf(stderr, "%s: Threaded mode not available.\n", me);
1406             return 1;
1407 #endif
1408             break;
1409         case 'l':
1410             option_copy(control_block.logfile, arg);
1411             yaz_log_init_file(control_block.logfile);
1412             break;
1413         case 'm':
1414             if (!arg) {
1415                 fprintf(stderr, "%s: Specify time format for log file.\n", me);
1416                 return(1);
1417             }
1418             yaz_log_time_format(arg);
1419             break;
1420         case 'v':
1421             yaz_log_init_level(yaz_log_mask_str(arg));
1422             get_logbits(1); 
1423             break;
1424         case 'a':
1425             option_copy(control_block.apdufile, arg);
1426             break;
1427         case 'u':
1428             option_copy(control_block.setuid, arg);
1429             break;
1430         case 'c':
1431             option_copy(control_block.configname, arg);
1432             break;
1433         case 'C':
1434             option_copy(control_block.cert_fname, arg);
1435             break;
1436         case 'd':
1437             option_copy(control_block.daemon_name, arg);
1438             break;
1439         case 't':
1440             if (!arg || !(r = atoi(arg)))
1441             {
1442                 fprintf(stderr, "%s: Specify positive timeout for -t.\n", me);
1443                 return(1);
1444             }
1445             control_block.idle_timeout = r;
1446             break;
1447         case  'k':
1448             if (!arg || !(r = atoi(arg)))
1449             {
1450                 fprintf(stderr, "%s: Specify positive size for -k.\n", me);
1451                 return(1);
1452             }
1453             control_block.maxrecordsize = r * 1024;
1454             break;
1455         case 'i':
1456             control_block.inetd = 1;
1457             break;
1458         case 'w':
1459             if (chdir(arg))
1460             {
1461                 perror(arg);            
1462                 return 1;
1463             }
1464             break;
1465         case 'A':
1466             max_sessions = atoi(arg);
1467             break;
1468         case 'p':
1469             option_copy(control_block.pid_fname, arg);
1470             break;
1471         case 'f':
1472 #if YAZ_HAVE_XML2
1473             option_copy(control_block.xml_config, arg);
1474 #else
1475             fprintf(stderr, "%s: Option -f unsupported since YAZ is compiled without Libxml2 support\n", me);
1476             exit(1);
1477 #endif
1478             break;
1479         case 'D':
1480             control_block.background = 1;
1481             break;
1482         case 'r':
1483             if (!arg || !(r = atoi(arg)))
1484             {
1485                 fprintf(stderr, "%s: Specify positive size for -r.\n", me);
1486                 return(1);
1487             }
1488             yaz_log_init_max_size(r * 1024);
1489             break;
1490         default:
1491             fprintf(stderr, "Usage: %s [ -a <pdufile> -v <loglevel>"
1492                     " -l <logfile> -u <user> -c <config> -t <minutes>"
1493                     " -k <kilobytes> -d <daemon> -p <pidfile> -C certfile"
1494                     " -ziDST1 -m <time-format> -w <directory> <listener-addr>... ]\n", me);
1495             return 1;
1496         }
1497     }
1498     return 0;
1499 }
1500
1501 void statserv_sc_stop(yaz_sc_t s)
1502 {
1503     statserv_closedown();
1504     statserv_reset();
1505 }
1506
1507 int statserv_main(int argc, char **argv,
1508                   bend_initresult *(*bend_init)(bend_initrequest *r),
1509                   void (*bend_close)(void *handle))
1510 {
1511     int ret;
1512     struct statserv_options_block *cb = &control_block;
1513
1514     /* control block does not have service_name member on Unix */
1515     yaz_sc_t s = yaz_sc_create(
1516 #ifdef WIN32
1517         cb->service_name, cb->service_display_name
1518 #else
1519         0, 0
1520 #endif
1521         );
1522
1523     cb->bend_init = bend_init;
1524     cb->bend_close = bend_close;
1525
1526     ret = yaz_sc_program(s, argc, argv, statserv_sc_main, statserv_sc_stop);
1527     yaz_sc_destroy(&s);
1528     return ret;
1529 }
1530
1531 /*
1532  * Local variables:
1533  * c-basic-offset: 4
1534  * indent-tabs-mode: nil
1535  * End:
1536  * vim: shiftwidth=4 tabstop=8 expandtab
1537  */
1538