Rename bt_ctf_X -> bt_X, maintain backward compat. for pre-2.0 CTF writer
[babeltrace.git] / plugins / libctfcopytrace / ctfcopytrace.c
1 /*
2 * copytrace.c
3 *
4 * Babeltrace library to create a copy of a CTF trace
5 *
6 * Copyright 2017 Julien Desfossez <jdesfossez@efficios.com>
7 *
8 * Author: Julien Desfossez <jdesfossez@efficios.com>
9 *
10 * Permission is hereby granted, free of charge, to any person obtaining a copy
11 * of this software and associated documentation files (the "Software"), to deal
12 * in the Software without restriction, including without limitation the rights
13 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 * copies of the Software, and to permit persons to whom the Software is
15 * furnished to do so, subject to the following conditions:
16 *
17 * The above copyright notice and this permission notice shall be included in
18 * all copies or substantial portions of the Software.
19 *
20 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 * SOFTWARE.
27 */
28
29 #define BT_LOG_TAG "PLUGIN-CTFCOPYTRACE-LIB"
30 #include "logging.h"
31
32 #include <babeltrace/babeltrace.h>
33 #include <assert.h>
34
35 #include "ctfcopytrace.h"
36 #include "clock-fields.h"
37
38 BT_HIDDEN
39 struct bt_clock_class *ctf_copy_clock_class(FILE *err,
40 struct bt_clock_class *clock_class)
41 {
42 int64_t offset, offset_s;
43 int int_ret;
44 uint64_t u64_ret;
45 const char *name, *description;
46 struct bt_clock_class *writer_clock_class = NULL;
47
48 assert(err && clock_class);
49
50 name = bt_clock_class_get_name(clock_class);
51 assert(name);
52
53 writer_clock_class = bt_clock_class_create(name,
54 bt_clock_class_get_frequency(clock_class));
55 if (!writer_clock_class) {
56 BT_LOGE_STR("Failed to create clock class.");
57 goto end;
58 }
59
60 description = bt_clock_class_get_description(clock_class);
61 if (description) {
62 int_ret = bt_clock_class_set_description(writer_clock_class,
63 description);
64 assert(!int_ret);
65 }
66
67 u64_ret = bt_clock_class_get_precision(clock_class);
68 assert(u64_ret != -1ULL);
69
70 int_ret = bt_clock_class_set_precision(writer_clock_class,
71 u64_ret);
72 assert(!int_ret);
73
74 int_ret = bt_clock_class_get_offset_s(clock_class, &offset_s);
75 assert(!int_ret);
76
77 int_ret = bt_clock_class_set_offset_s(writer_clock_class, offset_s);
78 assert(!int_ret);
79
80 int_ret = bt_clock_class_get_offset_cycles(clock_class, &offset);
81 assert(!int_ret);
82
83 int_ret = bt_clock_class_set_offset_cycles(writer_clock_class, offset);
84 assert(!int_ret);
85
86 int_ret = bt_clock_class_is_absolute(clock_class);
87 assert(int_ret >= 0);
88
89 int_ret = bt_clock_class_set_is_absolute(writer_clock_class, int_ret);
90 assert(!int_ret);
91
92 end:
93 return writer_clock_class;
94 }
95
96 BT_HIDDEN
97 enum bt_component_status ctf_copy_clock_classes(FILE *err,
98 struct bt_trace *writer_trace,
99 struct bt_stream_class *writer_stream_class,
100 struct bt_trace *trace)
101 {
102 enum bt_component_status ret;
103 int int_ret, clock_class_count, i;
104
105 clock_class_count = bt_trace_get_clock_class_count(trace);
106
107 for (i = 0; i < clock_class_count; i++) {
108 struct bt_clock_class *writer_clock_class;
109 struct bt_clock_class *clock_class =
110 bt_trace_get_clock_class_by_index(trace, i);
111
112 assert(clock_class);
113
114 writer_clock_class = ctf_copy_clock_class(err, clock_class);
115 bt_put(clock_class);
116 if (!writer_clock_class) {
117 BT_LOGE_STR("Failed to copy clock class.");
118 ret = BT_COMPONENT_STATUS_ERROR;
119 goto end;
120 }
121
122 int_ret = bt_trace_add_clock_class(writer_trace, writer_clock_class);
123 if (int_ret != 0) {
124 BT_PUT(writer_clock_class);
125 BT_LOGE_STR("Failed to add clock class.");
126 ret = BT_COMPONENT_STATUS_ERROR;
127 goto end;
128 }
129
130 /*
131 * Ownership transferred to the trace.
132 */
133 bt_put(writer_clock_class);
134 }
135
136 ret = BT_COMPONENT_STATUS_OK;
137
138 end:
139 return ret;
140 }
141
142 BT_HIDDEN
143 struct bt_event_class *ctf_copy_event_class(FILE *err,
144 struct bt_event_class *event_class)
145 {
146 struct bt_event_class *writer_event_class = NULL;
147 struct bt_field_type *context, *payload_type;
148 const char *name;
149 int ret;
150 int64_t id;
151 enum bt_event_class_log_level log_level;
152 const char *emf_uri;
153
154 name = bt_event_class_get_name(event_class);
155
156 writer_event_class = bt_event_class_create(name);
157 assert(writer_event_class);
158
159 id = bt_event_class_get_id(event_class);
160 assert(id >= 0);
161
162 ret = bt_event_class_set_id(writer_event_class, id);
163 if (ret) {
164 BT_LOGE_STR("Failed to set event_class id.");
165 goto error;
166 }
167
168 log_level = bt_event_class_get_log_level(event_class);
169 if (log_level < 0) {
170 BT_LOGE_STR("Failed to get log_level.");
171 goto error;
172 }
173
174 ret = bt_event_class_set_log_level(writer_event_class, log_level);
175 if (ret) {
176 BT_LOGE_STR("Failed to set log_level.");
177 goto error;
178 }
179
180 emf_uri = bt_event_class_get_emf_uri(event_class);
181 if (emf_uri) {
182 ret = bt_event_class_set_emf_uri(writer_event_class,
183 emf_uri);
184 if (ret) {
185 BT_LOGE_STR("Failed to set emf uri.");
186 goto error;
187 }
188 }
189
190 payload_type = bt_event_class_get_payload_type(event_class);
191 if (payload_type) {
192 ret = bt_event_class_set_payload_type(writer_event_class,
193 payload_type);
194 if (ret < 0) {
195 BT_LOGE_STR("Failed to set payload type.");
196 goto error;
197 }
198 BT_PUT(payload_type);
199 }
200
201 context = bt_event_class_get_context_type(event_class);
202 if (context) {
203 ret = bt_event_class_set_context_type(
204 writer_event_class, context);
205 BT_PUT(context);
206 if (ret < 0) {
207 BT_LOGE_STR("Failed to set context type.");
208 goto error;
209 }
210 }
211
212 goto end;
213
214 error:
215 BT_PUT(writer_event_class);
216 end:
217 return writer_event_class;
218 }
219
220 BT_HIDDEN
221 enum bt_component_status ctf_copy_event_classes(FILE *err,
222 struct bt_stream_class *stream_class,
223 struct bt_stream_class *writer_stream_class)
224 {
225 enum bt_component_status ret = BT_COMPONENT_STATUS_OK;
226 struct bt_event_class *event_class = NULL, *writer_event_class = NULL;
227 int count, i;
228
229 count = bt_stream_class_get_event_class_count(stream_class);
230 assert(count >= 0);
231
232 for (i = 0; i < count; i++) {
233 int int_ret;
234
235 event_class = bt_stream_class_get_event_class_by_index(
236 stream_class, i);
237 assert(event_class);
238
239 if (i < bt_stream_class_get_event_class_count(writer_stream_class)) {
240 writer_event_class = bt_stream_class_get_event_class_by_index(
241 writer_stream_class, i);
242 if (writer_event_class) {
243 /*
244 * If the writer_event_class already exists,
245 * just skip it. It can be used to resync the
246 * event_classes after a trace has become
247 * static.
248 */
249 BT_PUT(writer_event_class);
250 BT_PUT(event_class);
251 continue;
252 }
253 }
254
255 writer_event_class = ctf_copy_event_class(err, event_class);
256 if (!writer_event_class) {
257 BT_LOGE_STR("Failed to copy event_class.");
258 ret = BT_COMPONENT_STATUS_ERROR;
259 goto error;
260 }
261
262 int_ret = bt_stream_class_add_event_class(writer_stream_class,
263 writer_event_class);
264 if (int_ret < 0) {
265 BT_LOGE_STR("Failed to add event class.");
266 ret = BT_COMPONENT_STATUS_ERROR;
267 goto error;
268 }
269 BT_PUT(writer_event_class);
270 BT_PUT(event_class);
271 }
272
273 goto end;
274
275 error:
276 bt_put(event_class);
277 bt_put(writer_event_class);
278 end:
279 return ret;
280 }
281
282 BT_HIDDEN
283 struct bt_stream_class *ctf_copy_stream_class(FILE *err,
284 struct bt_stream_class *stream_class,
285 struct bt_trace *writer_trace,
286 bool override_ts64)
287 {
288 struct bt_field_type *type = NULL;
289 struct bt_stream_class *writer_stream_class = NULL;
290 int ret_int;
291 const char *name = bt_stream_class_get_name(stream_class);
292
293 writer_stream_class = bt_stream_class_create_empty(name);
294 assert(writer_stream_class);
295
296 type = bt_stream_class_get_packet_context_type(stream_class);
297 if (type) {
298 ret_int = bt_stream_class_set_packet_context_type(
299 writer_stream_class, type);
300 if (ret_int < 0) {
301 BT_LOGE_STR("Failed to set packet_context type.");
302 goto error;
303 }
304 BT_PUT(type);
305 }
306
307 type = bt_stream_class_get_event_header_type(stream_class);
308 if (type) {
309 ret_int = bt_trace_get_clock_class_count(writer_trace);
310 assert(ret_int >= 0);
311 if (override_ts64 && ret_int > 0) {
312 struct bt_field_type *new_event_header_type;
313
314 new_event_header_type = override_header_type(err, type,
315 writer_trace);
316 if (!new_event_header_type) {
317 BT_LOGE_STR("Failed to override header type.");
318 goto error;
319 }
320 ret_int = bt_stream_class_set_event_header_type(
321 writer_stream_class, new_event_header_type);
322 BT_PUT(new_event_header_type);
323 if (ret_int < 0) {
324 BT_LOGE_STR("Failed to set event_header type.");
325 goto error;
326 }
327 } else {
328 ret_int = bt_stream_class_set_event_header_type(
329 writer_stream_class, type);
330 if (ret_int < 0) {
331 BT_LOGE_STR("Failed to set event_header type.");
332 goto error;
333 }
334 }
335 BT_PUT(type);
336 }
337
338 type = bt_stream_class_get_event_context_type(stream_class);
339 if (type) {
340 ret_int = bt_stream_class_set_event_context_type(
341 writer_stream_class, type);
342 if (ret_int < 0) {
343 BT_LOGE_STR("Failed to set event_contexttype.");
344 goto error;
345 }
346 }
347 BT_PUT(type);
348
349 goto end;
350
351 error:
352 BT_PUT(writer_stream_class);
353 end:
354 bt_put(type);
355 return writer_stream_class;
356 }
357
358 BT_HIDDEN
359 int ctf_stream_copy_packet_header(FILE *err, struct bt_packet *packet,
360 struct bt_stream *writer_stream)
361 {
362 struct bt_field *packet_header = NULL, *writer_packet_header = NULL;
363 int ret = 0;
364
365 packet_header = bt_packet_get_header(packet);
366 if (!packet_header) {
367 goto end;
368 }
369
370 writer_packet_header = bt_field_copy(packet_header);
371 if (!writer_packet_header) {
372 BT_LOGE_STR("Failed to copy field from stream packet header.");
373 goto error;
374 }
375
376 ret = bt_stream_set_packet_header(writer_stream,
377 writer_packet_header);
378 if (ret) {
379 BT_LOGE_STR("Failed to set stream packet header.");
380 goto error;
381 }
382
383 goto end;
384
385 error:
386 ret = -1;
387 end:
388 bt_put(writer_packet_header);
389 bt_put(packet_header);
390 return ret;
391 }
392
393 BT_HIDDEN
394 int ctf_packet_copy_header(FILE *err, struct bt_packet *packet,
395 struct bt_packet *writer_packet)
396 {
397 struct bt_field *packet_header = NULL, *writer_packet_header = NULL;
398 int ret = 0;
399
400 packet_header = bt_packet_get_header(packet);
401 if (!packet_header) {
402 goto end;
403 }
404
405 writer_packet_header = bt_field_copy(packet_header);
406 if (!writer_packet_header) {
407 BT_LOGE_STR("Failed to copy field from packet header.");
408 goto error;
409 }
410
411 ret = bt_packet_set_header(writer_packet, writer_packet_header);
412 if (ret) {
413 BT_LOGE_STR("Failed to set packet header.");
414 goto error;
415 }
416
417 goto end;
418
419 error:
420 ret = -1;
421 end:
422 bt_put(packet_header);
423 bt_put(writer_packet_header);
424 return ret;
425 }
426
427 BT_HIDDEN
428 int ctf_stream_copy_packet_context(FILE *err, struct bt_packet *packet,
429 struct bt_stream *writer_stream)
430 {
431 struct bt_field *packet_context = NULL, *writer_packet_context = NULL;
432 int ret = 0;
433
434 packet_context = bt_packet_get_context(packet);
435 if (!packet_context) {
436 goto end;
437 }
438
439 writer_packet_context = bt_field_copy(packet_context);
440 if (!writer_packet_context) {
441 BT_LOGE_STR("Failed to copy field from stream packet context.");
442 goto error;
443 }
444
445 ret = bt_stream_set_packet_context(writer_stream,
446 writer_packet_context);
447 if (ret) {
448 BT_LOGE_STR("Failed to set stream packet context.");
449 goto error;
450 }
451
452 goto end;
453
454 error:
455 ret = -1;
456 end:
457 bt_put(packet_context);
458 bt_put(writer_packet_context);
459 return ret;
460 }
461
462 BT_HIDDEN
463 int ctf_packet_copy_context(FILE *err, struct bt_packet *packet,
464 struct bt_stream *writer_stream,
465 struct bt_packet *writer_packet)
466 {
467 struct bt_field *packet_context = NULL, *writer_packet_context = NULL;
468 int ret = 0;
469
470 packet_context = bt_packet_get_context(packet);
471 if (!packet_context) {
472 goto end;
473 }
474
475 writer_packet_context = bt_field_copy(packet_context);
476 if (!writer_packet_context) {
477 BT_LOGE_STR("Failed to copy field from packet context.");
478 goto error;
479 }
480
481 ret = bt_packet_set_context(writer_packet, writer_packet_context);
482 if (ret) {
483 BT_LOGE_STR("Failed to set packet context.");
484 goto error;
485 }
486
487 goto end;
488
489 error:
490 ret = -1;
491 end:
492 bt_put(writer_packet_context);
493 bt_put(packet_context);
494 return ret;
495 }
496
497 BT_HIDDEN
498 int ctf_copy_event_header(FILE *err, struct bt_event *event,
499 struct bt_event_class *writer_event_class,
500 struct bt_event *writer_event,
501 struct bt_field *event_header)
502 {
503 struct bt_clock_class *clock_class = NULL, *writer_clock_class = NULL;
504 struct bt_clock_value *clock_value = NULL, *writer_clock_value = NULL;
505
506 int ret;
507 struct bt_field *writer_event_header = NULL;
508 uint64_t value;
509
510 clock_class = event_get_clock_class(err, event);
511 if (!clock_class) {
512 BT_LOGE_STR("Failed to get event clock_class.");
513 goto error;
514 }
515
516 clock_value = bt_event_get_clock_value(event, clock_class);
517 BT_PUT(clock_class);
518 assert(clock_value);
519
520 ret = bt_clock_value_get_value(clock_value, &value);
521 BT_PUT(clock_value);
522 if (ret) {
523 BT_LOGE_STR("Failed to get clock value.");
524 goto error;
525 }
526
527 writer_clock_class = event_get_clock_class(err, writer_event);
528 if (!writer_clock_class) {
529 BT_LOGE_STR("Failed to get event clock_class.");
530 goto error;
531 }
532
533 writer_clock_value = bt_clock_value_create(writer_clock_class, value);
534 BT_PUT(writer_clock_class);
535 if (!writer_clock_value) {
536 BT_LOGE_STR("Failed to create clock value.");
537 goto error;
538 }
539
540 ret = bt_event_set_clock_value(writer_event, writer_clock_value);
541 BT_PUT(writer_clock_value);
542 if (ret) {
543 BT_LOGE_STR("Failed to set clock value.");
544 goto error;
545 }
546
547 writer_event_header = bt_field_copy(event_header);
548 if (!writer_event_header) {
549 BT_LOGE_STR("Failed to copy event_header.");
550 goto end;
551 }
552
553 ret = bt_event_set_header(writer_event, writer_event_header);
554 BT_PUT(writer_event_header);
555 if (ret < 0) {
556 BT_LOGE_STR("Failed to set event_header.");
557 goto error;
558 }
559
560 ret = 0;
561
562 goto end;
563
564 error:
565 ret = -1;
566 end:
567 return ret;
568 }
569
570 static
571 struct bt_trace *event_class_get_trace(FILE *err,
572 struct bt_event_class *event_class)
573 {
574 struct bt_trace *trace = NULL;
575 struct bt_stream_class *stream_class = NULL;
576
577 stream_class = bt_event_class_get_stream_class(event_class);
578 assert(stream_class);
579
580 trace = bt_stream_class_get_trace(stream_class);
581 assert(trace);
582
583 bt_put(stream_class);
584 return trace;
585 }
586
587 BT_HIDDEN
588 struct bt_event *ctf_copy_event(FILE *err, struct bt_event *event,
589 struct bt_event_class *writer_event_class,
590 bool override_ts64)
591 {
592 struct bt_event *writer_event = NULL;
593 struct bt_field *field = NULL, *copy_field = NULL;
594 struct bt_trace *writer_trace = NULL;
595 int ret;
596
597 writer_event = bt_event_create(writer_event_class);
598 if (!writer_event) {
599 BT_LOGE_STR("Failed to create event.");
600 goto error;
601 }
602
603 writer_trace = event_class_get_trace(err, writer_event_class);
604 if (!writer_trace) {
605 BT_LOGE_STR("Failed to get trace from event_class.");
606 goto error;
607 }
608
609 field = bt_event_get_header(event);
610 if (field) {
611 /*
612 * If override_ts64, we override all integer fields mapped to a
613 * clock to a uint64_t field type, otherwise, we just copy it as
614 * is.
615 */
616 ret = bt_trace_get_clock_class_count(writer_trace);
617 assert(ret >= 0);
618
619 if (override_ts64 && ret > 0) {
620 copy_field = bt_event_get_header(writer_event);
621 assert(copy_field);
622
623 ret = copy_override_field(err, event, writer_event, field,
624 copy_field);
625 if (ret) {
626 BT_LOGE_STR("Failed to copy and override field.");
627 goto error;
628 }
629 BT_PUT(copy_field);
630 } else {
631 ret = ctf_copy_event_header(err, event, writer_event_class,
632 writer_event, field);
633 if (ret) {
634 BT_LOGE_STR("Failed to copy event_header.");
635 goto error;
636 }
637 }
638 BT_PUT(field);
639 }
640
641 /* Optional field, so it can fail silently. */
642 field = bt_event_get_stream_event_context(event);
643 if (field) {
644 copy_field = bt_field_copy(field);
645 if (!copy_field) {
646 BT_LOGE_STR("Failed to copy field.");
647 goto error;
648 }
649 ret = bt_event_set_stream_event_context(writer_event,
650 copy_field);
651 if (ret < 0) {
652 BT_LOGE_STR("Failed to set stream_event_context.");
653 goto error;
654 }
655 BT_PUT(field);
656 BT_PUT(copy_field);
657 }
658
659 /* Optional field, so it can fail silently. */
660 field = bt_event_get_event_context(event);
661 if (field) {
662 copy_field = bt_field_copy(field);
663 if (!copy_field) {
664 BT_LOGE_STR("Failed to copy field.");
665 goto error;
666 }
667 ret = bt_event_set_event_context(writer_event, copy_field);
668 if (ret < 0) {
669 BT_LOGE_STR("Failed to set event_context.");
670 goto error;
671 }
672 BT_PUT(field);
673 BT_PUT(copy_field);
674 }
675
676 field = bt_event_get_event_payload(event);
677 if (field) {
678 copy_field = bt_field_copy(field);
679 if (!copy_field) {
680 BT_LOGE_STR("Failed to copy field.");
681 goto error;
682 }
683 ret = bt_event_set_event_payload(writer_event, copy_field);
684 if (ret < 0) {
685 BT_LOGE_STR("Failed to set event_payload.");
686 goto error;
687 }
688 BT_PUT(field);
689 BT_PUT(copy_field);
690 }
691
692 goto end;
693
694 error:
695 BT_PUT(writer_event);
696 end:
697 bt_put(field);
698 bt_put(copy_field);
699 bt_put(writer_trace);
700 return writer_event;
701 }
702
703 BT_HIDDEN
704 enum bt_component_status ctf_copy_trace(FILE *err, struct bt_trace *trace,
705 struct bt_trace *writer_trace)
706 {
707 enum bt_component_status ret = BT_COMPONENT_STATUS_OK;
708 int field_count, i, int_ret;
709 struct bt_field_type *header_type = NULL;
710 enum bt_byte_order order;
711 const char *trace_name;
712 const unsigned char *trace_uuid;
713
714 field_count = bt_trace_get_environment_field_count(trace);
715 for (i = 0; i < field_count; i++) {
716 int ret_int;
717 const char *name;
718 struct bt_value *value = NULL;
719
720 name = bt_trace_get_environment_field_name_by_index(
721 trace, i);
722 assert(name);
723
724 value = bt_trace_get_environment_field_value_by_index(
725 trace, i);
726 assert(value);
727
728 ret_int = bt_trace_set_environment_field(writer_trace,
729 name, value);
730 BT_PUT(value);
731 if (ret_int < 0) {
732 BT_LOGE("Failed to set environment: field-name=\"%s\"",
733 name);
734 ret = BT_COMPONENT_STATUS_ERROR;
735 goto end;
736 }
737 }
738
739 order = bt_trace_get_native_byte_order(trace);
740 assert(order != BT_BYTE_ORDER_UNKNOWN);
741
742 /*
743 * Only explicitly set the writer trace's native byte order if
744 * the original trace has a specific one. Otherwise leave what
745 * the CTF writer object chooses, which is the machine's native
746 * byte order.
747 */
748 if (order != BT_BYTE_ORDER_UNSPECIFIED) {
749 ret = bt_trace_set_native_byte_order(writer_trace, order);
750 if (ret) {
751 BT_LOGE_STR("Failed to set native byte order.");
752 ret = BT_COMPONENT_STATUS_ERROR;
753 goto end;
754 }
755 }
756
757 header_type = bt_trace_get_packet_header_type(trace);
758 if (header_type) {
759 int_ret = bt_trace_set_packet_header_type(writer_trace, header_type);
760 BT_PUT(header_type);
761 if (int_ret < 0) {
762 BT_LOGE_STR("Failed to set packet header type.");
763 ret = BT_COMPONENT_STATUS_ERROR;
764 goto end;
765 }
766 }
767
768 trace_name = bt_trace_get_name(trace);
769 if (trace_name) {
770 int_ret = bt_trace_set_name(writer_trace, trace_name);
771 if (int_ret < 0) {
772 BT_LOGE_STR("Failed to set trace name.");
773 ret = BT_COMPONENT_STATUS_ERROR;
774 goto end;
775 }
776 }
777
778 trace_uuid = bt_trace_get_uuid(trace);
779 if (trace_uuid) {
780 int_ret = bt_trace_set_uuid(writer_trace, trace_uuid);
781 if (int_ret < 0) {
782 BT_LOGE_STR("Failed to set trace UUID.");
783 ret = BT_COMPONENT_STATUS_ERROR;
784 goto end;
785 }
786 }
787
788 end:
789 return ret;
790 }
This page took 0.046647 seconds and 4 git commands to generate.