/*- * Copyright (c) 1993, 1994 * The Regents of the University of California. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * This product includes software developed by the University of * California, Berkeley and its contributors. * 4. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #ifndef lint static char sccsid[] = "@(#)recover.c 8.50 (Berkeley) 3/23/94"; #endif /* not lint */ #include #include #include #include /* * We include , because the flock(2) #defines were * found there on historical systems. We also include * because the open(2) #defines are found there on newer systems. */ #include #include /* MAXHOSTNAMELEN on some systems. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "compat.h" #include #include #include #include "vi.h" /* * Recovery code. * * The basic scheme is there's a btree file, whose name we specify. The first * time a file is modified, and then at RCV_PERIOD intervals after that, the * btree file is synced to disk. Each time a keystroke is requested for a file * the terminal routines check to see if the file needs to be synced. This, of * course means that the data structures had better be consistent each time the * key routines are called. * * We don't use timers other than to flag that the file should be synced. This * would require that the SCR and EXF data structures be locked, the dbopen(3) * routines lock out the timers for each update, etc. It's just not worth it. * The only way we can lose in the current scheme is if the file is saved, then * the user types furiously for RCV_PERIOD - 1 seconds, and types nothing more. * Not likely. * * When a file is first modified, a file which can be handed off to the mailer * is created. The file contains normal headers, with two additions, which * occur in THIS order, as the FIRST TWO headers: * * Vi-recover-file: file_name * Vi-recover-path: recover_path * * Since newlines delimit the headers, this means that file names cannot * have newlines in them, but that's probably okay. * * Btree files are named "vi.XXXX" and recovery files are named "recover.XXXX". */ #define VI_FHEADER "Vi-recover-file: " #define VI_PHEADER "Vi-recover-path: " static int rcv_mailfile __P((SCR *, EXF *)); static void rcv_syncit __P((SCR *, int)); /* * rcv_tmp -- * Build a file name that will be used as the recovery file. */ int rcv_tmp(sp, ep, name) SCR *sp; EXF *ep; char *name; { struct stat sb; int fd; char *dp, *p, path[MAXPATHLEN]; /* * 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 users recovery files. If * the sticky bit has the BSD semantics, that too will be impossible. */ dp = O_STR(sp, O_RECDIR); if (stat(dp, &sb)) { if (errno != ENOENT || mkdir(dp, 0)) { msgq(sp, M_ERR, "Error: %s: %s", dp, strerror(errno)); return (1); } (void)chmod(dp, S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX); } /* Newlines delimit the mail messages. */ for (p = name; *p; ++p) if (*p == '\n') { msgq(sp, M_ERR, "Files with newlines in the name are unrecoverable."); return (1); } (void)snprintf(path, sizeof(path), "%s/vi.XXXXXX", dp); /* * !!! * We depend on mkstemp(3) setting the permissions correctly. * GP's, we do it ourselves, to keep the window as small as * possible. */ if ((fd = mkstemp(path)) == -1) { msgq(sp, M_ERR, "Error: %s: %s", dp, strerror(errno)); return (1); } (void)chmod(path, S_IRUSR | S_IWUSR); (void)close(fd); if ((ep->rcv_path = strdup(path)) == NULL) { msgq(sp, M_SYSERR, NULL); (void)unlink(path); return (1); } /* We believe the file is recoverable. */ F_SET(ep, F_RCV_ON); return (0); } /* * rcv_init -- * Force the file to be snapshotted for recovery. */ int rcv_init(sp, ep) SCR *sp; EXF *ep; { recno_t lno; int btear; F_CLR(ep, F_FIRSTMODIFY | F_RCV_ON); /* * If not already recoverying a file, build a file to mail * to the user. */ if (ep->rcv_mpath == NULL && rcv_mailfile(sp, ep)) goto err; /* Force read of entire file. */ if (file_lline(sp, ep, &lno)) goto err; /* Turn on a busy message, and sync it to backing store. */ btear = F_ISSET(sp, S_EXSILENT) ? 0 : !busy_on(sp, "Copying file for recovery..."); if (ep->db->sync(ep->db, R_RECNOSYNC)) { msgq(sp, M_ERR, "Preservation failed: %s: %s", ep->rcv_path, strerror(errno)); if (btear) busy_off(sp); goto err; } if (btear) busy_off(sp); if (!F_ISSET(sp->gp, G_RECOVER_SET) && rcv_on(sp, ep)) { err: msgq(sp, M_ERR, "Recovery after system crash not possible."); return (1); } /* We believe the file is recoverable. */ F_SET(ep, F_RCV_ON); return (0); } /* * rcv_mailfile -- * Build the file to mail to the user. */ static int rcv_mailfile(sp, ep) SCR *sp; EXF *ep; { struct passwd *pw; uid_t uid; FILE *fp; time_t now; int fd; char *p, *t, host[MAXHOSTNAMELEN], path[MAXPATHLEN]; if ((pw = getpwuid(uid = getuid())) == NULL) { msgq(sp, M_ERR, "Information on user id %u not found.", uid); return (1); } (void)snprintf(path, sizeof(path), "%s/recover.XXXXXX", O_STR(sp, O_RECDIR)); if ((fd = mkstemp(path)) == -1 || (fp = fdopen(fd, "w")) == NULL) { msgq(sp, M_ERR, "Error: %s: %s", O_STR(sp, O_RECDIR), strerror(errno)); if (fd != -1) (void)close(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. There's an obvious window between the mkstemp call * and the lock, but it's pretty small. */ if ((ep->rcv_fd = dup(fd)) == -1 || flock(ep->rcv_fd, LOCK_EX | LOCK_NB)) msgq(sp, M_SYSERR, "Unable to lock recovery file"); if ((ep->rcv_mpath = strdup(path)) == NULL) { msgq(sp, M_SYSERR, NULL); (void)fclose(fp); return (1); } t = FILENAME(sp->frp); if ((p = strrchr(t, '/')) == NULL) p = t; else ++p; (void)time(&now); (void)gethostname(host, sizeof(host)); (void)fprintf(fp, "%s%s\n%s%s\n%s\n%s\n%s%s\n%s%s\n%s\n\n", VI_FHEADER, p, /* Non-standard. */ VI_PHEADER, ep->rcv_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). */ (void)fprintf(fp, "%s%.24s%s%s\n%s%s", "On ", ctime(&now), ", the user ", pw->pw_name, "was editing a file named ", p); if (p != t) (void)fprintf(fp, " (%s)", t); (void)fprintf(fp, "\n%s%s%s\n", "on the machine ", host, ", when it was saved for\nrecovery."); (void)fprintf(fp, "\n%s\n%s\n%s\n\n", "You can recover most, if not all, of the changes", "to this file using the -l and -r options to nvi(1)", "or nex(1)."); if (fflush(fp) || ferror(fp)) { msgq(sp, M_SYSERR, NULL); (void)fclose(fp); return (1); } (void)fclose(fp); return (0); } /* * rcv_sync -- * Sync the backing file. */ int rcv_sync(sp, ep) SCR *sp; EXF *ep; { if (ep->db->sync(ep->db, R_RECNOSYNC)) { F_CLR(ep, F_RCV_ON); msgq(sp, M_ERR, "Automatic file backup failed: %s: %s", ep->rcv_path, strerror(errno)); return (1); } return (0); } /* * rcv_hup -- * Recovery SIGHUP interrupt handler. (Modem line dropped, or * xterm window closed.) */ void rcv_hup() { SCR *sp; /* * Walk the lists of screens, sync'ing the files; only sync * each file once. Send email to the user for each file saved. */ for (sp = __global_list->dq.cqh_first; sp != (void *)&__global_list->dq; sp = sp->q.cqe_next) rcv_syncit(sp, 1); for (sp = __global_list->hq.cqh_first; sp != (void *)&__global_list->hq; sp = sp->q.cqe_next) rcv_syncit(sp, 1); /* * Die with the proper exit status. Don't bother using * sigaction(2) 'cause we want the default behavior. */ (void)signal(SIGHUP, SIG_DFL); (void)kill(0, SIGHUP); /* NOTREACHED */ exit (1); } /* * rcv_term -- * Recovery SIGTERM interrupt handler. (Reboot or halt is running.) */ void rcv_term() { SCR *sp; /* * Walk the lists of screens, sync'ing the files; only sync * each file once. */ for (sp = __global_list->dq.cqh_first; sp != (void *)&__global_list->dq; sp = sp->q.cqe_next) rcv_syncit(sp, 0); for (sp = __global_list->hq.cqh_first; sp != (void *)&__global_list->hq; sp = sp->q.cqe_next) rcv_syncit(sp, 0); /* * Die with the proper exit status. Don't bother using * sigaction(2) 'cause we want the default behavior. */ (void)signal(SIGTERM, SIG_DFL); (void)kill(0, SIGTERM); /* NOTREACHED */ exit (1); } /* * rcv_syncit -- * Sync the file, optionally send mail. */ static void rcv_syncit(sp, email) SCR *sp; int email; { EXF *ep; char comm[1024]; if ((ep = sp->ep) == NULL || !F_ISSET(ep, F_MODIFIED) || !F_ISSET(ep, F_RCV_ON)) return; (void)ep->db->sync(ep->db, R_RECNOSYNC); F_SET(ep, F_RCV_NORM); /* * !!! * If you need to port this to a system that doesn't have sendmail, * the -t flag being used causes sendmail to read the message for * the recipients instead of us specifying them some other way. */ if (email) { (void)snprintf(comm, sizeof(comm), "%s -t < %s", _PATH_SENDMAIL, ep->rcv_mpath); (void)system(comm); } (void)file_end(sp, ep, 1); } /* * people making love * never exactly the same * just like a snowflake * * rcv_list -- * List the files that can be recovered by this user. */ int rcv_list(sp) SCR *sp; { struct dirent *dp; struct stat sb; DIR *dirp; FILE *fp; int found; char *p, file[1024]; if (chdir(O_STR(sp, O_RECDIR)) || (dirp = opendir(".")) == NULL) { (void)fprintf(stderr, "vi: %s: %s\n", O_STR(sp, O_RECDIR), strerror(errno)); return (1); } for (found = 0; (dp = readdir(dirp)) != NULL;) { if (strncmp(dp->d_name, "recover.", 8)) continue; /* If it's readable, it's recoverable. */ if ((fp = fopen(dp->d_name, "r")) == NULL) continue; /* If it's locked, it's live. */ if (flock(fileno(fp), LOCK_EX | LOCK_NB)) { (void)fclose(fp); continue; } /* Check the header, get the file name. */ if (fgets(file, sizeof(file), fp) == NULL || strncmp(file, VI_FHEADER, sizeof(VI_FHEADER) - 1) || (p = strchr(file, '\n')) == NULL) { (void)fprintf(stderr, "vi: %s: malformed recovery file.\n", dp->d_name); goto next; } *p = '\0'; /* Get the last modification time. */ if (fstat(fileno(fp), &sb)) { (void)fprintf(stderr, "vi: %s: %s\n", dp->d_name, strerror(errno)); goto next; } /* Display. */ (void)printf("%s: %s", file + sizeof(VI_FHEADER) - 1, ctime(&sb.st_mtime)); found = 1; next: (void)fclose(fp); } if (found == 0) (void)printf("vi: no files to recover.\n"); (void)closedir(dirp); return (0); } /* * rcv_read -- * Start a recovered file as the file to edit. */ int rcv_read(sp, name) SCR *sp; char *name; { struct dirent *dp; struct stat sb; DIR *dirp; FREF *frp; FILE *fp, *sv_fp; time_t rec_mtime; int found, requested; char *p, *t, *recp, *pathp; char recpath[MAXPATHLEN], file[MAXPATHLEN], path[MAXPATHLEN]; if ((dirp = opendir(O_STR(sp, O_RECDIR))) == NULL) { msgq(sp, M_ERR, "%s: %s", O_STR(sp, O_RECDIR), strerror(errno)); return (1); } sv_fp = NULL; rec_mtime = 0; recp = pathp = NULL; for (found = requested = 0; (dp = readdir(dirp)) != NULL;) { if (strncmp(dp->d_name, "recover.", 8)) continue; /* If it's readable, it's recoverable. */ (void)snprintf(recpath, sizeof(recpath), "%s/%s", O_STR(sp, O_RECDIR), dp->d_name); if ((fp = fopen(recpath, "r")) == NULL) continue; /* If it's locked, it's live. */ if (flock(fileno(fp), LOCK_EX | LOCK_NB)) { (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(sp, M_ERR, "%s: malformed recovery file.", recpath); goto next; } ++found; *t = *p = '\0'; /* Get the last modification time. */ if (fstat(fileno(fp), &sb)) { msgq(sp, M_ERR, "vi: %s: %s", dp->d_name, strerror(errno)); 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. */ if (recp == NULL || rec_mtime < sb.st_mtime) { p = recp; t = pathp; if ((recp = strdup(recpath)) == NULL) { msgq(sp, M_ERR, "vi: Error: %s.\n", strerror(errno)); recp = p; goto next; } if ((pathp = strdup(path)) == NULL) { msgq(sp, M_ERR, "vi: Error: %s.\n", strerror(errno)); FREE(recp, strlen(recp) + 1); recp = p; pathp = t; goto next; } if (p != NULL) { FREE(p, strlen(p) + 1); FREE(t, strlen(t) + 1); } rec_mtime = sb.st_mtime; if (sv_fp != NULL) (void)fclose(sv_fp); sv_fp = fp; } else next: (void)fclose(fp); } (void)closedir(dirp); if (recp == NULL) { msgq(sp, M_INFO, "No files named %s, owned by you, to edit.", name); return (1); } if (found) { if (requested > 1) msgq(sp, M_INFO, "There are older versions of this file for you to recover."); if (found > requested) msgq(sp, M_INFO, "There are other files for you to recover."); } /* Create the FREF structure, start the btree file. */ if ((frp = file_add(sp, NULL, name, 0)) == NULL || file_init(sp, frp, pathp + sizeof(VI_PHEADER) - 1, 0)) { FREE(recp, strlen(recp) + 1); FREE(pathp, strlen(pathp) + 1); (void)fclose(sv_fp); 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, so just get a copy. */ if ((sp->ep->rcv_fd = dup(fileno(sv_fp))) != -1) (void)fclose(sv_fp); sp->ep->rcv_mpath = recp; return (0); }