Move the code from lttd to liblttd and adapt everything so it works
[ltt-control.git] / liblttd / liblttd.c
1 /* lttd
2 *
3 * Linux Trace Toolkit Daemon
4 *
5 * This is a simple daemon that reads a few relay+debugfs channels and save
6 * them in a trace.
7 *
8 * CPU hot-plugging is supported using inotify.
9 *
10 * Copyright 2005 -
11 * Mathieu Desnoyers <mathieu.desnoyers@polymtl.ca>
12 */
13
14 #ifdef HAVE_CONFIG_H
15 #include <config.h>
16 #endif
17
18 #include "liblttd.h"
19
20 #define _REENTRANT
21 #define _GNU_SOURCE
22 #include <features.h>
23 #include <stdio.h>
24 #include <unistd.h>
25 #include <errno.h>
26 #include <sys/types.h>
27 #include <stdlib.h>
28 #include <dirent.h>
29 #include <string.h>
30 #include <fcntl.h>
31 #include <sys/stat.h>
32 #include <sys/poll.h>
33 #include <sys/mman.h>
34 #include <sys/syscall.h>
35 #include <unistd.h>
36 #include <asm/ioctls.h>
37
38 #include <linux/version.h>
39
40 /* Relayfs IOCTL */
41 #include <asm/ioctl.h>
42 #include <asm/types.h>
43
44 /* Get the next sub buffer that can be read. */
45 #define RELAY_GET_SB _IOR(0xF5, 0x00,__u32)
46 /* Release the oldest reserved (by "get") sub buffer. */
47 #define RELAY_PUT_SB _IOW(0xF5, 0x01,__u32)
48 /* returns the number of sub buffers in the per cpu channel. */
49 #define RELAY_GET_N_SB _IOR(0xF5, 0x02,__u32)
50 /* returns the size of the current sub buffer. */
51 #define RELAY_GET_SB_SIZE _IOR(0xF5, 0x03, __u32)
52 /* returns the size of data to consume in the current sub-buffer. */
53 #define RELAY_GET_MAX_SB_SIZE _IOR(0xF5, 0x04, __u32)
54
55
56 #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,14)
57 #include <sys/inotify.h>
58
59 #define HAS_INOTIFY
60 #else
61 static inline int inotify_init (void)
62 {
63 return -1;
64 }
65
66 static inline int inotify_add_watch (int fd, const char *name, __u32 mask)
67 {
68 return 0;
69 }
70
71 static inline int inotify_rm_watch (int fd, __u32 wd)
72 {
73 return 0;
74 }
75 #undef HAS_INOTIFY
76 #endif
77
78 struct liblttd_callbacks *callbacks;
79
80 struct channel_trace_fd {
81 struct fd_pair *pair;
82 int num_pairs;
83 };
84
85 struct inotify_watch {
86 int wd;
87 char path_channel[PATH_MAX];
88 char *base_path_channel;
89 };
90
91 struct inotify_watch_array {
92 struct inotify_watch *elem;
93 int num;
94 };
95
96 struct channel_trace_fd fd_pairs = { NULL, 0 };
97 int inotify_fd = -1;
98 struct inotify_watch_array inotify_watch_array = { NULL, 0 };
99
100 /* protects fd_pairs and inotify_watch_array */
101 pthread_rwlock_t fd_pairs_lock = PTHREAD_RWLOCK_INITIALIZER;
102
103 static char *channel_name = NULL;
104 static unsigned long num_threads = 1;
105 volatile static int quit_program = 0; /* For signal handler */
106 static int dump_flight_only = 0;
107 static int dump_normal_only = 0;
108 static int verbose_mode = 0;
109
110 #define printf_verbose(fmt, args...) \
111 do { \
112 if (verbose_mode) \
113 printf(fmt, ##args); \
114 } while (0)
115
116
117 int open_buffer_file(char *filename, char *path_channel,
118 char *base_path_channel, struct channel_trace_fd *fd_pairs)
119 {
120 int open_ret = 0;
121 int ret = 0;
122
123 if(strncmp(filename, "flight-", sizeof("flight-")-1) != 0) {
124 if(dump_flight_only) {
125 printf_verbose("Skipping normal channel %s\n",
126 path_channel);
127 return 0;
128 }
129 } else {
130 if(dump_normal_only) {
131 printf_verbose("Skipping flight channel %s\n",
132 path_channel);
133 return 0;
134 }
135 }
136 printf_verbose("Opening file.\n");
137
138 fd_pairs->pair = realloc(fd_pairs->pair,
139 ++fd_pairs->num_pairs * sizeof(struct fd_pair));
140
141 /* Open the channel in read mode */
142 fd_pairs->pair[fd_pairs->num_pairs-1].channel =
143 open(path_channel, O_RDONLY | O_NONBLOCK);
144 if(fd_pairs->pair[fd_pairs->num_pairs-1].channel == -1) {
145 perror(path_channel);
146 fd_pairs->num_pairs--;
147 return 0; /* continue */
148 }
149
150 if(callbacks->on_open_channel) ret = callbacks->on_open_channel(
151 callbacks, &fd_pairs->pair[fd_pairs->num_pairs-1],
152 base_path_channel);
153
154 if(ret != 0) {
155 open_ret = -1;
156 close(fd_pairs->pair[fd_pairs->num_pairs-1].channel);
157 fd_pairs->num_pairs--;
158 goto end;
159 }
160
161 end:
162 return open_ret;
163 }
164
165 int open_channel_trace_pairs(char *subchannel_name,
166 char *base_subchannel_name,
167 struct channel_trace_fd *fd_pairs, int *inotify_fd,
168 struct inotify_watch_array *iwatch_array)
169 {
170 DIR *channel_dir = opendir(subchannel_name);
171 struct dirent *entry;
172 struct stat stat_buf;
173 int ret;
174 char path_channel[PATH_MAX];
175 int path_channel_len;
176 char *path_channel_ptr;
177 char *base_subchannel_ptr;
178
179 int open_ret = 0;
180
181 if(channel_dir == NULL) {
182 perror(subchannel_name);
183 open_ret = ENOENT;
184 goto end;
185 }
186
187 printf_verbose("Calling on new channels folder");
188 if(callbacks->on_new_channels_folder) ret = callbacks->
189 on_new_channels_folder(callbacks,
190 base_subchannel_name);
191 if(ret == -1) {
192 open_ret = -1;
193 goto end;
194 }
195
196 strncpy(path_channel, subchannel_name, PATH_MAX-1);
197 path_channel_len = strlen(path_channel);
198 path_channel[path_channel_len] = '/';
199 path_channel_len++;
200 path_channel_ptr = path_channel + path_channel_len;
201 base_subchannel_ptr = path_channel +
202 (base_subchannel_name - subchannel_name);
203
204 #ifdef HAS_INOTIFY
205 iwatch_array->elem = realloc(iwatch_array->elem,
206 ++iwatch_array->num * sizeof(struct inotify_watch));
207
208 printf_verbose("Adding inotify for channel %s\n", path_channel);
209 iwatch_array->elem[iwatch_array->num-1].wd = inotify_add_watch(*inotify_fd, path_channel, IN_CREATE);
210 strcpy(iwatch_array->elem[iwatch_array->num-1].path_channel, path_channel);
211 iwatch_array->elem[iwatch_array->num-1].base_path_channel =
212 iwatch_array->elem[iwatch_array->num-1].path_channel +
213 (base_subchannel_name - subchannel_name);
214 printf_verbose("Added inotify for channel %s, wd %u\n",
215 iwatch_array->elem[iwatch_array->num-1].path_channel,
216 iwatch_array->elem[iwatch_array->num-1].wd);
217 #endif
218
219 while((entry = readdir(channel_dir)) != NULL) {
220
221 if(entry->d_name[0] == '.') continue;
222
223 strncpy(path_channel_ptr, entry->d_name, PATH_MAX - path_channel_len);
224
225 ret = stat(path_channel, &stat_buf);
226 if(ret == -1) {
227 perror(path_channel);
228 continue;
229 }
230
231 printf_verbose("Channel file : %s\n", path_channel);
232
233 if(S_ISDIR(stat_buf.st_mode)) {
234
235 printf_verbose("Entering channel subdirectory...\n");
236 ret = open_channel_trace_pairs(path_channel, base_subchannel_ptr, fd_pairs,
237 inotify_fd, iwatch_array);
238 if(ret < 0) continue;
239 } else if(S_ISREG(stat_buf.st_mode)) {
240 open_ret = open_buffer_file(entry->d_name, path_channel, base_subchannel_ptr,
241 fd_pairs);
242 if(open_ret)
243 goto end;
244 }
245 }
246
247 end:
248 closedir(channel_dir);
249
250 return open_ret;
251 }
252
253
254 int read_subbuffer(struct fd_pair *pair)
255 {
256 unsigned int consumed_old, len;
257 int err;
258 long ret;
259 off_t offset;
260
261
262 err = ioctl(pair->channel, RELAY_GET_SB, &consumed_old);
263 printf_verbose("cookie : %u\n", consumed_old);
264 if(err != 0) {
265 ret = errno;
266 perror("Reserving sub buffer failed (everything is normal, it is due to concurrency)");
267 goto get_error;
268 }
269
270 err = ioctl(pair->channel, RELAY_GET_SB_SIZE, &len);
271 if(err != 0) {
272 ret = errno;
273 perror("Getting sub-buffer len failed.");
274 goto get_error;
275 }
276
277 if(callbacks->on_read_subbuffer) ret = callbacks->on_read_subbuffer(
278 callbacks, pair, len);
279
280 write_error:
281 ret = 0;
282 err = ioctl(pair->channel, RELAY_PUT_SB, &consumed_old);
283 if(err != 0) {
284 ret = errno;
285 if(errno == EFAULT) {
286 perror("Error in unreserving sub buffer\n");
287 } else if(errno == EIO) {
288 /* Should never happen with newer LTTng versions */
289 perror("Reader has been pushed by the writer, last sub-buffer corrupted.");
290 }
291 goto get_error;
292 }
293
294 get_error:
295 return ret;
296 }
297
298
299 int map_channels(struct channel_trace_fd *fd_pairs,
300 int idx_begin, int idx_end)
301 {
302 int i,j;
303 int ret=0;
304
305 if(fd_pairs->num_pairs <= 0) {
306 printf("No channel to read\n");
307 goto end;
308 }
309
310 /* Get the subbuf sizes and number */
311
312 for(i=idx_begin;i<idx_end;i++) {
313 struct fd_pair *pair = &fd_pairs->pair[i];
314
315 ret = ioctl(pair->channel, RELAY_GET_N_SB, &pair->n_sb);
316 if(ret != 0) {
317 perror("Error in getting the number of sub-buffers");
318 goto end;
319 }
320 ret = ioctl(pair->channel, RELAY_GET_MAX_SB_SIZE,
321 &pair->max_sb_size);
322 if(ret != 0) {
323 perror("Error in getting the max sub-buffer size");
324 goto end;
325 }
326 ret = pthread_mutex_init(&pair->mutex, NULL); /* Fast mutex */
327 if(ret != 0) {
328 perror("Error in mutex init");
329 goto end;
330 }
331 }
332
333 end:
334 return ret;
335 }
336
337 int unmap_channels(struct channel_trace_fd *fd_pairs)
338 {
339 int j;
340 int ret=0;
341
342 /* Munmap each FD */
343 for(j=0;j<fd_pairs->num_pairs;j++) {
344 struct fd_pair *pair = &fd_pairs->pair[j];
345 int err_ret;
346
347 err_ret = pthread_mutex_destroy(&pair->mutex);
348 if(err_ret != 0) {
349 perror("Error in mutex destroy");
350 }
351 ret |= err_ret;
352 }
353
354 return ret;
355 }
356
357 #ifdef HAS_INOTIFY
358 /* Inotify event arrived.
359 *
360 * Only support add file for now.
361 */
362
363 int read_inotify(int inotify_fd,
364 struct channel_trace_fd *fd_pairs,
365 struct inotify_watch_array *iwatch_array)
366 {
367 char buf[sizeof(struct inotify_event) + PATH_MAX];
368 char path_channel[PATH_MAX];
369 ssize_t len;
370 struct inotify_event *ievent;
371 size_t offset;
372 unsigned int i;
373 int ret;
374 int old_num;
375
376 offset = 0;
377 len = read(inotify_fd, buf, sizeof(struct inotify_event) + PATH_MAX);
378 if(len < 0) {
379
380 if(errno == EAGAIN)
381 return 0; /* another thread got the data before us */
382
383 printf("Error in read from inotify FD %s.\n", strerror(len));
384 return -1;
385 }
386 while(offset < len) {
387 ievent = (struct inotify_event *)&(buf[offset]);
388 for(i=0; i<iwatch_array->num; i++) {
389 if(iwatch_array->elem[i].wd == ievent->wd &&
390 ievent->mask == IN_CREATE) {
391 printf_verbose(
392 "inotify wd %u event mask : %u for %s%s\n",
393 ievent->wd, ievent->mask,
394 iwatch_array->elem[i].path_channel,
395 ievent->name);
396 old_num = fd_pairs->num_pairs;
397 strcpy(path_channel, iwatch_array->elem[i].path_channel);
398 strcat(path_channel, ievent->name);
399 if(ret = open_buffer_file(ievent->name, path_channel,
400 path_channel + (iwatch_array->elem[i].base_path_channel -
401 iwatch_array->elem[i].path_channel), fd_pairs)) {
402 printf("Error opening buffer file\n");
403 return -1;
404 }
405 if(ret = map_channels(fd_pairs, old_num, fd_pairs->num_pairs)) {
406 printf("Error mapping channel\n");
407 return -1;
408 }
409
410 }
411 }
412 offset += sizeof(*ievent) + ievent->len;
413 }
414 }
415 #endif //HAS_INOTIFY
416
417 /* read_channels
418 *
419 * Thread worker.
420 *
421 * Read the debugfs channels and write them in the paired tracefiles.
422 *
423 * @fd_pairs : paired channels and trace files.
424 *
425 * returns 0 on success, -1 on error.
426 *
427 * Note that the high priority polled channels are consumed first. We then poll
428 * again to see if these channels are still in priority. Only when no
429 * high priority channel is left, we start reading low priority channels.
430 *
431 * Note that a channel is considered high priority when the buffer is almost
432 * full.
433 */
434
435 int read_channels(unsigned long thread_num, struct channel_trace_fd *fd_pairs,
436 int inotify_fd, struct inotify_watch_array *iwatch_array)
437 {
438 struct pollfd *pollfd = NULL;
439 int num_pollfd;
440 int i,j;
441 int num_rdy, num_hup;
442 int high_prio;
443 int ret = 0;
444 int inotify_fds;
445 unsigned int old_num;
446
447 #ifdef HAS_INOTIFY
448 inotify_fds = 1;
449 #else
450 inotify_fds = 0;
451 #endif
452
453 pthread_rwlock_rdlock(&fd_pairs_lock);
454
455 /* Start polling the FD. Keep one fd for inotify */
456 pollfd = malloc((inotify_fds + fd_pairs->num_pairs) * sizeof(struct pollfd));
457
458 #ifdef HAS_INOTIFY
459 pollfd[0].fd = inotify_fd;
460 pollfd[0].events = POLLIN|POLLPRI;
461 #endif
462
463 for(i=0;i<fd_pairs->num_pairs;i++) {
464 pollfd[inotify_fds+i].fd = fd_pairs->pair[i].channel;
465 pollfd[inotify_fds+i].events = POLLIN|POLLPRI;
466 }
467 num_pollfd = inotify_fds + fd_pairs->num_pairs;
468
469
470 pthread_rwlock_unlock(&fd_pairs_lock);
471
472 while(1) {
473 high_prio = 0;
474 num_hup = 0;
475 #ifdef DEBUG
476 printf("Press a key for next poll...\n");
477 char buf[1];
478 read(STDIN_FILENO, &buf, 1);
479 printf("Next poll (polling %d fd) :\n", num_pollfd);
480 #endif //DEBUG
481
482 /* Have we received a signal ? */
483 if(quit_program) break;
484
485 num_rdy = poll(pollfd, num_pollfd, -1);
486
487 if(num_rdy == -1) {
488 perror("Poll error");
489 goto free_fd;
490 }
491
492 printf_verbose("Data received\n");
493 #ifdef HAS_INOTIFY
494 switch(pollfd[0].revents) {
495 case POLLERR:
496 printf_verbose(
497 "Error returned in polling inotify fd %d.\n",
498 pollfd[0].fd);
499 break;
500 case POLLHUP:
501 printf_verbose(
502 "Polling inotify fd %d tells it has hung up.\n",
503 pollfd[0].fd);
504 break;
505 case POLLNVAL:
506 printf_verbose(
507 "Polling inotify fd %d tells fd is not open.\n",
508 pollfd[0].fd);
509 break;
510 case POLLPRI:
511 case POLLIN:
512 printf_verbose(
513 "Polling inotify fd %d : data ready.\n",
514 pollfd[0].fd);
515
516 pthread_rwlock_wrlock(&fd_pairs_lock);
517 read_inotify(inotify_fd, fd_pairs, iwatch_array);
518 pthread_rwlock_unlock(&fd_pairs_lock);
519
520 break;
521 }
522 #endif
523
524 for(i=inotify_fds;i<num_pollfd;i++) {
525 switch(pollfd[i].revents) {
526 case POLLERR:
527 printf_verbose(
528 "Error returned in polling fd %d.\n",
529 pollfd[i].fd);
530 num_hup++;
531 break;
532 case POLLHUP:
533 printf_verbose(
534 "Polling fd %d tells it has hung up.\n",
535 pollfd[i].fd);
536 num_hup++;
537 break;
538 case POLLNVAL:
539 printf_verbose(
540 "Polling fd %d tells fd is not open.\n",
541 pollfd[i].fd);
542 num_hup++;
543 break;
544 case POLLPRI:
545 pthread_rwlock_rdlock(&fd_pairs_lock);
546 if(pthread_mutex_trylock(&fd_pairs->pair[i-inotify_fds].mutex) == 0) {
547 printf_verbose(
548 "Urgent read on fd %d\n",
549 pollfd[i].fd);
550 /* Take care of high priority channels first. */
551 high_prio = 1;
552 /* it's ok to have an unavailable sub-buffer */
553 ret = read_subbuffer(&fd_pairs->pair[i-inotify_fds]);
554 if(ret == EAGAIN) ret = 0;
555
556 ret = pthread_mutex_unlock(&fd_pairs->pair[i-inotify_fds].mutex);
557 if(ret)
558 printf("Error in mutex unlock : %s\n", strerror(ret));
559 }
560 pthread_rwlock_unlock(&fd_pairs_lock);
561 break;
562 }
563 }
564 /* If every buffer FD has hung up, we end the read loop here */
565 if(num_hup == num_pollfd - inotify_fds) break;
566
567 if(!high_prio) {
568 for(i=inotify_fds;i<num_pollfd;i++) {
569 switch(pollfd[i].revents) {
570 case POLLIN:
571 pthread_rwlock_rdlock(&fd_pairs_lock);
572 if(pthread_mutex_trylock(&fd_pairs->pair[i-inotify_fds].mutex) == 0) {
573 /* Take care of low priority channels. */
574 printf_verbose(
575 "Normal read on fd %d\n",
576 pollfd[i].fd);
577 /* it's ok to have an unavailable subbuffer */
578 ret = read_subbuffer(&fd_pairs->pair[i-inotify_fds]);
579 if(ret == EAGAIN) ret = 0;
580
581 ret = pthread_mutex_unlock(&fd_pairs->pair[i-inotify_fds].mutex);
582 if(ret)
583 printf("Error in mutex unlock : %s\n", strerror(ret));
584 }
585 pthread_rwlock_unlock(&fd_pairs_lock);
586 break;
587 }
588 }
589 }
590
591 /* Update pollfd array if an entry was added to fd_pairs */
592 pthread_rwlock_rdlock(&fd_pairs_lock);
593 if((inotify_fds + fd_pairs->num_pairs) != num_pollfd) {
594 pollfd = realloc(pollfd,
595 (inotify_fds + fd_pairs->num_pairs) * sizeof(struct pollfd));
596 for(i=num_pollfd-inotify_fds;i<fd_pairs->num_pairs;i++) {
597 pollfd[inotify_fds+i].fd = fd_pairs->pair[i].channel;
598 pollfd[inotify_fds+i].events = POLLIN|POLLPRI;
599 }
600 num_pollfd = fd_pairs->num_pairs + inotify_fds;
601 }
602 pthread_rwlock_unlock(&fd_pairs_lock);
603
604 /* NB: If the fd_pairs structure is updated by another thread from this
605 * point forward, the current thread will wait in the poll without
606 * monitoring the new channel. However, this thread will add the
607 * new channel on next poll (and this should not take too much time
608 * on a loaded system).
609 *
610 * This event is quite unlikely and can only occur if a CPU is
611 * hot-plugged while multple lttd threads are running.
612 */
613 }
614
615 free_fd:
616 free(pollfd);
617
618 end:
619 return ret;
620 }
621
622
623 void close_channel_trace_pairs(struct channel_trace_fd *fd_pairs, int inotify_fd,
624 struct inotify_watch_array *iwatch_array)
625 {
626 int i;
627 int ret;
628
629 for(i=0;i<fd_pairs->num_pairs;i++) {
630 ret = close(fd_pairs->pair[i].channel);
631 if(ret == -1) perror("Close error on channel");
632 if(callbacks->on_close_channel) {
633 ret = callbacks->on_close_channel(
634 callbacks, &fd_pairs->pair[i]);
635 if(ret != 0) perror("Error on close channel callback");
636 }
637 }
638 free(fd_pairs->pair);
639 free(iwatch_array->elem);
640 }
641
642 /* Thread worker */
643 void * thread_main(void *arg)
644 {
645 long ret = 0;
646 unsigned long thread_num = (unsigned long)arg;
647
648 if(callbacks->on_new_thread)
649 ret = callbacks->on_new_thread(callbacks, thread_num);
650
651 if (ret < 0) {
652 return (void*)ret;
653 }
654 ret = read_channels(thread_num, &fd_pairs, inotify_fd, &inotify_watch_array);
655
656 if(callbacks->on_close_thread)
657 callbacks->on_close_thread(callbacks, thread_num);
658
659 return (void*)ret;
660 }
661
662 /*on_close_thread has to be reentrant, it'll be called by many threads*/
663 int(*on_close_thread)(struct liblttd_callbacks *data, unsigned long thread_num);
664
665 int channels_init()
666 {
667 int ret = 0;
668
669 inotify_fd = inotify_init();
670 fcntl(inotify_fd, F_SETFL, O_NONBLOCK);
671
672 if(ret = open_channel_trace_pairs(channel_name,
673 channel_name + strlen(channel_name), &fd_pairs,
674 &inotify_fd, &inotify_watch_array))
675 goto close_channel;
676 if (fd_pairs.num_pairs == 0) {
677 printf("No channel available for reading, exiting\n");
678 ret = -ENOENT;
679 goto close_channel;
680 }
681 if(ret = map_channels(&fd_pairs, 0, fd_pairs.num_pairs))
682 goto close_channel;
683 return 0;
684
685 close_channel:
686 close_channel_trace_pairs(&fd_pairs, inotify_fd, &inotify_watch_array);
687 if(inotify_fd >= 0)
688 close(inotify_fd);
689 return ret;
690 }
691
692 int liblttd_start(char *channel_path, unsigned long n_threads,
693 int flight_only, int normal_only, int verbose,
694 struct liblttd_callbacks *user_data){
695 int ret = 0;
696 pthread_t *tids;
697 unsigned long i;
698 void *tret;
699
700 channel_name = channel_path;
701 num_threads = n_threads;
702 dump_flight_only = flight_only;
703 dump_normal_only = normal_only;
704 verbose_mode = verbose;
705 callbacks = user_data;
706
707 if(ret = channels_init())
708 return ret;
709
710 tids = malloc(sizeof(pthread_t) * num_threads);
711 for(i=0; i<num_threads; i++) {
712
713 ret = pthread_create(&tids[i], NULL, thread_main, (void*)i);
714 if(ret) {
715 perror("Error creating thread");
716 break;
717 }
718 }
719
720 for(i=0; i<num_threads; i++) {
721 ret = pthread_join(tids[i], &tret);
722 if(ret) {
723 perror("Error joining thread");
724 break;
725 }
726 if((long)tret != 0) {
727 printf("Error %s occured in thread %ld\n",
728 strerror((long)tret), i);
729 }
730 }
731
732 free(tids);
733 ret = unmap_channels(&fd_pairs);
734 close_channel_trace_pairs(&fd_pairs, inotify_fd, &inotify_watch_array);
735 if(inotify_fd >= 0)
736 close(inotify_fd);
737
738 if(callbacks->on_trace_end) callbacks->on_trace_end(callbacks);
739
740 return ret;
741 }
742
743 int liblttd_stop() {
744 quit_program = 1;
745 return 0;
746 }
747
This page took 0.050851 seconds and 4 git commands to generate.