1 /* $NetBSD: interactive.c,v 1.28 2019/02/03 03:19:26 mrg Exp $ */ 2 3 /* 4 * Copyright (c) 1985, 1993 5 * The Regents of the University of California. All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 1. Redistributions of source code must retain the above copyright 11 * notice, this list of conditions and the following disclaimer. 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 3. Neither the name of the University nor the names of its contributors 16 * may be used to endorse or promote products derived from this software 17 * without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 23 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 25 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 29 * SUCH DAMAGE. 30 */ 31 32 #include <sys/cdefs.h> 33 #ifndef lint 34 #if 0 35 static char sccsid[] = "@(#)interactive.c 8.5 (Berkeley) 5/1/95"; 36 #else 37 __RCSID("$NetBSD: interactive.c,v 1.28 2019/02/03 03:19:26 mrg Exp $"); 38 #endif 39 #endif /* not lint */ 40 41 #include <sys/param.h> 42 #include <sys/time.h> 43 #include <sys/stat.h> 44 45 #include <ufs/ufs/dinode.h> 46 #include <ufs/ufs/dir.h> 47 #include <ufs/ffs/fs.h> 48 #include <protocols/dumprestore.h> 49 50 #include <setjmp.h> 51 #include <glob.h> 52 #include <stdio.h> 53 #include <stdlib.h> 54 #include <string.h> 55 56 #include "restore.h" 57 #include "extern.h" 58 59 #define round(a, b) (((a) + (b) - 1) / (b) * (b)) 60 61 /* 62 * Things to handle interruptions. 63 */ 64 static int runshell; 65 static jmp_buf reset; 66 static char *nextarg = NULL; 67 68 /* 69 * Structure and routines associated with listing directories. 70 */ 71 struct afile { 72 ino_t fnum; /* inode number of file */ 73 char *fname; /* file name */ 74 size_t len; /* name length */ 75 char prefix; /* prefix character */ 76 char postfix; /* postfix character */ 77 }; 78 struct arglist { 79 int freeglob; /* glob structure needs to be freed */ 80 int argcnt; /* next globbed argument to return */ 81 glob_t glob; /* globbing information */ 82 char *cmd; /* the current command */ 83 }; 84 85 static char *copynext(char *, char *); 86 static int fcmp(const void *, const void *); 87 static void formatf(struct afile *, int); 88 static void getcmd(char *, char *, char *, struct arglist *); 89 struct dirent *glob_readdir(RST_DIR *dirp); 90 static int glob_stat(const char *, struct stat *); 91 static void mkentry(char *, struct direct *, struct afile *); 92 static void printlist(char *, char *); 93 94 /* 95 * Read and execute commands from the terminal. 96 */ 97 void 98 runcmdshell(void) 99 { 100 struct entry *np; 101 ino_t ino; 102 struct arglist arglist; 103 char curdir[MAXPATHLEN]; 104 char name[MAXPATHLEN]; 105 char cmd[BUFSIZ]; 106 107 arglist.freeglob = 0; 108 arglist.argcnt = 0; 109 arglist.glob.gl_flags = GLOB_ALTDIRFUNC; 110 arglist.glob.gl_opendir = (void *)rst_opendir; 111 arglist.glob.gl_readdir = (void *)glob_readdir; 112 arglist.glob.gl_closedir = (void *)rst_closedir; 113 arglist.glob.gl_lstat = glob_stat; 114 arglist.glob.gl_stat = glob_stat; 115 canon("/", curdir); 116 loop: 117 if (setjmp(reset) != 0) { 118 if (arglist.freeglob != 0) { 119 arglist.freeglob = 0; 120 arglist.argcnt = 0; 121 globfree(&arglist.glob); 122 } 123 nextarg = NULL; 124 volno = 0; 125 } 126 runshell = 1; 127 getcmd(curdir, cmd, name, &arglist); 128 switch (cmd[0]) { 129 /* 130 * Add elements to the extraction list. 131 */ 132 case 'a': 133 if (strncmp(cmd, "add", strlen(cmd)) != 0) 134 goto bad; 135 ino = dirlookup(name); 136 if (ino == 0) 137 break; 138 if (ino == UFS_ROOTINO) 139 dotflag = 1; 140 if (mflag) 141 pathcheck(name); 142 treescan(name, ino, addfile); 143 break; 144 /* 145 * Change working directory. 146 */ 147 case 'c': 148 if (strncmp(cmd, "cd", strlen(cmd)) != 0) 149 goto bad; 150 ino = dirlookup(name); 151 if (ino == 0) 152 break; 153 if (inodetype(ino) == LEAF) { 154 fprintf(stderr, "%s: not a directory\n", name); 155 break; 156 } 157 (void) strcpy(curdir, name); 158 break; 159 /* 160 * Delete elements from the extraction list. 161 */ 162 case 'd': 163 if (strncmp(cmd, "delete", strlen(cmd)) != 0) 164 goto bad; 165 np = lookupname(name); 166 if (np == NULL || (np->e_flags & NEW) == 0) { 167 fprintf(stderr, "%s: not on extraction list\n", name); 168 break; 169 } 170 treescan(name, np->e_ino, deletefile); 171 break; 172 /* 173 * Extract the requested list. 174 */ 175 case 'e': 176 if (strncmp(cmd, "extract", strlen(cmd)) != 0) 177 goto bad; 178 createfiles(); 179 createlinks(); 180 setdirmodes(0); 181 if (dflag) 182 checkrestore(); 183 volno = 0; 184 break; 185 /* 186 * List available commands. 187 */ 188 case 'h': 189 if (strncmp(cmd, "help", strlen(cmd)) != 0) 190 goto bad; 191 /* FALLTHROUGH */ 192 case '?': 193 fprintf(stderr, "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", 194 "Available commands are:\n", 195 "\tls [arg] - list directory\n", 196 "\tcd arg - change directory\n", 197 "\tpwd - print current directory\n", 198 "\tadd [arg] - add `arg' to list of", 199 " files to be extracted\n", 200 "\tdelete [arg] - delete `arg' from", 201 " list of files to be extracted\n", 202 "\textract - extract requested files\n", 203 "\tsetmodes - set modes of requested directories\n", 204 "\tquit or xit - immediately exit program\n", 205 "\twhat - list dump header information\n", 206 "\tverbose - toggle verbose flag", 207 " (useful with ``ls'')\n", 208 "\thelp or `?' - print this list\n", 209 "\tDebug - turn on debugging\n", 210 "If no `arg' is supplied, the current", 211 " directory is used\n"); 212 break; 213 /* 214 * List a directory. 215 */ 216 case 'l': 217 if (strncmp(cmd, "ls", strlen(cmd)) != 0) 218 goto bad; 219 printlist(name, curdir); 220 break; 221 /* 222 * Print current directory. 223 */ 224 case 'p': 225 if (strncmp(cmd, "pwd", strlen(cmd)) != 0) 226 goto bad; 227 if (curdir[1] == '\0') 228 fprintf(stderr, "/\n"); 229 else 230 fprintf(stderr, "%s\n", &curdir[1]); 231 break; 232 /* 233 * Quit. 234 */ 235 case 'q': 236 if (strncmp(cmd, "quit", strlen(cmd)) != 0) 237 goto bad; 238 return; 239 case 'x': 240 if (strncmp(cmd, "xit", strlen(cmd)) != 0) 241 goto bad; 242 return; 243 /* 244 * Toggle verbose mode. 245 */ 246 case 'v': 247 if (strncmp(cmd, "verbose", strlen(cmd)) != 0) 248 goto bad; 249 if (vflag) { 250 fprintf(stderr, "verbose mode off\n"); 251 vflag = 0; 252 break; 253 } 254 fprintf(stderr, "verbose mode on\n"); 255 vflag++; 256 break; 257 /* 258 * Just restore requested directory modes. 259 */ 260 case 's': 261 if (strncmp(cmd, "setmodes", strlen(cmd)) != 0) 262 goto bad; 263 setdirmodes(FORCE); 264 break; 265 /* 266 * Print out dump header information. 267 */ 268 case 'w': 269 if (strncmp(cmd, "what", strlen(cmd)) != 0) 270 goto bad; 271 printdumpinfo(); 272 break; 273 /* 274 * Turn on debugging. 275 */ 276 case 'D': 277 if (strncmp(cmd, "Debug", strlen(cmd)) != 0) 278 goto bad; 279 if (dflag) { 280 fprintf(stderr, "debugging mode off\n"); 281 dflag = 0; 282 break; 283 } 284 fprintf(stderr, "debugging mode on\n"); 285 dflag++; 286 break; 287 /* 288 * Unknown command. 289 */ 290 default: 291 bad: 292 fprintf(stderr, "%s: unknown command; type ? for help\n", cmd); 293 break; 294 } 295 goto loop; 296 } 297 298 /* 299 * Read and parse an interactive command. 300 * The first word on the line is assigned to "cmd". If 301 * there are no arguments on the command line, then "curdir" 302 * is returned as the argument. If there are arguments 303 * on the line they are returned one at a time on each 304 * successive call to getcmd. Each argument is first assigned 305 * to "name". If it does not start with "/" the pathname in 306 * "curdir" is prepended to it. Finally "canon" is called to 307 * eliminate any embedded ".." components. 308 */ 309 static void 310 getcmd(char *curdir, char *cmd, char *name, struct arglist *ap) 311 { 312 char *cp; 313 static char input[BUFSIZ]; 314 char output[BUFSIZ]; 315 int globretval; 316 # define rawname input /* save space by reusing input buffer */ 317 318 /* 319 * Check to see if still processing arguments. 320 */ 321 if (ap->argcnt > 0) 322 goto retnext; 323 if (nextarg != NULL) 324 goto getnext; 325 /* 326 * Read a command line and trim off trailing white space. 327 */ 328 do { 329 fprintf(stderr, "%s > ", getprogname()); 330 (void) fflush(stderr); 331 (void) fgets(input, BUFSIZ, terminal); 332 } while (!feof(terminal) && input[0] == '\n'); 333 if (feof(terminal)) { 334 (void) strcpy(cmd, "quit"); 335 return; 336 } 337 for (cp = &input[strlen(input) - 2]; *cp == ' ' || *cp == '\t'; cp--) 338 /* trim off trailing white space and newline */; 339 *++cp = '\0'; 340 /* 341 * Copy the command into "cmd". 342 */ 343 cp = copynext(input, cmd); 344 ap->cmd = cmd; 345 /* 346 * If no argument, use curdir as the default. 347 */ 348 if (*cp == '\0') { 349 (void) strcpy(name, curdir); 350 return; 351 } 352 nextarg = cp; 353 /* 354 * Find the next argument. 355 */ 356 getnext: 357 cp = copynext(nextarg, rawname); 358 if (*cp == '\0') 359 nextarg = NULL; 360 else 361 nextarg = cp; 362 /* 363 * If it is an absolute pathname, canonicalize it and return it. 364 */ 365 if (rawname[0] == '/') { 366 canon(rawname, name); 367 } else { 368 /* 369 * For relative pathnames, prepend the current directory to 370 * it then canonicalize and return it. 371 */ 372 (void) strcpy(output, curdir); 373 (void) strcat(output, "/"); 374 (void) strcat(output, rawname); 375 canon(output, name); 376 } 377 if ((globretval = glob(name, GLOB_ALTDIRFUNC, NULL, &ap->glob)) < 0) { 378 fprintf(stderr, "%s: %s: ", ap->cmd, name); 379 switch (globretval) { 380 case GLOB_NOSPACE: 381 fprintf(stderr, "out of memory\n"); 382 break; 383 case GLOB_NOMATCH: 384 fprintf(stderr, "no filename match.\n"); 385 break; 386 case GLOB_ABORTED: 387 fprintf(stderr, "glob() aborted.\n"); 388 break; 389 default: 390 fprintf(stderr, "unknown error!\n"); 391 break; 392 } 393 } 394 if (ap->glob.gl_pathc == 0) 395 return; 396 ap->freeglob = 1; 397 ap->argcnt = ap->glob.gl_pathc; 398 399 retnext: 400 strcpy(name, ap->glob.gl_pathv[ap->glob.gl_pathc - ap->argcnt]); 401 if (--ap->argcnt == 0) { 402 ap->freeglob = 0; 403 globfree(&ap->glob); 404 } 405 # undef rawname 406 } 407 408 /* 409 * Strip off the next token of the input. 410 */ 411 static char * 412 copynext(char *input, char *output) 413 { 414 char *cp, *bp; 415 char quote; 416 417 for (cp = input; *cp == ' ' || *cp == '\t'; cp++) 418 /* skip to argument */; 419 bp = output; 420 while (*cp != ' ' && *cp != '\t' && *cp != '\0') { 421 /* 422 * Handle back slashes. 423 */ 424 if (*cp == '\\') { 425 if (*++cp == '\0') { 426 fprintf(stderr, 427 "command lines cannot be continued\n"); 428 continue; 429 } 430 *bp++ = *cp++; 431 continue; 432 } 433 /* 434 * The usual unquoted case. 435 */ 436 if (*cp != '\'' && *cp != '"') { 437 *bp++ = *cp++; 438 continue; 439 } 440 /* 441 * Handle single and double quotes. 442 */ 443 quote = *cp++; 444 while (*cp != quote && *cp != '\0') 445 *bp++ = *cp++; 446 if (*cp++ == '\0') { 447 fprintf(stderr, "missing %c\n", quote); 448 cp--; 449 continue; 450 } 451 } 452 *bp = '\0'; 453 return (cp); 454 } 455 456 /* 457 * Canonicalize file names to always start with ``./'' and 458 * remove any imbedded "." and ".." components. 459 */ 460 void 461 canon(const char *rawname, char *canonname) 462 { 463 char *cp, *np; 464 465 if (strcmp(rawname, ".") == 0 || strncmp(rawname, "./", 2) == 0) 466 (void) strcpy(canonname, ""); 467 else if (rawname[0] == '/') 468 (void) strcpy(canonname, "."); 469 else 470 (void) strcpy(canonname, "./"); 471 (void) strcat(canonname, rawname); 472 /* 473 * Eliminate multiple and trailing '/'s 474 */ 475 for (cp = np = canonname; *np != '\0'; cp++) { 476 *cp = *np++; 477 while (*cp == '/' && *np == '/') 478 np++; 479 } 480 *cp = '\0'; 481 if (*--cp == '/') 482 *cp = '\0'; 483 /* 484 * Eliminate extraneous "." and ".." from pathnames. 485 */ 486 for (np = canonname; *np != '\0'; ) { 487 np++; 488 cp = np; 489 while (*np != '/' && *np != '\0') 490 np++; 491 if (np - cp == 1 && *cp == '.') { 492 cp--; 493 (void) strcpy(cp, np); 494 np = cp; 495 } 496 if (np - cp == 2 && strncmp(cp, "..", 2) == 0) { 497 cp--; 498 while (cp > &canonname[1] && *--cp != '/') 499 /* find beginning of name */; 500 (void) strcpy(cp, np); 501 np = cp; 502 } 503 } 504 } 505 506 /* 507 * Do an "ls" style listing of a directory 508 */ 509 static void 510 printlist(char *name, char *basename) 511 { 512 struct afile *fp, *list, *listp; 513 struct direct *dp; 514 struct afile single; 515 RST_DIR *dirp; 516 int entries, len, namelen; 517 char locname[MAXPATHLEN + 1]; 518 519 dp = pathsearch(name); 520 listp = NULL; 521 if (dp == NULL || (!dflag && TSTINO(dp->d_ino, dumpmap) == 0) || 522 (!vflag && dp->d_ino == UFS_WINO)) 523 return; 524 if ((dirp = rst_opendir(name)) == NULL) { 525 entries = 1; 526 list = &single; 527 mkentry(name, dp, list); 528 len = strlen(basename) + 1; 529 if (strlen(name) - len > single.len) { 530 freename(single.fname); 531 single.fname = savename(&name[len]); 532 single.len = strlen(single.fname); 533 } 534 } else { 535 entries = 0; 536 while ((dp = rst_readdir(dirp)) != NULL) 537 entries++; 538 rst_closedir(dirp); 539 list = (struct afile *)malloc(entries * sizeof(struct afile)); 540 if (list == NULL) { 541 fprintf(stderr, "ls: out of memory\n"); 542 return; 543 } 544 if ((dirp = rst_opendir(name)) == NULL) 545 panic("directory reopen failed\n"); 546 fprintf(stderr, "%s:\n", name); 547 entries = 0; 548 listp = list; 549 (void) strncpy(locname, name, MAXPATHLEN); 550 (void) strncat(locname, "/", MAXPATHLEN); 551 namelen = strlen(locname); 552 while ((dp = rst_readdir(dirp)) != NULL) { 553 if (!dflag && TSTINO(dp->d_ino, dumpmap) == 0) 554 continue; 555 if (!vflag && (dp->d_ino == UFS_WINO || 556 strcmp(dp->d_name, ".") == 0 || 557 strcmp(dp->d_name, "..") == 0)) 558 continue; 559 locname[namelen] = '\0'; 560 if (namelen + dp->d_namlen >= MAXPATHLEN) { 561 fprintf(stderr, "%s%s: name exceeds %d char\n", 562 locname, dp->d_name, MAXPATHLEN); 563 } else { 564 (void) strncat(locname, dp->d_name, 565 (int)dp->d_namlen); 566 mkentry(locname, dp, listp++); 567 entries++; 568 } 569 } 570 rst_closedir(dirp); 571 if (entries == 0) { 572 fprintf(stderr, "\n"); 573 free(list); 574 return; 575 } 576 qsort((char *)list, entries, sizeof(struct afile), fcmp); 577 } 578 formatf(list, entries); 579 if (dirp != NULL) { 580 for (fp = listp - 1; fp >= list; fp--) 581 freename(fp->fname); 582 fprintf(stderr, "\n"); 583 free(list); 584 } 585 } 586 587 /* 588 * Read the contents of a directory. 589 */ 590 static void 591 mkentry(char *name, struct direct *dp, struct afile *fp) 592 { 593 char *cp; 594 struct entry *np; 595 596 fp->fnum = dp->d_ino; 597 fp->fname = savename(dp->d_name); 598 for (cp = fp->fname; *cp; cp++) 599 if (!vflag && (*cp < ' ' || *cp >= 0177)) 600 *cp = '?'; 601 fp->len = cp - fp->fname; 602 if (dflag && TSTINO(fp->fnum, dumpmap) == 0) 603 fp->prefix = '^'; 604 else if ((np = lookupname(name)) != NULL && (np->e_flags & NEW)) 605 fp->prefix = '*'; 606 else 607 fp->prefix = ' '; 608 switch(dp->d_type) { 609 610 default: 611 fprintf(stderr, "Warning: undefined file type %d\n", 612 dp->d_type); 613 /* fall through */ 614 case DT_REG: 615 fp->postfix = ' '; 616 break; 617 618 case DT_LNK: 619 fp->postfix = '@'; 620 break; 621 622 case DT_FIFO: 623 case DT_SOCK: 624 fp->postfix = '='; 625 break; 626 627 case DT_CHR: 628 case DT_BLK: 629 fp->postfix = '#'; 630 break; 631 632 case DT_WHT: 633 fp->postfix = '%'; 634 break; 635 636 case DT_UNKNOWN: 637 case DT_DIR: 638 if (inodetype(dp->d_ino) == NODE) 639 fp->postfix = '/'; 640 else 641 fp->postfix = ' '; 642 break; 643 } 644 return; 645 } 646 647 /* 648 * Print out a pretty listing of a directory 649 */ 650 static void 651 formatf(struct afile *list, int nentry) 652 { 653 struct afile *fp, *endlist; 654 int haveprefix, havepostfix; 655 ino_t bigino; 656 size_t width, w; 657 int i, j, precision, columns, lines; 658 659 width = 0; 660 haveprefix = 0; 661 havepostfix = 0; 662 precision = 0; 663 bigino = UFS_ROOTINO; 664 endlist = &list[nentry]; 665 for (fp = &list[0]; fp < endlist; fp++) { 666 if (bigino < fp->fnum) 667 bigino = fp->fnum; 668 if (width < fp->len) 669 width = fp->len; 670 if (fp->prefix != ' ') 671 haveprefix = 1; 672 if (fp->postfix != ' ') 673 havepostfix = 1; 674 } 675 if (haveprefix) 676 width++; 677 if (havepostfix) 678 width++; 679 if (vflag) { 680 for (precision = 0, i = bigino; i > 0; i /= 10) 681 precision++; 682 width += precision + 1; 683 } 684 width++; 685 columns = 81 / width; 686 if (columns == 0) 687 columns = 1; 688 lines = (nentry + columns - 1) / columns; 689 for (i = 0; i < lines; i++) { 690 for (j = 0; j < columns; j++) { 691 fp = &list[j * lines + i]; 692 if (vflag) { 693 fprintf(stderr, "%*llu ", precision, 694 (unsigned long long)fp->fnum); 695 fp->len += precision + 1; 696 } 697 if (haveprefix) { 698 putc(fp->prefix, stderr); 699 fp->len++; 700 } 701 fprintf(stderr, "%s", fp->fname); 702 if (havepostfix) { 703 putc(fp->postfix, stderr); 704 fp->len++; 705 } 706 if (fp + lines >= endlist) { 707 fprintf(stderr, "\n"); 708 break; 709 } 710 for (w = fp->len; w < width; w++) 711 putc(' ', stderr); 712 } 713 } 714 } 715 716 /* 717 * Skip over directory entries that are not on the tape 718 * 719 * First have to get definition of a dirent. 720 */ 721 #undef DIRBLKSIZ 722 #include <dirent.h> 723 #undef d_ino 724 725 struct dirent * 726 glob_readdir(RST_DIR *dirp) 727 { 728 struct direct *dp; 729 static struct dirent adirent; 730 731 while ((dp = rst_readdir(dirp)) != NULL) { 732 if (!vflag && dp->d_fileno == UFS_WINO) 733 continue; 734 if (dflag || TSTINO(dp->d_fileno, dumpmap)) 735 break; 736 } 737 if (dp == NULL) 738 return (NULL); 739 adirent.d_fileno = dp->d_fileno; 740 adirent.d_namlen = dp->d_namlen; 741 memmove(adirent.d_name, dp->d_name, dp->d_namlen + 1); 742 return (&adirent); 743 } 744 745 /* 746 * Return st_mode information in response to stat or lstat calls 747 */ 748 static int 749 glob_stat(const char *name, struct stat *stp) 750 { 751 struct direct *dp; 752 753 dp = pathsearch(name); 754 if (dp == NULL || (!dflag && TSTINO(dp->d_fileno, dumpmap) == 0) || 755 (!vflag && dp->d_fileno == UFS_WINO)) 756 return (-1); 757 if (inodetype(dp->d_fileno) == NODE) 758 stp->st_mode = S_IFDIR; 759 else 760 stp->st_mode = S_IFREG; 761 return (0); 762 } 763 764 /* 765 * Comparison routine for qsort. 766 */ 767 static int 768 fcmp(const void *f1, const void *f2) 769 { 770 return (strcmp(((const struct afile *)f1)->fname, 771 ((const struct afile *)f2)->fname)); 772 } 773 774 /* 775 * respond to interrupts 776 */ 777 void 778 /*ARGSUSED*/ 779 onintr(int signo __unused) 780 { 781 if (command == 'i' && runshell) 782 longjmp(reset, 1); 783 if (reply("restore interrupted, continue") == FAIL) 784 exit(1); 785 } 786