1 /* ex: set tabstop=2 expandtab shiftwidth=2 softtabstop=2: */
2 /*
3 *
4 * Copyright (c) 2003 The Regents of the University of California. All
5 * rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * - Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 *
14 * - Neither the name of the University nor the names of its
15 * contributors may be used to endorse or promote products derived
16 * from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS''
19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
22 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
26 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 *
30 */
31
32
33 /*
34 * ping.c: This a "ping" client, which broadcasts a 'ping' packet, and
35 * then waits for replies from the network. This program, and the
36 * corresponding 'pingd' server, serve as a useful example for various
37 * tasks:
38 *
39 * - How to create an application-layer protocol format
40 * - How to send packets to the network
41 * - How to wait for and react to packets that come from the network
42 * - How to filter incoming packets
43 *
44 * $Id: dsr_ping.c,v 1.1 2007-08-13 02:16:22 allenm Exp $
45 */
46
47 char ping_c_cvsid[] = "$Id: dsr_ping.c,v 1.1 2007-08-13 02:16:22 allenm Exp $";
48
49 #include <stdio.h>
50 #include <stdlib.h>
51 #include <sys/time.h>
52 #include <arpa/inet.h>
53
54 #include "emrun/emrun.h"
55 #include "link/link.h"
56 #include "ping.h"
57
58 #ifndef SECOND
59 #define SECOND 1000
60 #endif
61 #define DEFAULT_STARTUP_DELAY (20 * SECOND)
62
63
64 /*******************************************************************/
65
66
67 #define MAX_PINGS 1000
68
69
70
71 typedef struct _wake_status {
72 int64_t cmd_sent_timestamp;
73 int64_t path_est_timestamp;
74 int valid;
75 int ping_rcvd;
76 int sleep_state;
77 } wake_status_t;
78
79
80 typedef struct ping_state {
81 int random_id; /* our randomly selected ID, to
82 * differentiate our packets from any
83 * other ping processes that might be
84 * happening at the same time. */
85 int last_seqno; /* last seqno we used */
86 int last_seqno_rcv; /* last seqno received (used to detect
87 * duplicates when using hbh and acks are
88 * lost) */
89 if_id_t last_replier; /* src address of last reply */
90 int total_sent; /* total pings sent */
91 struct timeval time_sent; /* time we sent the most recent ping */
92 struct timeval rtt[MAX_PINGS];/* rtts of responses we've received */
93 int num_replies; /* number of responses we've received */
94 int request_receipts; /* command line arg to request receipts */
95 lu_context_t *link; /* link we're using to ping on */
96 if_id_t dest; /* node id destination of our ping */
97 uint32_t period;
98 uint32_t max_pings;
99 int logging;
100
101 status_context_t *dsrping_status_ev;
102 status_client_context_t *wakestat_ref;
103 wake_status_t wakestatus;
104 } ping_state_t;
105
106
107
108
109
110 int64_t get_time();
111 void record_command(char *type, uint16_t seq, ping_state_t *p);
112 void dsrping_init_status(ping_state_t *p);
113 int dsrping_status_binary(status_context_t *info, buf_t *buf);
114 int dsrping_status_printable(status_context_t *info, buf_t *buf);
115 //int dsrping_status_write(status_context_t *info, char *command,
116 // size_t buf_size);
117 //void mote_wakestat_open(ping_state_t *p);
118 //int handle_wakestat_input(void *new_buffer, size_t size, void *data);
119
120
121 #define DSRPING_STATUS_DEV_NAME ("/dev/dsr/pingstatus")
122 //#define MOTE_WAKE_DEV_NAME ("/dev/herd/wakestat")
123
124
125 /*
126 int handle_wakestat_input(void *new_buffer, size_t size, void *data)
127 {
128 free(new_buffer);
129 return EVENT_RENEW;
130 }
131 */
132 /*
133 void mote_wakestat_open(ping_state_t *p)
134 {
135 status_client_opts_t opts = {
136 devname: sim_path(MOTE_WAKE_DEV_NAME),
137 private_data: p,
138 handler: handle_wakestat_input,
139 read_as_ascii: 0,
140 silent: 1,
141 };
142
143 if (g_status_client_open(opts.devname, opts.handler, p,
144 &p->wakestat_ref) < 0) {
145 elog(LOG_ERR, "Can't open status device %s: %m",
146 opts.devname);
147 exit(1);
148 }
149 }
150 */
151
152 void dsrping_init_status(ping_state_t *p)
153 {
154 status_dev_opts_t s1_opts = {
155 device: {
156 device_info: p
157 },
158 printable: dsrping_status_printable,
159 binary: dsrping_status_binary,
160 // write: dsrping_status_write,
161 };
162
163 s1_opts.device.devname = sim_path(DSRPING_STATUS_DEV_NAME);
164
165 if (g_status_dev(&s1_opts, &p->dsrping_status_ev) < 0) {
166 elog(LOG_CRIT, "Can't create status dev %s: %m",
167 s1_opts.device.devname);
168 exit(1);
169 }
170
171 }
172
173
174 int dsrping_status_binary(status_context_t *info, buf_t *buf)
175 {
176 return STATUS_MSG_COMPLETE;
177 }
178
179
180 int dsrping_status_printable(status_context_t *info, buf_t *buf)
181 {
182 ping_state_t *p = (ping_state_t *)sd_data(info);
183 wake_status_t *ws = &p->wakestatus;
184
185 bufprintf(buf, "Valid = %d\n", ws->valid);
186 bufprintf(buf, "Start time = %lld\n", ws->cmd_sent_timestamp);
187 bufprintf(buf, "End time = %lld\n", ws->path_est_timestamp);
188 bufprintf(buf, "Reply rcvd = %d\n", ws->ping_rcvd);
189
190 return STATUS_MSG_COMPLETE;
191 }
192
193 /*
194 int dsrping_status_write(status_context_t *info, char *command,
195 size_t buf_size)
196 {
197 ping_state_t *p = (ping_state_t *)sd_data(info);
198 wake_status_t *ws = &p->wakestatus;
199
200
201 if (strncasecmp(command, "wakeup", strlen("wakeup")) == 0) {
202 buf_t *cmd = buf_new();
203
204 // reset wake status
205 memset(ws, 0, sizeof(wake_status_t));
206 // set valid to 1
207 ws->valid = 1;
208
209 // create command
210 bufprintf(cmd, "go");
211
212 // send command
213 if (status_client_write(sim_path(MOTE_WAKE_DEV_NAME),
214 cmd, STATUS_M_ASCII) < 0) {
215 elog(LOG_ERR, "Can't send command to status device: %m");
216 exit(1);
217 }
218
219 buf_free(cmd);
220 // timestamp start time
221 ws->cmd_sent_timestamp = get_time();
222 g_status_dev_notify(p->dsrping_status_ev);
223 } else {
224 elog(LOG_ERR, "Got unrecognized command %s", command);
225 }
226
227 return STATUS_WRITE_DONE;
228 }
229 */
230
231
232 /*******************************************************************/
233
234 /*
235 * Callback activated when we are asked to shut down by emrun.
236 * Compute and print summary statistics of all replies received
237 */
238 void ping_shutdown(void *data)
239 {
240 ping_state_t *p = (ping_state_t *) data;
241 struct timeval min, max, sum;
242 int i;
243
244 if (!p->num_replies) {
245 elog_g(LOG_NOTICE, "%d pings sent, no replies received", p->total_sent);
246 goto done;
247 }
248
249 min = max = sum = p->rtt[0];
250
251 /* This loop finds the min and max RTTs, sums all the RTTs for
252 * average, and counts how many replies we received. */
253 for (i = 1; i < p->num_replies; i++) {
254
255 /* misc_tv_* are convenience functions for doing computations on a
256 * struct timeval found in libmisc */
257 if (misc_tv_offset_neg(&p->rtt[i], &min) < 0)
258 min = p->rtt[i];
259 if (misc_tv_offset_neg(&p->rtt[i], &max) > 0)
260 max = p->rtt[i];
261
262 misc_tv_add(&sum, &p->rtt[i]);
263 }
264
265 /* compute average from the sum */
266 elog_g(LOG_NOTICE, "%d pings sent, %d replies received, "
267 "min/avg/max rtt %.1f/%.1f/%.1f ms",
268 p->total_sent, p->num_replies,
269 misc_tv_msec_f(&min),
270
271 ((float) sum.tv_sec + (sum.tv_usec / MILLION_F)) * 1000.0 /
272 p->num_replies,
273
274 misc_tv_msec_f(&max));
275
276 /* Now shut down */
277 done:
278 exit(0);
279 }
280
281
282 /*
283 * Construct a ping packet, and send it out over the wire, using the
284 * link context stored as part of our "ping" state.
285 */
286 void ping_send(ping_state_t *p)
287 {
288 /* Allocate a small buffer for the packet */
289 char buf[200];
290 link_pkt_t *pkt = (link_pkt_t *) buf;
291 ping_pkt_t *ping_pkt = (ping_pkt_t *) pkt->data;
292 int this_seq = p->last_seqno + 1;
293
294 /* initialize the outer header (the link_pkt header) */
295 memset(buf, 0, sizeof(buf));
296 pkt->src.id = my_node_id;
297 pkt->dst.id = p->dest;
298 pkt->type = PKT_TYPE_PING;
299
300 /* and our application-layer (ping) protocol data */
301 ping_pkt->cmd = PING_REQUEST;
302 ping_pkt->random_id = p->random_id;
303 ping_pkt->seqno = this_seq;
304 ping_pkt->node_id = my_node_id;
305
306 /* request a receipt.. */
307 /* if you request a receipt, you will receive back a MAC_CTRL message that tells
308 * what happened to your packet. */
309 if (p->request_receipts)
310 link_receipt_request(pkt, NULL);
311
312 if (p->link==NULL) {
313 elog(LOG_ERR, "Can't send pkt on a NULL link!");
314 exit(1);
315 }
316
317 /* now launch the packet! */
318 if (lu_send(p->link, pkt, sizeof(ping_pkt_t)) < 0) {
319 if (errno != EAGAIN)
320 elog(LOG_ERR, "can't send on %s: %m", lu_name(p->link, NULL));
321 }
322 else {
323 elog(LOG_NOTICE, "send ping seqno %d to dest %s", ping_pkt->seqno,
324 print_if_id(pkt->dst.id));
325 elog(LOG_DEBUG(5), "command: %d, random id: %d, data len: %d",
326 ping_pkt->cmd, ping_pkt->random_id, sizeof(ping_pkt_t));
327
328 record_command("CMD_PING_REQ_SENT", ping_pkt->seqno, p);
329
330 /* remember what time it is, so we can calculate RTT */
331 gettimeofday(&p->time_sent, NULL);
332 p->last_seqno = this_seq;
333 p->total_sent++;
334 }
335 }
336
337
338 /* Called every time our periodic ping timer expies. */
339 int ping_periodic_timer(void *data, int interval, g_event_t *ev)
340 {
341 ping_state_t *p = (ping_state_t *) data;
342
343 ping_send(p);
344
345 return EVENT_RENEW;
346 }
347
348
349
350 int startup_timer(void *data, int interval, g_event_t *ev)
351 {
352 ping_state_t *p = (ping_state_t *)data;
353 /* Send an initial ping */
354 ping_send(p);
355
356 /* And set a timer to send one every 'period' miliseconds */
357 g_timer_add(p->period, ping_periodic_timer, p, NULL, NULL);
358
359 return TIMER_DONE;
360 }
361
362
363 /*
364 * This callback is called whenever a packet arrives on the link we
365 * opened. "pkt" is a pointer to a link_pkt_t header followed by
366 * data_len bytes of data. (data_len is the length of the data
367 * following the header; it doesn't include the size of the link_pkt_t
368 * header itself.) "link" is a pointer to the link that the packet
369 * was received on, and can be used for sending a reply packet (we
370 * don't send reply packets from the receiver callback here, but see
371 * pingd for an example where we do.)
372 */
373 int ping_receiver(lu_context_t *link, link_pkt_t *link_pkt, ssize_t data_len)
374 {
375 /* flag if duplicate */
376 int duplicate=0;
377
378 /* Get a pointer to the data portion of the packet */
379 ping_pkt_t *ping_pkt = (ping_pkt_t *) link_pkt->data;
380
381 /* We stored a pointer to the 'ping_state' struct as the "data"
382 * field of link_opts in main(). Now, we retrieve that pointer. */
383 ping_state_t *p = (ping_state_t *) lu_data(link);
384
385 /*
386 * Make sure the packet has the right packet type. Since we
387 * specified the desired packet type when we registered to receive
388 * packets, this test should never fail.
389 */
390 if (link_pkt->type != PKT_TYPE_PING) {
391 if ((link_pkt->type != PKT_TYPE_MAC_CTRL)
392 || (link_pkt->ext_type != MAC_CTRL_RECEIPT)) {
393 elog(LOG_DEBUG(0), "got a packet not meant for us - filter failed!");
394 goto done;
395 }
396 }
397
398 if ((link_pkt->type == PKT_TYPE_MAC_CTRL) &&
399 (link_pkt->ext_type == MAC_CTRL_RECEIPT)) {
400
401 if (link_pkt->retval != 0) {
402 elog(LOG_ERR, "Ping to destination %d failed: %d",
403 link_pkt->dst.id, link_pkt->retval);
404
405 }
406 goto done;
407 }
408
409
410
411 /*
412 * Make sure the data portion of the packet is exactly the right
413 * size (i.e., the size of our "ping" application-layer protocol
414 * frame.
415 */
416 if (data_len != sizeof(ping_pkt_t)) {
417 elog(LOG_ERR, "got a short ping packet (%d bytes)!", data_len);
418 goto done;
419 }
420
421 /* We now know that the data pointed to by ping_pkt is valid */
422
423 /*
424 * Ignore everything but ping replies (i.e. ignore requests that
425 * other ping clients might be sending.)
426 */
427 if (ping_pkt->cmd != PING_REPLY) {
428 elog(LOG_DEBUG(2), "got non-reply packet - dropping");
429 goto done;
430 }
431
432 /*
433 * Ignore replies not meant for us (based on the random ID), or that
434 * are arriving late for the previous request
435 */
436 if (ping_pkt->random_id != p->random_id ||
437 ping_pkt->seqno != p->last_seqno) {
438 elog(LOG_DEBUG(2), "dropping old or not-for-us reply packet");
439 goto done;
440 }
441
442 /* Flag (some) duplicates that arrive due to the lost of an ack, when using
443 * a hop by hop reliable mechanism like hbh. Will usually work using
444 * unicast pings, will sometimes work with broadcasts.
445 *
446 * NOTE: when using multihop routing *and* if the links in the path
447 * are not very reliable, it would be possible that a later ping reply
448 * could get to us before a previous one (out of order delivery).
449 * The chances of out of order delivery increase with smaller ping
450 * intervals and with a low reliability network. If this is the case,
451 * you may want to maintain not just the last packet recieved but a
452 * list (buffer) of the last X packet received, such that you can fill
453 * the holes when you have out of order delivery. The current simple
454 * implementation detects duplicates but it does not deal with out of
455 * order delivery.
456 */
457 if (ping_pkt->seqno < p->last_seqno_rcv ||
458 (ping_pkt->seqno == p->last_seqno_rcv &&
459 link_pkt->src.id == p->last_replier)) {
460 duplicate = 1;
461 } else { /* update state */
462 p->last_seqno_rcv = ping_pkt->seqno;
463 p->last_replier = link_pkt->src.id;
464 }
465
466 /*
467 * compute the roundtrip time, by subtracting the time the reply
468 * arrived from the time the original ping was sent.
469 */
470 misc_tv_sub(&link_pkt->rcv_time, &p->time_sent);
471 {
472 int offset = p->num_replies % MAX_PINGS;
473 p->rtt[offset] = link_pkt->rcv_time;
474 }
475 p->num_replies++;
476
477 record_command("CMD_PING_REPLY_RCVD", ping_pkt->seqno, p);
478
479 // if wakestatus is valid and we haven't received a ping yet
480 if (p->wakestatus.valid==1 && p->wakestatus.ping_rcvd==0) {
481 // set ping_rcvd to one and timestamp the event
482 p->wakestatus.ping_rcvd = 1;
483 p->wakestatus.path_est_timestamp = get_time();
484 // now notify status dev
485 g_status_dev_notify(p->dsrping_status_ev);
486 }
487
488 elog(LOG_NOTICE, "got reply %d from node %d, iface %s, rtt %.2f ms, %d hops away %s",
489 p->num_replies,
490 ping_pkt->node_id,
491 print_if_id(link_pkt->src.id),
492 misc_tv_msec_f(&link_pkt->rcv_time),
493 PING_MAX_HOPS - link_pkt->max_hops,
494 duplicate ? "(DUP!)" : "");
495
496 if ((p->max_pings > 0) && (p->num_replies >= p->max_pings)) {
497 elog(LOG_ALERT, "max pings exceeded; shutting down");
498 ping_shutdown(p);
499 }
500
501 done:
502 /* note, packet must be freed! */
503 free(link_pkt);
504 return EVENT_RENEW;
505 }
506
507
508 void usage(char *name)
509 {
510 misc_print_usage
511 (name, "-U <device> [-f] [-i <interval>] [-p <pot value>] [-d <destination>]",
512 " --uses <device>: Specify device to use\n"
513 " --flood: send pings as fast as possible\n"
514 " --interval: send pings as specified interval (ms)\n"
515 " --pot <value>: TX potentiomenter value\n"
516 " --dest <value>: node id destination of the pings\n"
517 " --request_receipts: turn on receipt requests\n"
518 );
519 exit(1);
520 }
521
522
523 int main(int argc, char *argv[])
524 {
525 ping_state_t ping_state;
526 uint period = 1000;
527 uint startup_delay = DEFAULT_STARTUP_DELAY;
528 int pot = 0;
529 char *uses = NULL;
530
531 emrun_opts_t emrun_opts = {
532 shutdown: ping_shutdown,
533 data: (void *) &ping_state
534 };
535
536 /* Generic initialization common to most software */
537 misc_init(&argc, argv, CVSTAG);
538
539 /* clean up our state variable */
540 memset(&ping_state, 0, sizeof(ping_state));
541
542 /* get the --uses arg */
543 uses = link_parse_uses(&argc, argv, NULL);
544 if (uses == NULL) {
545 elog(LOG_CRIT, "Please specify a link to use!");
546 usage(argv[0]);
547 }
548
549 /* ping flood option */
550 if (misc_parse_out_switch(&argc, argv, "flood", 'f'))
551 period = 10;
552
553 /* ping interval option */
554 misc_parse_option_as_uint(&argc, argv, "interval", 'i', &period);
555 ping_state.period = period;
556
557 misc_parse_option_as_uint(&argc, argv, "delay", 'l', &startup_delay);
558
559 /* ping pot option */
560 misc_parse_option_as_int(&argc, argv, "pot", 'p', &pot);
561
562 if (misc_parse_out_switch(&argc, argv, "enable-log", 0)) {
563 ping_state.logging=1;
564 } else {
565 ping_state.logging=0;
566 }
567
568 /* request receipt option */
569 ping_state.request_receipts = 1;
570 //ping_state.request_receipts = misc_parse_out_switch(&argc, argv, "request_receipts", 0);
571
572 /* set default destination to broadcast */
573 ping_state.dest = LINK_BROADCAST;
574
575 /* ping destination option */
576 char *dest_str = misc_parse_out_option(&argc, argv, "dest", 'd');
577 if (dest_str && parse_if_id(dest_str, &(ping_state.dest)) < 0) {
578 elog(LOG_CRIT, "invalid destination address: '%s'.. use 0xXX, DD, or dotted-quad",
579 dest_str);
580 exit(1);
581 }
582
583 if (ping_state.dest==0) {
584 // 0 means don't send anything, so we just exit
585 elog(LOG_NOTICE, "Got destination 0, exiting");
586 exit(1);
587 }
588
589 /* add'l args? */
590 if (misc_args_remain(&argc, argv)) {
591 elog(LOG_CRIT, "Additional unparsed arguments!");
592 usage(argv[0]);
593 }
594
595 /*
596 * Pick our random ID (a 14 bit number, from the definition of the
597 * ping packet in ping.h
598 */
599 ping_state.random_id = random_range(1, (1 << 13));
600
601 /*
602 * Open the link-layer device. The options struct tells it we want
603 * the ping_receiver function to be called every time a packet of
604 * type PKT_TYPE_PING arrives.
605 *
606 * If the link-opening succeeds, the link struct is returned to us
607 * using the 2nd argument (i.e., written to ping_state.link)
608 */
609 {
610 lu_opts_t lu_opts = {
611 opts: {
612 name: uses,
613 data: &ping_state,
614 // filter removed to allow for DST_UNREACHABLE replies too!
615 // pkt_type: PKT_TYPE_PING /* only give us ping-type packets */
616 },
617 receive: ping_receiver /* call this func when packets arrive */
618 };
619
620 if (lu_open(&lu_opts, &ping_state.link) < 0) {
621 elog(LOG_CRIT, "can't open %s: %m", link_name(&lu_opts.opts, NULL));
622 exit(1);
623 }
624
625 /* Try to set the potentiometer of the radio to 'pot' */
626 // ONLY if the pot has in fact been set...no more 70ies
627 if (pot > 0) {
628 if (lu_ioctl(ping_state.link, LINK_SET_POT, &pot) < 0)
629 elog(LOG_CRIT, "warning: can't set potentiometer: %m");
630 }
631
632 elog_g(LOG_NOTICE, "running, using %s", lu_name(ping_state.link, NULL));
633 }
634
635 g_timer_add(startup_delay, startup_timer, &ping_state, NULL, NULL);
636
637 #if 0
638 /* Send an initial ping */
639 ping_send(&ping_state);
640
641 /* And set a timer to send one every 'period' miliseconds */
642 g_timer_add(period, ping_periodic_timer, &ping_state, NULL, NULL);
643 #endif
644
645 /*
646 * Start the event loop running - it should never exit (the shutdown
647 * handler is called when the program is supposed to stop)
648 */
649
650
651 // init status dev
652 // mote_wakestat_open(&ping_state);
653 dsrping_init_status(&ping_state);
654
655 emrun_init(&emrun_opts); /* this should be the last initialization */
656 g_main();
657 elog_g(LOG_CRIT, "event loop terminated abnormally!");
658 return 0;
659 }
660
661
662 int64_t get_time()
663 {
664 int64_t time_now=0;
665 struct timeval now={0};
666
667 gettimeofday(&now, NULL);
668 time_now = misc_timeval_to_int64(&now);
669
670 return time_now;
671 }
672
673
674 void record_command(char *type, uint16_t seq, ping_state_t *p)
675 {
676 #if 0
677 FILE *logfile = NULL;
678 int64_t time_now=0;
679 struct timeval now={0};
680
681 if (p->logging == 0) {
682 // logging is OFF, return
683 return;
684 }
685
686 logfile = fopen("/scratch/latlog", "a");
687 if (logfile==NULL) {
688 elog(LOG_ERR, "Couldn't open latlog: %m");
689 exit(1);
690 }
691
692 gettimeofday(&now, NULL);
693 time_now = misc_timeval_to_int64(&now);
694 fprintf(logfile, "%lld:%d:%s:%d:%s\n",
695 time_now,
696 my_node_id,
697 "P",
698 seq,
699 type);
700 fflush(logfile);
701 fclose(logfile);
702 #endif
703 }
704
705
This page was automatically generated by the
LXR engine.
Visit the LXR main site for more
information.