]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_tree.c
cb9b56cbd09d37af72fe338d5b901bb85b66ac22
[bacula/bacula] / bacula / src / dird / ua_tree.c
1 /*
2  *
3  *   Bacula Director -- User Agent Database File tree for Restore
4  *      command. This file interacts with the user implementing the
5  *      UA tree commands.
6  *
7  *     Kern Sibbald, July MMII
8  *
9  *   Version $Id$
10  */
11 /*
12    Copyright (C) 2002-2005 Kern Sibbald
13
14    This program is free software; you can redistribute it and/or
15    modify it under the terms of the GNU General Public License
16    version 2 as amended with additional clauses defined in the
17    file LICENSE in the main source directory.
18
19    This program is distributed in the hope that it will be useful,
20    but WITHOUT ANY WARRANTY; without even the implied warranty of
21    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
22    the file LICENSE for additional details.
23
24  */
25
26 #include "bacula.h"
27 #include "dird.h"
28 #ifdef HAVE_FNMATCH
29 #include <fnmatch.h>
30 #else
31 #include "lib/fnmatch.h"
32 #endif
33 #include "findlib/find.h"
34
35
36 /* Forward referenced commands */
37
38 static int markcmd(UAContext *ua, TREE_CTX *tree);
39 static int markdircmd(UAContext *ua, TREE_CTX *tree);
40 static int countcmd(UAContext *ua, TREE_CTX *tree);
41 static int findcmd(UAContext *ua, TREE_CTX *tree);
42 static int lscmd(UAContext *ua, TREE_CTX *tree);
43 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree);
44 static int dircmd(UAContext *ua, TREE_CTX *tree);
45 static int estimatecmd(UAContext *ua, TREE_CTX *tree);
46 static int helpcmd(UAContext *ua, TREE_CTX *tree);
47 static int cdcmd(UAContext *ua, TREE_CTX *tree);
48 static int pwdcmd(UAContext *ua, TREE_CTX *tree);
49 static int unmarkcmd(UAContext *ua, TREE_CTX *tree);
50 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree);
51 static int quitcmd(UAContext *ua, TREE_CTX *tree);
52 static int donecmd(UAContext *ua, TREE_CTX *tree);
53
54
55 struct cmdstruct { const char *key; int (*func)(UAContext *ua, TREE_CTX *tree); const char *help; };
56 static struct cmdstruct commands[] = {
57  { N_("cd"),         cdcmd,        _("change current directory")},
58  { N_("count"),      countcmd,     _("count marked files in and below the cd")},
59  { N_("dir"),        dircmd,       _("long list current directory, wildcards allowed")},
60  { N_("done"),       donecmd,      _("leave file selection mode")},
61  { N_("estimate"),   estimatecmd,  _("estimate restore size")},
62  { N_("exit"),       donecmd,      _("same as done command")},
63  { N_("find"),       findcmd,      _("find files, wildcards allowed")},
64  { N_("help"),       helpcmd,      _("print help")},
65  { N_("ls"),         lscmd,        _("list current directory, wildcards allowed")},
66  { N_("lsmark"),     lsmarkcmd,    _("list the marked files in and below the cd")},
67  { N_("mark"),       markcmd,      _("mark dir/file to be restored recursively in dirs")},
68  { N_("markdir"),    markdircmd,   _("mark directory name to be restored (no files)")},
69  { N_("pwd"),        pwdcmd,       _("print current working directory")},
70  { N_("unmark"),     unmarkcmd,    _("unmark dir/file to be restored recursively in dir")},
71  { N_("unmarkdir"),  unmarkdircmd, _("unmark directory name only no recursion")},
72  { N_("quit"),       quitcmd,      _("quit and do not do restore")},
73  { N_("?"),          helpcmd,      _("print help")},
74              };
75 #define comsize (sizeof(commands)/sizeof(struct cmdstruct))
76
77
78 /*
79  * Enter a prompt mode where the user can select/deselect
80  *  files to be restored. This is sort of like a mini-shell
81  *  that allows "cd", "pwd", "add", "rm", ...
82  */
83 bool user_select_files_from_tree(TREE_CTX *tree)
84 {
85    char cwd[2000];
86    bool stat;
87    /* Get a new context so we don't destroy restore command args */
88    UAContext *ua = new_ua_context(tree->ua->jcr);
89    ua->UA_sock = tree->ua->UA_sock;   /* patch in UA socket */
90
91    bsendmsg(tree->ua, _(
92       "\nYou are now entering file selection mode where you add (mark) and\n"
93       "remove (unmark) files to be restored. No files are initially added, unless\n"
94       "you used the \"all\" keyword on the command line.\n"
95       "Enter \"done\" to leave this mode.\n\n"));
96    /*
97     * Enter interactive command handler allowing selection
98     *  of individual files.
99     */
100    tree->node = (TREE_NODE *)tree->root;
101    tree_getpath(tree->node, cwd, sizeof(cwd));
102    bsendmsg(tree->ua, _("cwd is: %s\n"), cwd);
103    for ( ;; ) {
104       int found, len, i;
105       if (!get_cmd(ua, "$ ")) {
106          break;
107       }
108       parse_ua_args(ua);
109       if (ua->argc == 0) {
110          break;
111       }
112
113       len = strlen(ua->argk[0]);
114       found = 0;
115       stat = false;
116       for (i=0; i<(int)comsize; i++)       /* search for command */
117          if (strncasecmp(ua->argk[0],  _(commands[i].key), len) == 0) {
118             stat = (*commands[i].func)(ua, tree);   /* go execute command */
119             found = 1;
120             break;
121          }
122       if (!found) {
123          bsendmsg(tree->ua, _("Illegal command. Enter \"done\" to exit.\n"));
124          continue;
125       }
126       if (!stat) {
127          break;
128       }
129    }
130    ua->UA_sock = NULL;                /* don't release restore socket */
131    stat = !ua->quit;
132    ua->quit = false;
133    free_ua_context(ua);               /* get rid of temp UA context */
134    return stat;
135 }
136
137
138 /*
139  * This callback routine is responsible for inserting the
140  *  items it gets into the directory tree. For each JobId selected
141  *  this routine is called once for each file. We do not allow
142  *  duplicate filenames, but instead keep the info from the most
143  *  recent file entered (i.e. the JobIds are assumed to be sorted)
144  *
145  *   See uar_sel_files in sql_cmds.c for query that calls us.
146  *      row[0]=Path, row[1]=Filename, row[2]=FileIndex
147  *      row[3]=JobId row[4]=LStat
148  */
149 int insert_tree_handler(void *ctx, int num_fields, char **row)
150 {
151    struct stat statp;
152    TREE_CTX *tree = (TREE_CTX *)ctx;
153    TREE_NODE *node;
154    int type;
155    bool hard_link, ok;
156    int FileIndex;
157    JobId_t JobId;
158
159    if (*row[1] == 0) {                /* no filename => directory */
160       if (*row[0] != '/') {           /* Must be Win32 directory */
161          type = TN_DIR_NLS;
162       } else {
163          type = TN_DIR;
164       }
165    } else {
166       type = TN_FILE;
167    }
168    hard_link = (decode_LinkFI(row[4], &statp) != 0);
169    node = insert_tree_node(row[0], row[1], type, tree->root, NULL);
170    JobId = str_to_int64(row[3]);
171    FileIndex = str_to_int64(row[2]);
172    /*
173     * - The first time we see a file (node->inserted==true), we accept it.
174     * - In the same JobId, we accept only the first copy of a
175     *   hard linked file (the others are simply pointers).
176     * - In the same JobId, we accept the last copy of any other
177     *   file -- in particular directories.
178     *
179     * All the code to set ok could be condensed to a single
180     *  line, but it would be even harder to read.
181     */
182    ok = true;
183    if (!node->inserted && JobId == node->JobId) {
184       if ((hard_link && FileIndex > node->FileIndex) ||
185           (!hard_link && FileIndex < node->FileIndex)) {
186          ok = false;
187       }
188    }
189    if (ok) {
190       node->hard_link = hard_link;
191       node->FileIndex = FileIndex;
192       node->JobId = JobId;
193       node->type = type;
194       node->soft_link = S_ISLNK(statp.st_mode) != 0;
195       if (tree->all) {
196          node->extract = true;          /* extract all by default */
197          if (type == TN_DIR || type == TN_DIR_NLS) {
198             node->extract_dir = true;   /* if dir, extract it */
199          }
200       }
201    }
202    if (node->inserted) {
203       tree->FileCount++;
204       if (tree->DeltaCount > 0 && (tree->FileCount-tree->LastCount) > tree->DeltaCount) {
205          bsendmsg(tree->ua, "+");
206          tree->LastCount = tree->FileCount;
207       }
208    }
209    tree->cnt++;
210    return 0;
211 }
212
213
214 /*
215  * Set extract to value passed. We recursively walk
216  *  down the tree setting all children if the
217  *  node is a directory.
218  */
219 static int set_extract(UAContext *ua, TREE_NODE *node, TREE_CTX *tree, bool extract)
220 {
221    TREE_NODE *n;
222    FILE_DBR fdbr;
223    struct stat statp;
224    int count = 0;
225
226    node->extract = extract;
227    if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
228       node->extract_dir = extract;    /* set/clear dir too */
229    }
230    if (node->type != TN_NEWDIR) {
231       count++;
232    }
233    /* For a non-file (i.e. directory), we see all the children */
234    if (node->type != TN_FILE || (node->soft_link && tree_node_has_child(node))) {
235       /* Recursive set children within directory */
236       foreach_child(n, node) {
237          count += set_extract(ua, n, tree, extract);
238       }
239       /*
240        * Walk up tree marking any unextracted parent to be
241        * extracted.
242        */
243       if (extract) {
244          while (node->parent && !node->parent->extract_dir) {
245             node = node->parent;
246             node->extract_dir = true;
247          }
248       }
249    } else if (extract) {
250       char cwd[2000];
251       /*
252        * Ordinary file, we get the full path, look up the
253        * attributes, decode them, and if we are hard linked to
254        * a file that was saved, we must load that file too.
255        */
256       tree_getpath(node, cwd, sizeof(cwd));
257       fdbr.FileId = 0;
258       fdbr.JobId = node->JobId;
259       if (node->hard_link && db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
260          int32_t LinkFI;
261          decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
262          /*
263           * If we point to a hard linked file, traverse the tree to
264           * find that file, and mark it to be restored as well. It
265           * must have the Link we just obtained and the same JobId.
266           */
267          if (LinkFI) {
268             for (n=first_tree_node(tree->root); n; n=next_tree_node(n)) {
269                if (n->FileIndex == LinkFI && n->JobId == node->JobId) {
270                   n->extract = true;
271                   if (n->type == TN_DIR || n->type == TN_DIR_NLS) {
272                      n->extract_dir = true;
273                   }
274                   break;
275                }
276             }
277          }
278       }
279    }
280    return count;
281 }
282
283 /*
284  * Recursively mark the current directory to be restored as
285  *  well as all directories and files below it.
286  */
287 static int markcmd(UAContext *ua, TREE_CTX *tree)
288 {
289    TREE_NODE *node;
290    int count = 0;
291    char ec1[50];
292
293    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
294       bsendmsg(ua, _("No files marked.\n"));
295       return 1;
296    }
297    for (int i=1; i < ua->argc; i++) {
298       foreach_child(node, tree->node) {
299          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
300             count += set_extract(ua, node, tree, true);
301          }
302       }
303    }
304    if (count == 0) {
305       bsendmsg(ua, _("No files marked.\n"));
306    } else {
307       bsendmsg(ua, _("%s file%s marked.\n"),
308                edit_uint64_with_commas(count, ec1), count==0?"":"s");
309    }
310    return 1;
311 }
312
313 static int markdircmd(UAContext *ua, TREE_CTX *tree)
314 {
315    TREE_NODE *node;
316    int count = 0;
317    char ec1[50];
318
319    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
320       bsendmsg(ua, _("No files marked.\n"));
321       return 1;
322    }
323    for (int i=1; i < ua->argc; i++) {
324       foreach_child(node, tree->node) {
325          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
326             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
327                node->extract_dir = true;
328                count++;
329             }
330          }
331       }
332    }
333    if (count == 0) {
334       bsendmsg(ua, _("No directories marked.\n"));
335    } else {
336       bsendmsg(ua, _("%s director%s marked.\n"),
337                edit_uint64_with_commas(count, ec1), count==1?"y":"ies");
338    }
339    return 1;
340 }
341
342
343 static int countcmd(UAContext *ua, TREE_CTX *tree)
344 {
345    int total, num_extract;
346    char ec1[50], ec2[50];
347
348    total = num_extract = 0;
349    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
350       if (node->type != TN_NEWDIR) {
351          total++;
352          if (node->extract || node->extract_dir) {
353             num_extract++;
354          }
355       }
356    }
357    bsendmsg(ua, "%s total files/dirs. %s marked to be restored.\n",
358             edit_uint64_with_commas(total, ec1),
359             edit_uint64_with_commas(num_extract, ec2));
360    return 1;
361 }
362
363 static int findcmd(UAContext *ua, TREE_CTX *tree)
364 {
365    char cwd[2000];
366
367    if (ua->argc == 1) {
368       bsendmsg(ua, _("No file specification given.\n"));
369       return 0;
370    }
371
372    for (int i=1; i < ua->argc; i++) {
373       for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
374          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
375             const char *tag;
376             tree_getpath(node, cwd, sizeof(cwd));
377             if (node->extract) {
378                tag = "*";
379             } else if (node->extract_dir) {
380                tag = "+";
381             } else {
382                tag = "";
383             }
384             bsendmsg(ua, "%s%s\n", tag, cwd);
385          }
386       }
387    }
388    return 1;
389 }
390
391
392
393 static int lscmd(UAContext *ua, TREE_CTX *tree)
394 {
395    TREE_NODE *node;
396
397    if (!tree_node_has_child(tree->node)) {
398       return 1;
399    }
400    foreach_child(node, tree->node) {
401       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
402          const char *tag;
403          if (node->extract) {
404             tag = "*";
405          } else if (node->extract_dir) {
406             tag = "+";
407          } else {
408             tag = "";
409          }
410          bsendmsg(ua, "%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
411       }
412    }
413    return 1;
414 }
415
416 /*
417  * Ls command that lists only the marked files
418  */
419 static void rlsmark(UAContext *ua, TREE_NODE *tnode)
420 {
421    TREE_NODE *node;
422    if (!tree_node_has_child(tnode)) {
423       return;
424    }
425    foreach_child(node, tnode) {
426       if ((ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) &&
427           (node->extract || node->extract_dir)) {
428          const char *tag;
429          if (node->extract) {
430             tag = "*";
431          } else if (node->extract_dir) {
432             tag = "+";
433          } else {
434             tag = "";
435          }
436          bsendmsg(ua, "%s%s%s\n", tag, node->fname, tree_node_has_child(node)?"/":"");
437          if (tree_node_has_child(node)) {
438             rlsmark(ua, node);
439          }
440       }
441    }
442 }
443
444 static int lsmarkcmd(UAContext *ua, TREE_CTX *tree)
445 {
446    rlsmark(ua, tree->node);
447    return 1;
448 }
449
450
451
452 extern char *getuser(uid_t uid, char *name, int len);
453 extern char *getgroup(gid_t gid, char *name, int len);
454
455 /*
456  * This is actually the long form used for "dir"
457  */
458 static void ls_output(char *buf, const char *fname, const char *tag, struct stat *statp)
459 {
460    char *p;
461    const char *f;
462    char ec1[30];
463    char en1[30], en2[30];
464    int n;
465
466    p = encode_mode(statp->st_mode, buf);
467    n = sprintf(p, "  %2d ", (uint32_t)statp->st_nlink);
468    p += n;
469    n = sprintf(p, "%-8.8s %-8.8s", getuser(statp->st_uid, en1, sizeof(en1)),
470                getgroup(statp->st_gid, en2, sizeof(en2)));
471    p += n;
472    n = sprintf(p, "%10.10s  ", edit_uint64(statp->st_size, ec1));
473    p += n;
474    p = encode_time(statp->st_ctime, p);
475    *p++ = ' ';
476    *p++ = *tag;
477    for (f=fname; *f; ) {
478       *p++ = *f++;
479    }
480    *p = 0;
481 }
482
483
484 /*
485  * Like ls command, but give more detail on each file
486  */
487 static int dircmd(UAContext *ua, TREE_CTX *tree)
488 {
489    TREE_NODE *node;
490    FILE_DBR fdbr;
491    struct stat statp;
492    char buf[1100];
493    char cwd[1100], *pcwd;
494
495    if (!tree_node_has_child(tree->node)) {
496       bsendmsg(ua, "Node %s has no children.\n", tree->node->fname);
497       return 1;
498    }
499
500    foreach_child(node, tree->node) {
501       const char *tag;
502       if (ua->argc == 1 || fnmatch(ua->argk[1], node->fname, 0) == 0) {
503          if (node->extract) {
504             tag = "*";
505          } else if (node->extract_dir) {
506             tag = "+";
507          } else {
508             tag = " ";
509          }
510          tree_getpath(node, cwd, sizeof(cwd));
511          fdbr.FileId = 0;
512          fdbr.JobId = node->JobId;
513          /*
514           * Strip / from soft links to directories.
515           *   This is because soft links to files have a trailing slash
516           *   when returned from tree_getpath, but db_get_file_attr...
517           *   treats soft links as files, so they do not have a trailing
518           *   slash like directory names.
519           */
520          if (node->type == TN_FILE && tree_node_has_child(node)) {
521             bstrncpy(buf, cwd, sizeof(buf));
522             pcwd = buf;
523             int len = strlen(buf);
524             if (len > 1) {
525                buf[len-1] = 0;        /* strip trailing / */
526             }
527          } else {
528             pcwd = cwd;
529          }
530          if (db_get_file_attributes_record(ua->jcr, ua->db, pcwd, NULL, &fdbr)) {
531             int32_t LinkFI;
532             decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
533          } else {
534             /* Something went wrong getting attributes -- print name */
535             memset(&statp, 0, sizeof(statp));
536          }
537          ls_output(buf, cwd, tag, &statp);
538          bsendmsg(ua, "%s\n", buf);
539       }
540    }
541    return 1;
542 }
543
544
545 static int estimatecmd(UAContext *ua, TREE_CTX *tree)
546 {
547    int total, num_extract;
548    uint64_t total_bytes = 0;
549    FILE_DBR fdbr;
550    struct stat statp;
551    char cwd[1100];
552    char ec1[50];
553
554    total = num_extract = 0;
555    for (TREE_NODE *node=first_tree_node(tree->root); node; node=next_tree_node(node)) {
556       if (node->type != TN_NEWDIR) {
557          total++;
558          /* If regular file, get size */
559          if (node->extract && node->type == TN_FILE) {
560             num_extract++;
561             tree_getpath(node, cwd, sizeof(cwd));
562             fdbr.FileId = 0;
563             fdbr.JobId = node->JobId;
564             if (db_get_file_attributes_record(ua->jcr, ua->db, cwd, NULL, &fdbr)) {
565                int32_t LinkFI;
566                decode_stat(fdbr.LStat, &statp, &LinkFI); /* decode stat pkt */
567                if (S_ISREG(statp.st_mode) && statp.st_size > 0) {
568                   total_bytes += statp.st_size;
569                }
570             }
571          /* Directory, count only */
572          } else if (node->extract || node->extract_dir) {
573             num_extract++;
574          }
575       }
576    }
577    bsendmsg(ua, "%d total files; %d marked to be restored; %s bytes.\n",
578             total, num_extract, edit_uint64_with_commas(total_bytes, ec1));
579    return 1;
580 }
581
582
583
584 static int helpcmd(UAContext *ua, TREE_CTX *tree)
585 {
586    unsigned int i;
587
588    bsendmsg(ua, _("  Command    Description\n  =======    ===========\n"));
589    for (i=0; i<comsize; i++) {
590       bsendmsg(ua, _("  %-10s %s\n"), _(commands[i].key), _(commands[i].help));
591    }
592    bsendmsg(ua, "\n");
593    return 1;
594 }
595
596 /*
597  * Change directories.  Note, if the user specifies x: and it fails,
598  *   we assume it is a Win32 absolute cd rather than relative and
599  *   try a second time with /x: ...  Win32 kludge.
600  */
601 static int cdcmd(UAContext *ua, TREE_CTX *tree)
602 {
603    TREE_NODE *node;
604    char cwd[2000];
605
606    if (ua->argc != 2) {
607       return 1;
608    }
609    strip_leading_space(ua->argk[1]);
610    node = tree_cwd(ua->argk[1], tree->root, tree->node);
611    if (!node) {
612       /* Try once more if Win32 drive -- make absolute */
613       if (ua->argk[1][1] == ':') {  /* win32 drive */
614          bstrncpy(cwd, "/", sizeof(cwd));
615          bstrncat(cwd, ua->argk[1], sizeof(cwd));
616          node = tree_cwd(cwd, tree->root, tree->node);
617       }
618       if (!node) {
619          bsendmsg(ua, _("Invalid path given.\n"));
620       } else {
621          tree->node = node;
622       }
623    } else {
624       tree->node = node;
625    }
626    tree_getpath(tree->node, cwd, sizeof(cwd));
627    bsendmsg(ua, _("cwd is: %s\n"), cwd);
628    return 1;
629 }
630
631 static int pwdcmd(UAContext *ua, TREE_CTX *tree)
632 {
633    char cwd[2000];
634    tree_getpath(tree->node, cwd, sizeof(cwd));
635    bsendmsg(ua, _("cwd is: %s\n"), cwd);
636    return 1;
637 }
638
639
640 static int unmarkcmd(UAContext *ua, TREE_CTX *tree)
641 {
642    TREE_NODE *node;
643    int count = 0;
644
645    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
646       bsendmsg(ua, _("No files unmarked.\n"));
647       return 1;
648    }
649    for (int i=1; i < ua->argc; i++) {
650       foreach_child(node, tree->node) {
651          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
652             count += set_extract(ua, node, tree, false);
653          }
654       }
655    }
656    if (count == 0) {
657       bsendmsg(ua, _("No files unmarked.\n"));
658    } else {
659       bsendmsg(ua, _("%d file%s unmarked.\n"), count, count==0?"":"s");
660    }
661    return 1;
662 }
663
664 static int unmarkdircmd(UAContext *ua, TREE_CTX *tree)
665 {
666    TREE_NODE *node;
667    int count = 0;
668
669    if (ua->argc < 2 || !tree_node_has_child(tree->node)) {
670       bsendmsg(ua, _("No directories unmarked.\n"));
671       return 1;
672    }
673
674    for (int i=1; i < ua->argc; i++) {
675       foreach_child(node, tree->node) {
676          if (fnmatch(ua->argk[i], node->fname, 0) == 0) {
677             if (node->type == TN_DIR || node->type == TN_DIR_NLS) {
678                node->extract_dir = false;
679                count++;
680             }
681          }
682       }
683    }
684
685    if (count == 0) {
686       bsendmsg(ua, _("No directories unmarked.\n"));
687    } else {
688       bsendmsg(ua, _("%d director%s unmarked.\n"), count, count==1?"y":"ies");
689    }
690    return 1;
691 }
692
693
694 static int donecmd(UAContext *ua, TREE_CTX *tree)
695 {
696    return 0;
697 }
698
699 static int quitcmd(UAContext *ua, TREE_CTX *tree)
700 {
701    ua->quit = true;
702    return 0;
703 }