blob: 80528162152a408d0812ce3271353ccf69a7d1a9 [file] [log] [blame]
Willy Tarreaubaaee002006-06-26 02:48:02 +02001/*
2 * General logging functions.
3 *
4 * Copyright 2000-2006 Willy Tarreau <w@1wt.eu>
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version
9 * 2 of the License, or (at your option) any later version.
10 *
11 */
12
13#include <stdarg.h>
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
17#include <syslog.h>
18#include <time.h>
19#include <unistd.h>
20
21#include <sys/time.h>
22
Willy Tarreaue3ba5f02006-06-29 18:54:54 +020023#include <common/config.h>
Willy Tarreau2dd0d472006-06-29 17:53:05 +020024#include <common/standard.h>
Willy Tarreaufb278672006-10-15 15:38:50 +020025#include <common/time.h>
Willy Tarreaubaaee002006-06-26 02:48:02 +020026
27#include <types/backend.h>
28#include <types/global.h>
Willy Tarreaubaaee002006-06-26 02:48:02 +020029#include <types/log.h>
Willy Tarreauc0dde7a2007-01-01 21:38:07 +010030#include <types/proto_http.h>
Willy Tarreaubaaee002006-06-26 02:48:02 +020031#include <types/proxy.h>
32#include <types/session.h>
33
34
35const char *log_facilities[NB_LOG_FACILITIES] = {
36 "kern", "user", "mail", "daemon",
37 "auth", "syslog", "lpr", "news",
38 "uucp", "cron", "auth2", "ftp",
39 "ntp", "audit", "alert", "cron2",
40 "local0", "local1", "local2", "local3",
41 "local4", "local5", "local6", "local7"
42};
43
44
45const char *log_levels[NB_LOG_LEVELS] = {
46 "emerg", "alert", "crit", "err",
47 "warning", "notice", "info", "debug"
48};
49
50const char *monthname[12] = {
51 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
52 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
53};
54
55const char sess_term_cond[8] = "-cCsSPRI"; /* normal, CliTo, CliErr, SrvTo, SrvErr, PxErr, Resource, Internal */
Willy Tarreaub8750a82006-09-03 09:56:00 +020056const char sess_fin_state[8] = "-RCHDLQT"; /* cliRequest, srvConnect, srvHeader, Data, Last, Queue, Tarpit */
Willy Tarreaubaaee002006-06-26 02:48:02 +020057const char sess_cookie[4] = "NIDV"; /* No cookie, Invalid cookie, cookie for a Down server, Valid cookie */
58const char sess_set_cookie[8] = "N1I3PD5R"; /* No set-cookie, unknown, Set-Cookie Inserted, unknown,
59 Set-cookie seen and left unchanged (passive), Set-cookie Deleted,
60 unknown, Set-cookie Rewritten */
61void **pool_requri = NULL;
62
63
64/*
65 * Displays the message on stderr with the date and pid. Overrides the quiet
66 * mode during startup.
67 */
Willy Tarreaub17916e2006-10-15 15:17:57 +020068void Alert(const char *fmt, ...)
Willy Tarreaubaaee002006-06-26 02:48:02 +020069{
70 va_list argp;
Willy Tarreaubaaee002006-06-26 02:48:02 +020071 struct tm *tm;
72
73 if (!(global.mode & MODE_QUIET) || (global.mode & (MODE_VERBOSE | MODE_STARTING))) {
74 va_start(argp, fmt);
75
Willy Tarreaubf736132006-10-15 22:54:47 +020076 tm = localtime((time_t *)&now.tv_sec);
Willy Tarreaubaaee002006-06-26 02:48:02 +020077 fprintf(stderr, "[ALERT] %03d/%02d%02d%02d (%d) : ",
78 tm->tm_yday, tm->tm_hour, tm->tm_min, tm->tm_sec, (int)getpid());
79 vfprintf(stderr, fmt, argp);
80 fflush(stderr);
81 va_end(argp);
82 }
83}
84
85
86/*
87 * Displays the message on stderr with the date and pid.
88 */
Willy Tarreaub17916e2006-10-15 15:17:57 +020089void Warning(const char *fmt, ...)
Willy Tarreaubaaee002006-06-26 02:48:02 +020090{
91 va_list argp;
Willy Tarreaubaaee002006-06-26 02:48:02 +020092 struct tm *tm;
93
94 if (!(global.mode & MODE_QUIET) || (global.mode & MODE_VERBOSE)) {
95 va_start(argp, fmt);
96
Willy Tarreaubf736132006-10-15 22:54:47 +020097 tm = localtime((time_t *)&now.tv_sec);
Willy Tarreaubaaee002006-06-26 02:48:02 +020098 fprintf(stderr, "[WARNING] %03d/%02d%02d%02d (%d) : ",
99 tm->tm_yday, tm->tm_hour, tm->tm_min, tm->tm_sec, (int)getpid());
100 vfprintf(stderr, fmt, argp);
101 fflush(stderr);
102 va_end(argp);
103 }
104}
105
106/*
107 * Displays the message on <out> only if quiet mode is not set.
108 */
Willy Tarreaub17916e2006-10-15 15:17:57 +0200109void qfprintf(FILE *out, const char *fmt, ...)
Willy Tarreaubaaee002006-06-26 02:48:02 +0200110{
111 va_list argp;
112
113 if (!(global.mode & MODE_QUIET) || (global.mode & MODE_VERBOSE)) {
114 va_start(argp, fmt);
115 vfprintf(out, fmt, argp);
116 fflush(out);
117 va_end(argp);
118 }
119}
120
121/*
122 * returns log level for <lev> or -1 if not found.
123 */
124int get_log_level(const char *lev)
125{
126 int level;
127
128 level = NB_LOG_LEVELS - 1;
129 while (level >= 0 && strcmp(log_levels[level], lev))
130 level--;
131
132 return level;
133}
134
135
136/*
137 * returns log facility for <fac> or -1 if not found.
138 */
139int get_log_facility(const char *fac)
140{
141 int facility;
142
143 facility = NB_LOG_FACILITIES - 1;
144 while (facility >= 0 && strcmp(log_facilities[facility], fac))
145 facility--;
146
147 return facility;
148}
149
150
151#define FD_SETS_ARE_BITFIELDS
152#ifdef FD_SETS_ARE_BITFIELDS
153/*
154 * This map is used with all the FD_* macros to check whether a particular bit
155 * is set or not. Each bit represents an ACSII code. FD_SET() sets those bytes
156 * which should be encoded. When FD_ISSET() returns non-zero, it means that the
157 * byte should be encoded. Be careful to always pass bytes from 0 to 255
158 * exclusively to the macros.
159 */
160fd_set hdr_encode_map[(sizeof(fd_set) > (256/8)) ? 1 : ((256/8) / sizeof(fd_set))];
161fd_set url_encode_map[(sizeof(fd_set) > (256/8)) ? 1 : ((256/8) / sizeof(fd_set))];
162
163#else
164#error "Check if your OS uses bitfields for fd_sets"
165#endif
166
167/*
168 * This function sends a syslog message to both log servers of a proxy,
169 * or to global log servers if the proxy is NULL.
170 * It also tries not to waste too much time computing the message header.
171 * It doesn't care about errors nor does it report them.
172 */
Willy Tarreaub17916e2006-10-15 15:17:57 +0200173void send_log(struct proxy *p, int level, const char *message, ...)
Willy Tarreaubaaee002006-06-26 02:48:02 +0200174{
175 static int logfd = -1; /* syslog UDP socket */
176 static long tvsec = -1; /* to force the string to be initialized */
Willy Tarreaubaaee002006-06-26 02:48:02 +0200177 va_list argp;
178 static char logmsg[MAX_SYSLOG_LEN];
179 static char *dataptr = NULL;
180 int fac_level;
181 int hdr_len, data_len;
182 struct sockaddr_in *sa[2];
183 int facilities[2], loglevel[2];
184 int nbloggers = 0;
185 char *log_ptr;
186
187 if (logfd < 0) {
188 if ((logfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
189 return;
190 }
191
192 if (level < 0 || progname == NULL || message == NULL)
193 return;
194
Willy Tarreau2b35c952006-10-15 15:25:48 +0200195 if (now.tv_sec != tvsec || dataptr == NULL) {
Willy Tarreaubaaee002006-06-26 02:48:02 +0200196 /* this string is rebuild only once a second */
Willy Tarreaubf736132006-10-15 22:54:47 +0200197 struct tm *tm = localtime((time_t *)&now.tv_sec);
Willy Tarreau2b35c952006-10-15 15:25:48 +0200198 tvsec = now.tv_sec;
Willy Tarreaubaaee002006-06-26 02:48:02 +0200199
200 hdr_len = snprintf(logmsg, sizeof(logmsg),
201 "<<<<>%s %2d %02d:%02d:%02d %s[%d]: ",
202 monthname[tm->tm_mon],
203 tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec,
204 progname, pid);
205 /* WARNING: depending upon implementations, snprintf may return
206 * either -1 or the number of bytes that would be needed to store
207 * the total message. In both cases, we must adjust it.
208 */
209 if (hdr_len < 0 || hdr_len > sizeof(logmsg))
210 hdr_len = sizeof(logmsg);
211
212 dataptr = logmsg + hdr_len;
213 }
214
215 va_start(argp, message);
Willy Tarreau8d5d7f22007-01-21 19:16:41 +0100216 /*
217 * FIXME: we take a huge performance hit here. We might have to replace
218 * vsnprintf() for a hard-coded log writer.
219 */
Willy Tarreaubaaee002006-06-26 02:48:02 +0200220 data_len = vsnprintf(dataptr, logmsg + sizeof(logmsg) - dataptr, message, argp);
221 if (data_len < 0 || data_len > (logmsg + sizeof(logmsg) - dataptr))
222 data_len = logmsg + sizeof(logmsg) - dataptr;
223 va_end(argp);
224 dataptr[data_len - 1] = '\n'; /* force a break on ultra-long lines */
225
226 if (p == NULL) {
227 if (global.logfac1 >= 0) {
228 sa[nbloggers] = &global.logsrv1;
229 facilities[nbloggers] = global.logfac1;
230 loglevel[nbloggers] = global.loglev1;
231 nbloggers++;
232 }
233 if (global.logfac2 >= 0) {
234 sa[nbloggers] = &global.logsrv2;
235 facilities[nbloggers] = global.logfac2;
236 loglevel[nbloggers] = global.loglev2;
237 nbloggers++;
238 }
239 } else {
240 if (p->logfac1 >= 0) {
241 sa[nbloggers] = &p->logsrv1;
242 facilities[nbloggers] = p->logfac1;
243 loglevel[nbloggers] = p->loglev1;
244 nbloggers++;
245 }
246 if (p->logfac2 >= 0) {
247 sa[nbloggers] = &p->logsrv2;
248 facilities[nbloggers] = p->logfac2;
249 loglevel[nbloggers] = p->loglev2;
250 nbloggers++;
251 }
252 }
253
254 while (nbloggers-- > 0) {
255 /* we can filter the level of the messages that are sent to each logger */
256 if (level > loglevel[nbloggers])
257 continue;
258
259 /* For each target, we may have a different facility.
260 * We can also have a different log level for each message.
261 * This induces variations in the message header length.
262 * Since we don't want to recompute it each time, nor copy it every
263 * time, we only change the facility in the pre-computed header,
264 * and we change the pointer to the header accordingly.
265 */
266 fac_level = (facilities[nbloggers] << 3) + level;
267 log_ptr = logmsg + 3; /* last digit of the log level */
268 do {
269 *log_ptr = '0' + fac_level % 10;
270 fac_level /= 10;
271 log_ptr--;
272 } while (fac_level && log_ptr > logmsg);
273 *log_ptr = '<';
274
275 /* the total syslog message now starts at logptr, for dataptr+data_len-logptr */
276
277#ifndef MSG_NOSIGNAL
278 sendto(logfd, log_ptr, dataptr + data_len - log_ptr, MSG_DONTWAIT,
279 (struct sockaddr *)sa[nbloggers], sizeof(**sa));
280#else
281 sendto(logfd, log_ptr, dataptr + data_len - log_ptr, MSG_DONTWAIT | MSG_NOSIGNAL,
282 (struct sockaddr *)sa[nbloggers], sizeof(**sa));
283#endif
284 }
285}
286
287
288/*
289 * send a log for the session when we have enough info about it
290 */
291void sess_log(struct session *s)
292{
293 char pn[INET6_ADDRSTRLEN + strlen(":65535")];
Willy Tarreau73de9892006-11-30 11:40:23 +0100294 struct proxy *fe = s->fe;
Willy Tarreauddb358d2006-12-17 22:55:52 +0100295 struct proxy *be = s->be;
296 struct proxy *prx_log;
Willy Tarreau0f7562b2007-01-07 15:46:13 +0100297 struct http_req *hreq = &s->hreq;
Willy Tarreauddb358d2006-12-17 22:55:52 +0100298 int log, tolog;
Willy Tarreaubaaee002006-06-26 02:48:02 +0200299 char *uri;
300 char *pxid;
301 char *srv;
302 struct tm *tm;
303
304 /* This is a first attempt at a better logging system.
305 * For now, we rely on send_log() to provide the date, although it obviously
306 * is the date of the log and not of the request, and most fields are not
307 * computed.
308 */
309
Willy Tarreau97de6242006-12-27 17:18:38 +0100310 /* FIXME: let's limit ourselves to frontend logging for now. */
311 tolog = (fe->to_log /*| be->to_log | be->beprm->to_log*/);
Willy Tarreauddb358d2006-12-17 22:55:52 +0100312
313 log = tolog & ~s->logs.logwait;
Willy Tarreaubaaee002006-06-26 02:48:02 +0200314
315 if (s->cli_addr.ss_family == AF_INET)
316 inet_ntop(AF_INET,
317 (const void *)&((struct sockaddr_in *)&s->cli_addr)->sin_addr,
318 pn, sizeof(pn));
319 else
320 inet_ntop(AF_INET6,
321 (const void *)&((struct sockaddr_in6 *)(&s->cli_addr))->sin6_addr,
322 pn, sizeof(pn));
323
324 uri = (log & LW_REQ) ? s->logs.uri ? s->logs.uri : "<BADREQ>" : "";
Willy Tarreauddb358d2006-12-17 22:55:52 +0100325 pxid = be->beprm->id;
326 srv = (tolog & LW_SVID) ?
Willy Tarreaubaaee002006-06-26 02:48:02 +0200327 (s->data_source != DATA_SRC_STATS) ?
328 (s->srv != NULL) ? s->srv->id : "<NOSRV>" : "<STATS>" : "-";
329
Willy Tarreaubf736132006-10-15 22:54:47 +0200330 tm = localtime((time_t *)&s->logs.tv_accept.tv_sec);
Willy Tarreauddb358d2006-12-17 22:55:52 +0100331
332 if (fe->logfac1 >= 0)
333 prx_log = fe;
Willy Tarreau97de6242006-12-27 17:18:38 +0100334 /*
335 * FIXME: should we fall back to the backend if the frontend did not
336 * define any log ? It seems like we should not permit such complex
337 * setups because they would induce a debugging nightmare for the
338 * admin.
339 */
340 // else if (be->logfac1 >= 0)
341 // prx_log = be;
Willy Tarreauddb358d2006-12-17 22:55:52 +0100342 else
343 prx_log = NULL; /* global */
344
345 if (tolog & LW_REQ) {
Willy Tarreaubaaee002006-06-26 02:48:02 +0200346 char tmpline[MAX_SYSLOG_LEN], *h;
347 int hdr;
348
349 h = tmpline;
Willy Tarreauddb358d2006-12-17 22:55:52 +0100350
351 /* right now, header capture is limited to the frontend only */
352 if (fe->to_log & LW_REQHDR && (h < tmpline + sizeof(tmpline) - 10)) {
Willy Tarreaubaaee002006-06-26 02:48:02 +0200353 *(h++) = ' ';
354 *(h++) = '{';
Willy Tarreauddb358d2006-12-17 22:55:52 +0100355 for (hdr = 0; hdr < fe->nb_req_cap; hdr++) {
Willy Tarreaubaaee002006-06-26 02:48:02 +0200356 if (hdr)
357 *(h++) = '|';
Willy Tarreau0f7562b2007-01-07 15:46:13 +0100358 if (hreq->req.cap[hdr] != NULL)
Willy Tarreaubaaee002006-06-26 02:48:02 +0200359 h = encode_string(h, tmpline + sizeof(tmpline) - 7,
Willy Tarreau0f7562b2007-01-07 15:46:13 +0100360 '#', hdr_encode_map, hreq->req.cap[hdr]);
Willy Tarreaubaaee002006-06-26 02:48:02 +0200361 }
362 *(h++) = '}';
363 }
364
Willy Tarreauddb358d2006-12-17 22:55:52 +0100365 if (fe->to_log & LW_RSPHDR && (h < tmpline + sizeof(tmpline) - 7)) {
Willy Tarreaubaaee002006-06-26 02:48:02 +0200366 *(h++) = ' ';
367 *(h++) = '{';
Willy Tarreauddb358d2006-12-17 22:55:52 +0100368 for (hdr = 0; hdr < fe->nb_rsp_cap; hdr++) {
Willy Tarreaubaaee002006-06-26 02:48:02 +0200369 if (hdr)
370 *(h++) = '|';
Willy Tarreau362b34d2007-01-21 20:49:31 +0100371 if (hreq->rsp.cap[hdr] != NULL)
Willy Tarreaubaaee002006-06-26 02:48:02 +0200372 h = encode_string(h, tmpline + sizeof(tmpline) - 4,
Willy Tarreau362b34d2007-01-21 20:49:31 +0100373 '#', hdr_encode_map, hreq->rsp.cap[hdr]);
Willy Tarreaubaaee002006-06-26 02:48:02 +0200374 }
375 *(h++) = '}';
376 }
377
378 if (h < tmpline + sizeof(tmpline) - 4) {
379 *(h++) = ' ';
380 *(h++) = '"';
381 h = encode_string(h, tmpline + sizeof(tmpline) - 1,
382 '#', url_encode_map, uri);
383 *(h++) = '"';
384 }
385 *h = '\0';
386
Willy Tarreauddb358d2006-12-17 22:55:52 +0100387 send_log(prx_log, LOG_INFO,
Willy Tarreauebd61602006-12-30 11:54:15 +0100388 "%s:%d [%02d/%s/%04d:%02d:%02d:%02d.%03d]"
389 " %s %s/%s %d/%d/%d/%d/%s%d %d %s%lld"
390 " %s %s %c%c%c%c %d/%d/%d/%d %d/%d%s\n",
Willy Tarreaubaaee002006-06-26 02:48:02 +0200391 pn,
392 (s->cli_addr.ss_family == AF_INET) ?
393 ntohs(((struct sockaddr_in *)&s->cli_addr)->sin_port) :
394 ntohs(((struct sockaddr_in6 *)&s->cli_addr)->sin6_port),
395 tm->tm_mday, monthname[tm->tm_mon], tm->tm_year+1900,
Willy Tarreauebd61602006-12-30 11:54:15 +0100396 tm->tm_hour, tm->tm_min, tm->tm_sec, s->logs.tv_accept.tv_usec/1000,
397 fe->id, pxid, srv,
Willy Tarreaubaaee002006-06-26 02:48:02 +0200398 s->logs.t_request,
399 (s->logs.t_queue >= 0) ? s->logs.t_queue - s->logs.t_request : -1,
400 (s->logs.t_connect >= 0) ? s->logs.t_connect - s->logs.t_queue : -1,
401 (s->logs.t_data >= 0) ? s->logs.t_data - s->logs.t_connect : -1,
Willy Tarreau97de6242006-12-27 17:18:38 +0100402 (tolog & LW_BYTES) ? "" : "+", s->logs.t_close,
Willy Tarreaubaaee002006-06-26 02:48:02 +0200403 s->logs.status,
Willy Tarreau35d66b02007-01-02 00:28:21 +0100404 (tolog & LW_BYTES) ? "" : "+", s->logs.bytes_in,
Willy Tarreaubaaee002006-06-26 02:48:02 +0200405 s->logs.cli_cookie ? s->logs.cli_cookie : "-",
406 s->logs.srv_cookie ? s->logs.srv_cookie : "-",
407 sess_term_cond[(s->flags & SN_ERR_MASK) >> SN_ERR_SHIFT],
408 sess_fin_state[(s->flags & SN_FINST_MASK) >> SN_FINST_SHIFT],
Willy Tarreauddb358d2006-12-17 22:55:52 +0100409 (be->beprm->options & PR_O_COOK_ANY) ? sess_cookie[(s->flags & SN_CK_MASK) >> SN_CK_SHIFT] : '-',
410 (be->beprm->options & PR_O_COOK_ANY) ? sess_set_cookie[(s->flags & SN_SCK_MASK) >> SN_SCK_SHIFT] : '-',
Willy Tarreauebd61602006-12-30 11:54:15 +0100411 actconn, fe->feconn, be->beprm->beconn, s->srv ? s->srv->cur_sess : 0,
Willy Tarreaubaaee002006-06-26 02:48:02 +0200412 s->logs.srv_queue_size, s->logs.prx_queue_size, tmpline);
413 }
414 else {
Willy Tarreauebd61602006-12-30 11:54:15 +0100415 send_log(prx_log, LOG_INFO, "%s:%d [%02d/%s/%04d:%02d:%02d:%02d.%03d]"
416 " %s %s/%s %d/%d/%s%d %s%lld"
417 " %c%c %d/%d/%d/%d %d/%d\n",
Willy Tarreaubaaee002006-06-26 02:48:02 +0200418 pn,
419 (s->cli_addr.ss_family == AF_INET) ?
420 ntohs(((struct sockaddr_in *)&s->cli_addr)->sin_port) :
421 ntohs(((struct sockaddr_in6 *)&s->cli_addr)->sin6_port),
422 tm->tm_mday, monthname[tm->tm_mon], tm->tm_year+1900,
Willy Tarreauebd61602006-12-30 11:54:15 +0100423 tm->tm_hour, tm->tm_min, tm->tm_sec, s->logs.tv_accept.tv_usec/1000,
424 fe->id, pxid, srv,
Willy Tarreaubaaee002006-06-26 02:48:02 +0200425 (s->logs.t_queue >= 0) ? s->logs.t_queue : -1,
426 (s->logs.t_connect >= 0) ? s->logs.t_connect - s->logs.t_queue : -1,
Willy Tarreauddb358d2006-12-17 22:55:52 +0100427 (tolog & LW_BYTES) ? "" : "+", s->logs.t_close,
Willy Tarreau35d66b02007-01-02 00:28:21 +0100428 (tolog & LW_BYTES) ? "" : "+", s->logs.bytes_in,
Willy Tarreaubaaee002006-06-26 02:48:02 +0200429 sess_term_cond[(s->flags & SN_ERR_MASK) >> SN_ERR_SHIFT],
430 sess_fin_state[(s->flags & SN_FINST_MASK) >> SN_FINST_SHIFT],
Willy Tarreauebd61602006-12-30 11:54:15 +0100431 actconn, fe->feconn, be->beprm->beconn, s->srv ? s->srv->cur_sess : 0,
Willy Tarreaubaaee002006-06-26 02:48:02 +0200432 s->logs.srv_queue_size, s->logs.prx_queue_size);
433 }
434
435 s->logs.logwait = 0;
436}
437
438
439/*
440 * Initializes some data needed later.
441 */
442void init_log()
443{
444 int i;
445 char *tmp;
446
447 /* initialize the log header encoding map : '{|}"#' should be encoded with
448 * '#' as prefix, as well as non-printable characters ( <32 or >= 127 ).
449 * URL encoding only requires '"', '#' to be encoded as well as non-
450 * printable characters above.
451 */
452 memset(hdr_encode_map, 0, sizeof(hdr_encode_map));
453 memset(url_encode_map, 0, sizeof(url_encode_map));
454 for (i = 0; i < 32; i++) {
455 FD_SET(i, hdr_encode_map);
456 FD_SET(i, url_encode_map);
457 }
458 for (i = 127; i < 256; i++) {
459 FD_SET(i, hdr_encode_map);
460 FD_SET(i, url_encode_map);
461 }
462
463 tmp = "\"#{|}";
464 while (*tmp) {
465 FD_SET(*tmp, hdr_encode_map);
466 tmp++;
467 }
468
469 tmp = "\"#";
470 while (*tmp) {
471 FD_SET(*tmp, url_encode_map);
472 tmp++;
473 }
474}
475
476/*
477 * Local variables:
478 * c-indent-level: 8
479 * c-basic-offset: 8
480 * End:
481 */