d8e11786b29842896e08c0fae4e6d7809fd7294c
[debian/amanda] / server-src / amflush.c
1 /*
2  * Amanda, The Advanced Maryland Automatic Network Disk Archiver
3  * Copyright (c) 1991-1998 University of Maryland at College Park
4  * All Rights Reserved.
5  *
6  * Permission to use, copy, modify, distribute, and sell this software and its
7  * documentation for any purpose is hereby granted without fee, provided that
8  * the above copyright notice appear in all copies and that both that
9  * copyright notice and this permission notice appear in supporting
10  * documentation, and that the name of U.M. not be used in advertising or
11  * publicity pertaining to distribution of the software without specific,
12  * written prior permission.  U.M. makes no representations about the
13  * suitability of this software for any purpose.  It is provided "as is"
14  * without express or implied warranty.
15  *
16  * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
18  * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
20  * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
21  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22  *
23  * Authors: the Amanda Development Team.  Its members are listed in a
24  * file named AUTHORS, in the root directory of this distribution.
25  */
26 /*
27  * $Id: amflush.c,v 1.95 2006/07/25 21:41:24 martinea Exp $
28  *
29  * write files from work directory onto tape
30  */
31 #include "amanda.h"
32
33 #include "match.h"
34 #include "conffile.h"
35 #include "diskfile.h"
36 #include "tapefile.h"
37 #include "logfile.h"
38 #include "clock.h"
39 #include "holding.h"
40 #include "driverio.h"
41 #include "server_util.h"
42 #include "timestamp.h"
43
44 static char *conf_logdir;
45 FILE *driver_stream;
46 char *driver_program;
47 char *reporter_program;
48 char *logroll_program;
49 char *datestamp;
50 char *amflush_timestamp;
51 char *amflush_datestamp;
52
53 /* local functions */
54 void flush_holdingdisk(char *diskdir, char *datestamp);
55 static GSList * pick_datestamp(void);
56 void confirm(GSList *datestamp_list);
57 void redirect_stderr(void);
58 void detach(void);
59 void run_dumps(void);
60 static int get_letter_from_user(void);
61
62 int
63 main(
64     int         argc,
65     char **     argv)
66 {
67     int foreground;
68     int batch;
69     int redirect;
70     char **datearg = NULL;
71     int nb_datearg = 0;
72     char *conf_diskfile;
73     char *conf_tapelist;
74     char *conf_logfile;
75     int conf_usetimestamps;
76     disklist_t diskq;
77     disk_t *dp;
78     pid_t pid;
79     pid_t driver_pid, reporter_pid;
80     amwait_t exitcode;
81     int opt;
82     GSList *holding_list=NULL, *holding_file;
83     int driver_pipe[2];
84     char date_string[100];
85     char date_string_standard[100];
86     time_t today;
87     char *errstr;
88     struct tm *tm;
89     char *tapedev;
90     char *tpchanger;
91     char *qdisk, *qhname;
92     GSList *datestamp_list = NULL;
93     config_overrides_t *cfg_ovr;
94     char **config_options;
95
96     /*
97      * Configure program for internationalization:
98      *   1) Only set the message locale for now.
99      *   2) Set textdomain for all amanda related programs to "amanda"
100      *      We don't want to be forced to support dozens of message catalogs.
101      */  
102     setlocale(LC_MESSAGES, "C");
103     textdomain("amanda"); 
104
105     safe_fd(-1, 0);
106     safe_cd();
107
108     set_pname("amflush");
109
110     /* Don't die when child closes pipe */
111     signal(SIGPIPE, SIG_IGN);
112
113     dbopen(DBG_SUBDIR_SERVER);
114
115     add_amanda_log_handler(amanda_log_stderr);
116     foreground = 0;
117     batch = 0;
118     redirect = 1;
119
120     /* process arguments */
121
122     cfg_ovr = new_config_overrides(argc/2);
123     while((opt = getopt(argc, argv, "bfso:D:")) != EOF) {
124         switch(opt) {
125         case 'b': batch = 1;
126                   break;
127         case 'f': foreground = 1;
128                   break;
129         case 's': redirect = 0;
130                   break;
131         case 'o': add_config_override_opt(cfg_ovr, optarg);
132                   break;
133         case 'D': if (datearg == NULL)
134                       datearg = alloc(21*SIZEOF(char *));
135                   if(nb_datearg == 20) {
136                       g_fprintf(stderr,_("maximum of 20 -D arguments.\n"));
137                       exit(1);
138                   }
139                   datearg[nb_datearg++] = stralloc(optarg);
140                   datearg[nb_datearg] = NULL;
141                   break;
142         }
143     }
144     argc -= optind, argv += optind;
145
146     if(!foreground && !redirect) {
147         g_fprintf(stderr,_("Can't redirect to stdout/stderr if not in forground.\n"));
148         exit(1);
149     }
150
151     if(argc < 1) {
152         error(_("Usage: amflush [-b] [-f] [-s] [-D date]* [-o configoption]* <confdir> [host [disk]* ]*"));
153         /*NOTREACHED*/
154     }
155
156     set_config_overrides(cfg_ovr);
157     config_init(CONFIG_INIT_EXPLICIT_NAME,
158                 argv[0]);
159
160     conf_diskfile = config_dir_relative(getconf_str(CNF_DISKFILE));
161     read_diskfile(conf_diskfile, &diskq);
162     amfree(conf_diskfile);
163
164     if (config_errors(NULL) >= CFGERR_WARNINGS) {
165         config_print_errors();
166         if (config_errors(NULL) >= CFGERR_ERRORS) {
167             g_critical(_("errors processing config file"));
168         }
169     }
170
171     check_running_as(RUNNING_AS_DUMPUSER);
172
173     dbrename(get_config_name(), DBG_SUBDIR_SERVER);
174
175     errstr = match_disklist(&diskq, argc-1, argv+1);
176     if (errstr) {
177         g_printf(_("%s"),errstr);
178         amfree(errstr);
179     }
180
181     conf_tapelist = config_dir_relative(getconf_str(CNF_TAPELIST));
182     if(read_tapelist(conf_tapelist)) {
183         error(_("could not load tapelist \"%s\""), conf_tapelist);
184         /*NOTREACHED*/
185     }
186     amfree(conf_tapelist);
187
188     conf_usetimestamps = getconf_boolean(CNF_USETIMESTAMPS);
189
190     amflush_datestamp = get_datestamp_from_time(0);
191     if(conf_usetimestamps == 0) {
192         amflush_timestamp = stralloc(amflush_datestamp);
193     }
194     else {
195         amflush_timestamp = get_timestamp_from_time(0);
196     }
197
198     conf_logdir = config_dir_relative(getconf_str(CNF_LOGDIR));
199     conf_logfile = vstralloc(conf_logdir, "/log", NULL);
200     if (access(conf_logfile, F_OK) == 0) {
201         run_amcleanup(get_config_name());
202     }
203     if (access(conf_logfile, F_OK) == 0) {
204         char *process_name = get_master_process(conf_logfile);
205         error(_("%s exists: %s is already running, or you must run amcleanup"), conf_logfile, process_name);
206         /*NOTREACHED*/
207     }
208
209     driver_program = vstralloc(amlibexecdir, "/", "driver", NULL);
210     reporter_program = vstralloc(sbindir, "/", "amreport", NULL);
211     logroll_program = vstralloc(amlibexecdir, "/", "amlogroll", NULL);
212
213     tapedev = getconf_str(CNF_TAPEDEV);
214     tpchanger = getconf_str(CNF_TPCHANGER);
215     if (tapedev == NULL && tpchanger == NULL) {
216         error(_("No tapedev or tpchanger specified"));
217     }
218
219     /* if dates were specified (-D), then use match_datestamp
220      * against the list of all datestamps to turn that list
221      * into a set of existing datestamps (basically, evaluate the
222      * expressions into actual datestamps) */
223     if(datearg) {
224         GSList *all_datestamps;
225         GSList *datestamp;
226         int i, ok;
227
228         all_datestamps = holding_get_all_datestamps();
229         for(datestamp = all_datestamps; datestamp != NULL; datestamp = datestamp->next) {
230             ok = 0;
231             for(i=0; i<nb_datearg && ok==0; i++) {
232                 ok = match_datestamp(datearg[i], (char *)datestamp->data);
233             }
234             if (ok)
235                 datestamp_list = g_slist_insert_sorted(datestamp_list,
236                     stralloc((char *)datestamp->data),
237                     g_compare_strings);
238         }
239         g_slist_free_full(all_datestamps);
240     }
241     else {
242         /* otherwise, in batch mode, use all datestamps */
243         if(batch) {
244             datestamp_list = holding_get_all_datestamps();
245         }
246         /* or allow the user to pick datestamps */
247         else {
248             datestamp_list = pick_datestamp();
249         }
250     }
251
252     if(!datestamp_list) {
253         g_printf(_("Could not find any Amanda directories to flush.\n"));
254         exit(1);
255     }
256
257     holding_list = holding_get_files_for_flush(datestamp_list);
258     if (holding_list == NULL) {
259         g_printf(_("Could not find any valid dump image, check directory.\n"));
260         exit(1);
261     }
262
263     if (access(conf_logfile, F_OK) == 0) {
264         char *process_name = get_master_process(conf_logfile);
265         error(_("%s exists: someone started %s"), conf_logfile, process_name);
266         /*NOTREACHED*/
267     }
268     log_add(L_INFO, "%s pid %ld", get_pname(), (long)getpid());
269
270     if(!batch) confirm(datestamp_list);
271
272     for(dp = diskq.head; dp != NULL; dp = dp->next) {
273         if(dp->todo) {
274             char *qname;
275             qname = quote_string(dp->name);
276             log_add(L_DISK, "%s %s", dp->host->hostname, qname);
277             amfree(qname);
278         }
279     }
280
281     if(!foreground) { /* write it before redirecting stdout */
282         puts(_("Running in background, you can log off now."));
283         puts(_("You'll get mail when amflush is finished."));
284     }
285
286     if(redirect) redirect_stderr();
287
288     if(!foreground) detach();
289
290     add_amanda_log_handler(amanda_log_stderr);
291     add_amanda_log_handler(amanda_log_trace_log);
292     today = time(NULL);
293     tm = localtime(&today);
294     if (tm) {
295         strftime(date_string, 100, "%a %b %e %H:%M:%S %Z %Y", tm);
296         strftime(date_string_standard, 100, "%Y-%m-%d %H:%M:%S %Z", tm);
297     } else {
298         error(_("BAD DATE")); /* should never happen */
299     }
300     g_fprintf(stderr, _("amflush: start at %s\n"), date_string);
301     g_fprintf(stderr, _("amflush: datestamp %s\n"), amflush_timestamp);
302     g_fprintf(stderr, _("amflush: starttime %s\n"), amflush_timestamp);
303     g_fprintf(stderr, _("amflush: starttime-locale-independent %s\n"),
304               date_string_standard);
305     log_add(L_START, _("date %s"), amflush_timestamp);
306
307     /* START DRIVER */
308     if(pipe(driver_pipe) == -1) {
309         error(_("error [opening pipe to driver: %s]"), strerror(errno));
310         /*NOTREACHED*/
311     }
312     if((driver_pid = fork()) == 0) {
313         /*
314          * This is the child process.
315          */
316         dup2(driver_pipe[0], 0);
317         close(driver_pipe[1]);
318         config_options = get_config_options(3);
319         config_options[0] = "driver";
320         config_options[1] = get_config_name();
321         config_options[2] = "nodump";
322         safe_fd(-1, 0);
323         execve(driver_program, config_options, safe_env());
324         error(_("cannot exec %s: %s"), driver_program, strerror(errno));
325         /*NOTREACHED*/
326     } else if(driver_pid == -1) {
327         error(_("cannot fork for %s: %s"), driver_program, strerror(errno));
328         /*NOTREACHED*/
329     }
330     driver_stream = fdopen(driver_pipe[1], "w");
331     if (!driver_stream) {
332         error(_("Can't fdopen: %s"), strerror(errno));
333         /*NOTREACHED*/
334     }
335
336     g_fprintf(driver_stream, "DATE %s\n", amflush_timestamp);
337     for(holding_file=holding_list; holding_file != NULL;
338                                    holding_file = holding_file->next) {
339         dumpfile_t file;
340         holding_file_get_dumpfile((char *)holding_file->data, &file);
341
342         if (holding_file_size((char *)holding_file->data, 1) <= 0) {
343             log_add(L_INFO, "%s: removing file with no data.",
344                     (char *)holding_file->data);
345             holding_file_unlink((char *)holding_file->data);
346             dumpfile_free_data(&file);
347             continue;
348         }
349
350         dp = lookup_disk(file.name, file.disk);
351         if (!dp) {
352             error("dp == NULL");
353             /*NOTREACHED*/
354         }
355         if (dp->todo == 0) continue;
356
357         qdisk = quote_string(file.disk);
358         qhname = quote_string((char *)holding_file->data);
359         g_fprintf(stderr,
360                 "FLUSH %s %s %s %d %s\n",
361                 file.name,
362                 qdisk,
363                 file.datestamp,
364                 file.dumplevel,
365                 qhname);
366         g_fprintf(driver_stream,
367                 "FLUSH %s %s %s %d %s\n",
368                 file.name,
369                 qdisk,
370                 file.datestamp,
371                 file.dumplevel,
372                 qhname);
373         amfree(qdisk);
374         amfree(qhname);
375         dumpfile_free_data(&file);
376     }
377     g_fprintf(stderr, "ENDFLUSH\n"); fflush(stderr);
378     g_fprintf(driver_stream, "ENDFLUSH\n"); fflush(driver_stream);
379     fclose(driver_stream);
380
381     /* WAIT DRIVER */
382     while(1) {
383         if((pid = wait(&exitcode)) == -1) {
384             if(errno == EINTR) {
385                 continue;
386             } else {
387                 error(_("wait for %s: %s"), driver_program, strerror(errno));
388                 /*NOTREACHED*/
389             }
390         } else if (pid == driver_pid) {
391             break;
392         }
393     }
394
395     g_slist_free_full(datestamp_list);
396     datestamp_list = NULL;
397     g_slist_free_full(holding_list);
398     holding_list = NULL;
399
400     if(redirect) { /* rename errfile */
401         char *errfile, *errfilex, *nerrfilex, number[100];
402         int tapecycle;
403         int maxdays, days;
404                 
405         struct stat stat_buf;
406
407         errfile = vstralloc(conf_logdir, "/amflush", NULL);
408         errfilex = NULL;
409         nerrfilex = NULL;
410         tapecycle = getconf_int(CNF_TAPECYCLE);
411         maxdays = tapecycle + 2;
412         days = 1;
413         /* First, find out the last existing errfile,           */
414         /* to avoid ``infinite'' loops if tapecycle is infinite */
415
416         g_snprintf(number,100,"%d",days);
417         errfilex = newvstralloc(errfilex, errfile, ".", number, NULL);
418         while ( days < maxdays && stat(errfilex,&stat_buf)==0) {
419             days++;
420             g_snprintf(number,100,"%d",days);
421             errfilex = newvstralloc(errfilex, errfile, ".", number, NULL);
422         }
423         g_snprintf(number,100,"%d",days);
424         errfilex = newvstralloc(errfilex, errfile, ".", number, NULL);
425         nerrfilex = NULL;
426         while (days > 1) {
427             amfree(nerrfilex);
428             nerrfilex = errfilex;
429             days--;
430             g_snprintf(number,100,"%d",days);
431             errfilex = vstralloc(errfile, ".", number, NULL);
432             if (rename(errfilex, nerrfilex) != 0) {
433                 error(_("cannot rename \"%s\" to \"%s\": %s"),
434                       errfilex, nerrfilex, strerror(errno));
435                 /*NOTREACHED*/
436             }
437         }
438         errfilex = newvstralloc(errfilex, errfile, ".1", NULL);
439         if (rename(errfile,errfilex) != 0) {
440             error(_("cannot rename \"%s\" to \"%s\": %s"),
441                   errfilex, nerrfilex, strerror(errno));
442             /*NOTREACHED*/
443         }
444         amfree(errfile);
445         amfree(errfilex);
446         amfree(nerrfilex);
447     }
448
449     /*
450      * Have amreport generate report and send mail.  Note that we do
451      * not bother checking the exit status.  If it does not work, it
452      * can be rerun.
453      */
454
455     if((reporter_pid = fork()) == 0) {
456         /*
457          * This is the child process.
458          */
459         config_options = get_config_options(3);
460         config_options[0] = "amreport";
461         config_options[1] = get_config_name();
462         config_options[2] = "--from-amdump";
463         safe_fd(-1, 0);
464         execve(reporter_program, config_options, safe_env());
465         error(_("cannot exec %s: %s"), reporter_program, strerror(errno));
466         /*NOTREACHED*/
467     } else if(reporter_pid == -1) {
468         error(_("cannot fork for %s: %s"), reporter_program, strerror(errno));
469         /*NOTREACHED*/
470     }
471     while(1) {
472         if((pid = wait(&exitcode)) == -1) {
473             if(errno == EINTR) {
474                 continue;
475             } else {
476                 error(_("wait for %s: %s"), reporter_program, strerror(errno));
477                 /*NOTREACHED*/
478             }
479         } else if (pid == reporter_pid) {
480             break;
481         }
482     }
483
484     log_add(L_INFO, "pid-done %ld", (long)getpid());
485
486     /*
487      * Call amlogroll to rename the log file to its datestamped version.
488      * Since we exec at this point, our exit code will be that of amlogroll.
489      */
490     config_options = get_config_options(2);
491     config_options[0] = "amlogroll";
492     config_options[1] = get_config_name();
493     safe_fd(-1, 0);
494     execve(logroll_program, config_options, safe_env());
495     error(_("cannot exec %s: %s"), logroll_program, strerror(errno));
496     /*NOTREACHED*/
497     return 0;                           /* keep the compiler happy */
498 }
499
500
501 static int
502 get_letter_from_user(void)
503 {
504     int r, ch;
505
506     fflush(stdout); fflush(stderr);
507     while((ch = getchar()) != EOF && ch != '\n' && g_ascii_isspace(ch)) {
508         (void)ch; /* Quite lint */
509     }
510     if(ch == '\n') {
511         r = '\0';
512     } else if (ch != EOF) {
513         r = ch;
514         if(islower(r)) r = toupper(r);
515         while((ch = getchar()) != EOF && ch != '\n') { 
516             (void)ch; /* Quite lint */
517         }
518     } else {
519         r = ch;
520         clearerr(stdin);
521     }
522     return r;
523 }
524
525 /* Allow the user to select a set of datestamps from those in
526  * holding disks.  The result can be passed to 
527  * holding_get_files_for_flush.  If less than two dates are
528  * available, then no user interaction takes place.
529  *
530  * @returns: a new GSList listing the selected datestamps
531  */
532 static GSList *
533 pick_datestamp(void)
534 {
535     GSList *datestamp_list;
536     GSList *r_datestamp_list = NULL;
537     GSList *ds;
538     char **datestamps = NULL;
539     int i;
540     char *answer = NULL;
541     char *a = NULL;
542     int ch = 0;
543     char max_char = '\0', chupper = '\0';
544
545     datestamp_list = holding_get_all_datestamps();
546
547     if(g_slist_length(datestamp_list) < 2) {
548         return datestamp_list;
549     } else {
550         datestamps = alloc(g_slist_length(datestamp_list) * SIZEOF(char *));
551         for(ds = datestamp_list, i=0; ds != NULL; ds = ds->next,i++) {
552             datestamps[i] = (char *)ds->data; /* borrowing reference */
553         }
554
555         while(1) {
556             puts(_("\nMultiple Amanda runs in holding disks; please pick one by letter:"));
557             for(ds = datestamp_list, max_char = 'A';
558                 ds != NULL && max_char <= 'Z';
559                 ds = ds->next, max_char++) {
560                 g_printf("  %c. %s\n", max_char, (char *)ds->data);
561             }
562             max_char--;
563             g_printf(_("Select datestamps to flush [A..%c or <enter> for all]: "), max_char);
564             fflush(stdout); fflush(stderr);
565             amfree(answer);
566             if ((answer = agets(stdin)) == NULL) {
567                 clearerr(stdin);
568                 continue;
569             }
570
571             if (*answer == '\0' || strncasecmp(answer, "ALL", 3) == 0) {
572                 break;
573             }
574
575             a = answer;
576             while ((ch = *a++) != '\0') {
577                 if (!g_ascii_isspace(ch))
578                     break;
579             }
580
581             /* rewrite the selected list into r_datestamp_list, then copy it over
582              * to datestamp_list */
583             do {
584                 if (g_ascii_isspace(ch) || ch == ',') {
585                     continue;
586                 }
587                 chupper = (char)toupper(ch);
588                 if (chupper < 'A' || chupper > max_char) {
589                     g_slist_free_full(r_datestamp_list);
590                     r_datestamp_list = NULL;
591                     break;
592                 }
593                 r_datestamp_list = g_slist_append(r_datestamp_list,
594                                            stralloc(datestamps[chupper - 'A']));
595             } while ((ch = *a++) != '\0');
596             if (r_datestamp_list && ch == '\0') {
597                 g_slist_free_full(datestamp_list);
598                 datestamp_list = r_datestamp_list;
599                 break;
600             }
601         }
602     }
603     amfree(datestamps); /* references in this array are borrowed */
604     amfree(answer);
605
606     return datestamp_list;
607 }
608
609
610 /*
611  * confirm before detaching and running
612  */
613
614 void
615 confirm(GSList *datestamp_list)
616 {
617     tape_t *tp;
618     char *tpchanger;
619     GSList *datestamp;
620     int ch;
621     char *extra;
622
623     g_printf(_("\nToday is: %s\n"),amflush_datestamp);
624     g_printf(_("Flushing dumps from"));
625     extra = "";
626     for(datestamp = datestamp_list; datestamp != NULL; datestamp = datestamp->next) {
627         g_printf("%s %s", extra, (char *)datestamp->data);
628         extra = ",";
629     }
630     tpchanger = getconf_str(CNF_TPCHANGER);
631     if(*tpchanger != '\0') {
632         g_printf(_(" using tape changer \"%s\".\n"), tpchanger);
633     } else {
634         g_printf(_(" to tape drive \"%s\".\n"), getconf_str(CNF_TAPEDEV));
635     }
636
637     g_printf(_("Expecting "));
638     tp = lookup_last_reusable_tape(0);
639     if(tp != NULL)
640         g_printf(_("tape %s or "), tp->label);
641     g_printf(_("a new tape."));
642     tp = lookup_tapepos(1);
643     if(tp != NULL)
644         g_printf(_("  (The last dumps were to tape %s)"), tp->label);
645
646     while (1) {
647         g_printf(_("\nAre you sure you want to do this [yN]? "));
648         if((ch = get_letter_from_user()) == 'Y') {
649             return;
650         } else if (ch == 'N' || ch == '\0' || ch == EOF) {
651             if (ch == EOF) {
652                 putchar('\n');
653             }
654             break;
655         }
656     }
657
658     g_printf(_("Ok, quitting.  Run amflush again when you are ready.\n"));
659     exit(1);
660 }
661
662 void
663 redirect_stderr(void)
664 {
665     int fderr;
666     char *errfile;
667
668     fflush(stdout); fflush(stderr);
669     errfile = vstralloc(conf_logdir, "/amflush", NULL);
670     if((fderr = open(errfile, O_WRONLY| O_CREAT | O_TRUNC, 0600)) == -1) {
671         error(_("could not open %s: %s"), errfile, strerror(errno));
672         /*NOTREACHED*/
673     }
674     dup2(fderr,1);
675     dup2(fderr,2);
676     aclose(fderr);
677     amfree(errfile);
678 }
679
680 void
681 detach(void)
682 {
683     int fd;
684
685     fflush(stdout); fflush(stderr);
686     if((fd = open("/dev/null", O_RDWR, 0666)) == -1) {
687         error(_("could not open /dev/null: %s"), strerror(errno));
688         /*NOTREACHED*/
689     }
690
691     dup2(fd,0);
692     aclose(fd);
693
694     switch(fork()) {
695     case -1:
696         error(_("could not fork: %s"), strerror(errno));
697         /*NOTREACHED*/
698
699     case 0:
700         setsid();
701         return;
702     }
703
704     exit(0);
705 }