source: examples/tutorial/ipdist-parallel.c @ 6c8579e

develop
Last change on this file since 6c8579e was 6c8579e, checked in by Jacob Van Walraven <jcv9@…>, 4 years ago

Added more statistics

  • Property mode set to 100644
File size: 21.3 KB
Line 
1#include "libtrace_parallel.h"
2#include <stdio.h>
3#include <stdlib.h>
4#include <string.h>
5#include <sys/socket.h>
6#include <netinet/in.h>
7#include <arpa/inet.h>
8#include <time.h>
9
10/* Structure to hold the counters each thread has its own one of these */
11struct addr_local {
12        /* Holds the counts of each number occurance per octet, These are cleared after every output. */
13        uint64_t src[4][256];
14        uint64_t dst[4][256];
15        /* Holds the results from the previous output */
16        uint64_t src_lastoutput[4][256];
17        uint64_t dst_lastoutput[4][256];
18        /* Holds the timestamp */
19        uint64_t lastkey;
20        /* Is the count of the number of packets processed, This is cleared after every output. */
21        uint64_t packets;
22        uint64_t output_count;
23        /* Pointer to stats structure */
24        struct addr_stats *stats;
25
26};
27struct addr_stats {
28        /* Holds the percentage change compared to the previous output */
29        float src[4][256];
30        float dst[4][256];
31        struct addr_rank *rank_src[4];
32        struct addr_rank *rank_dst[4];
33};
34struct addr_rank {
35        uint8_t addr;
36        /* count is the priority */
37        uint64_t count;
38        /* pointer to next ranking item */
39        struct addr_rank* next;
40};
41
42/* Structure to hold excluded networks */
43struct exclude_networks {
44        int count;
45        struct network *networks;
46};
47struct network {
48        uint32_t address;
49        uint32_t mask;
50        uint32_t network;
51};
52
53/* interval between outputs in seconds */
54uint64_t tickrate;
55
56char *stats_outputdir = "/home/jcv9/output/";
57/* Calculate and plot the percentage change from the previous plot */
58int stats_percentage_change = 1;
59int stats_ranking = 1;
60
61
62/*************************************************************************
63Priority queue linked list */
64
65static struct addr_rank *rank_new(uint8_t addr, uint64_t count) {
66        struct addr_rank *tmp = malloc(sizeof(struct addr_rank));
67        tmp->addr = addr;
68        tmp->count = count;
69        tmp->next = NULL;
70
71        return tmp;
72}
73static uint8_t peak(struct addr_rank **head) {
74        return (*head)->addr;
75}
76static void pop(struct addr_rank **head) {
77        struct addr_rank* tmp = *head;
78        (*head) = (*head)->next;
79        free(tmp);
80}
81static void push(struct addr_rank **head, uint8_t addr, uint64_t count) {
82        struct addr_rank *curr = (*head);
83        struct addr_rank *tmp = rank_new(addr, count);
84
85        /* Check if the new node has a greater priority than the head */
86        if((*head)->count < count) {
87                tmp->next = *head;
88                (*head) = tmp;
89        } else {
90                /* Jump through the list until we find the correct position */
91                while (curr->next != NULL && curr->next->count > count) {
92                        curr = curr->next;
93                }
94
95                tmp->next = curr->next;
96                curr->next = tmp;
97        }
98}
99/*************************************************************************/
100
101
102static void compute_stats(struct addr_local *tally) {
103        int i, j;
104
105        /* Calculates the percentage change from the last output. NEED TO MAKE THIS WEIGHTED */
106        if(stats_percentage_change) {
107                for(i=0;i<256;i++) {
108                        for(j=0;j<4;j++) {
109                                tally->stats->src[j][i] = 0;
110                                tally->stats->dst[j][i] = 0;
111                                if(tally->src[j][i] != 0) {
112                                        tally->stats->src[j][i] = (((float)tally->src[j][i] - (float)tally->src_lastoutput[j][i]) / (float)tally->src[j][i]) * 100;
113                                }
114                                if(tally->dst[j][i] != 0) {
115                                        tally->stats->dst[j][i] = (((float)tally->dst[j][i] - (float)tally->dst_lastoutput[j][i]) / (float)tally->dst[j][i]) * 100;
116                                }
117                        }
118                }
119        }
120
121        /* RANKING SYSTEM */
122        if(stats_ranking) {
123                for(i=0;i<4;i++) {
124                        tally->stats->rank_src[i] = rank_new(0, tally->src[i][0]);
125                        tally->stats->rank_dst[i] = rank_new(0, tally->dst[i][0]);
126
127                        for(j=1;j<256;j++) {
128                                /* Push everything into the priority queue
129                                 * each item will be popped off in the correct order */
130                                push(&tally->stats->rank_src[i], j, tally->src[i][j]);
131                                push(&tally->stats->rank_dst[i], j, tally->dst[i][j]);
132                        }
133                }
134        }
135}
136
137static void per_tick(libtrace_t *trace, libtrace_thread_t *thread, void *global, void *tls, uint64_t tick) {
138
139        struct addr_local *result = (struct addr_local *)malloc(sizeof(struct addr_local));
140        /* Proccessing thread local storage */
141        struct addr_local *local = (struct addr_local *)tls;
142
143        /* Populate the result structure from the threads local storage and clear threads local storage*/
144        int i, j;
145        for(i=0;i<4;i++) {
146                for(j=0;j<256;j++) {
147                        result->src[i][j] = local->src[i][j];
148                        result->dst[i][j] = local->dst[i][j];
149                        /* clear local storage */
150                        local->src[i][j] = 0;
151                        local->dst[i][j] = 0;
152                }
153        }
154        result->packets = local->packets;
155        local->packets = 0;
156
157        /* Push result to the combiner */
158        trace_publish_result(trace, thread, tick, (libtrace_generic_t){.ptr=result}, RESULT_USER);
159}
160
161/* Start callback function - This is run for each thread when it starts */
162static void *start_callback(libtrace_t *trace, libtrace_thread_t *thread, void *global) {
163
164        /* Create and initialize the local counter struct */
165        struct addr_local *local = (struct addr_local *)malloc(sizeof(struct addr_local));
166        int i, j;
167        for(i=0;i<4;i++) {
168                for(j=0;j<256;j++) {
169                        local->src[i][j] = 0;
170                        local->dst[i][j] = 0;
171                }
172        }
173        local->lastkey = 0;
174        local->packets = 0;
175
176        /* return the local storage so it is available for all other callbacks for the thread*/
177        return local;
178}
179
180/* Checks if address is part of a excluded subnet. */
181static int network_excluded(uint32_t address, struct exclude_networks *exclude) {
182
183        int i;
184        for(i=0;i<exclude->count;i++) {
185                /* Convert address into a network address */
186                uint32_t net_addr = address & exclude->networks[i].mask;
187
188                /* If this matches the network address from the excluded list we need to exclude this
189                   address. */
190                if(net_addr == exclude->networks[i].network) {
191                        return 1;
192                }
193        }
194
195        /* If we got this far the address should not be excluded */
196        return 0;
197}
198
199static void process_ip(struct sockaddr *ip, struct addr_local *local, struct exclude_networks *exclude, int srcaddr) {
200
201        /* Checks if the ip is of type IPv4 */
202        if (ip->sa_family == AF_INET) {
203
204                /* IPv4 - cast the generic sockaddr to a sockaddr_in */
205                struct sockaddr_in *v4 = (struct sockaddr_in *)ip;
206                /* Get in_addr from sockaddr */
207                struct in_addr ip4 = (struct in_addr)v4->sin_addr;
208                /* Ensure the address is in network byte order */
209                uint32_t address = htonl(ip4.s_addr);
210
211                /* Check if the address is part of an excluded network. */
212                if(network_excluded(address, exclude) == 0) {
213
214                        /* Split the IPv4 address into each octet */
215                        uint8_t octet[4];
216                        octet[0] = (address & 0xff000000) >> 24;
217                        octet[1] = (address & 0x00ff0000) >> 16;
218                        octet[2] = (address & 0x0000ff00) >> 8;
219                        octet[3] = (address & 0x000000ff);
220
221                        /* check if the supplied address was a source or destination,
222                           increment the correct one */
223                        if(srcaddr) {
224                                int i;
225                                for(i=0;i<4;i++) {
226                                        local->src[i][octet[i]] += 1;
227                                }
228                        } else {
229                                int i;
230                                for(i=0;i<4;i++) {
231                                        local->dst[i][octet[i]] += 1;
232                                }
233                        }
234                }
235        }
236}
237
238/* Per packet callback function run by each thread */
239static libtrace_packet_t *per_packet(libtrace_t *trace, libtrace_thread_t *thread, void *global, void *tls,
240        libtrace_packet_t *packet) {
241
242        /* Regain access to the address counter structure */
243        struct addr_local *local = (struct addr_local *)tls;
244
245        /* If this is the first packet set the lastkey to the packets timestamp */
246        if(local->lastkey == 0) {
247                local->lastkey = trace_get_erf_timestamp(packet);
248        }
249
250        /* Increment the packet count */
251        local->packets += 1;
252
253        /* Regain access to excluded networks pointer */
254        struct exclude_networks *exclude = (struct exclude_networks *)global;
255
256        struct sockaddr_storage addr;
257        struct sockaddr *ip;
258
259        /* Get the source IP address */
260        ip = trace_get_source_address(packet, (struct sockaddr *)&addr);
261        /* If a source ip address was found */
262        if(ip != NULL) {
263                process_ip(ip, local, exclude, 1);
264        }
265
266        /* Get the destination IP address */
267        ip = trace_get_destination_address(packet, (struct sockaddr *)&addr);
268        /* If a destination ip address was found */
269        if(ip != NULL) {
270                process_ip(ip, local, exclude, 0);
271        }
272
273        /* If this trace is not live we will manually call "per tick" */
274        if(!trace_get_information(trace)->live) {
275                /* get the current packets timestamp */
276                uint64_t timestamp = trace_get_erf_timestamp(packet);
277
278                /* We only want to call per_tick if we are due to output something
279                 * Right shifting these converts them to seconds, tickrate is in seconds */
280                if((timestamp >> 32) >= (local->lastkey >> 32) + tickrate) {
281                        per_tick(trace, thread, global, local, timestamp);
282                        local->lastkey = timestamp;
283                }
284        }
285
286        /* Return the packet to libtrace */
287        return packet;
288}
289
290/* Stopping callback function - When a thread closes */
291static void stop_processing(libtrace_t *trace, libtrace_thread_t *thread, void *global, void *tls) {
292
293        /* cast the local storage structure */
294        struct addr_local *local = (struct addr_local *)tls;
295        /* Create structure to store the result */
296        struct addr_local *result = (struct addr_local *)malloc(sizeof(struct addr_local));
297
298        /* Populate the result */
299        int i, j;
300        for(i=0;i<4;i++) {
301                for(j=0;j<256;j++) {
302                        result->src[i][j] = local->src[i][j];
303                        result->dst[i][j] = local->dst[i][j];
304                }
305        }
306        result->packets = local->packets;
307
308        /* Send the final results to the combiner */
309        trace_publish_result(trace, thread, 0, (libtrace_generic_t){.ptr=result}, RESULT_USER);
310
311        /* Cleanup the local storage */
312        free(local);
313}
314
315/* Starting callback for reporter thread */
316static void *start_reporter(libtrace_t *trace, libtrace_thread_t *thread, void *global) {
317        /* Create tally structure */
318        struct addr_local *tally = (struct addr_local *)malloc(sizeof(struct addr_local));
319        tally->stats = malloc(sizeof(struct addr_stats));
320
321        /* Initialize the tally structure */
322        int i, j;
323        for(i=0;i<4;i++) {
324                for(j=0;j<256;j++) {
325                        tally->src[i][j] = 0;
326                        tally->dst[i][j] = 0;
327                        tally->src_lastoutput[i][j] = 0;
328                        tally->dst_lastoutput[i][j] = 0;
329                        tally->stats->src[i][j] = 0;
330                        tally->stats->dst[i][j] = 0;
331                }
332        }
333        tally->lastkey = 0;
334        tally->packets = 0;
335        tally->output_count = 0;
336
337        return tally;
338}
339
340static void plot_results(struct addr_local *tally, uint64_t tick) {
341
342        /* Calculations before reporting the results */
343        /* Need to initialise lastoutput values on first pass,
344         * this is so we have a base line for percentage changed */
345        if(tally->output_count == 0) {
346                for(i=0;i<4;i++) {
347                        for(j=0;j<256;j++) {
348                                tally->src_lastoutput[i][j] = tally->src[i][j];
349                                tally->dst_lastoutput[i][j] = tally->dst[i][j];
350                        }
351                }
352         }
353        /* Compute the stats */
354        compute_stats(tally);
355
356        /* Finaly output the results */
357        printf("Generating output \"%sipdist-%u\"\n", stats_outputdir, tick);
358
359        /* Output the results */
360        char outputfile[255];
361        snprintf(outputfile, sizeof(outputfile), "%sipdist-%u.data", stats_outputdir, tick);
362        FILE *tmp = fopen(outputfile, "w");
363        int i, j;
364        fprintf(tmp, "#\tHits");
365        if(stats_percentage_change) {
366                fprintf(tmp, "\t\t\t\t\t\t\t\tPercentage");
367        }
368        if(stats_ranking) {
369                fprintf(tmp, "\t\t\t\t\t\t\t\tRanking");
370        }
371        fprintf(tmp, "\n");
372        fprintf(tmp, "#num\toctet1\t\toctet2\t\toctet3\t\toctet4");
373        if(stats_percentage_change) {
374                fprintf(tmp, "\t\toctet1\t\toctet2\t\toctet3\t\toctet4");
375        }
376        if(stats_ranking) {
377                fprintf(tmp, "\t\toctet1\t\toctet2\t\toctet3\t\toctet4");
378        }
379        fprintf(tmp, "\n");
380        fprintf(tmp, "#\tsrc\tdst\tsrc\tdst\tsrc\tdst\tsrc\tdst");
381        if(stats_percentage_change) {
382                fprintf(tmp, "\tsrc\tdst\tsrc\tdst\tsrc\tdst\tsrc\tdst");
383        }
384        if(stats_ranking) {
385                fprintf(tmp, "\tsrc\tdst\tsrc\tdst\tsrc\tdst\tsrc\tdst");
386        }
387        fprintf(tmp, "\n");
388        for(i=0;i<256;i++) {
389                fprintf(tmp, "%d", i);
390                for(j=0;j<4;j++) {
391                        fprintf(tmp, "\t%d\t%d", tally->src[j][i], tally->dst[j][i]);
392                }
393                if(stats_percentage_change) {
394                        for(j=0;j<4;j++) {
395                                fprintf(tmp, "\t%.0f\t%.0f", tally->stats->src[j][i], tally->stats->dst[j][i]);
396                        }
397                }
398                if(stats_ranking) {
399                        for(j=0;j<4;j++) {
400                                /* Get the highest ranking to lowest ranking octets */
401                                fprintf(tmp, "\t%d", peak(&tally->stats->rank_src[j]));
402                                fprintf(tmp, "\t%d", peak(&tally->stats->rank_dst[j]));
403                                pop(&tally->stats->rank_src[j]);
404                                pop(&tally->stats->rank_dst[j]);
405                        }
406                }
407                fprintf(tmp, "\n");
408        }
409        fclose(tmp);
410
411        if(stats_ranking) {
412                for(i=0;i<4;i++) {
413                        free(tally->stats->rank_src[i]);
414                        free(tally->stats->rank_dst[i]);
415                }
416        }
417
418        /* Plot the results */
419        for(i=0;i<4;i++) {
420                char outputplot[255];
421                snprintf(outputplot, sizeof(outputplot), "%sipdist-%u-octet%d.png", stats_outputdir, tick, i+1);
422
423                /* Open pipe to gnuplot */
424                FILE *gnuplot = popen("gnuplot -persistent", "w");
425                /* send all commands to gnuplot */
426                fprintf(gnuplot, "set term png size 1280,960 \n");
427                fprintf(gnuplot, "set title 'IP Distribution - Octet %d'\n", i+1);
428                fprintf(gnuplot, "set xrange[0:255]\n");
429                fprintf(gnuplot, "set xlabel 'Prefix'\n");
430                fprintf(gnuplot, "set ylabel 'Hits'\n");
431                fprintf(gnuplot, "set xtics 0,10,255\n");
432                fprintf(gnuplot, "set output '%s'\n", outputplot);
433                if(stats_percentage_change) {
434                        fprintf(gnuplot, "set y2label 'Percentage Change'\n");
435                        fprintf(gnuplot, "set y2range[-100:100]\n");
436                        fprintf(gnuplot, "set ytics nomirror\n");
437                        fprintf(gnuplot, "plot '%s' using 1:%d title 'Source octet %d' axes x1y1 with boxes,", outputfile, i+2, i+1);
438                        fprintf(gnuplot, "'%s' using 1:%d title 'Destination octet %d' axes x1y1 with boxes,", outputfile, i+3, i+1);
439                        fprintf(gnuplot, "'%s' using 1:%d title 'Octet %d source change' axes x1y2 with lines,", outputfile, i+10, i+1);
440                        fprintf(gnuplot, "'%s' using 1:%d title 'Octet %d destination change' axes x1y2 with lines\n", outputfile, i+11, i+1);
441                } else {
442                        fprintf(gnuplot, "plot '%s' using 1:%d title 'Source octet %d' axes x1y1 with boxes,", outputfile, i+2, i+1);
443                        fprintf(gnuplot, "'%s' using 1:%d title 'Destination octet %d' axes x1y1 with boxes\n", outputfile, i+3, i+1);
444                }
445                fprintf(gnuplot, "replot");
446                pclose(gnuplot);
447        }
448}
449
450
451/* Callback when a result is given to the reporter thread */
452static void per_result(libtrace_t *trace, libtrace_thread_t *sender, void *global,
453        void *tls, libtrace_result_t *result) {
454
455        struct addr_local *results;
456        struct addr_local *tally;
457        uint64_t key;
458
459        /* We only want to handle results containing our user-defined structure  */
460        if(result->type != RESULT_USER) {
461                return;
462        }
463
464        /* This key is the key that was passed into trace_publish_results
465         * this will contain the erf timestamp for the packet */
466        key = result->key;
467
468        /* result->value is a libtrace_generic_t that was passed into trace_publish_results() */
469        results = (struct addr_local *)result->value.ptr;
470
471        /* Grab our tally out of thread local storage */
472        tally = (struct addr_local *)tls;
473
474        /* Add all the results to the tally */
475        int i, j;
476        for(i=0;i<4;i++) {
477                for(j=0;j<256;j++) {
478                        tally->src[i][j] += results->src[i][j];
479                        tally->dst[i][j] += results->dst[i][j];
480                }
481        }
482        tally->packets += results->packets;
483
484        /* If the current timestamp is greater than the last printed plus the interval, output a result */
485        if((key >> 32) >= (tally->lastkey >> 32) + tickrate) {
486
487                /* update last key */
488                tally->lastkey = key;
489
490                /* Plot the result with the key in epoch seconds*/
491                plot_results(tally, key >> 32);
492
493                /* increment the output counter */
494                tally->output_count++;
495
496                /* clear the tally but copy old values over first*/
497                for(i=0;i<4;i++) {
498                        for(j=0;j<256;j++) {
499                                tally->src_lastoutput[i][j] = tally->src[i][j];
500                                tally->dst_lastoutput[i][j] = tally->dst[i][j];
501                                tally->src[i][j] = 0;
502                                tally->dst[i][j] = 0;
503                        }
504                }
505                tally->packets = 0;
506
507        }
508
509        /* Cleanup the thread results */
510        free(results);
511}
512
513/* Callback when the reporter thread stops (essentially when the program ends) */
514static void stop_reporter(libtrace_t *trace, libtrace_thread_t *thread, void *global, void *tls) {
515
516        /* Get the tally from the thread local storage */
517        struct addr_local *tally = (struct addr_local *)tls;
518
519        /* If there is any remaining data in the tally plot it */
520        if(tally->packets > 0) {
521                /* Then plot the results */
522                plot_results(tally, (tally->lastkey >> 32) + 1);
523        }
524        /* Cleanup tally results*/
525        free(tally);
526}
527
528static void libtrace_cleanup(libtrace_t *trace, libtrace_callback_set_t *processing,
529        libtrace_callback_set_t *reporting) {
530        /* Only destroy if the structure exists */
531        if(trace) {
532                trace_destroy(trace);
533        }
534        if(processing) {
535                trace_destroy_callback_set(processing);
536        }
537        if(reporting) {
538                trace_destroy_callback_set(reporting);
539        }
540}
541
542/* Converts a string representation eg 1.2.3.4/24 into a network structure */
543static int get_network(char *network_string, struct network *network) {
544
545        char delim[] = "/";
546        /* Split the address and mask portion of the string */
547        char *address = strtok(network_string, delim);
548        char *mask = strtok(NULL, delim);
549
550        /* Check the subnet mask is valid */
551        if(atoi(mask) == 0 || atoi(mask) > 32 || atoi(mask) < 0) {
552                return 1;
553        }
554        /* right shift so netmask is in network byte order */
555        network->mask = 0xffffffff << (32 - atoi(mask));
556
557        struct in_addr addr;
558        /* Convert address string into uint32_t and check its valid */
559        if(inet_aton(address, &addr) == 0) {
560                return 2;
561        }
562        /* Ensure its saved in network byte order */
563        network->address = htonl(addr.s_addr);
564
565        /* Calculate the network address */
566        network->network = network->address & network->mask;
567
568        return 0;
569}
570
571int main(int argc, char *argv[]) {
572
573        libtrace_t *trace;
574        /* Callbacks for processing and reporting threads */
575        libtrace_callback_set_t *processing, *reporter;
576
577
578        /* Ensure the input URI was supplied */
579        if(argc < 3) {
580                fprintf(stderr, "Usage: %s inputURI [outputInterval (Seconds)] [excluded networks]\n", argv[0]);
581                fprintf(stderr, "       eg. ./ipdist input.erf 60 210.10.3.0/24 70.5.0.0/16\n");
582                return 1;
583        }
584        /* Convert tick into an int */
585        tickrate = atoi(argv[2]);
586
587
588        /* Create the trace */
589        trace = trace_create(argv[1]);
590        /* Ensure no error has occured creating the trace */
591        if(trace_is_err(trace)) {
592                trace_perror(trace, "Creating trace");
593                return 1;
594        }
595
596        /* Setup the processing threads */
597        processing = trace_create_callback_set();
598        trace_set_starting_cb(processing, start_callback);
599        trace_set_packet_cb(processing, per_packet);
600        trace_set_stopping_cb(processing, stop_processing);
601        trace_set_tick_interval_cb(processing, per_tick);
602        /* Setup the reporter threads */
603        reporter = trace_create_callback_set();
604        trace_set_starting_cb(reporter, start_reporter);
605        trace_set_result_cb(reporter, per_result);
606        trace_set_stopping_cb(reporter, stop_reporter);
607
608        /* Parallel specific configuration MUST BE PERFORMED AFTER TRACE IS CREATED */
609        trace_set_perpkt_threads(trace, 4);
610        /* Order the results by timestamp */
611        trace_set_combiner(trace, &combiner_ordered, (libtrace_generic_t){0});
612        /* Try to balance the load across all processing threads */
613        trace_set_hasher(trace, HASHER_BALANCE, NULL, NULL);
614
615        /* Set the tick interval only if this is a live capture */
616        if(trace_get_information(trace)->live) {
617                /* tickrate is in seconds but tick_interval expects milliseconds */
618                trace_set_tick_interval(trace, tickrate*1000);
619        }
620        /* Do not buffer the reports */
621        trace_set_reporter_thold(trace, 1);
622
623
624        /* Setup excluded networks if any were supplied */
625        struct exclude_networks *exclude = malloc(sizeof(struct exclude_networks));
626        exclude->networks = malloc(sizeof(struct network)*(argc-3));
627        if(exclude == NULL || exclude->networks == NULL) {
628                fprintf(stderr, "Unable to allocate memory");
629                libtrace_cleanup(trace, processing, reporter);
630                return 1;
631        }
632        exclude->count = 0;
633        int i;
634        for(i=0;i<argc-3;i++) {
635                /* convert the network string into a network structure */
636                if(get_network(argv[i+3], &exclude->networks[i]) != 0) {
637                        fprintf(stderr, "Error creating excluded network");
638                        return 1;
639                }
640                /* increment the count of excluded networks */
641                exclude->count += 1;
642        }
643
644
645        /* Start the trace, if it errors return */
646        if(trace_pstart(trace, exclude, processing, reporter)) {
647                trace_perror(trace, "Starting parallel trace");
648                libtrace_cleanup(trace, processing, reporter);
649                return 1;
650        }
651
652        /* This will wait for all threads to complete */
653        trace_join(trace);
654
655        /* Clean up everything */
656        free(exclude->networks);
657        free(exclude);
658        libtrace_cleanup(trace, processing, reporter);
659
660        return 0;
661}
Note: See TracBrowser for help on using the repository browser.