fix typographical mistake in warning message
[collectd.git] / src / ping.c
1 /**
2  * collectd - src/ping.c
3  * Copyright (C) 2005-2012  Florian octo Forster
4  *
5  * Permission is hereby granted, free of charge, to any person obtaining a
6  * copy of this software and associated documentation files (the "Software"),
7  * to deal in the Software without restriction, including without limitation
8  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9  * and/or sell copies of the Software, and to permit persons to whom the
10  * Software is furnished to do so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice shall be included in
13  * all copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21  * DEALINGS IN THE SOFTWARE.
22  *
23  * Authors:
24  *   Florian octo Forster <octo at collectd.org>
25  **/
26
27 #include "collectd.h"
28
29 #include "common.h"
30 #include "plugin.h"
31 #include "configfile.h"
32 #include "utils_complain.h"
33
34 #include <netinet/in.h>
35 #if HAVE_NETDB_H
36 # include <netdb.h> /* NI_MAXHOST */
37 #endif
38
39 #ifdef HAVE_SYS_CAPABILITY_H
40 # include <sys/capability.h>
41 #endif
42
43 #include <oping.h>
44
45 #ifndef NI_MAXHOST
46 # define NI_MAXHOST 1025
47 #endif
48
49 #if defined(OPING_VERSION) && (OPING_VERSION >= 1003000)
50 # define HAVE_OPING_1_3
51 #endif
52
53 /*
54  * Private data types
55  */
56 struct hostlist_s
57 {
58   char *host;
59
60   uint32_t pkg_sent;
61   uint32_t pkg_recv;
62   uint32_t pkg_missed;
63
64   double latency_total;
65   double latency_squared;
66
67   struct hostlist_s *next;
68 };
69 typedef struct hostlist_s hostlist_t;
70
71 /*
72  * Private variables
73  */
74 static hostlist_t *hostlist_head = NULL;
75
76 static char  *ping_source = NULL;
77 #ifdef HAVE_OPING_1_3
78 static char  *ping_device = NULL;
79 #endif
80 static char  *ping_data = NULL;
81 static int    ping_ttl = PING_DEF_TTL;
82 static double ping_interval = 1.0;
83 static double ping_timeout = 0.9;
84 static int    ping_max_missed = -1;
85
86 static int             ping_thread_loop = 0;
87 static int             ping_thread_error = 0;
88 static pthread_t       ping_thread_id;
89 static pthread_mutex_t ping_lock = PTHREAD_MUTEX_INITIALIZER;
90 static pthread_cond_t  ping_cond = PTHREAD_COND_INITIALIZER;
91
92 static const char *config_keys[] =
93 {
94   "Host",
95   "SourceAddress",
96 #ifdef HAVE_OPING_1_3
97   "Device",
98 #endif
99   "Size",
100   "TTL",
101   "Interval",
102   "Timeout",
103   "MaxMissed"
104 };
105 static int config_keys_num = STATIC_ARRAY_SIZE (config_keys);
106
107 /*
108  * Private functions
109  */
110 /* Assure that `ts->tv_nsec' is in the range 0 .. 999999999 */
111 static void time_normalize (struct timespec *ts) /* {{{ */
112 {
113   while (ts->tv_nsec < 0)
114   {
115     if (ts->tv_sec == 0)
116     {
117       ts->tv_nsec = 0;
118       return;
119     }
120
121     ts->tv_sec  -= 1;
122     ts->tv_nsec += 1000000000;
123   }
124
125   while (ts->tv_nsec >= 1000000000)
126   {
127     ts->tv_sec  += 1;
128     ts->tv_nsec -= 1000000000;
129   }
130 } /* }}} void time_normalize */
131
132 /* Add `ts_int' to `tv_begin' and store the result in `ts_dest'. If the result
133  * is larger than `tv_end', copy `tv_end' to `ts_dest' instead. */
134 static void time_calc (struct timespec *ts_dest, /* {{{ */
135     const struct timespec *ts_int,
136     const struct timeval  *tv_begin,
137     const struct timeval  *tv_end)
138 {
139   ts_dest->tv_sec = tv_begin->tv_sec + ts_int->tv_sec;
140   ts_dest->tv_nsec = (tv_begin->tv_usec * 1000) + ts_int->tv_nsec;
141   time_normalize (ts_dest);
142
143   /* Assure that `(begin + interval) > end'.
144    * This may seem overly complicated, but `tv_sec' is of type `time_t'
145    * which may be `unsigned. *sigh* */
146   if ((tv_end->tv_sec > ts_dest->tv_sec)
147       || ((tv_end->tv_sec == ts_dest->tv_sec)
148         && ((tv_end->tv_usec * 1000) > ts_dest->tv_nsec)))
149   {
150     ts_dest->tv_sec = tv_end->tv_sec;
151     ts_dest->tv_nsec = 1000 * tv_end->tv_usec;
152   }
153
154   time_normalize (ts_dest);
155 } /* }}} void time_calc */
156
157 static int ping_dispatch_all (pingobj_t *pingobj) /* {{{ */
158 {
159   hostlist_t *hl;
160   int status;
161
162   for (pingobj_iter_t *iter = ping_iterator_get (pingobj);
163       iter != NULL;
164       iter = ping_iterator_next (iter))
165   { /* {{{ */
166     char userhost[NI_MAXHOST];
167     double latency;
168     size_t param_size;
169
170     param_size = sizeof (userhost);
171     status = ping_iterator_get_info (iter,
172 #ifdef PING_INFO_USERNAME
173         PING_INFO_USERNAME,
174 #else
175         PING_INFO_HOSTNAME,
176 #endif
177         userhost, &param_size);
178     if (status != 0)
179     {
180       WARNING ("ping plugin: ping_iterator_get_info failed: %s",
181           ping_get_error (pingobj));
182       continue;
183     }
184
185     for (hl = hostlist_head; hl != NULL; hl = hl->next)
186       if (strcmp (userhost, hl->host) == 0)
187         break;
188
189     if (hl == NULL)
190     {
191       WARNING ("ping plugin: Cannot find host %s.", userhost);
192       continue;
193     }
194
195     param_size = sizeof (latency);
196     status = ping_iterator_get_info (iter, PING_INFO_LATENCY,
197         (void *) &latency, &param_size);
198     if (status != 0)
199     {
200       WARNING ("ping plugin: ping_iterator_get_info failed: %s",
201           ping_get_error (pingobj));
202       continue;
203     }
204
205     hl->pkg_sent++;
206     if (latency >= 0.0)
207     {
208       hl->pkg_recv++;
209       hl->latency_total += latency;
210       hl->latency_squared += (latency * latency);
211
212       /* reset missed packages counter */
213       hl->pkg_missed = 0;
214     } else
215       hl->pkg_missed++;
216
217     /* if the host did not answer our last N packages, trigger a resolv. */
218     if ((ping_max_missed >= 0)
219         && (hl->pkg_missed >= ((uint32_t) ping_max_missed)))
220     { /* {{{ */
221       /* we reset the missed package counter here, since we only want to
222        * trigger a resolv every N packages and not every package _AFTER_ N
223        * missed packages */
224       hl->pkg_missed = 0;
225
226       WARNING ("ping plugin: host %s has not answered %d PING requests,"
227           " triggering resolve", hl->host, ping_max_missed);
228
229       /* we trigger the resolv simply be removeing and adding the host to our
230        * ping object */
231       status = ping_host_remove (pingobj, hl->host);
232       if (status != 0)
233       {
234         WARNING ("ping plugin: ping_host_remove (%s) failed.", hl->host);
235       }
236       else
237       {
238         status = ping_host_add (pingobj, hl->host);
239         if (status != 0)
240           ERROR ("ping plugin: ping_host_add (%s) failed.", hl->host);
241       }
242     } /* }}} ping_max_missed */
243   } /* }}} for (iter) */
244
245   return (0);
246 } /* }}} int ping_dispatch_all */
247
248 static void *ping_thread (void *arg) /* {{{ */
249 {
250   pingobj_t *pingobj = NULL;
251
252   struct timeval  tv_begin;
253   struct timeval  tv_end;
254   struct timespec ts_wait;
255   struct timespec ts_int;
256
257   int count;
258
259   c_complain_t complaint = C_COMPLAIN_INIT_STATIC;
260
261   pthread_mutex_lock (&ping_lock);
262
263   pingobj = ping_construct ();
264   if (pingobj == NULL)
265   {
266     ERROR ("ping plugin: ping_construct failed.");
267     ping_thread_error = 1;
268     pthread_mutex_unlock (&ping_lock);
269     return ((void *) -1);
270   }
271
272   if (ping_source != NULL)
273     if (ping_setopt (pingobj, PING_OPT_SOURCE, (void *) ping_source) != 0)
274       ERROR ("ping plugin: Failed to set source address: %s",
275           ping_get_error (pingobj));
276
277 #ifdef HAVE_OPING_1_3
278   if (ping_device != NULL)
279     if (ping_setopt (pingobj, PING_OPT_DEVICE, (void *) ping_device) != 0)
280       ERROR ("ping plugin: Failed to set device: %s",
281           ping_get_error (pingobj));
282 #endif
283
284   ping_setopt (pingobj, PING_OPT_TIMEOUT, (void *) &ping_timeout);
285   ping_setopt (pingobj, PING_OPT_TTL, (void *) &ping_ttl);
286
287   if (ping_data != NULL)
288     ping_setopt (pingobj, PING_OPT_DATA, (void *) ping_data);
289
290   /* Add all the hosts to the ping object. */
291   count = 0;
292   for (hostlist_t *hl = hostlist_head; hl != NULL; hl = hl->next)
293   {
294     int tmp_status;
295     tmp_status = ping_host_add (pingobj, hl->host);
296     if (tmp_status != 0)
297       WARNING ("ping plugin: ping_host_add (%s) failed: %s",
298           hl->host, ping_get_error (pingobj));
299     else
300       count++;
301   }
302
303   if (count == 0)
304   {
305     ERROR ("ping plugin: No host could be added to ping object. Giving up.");
306     ping_thread_error = 1;
307     pthread_mutex_unlock (&ping_lock);
308     return ((void *) -1);
309   }
310
311   /* Set up `ts_int' */
312   {
313     double temp_sec;
314     double temp_nsec;
315
316     temp_nsec = modf (ping_interval, &temp_sec);
317     ts_int.tv_sec  = (time_t) temp_sec;
318     ts_int.tv_nsec = (long) (temp_nsec * 1000000000L);
319   }
320
321   while (ping_thread_loop > 0)
322   {
323     int status;
324     _Bool send_successful = 0;
325
326     if (gettimeofday (&tv_begin, NULL) < 0)
327     {
328       char errbuf[1024];
329       ERROR ("ping plugin: gettimeofday failed: %s",
330           sstrerror (errno, errbuf, sizeof (errbuf)));
331       ping_thread_error = 1;
332       break;
333     }
334
335     pthread_mutex_unlock (&ping_lock);
336
337     status = ping_send (pingobj);
338     if (status < 0)
339     {
340       c_complain (LOG_ERR, &complaint, "ping plugin: ping_send failed: %s",
341           ping_get_error (pingobj));
342     }
343     else
344     {
345       c_release (LOG_NOTICE, &complaint, "ping plugin: ping_send succeeded.");
346       send_successful = 1;
347     }
348
349     pthread_mutex_lock (&ping_lock);
350
351     if (ping_thread_loop <= 0)
352       break;
353
354     if (send_successful)
355       (void) ping_dispatch_all (pingobj);
356
357     if (gettimeofday (&tv_end, NULL) < 0)
358     {
359       char errbuf[1024];
360       ERROR ("ping plugin: gettimeofday failed: %s",
361           sstrerror (errno, errbuf, sizeof (errbuf)));
362       ping_thread_error = 1;
363       break;
364     }
365
366     /* Calculate the absolute time until which to wait and store it in
367      * `ts_wait'. */
368     time_calc (&ts_wait, &ts_int, &tv_begin, &tv_end);
369
370     pthread_cond_timedwait (&ping_cond, &ping_lock, &ts_wait);
371     if (ping_thread_loop <= 0)
372       break;
373   } /* while (ping_thread_loop > 0) */
374
375   pthread_mutex_unlock (&ping_lock);
376   ping_destroy (pingobj);
377
378   return ((void *) 0);
379 } /* }}} void *ping_thread */
380
381 static int start_thread (void) /* {{{ */
382 {
383   int status;
384
385   pthread_mutex_lock (&ping_lock);
386
387   if (ping_thread_loop != 0)
388   {
389     pthread_mutex_unlock (&ping_lock);
390     return (0);
391   }
392
393   ping_thread_loop = 1;
394   ping_thread_error = 0;
395   status = plugin_thread_create (&ping_thread_id, /* attr = */ NULL,
396       ping_thread, /* arg = */ (void *) 0);
397   if (status != 0)
398   {
399     ping_thread_loop = 0;
400     ERROR ("ping plugin: Starting thread failed.");
401     pthread_mutex_unlock (&ping_lock);
402     return (-1);
403   }
404
405   pthread_mutex_unlock (&ping_lock);
406   return (0);
407 } /* }}} int start_thread */
408
409 static int stop_thread (void) /* {{{ */
410 {
411   int status;
412
413   pthread_mutex_lock (&ping_lock);
414
415   if (ping_thread_loop == 0)
416   {
417     pthread_mutex_unlock (&ping_lock);
418     return (-1);
419   }
420
421   ping_thread_loop = 0;
422   pthread_cond_broadcast (&ping_cond);
423   pthread_mutex_unlock (&ping_lock);
424
425   status = pthread_join (ping_thread_id, /* return = */ NULL);
426   if (status != 0)
427   {
428     ERROR ("ping plugin: Stopping thread failed.");
429     status = -1;
430   }
431
432   pthread_mutex_lock (&ping_lock);
433   memset (&ping_thread_id, 0, sizeof (ping_thread_id));
434   ping_thread_error = 0;
435   pthread_mutex_unlock (&ping_lock);
436
437   return (status);
438 } /* }}} int stop_thread */
439
440 static int ping_init (void) /* {{{ */
441 {
442   if (hostlist_head == NULL)
443   {
444     NOTICE ("ping plugin: No hosts have been configured.");
445     return (-1);
446   }
447
448   if (ping_timeout > ping_interval)
449   {
450     ping_timeout = 0.9 * ping_interval;
451     WARNING ("ping plugin: Timeout is greater than interval. "
452         "Will use a timeout of %gs.", ping_timeout);
453   }
454
455 #if defined(HAVE_SYS_CAPABILITY_H) && defined(CAP_NET_RAW)
456   if (check_capability (CAP_NET_RAW) != 0)
457   {
458     if (getuid () == 0)
459       WARNING ("ping plugin: Running collectd as root, but the CAP_NET_RAW "
460           "capability is missing. The plugin's read function will probably "
461           "fail. Is your init system dropping capabilities?");
462     else
463       WARNING ("ping plugin: collectd doesn't have the CAP_NET_RAW capability. "
464           "If you don't want to run collectd as root, try running \"setcap "
465           "cap_net_raw=ep\" on the collectd binary.");
466   }
467 #endif
468
469   return (start_thread ());
470 } /* }}} int ping_init */
471
472 static int config_set_string (const char *name, /* {{{ */
473     char **var, const char *value)
474 {
475   char *tmp;
476
477   tmp = strdup (value);
478   if (tmp == NULL)
479   {
480     char errbuf[1024];
481     ERROR ("ping plugin: Setting `%s' to `%s' failed: strdup failed: %s",
482         name, value, sstrerror (errno, errbuf, sizeof (errbuf)));
483     return (1);
484   }
485
486   if (*var != NULL)
487     free (*var);
488   *var = tmp;
489   return (0);
490 } /* }}} int config_set_string */
491
492 static int ping_config (const char *key, const char *value) /* {{{ */
493 {
494   if (strcasecmp (key, "Host") == 0)
495   {
496     hostlist_t *hl;
497     char *host;
498
499     hl = malloc (sizeof (*hl));
500     if (hl == NULL)
501     {
502       char errbuf[1024];
503       ERROR ("ping plugin: malloc failed: %s",
504           sstrerror (errno, errbuf, sizeof (errbuf)));
505       return (1);
506     }
507
508     host = strdup (value);
509     if (host == NULL)
510     {
511       char errbuf[1024];
512       sfree (hl);
513       ERROR ("ping plugin: strdup failed: %s",
514           sstrerror (errno, errbuf, sizeof (errbuf)));
515       return (1);
516     }
517
518     hl->host = host;
519     hl->pkg_sent = 0;
520     hl->pkg_recv = 0;
521     hl->pkg_missed = 0;
522     hl->latency_total = 0.0;
523     hl->latency_squared = 0.0;
524     hl->next = hostlist_head;
525     hostlist_head = hl;
526   }
527   else if (strcasecmp (key, "SourceAddress") == 0)
528   {
529     int status = config_set_string (key, &ping_source, value);
530     if (status != 0)
531       return (status);
532   }
533 #ifdef HAVE_OPING_1_3
534   else if (strcasecmp (key, "Device") == 0)
535   {
536     int status = config_set_string (key, &ping_device, value);
537     if (status != 0)
538       return (status);
539   }
540 #endif
541   else if (strcasecmp (key, "TTL") == 0)
542   {
543     int ttl = atoi (value);
544     if ((ttl > 0) && (ttl <= 255))
545       ping_ttl = ttl;
546     else
547       WARNING ("ping plugin: Ignoring invalid TTL %i.", ttl);
548   }
549   else if (strcasecmp (key, "Interval") == 0)
550   {
551     double tmp;
552
553     tmp = atof (value);
554     if (tmp > 0.0)
555       ping_interval = tmp;
556     else
557       WARNING ("ping plugin: Ignoring invalid interval %g (%s)",
558           tmp, value);
559   }
560   else if (strcasecmp (key, "Size") == 0) {
561     size_t size = (size_t) atoi (value);
562
563     /* Max IP packet size - (IPv6 + ICMP) = 65535 - (40 + 8) = 65487 */
564     if (size <= 65487)
565     {
566       sfree (ping_data);
567       ping_data = malloc (size + 1);
568       if (ping_data == NULL)
569       {
570         ERROR ("ping plugin: malloc failed.");
571         return (1);
572       }
573
574       /* Note: By default oping is using constant string
575        * "liboping -- ICMP ping library <http://octo.it/liboping/>"
576        * which is exactly 56 bytes.
577        *
578        * Optimally we would follow the ping(1) behaviour, but we
579        * cannot use byte 00 or start data payload at exactly same
580        * location, due to oping library limitations. */
581       for (size_t i = 0; i < size; i++) /* {{{ */
582       {
583         /* This restricts data pattern to be only composed of easily
584          * printable characters, and not NUL character. */
585         ping_data[i] = ('0' + i % 64);
586       }  /* }}} for (i = 0; i < size; i++) */
587       ping_data[size] = 0;
588     } else
589       WARNING ("ping plugin: Ignoring invalid Size %zu.", size);
590   }
591   else if (strcasecmp (key, "Timeout") == 0)
592   {
593     double tmp;
594
595     tmp = atof (value);
596     if (tmp > 0.0)
597       ping_timeout = tmp;
598     else
599       WARNING ("ping plugin: Ignoring invalid timeout %g (%s)",
600           tmp, value);
601   }
602   else if (strcasecmp (key, "MaxMissed") == 0)
603   {
604     ping_max_missed = atoi (value);
605     if (ping_max_missed < 0)
606       INFO ("ping plugin: MaxMissed < 0, disabled re-resolving of hosts");
607   }
608   else
609   {
610     return (-1);
611   }
612
613   return (0);
614 } /* }}} int ping_config */
615
616 static void submit (const char *host, const char *type, /* {{{ */
617     gauge_t value)
618 {
619   value_t values[1];
620   value_list_t vl = VALUE_LIST_INIT;
621
622   values[0].gauge = value;
623
624   vl.values = values;
625   vl.values_len = 1;
626   sstrncpy (vl.host, hostname_g, sizeof (vl.host));
627   sstrncpy (vl.plugin, "ping", sizeof (vl.plugin));
628   sstrncpy (vl.plugin_instance, "", sizeof (vl.plugin_instance));
629   sstrncpy (vl.type_instance, host, sizeof (vl.type_instance));
630   sstrncpy (vl.type, type, sizeof (vl.type));
631
632   plugin_dispatch_values (&vl);
633 } /* }}} void ping_submit */
634
635 static int ping_read (void) /* {{{ */
636 {
637   if (ping_thread_error != 0)
638   {
639     ERROR ("ping plugin: The ping thread had a problem. Restarting it.");
640
641     stop_thread ();
642
643     for (hostlist_t *hl = hostlist_head; hl != NULL; hl = hl->next)
644     {
645       hl->pkg_sent = 0;
646       hl->pkg_recv = 0;
647       hl->latency_total = 0.0;
648       hl->latency_squared = 0.0;
649     }
650
651     start_thread ();
652
653     return (-1);
654   } /* if (ping_thread_error != 0) */
655
656   for (hostlist_t *hl = hostlist_head; hl != NULL; hl = hl->next) /* {{{ */
657   {
658     uint32_t pkg_sent;
659     uint32_t pkg_recv;
660     double latency_total;
661     double latency_squared;
662
663     double latency_average;
664     double latency_stddev;
665
666     double droprate;
667
668     /* Locking here works, because the structure of the linked list is only
669      * changed during configure and shutdown. */
670     pthread_mutex_lock (&ping_lock);
671
672     pkg_sent = hl->pkg_sent;
673     pkg_recv = hl->pkg_recv;
674     latency_total = hl->latency_total;
675     latency_squared = hl->latency_squared;
676
677     hl->pkg_sent = 0;
678     hl->pkg_recv = 0;
679     hl->latency_total = 0.0;
680     hl->latency_squared = 0.0;
681
682     pthread_mutex_unlock (&ping_lock);
683
684     /* This e. g. happens when starting up. */
685     if (pkg_sent == 0)
686     {
687       DEBUG ("ping plugin: No packages for host %s have been sent.",
688           hl->host);
689       continue;
690     }
691
692     /* Calculate average. Beware of division by zero. */
693     if (pkg_recv == 0)
694       latency_average = NAN;
695     else
696       latency_average = latency_total / ((double) pkg_recv);
697
698     /* Calculate standard deviation. Beware even more of division by zero. */
699     if (pkg_recv == 0)
700       latency_stddev = NAN;
701     else if (pkg_recv == 1)
702       latency_stddev = 0.0;
703     else
704       latency_stddev = sqrt (((((double) pkg_recv) * latency_squared)
705           - (latency_total * latency_total))
706           / ((double) (pkg_recv * (pkg_recv - 1))));
707
708     /* Calculate drop rate. */
709     droprate = ((double) (pkg_sent - pkg_recv)) / ((double) pkg_sent);
710
711     submit (hl->host, "ping", latency_average);
712     submit (hl->host, "ping_stddev", latency_stddev);
713     submit (hl->host, "ping_droprate", droprate);
714   } /* }}} for (hl = hostlist_head; hl != NULL; hl = hl->next) */
715
716   return (0);
717 } /* }}} int ping_read */
718
719 static int ping_shutdown (void) /* {{{ */
720 {
721   hostlist_t *hl;
722
723   INFO ("ping plugin: Shutting down thread.");
724   if (stop_thread () < 0)
725     return (-1);
726
727   hl = hostlist_head;
728   while (hl != NULL)
729   {
730     hostlist_t *hl_next;
731
732     hl_next = hl->next;
733
734     sfree (hl->host);
735     sfree (hl);
736
737     hl = hl_next;
738   }
739
740   if (ping_data != NULL) {
741     free (ping_data);
742     ping_data = NULL;
743   }
744
745   return (0);
746 } /* }}} int ping_shutdown */
747
748 void module_register (void)
749 {
750   plugin_register_config ("ping", ping_config,
751       config_keys, config_keys_num);
752   plugin_register_init ("ping", ping_init);
753   plugin_register_read ("ping", ping_read);
754   plugin_register_shutdown ("ping", ping_shutdown);
755 } /* void module_register */
756
757 /* vim: set sw=2 sts=2 et fdm=marker : */