/* * ex_shell -- :sh[ell] * Invoke the program named in the SHELL environment variable * with the argument -i. * * PUBLIC: int ex_shell __P((SCR *, EXCMD *)); */ int ex_shell(SCR *sp, EXCMD *cmdp) { int rval; char buf[MAXPATHLEN]; /* We'll need a shell. */ if (opts_empty(sp, O_SHELL, 0)) return (1); /* * XXX * Assumes all shells use -i. */ (void)snprintf(buf, sizeof(buf), "%s -i", O_STR(sp, O_SHELL)); /* Restore the window name. */ (void)sp->gp->scr_rename(sp, NULL, 0); /* If we're still in a vi screen, move out explicitly. */ rval = ex_exec_proc(sp, cmdp, buf, NULL, !F_ISSET(sp, SC_SCR_EXWROTE)); /* Set the window name. */ (void)sp->gp->scr_rename(sp, sp->frp->name, 1); /* * !!! * Historically, vi didn't require a continue message after the * return of the shell. Match it. */ F_SET(sp, SC_EX_WAIT_NO); return (rval); }
/* * ex_script -- : sc[ript][!] [file] * Switch to script mode. * * PUBLIC: int ex_script __P((SCR *, EXCMD *)); */ int ex_script(SCR *sp, EXCMD *cmdp) { /* Vi only command. */ if (!F_ISSET(sp, SC_VI)) { msgq(sp, M_ERR, "150|The script command is only available in vi mode"); return (1); } /* Avoid double run. */ if (F_ISSET(sp, SC_SCRIPT)) { msgq(sp, M_ERR, "The script command is already runninng"); return (1); } /* We're going to need a shell. */ if (opts_empty(sp, O_SHELL, 0)) return (1); /* Switch to the new file. */ if (cmdp->argc != 0 && ex_edit(sp, cmdp)) return (1); /* Create the shell, figure out the prompt. */ if (sscr_init(sp)) return (1); return (0); }
/* * rcv_tmp -- * Build a file name that will be used as the recovery file. * * PUBLIC: int rcv_tmp __P((SCR *, EXF *, char *)); */ int rcv_tmp(SCR *sp, EXF *ep, char *name) { struct stat sb; int fd; char path[MAXPATHLEN]; const char *dp; /* * !!! * ep MAY NOT BE THE SAME AS sp->ep, DON'T USE THE LATTER. * * * If the recovery directory doesn't exist, try and create it. As * the recovery files are themselves protected from reading/writing * by other than the owner, the worst that can happen is that a user * would have permission to remove other user's recovery files. If * the sticky bit has the BSD semantics, that too will be impossible. */ if (opts_empty(sp, O_RECDIR, 0)) goto err; dp = O_STR(sp, O_RECDIR); if (stat(dp, &sb)) { if (errno != ENOENT || mkdir(dp, 0)) { msgq(sp, M_SYSERR, "%s", dp); goto err; } (void)chmod(dp, S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX); } /* Newlines delimit the mail messages. */ if (strchr(name, '\n')) { msgq(sp, M_ERR, "055|Files with newlines in the name are unrecoverable"); goto err; } (void)snprintf(path, sizeof(path), "%s/vi.XXXXXX", dp); if ((fd = rcv_mktemp(sp, path, dp, S_IRWXU)) == -1) goto err; (void)close(fd); if ((ep->rcv_path = strdup(path)) == NULL) { msgq(sp, M_SYSERR, NULL); (void)unlink(path); err: msgq(sp, M_ERR, "056|Modifications not recoverable if the session fails"); return (1); } /* We believe the file is recoverable. */ F_SET(ep, F_RCV_ON); return (0); }
/* * ex_exec_proc -- * Run a separate process. * * PUBLIC: int ex_exec_proc __P((SCR *, EXCMD *, const char *, const char *, int)); */ int ex_exec_proc(SCR *sp, EXCMD *cmdp, const char *cmd, const char *msg, int need_newline) { GS *gp; const char *name; pid_t pid; gp = sp->gp; /* We'll need a shell. */ if (opts_empty(sp, O_SHELL, 0)) return (1); /* Enter ex mode. */ if (F_ISSET(sp, SC_VI)) { if (gp->scr_screen(sp, SC_EX)) { ex_wemsg(sp, cmdp->cmd->name, EXM_NOCANON); return (1); } (void)gp->scr_attr(sp, SA_ALTERNATE, 0); F_SET(sp, SC_SCR_EX | SC_SCR_EXWROTE); } /* Put out additional newline, message. */ if (need_newline) (void)ex_puts(sp, "\n"); if (msg != NULL) { (void)ex_puts(sp, msg); (void)ex_puts(sp, "\n"); } (void)ex_fflush(sp); switch (pid = vfork()) { case -1: /* Error. */ msgq(sp, M_SYSERR, "vfork"); return (1); case 0: /* Utility. */ if (gp->scr_child) gp->scr_child(sp); if ((name = strrchr(O_STR(sp, O_SHELL), '/')) == NULL) name = O_STR(sp, O_SHELL); else ++name; execl(O_STR(sp, O_SHELL), name, "-c", cmd, (char *)NULL); msgq_str(sp, M_SYSERR, O_STR(sp, O_SHELL), "execl: %s"); _exit(127); /* NOTREACHED */ default: /* Parent. */ return (proc_wait(sp, (long)pid, cmd, 0, 0)); } /* NOTREACHED */ }
/* * cl_ex_init -- * Initialize the ex screen. */ static int cl_ex_init(SCR *sp) { CL_PRIVATE *clp; int error; const char *ttype; clp = CLP(sp); /* If already initialized, just set the terminal modes. */ if (F_ISSET(clp, CL_SCR_EX_INIT)) goto fast; /* If not reading from a file, we're done. */ if (!F_ISSET(clp, CL_STDIN_TTY)) return (0); if (F_ISSET(clp, CL_CHANGE_TERM)) { if (F_ISSET(clp, CL_SETUPTERM) && del_curterm(cur_term)) return (1); F_CLR(clp, CL_SETUPTERM | CL_CHANGE_TERM); } if (!F_ISSET(clp, CL_SETUPTERM)) { /* We'll need a terminal type. */ if (opts_empty(sp, O_TERM, 0)) return (1); ttype = O_STR(sp, O_TERM); (void)setupterm(ttype, STDOUT_FILENO, &error); if (error == 0 || error == -1) return (1); } /* Get the ex termcap/terminfo strings. */ (void)cl_getcap(sp, "cup", &clp->cup); (void)cl_getcap(sp, "smso", &clp->smso); (void)cl_getcap(sp, "rmso", &clp->rmso); (void)cl_getcap(sp, "el", &clp->el); (void)cl_getcap(sp, "cuu1", &clp->cuu1); /* Enter_standout_mode and exit_standout_mode are paired. */ if (clp->smso == NULL || clp->rmso == NULL) { if (clp->smso != NULL) { free(clp->smso); clp->smso = NULL; } if (clp->rmso != NULL) { free(clp->rmso); clp->rmso = NULL; } } /* * Turn on canonical mode, with normal input and output processing. * Start with the original terminal settings as the user probably * had them (including any local extensions) set correctly for the * current terminal. * * !!! * We can't get everything that we need portably; for example, ONLCR, * mapping <newline> to <carriage-return> on output isn't required * by POSIX 1003.1b-1993. If this turns out to be a problem, then * we'll either have to play some games on the mapping, or we'll have * to make all ex printf's output \r\n instead of \n. */ clp->ex_enter = clp->orig; clp->ex_enter.c_lflag |= ECHO | ECHOE | ECHOK | ICANON | IEXTEN | ISIG; #ifdef ECHOCTL clp->ex_enter.c_lflag |= ECHOCTL; #endif #ifdef ECHOKE clp->ex_enter.c_lflag |= ECHOKE; #endif clp->ex_enter.c_iflag |= ICRNL; clp->ex_enter.c_oflag |= OPOST; #ifdef ONLCR clp->ex_enter.c_oflag |= ONLCR; #endif fast: if (tcsetattr(STDIN_FILENO, TCSADRAIN | TCSASOFT, &clp->ex_enter)) { if (errno == EINTR) goto fast; msgq(sp, M_SYSERR, "tcsetattr"); return (1); } return (0); }
/* * cl_vi_init -- * Initialize the curses vi screen. */ static int cl_vi_init(SCR *sp) { CL_PRIVATE *clp; char *o_cols, *o_lines, *o_term; const char *ttype; clp = CLP(sp); /* If already initialized, just set the terminal modes. */ if (F_ISSET(clp, CL_SCR_VI_INIT)) goto fast; /* Curses vi always reads from (and writes to) a terminal. */ if (!F_ISSET(clp, CL_STDIN_TTY) || !isatty(STDOUT_FILENO)) { msgq(sp, M_ERR, "016|Vi's standard input and output must be a terminal"); return (1); } /* We'll need a terminal type. */ if (opts_empty(sp, O_TERM, 0)) return (1); ttype = O_STR(sp, O_TERM); /* * XXX * Changing the row/column and terminal values is done by putting them * into the environment, which is then read by curses. What this loses * in ugliness, it makes up for in stupidity. We can't simply put the * values into the environment ourselves, because in the presence of a * kernel mechanism for returning the window size, entering values into * the environment will screw up future screen resizing events, e.g. if * the user enters a :shell command and then resizes their window. So, * if they weren't already in the environment, we make sure to delete * them immediately after setting them. * * XXX * Putting the TERM variable into the environment is necessary, even * though we're using newterm() here. We may be using initscr() as * the underlying function. */ o_term = getenv("TERM"); cl_putenv(sp, "TERM", ttype, 0); o_lines = getenv("LINES"); cl_putenv(sp, "LINES", NULL, (u_long)O_VAL(sp, O_LINES)); o_cols = getenv("COLUMNS"); cl_putenv(sp, "COLUMNS", NULL, (u_long)O_VAL(sp, O_COLUMNS)); /* Delete cur_term if exists. */ if (F_ISSET(clp, CL_SETUPTERM)) { if (del_curterm(cur_term)) return (1); F_CLR(clp, CL_SETUPTERM); } /* * XXX * The SunOS initscr() can't be called twice. Don't even think about * using it. It fails in subtle ways (e.g. select(2) on fileno(stdin) * stops working). (The SVID notes that applications should only call * initscr() once.) * * XXX * The HP/UX newterm doesn't support the NULL first argument, so we * have to specify the terminal type. */ errno = 0; if ((clp->screen = newterm(__UNCONST(ttype), stdout, stdin)) == NULL) { if (errno) msgq(sp, M_SYSERR, "%s", ttype); else msgq(sp, M_ERR, "%s: unknown terminal type", ttype); return (1); } if (o_term == NULL) cl_unsetenv(sp, "TERM"); if (o_lines == NULL) cl_unsetenv(sp, "LINES"); if (o_cols == NULL) cl_unsetenv(sp, "COLUMNS"); /* * XXX * Someone got let out alone without adult supervision -- the SunOS * newterm resets the signal handlers. There's a race, but it's not * worth closing. */ (void)sig_init(sp->gp, sp); /* * We use raw mode. What we want is 8-bit clean, however, signals * and flow control should continue to work. Admittedly, it sounds * like cbreak, but it isn't. Using cbreak() can get you additional * things like IEXTEN, which turns on flags like DISCARD and LNEXT. * * !!! * If raw isn't turning off echo and newlines, something's wrong. * However, it shouldn't hurt. */ noecho(); /* No character echo. */ nonl(); /* No CR/NL translation. */ raw(); /* 8-bit clean. */ idlok(stdscr, 1); /* Use hardware insert/delete line. */ /* Put the cursor keys into application mode. */ (void)keypad(stdscr, TRUE); /* * XXX * The screen TI sequence just got sent. See the comment in * cl_funcs.c:cl_attr(). */ clp->ti_te = TI_SENT; /* * XXX * Historic implementations of curses handled SIGTSTP signals * in one of three ways. They either: * * 1: Set their own handler, regardless. * 2: Did not set a handler if a handler was already installed. * 3: Set their own handler, but then called any previously set * handler after completing their own cleanup. * * We don't try and figure out which behavior is in place, we force * it to SIG_DFL after initializing the curses interface, which means * that curses isn't going to take the signal. Since curses isn't * reentrant (i.e., the whole curses SIGTSTP interface is a fantasy), * we're doing The Right Thing. */ (void)signal(SIGTSTP, SIG_DFL); /* * If flow control was on, turn it back on. Turn signals on. ISIG * turns on VINTR, VQUIT, VDSUSP and VSUSP. The main curses code * already installed a handler for VINTR. We're going to disable the * other three. * * XXX * We want to use ^Y as a vi scrolling command. If the user has the * DSUSP character set to ^Y (common practice) clean it up. As it's * equally possible that the user has VDSUSP set to 'a', we disable * it regardless. It doesn't make much sense to suspend vi at read, * so I don't think anyone will care. Alternatively, we could look * it up in the table of legal command characters and turn it off if * it matches one. VDSUSP wasn't in POSIX 1003.1-1990, so we test for * it. * * XXX * We don't check to see if the user had signals enabled originally. * If they didn't, it's unclear what we're supposed to do here, but * it's also pretty unlikely. */ if (tcgetattr(STDIN_FILENO, &clp->vi_enter)) { msgq(sp, M_SYSERR, "tcgetattr"); goto err; } if (clp->orig.c_iflag & IXON) clp->vi_enter.c_iflag |= IXON; if (clp->orig.c_iflag & IXOFF) clp->vi_enter.c_iflag |= IXOFF; clp->vi_enter.c_lflag |= ISIG; #ifdef VDSUSP clp->vi_enter.c_cc[VDSUSP] = _POSIX_VDISABLE; #endif clp->vi_enter.c_cc[VQUIT] = _POSIX_VDISABLE; clp->vi_enter.c_cc[VSUSP] = _POSIX_VDISABLE; /* * XXX * OSF/1 doesn't turn off the <discard>, <literal-next> or <status> * characters when curses switches into raw mode. It should be OK * to do it explicitly for everyone. */ #ifdef VDISCARD clp->vi_enter.c_cc[VDISCARD] = _POSIX_VDISABLE; #endif #ifdef VLNEXT clp->vi_enter.c_cc[VLNEXT] = _POSIX_VDISABLE; #endif #ifdef VSTATUS clp->vi_enter.c_cc[VSTATUS] = _POSIX_VDISABLE; #endif /* Initialize terminal based information. */ if (cl_term_init(sp)) goto err; fast: /* Set the terminal modes. */ if (tcsetattr(STDIN_FILENO, TCSASOFT | TCSADRAIN, &clp->vi_enter)) { msgq(sp, M_SYSERR, "tcsetattr"); err: (void)cl_vi_end(sp->gp); return (1); } return (0); }
/* * sscr_init -- * Create a pty setup for a shell. */ static int sscr_init(SCR *sp) { SCRIPT *sc; char *sh, *sh_path; /* We're going to need a shell. */ if (opts_empty(sp, O_SHELL, 0)) return (1); MALLOC_RET(sp, sc, sizeof(SCRIPT)); sp->script = sc; sc->sh_prompt = NULL; sc->sh_prompt_len = 0; /* * There are two different processes running through this code. * They are the shell and the parent. */ sc->sh_master = sc->sh_slave = -1; if (tcgetattr(STDIN_FILENO, &sc->sh_term) == -1) { msgq(sp, M_SYSERR, "tcgetattr"); goto err; } /* * Turn off output postprocessing and echo. */ sc->sh_term.c_oflag &= ~OPOST; sc->sh_term.c_cflag &= ~(ECHO|ECHOE|ECHONL|ECHOK); if (ioctl(STDIN_FILENO, TIOCGWINSZ, &sc->sh_win) == -1) { msgq(sp, M_SYSERR, "tcgetattr"); goto err; } if (openpty(&sc->sh_master, &sc->sh_slave, sc->sh_name, &sc->sh_term, &sc->sh_win) == -1) { msgq(sp, M_SYSERR, "pty"); goto err; } /* * __TK__ huh? * Don't use vfork() here, because the signal semantics differ from * implementation to implementation. */ switch (sc->sh_pid = fork()) { case -1: /* Error. */ msgq(sp, M_SYSERR, "fork"); err: if (sc->sh_master != -1) (void)close(sc->sh_master); if (sc->sh_slave != -1) (void)close(sc->sh_slave); return (1); case 0: /* Utility. */ /* * XXX * So that shells that do command line editing turn it off. */ if (setenv("TERM", "emacs", 1) == -1 || setenv("TERMCAP", "emacs:", 1) == -1 || setenv("EMACS", "t", 1) == -1) _exit(126); (void)setsid(); /* * 4.4BSD allocates a controlling terminal using the TIOCSCTTY * ioctl, not by opening a terminal device file. POSIX 1003.1 * doesn't define a portable way to do this. */ (void)ioctl(sc->sh_slave, TIOCSCTTY, 0); (void)close(sc->sh_master); (void)dup2(sc->sh_slave, STDIN_FILENO); (void)dup2(sc->sh_slave, STDOUT_FILENO); (void)dup2(sc->sh_slave, STDERR_FILENO); (void)close(sc->sh_slave); /* Assumes that all shells have -i. */ sh_path = O_STR(sp, O_SHELL); if ((sh = strrchr(sh_path, '/')) == NULL) sh = sh_path; else ++sh; execl(sh_path, sh, "-i", (char *)NULL); msgq_str(sp, M_SYSERR, sh_path, "execl: %s"); _exit(127); default: /* Parent. */ break; } if (sscr_getprompt(sp)) return (1); F_SET(sp, SC_SCRIPT); F_SET(sp->gp, G_SCRWIN); return (0); }
/* * file_write -- * Write the file to disk. Historic vi had fairly convoluted * semantics for whether or not writes would happen. That's * why all the flags. * * PUBLIC: int file_write __P((SCR *, MARK *, MARK *, char *, int)); */ int file_write(SCR *sp, MARK *fm, MARK *tm, char *name, int flags) { enum { NEWFILE, OLDFILE } mtype; struct stat sb; EXF *ep; FILE *fp; FREF *frp; MARK from, to; size_t len; u_long nlno, nch; int fd, nf, noname, oflags, rval; char *p, *s, *t, buf[MAXPATHLEN + 64]; const char *msgstr; ep = sp->ep; frp = sp->frp; /* * Writing '%', or naming the current file explicitly, has the * same semantics as writing without a name. */ if (name == NULL || !strcmp(name, frp->name)) { noname = 1; name = frp->name; } else noname = 0; /* Can't write files marked read-only, unless forced. */ if (!LF_ISSET(FS_FORCE) && noname && O_ISSET(sp, O_READONLY)) { msgq(sp, M_ERR, LF_ISSET(FS_POSSIBLE) ? "244|Read-only file, not written; use ! to override" : "245|Read-only file, not written"); return (1); } /* If not forced, not appending, and "writeany" not set ... */ if (!LF_ISSET(FS_FORCE | FS_APPEND) && !O_ISSET(sp, O_WRITEANY)) { /* Don't overwrite anything but the original file. */ if ((!noname || F_ISSET(frp, FR_NAMECHANGE)) && !stat(name, &sb)) { msgq_str(sp, M_ERR, name, LF_ISSET(FS_POSSIBLE) ? "246|%s exists, not written; use ! to override" : "247|%s exists, not written"); return (1); } /* * Don't write part of any existing file. Only test for the * original file, the previous test catches anything else. */ if (!LF_ISSET(FS_ALL) && noname && !stat(name, &sb)) { msgq(sp, M_ERR, LF_ISSET(FS_POSSIBLE) ? "248|Partial file, not written; use ! to override" : "249|Partial file, not written"); return (1); } } /* * Figure out if the file already exists -- if it doesn't, we display * the "new file" message. The stat might not be necessary, but we * just repeat it because it's easier than hacking the previous tests. * The information is only used for the user message and modification * time test, so we can ignore the obvious race condition. * * One final test. If we're not forcing or appending the current file, * and we have a saved modification time, object if the file changed * since we last edited or wrote it, and make them force it. */ if (stat(name, &sb)) mtype = NEWFILE; else { if (noname && !LF_ISSET(FS_FORCE | FS_APPEND) && ((F_ISSET(ep, F_DEVSET) && (sb.st_dev != ep->mdev || sb.st_ino != ep->minode)) || sb.st_mtime != ep->mtime)) { msgq_str(sp, M_ERR, name, LF_ISSET(FS_POSSIBLE) ? "250|%s: file modified more recently than this copy; use ! to override" : "251|%s: file modified more recently than this copy"); return (1); } mtype = OLDFILE; } /* Set flags to create, write, and either append or truncate. */ oflags = O_CREAT | O_WRONLY | (LF_ISSET(FS_APPEND) ? O_APPEND : O_TRUNC); /* Backup the file if requested. */ if (!opts_empty(sp, O_BACKUP, 1) && file_backup(sp, name, O_STR(sp, O_BACKUP)) && !LF_ISSET(FS_FORCE)) return (1); /* Open the file. */ SIGBLOCK; if ((fd = open(name, oflags, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)) < 0) { msgq_str(sp, M_SYSERR, name, "%s"); SIGUNBLOCK; return (1); } SIGUNBLOCK; /* Try and get a lock. */ if (!noname && file_lock(sp, NULL, NULL, fd, 0) == LOCK_UNAVAIL) msgq_str(sp, M_ERR, name, "252|%s: write lock was unavailable"); #if __linux__ /* * XXX * In libc 4.5.x, fdopen(fd, "w") clears the O_APPEND flag (if set). * This bug is fixed in libc 4.6.x. * * This code works around this problem for libc 4.5.x users. * Note that this code is harmless if you're using libc 4.6.x. */ if (LF_ISSET(FS_APPEND) && lseek(fd, (off_t)0, SEEK_END) < 0) { msgq(sp, M_SYSERR, name); return (1); } #endif /* * Use stdio for buffering. * * XXX * SVR4.2 requires the fdopen mode exactly match the original open * mode, i.e. you have to open with "a" if appending. */ if ((fp = fdopen(fd, LF_ISSET(FS_APPEND) ? "a" : "w")) == NULL) { msgq_str(sp, M_SYSERR, name, "%s"); (void)close(fd); return (1); } /* Build fake addresses, if necessary. */ if (fm == NULL) { from.lno = 1; from.cno = 0; fm = &from; if (db_last(sp, &to.lno)) return (1); to.cno = 0; tm = &to; } rval = ex_writefp(sp, name, fp, fm, tm, &nlno, &nch, 0); /* * Save the new last modification time -- even if the write fails * we re-init the time. That way the user can clean up the disk * and rewrite without having to force it. */ if (noname) { if (stat(name, &sb)) time(&ep->mtime); else { F_SET(ep, F_DEVSET); ep->mdev = sb.st_dev; ep->minode = sb.st_ino; ep->mtime = sb.st_mtime; } } /* * If the write failed, complain loudly. ex_writefp() has already * complained about the actual error, reinforce it if data was lost. */ if (rval) { if (!LF_ISSET(FS_APPEND)) msgq_str(sp, M_ERR, name, "254|%s: WARNING: FILE TRUNCATED"); return (1); } /* * Once we've actually written the file, it doesn't matter that the * file name was changed -- if it was, we've already whacked it. */ F_CLR(frp, FR_NAMECHANGE); /* * If wrote the entire file, and it wasn't by appending it to a file, * clear the modified bit. If the file was written to the original * file name and the file is a temporary, set the "no exit" bit. This * permits the user to write the file and use it in the context of the * filesystem, but still keeps them from discarding their changes by * exiting. */ if (LF_ISSET(FS_ALL) && !LF_ISSET(FS_APPEND)) { F_CLR(ep, F_MODIFIED); if (F_ISSET(frp, FR_TMPFILE)) { if (noname) F_SET(frp, FR_TMPEXIT); else F_CLR(frp, FR_TMPEXIT); } } p = msg_print(sp, name, &nf); switch (mtype) { case NEWFILE: msgstr = msg_cat(sp, "256|%s: new file: %lu lines, %lu characters", NULL); len = snprintf(buf, sizeof(buf), msgstr, p, nlno, nch); break; case OLDFILE: msgstr = msg_cat(sp, LF_ISSET(FS_APPEND) ? "315|%s: appended: %lu lines, %lu characters" : "257|%s: %lu lines, %lu characters", NULL); len = snprintf(buf, sizeof(buf), msgstr, p, nlno, nch); break; default: abort(); } /* * There's a nasty problem with long path names. Cscope and tags files * can result in long paths and vi will request a continuation key from * the user. Unfortunately, the user has typed ahead, and chaos will * result. If we assume that the characters in the filenames only take * a single screen column each, we can trim the filename. */ s = buf; if (len >= sp->cols) { for (s = buf, t = buf + strlen(p); s < t && (*s != '/' || len >= sp->cols - 3); ++s, --len); if (s == t) s = buf; else { *--s = '.'; /* Leading ellipses. */ *--s = '.'; *--s = '.'; } } msgq(sp, M_INFO, s); if (nf) FREE_SPACE(sp, p, 0); return (0); }
/* * file_init -- * Start editing a file, based on the FREF structure. If successsful, * let go of any previous file. Don't release the previous file until * absolutely sure we have the new one. * * PUBLIC: int file_init __P((SCR *, FREF *, char *, int)); */ int file_init(SCR *sp, FREF *frp, char *rcv_name, int flags) { EXF *ep; struct stat sb; size_t psize; int fd, exists, open_err, readonly, stolen; char *oname, tname[MAXPATHLEN]; stolen = open_err = readonly = 0; /* * If the file is a recovery file, let the recovery code handle it. * Clear the FR_RECOVER flag first -- the recovery code does set up, * and then calls us! If the recovery call fails, it's probably * because the named file doesn't exist. So, move boldly forward, * presuming that there's an error message the user will get to see. */ if (F_ISSET(frp, FR_RECOVER)) { F_CLR(frp, FR_RECOVER); return (rcv_read(sp, frp)); } /* * Required FRP initialization; the only flag we keep is the * cursor information. */ F_CLR(frp, ~FR_CURSORSET); /* * Scan the user's path to find the file that we're going to * try and open. */ if (file_spath(sp, frp, &sb, &exists)) return (1); /* * Check whether we already have this file opened in some * other screen. */ if (exists) { EXF *exfp; for (exfp = sp->gp->exfq.cqh_first; exfp != (EXF *)&sp->gp->exfq; exfp = exfp->q.cqe_next) { if (exfp->mdev == sb.st_dev && exfp->minode == sb.st_ino && (exfp != sp->ep || exfp->refcnt > 1)) { ep = exfp; goto postinit; } } } /* * Required EXF initialization: * Flush the line caches. * Default recover mail file fd to -1. * Set initial EXF flag bits. */ CALLOC_RET(sp, ep, EXF *, 1, sizeof(EXF)); CIRCLEQ_INIT(&ep->scrq); sp->c_lno = ep->c_nlines = OOBLNO; ep->rcv_fd = ep->fcntl_fd = -1; F_SET(ep, F_FIRSTMODIFY); /* * If no name or backing file, for whatever reason, create a backing * temporary file, saving the temp file name so we can later unlink * it. If the user never named this file, copy the temporary file name * to the real name (we display that until the user renames it). */ oname = frp->name; if (LF_ISSET(FS_OPENERR) || oname == NULL || !exists) { if (opts_empty(sp, O_TMP_DIRECTORY, 0)) goto err; (void)snprintf(tname, sizeof(tname), "%s/vi.XXXXXX", O_STR(sp, O_TMP_DIRECTORY)); if ((fd = mkstemp(tname)) == -1) { msgq(sp, M_SYSERR, "237|Unable to create temporary file"); goto err; } (void)close(fd); if (frp->name == NULL) F_SET(frp, FR_TMPFILE); if ((frp->tname = strdup(tname)) == NULL || (frp->name == NULL && (frp->name = strdup(tname)) == NULL)) { if (frp->tname != NULL) { free(frp->tname); } msgq(sp, M_SYSERR, NULL); (void)unlink(tname); goto err; } oname = frp->tname; psize = 1024; if (!LF_ISSET(FS_OPENERR)) F_SET(frp, FR_NEWFILE); time(&ep->mtime); } else { /* * XXX * A seat of the pants calculation: try to keep the file in * 15 pages or less. Don't use a page size larger than 10K * (vi should have good locality) or smaller than 1K. */ psize = ((sb.st_size / 15) + 1023) / 1024; if (psize > 10) psize = 10; if (psize == 0) psize = 1; psize *= 1024; F_SET(ep, F_DEVSET); ep->mdev = sb.st_dev; ep->minode = sb.st_ino; ep->mtime = sb.st_mtime; if (!S_ISREG(sb.st_mode)) msgq_str(sp, M_ERR, oname, "238|Warning: %s is not a regular file"); } /* Set up recovery. */ if (rcv_name == NULL) { /* ep->rcv_path NULL if rcv_tmp fails */ rcv_tmp(sp, ep, frp->name); } else { if ((ep->rcv_path = strdup(rcv_name)) == NULL) { msgq(sp, M_SYSERR, NULL); goto err; } F_SET(ep, F_MODIFIED); } if (db_setup(sp, ep)) goto err; /* Open a db structure. */ if ((sp->db_error = db_create(&ep->db, 0, 0)) != 0) { msgq(sp, M_DBERR, "db_create"); goto err; } ep->db->set_re_delim(ep->db, '\n'); /* Always set. */ ep->db->set_pagesize(ep->db, psize); ep->db->set_flags(ep->db, DB_RENUMBER | DB_SNAPSHOT); if (rcv_name == NULL) ep->db->set_re_source(ep->db, oname); /* * Don't let db use mmap when using fcntl for locking */ #ifdef HAVE_LOCK_FCNTL #define NOMMAPIFFCNTL DB_NOMMAP #else #define NOMMAPIFFCNTL 0 #endif #define _DB_OPEN_MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH if ((sp->db_error = db_open(ep->db, ep->rcv_path, DB_RECNO, ((rcv_name == 0) ? DB_TRUNCATE : 0) | VI_DB_THREAD | NOMMAPIFFCNTL, _DB_OPEN_MODE)) != 0) { msgq_str(sp, M_DBERR, rcv_name == NULL ? oname : rcv_name, "%s"); /* * !!! * Historically, vi permitted users to edit files that couldn't * be read. This isn't useful for single files from a command * line, but it's quite useful for "vi *.c", since you can skip * past files that you can't read. */ ep->db = NULL; /* Don't close it; it wasn't opened */ if (LF_ISSET(FS_OPENERR)) goto err; open_err = 1; goto oerr; } /* re_source is loaded into the database. * Close it and reopen it in the environment. */ if ((sp->db_error = ep->db->close(ep->db, 0))) { msgq(sp, M_DBERR, "close"); goto err; } if ((sp->db_error = db_create(&ep->db, ep->env, 0)) != 0) { msgq(sp, M_DBERR, "db_create 2"); goto err; } if ((sp->db_error = db_open(ep->db, ep->rcv_path, DB_RECNO, VI_DB_THREAD | NOMMAPIFFCNTL, _DB_OPEN_MODE)) != 0) { msgq_str(sp, M_DBERR, ep->rcv_path, "%s"); goto err; } /* * Do the remaining things that can cause failure of the new file, * mark and logging initialization. */ if (mark_init(sp, ep) || log_init(sp, ep)) goto err; postinit: /* * Set the alternate file name to be the file we're discarding. * * !!! * Temporary files can't become alternate files, so there's no file * name. This matches historical practice, although it could only * happen in historical vi as the result of the initial command, i.e. * if vi was executed without a file name. */ if (LF_ISSET(FS_SETALT)) set_alt_name(sp, sp->frp == NULL || F_ISSET(sp->frp, FR_TMPFILE) ? NULL : sp->frp->name); /* * Close the previous file; if that fails, close the new one and run * for the border. * * !!! * There's a nasty special case. If the user edits a temporary file, * and then does an ":e! %", we need to re-initialize the backing * file, but we can't change the name. (It's worse -- we're dealing * with *names* here, we can't even detect that it happened.) Set a * flag so that the file_end routine ignores the backing information * of the old file if it happens to be the same as the new one. * * !!! * Side-effect: after the call to file_end(), sp->frp may be NULL. */ if (sp->ep != NULL) { F_SET(frp, FR_DONTDELETE); if (file_end(sp, NULL, LF_ISSET(FS_FORCE))) { (void)file_end(sp, ep, 1); goto err; } sp->ep = NULL; F_CLR(frp, FR_DONTDELETE); } /* * Lock the file; if it's a recovery file, it should already be * locked. Note, we acquire the lock after the previous file * has been ended, so that we don't get an "already locked" error * for ":edit!". * * XXX * While the user can't interrupt us between the open and here, * there's a race between the dbopen() and the lock. Not much * we can do about it. * * XXX * We don't make a big deal of not being able to lock the file. As * locking rarely works over NFS, and often fails if the file was * mmap(2)'d, it's far too common to do anything like print an error * message, let alone make the file readonly. At some future time, * when locking is a little more reliable, this should change to be * an error. */ if (rcv_name == NULL && ep->refcnt == 0) { if ((ep->fd = open(oname, O_RDWR)) == -1) goto no_lock; switch (file_lock(sp, oname, &ep->fcntl_fd, ep->fd, 1)) { case LOCK_FAILED: no_lock: F_SET(frp, FR_UNLOCKED); break; case LOCK_UNAVAIL: readonly = 1; msgq_str(sp, M_INFO, oname, "239|%s already locked, session is read-only"); break; case LOCK_SUCCESS: break; } } /* * Historically, the readonly edit option was set per edit buffer in * vi, unless the -R command-line option was specified or the program * was executed as "view". (Well, to be truthful, if the letter 'w' * occurred anywhere in the program name, but let's not get into that.) * So, the persistant readonly state has to be stored in the screen * structure, and the edit option value toggles with the contents of * the edit buffer. If the persistant readonly flag is set, set the * readonly edit option. * * Otherwise, try and figure out if a file is readonly. This is a * dangerous thing to do. The kernel is the only arbiter of whether * or not a file is writeable, and the best that a user program can * do is guess. Obvious loopholes are files that are on a file system * mounted readonly (access catches this one on a few systems), or * alternate protection mechanisms, ACL's for example, that we can't * portably check. Lots of fun, and only here because users whined. * * !!! * Historic vi displayed the readonly message if none of the file * write bits were set, or if an an access(2) call on the path * failed. This seems reasonable. If the file is mode 444, root * users may want to know that the owner of the file did not expect * it to be written. * * Historic vi set the readonly bit if no write bits were set for * a file, even if the access call would have succeeded. This makes * the superuser force the write even when vi expects that it will * succeed. I'm less supportive of this semantic, but it's historic * practice and the conservative approach to vi'ing files as root. * * It would be nice if there was some way to update this when the user * does a "^Z; chmod ...". The problem is that we'd first have to * distinguish between readonly bits set because of file permissions * and those set for other reasons. That's not too hard, but deciding * when to reevaluate the permissions is trickier. An alternative * might be to turn off the readonly bit if the user forces a write * and it succeeds. * * XXX * Access(2) doesn't consider the effective uid/gid values. This * probably isn't a problem for vi when it's running standalone. */ if (readonly || F_ISSET(sp, SC_READONLY) || (!F_ISSET(frp, FR_NEWFILE) && (!(sb.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH)) || access(frp->name, W_OK)))) O_SET(sp, O_READONLY); else O_CLR(sp, O_READONLY); /* Switch... */ ++ep->refcnt; CIRCLEQ_INSERT_HEAD(&ep->scrq, sp, eq); sp->ep = ep; sp->frp = frp; /* Set the initial cursor position, queue initial command. */ file_cinit(sp); /* Report conversion errors again. */ F_CLR(sp, SC_CONV_ERROR); /* Redraw the screen from scratch, schedule a welcome message. */ F_SET(sp, SC_SCR_REFORMAT | SC_STATUS); if (frp->lno == OOBLNO) F_SET(sp, SC_SCR_TOP); /* Append into the chain of file structures. */ if (ep->refcnt == 1) CIRCLEQ_INSERT_TAIL(&sp->gp->exfq, ep, q); return (0); err: if (frp->name != NULL) { free(frp->name); frp->name = NULL; } if (frp->tname != NULL) { (void)unlink(frp->tname); free(frp->tname); frp->tname = NULL; } oerr: if (F_ISSET(ep, F_RCV_ON)) (void)unlink(ep->rcv_path); if (ep->rcv_path != NULL) { free(ep->rcv_path); ep->rcv_path = NULL; } if (ep->db != NULL) { (void)ep->db->close(ep->db, DB_NOSYNC); ep->db = NULL; } free(ep); return (open_err && !LF_ISSET(FS_OPENERR) ? file_init(sp, frp, rcv_name, flags | FS_OPENERR) : 1); }
/* * rcv_read -- * Start a recovered file as the file to edit. * * PUBLIC: int rcv_read __P((SCR *, FREF *)); */ int rcv_read(SCR *sp, FREF *frp) { struct dirent *dp; struct stat sb; DIR *dirp; EXF *ep; time_t rec_mtime; int fd, found, locked = 0, requested, sv_fd; char *name, *p, *t, *recp, *pathp; const char *rp; char file[MAXPATHLEN], path[MAXPATHLEN], recpath[MAXPATHLEN]; if (opts_empty(sp, O_RECDIR, 0)) return (1); rp = O_STR(sp, O_RECDIR); if ((dirp = opendir(rp)) == NULL) { msgq_str(sp, M_ERR, rp, "%s"); return (1); } name = frp->name; sv_fd = -1; rec_mtime = 0; recp = pathp = NULL; for (found = requested = 0; (dp = readdir(dirp)) != NULL;) { if (strncmp(dp->d_name, "recover.", 8)) continue; (void)snprintf(recpath, sizeof(recpath), "%s/%s", rp, dp->d_name); /* * If it's readable, it's recoverable. It would be very * nice to use stdio(3), but, we can't because that would * require closing and then reopening the file so that we * could have a lock and still close the FP. Another tip * of the hat to fcntl(2). * * XXX * Should be O_RDONLY, we don't want to write it. However, * if we're using fcntl(2), there's no way to lock a file * descriptor that's not open for writing. */ if ((fd = open(recpath, O_RDWR, 0)) == -1) continue; switch (file_lock(sp, NULL, NULL, fd, 1)) { case LOCK_FAILED: /* * XXX * Assume that a lock can't be acquired, but that we * should permit recovery anyway. If this is wrong, * and someone else is using the file, we're going to * die horribly. */ locked = 0; break; case LOCK_SUCCESS: locked = 1; break; case LOCK_UNAVAIL: /* If it's locked, it's live. */ (void)close(fd); continue; } /* Check the headers. */ if (rcv_gets(file, sizeof(file), fd) == NULL || strncmp(file, VI_FHEADER, sizeof(VI_FHEADER) - 1) || (p = strchr(file, '\n')) == NULL || rcv_gets(path, sizeof(path), fd) == NULL || strncmp(path, VI_PHEADER, sizeof(VI_PHEADER) - 1) || (t = strchr(path, '\n')) == NULL) { msgq_str(sp, M_ERR, recpath, "067|%s: malformed recovery file"); goto next; } *p = *t = '\0'; ++found; /* * If the file doesn't exist, it's an orphaned recovery file, * toss it. * * XXX * This can occur if the backup file was deleted and we crashed * before deleting the email file. */ errno = 0; if (stat(path + sizeof(VI_PHEADER) - 1, &sb) && errno == ENOENT) { (void)unlink(dp->d_name); goto next; } /* Check the file name. */ if (strcmp(file + sizeof(VI_FHEADER) - 1, name)) goto next; ++requested; /* * If we've found more than one, take the most recent. * * XXX * Since we're using st_mtime, for portability reasons, * we only get a single second granularity, instead of * getting it right. */ (void)fstat(fd, &sb); if (recp == NULL || rec_mtime < sb.st_mtime) { p = recp; t = pathp; if ((recp = strdup(recpath)) == NULL) { msgq(sp, M_SYSERR, NULL); recp = p; goto next; } if ((pathp = strdup(path)) == NULL) { msgq(sp, M_SYSERR, NULL); free(recp); recp = p; pathp = t; goto next; } if (p != NULL) { free(p); free(t); } rec_mtime = sb.st_mtime; if (sv_fd != -1) (void)close(sv_fd); sv_fd = fd; } else next: (void)close(fd); } (void)closedir(dirp); if (recp == NULL) { msgq_str(sp, M_INFO, name, "068|No files named %s, readable by you, to recover"); return (1); } if (found) { if (requested > 1) msgq(sp, M_INFO, "069|There are older versions of this file for you to recover"); if (found > requested) msgq(sp, M_INFO, "070|There are other files for you to recover"); } /* * Create the FREF structure, start the btree file. * * XXX * file_init() is going to set ep->rcv_path. */ if (file_init(sp, frp, pathp + sizeof(VI_PHEADER) - 1, 0)) { free(recp); free(pathp); (void)close(sv_fd); return (1); } /* * We keep an open lock on the file so that the recover option can * distinguish between files that are live and those that need to * be recovered. The lock is already acquired, just copy it. */ ep = sp->ep; ep->rcv_mpath = recp; ep->rcv_fd = sv_fd; if (!locked) F_SET(frp, FR_UNLOCKED); /* We believe the file is recoverable. */ F_SET(ep, F_RCV_ON); free(pathp); return (0); }
/* * people making love * never exactly the same * just like a snowflake * * rcv_list -- * List the files that can be recovered by this user. * * PUBLIC: int rcv_list __P((SCR *)); */ int rcv_list(SCR *sp) { struct dirent *dp; struct stat sb; DIR *dirp; FILE *fp; int found; char *p, *t; const char *d; char file[MAXPATHLEN], path[MAXPATHLEN]; /* Open the recovery directory for reading. */ if (opts_empty(sp, O_RECDIR, 0)) return (1); d = O_STR(sp, O_RECDIR); if (chdir(d) || (dirp = opendir(".")) == NULL) { msgq_str(sp, M_SYSERR, d, "recdir: %s"); return (1); } /* Read the directory. */ for (found = 0; (dp = readdir(dirp)) != NULL;) { if (strncmp(dp->d_name, "recover.", 8)) continue; /* * If it's readable, it's recoverable. * * XXX * Should be "r", we don't want to write the file. However, * if we're using fcntl(2), there's no way to lock a file * descriptor that's not open for writing. */ if ((fp = fopen(dp->d_name, "r+")) == NULL) continue; switch (file_lock(sp, NULL, NULL, fileno(fp), 1)) { case LOCK_FAILED: /* * XXX * Assume that a lock can't be acquired, but that we * should permit recovery anyway. If this is wrong, * and someone else is using the file, we're going to * die horribly. */ break; case LOCK_SUCCESS: break; case LOCK_UNAVAIL: /* If it's locked, it's live. */ (void)fclose(fp); continue; } /* Check the headers. */ if (fgets(file, sizeof(file), fp) == NULL || strncmp(file, VI_FHEADER, sizeof(VI_FHEADER) - 1) || (p = strchr(file, '\n')) == NULL || fgets(path, sizeof(path), fp) == NULL || strncmp(path, VI_PHEADER, sizeof(VI_PHEADER) - 1) || (t = strchr(path, '\n')) == NULL) { msgq_str(sp, M_ERR, dp->d_name, "066|%s: malformed recovery file"); goto next; } *p = *t = '\0'; /* * If the file doesn't exist, it's an orphaned recovery file, * toss it. * * XXX * This can occur if the backup file was deleted and we crashed * before deleting the email file. */ errno = 0; if (stat(path + sizeof(VI_PHEADER) - 1, &sb) && errno == ENOENT) { (void)unlink(dp->d_name); goto next; } /* Get the last modification time and display. */ (void)fstat(fileno(fp), &sb); (void)printf("%.24s: %s\n", ctime(&sb.st_mtime), file + sizeof(VI_FHEADER) - 1); found = 1; /* Close, discarding lock. */ next: (void)fclose(fp); } if (found == 0) (void)printf("%s: No files to recover\n", sp->gp->progname); (void)closedir(dirp); return (0); }
/* * rcv_mailfile -- * Build the file to mail to the user. */ static int rcv_mailfile(SCR *sp, int issync, char *cp_path) { EXF *ep; GS *gp; struct passwd *pw; size_t len; time_t now; uid_t uid; int fd; char *p, *t, buf[4096], mpath[MAXPATHLEN]; const char *dp; char *t1, *t2, *t3; /* * XXX * MAXHOSTNAMELEN is in various places on various systems, including * <netdb.h> and <sys/socket.h>. If not found, use a large default. */ #ifndef MAXHOSTNAMELEN #define MAXHOSTNAMELEN 1024 #endif char host[MAXHOSTNAMELEN]; gp = sp->gp; if ((pw = getpwuid(uid = getuid())) == NULL) { msgq(sp, M_ERR, "062|Information on user id %u not found", uid); return (1); } if (opts_empty(sp, O_RECDIR, 0)) return (1); dp = O_STR(sp, O_RECDIR); (void)snprintf(mpath, sizeof(mpath), "%s/recover.XXXXXX", dp); if ((fd = rcv_mktemp(sp, mpath, dp, S_IRUSR | S_IWUSR)) == -1) return (1); /* * XXX * We keep an open lock on the file so that the recover option can * distinguish between files that are live and those that need to * be recovered. There's an obvious window between the mkstemp call * and the lock, but it's pretty small. */ ep = sp->ep; if (file_lock(sp, NULL, NULL, fd, 1) != LOCK_SUCCESS) msgq(sp, M_SYSERR, "063|Unable to lock recovery file"); if (!issync) { /* Save the recover file descriptor, and mail path. */ ep->rcv_fd = fd; if ((ep->rcv_mpath = strdup(mpath)) == NULL) { msgq(sp, M_SYSERR, NULL); goto err; } cp_path = ep->rcv_path; } /* * XXX * We can't use stdio(3) here. The problem is that we may be using * fcntl(2), so if ANY file descriptor into the file is closed, the * lock is lost. So, we could never close the FILE *, even if we * dup'd the fd first. */ t = sp->frp->name; if ((p = strrchr(t, '/')) == NULL) p = t; else ++p; (void)time(&now); (void)gethostname(host, sizeof(host)); len = snprintf(buf, sizeof(buf), "%s%s\n%s%s\n%s\n%s\n%s%s\n%s%s\n%s\n\n", VI_FHEADER, t, /* Non-standard. */ VI_PHEADER, cp_path, /* Non-standard. */ "Reply-To: root", "From: root (Nvi recovery program)", "To: ", pw->pw_name, "Subject: Nvi saved the file ", p, "Precedence: bulk"); /* For vacation(1). */ if (len > sizeof(buf) - 1) goto lerr; if ((size_t)write(fd, buf, len) != len) goto werr; len = snprintf(buf, sizeof(buf), "%s%.24s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n\n", "On ", ctime(&now), ", the user ", pw->pw_name, " was editing a file named ", t, " on the machine ", host, ", when it was saved for recovery. ", "You can recover most, if not all, of the changes ", "to this file using the -r option to ", gp->progname, ":\n\n\t", gp->progname, " -r ", t); if (len > sizeof(buf) - 1) { lerr: msgq(sp, M_ERR, "064|Recovery file buffer overrun"); goto err; } /* * Format the message. (Yes, I know it's silly.) * Requires that the message end in a <newline>. */ #define FMTCOLS 60 for (t1 = buf; len > 0; len -= t2 - t1, t1 = t2) { /* Check for a short length. */ if (len <= FMTCOLS) { t2 = t1 + (len - 1); goto wout; } /* Check for a required <newline>. */ t2 = strchr(t1, '\n'); if (t2 - t1 <= FMTCOLS) goto wout; /* Find the closest space, if any. */ for (t3 = t2; t2 > t1; --t2) if (*t2 == ' ') { if (t2 - t1 <= FMTCOLS) goto wout; t3 = t2; } t2 = t3; /* t2 points to the last character to display. */ wout: *t2++ = '\n'; /* t2 points one after the last character to display. */ if (write(fd, t1, t2 - t1) != t2 - t1) goto werr; } if (issync) { rcv_email(sp, mpath); if (close(fd)) { werr: msgq(sp, M_SYSERR, "065|Recovery file"); goto err; } } return (0); err: if (!issync) ep->rcv_fd = -1; if (fd != -1) (void)close(fd); return (1); }
/* * rcv_sync -- * Sync the file, optionally: * flagging the backup file to be preserved * snapshotting the backup file and send email to the user * sending email to the user if the file was modified * ending the file session * * PUBLIC: int rcv_sync __P((SCR *, u_int)); */ int rcv_sync(SCR *sp, u_int flags) { EXF *ep; int fd, rval; char buf[1024]; const char *dp; /* Make sure that there's something to recover/sync. */ ep = sp->ep; if (ep == NULL || !F_ISSET(ep, F_RCV_ON)) return (0); /* Sync the file if it's been modified. */ if (F_ISSET(ep, F_MODIFIED)) { /* * If we are using a db1 version of the database, * we want to sync the underlying btree not the * recno tree which is transient anyway. */ #ifndef R_RECNOSYNC #define R_RECNOSYNC 0 #endif if (ep->db->sync(ep->db, R_RECNOSYNC)) { F_CLR(ep, F_RCV_ON | F_RCV_NORM); msgq_str(sp, M_SYSERR, ep->rcv_path, "060|File backup failed: %s"); return (1); } /* REQUEST: don't remove backing file on exit. */ if (LF_ISSET(RCV_PRESERVE)) F_SET(ep, F_RCV_NORM); /* REQUEST: send email. */ if (LF_ISSET(RCV_EMAIL)) rcv_email(sp, ep->rcv_mpath); } /* * !!! * Each time the user exec's :preserve, we have to snapshot all of * the recovery information, i.e. it's like the user re-edited the * file. We copy the DB(3) backing file, and then create a new mail * recovery file, it's simpler than exiting and reopening all of the * underlying files. * * REQUEST: snapshot the file. */ rval = 0; if (LF_ISSET(RCV_SNAPSHOT)) { if (opts_empty(sp, O_RECDIR, 0)) goto err; dp = O_STR(sp, O_RECDIR); (void)snprintf(buf, sizeof(buf), "%s/vi.XXXXXX", dp); if ((fd = rcv_mktemp(sp, buf, dp, S_IRUSR | S_IWUSR)) == -1) goto err; sp->gp->scr_busy(sp, "061|Copying file for recovery...", BUSY_ON); if (rcv_copy(sp, fd, ep->rcv_path) || close(fd) || rcv_mailfile(sp, 1, buf)) { (void)unlink(buf); (void)close(fd); rval = 1; } sp->gp->scr_busy(sp, NULL, BUSY_OFF); } if (0) { err: rval = 1; } /* REQUEST: end the file session. */ if (LF_ISSET(RCV_ENDSESSION) && file_end(sp, NULL, 1)) rval = 1; return (rval); }
/* * ex_filter -- * Run a range of lines through a filter utility and optionally * replace the original text with the stdout/stderr output of * the utility. * * PUBLIC: int ex_filter __P((SCR *, * PUBLIC: EXCMD *, MARK *, MARK *, MARK *, CHAR_T *, enum filtertype)); */ int ex_filter(SCR *sp, EXCMD *cmdp, MARK *fm, MARK *tm, MARK *rp, CHAR_T *cmd, enum filtertype ftype) { FILE *ifp, *ofp; pid_t parent_writer_pid, utility_pid; db_recno_t nread; int input[2], output[2], rval; char *name; char *np; size_t nlen; rval = 0; /* Set return cursor position, which is never less than line 1. */ *rp = *fm; if (rp->lno == 0) rp->lno = 1; /* We're going to need a shell. */ if (opts_empty(sp, O_SHELL, 0)) return (1); /* * There are three different processes running through this code. * They are the utility, the parent-writer and the parent-reader. * The parent-writer is the process that writes from the file to * the utility, the parent reader is the process that reads from * the utility. * * Input and output are named from the utility's point of view. * The utility reads from input[0] and the parent(s) write to * input[1]. The parent(s) read from output[0] and the utility * writes to output[1]. * * !!! * Historically, in the FILTER_READ case, the utility reads from * the terminal (e.g. :r! cat works). Otherwise open up utility * input pipe. */ ofp = NULL; input[0] = input[1] = output[0] = output[1] = -1; if (ftype != FILTER_READ && pipe(input) < 0) { msgq(sp, M_SYSERR, "pipe"); goto err; } /* Open up utility output pipe. */ if (pipe(output) < 0) { msgq(sp, M_SYSERR, "pipe"); goto err; } if ((ofp = fdopen(output[0], "r")) == NULL) { msgq(sp, M_SYSERR, "fdopen"); goto err; } /* Fork off the utility process. */ switch (utility_pid = vfork()) { case -1: /* Error. */ msgq(sp, M_SYSERR, "vfork"); err: if (input[0] != -1) (void)close(input[0]); if (input[1] != -1) (void)close(input[1]); if (ofp != NULL) (void)fclose(ofp); else if (output[0] != -1) (void)close(output[0]); if (output[1] != -1) (void)close(output[1]); return (1); case 0: /* Utility. */ /* * Redirect stdin from the read end of the input pipe, and * redirect stdout/stderr to the write end of the output pipe. * * !!! * Historically, ex only directed stdout into the input pipe, * letting stderr come out on the terminal as usual. Vi did * not, directing both stdout and stderr into the input pipe. * We match that practice in both ex and vi for consistency. */ if (input[0] != -1) (void)dup2(input[0], STDIN_FILENO); (void)dup2(output[1], STDOUT_FILENO); (void)dup2(output[1], STDERR_FILENO); /* Close the utility's file descriptors. */ if (input[0] != -1) (void)close(input[0]); if (input[1] != -1) (void)close(input[1]); (void)close(output[0]); (void)close(output[1]); if ((name = strrchr(O_STR(sp, O_SHELL), '/')) == NULL) name = O_STR(sp, O_SHELL); else ++name; INT2SYS(sp, cmd, STRLEN(cmd)+1, np, nlen); execl(O_STR(sp, O_SHELL), name, "-c", np, (char *)NULL); msgq_str(sp, M_SYSERR, O_STR(sp, O_SHELL), "execl: %s"); _exit (127); /* NOTREACHED */ default: /* Parent-reader, parent-writer. */ /* Close the pipe ends neither parent will use. */ if (input[0] != -1) (void)close(input[0]); (void)close(output[1]); break; } /* * FILTER_RBANG, FILTER_READ: * * Reading is the simple case -- we don't need a parent writer, * so the parent reads the output from the read end of the output * pipe until it finishes, then waits for the child. Ex_readfp * appends to the MARK, and closes ofp. * * For FILTER_RBANG, there is nothing to write to the utility. * Make sure it doesn't wait forever by closing its standard * input. * * !!! * Set the return cursor to the last line read in for FILTER_READ. * Historically, this behaves differently from ":r file" command, * which leaves the cursor at the first line read in. Check to * make sure that it's not past EOF because we were reading into an * empty file. */ if (ftype == FILTER_RBANG || ftype == FILTER_READ) { if (ftype == FILTER_RBANG) (void)close(input[1]); if (ex_readfp(sp, "filter", ofp, fm, &nread, 1)) rval = 1; sp->rptlines[L_ADDED] += nread; if (ftype == FILTER_READ) if (fm->lno == 0) rp->lno = nread; else rp->lno += nread; goto uwait; } /* * FILTER_BANG, FILTER_WRITE * * Here we need both a reader and a writer. Temporary files are * expensive and we'd like to avoid disk I/O. Using pipes has the * obvious starvation conditions. It's done as follows: * * fork * child * write lines out * exit * parent * FILTER_BANG: * read lines into the file * delete old lines * FILTER_WRITE * read and display lines * wait for child * * XXX * We get away without locking the underlying database because we know * that none of the records that we're reading will be modified until * after we've read them. This depends on the fact that the current * B+tree implementation doesn't balance pages or similar things when * it inserts new records. When the DB code has locking, we should * treat vi as if it were multiple applications sharing a database, and * do the required locking. If necessary a work-around would be to do * explicit locking in the line.c:db_get() code, based on the flag set * here. */ F_SET(sp->ep, F_MULTILOCK); switch (parent_writer_pid = fork()) { case -1: /* Error. */ msgq(sp, M_SYSERR, "fork"); (void)close(input[1]); (void)close(output[0]); rval = 1; break; case 0: /* Parent-writer. */ /* * Write the selected lines to the write end of the input * pipe. This instance of ifp is closed by ex_writefp. */ (void)close(output[0]); if ((ifp = fdopen(input[1], "w")) == NULL) _exit (1); _exit(ex_writefp(sp, "filter", ifp, fm, tm, NULL, NULL, 1)); /* NOTREACHED */ default: /* Parent-reader. */ (void)close(input[1]); if (ftype == FILTER_WRITE) { /* * Read the output from the read end of the output * pipe and display it. Filter_ldisplay closes ofp. */ if (filter_ldisplay(sp, ofp)) rval = 1; } else { /* * Read the output from the read end of the output * pipe. Ex_readfp appends to the MARK and closes * ofp. */ if (ex_readfp(sp, "filter", ofp, tm, &nread, 1)) rval = 1; sp->rptlines[L_ADDED] += nread; } /* Wait for the parent-writer. */ if (proc_wait(sp, (long)parent_writer_pid, "parent-writer", 0, 1)) rval = 1; /* Delete any lines written to the utility. */ if (rval == 0 && ftype == FILTER_BANG && (cut(sp, NULL, fm, tm, CUT_LINEMODE) || del(sp, fm, tm, 1))) { rval = 1; break; } /* * If the filter had no output, we may have just deleted * the cursor. Don't do any real error correction, we'll * try and recover later. */ if (rp->lno > 1 && !db_exist(sp, rp->lno)) --rp->lno; break; } F_CLR(sp->ep, F_MULTILOCK); /* * !!! * Ignore errors on vi file reads, to make reads prettier. It's * completely inconsistent, and historic practice. */ uwait: INT2CHAR(sp, cmd, STRLEN(cmd) + 1, np, nlen); return (proc_wait(sp, (long)utility_pid, np, ftype == FILTER_READ && F_ISSET(sp, SC_VI) ? 1 : 0, 0) || rval); }