source: trunk/mondo/src/common/libmondo-fork.c @ 900

Last change on this file since 900 was 900, checked in by Bruno Cornec, 14 years ago

Huge patch to introduce low level functions that will bw used everywhere (mr_free, mr_asprintf, ...)
Nearly linking now due to that.

  • Property svn:keywords set to Id
File size: 18.4 KB
Line 
1/* libmondo-fork.c - subroutines for handling forking/pthreads/etc.
2 * $Id: libmondo-fork.c 900 2006-10-24 06:49:18Z bruno $
3 */
4
5#include "my-stuff.h"
6#include "mondostructures.h"
7#include "libmondo-fork.h"
8#include "libmondo-string-EXT.h"
9#include "newt-specific-EXT.h"
10#include "libmondo-files-EXT.h"
11#include "libmondo-tools-EXT.h"
12#include "mr_mem.h"
13
14extern bool g_text_mode;
15pid_t g_buffer_pid = 0;
16
17
18/**
19 * Call a program and retrieve its last line of output.
20 * @param call The program to run.
21 * @return The last line of its output.
22 * @note The returned value should be freed by caller
23 */
24char *call_program_and_get_last_line_of_output(char *call)
25{
26    char *result = NULL;
27    FILE *fin = NULL;
28    size_t n = 0;
29
30    assert_string_is_neither_NULL_nor_zerolength(call);
31
32    if ((fin = popen(call, "r"))) {
33        for (mr_getline(&result, &n, fin); !feof(fin); mr_getline(&result, &n, fin));
34        paranoid_pclose(fin);
35    } else {
36        log_OS_error("Unable to popen call");
37    }
38    return(result);
39}
40
41#define MONDO_POPMSG  _("Your PC will not retract the CD tray automatically. Please call mondoarchive with the -m (manual CD tray) flag.")
42
43/**
44 * Call mkisofs to create an ISO image.
45 * @param bkpinfo The backup information structure. Fields used:
46 * - @c bkpinfo->manual_cd_tray
47 * - @c bkpinfo->backup_media_type
48 * - @c bkpinfo->please_dont_eject_when_restoring
49 * @param basic_call The call to mkisofs. May contain tokens that will be resolved to actual data. The tokens are:
50 * - @c _ISO_ will become the ISO file (@p isofile)
51 * - @c _CD#_ becomes the CD number (@p cd_no)
52 * - @c _ERR_ becomes the logfile (@p g_logfile)
53 * @param isofile Replaces @c _ISO_ in @p basic_call. Should probably be the ISO image to create (-o parameter to mkisofs).
54 * @param cd_no Replaces @c _CD#_ in @p basic_call. Should probably be the CD number.
55 * @param logstub Unused.
56 * @param what_i_am_doing The action taking place (e.g. "Making ISO #1"). Used as the title of the progress dialog.
57 * @return Exit code of @c mkisofs (0 is success, anything else indicates failure).
58 * @bug @p logstub is unused.
59 */
60int
61eval_call_to_make_ISO(struct s_bkpinfo *bkpinfo,
62                      char *basic_call, char *isofile,
63                      int cd_no, char *logstub, char *what_i_am_doing)
64{
65
66    /*@ int's  *** */
67    int retval = 0;
68
69
70    /*@ buffers      *** */
71    char *midway_call, *ultimate_call, *tmp, *command,
72        *cd_number_str;
73    char *p;
74
75/*@***********   End Variables ***************************************/
76
77    log_msg(3, "Starting");
78    assert(bkpinfo != NULL);
79    assert_string_is_neither_NULL_nor_zerolength(basic_call);
80    assert_string_is_neither_NULL_nor_zerolength(isofile);
81    assert_string_is_neither_NULL_nor_zerolength(logstub);
82    if (!(midway_call = malloc(1200))) {
83        fatal_error("Cannot malloc midway_call");
84    }
85    if (!(ultimate_call = malloc(1200))) {
86        fatal_error("Cannot malloc ultimate_call");
87    }
88    if (!(tmp = malloc(1200))) {
89        fatal_error("Cannot malloc tmp");
90    }
91
92    mr_asprintf(&cd_number_str, "%d", cd_no);
93    resolve_naff_tokens(midway_call, basic_call, isofile, "_ISO_");
94    resolve_naff_tokens(tmp, midway_call, cd_number_str, "_CD#_");
95    mr_free(cd_number_str);
96
97    resolve_naff_tokens(ultimate_call, tmp, MONDO_LOGFILE, "_ERR_");
98    log_msg(4, "basic call = '%s'", basic_call);
99    log_msg(4, "midway_call = '%s'", midway_call);
100    log_msg(4, "tmp = '%s'", tmp);
101    log_msg(4, "ultimate call = '%s'", ultimate_call);
102    mr_asprintf(&command, "%s >> %s", ultimate_call, MONDO_LOGFILE);
103
104    log_to_screen
105        (_("Please be patient. Do not be alarmed by on-screen inactivity."));
106    log_msg(4, "Calling open_evalcall_form() with what_i_am_doing='%s'",
107            what_i_am_doing);
108    if (bkpinfo->manual_cd_tray) {
109        /* Find everything after a 2>> and remove it */
110        p = strstr(command, "2>>");
111        if (p) {
112            for (; *p != ' ' && *p != '\0'; p++) {
113                *p = ' ';
114            }
115        }
116#ifndef _XWIN
117        if (!g_text_mode) {
118            newtSuspend();
119        }
120#endif
121        log_msg(1, "command = '%s'", command);
122        retval += system(command);
123        if (!g_text_mode) {
124            newtResume();
125        }
126        if (retval) {
127            log_msg(2, "Basic call '%s' returned an error.", basic_call);
128            popup_and_OK(_("Press ENTER to continue."));
129            popup_and_OK
130                (_("mkisofs and/or cdrecord returned an error. CD was not created"));
131        }
132    }
133    /* if text mode then do the above & RETURN; if not text mode, do this... */
134    else {
135        log_msg(3, "command = '%s'", command);
136//      yes_this_is_a_goto:
137        retval =
138            run_external_binary_with_percentage_indicator_NEW
139            (what_i_am_doing, command);
140    }
141    mr_free(command);
142
143    mr_free(midway_call);
144    mr_free(ultimate_call);
145    mr_free(tmp);
146    return (retval);
147}
148
149
150/**
151 * Run a program and log its output (stdout and stderr) to the logfile.
152 * @param program The program to run. Passed to the shell, so you can use pipes etc.
153 * @param debug_level If @p g_loglevel is higher than this, do not log the output.
154 * @return The exit code of @p program (depends on the command, but 0 almost always indicates success).
155 */
156int run_program_and_log_output(char *program, int debug_level)
157{
158    /*@ buffer ****************************************************** */
159    char *callstr = NULL;
160    char *incoming = NULL;
161
162    /*@ int ********************************************************* */
163    int res = 0;
164    size_t n = 0;
165    bool log_if_failure = FALSE;
166    bool log_if_success = FALSE;
167
168    /*@ pointers *************************************************** */
169    FILE *fin;
170    char *p;
171
172    /*@ end vars *************************************************** */
173
174    assert(program != NULL);
175    if (!program[0]) {
176        log_msg(2, "Warning - asked to run zerolength program");
177        return (1);
178    }
179
180    if (debug_level <= g_loglevel) {
181        log_if_success = TRUE;
182        log_if_failure = TRUE;
183    }
184    mr_asprintf(&callstr,
185            "%s > /tmp/mondo-run-prog-thing.tmp 2> /tmp/mondo-run-prog-thing.err",
186            program);
187    while ((p = strchr(callstr, '\r'))) {
188        *p = ' ';
189    }
190    while ((p = strchr(callstr, '\n'))) {
191        *p = ' ';
192    }                           /* single '=' is intentional */
193
194
195    res = system(callstr);
196    if (((res == 0) && log_if_success) || ((res != 0) && log_if_failure)) {
197        log_msg(0, "running: %s", callstr);
198        log_msg(0,
199                "--------------------------------start of output-----------------------------");
200    }
201    mr_free(callstr);
202
203    if (log_if_failure
204        &&
205        system
206        ("cat /tmp/mondo-run-prog-thing.err >> /tmp/mondo-run-prog-thing.tmp 2> /dev/null"))
207    {
208        log_OS_error("Command failed");
209    }
210    unlink("/tmp/mondo-run-prog-thing.err");
211    fin = fopen("/tmp/mondo-run-prog-thing.tmp", "r");
212    if (fin) {
213        for (mr_getline(&incoming, &n, fin); !feof(fin);
214             mr_getline(&incoming, &n, fin)) {
215            /* patch by Heiko Schlittermann */
216            p = incoming;
217            while (p && *p) {
218                if ((p = strchr(p, '%'))) {
219                    memmove(p, p + 1, strlen(p) + 1);
220                    p += 2;
221                }
222            }
223            /* end of patch */
224            strip_spaces(incoming);
225            if ((res == 0 && log_if_success)
226                || (res != 0 && log_if_failure)) {
227                log_msg(0, incoming);
228            }
229        }
230        mr_free(incoming);
231        paranoid_fclose(fin);
232    }
233    unlink("/tmp/mondo-run-prog-thing.tmp");
234    if ((res == 0 && log_if_success) || (res != 0 && log_if_failure)) {
235        log_msg(0,
236                "--------------------------------end of output------------------------------");
237        if (res) {
238            log_msg(0, "...ran with res=%d", res);
239        } else {
240            log_msg(0, "...ran just fine. :-)");
241        }
242    }
243//  else
244//    { log_msg (0, "-------------------------------ran w/ res=%d------------------------------", res); }
245    return (res);
246}
247
248
249/**
250 * Run a program and log its output to the screen.
251 * @param basic_call The program to run.
252 * @param what_i_am_doing The title of the evalcall form.
253 * @return The return value of the command (varies, but 0 almost always means success).
254 * @see run_program_and_log_output
255 * @see log_to_screen
256 */
257int run_program_and_log_to_screen(char *basic_call, char *what_i_am_doing)
258{
259    /*@ int ******************************************************** */
260    int retval = 0;
261    int res = 0;
262    int i;
263
264    /*@ pointers **************************************************** */
265    FILE *fin;
266
267    /*@ buffers **************************************************** */
268    char *tmp;
269    char *command;
270    char *lockfile;
271
272    /*@ end vars *************************************************** */
273
274    assert_string_is_neither_NULL_nor_zerolength(basic_call);
275
276    mr_asprintf(&lockfile, "/tmp/mojo-jojo.blah.XXXXXX");
277    mkstemp(lockfile);
278    mr_asprintf(&command,
279            "echo hi > %s ; %s >> %s 2>> %s; res=$?; sleep 1; rm -f %s; exit $res",
280            lockfile, basic_call, MONDO_LOGFILE, MONDO_LOGFILE, lockfile);
281    open_evalcall_form(what_i_am_doing);
282    mr_asprintf(&tmp, "Executing %s", basic_call);
283    log_msg(2, tmp);
284    mr_free(tmp);
285
286    if (!(fin = popen(command, "r"))) {
287        log_OS_error("Unable to popen-in command");
288        mr_asprintf(&tmp, _("Failed utterly to call '%s'"), command);
289        log_to_screen(tmp);
290        mr_free(tmp);
291        mr_free(lockfile);
292        mr_free(command);
293        return (1);
294    }
295    mr_free(command);
296
297    if (!does_file_exist(lockfile)) {
298        log_to_screen(_("Waiting for external binary to start"));
299        for (i = 0; i < 60 && !does_file_exist(lockfile); sleep(1), i++) {
300            log_msg(3, "Waiting for lockfile %s to exist", lockfile);
301        }
302    }
303    /* This works on Newt, and it gives quicker updates. */
304    for (; does_file_exist(lockfile); sleep(1)) {
305        log_file_end_to_screen(MONDO_LOGFILE, "");
306        update_evalcall_form(1);
307    }
308    /* Evaluate the status returned by pclose to get the exit code of the called program. */
309    errno = 0;
310    res = pclose(fin);
311    /* Log actual pclose errors. */
312    if (errno)
313        log_msg(5, "pclose err: %d", errno);
314    /* Check if we have a valid status. If we do, extract the called program's exit code. */
315    /* If we don't, highlight this fact by returning -1. */
316    if (WIFEXITED(res)) {
317        retval = WEXITSTATUS(res);
318    } else {
319        retval = -1;
320    }
321    close_evalcall_form();
322    unlink(lockfile);
323    mr_free(lockfile);
324
325    return (retval);
326}
327
328
329/**
330 * Apparently used. @bug This has a purpose, but what?
331 */
332#define PIMP_START_SZ "STARTSTARTSTART9ff3kff9a82gv34r7fghbkaBBC2T231hc81h42vws8"
333#define PIMP_END_SZ "ENDENDEND0xBBC10xBBC2T231hc81h42vws89ff3kff9a82gv34r7fghbka"
334
335
336
337
338int copy_from_src_to_dest(FILE * f_orig, FILE * f_archived, char direction)
339{
340// if dir=='w' then copy from orig to archived
341// if dir=='r' then copy from archived to orig
342    char *tmp = NULL;
343    char *buf = NULL;
344    long int bytes_to_be_read, bytes_read_in, bytes_written_out =
345        0, bufcap, subsliceno = 0;
346    int retval = 0;
347    FILE *fin = NULL;
348    FILE *fout = NULL;
349    FILE *ftmp = NULL;
350
351    log_msg(5, "Opening.");
352    bufcap = 256L * 1024L;
353    if (!(buf = malloc(bufcap))) {
354        fatal_error("Failed to malloc() buf");
355    }
356
357    if (direction == 'w') {
358        fin = f_orig;
359        fout = f_archived;
360        mr_asprintf(&tmp, "%-64s", PIMP_START_SZ);
361        if (fwrite(tmp, 1, 64, fout) != 64) {
362            fatal_error("Can't write the introductory block");
363        }
364        mr_free(tmp);
365
366        while (1) {
367            bytes_to_be_read = bytes_read_in = fread(buf, 1, bufcap, fin);
368            if (bytes_read_in == 0) {
369                break;
370            }
371            mr_asprintf(&tmp, "%-64ld", bytes_read_in);
372            if (fwrite(tmp, 1, 64, fout) != 64) {
373                fatal_error("Cannot write introductory block");
374            }
375            mr_free(tmp);
376
377            log_msg(7,
378                    "subslice #%ld --- I have read %ld of %ld bytes in from f_orig",
379                    subsliceno, bytes_read_in, bytes_to_be_read);
380            bytes_written_out += fwrite(buf, 1, bytes_read_in, fout);
381            mr_asprintf(&tmp, "%-64ld", subsliceno);
382            if (fwrite(tmp, 1, 64, fout) != 64) {
383                fatal_error("Cannot write post-thingy block");
384            }
385            mr_free(tmp);
386
387            log_msg(7, "Subslice #%d written OK", subsliceno);
388            subsliceno++;
389        }
390        mr_asprintf(&tmp, "%-64ld", 0L);
391        if (fwrite(tmp, 1, 64L, fout) != 64L) {
392            fatal_error("Cannot write final introductory block");
393        }
394    } else {
395        fin = f_archived;
396        fout = f_orig;
397        if (!(tmp = malloc(64L))) {
398            fatal_error("Failed to malloc() tmp");
399        }
400        if (fread(tmp, 1, 64L, fin) != 64L) {
401            fatal_error("Cannot read the introductory block");
402        }
403        log_msg(5, "tmp is %s", tmp);
404        if (!strstr(tmp, PIMP_START_SZ)) {
405            fatal_error("Can't find intro blk");
406        }
407        if (fread(tmp, 1, 64L, fin) != 64L) {
408            fatal_error("Cannot read introductory blk");
409        }
410        bytes_to_be_read = atol(tmp);
411        while (bytes_to_be_read > 0) {
412            log_msg(7, "subslice#%ld, bytes=%ld", subsliceno,
413                    bytes_to_be_read);
414            bytes_read_in = fread(buf, 1, bytes_to_be_read, fin);
415            if (bytes_read_in != bytes_to_be_read) {
416                fatal_error
417                    ("Danger, WIll Robinson. Failed to read whole subvol from archives.");
418            }
419            bytes_written_out += fwrite(buf, 1, bytes_read_in, fout);
420            if (fread(tmp, 1, 64L, fin) != 64L) {
421                fatal_error("Cannot read post-thingy block");
422            }
423            if (atol(tmp) != subsliceno) {
424                log_msg(1, "Wanted subslice %ld but got %ld ('%s')",
425                        subsliceno, atol(tmp), tmp);
426            }
427            log_msg(7, "Subslice #%ld read OK", subsliceno);
428            subsliceno++;
429            if (fread(tmp, 1, 64L, fin) != 64L) {
430                fatal_error("Cannot read introductory block");
431            }
432            bytes_to_be_read = atol(tmp);
433        }
434    }
435
436//  log_msg(4, "Written %ld of %ld bytes", bytes_written_out, bytes_read_in);
437
438    if (direction == 'w') {
439        mr_free(tmp);
440        mr_asprintf(&tmp, "%-64s", PIMP_END_SZ);
441        if (fwrite(tmp, 1, 64L, fout) != 64L) {
442            fatal_error("Can't write the final block");
443        }
444        mr_free(tmp);
445    } else {
446        log_msg(1, "tmpA is %s", tmp);
447        if (!strstr(tmp, PIMP_END_SZ)) {
448            if (fread(tmp, 1, 64L, fin) != 64L) {
449                fatal_error("Can't read the final block");
450            }
451            log_msg(5, "tmpB is %s", tmp);
452            if (!strstr(tmp, PIMP_END_SZ)) {
453                ftmp = fopen("/tmp/out.leftover", "w");
454                bytes_read_in = fread(tmp, 1, 64L, fin);
455                log_msg(1, "bytes_read_in = %ld", bytes_read_in);
456//      if (bytes_read_in!=128+64) { fatal_error("Can't read the terminating block"); }
457                fwrite(tmp, 1, bytes_read_in, ftmp);
458               
459                mr_free(tmp);
460                if (!(tmp = malloc(512))) {
461                    fatal_error("Failed to malloc() tmp");
462                }
463                /* BERLIOS : strange ???
464                s-printf(tmp, "I am here - %llu", ftello(fin));
465                log_msg(0, tmp);
466                */
467                fread(tmp, 1, 512, fin);
468                log_msg(0, "tmp = '%s'", tmp);
469                fwrite(tmp, 1, 512, ftmp);
470                fclose(ftmp);
471                fatal_error("Missing terminating block");
472            }
473        }
474        mr_free(tmp);
475    }
476
477    mr_free(buf);
478    log_msg(3, "Successfully copied %ld bytes", bytes_written_out);
479    return (retval);
480}
481
482/**
483 * Feed @p input_device through ntfsclone to @p output_fname.
484 * @param input_device The device to image.
485 * @param output_fname The file to write.
486 * @return 0 for success, nonzero for failure.
487 */
488int feed_into_ntfsprog(char *input_device, char *output_fname)
489{
490// BACKUP
491    int res = -1;
492    char *command = NULL;
493    char *tmp = NULL;
494
495    if (!does_file_exist(input_device)) {
496        fatal_error("input device does not exist");
497    }
498    tmp = find_home_of_exe("ntfsclone");
499    if (!tmp) {
500        fatal_error("ntfsclone not found");
501    }
502    mr_free(tmp);
503
504    mr_asprintf(&command, "ntfsclone --force --save-image --overwrite %s %s", output_fname, input_device);
505    res = run_program_and_log_output(command, 5);
506    mr_free(command);
507
508    unlink(output_fname);
509    return (res);
510}
511
512
513int run_external_binary_with_percentage_indicator_OLD(char *tt, char *cmd)
514{
515
516    int res = 0;
517    int percentage = 0;
518    int maxpc = 0;
519    int pcno = 0;
520    int last_pcno = 0;
521
522    char *command = NULL;
523    char *tempfile = NULL;
524    FILE *pin = NULL;
525
526    assert_string_is_neither_NULL_nor_zerolength(cmd);
527
528    tempfile = call_program_and_get_last_line_of_output("mktemp -q /tmp/mondo.XXXXXXXX");
529    mr_asprintf(&command, "%s >> %s 2>> %s; rm -f %s", cmd, tempfile, tempfile,
530            tempfile);
531    log_msg(3, command);
532    open_evalcall_form(tt);
533
534    if (!(pin = popen(command, "r"))) {
535        log_OS_error("fmt err");
536        mr_free(command);
537        mr_free(tempfile);
538        return (1);
539    }
540    mr_free(command);
541
542    maxpc = 100;
543// OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD
544    for (sleep(1); does_file_exist(tempfile); sleep(1)) {
545        pcno = grab_percentage_from_last_line_of_file(MONDO_LOGFILE);
546        if (pcno < 0 || pcno > 100) {
547            log_msg(5, "Weird pc#");
548            continue;
549        }
550        percentage = pcno * 100 / maxpc;
551        if (pcno <= 5 && last_pcno > 40) {
552            close_evalcall_form();
553            open_evalcall_form("_(Verifying...");
554        }
555        last_pcno = pcno;
556        update_evalcall_form(percentage);
557    }
558// OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD OLD
559    close_evalcall_form();
560    if (pclose(pin)) {
561        res++;
562        log_OS_error("Unable to pclose");
563    }
564    unlink(tempfile);
565    mr_free(tempfile);
566    return (res);
567}
568
569
570void *run_prog_in_bkgd_then_exit(void *info)
571{
572    char *sz_command;
573    static int res = 4444;
574
575    res = 999;
576    sz_command = (char *) info;
577    log_msg(4, "sz_command = '%s'", sz_command);
578    res = system(sz_command);
579    if (res > 256 && res != 4444) {
580        res = res / 256;
581    }
582    log_msg(4, "child res = %d", res);
583    sz_command[0] = '\0';
584    pthread_exit((void *) (&res));
585}
586
587
588int run_external_binary_with_percentage_indicator_NEW(char *tt, char *cmd)
589{
590
591    /*@ int *************************************************************** */
592    int res = 0;
593    int percentage = 0;
594    int maxpc = 100;
595    int pcno = 0;
596    int last_pcno = 0;
597    int counter = 0;
598
599    /*@ buffers *********************************************************** */
600    char *command;
601    /*@ pointers ********************************************************** */
602    static int chldres = 0;
603    int *pchild_result;
604    pthread_t childthread;
605
606    pchild_result = &chldres;
607    assert_string_is_neither_NULL_nor_zerolength(cmd);
608    assert_string_is_neither_NULL_nor_zerolength(tt);
609    *pchild_result = 999;
610
611    mr_asprintf(&command, "%s 2>> %s", cmd, MONDO_LOGFILE);
612    log_msg(3, "command = '%s'", command);
613    if ((res =
614         pthread_create(&childthread, NULL, run_prog_in_bkgd_then_exit,
615                        (void *) command))) {
616        fatal_error("Unable to create an archival thread");
617    }
618
619    log_msg(8, "Parent running");
620    open_evalcall_form(tt);
621    for (sleep(1); command[0] != '\0'; sleep(1)) {
622        pcno = grab_percentage_from_last_line_of_file(MONDO_LOGFILE);
623        if (pcno <= 0 || pcno > 100) {
624            log_msg(8, "Weird pc#");
625            continue;
626        }
627        percentage = pcno * 100 / maxpc;
628        if (pcno <= 5 && last_pcno >= 40) {
629            close_evalcall_form();
630            open_evalcall_form(_("Verifying..."));
631        }
632        if (counter++ >= 5) {
633            counter = 0;
634            log_file_end_to_screen(MONDO_LOGFILE, "");
635        }
636        last_pcno = pcno;
637        update_evalcall_form(percentage);
638    }
639    mr_free(command);
640
641    log_file_end_to_screen(MONDO_LOGFILE, "");
642    close_evalcall_form();
643    pthread_join(childthread, (void *) (&pchild_result));
644    if (pchild_result) {
645        res = *pchild_result;
646    } else {
647        res = 666;
648    }
649    log_msg(3, "Parent res = %d", res);
650    return (res);
651}
652
653
654/**
655 * Feed @p input_fifo through ntfsclone (restore) to @p output_device.
656 * @param input_fifo The ntfsclone file to read.
657 * @param output_device Where to put the output.
658 * @return The return value of ntfsclone (0 for success).
659 */
660int feed_outfrom_ntfsprog(char *output_device, char *input_fifo)
661{
662// RESTORE
663    int res = -1;
664    char *command = NULL;
665    char *tmp = NULL;
666
667    tmp = find_home_of_exe("ntfsclone");
668    if (!tmp) {
669        fatal_error("ntfsclone not found");
670    }
671    mr_free(tmp);
672
673    mr_asprintf(&command, "ntfsclone --force --restore-image --overwrite %s %s", output_device, input_fifo);
674    res = run_program_and_log_output(command, 5);
675    mr_free(command);
676    return (res);
677}
Note: See TracBrowser for help on using the repository browser.