blob: 8408be7c6f09cd59073495ba0de73e9fa5cdff72 [file] [log] [blame]
Willy Tarreau67b5a162019-08-11 16:38:56 +02001/*
2 * Event sink management
3 *
4 * Copyright (C) 2000-2019 Willy Tarreau - w@1wt.eu
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation, version 2.1
9 * exclusively.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
Willy Tarreau4c7e4b72020-05-27 12:58:42 +020021#include <haproxy/api.h>
Emeric Brun99c453d2020-05-25 15:01:04 +020022#include <common/cfgparse.h>
Willy Tarreau83487a82020-06-04 20:19:54 +020023#include <haproxy/cli.h>
Willy Tarreaueb6f7012020-05-27 16:21:26 +020024#include <import/ist.h>
Willy Tarreau853b2972020-05-27 18:01:47 +020025#include <haproxy/list.h>
Willy Tarreau92b4f132020-06-01 11:05:15 +020026#include <haproxy/time.h>
Willy Tarreau67b5a162019-08-11 16:38:56 +020027#include <proto/log.h>
Willy Tarreaud2ad57c2020-06-03 19:43:35 +020028#include <haproxy/ring.h>
Willy Tarreau3727a8a2020-06-04 17:37:26 +020029#include <haproxy/signal.h>
Willy Tarreauba2f73d2020-06-03 20:02:28 +020030#include <haproxy/sink.h>
Willy Tarreau9f830d72019-08-26 18:17:04 +020031#include <proto/stream_interface.h>
Willy Tarreau67b5a162019-08-11 16:38:56 +020032
33struct list sink_list = LIST_HEAD_INIT(sink_list);
34
Emeric Brun99c453d2020-05-25 15:01:04 +020035struct sink *cfg_sink;
36
Willy Tarreau67b5a162019-08-11 16:38:56 +020037struct sink *sink_find(const char *name)
38{
39 struct sink *sink;
40
41 list_for_each_entry(sink, &sink_list, sink_list)
42 if (strcmp(sink->name, name) == 0)
43 return sink;
44 return NULL;
45}
46
47/* creates a new sink and adds it to the list, it's still generic and not fully
48 * initialized. Returns NULL on allocation failure. If another one already
49 * exists with the same name, it will be returned. The caller can detect it as
50 * a newly created one has type SINK_TYPE_NEW.
51 */
Willy Tarreau973e6622019-08-20 11:57:52 +020052static struct sink *__sink_new(const char *name, const char *desc, enum sink_fmt fmt)
Willy Tarreau67b5a162019-08-11 16:38:56 +020053{
54 struct sink *sink;
55
56 sink = sink_find(name);
57 if (sink)
58 goto end;
59
Emeric Brun494c5052020-05-28 11:13:15 +020060 sink = calloc(1, sizeof(*sink));
Willy Tarreau67b5a162019-08-11 16:38:56 +020061 if (!sink)
62 goto end;
63
Emeric Brun99c453d2020-05-25 15:01:04 +020064 sink->name = strdup(name);
65 sink->desc = strdup(desc);
Willy Tarreau67b5a162019-08-11 16:38:56 +020066 sink->fmt = fmt;
67 sink->type = SINK_TYPE_NEW;
Christopher Fauleta63a5c22019-11-15 15:10:12 +010068 sink->maxlen = BUFSIZE;
Willy Tarreau67b5a162019-08-11 16:38:56 +020069 /* address will be filled by the caller if needed */
Willy Tarreau973e6622019-08-20 11:57:52 +020070 sink->ctx.fd = -1;
Willy Tarreau67b5a162019-08-11 16:38:56 +020071 sink->ctx.dropped = 0;
72 HA_RWLOCK_INIT(&sink->ctx.lock);
73 LIST_ADDQ(&sink_list, &sink->sink_list);
74 end:
75 return sink;
76}
77
Willy Tarreau973e6622019-08-20 11:57:52 +020078/* creates a sink called <name> of type FD associated to fd <fd>, format <fmt>,
79 * and description <desc>. Returns NULL on allocation failure or conflict.
80 * Perfect duplicates are merged (same type, fd, and name).
81 */
82struct sink *sink_new_fd(const char *name, const char *desc, enum sink_fmt fmt, int fd)
83{
84 struct sink *sink;
85
86 sink = __sink_new(name, desc, fmt);
87 if (!sink || (sink->type == SINK_TYPE_FD && sink->ctx.fd == fd))
88 goto end;
89
90 if (sink->type != SINK_TYPE_NEW) {
91 sink = NULL;
92 goto end;
93 }
94
95 sink->type = SINK_TYPE_FD;
96 sink->ctx.fd = fd;
97 end:
98 return sink;
99}
100
Willy Tarreau4ed23ca2019-08-23 15:47:49 +0200101/* creates a sink called <name> of type BUF of size <size>, format <fmt>,
102 * and description <desc>. Returns NULL on allocation failure or conflict.
103 * Perfect duplicates are merged (same type and name). If sizes differ, the
104 * largest one is kept.
105 */
106struct sink *sink_new_buf(const char *name, const char *desc, enum sink_fmt fmt, size_t size)
107{
108 struct sink *sink;
109
110 sink = __sink_new(name, desc, fmt);
111 if (!sink)
112 goto fail;
113
114 if (sink->type == SINK_TYPE_BUFFER) {
115 /* such a buffer already exists, we may have to resize it */
116 if (!ring_resize(sink->ctx.ring, size))
117 goto fail;
118 goto end;
119 }
120
121 if (sink->type != SINK_TYPE_NEW) {
122 /* already exists of another type */
123 goto fail;
124 }
125
126 sink->ctx.ring = ring_new(size);
127 if (!sink->ctx.ring) {
128 LIST_DEL(&sink->sink_list);
Emeric Brun99c453d2020-05-25 15:01:04 +0200129 free(sink->name);
130 free(sink->desc);
Willy Tarreau4ed23ca2019-08-23 15:47:49 +0200131 free(sink);
132 goto fail;
133 }
134
135 sink->type = SINK_TYPE_BUFFER;
136 end:
137 return sink;
138 fail:
139 return NULL;
140}
141
Willy Tarreau67b5a162019-08-11 16:38:56 +0200142/* tries to send <nmsg> message parts (up to 8, ignored above) from message
143 * array <msg> to sink <sink>. Formating according to the sink's preference is
Willy Tarreau8f240232019-08-27 16:41:06 +0200144 * done here. Lost messages are NOT accounted for. It is preferable to call
145 * sink_write() instead which will also try to emit the number of dropped
146 * messages when there are any. It returns >0 if it could write anything,
147 * <=0 otherwise.
Willy Tarreau67b5a162019-08-11 16:38:56 +0200148 */
Emeric Brunbd163812020-05-06 14:33:46 +0200149ssize_t __sink_write(struct sink *sink, const struct ist msg[], size_t nmsg,
150 int level, int facility, struct ist *tag,
151 struct ist *pid, struct ist *sd)
Willy Tarreau67b5a162019-08-11 16:38:56 +0200152{
Emeric Brunbd163812020-05-06 14:33:46 +0200153 int log_format;
Willy Tarreau67b5a162019-08-11 16:38:56 +0200154 char short_hdr[4];
Emeric Brunbd163812020-05-06 14:33:46 +0200155 struct ist pfx[6];
Willy Tarreaua1426de2019-08-27 14:21:02 +0200156 size_t npfx = 0;
Emeric Brunbd163812020-05-06 14:33:46 +0200157 char *hdr_ptr;
158 int fac_level;
159
160 if (sink->fmt == SINK_FMT_RAW)
161 goto send;
Willy Tarreau67b5a162019-08-11 16:38:56 +0200162
Willy Tarreau53ba9d92019-09-26 08:03:58 +0200163 if (sink->fmt == SINK_FMT_SHORT || sink->fmt == SINK_FMT_TIMED) {
Willy Tarreau67b5a162019-08-11 16:38:56 +0200164 short_hdr[0] = '<';
Emeric Brunbd163812020-05-06 14:33:46 +0200165 short_hdr[1] = '0' + level;
Willy Tarreau67b5a162019-08-11 16:38:56 +0200166 short_hdr[2] = '>';
167
Willy Tarreaua1426de2019-08-27 14:21:02 +0200168 pfx[npfx].ptr = short_hdr;
169 pfx[npfx].len = 3;
170 npfx++;
Emeric Brunbd163812020-05-06 14:33:46 +0200171 if (sink->fmt == SINK_FMT_SHORT)
172 goto send;
Willy Tarreaua1426de2019-08-27 14:21:02 +0200173 }
Willy Tarreau67b5a162019-08-11 16:38:56 +0200174
Emeric Brunbd163812020-05-06 14:33:46 +0200175
Willy Tarreau53ba9d92019-09-26 08:03:58 +0200176 if (sink->fmt == SINK_FMT_ISO || sink->fmt == SINK_FMT_TIMED) {
177 pfx[npfx].ptr = timeofday_as_iso_us(1);
178 pfx[npfx].len = 27;
179 npfx++;
Emeric Brunbd163812020-05-06 14:33:46 +0200180 goto send;
Willy Tarreau53ba9d92019-09-26 08:03:58 +0200181 }
Emeric Brunbd163812020-05-06 14:33:46 +0200182 else if (sink->fmt == SINK_FMT_RFC5424) {
183 pfx[npfx].ptr = logheader_rfc5424;
184 pfx[npfx].len = update_log_hdr_rfc5424(date.tv_sec) - pfx[npfx].ptr;
185 log_format = LOG_FORMAT_RFC5424;
186 }
187 else {
188 pfx[npfx].ptr = logheader;
189 pfx[npfx].len = update_log_hdr(date.tv_sec) - pfx[npfx].ptr;
190 log_format = LOG_FORMAT_RFC3164;
191 sd = NULL;
192 }
193
194 fac_level = (facility << 3) + level;
195 hdr_ptr = pfx[npfx].ptr + 3; /* last digit of the log level */
196 do {
197 *hdr_ptr = '0' + fac_level % 10;
198 fac_level /= 10;
199 hdr_ptr--;
200 } while (fac_level && hdr_ptr > pfx[npfx].ptr);
201 *hdr_ptr = '<';
202 pfx[npfx].len -= hdr_ptr - pfx[npfx].ptr;
203 pfx[npfx].ptr = hdr_ptr;
204 npfx++;
205
206 if (tag && tag->len) {
207 pfx[npfx].ptr = tag->ptr;
208 pfx[npfx].len = tag->len;
209 npfx++;
210 }
211 pfx[npfx].ptr = get_format_pid_sep1(log_format, &pfx[npfx].len);
212 if (pfx[npfx].len)
213 npfx++;
Willy Tarreau53ba9d92019-09-26 08:03:58 +0200214
Emeric Brunbd163812020-05-06 14:33:46 +0200215 if (pid && pid->len) {
216 pfx[npfx].ptr = pid->ptr;
217 pfx[npfx].len = pid->len;
218 npfx++;
219 }
220
221 pfx[npfx].ptr = get_format_pid_sep2(log_format, &pfx[npfx].len);
222 if (pfx[npfx].len)
223 npfx++;
224
225 if (sd && sd->len) {
226 pfx[npfx].ptr = sd->ptr;
227 pfx[npfx].len = sd->len;
228 npfx++;
229 }
230
231send:
Willy Tarreau973e6622019-08-20 11:57:52 +0200232 if (sink->type == SINK_TYPE_FD) {
Willy Tarreau8f240232019-08-27 16:41:06 +0200233 return fd_write_frag_line(sink->ctx.fd, sink->maxlen, pfx, npfx, msg, nmsg, 1);
Willy Tarreau4ed23ca2019-08-23 15:47:49 +0200234 }
235 else if (sink->type == SINK_TYPE_BUFFER) {
Willy Tarreau8f240232019-08-27 16:41:06 +0200236 return ring_write(sink->ctx.ring, sink->maxlen, pfx, npfx, msg, nmsg);
Willy Tarreau973e6622019-08-20 11:57:52 +0200237 }
Willy Tarreau8f240232019-08-27 16:41:06 +0200238 return 0;
239}
Willy Tarreau67b5a162019-08-11 16:38:56 +0200240
Willy Tarreau8f240232019-08-27 16:41:06 +0200241/* Tries to emit a message indicating the number of dropped events. In case of
242 * success, the amount of drops is reduced by as much. It's supposed to be
243 * called under an exclusive lock on the sink to avoid multiple produces doing
244 * the same. On success, >0 is returned, otherwise <=0 on failure.
245 */
Emeric Brunbd163812020-05-06 14:33:46 +0200246int sink_announce_dropped(struct sink *sink, int facility, struct ist *pid)
Willy Tarreau8f240232019-08-27 16:41:06 +0200247{
248 unsigned int dropped;
249 struct buffer msg;
250 struct ist msgvec[1];
251 char logbuf[64];
Emeric Brunbd163812020-05-06 14:33:46 +0200252 struct ist sd;
253 struct ist tag;
Willy Tarreau8f240232019-08-27 16:41:06 +0200254
255 while (unlikely((dropped = sink->ctx.dropped) > 0)) {
256 chunk_init(&msg, logbuf, sizeof(logbuf));
257 chunk_printf(&msg, "%u event%s dropped", dropped, dropped > 1 ? "s" : "");
258 msgvec[0] = ist2(msg.area, msg.data);
Emeric Brunbd163812020-05-06 14:33:46 +0200259
260 sd.ptr = default_rfc5424_sd_log_format;
261 sd.len = 2;
262 tag.ptr = global.log_tag.area;
263 tag.len = global.log_tag.data;
264 if (__sink_write(sink, msgvec, 1, LOG_NOTICE, facility, &tag, pid, &sd) <= 0)
Willy Tarreau8f240232019-08-27 16:41:06 +0200265 return 0;
266 /* success! */
267 HA_ATOMIC_SUB(&sink->ctx.dropped, dropped);
268 }
269 return 1;
Willy Tarreau67b5a162019-08-11 16:38:56 +0200270}
271
Willy Tarreau9f830d72019-08-26 18:17:04 +0200272/* parse the "show events" command, returns 1 if a message is returned, otherwise zero */
273static int cli_parse_show_events(char **args, char *payload, struct appctx *appctx, void *private)
274{
275 struct sink *sink;
Willy Tarreau1d181e42019-08-30 11:17:01 +0200276 int arg;
Willy Tarreau9f830d72019-08-26 18:17:04 +0200277
278 args++; // make args[1] the 1st arg
279
280 if (!*args[1]) {
281 /* no arg => report the list of supported sink */
Willy Tarreau1d181e42019-08-30 11:17:01 +0200282 chunk_printf(&trash, "Supported events sinks are listed below. Add -w(wait), -n(new). Any key to stop\n");
Willy Tarreau9f830d72019-08-26 18:17:04 +0200283 list_for_each_entry(sink, &sink_list, sink_list) {
284 chunk_appendf(&trash, " %-10s : type=%s, %u dropped, %s\n",
285 sink->name,
286 sink->type == SINK_TYPE_NEW ? "init" :
287 sink->type == SINK_TYPE_FD ? "fd" :
288 sink->type == SINK_TYPE_BUFFER ? "buffer" : "?",
289 sink->ctx.dropped, sink->desc);
290 }
291
292 trash.area[trash.data] = 0;
293 return cli_msg(appctx, LOG_WARNING, trash.area);
294 }
295
296 if (!cli_has_level(appctx, ACCESS_LVL_OPER))
297 return 1;
298
299 sink = sink_find(args[1]);
300 if (!sink)
301 return cli_err(appctx, "No such event sink");
302
303 if (sink->type != SINK_TYPE_BUFFER)
304 return cli_msg(appctx, LOG_NOTICE, "Nothing to report for this sink");
305
Willy Tarreau1d181e42019-08-30 11:17:01 +0200306 for (arg = 2; *args[arg]; arg++) {
307 if (strcmp(args[arg], "-w") == 0)
308 appctx->ctx.cli.i0 |= 1; // wait mode
309 else if (strcmp(args[arg], "-n") == 0)
310 appctx->ctx.cli.i0 |= 2; // seek to new
311 else if (strcmp(args[arg], "-nw") == 0 || strcmp(args[arg], "-wn") == 0)
312 appctx->ctx.cli.i0 |= 3; // seek to new + wait
313 else
314 return cli_err(appctx, "unknown option");
315 }
Willy Tarreau9f830d72019-08-26 18:17:04 +0200316 return ring_attach_cli(sink->ctx.ring, appctx);
317}
318
Emeric Brun494c5052020-05-28 11:13:15 +0200319/* Pre-configures a ring proxy to emmit connections */
320void sink_setup_proxy(struct proxy *px)
321{
322 px->last_change = now.tv_sec;
323 px->cap = PR_CAP_FE | PR_CAP_BE;
324 px->maxconn = 0;
325 px->conn_retries = 1;
326 px->timeout.server = TICK_ETERNITY;
327 px->timeout.client = TICK_ETERNITY;
328 px->timeout.connect = TICK_ETERNITY;
329 px->accept = NULL;
330 px->options2 |= PR_O2_INDEPSTR | PR_O2_SMARTCON | PR_O2_SMARTACC;
331 px->bind_proc = 0; /* will be filled by users */
332}
333
334/*
335 * IO Handler to handle message push to syslog tcp server
336 */
337static void sink_forward_io_handler(struct appctx *appctx)
338{
339 struct stream_interface *si = appctx->owner;
340 struct stream *s = si_strm(si);
341 struct sink *sink = strm_fe(s)->parent;
342 struct sink_forward_target *sft = appctx->ctx.sft.ptr;
343 struct ring *ring = sink->ctx.ring;
344 struct buffer *buf = &ring->buf;
345 uint64_t msg_len;
346 size_t len, cnt, ofs;
347 int ret = 0;
348
349 /* if stopping was requested, close immediatly */
350 if (unlikely(stopping))
351 goto close;
352
353 /* for rex because it seems reset to timeout
354 * and we don't want expire on this case
355 * with a syslog server
356 */
357 si_oc(si)->rex = TICK_ETERNITY;
358 /* rto should not change but it seems the case */
359 si_oc(si)->rto = TICK_ETERNITY;
360
361 /* an error was detected */
362 if (unlikely(si_ic(si)->flags & (CF_WRITE_ERROR|CF_SHUTW)))
363 goto close;
364
365 /* con closed by server side */
366 if ((si_oc(si)->flags & CF_SHUTW))
367 goto close;
368
369 /* if the connection is not established, inform the stream that we want
370 * to be notified whenever the connection completes.
371 */
372 if (si_opposite(si)->state < SI_ST_EST) {
373 si_cant_get(si);
374 si_rx_conn_blk(si);
375 si_rx_endp_more(si);
376 return;
377 }
378
379 HA_SPIN_LOCK(SFT_LOCK, &sft->lock);
380 if (appctx != sft->appctx) {
381 HA_SPIN_UNLOCK(SFT_LOCK, &sft->lock);
382 goto close;
383 }
384 ofs = sft->ofs;
385
386 HA_RWLOCK_WRLOCK(LOGSRV_LOCK, &ring->lock);
387 LIST_DEL_INIT(&appctx->wait_entry);
388 HA_RWLOCK_WRUNLOCK(LOGSRV_LOCK, &ring->lock);
389
390 HA_RWLOCK_RDLOCK(LOGSRV_LOCK, &ring->lock);
391
392 /* explanation for the initialization below: it would be better to do
393 * this in the parsing function but this would occasionally result in
394 * dropped events because we'd take a reference on the oldest message
395 * and keep it while being scheduled. Thus instead let's take it the
396 * first time we enter here so that we have a chance to pass many
397 * existing messages before grabbing a reference to a location. This
398 * value cannot be produced after initialization.
399 */
400 if (unlikely(ofs == ~0)) {
401 ofs = 0;
402
403 HA_ATOMIC_ADD(b_peek(buf, ofs), 1);
404 ofs += ring->ofs;
405 }
406
407 /* we were already there, adjust the offset to be relative to
408 * the buffer's head and remove us from the counter.
409 */
410 ofs -= ring->ofs;
411 BUG_ON(ofs >= buf->size);
412 HA_ATOMIC_SUB(b_peek(buf, ofs), 1);
413
414 /* in this loop, ofs always points to the counter byte that precedes
415 * the message so that we can take our reference there if we have to
416 * stop before the end (ret=0).
417 */
418 if (si_opposite(si)->state == SI_ST_EST) {
419 ret = 1;
420 while (ofs + 1 < b_data(buf)) {
421 cnt = 1;
422 len = b_peek_varint(buf, ofs + cnt, &msg_len);
423 if (!len)
424 break;
425 cnt += len;
426 BUG_ON(msg_len + ofs + cnt + 1 > b_data(buf));
427
428 if (unlikely(msg_len + 1 > b_size(&trash))) {
429 /* too large a message to ever fit, let's skip it */
430 ofs += cnt + msg_len;
431 continue;
432 }
433
434 chunk_reset(&trash);
435 len = b_getblk(buf, trash.area, msg_len, ofs + cnt);
436 trash.data += len;
437 trash.area[trash.data++] = '\n';
438
439 if (ci_putchk(si_ic(si), &trash) == -1) {
440 si_rx_room_blk(si);
441 ret = 0;
442 break;
443 }
444 ofs += cnt + msg_len;
445 }
446
447 HA_ATOMIC_ADD(b_peek(buf, ofs), 1);
448 ofs += ring->ofs;
449 sft->ofs = ofs;
450 }
451 HA_RWLOCK_RDUNLOCK(LOGSRV_LOCK, &ring->lock);
452
453 if (ret) {
454 /* let's be woken up once new data arrive */
455 HA_RWLOCK_WRLOCK(LOGSRV_LOCK, &ring->lock);
456 LIST_ADDQ(&ring->waiters, &appctx->wait_entry);
457 HA_RWLOCK_WRUNLOCK(LOGSRV_LOCK, &ring->lock);
458 si_rx_endp_done(si);
459 }
460 HA_SPIN_UNLOCK(SFT_LOCK, &sft->lock);
461
462 /* always drain data from server */
463 co_skip(si_oc(si), si_oc(si)->output);
464 return;
465
466close:
467 si_shutw(si);
468 si_shutr(si);
469 si_ic(si)->flags |= CF_READ_NULL;
470}
471
Emeric Brun97556472020-05-30 01:42:45 +0200472/*
473 * IO Handler to handle message push to syslog tcp server
474 * using octet counting frames
475 */
476static void sink_forward_oc_io_handler(struct appctx *appctx)
477{
478 struct stream_interface *si = appctx->owner;
479 struct stream *s = si_strm(si);
480 struct sink *sink = strm_fe(s)->parent;
481 struct sink_forward_target *sft = appctx->ctx.sft.ptr;
482 struct ring *ring = sink->ctx.ring;
483 struct buffer *buf = &ring->buf;
484 uint64_t msg_len;
485 size_t len, cnt, ofs;
486 int ret = 0;
487 char *p;
488
489 /* if stopping was requested, close immediatly */
490 if (unlikely(stopping))
491 goto close;
492
493 /* for rex because it seems reset to timeout
494 * and we don't want expire on this case
495 * with a syslog server
496 */
497 si_oc(si)->rex = TICK_ETERNITY;
498 /* rto should not change but it seems the case */
499 si_oc(si)->rto = TICK_ETERNITY;
500
501 /* an error was detected */
502 if (unlikely(si_ic(si)->flags & (CF_WRITE_ERROR|CF_SHUTW)))
503 goto close;
504
505 /* con closed by server side */
506 if ((si_oc(si)->flags & CF_SHUTW))
507 goto close;
508
509 /* if the connection is not established, inform the stream that we want
510 * to be notified whenever the connection completes.
511 */
512 if (si_opposite(si)->state < SI_ST_EST) {
513 si_cant_get(si);
514 si_rx_conn_blk(si);
515 si_rx_endp_more(si);
516 return;
517 }
518
519 HA_SPIN_LOCK(SFT_LOCK, &sft->lock);
520 if (appctx != sft->appctx) {
521 HA_SPIN_UNLOCK(SFT_LOCK, &sft->lock);
522 goto close;
523 }
524 ofs = sft->ofs;
525
526 HA_RWLOCK_WRLOCK(LOGSRV_LOCK, &ring->lock);
527 LIST_DEL_INIT(&appctx->wait_entry);
528 HA_RWLOCK_WRUNLOCK(LOGSRV_LOCK, &ring->lock);
529
530 HA_RWLOCK_RDLOCK(LOGSRV_LOCK, &ring->lock);
531
532 /* explanation for the initialization below: it would be better to do
533 * this in the parsing function but this would occasionally result in
534 * dropped events because we'd take a reference on the oldest message
535 * and keep it while being scheduled. Thus instead let's take it the
536 * first time we enter here so that we have a chance to pass many
537 * existing messages before grabbing a reference to a location. This
538 * value cannot be produced after initialization.
539 */
540 if (unlikely(ofs == ~0)) {
541 ofs = 0;
542
543 HA_ATOMIC_ADD(b_peek(buf, ofs), 1);
544 ofs += ring->ofs;
545 }
546
547 /* we were already there, adjust the offset to be relative to
548 * the buffer's head and remove us from the counter.
549 */
550 ofs -= ring->ofs;
551 BUG_ON(ofs >= buf->size);
552 HA_ATOMIC_SUB(b_peek(buf, ofs), 1);
553
554 /* in this loop, ofs always points to the counter byte that precedes
555 * the message so that we can take our reference there if we have to
556 * stop before the end (ret=0).
557 */
558 if (si_opposite(si)->state == SI_ST_EST) {
559 ret = 1;
560 while (ofs + 1 < b_data(buf)) {
561 cnt = 1;
562 len = b_peek_varint(buf, ofs + cnt, &msg_len);
563 if (!len)
564 break;
565 cnt += len;
566 BUG_ON(msg_len + ofs + cnt + 1 > b_data(buf));
567
568 chunk_reset(&trash);
569 p = ulltoa(msg_len, trash.area, b_size(&trash));
570 if (p) {
571 trash.data = (p - trash.area) + 1;
572 *p = ' ';
573 }
574
575 if (!p || (trash.data + msg_len > b_size(&trash))) {
576 /* too large a message to ever fit, let's skip it */
577 ofs += cnt + msg_len;
578 continue;
579 }
580
581 trash.data += b_getblk(buf, p + 1, msg_len, ofs + cnt);
582
583 if (ci_putchk(si_ic(si), &trash) == -1) {
584 si_rx_room_blk(si);
585 ret = 0;
586 break;
587 }
588 ofs += cnt + msg_len;
589 }
590
591 HA_ATOMIC_ADD(b_peek(buf, ofs), 1);
592 ofs += ring->ofs;
593 sft->ofs = ofs;
594 }
595 HA_RWLOCK_RDUNLOCK(LOGSRV_LOCK, &ring->lock);
596
597 if (ret) {
598 /* let's be woken up once new data arrive */
599 HA_RWLOCK_WRLOCK(LOGSRV_LOCK, &ring->lock);
600 LIST_ADDQ(&ring->waiters, &appctx->wait_entry);
601 HA_RWLOCK_WRUNLOCK(LOGSRV_LOCK, &ring->lock);
602 si_rx_endp_done(si);
603 }
604 HA_SPIN_UNLOCK(SFT_LOCK, &sft->lock);
605
606 /* always drain data from server */
607 co_skip(si_oc(si), si_oc(si)->output);
608 return;
609
610close:
611 si_shutw(si);
612 si_shutr(si);
613 si_ic(si)->flags |= CF_READ_NULL;
614}
615
Emeric Brun494c5052020-05-28 11:13:15 +0200616void __sink_forward_session_deinit(struct sink_forward_target *sft)
617{
618 struct stream_interface *si;
619 struct stream *s;
620 struct sink *sink;
621
622 if (!sft->appctx)
623 return;
624
625 si = sft->appctx->owner;
626 if (!si)
627 return;
628
629 s = si_strm(si);
630 if (!s)
631 return;
632
633 sink = strm_fe(s)->parent;
634 if (!sink)
635 return;
636
637 HA_RWLOCK_WRLOCK(LOGSRV_LOCK, &sink->ctx.ring->lock);
638 LIST_DEL_INIT(&sft->appctx->wait_entry);
639 HA_RWLOCK_WRUNLOCK(LOGSRV_LOCK, &sink->ctx.ring->lock);
640
641 sft->appctx = NULL;
642 task_wakeup(sink->forward_task, TASK_WOKEN_MSG);
643}
644
645
646static void sink_forward_session_release(struct appctx *appctx)
647{
648 struct sink_forward_target *sft = appctx->ctx.peers.ptr;
649
650 if (!sft)
651 return;
652
653 HA_SPIN_LOCK(SFT_LOCK, &sft->lock);
654 if (sft->appctx == appctx)
655 __sink_forward_session_deinit(sft);
656 HA_SPIN_UNLOCK(SFT_LOCK, &sft->lock);
657}
658
659static struct applet sink_forward_applet = {
660 .obj_type = OBJ_TYPE_APPLET,
661 .name = "<SINKFWD>", /* used for logging */
662 .fct = sink_forward_io_handler,
663 .release = sink_forward_session_release,
664};
665
Emeric Brun97556472020-05-30 01:42:45 +0200666static struct applet sink_forward_oc_applet = {
667 .obj_type = OBJ_TYPE_APPLET,
668 .name = "<SINKFWDOC>", /* used for logging */
669 .fct = sink_forward_oc_io_handler,
670 .release = sink_forward_session_release,
671};
672
Emeric Brun494c5052020-05-28 11:13:15 +0200673/*
674 * Create a new peer session in assigned state (connect will start automatically)
675 */
676static struct appctx *sink_forward_session_create(struct sink *sink, struct sink_forward_target *sft)
677{
678 struct proxy *p = sink->forward_px;
679 struct appctx *appctx;
680 struct session *sess;
681 struct stream *s;
Emeric Brun97556472020-05-30 01:42:45 +0200682 struct applet *applet = &sink_forward_applet;
683
684 if (sft->srv->log_proto == SRV_LOG_PROTO_OCTET_COUNTING)
685 applet = &sink_forward_oc_applet;
Emeric Brun494c5052020-05-28 11:13:15 +0200686
Emeric Brun97556472020-05-30 01:42:45 +0200687 appctx = appctx_new(applet, tid_bit);
Emeric Brun494c5052020-05-28 11:13:15 +0200688 if (!appctx)
689 goto out_close;
690
691 appctx->ctx.sft.ptr = (void *)sft;
692
693 sess = session_new(p, NULL, &appctx->obj_type);
694 if (!sess) {
695 ha_alert("out of memory in peer_session_create().\n");
696 goto out_free_appctx;
697 }
698
699 if ((s = stream_new(sess, &appctx->obj_type)) == NULL) {
700 ha_alert("Failed to initialize stream in peer_session_create().\n");
701 goto out_free_sess;
702 }
703
704
705 s->target = &sft->srv->obj_type;
706 if (!sockaddr_alloc(&s->target_addr))
707 goto out_free_strm;
708 *s->target_addr = sft->srv->addr;
709 s->flags = SF_ASSIGNED|SF_ADDR_SET;
710 s->si[1].flags |= SI_FL_NOLINGER;
711
712 s->do_log = NULL;
713 s->uniq_id = 0;
714
715 s->res.flags |= CF_READ_DONTWAIT;
716 /* for rto and rex to eternity to not expire on idle recv:
717 * We are using a syslog server.
718 */
719 s->res.rto = TICK_ETERNITY;
720 s->res.rex = TICK_ETERNITY;
721 sft->appctx = appctx;
722 task_wakeup(s->task, TASK_WOKEN_INIT);
723 return appctx;
724
725 /* Error unrolling */
726 out_free_strm:
727 LIST_DEL(&s->list);
728 pool_free(pool_head_stream, s);
729 out_free_sess:
730 session_free(sess);
731 out_free_appctx:
732 appctx_free(appctx);
733 out_close:
734 return NULL;
735}
736
737/*
738 * Task to handle connctions to forward servers
739 */
740static struct task *process_sink_forward(struct task * task, void *context, unsigned short state)
741{
742 struct sink *sink = (struct sink *)context;
743 struct sink_forward_target *sft = sink->sft;
744
745 task->expire = TICK_ETERNITY;
746
747 if (!stopping) {
748 while (sft) {
749 HA_SPIN_LOCK(SFT_LOCK, &sft->lock);
750 /* if appctx is NULL, start a new session */
751 if (!sft->appctx)
752 sft->appctx = sink_forward_session_create(sink, sft);
753 HA_SPIN_UNLOCK(SFT_LOCK, &sft->lock);
754 sft = sft->next;
755 }
756 }
757 else {
758 while (sft) {
759 HA_SPIN_LOCK(SFT_LOCK, &sft->lock);
760 /* awake applet to perform a clean close */
761 if (sft->appctx)
762 appctx_wakeup(sft->appctx);
763 HA_SPIN_UNLOCK(SFT_LOCK, &sft->lock);
764 sft = sft->next;
765 }
766 }
767
768 return task;
769}
770/*
771 * Init task to manage connctions to forward servers
772 *
773 * returns 0 in case of error.
774 */
775int sink_init_forward(struct sink *sink)
776{
777 sink->forward_task = task_new(MAX_THREADS_MASK);
778 if (!sink->forward_task)
779 return 0;
780
781 sink->forward_task->process = process_sink_forward;
782 sink->forward_task->context = (void *)sink;
783 sink->forward_sighandler = signal_register_task(0, sink->forward_task, 0);
784 task_wakeup(sink->forward_task, TASK_WOKEN_INIT);
785 return 1;
786}
Emeric Brun99c453d2020-05-25 15:01:04 +0200787/*
788 * Parse "ring" section and create corresponding sink buffer.
789 *
790 * The function returns 0 in success case, otherwise, it returns error
791 * flags.
792 */
793int cfg_parse_ring(const char *file, int linenum, char **args, int kwm)
794{
795 int err_code = 0;
796 const char *inv;
797 size_t size = BUFSIZE;
Emeric Brun494c5052020-05-28 11:13:15 +0200798 struct proxy *p;
Emeric Brun99c453d2020-05-25 15:01:04 +0200799
800 if (strcmp(args[0], "ring") == 0) { /* new peers section */
801 if (!*args[1]) {
802 ha_alert("parsing [%s:%d] : missing ring name.\n", file, linenum);
803 err_code |= ERR_ALERT | ERR_FATAL;
804 goto err;
805 }
806
807 inv = invalid_char(args[1]);
808 if (inv) {
809 ha_alert("parsing [%s:%d] : invalid ring name '%s' (character '%c' is not permitted).\n", file, linenum, args[1], *inv);
810 err_code |= ERR_ALERT | ERR_FATAL;
811 goto err;
812 }
813
814 if (sink_find(args[1])) {
815 ha_alert("parsing [%s:%d] : sink named '%s' already exists.\n", file, linenum, args[1]);
816 err_code |= ERR_ALERT | ERR_FATAL;
817 goto err;
818 }
819
820 cfg_sink = sink_new_buf(args[1], args[1] , SINK_FMT_RAW, size);
821 if (!cfg_sink || cfg_sink->type != SINK_TYPE_BUFFER) {
822 ha_alert("parsing [%s:%d] : unable to create a new sink buffer for ring '%s'.\n", file, linenum, args[1]);
823 err_code |= ERR_ALERT | ERR_FATAL;
824 goto err;
825 }
Emeric Brun494c5052020-05-28 11:13:15 +0200826
827 /* allocate new proxy to handle forwards */
828 p = calloc(1, sizeof *p);
829 if (!p) {
830 ha_alert("parsing [%s:%d] : out of memory.\n", file, linenum);
831 err_code |= ERR_ALERT | ERR_FATAL;
832 goto err;
833 }
834
835 init_new_proxy(p);
836 sink_setup_proxy(p);
837 p->parent = cfg_sink;
838 p->id = strdup(args[1]);
839 p->conf.args.file = p->conf.file = strdup(file);
840 p->conf.args.line = p->conf.line = linenum;
841 cfg_sink->forward_px = p;
Emeric Brun99c453d2020-05-25 15:01:04 +0200842 }
843 else if (strcmp(args[0], "size") == 0) {
844 size = atol(args[1]);
845 if (!size) {
846 ha_alert("parsing [%s:%d] : invalid size '%s' for new sink buffer.\n", file, linenum, args[1]);
847 err_code |= ERR_ALERT | ERR_FATAL;
848 goto err;
849 }
850
851 if (!cfg_sink || (cfg_sink->type != SINK_TYPE_BUFFER)
852 || !ring_resize(cfg_sink->ctx.ring, size)) {
853 ha_alert("parsing [%s:%d] : fail to set sink buffer size '%s'.\n", file, linenum, args[1]);
854 err_code |= ERR_ALERT | ERR_FATAL;
855 goto err;
856 }
857 }
Emeric Brun494c5052020-05-28 11:13:15 +0200858 else if (strcmp(args[0],"server") == 0) {
859 err_code |= parse_server(file, linenum, args, cfg_sink->forward_px, NULL, 1, 0);
860 }
861 else if (strcmp(args[0],"timeout") == 0) {
862 if (!cfg_sink || !cfg_sink->forward_px) {
863 ha_alert("parsing [%s:%d] : unable to set timeout '%s'.\n", file, linenum, args[1]);
864 err_code |= ERR_ALERT | ERR_FATAL;
865 goto err;
866 }
867
868 if (strcmp(args[1], "connect") == 0 ||
869 strcmp(args[1], "server") == 0) {
870 const char *res;
871 unsigned int tout;
872
873 if (!*args[2]) {
874 ha_alert("parsing [%s:%d] : '%s %s' expects <time> as argument.\n",
875 file, linenum, args[0], args[1]);
876 err_code |= ERR_ALERT | ERR_FATAL;
877 goto err;
878 }
879 res = parse_time_err(args[2], &tout, TIME_UNIT_MS);
880 if (res == PARSE_TIME_OVER) {
881 ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to <%s %s>, maximum value is 2147483647 ms (~24.8 days).\n",
882 file, linenum, args[2], args[0], args[1]);
883 err_code |= ERR_ALERT | ERR_FATAL;
884 goto err;
885 }
886 else if (res == PARSE_TIME_UNDER) {
887 ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to <%s %s>, minimum non-null value is 1 ms.\n",
888 file, linenum, args[2], args[0], args[1]);
889 err_code |= ERR_ALERT | ERR_FATAL;
890 goto err;
891 }
892 else if (res) {
893 ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to <%s %s>.\n",
894 file, linenum, *res, args[0], args[1]);
895 err_code |= ERR_ALERT | ERR_FATAL;
896 goto err;
897 }
898 if (args[1][2] == 'c')
899 cfg_sink->forward_px->timeout.connect = tout;
900 else
901 cfg_sink->forward_px->timeout.server = tout;
902 }
903 }
Emeric Brun99c453d2020-05-25 15:01:04 +0200904 else if (strcmp(args[0],"format") == 0) {
905 if (!cfg_sink) {
906 ha_alert("parsing [%s:%d] : unable to set format '%s'.\n", file, linenum, args[1]);
907 err_code |= ERR_ALERT | ERR_FATAL;
908 goto err;
909 }
910
911 if (strcmp(args[1], "raw") == 0) {
912 cfg_sink->fmt = SINK_FMT_RAW;
913 }
914 else if (strcmp(args[1], "short") == 0) {
915 cfg_sink->fmt = SINK_FMT_SHORT;
916 }
917 else if (strcmp(args[1], "iso") == 0) {
918 cfg_sink->fmt = SINK_FMT_ISO;
919 }
920 else if (strcmp(args[1], "timed") == 0) {
921 cfg_sink->fmt = SINK_FMT_TIMED;
922 }
923 else if (strcmp(args[1], "rfc3164") == 0) {
924 cfg_sink->fmt = SINK_FMT_RFC3164;
925 }
926 else if (strcmp(args[1], "rfc5424") == 0) {
927 cfg_sink->fmt = SINK_FMT_RFC5424;
928 }
929 else {
930 ha_alert("parsing [%s:%d] : unknown format '%s'.\n", file, linenum, args[1]);
931 err_code |= ERR_ALERT | ERR_FATAL;
932 goto err;
933 }
934 }
935 else if (strcmp(args[0],"maxlen") == 0) {
936 if (!cfg_sink) {
937 ha_alert("parsing [%s:%d] : unable to set event max length '%s'.\n", file, linenum, args[1]);
938 err_code |= ERR_ALERT | ERR_FATAL;
939 goto err;
940 }
941
942 cfg_sink->maxlen = atol(args[1]);
943 if (!cfg_sink->maxlen) {
944 ha_alert("parsing [%s:%d] : invalid size '%s' for new sink buffer.\n", file, linenum, args[1]);
945 err_code |= ERR_ALERT | ERR_FATAL;
946 goto err;
947 }
948 }
949 else if (strcmp(args[0],"description") == 0) {
950 if (!cfg_sink) {
951 ha_alert("parsing [%s:%d] : unable to set description '%s'.\n", file, linenum, args[1]);
952 err_code |= ERR_ALERT | ERR_FATAL;
953 goto err;
954 }
955
956 if (!*args[1]) {
957 ha_alert("parsing [%s:%d] : missing ring description text.\n", file, linenum);
958 err_code |= ERR_ALERT | ERR_FATAL;
959 goto err;
960 }
961
962 free(cfg_sink->desc);
963
964 cfg_sink->desc = strdup(args[1]);
965 if (!cfg_sink->desc) {
966 ha_alert("parsing [%s:%d] : fail to set description '%s'.\n", file, linenum, args[1]);
967 err_code |= ERR_ALERT | ERR_FATAL;
968 goto err;
969 }
970 }
Emeric Brun9f2ff3a2020-05-29 15:47:52 +0200971 else {
972 ha_alert("parsing [%s:%d] : unknown statement '%s'.\n", file, linenum, args[0]);
973 err_code |= ERR_ALERT | ERR_FATAL;
974 goto err;
975 }
Emeric Brun99c453d2020-05-25 15:01:04 +0200976
977err:
978 return err_code;
979}
980
981/*
982 * Post parsing "ring" section.
983 *
984 * The function returns 0 in success case, otherwise, it returns error
985 * flags.
986 */
987int cfg_post_parse_ring()
988{
989 int err_code = 0;
Emeric Brun494c5052020-05-28 11:13:15 +0200990 struct server *srv;
Emeric Brun99c453d2020-05-25 15:01:04 +0200991
992 if (cfg_sink && (cfg_sink->type == SINK_TYPE_BUFFER)) {
993 if (cfg_sink->maxlen > b_size(&cfg_sink->ctx.ring->buf)) {
994 ha_warning("ring '%s' event max length '%u' exceeds size, forced to size '%lu'.\n",
Willy Tarreau570a22b2020-06-02 12:00:46 +0200995 cfg_sink->name, cfg_sink->maxlen, (unsigned long)b_size(&cfg_sink->ctx.ring->buf));
Emeric Brun99c453d2020-05-25 15:01:04 +0200996 cfg_sink->maxlen = b_size(&cfg_sink->ctx.ring->buf);
997 err_code |= ERR_ALERT;
998 }
Emeric Brun494c5052020-05-28 11:13:15 +0200999
1000 /* prepare forward server descriptors */
1001 if (cfg_sink->forward_px) {
1002 srv = cfg_sink->forward_px->srv;
1003 while (srv) {
1004 struct sink_forward_target *sft;
1005 /* init ssl if needed */
1006 if (srv->use_ssl == 1 && xprt_get(XPRT_SSL) && xprt_get(XPRT_SSL)->prepare_srv) {
1007 if (xprt_get(XPRT_SSL)->prepare_srv(srv)) {
1008 ha_alert("unable to prepare SSL for server '%s' in ring '%s'.\n", srv->id, cfg_sink->name);
1009 err_code |= ERR_ALERT | ERR_FATAL;
1010 }
1011 }
Emeric Brun99c453d2020-05-25 15:01:04 +02001012
Emeric Brun494c5052020-05-28 11:13:15 +02001013 /* allocate sink_forward_target descriptor */
1014 sft = calloc(1, sizeof(*sft));
1015 if (!sft) {
1016 ha_alert("memory allocation error initializing server '%s' in ring '%s'.\n",srv->id, cfg_sink->name);
1017 err_code |= ERR_ALERT | ERR_FATAL;
1018 break;
1019 }
1020 sft->srv = srv;
1021 sft->appctx = NULL;
1022 sft->ofs = ~0; /* init ring offset */
1023 sft->next = cfg_sink->sft;
1024 HA_SPIN_INIT(&sft->lock);
1025
1026 /* mark server attached to the ring */
1027 if (!ring_attach(cfg_sink->ctx.ring)) {
1028 ha_alert("server '%s' sets too many watchers > 255 on ring '%s'.\n", srv->id, cfg_sink->name);
1029 err_code |= ERR_ALERT | ERR_FATAL;
1030 }
1031 cfg_sink->sft = sft;
1032 srv = srv->next;
1033 }
1034 sink_init_forward(cfg_sink);
1035 }
1036 }
Emeric Brun99c453d2020-05-25 15:01:04 +02001037 cfg_sink = NULL;
1038
1039 return err_code;
1040}
1041
1042/* resolve sink names at end of config. Returns 0 on success otherwise error
1043 * flags.
1044*/
1045int post_sink_resolve()
1046{
1047 int err_code = 0;
1048 struct logsrv *logsrv, *logb;
1049 struct sink *sink;
1050 struct proxy *px;
1051
1052 list_for_each_entry_safe(logsrv, logb, &global.logsrvs, list) {
1053 if (logsrv->type == LOG_TARGET_BUFFER) {
1054 sink = sink_find(logsrv->ring_name);
1055 if (!sink || sink->type != SINK_TYPE_BUFFER) {
1056 ha_alert("global log server uses unkown ring named '%s'.\n", logsrv->ring_name);
1057 err_code |= ERR_ALERT | ERR_FATAL;
1058 }
1059 logsrv->sink = sink;
1060 }
1061 }
1062
1063 for (px = proxies_list; px; px = px->next) {
1064 list_for_each_entry_safe(logsrv, logb, &px->logsrvs, list) {
1065 if (logsrv->type == LOG_TARGET_BUFFER) {
1066 sink = sink_find(logsrv->ring_name);
1067 if (!sink || sink->type != SINK_TYPE_BUFFER) {
1068 ha_alert("proxy '%s' log server uses unkown ring named '%s'.\n", px->id, logsrv->ring_name);
1069 err_code |= ERR_ALERT | ERR_FATAL;
1070 }
1071 logsrv->sink = sink;
1072 }
1073 }
1074 }
1075 return err_code;
1076}
1077
1078
Willy Tarreau973e6622019-08-20 11:57:52 +02001079static void sink_init()
1080{
1081 sink_new_fd("stdout", "standard output (fd#1)", SINK_FMT_RAW, 1);
1082 sink_new_fd("stderr", "standard output (fd#2)", SINK_FMT_RAW, 2);
Willy Tarreauf8340e32019-09-26 08:05:15 +02001083 sink_new_buf("buf0", "in-memory ring buffer", SINK_FMT_TIMED, 1048576);
Willy Tarreau4ed23ca2019-08-23 15:47:49 +02001084}
1085
1086static void sink_deinit()
1087{
1088 struct sink *sink, *sb;
1089
1090 list_for_each_entry_safe(sink, sb, &sink_list, sink_list) {
1091 if (sink->type == SINK_TYPE_BUFFER)
1092 ring_free(sink->ctx.ring);
1093 LIST_DEL(&sink->sink_list);
Emeric Brun99c453d2020-05-25 15:01:04 +02001094 free(sink->name);
1095 free(sink->desc);
Willy Tarreau4ed23ca2019-08-23 15:47:49 +02001096 free(sink);
1097 }
Willy Tarreau973e6622019-08-20 11:57:52 +02001098}
1099
1100INITCALL0(STG_REGISTER, sink_init);
Willy Tarreau4ed23ca2019-08-23 15:47:49 +02001101REGISTER_POST_DEINIT(sink_deinit);
Willy Tarreau973e6622019-08-20 11:57:52 +02001102
Willy Tarreau9f830d72019-08-26 18:17:04 +02001103static struct cli_kw_list cli_kws = {{ },{
Willy Tarreaufcf94982019-11-15 15:07:21 +01001104 { { "show", "events", NULL }, "show events [<sink>] : show event sink state", cli_parse_show_events, NULL, NULL },
Willy Tarreau9f830d72019-08-26 18:17:04 +02001105 {{},}
1106}};
1107
1108INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws);
1109
Emeric Brun99c453d2020-05-25 15:01:04 +02001110/* config parsers for this section */
1111REGISTER_CONFIG_SECTION("ring", cfg_parse_ring, cfg_post_parse_ring);
1112REGISTER_POST_CHECK(post_sink_resolve);
1113
Willy Tarreau67b5a162019-08-11 16:38:56 +02001114/*
1115 * Local variables:
1116 * c-indent-level: 8
1117 * c-basic-offset: 8
1118 * End:
1119 */