2 * Copyright (c) 1993, 1994
3 * The Regents of the University of California. All rights reserved.
4 * Copyright (c) 1993, 1994, 1995, 1996
5 * Keith Bostic. All rights reserved.
7 * See the LICENSE file for redistribution information.
13 static const char sccsid[] = "$Id: recover.c,v 11.3 2015/04/04 03:50:42 zy Exp $";
16 #include <sys/types.h>
17 #include <sys/queue.h>
21 * We include <sys/file.h>, because the open #defines were found there
22 * on historical systems. We also include <fcntl.h> because the open(2)
23 * #defines are found there on newer systems.
27 #include <bitstring.h>
33 #include <netinet/in.h> /* Required by resolv.h. */
41 #include "../ex/version.h"
43 #include "pathnames.h"
48 * The basic scheme is as follows. In the EXF structure, we maintain full
49 * paths of a b+tree file and a mail recovery file. The former is the file
50 * used as backing store by the DB package. The latter is the file that
51 * contains an email message to be sent to the user if we crash. The two
52 * simple states of recovery are:
54 * + first starting the edit session:
55 * the b+tree file exists and is mode 700, the mail recovery
57 * + after the file has been modified:
58 * the b+tree file exists and is mode 600, the mail recovery
59 * file exists, and is exclusively locked.
61 * In the EXF structure we maintain a file descriptor that is the locked
62 * file descriptor for the mail recovery file.
64 * To find out if a recovery file/backing file pair are in use, try to get
65 * a lock on the recovery file.
67 * To find out if a backing file can be deleted at boot time, check for an
68 * owner execute bit. (Yes, I know it's ugly, but it's either that or put
69 * special stuff into the backing file itself, or correlate the files at
70 * boot time, neither of which looks like fun.) Note also that there's a
71 * window between when the file is created and the X bit is set. It's small,
72 * but it's there. To fix the window, check for 0 length files as well.
74 * To find out if a file can be recovered, check the F_RCV_ON bit. Note,
75 * this DOES NOT mean that any initialization has been done, only that we
76 * haven't yet failed at setting up or doing recovery.
78 * To preserve a recovery file/backing file pair, set the F_RCV_NORM bit.
79 * If that bit is not set when ending a file session:
80 * If the EXF structure paths (rcv_path and rcv_mpath) are not NULL,
81 * they are unlink(2)'d, and free(3)'d.
82 * If the EXF file descriptor (rcv_fd) is not -1, it is closed.
84 * The backing b+tree file is set up when a file is first edited, so that
85 * the DB package can use it for on-disk caching and/or to snapshot the
86 * file. When the file is first modified, the mail recovery file is created,
87 * the backing file permissions are updated, the file is sync(2)'d to disk,
88 * and the timer is started. Then, at RCV_PERIOD second intervals, the
89 * b+tree file is synced to disk. RCV_PERIOD is measured using SIGALRM, which
90 * means that the data structures (SCR, EXF, the underlying tree structures)
91 * must be consistent when the signal arrives.
93 * The recovery mail file contains normal mail headers, with two additional
95 * X-vi-data: <file|path>;<base64 encoded path>
97 * MIME headers; the folding character is limited to ' '.
99 * Btree files are named "vi.XXXXXX" and recovery files are named
103 #define VI_DHEADER "X-vi-data:"
105 static int rcv_copy(SCR *, int, char *);
106 static void rcv_email(SCR *, char *);
107 static int rcv_mailfile(SCR *, int, char *);
108 static int rcv_mktemp(SCR *, char *, char *);
109 static int rcv_dlnwrite(SCR *, const char *, const char *, FILE *);
110 static int rcv_dlnread(SCR *, char **, char **, FILE *);
114 * Build a file name that will be used as the recovery file.
116 * PUBLIC: int rcv_tmp(SCR *, EXF *, char *);
130 * ep MAY NOT BE THE SAME AS sp->ep, DON'T USE THE LATTER.
133 * If the recovery directory doesn't exist, try and create it. As
134 * the recovery files are themselves protected from reading/writing
135 * by other than the owner, the worst that can happen is that a user
136 * would have permission to remove other user's recovery files. If
137 * the sticky bit has the BSD semantics, that too will be impossible.
139 if (opts_empty(sp, O_RECDIR, 0))
141 dp = O_STR(sp, O_RECDIR);
143 if (errno != ENOENT || mkdir(dp, 0)) {
144 msgq(sp, M_SYSERR, "%s", dp);
147 (void)chmod(dp, S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX);
150 if ((path = join(dp, "vi.XXXXXX")) == NULL)
152 if ((fd = rcv_mktemp(sp, path, dp)) == -1) {
156 (void)fchmod(fd, S_IRWXU);
162 "056|Modifications not recoverable if the session fails");
166 /* We believe the file is recoverable. */
173 * Force the file to be snapshotted for recovery.
175 * PUBLIC: int rcv_init(SCR *);
185 /* Only do this once. */
186 F_CLR(ep, F_FIRSTMODIFY);
188 /* If we already know the file isn't recoverable, we're done. */
189 if (!F_ISSET(ep, F_RCV_ON))
192 /* Turn off recoverability until we figure out if this will work. */
195 /* Test if we're recovering a file, not editing one. */
196 if (ep->rcv_mpath == NULL) {
197 /* Build a file to mail to the user. */
198 if (rcv_mailfile(sp, 0, NULL))
201 /* Force a read of the entire file. */
202 if (db_last(sp, &lno))
205 /* Turn on a busy message, and sync it to backing store. */
207 "057|Copying file for recovery...", BUSY_ON);
208 if (ep->db->sync(ep->db, R_RECNOSYNC)) {
209 msgq_str(sp, M_SYSERR, ep->rcv_path,
210 "058|Preservation failed: %s");
211 sp->gp->scr_busy(sp, NULL, BUSY_OFF);
214 sp->gp->scr_busy(sp, NULL, BUSY_OFF);
217 /* Turn off the owner execute bit. */
218 (void)chmod(ep->rcv_path, S_IRUSR | S_IWUSR);
220 /* We believe the file is recoverable. */
225 "059|Modifications not recoverable if the session fails");
231 * Sync the file, optionally:
232 * flagging the backup file to be preserved
233 * snapshotting the backup file and send email to the user
234 * sending email to the user if the file was modified
235 * ending the file session
237 * PUBLIC: int rcv_sync(SCR *, u_int);
248 /* Make sure that there's something to recover/sync. */
250 if (ep == NULL || !F_ISSET(ep, F_RCV_ON))
253 /* Sync the file if it's been modified. */
254 if (F_ISSET(ep, F_MODIFIED)) {
255 if (ep->db->sync(ep->db, R_RECNOSYNC)) {
256 F_CLR(ep, F_RCV_ON | F_RCV_NORM);
257 msgq_str(sp, M_SYSERR,
258 ep->rcv_path, "060|File backup failed: %s");
262 /* REQUEST: don't remove backing file on exit. */
263 if (LF_ISSET(RCV_PRESERVE))
264 F_SET(ep, F_RCV_NORM);
266 /* REQUEST: send email. */
267 if (LF_ISSET(RCV_EMAIL))
268 rcv_email(sp, ep->rcv_mpath);
273 * Each time the user exec's :preserve, we have to snapshot all of
274 * the recovery information, i.e. it's like the user re-edited the
275 * file. We copy the DB(3) backing file, and then create a new mail
276 * recovery file, it's simpler than exiting and reopening all of the
279 * REQUEST: snapshot the file.
282 if (LF_ISSET(RCV_SNAPSHOT)) {
283 if (opts_empty(sp, O_RECDIR, 0))
285 dp = O_STR(sp, O_RECDIR);
286 if ((buf = join(dp, "vi.XXXXXX")) == NULL) {
287 msgq(sp, M_SYSERR, NULL);
290 if ((fd = rcv_mktemp(sp, buf, dp)) == -1) {
295 "061|Copying file for recovery...", BUSY_ON);
296 if (rcv_copy(sp, fd, ep->rcv_path) ||
297 close(fd) || rcv_mailfile(sp, 1, buf)) {
303 sp->gp->scr_busy(sp, NULL, BUSY_OFF);
309 /* REQUEST: end the file session. */
310 if (LF_ISSET(RCV_ENDSESSION) && file_end(sp, NULL, 1))
318 * Build the file to mail to the user.
334 char *dp, *p, *t, *qt, *buf, *mpath;
340 * MAXHOSTNAMELEN/HOST_NAME_MAX are deprecated. We try sysconf(3)
341 * first, then fallback to _POSIX_HOST_NAME_MAX.
344 long hostmax = sysconf(_SC_HOST_NAME_MAX);
346 hostmax = _POSIX_HOST_NAME_MAX;
349 if ((pw = getpwuid(uid = getuid())) == NULL) {
351 "062|Information on user id %u not found", uid);
355 if (opts_empty(sp, O_RECDIR, 0))
357 dp = O_STR(sp, O_RECDIR);
358 if ((mpath = join(dp, "recover.XXXXXX")) == NULL) {
359 msgq(sp, M_SYSERR, NULL);
362 if ((fd = rcv_mktemp(sp, mpath, dp)) == -1) {
366 if ((fp = fdopen(fd, "w")) == NULL) {
374 * We keep an open lock on the file so that the recover option can
375 * distinguish between files that are live and those that need to
376 * be recovered. There's an obvious window between the mkstemp call
377 * and the lock, but it's pretty small.
380 if (file_lock(sp, NULL, fd, 1) != LOCK_SUCCESS)
381 msgq(sp, M_SYSERR, "063|Unable to lock recovery file");
383 /* Save the recover file descriptor, and mail path. */
384 ep->rcv_fd = dup(fd);
385 ep->rcv_mpath = mpath;
386 cp_path = ep->rcv_path;
390 if ((p = strrchr(t, '/')) == NULL)
396 if ((st = rcv_dlnwrite(sp, "file", t, fp))) {
401 if ((st = rcv_dlnwrite(sp, "path", cp_path, fp))) {
407 MALLOC(sp, host, char *, hostmax + 1);
410 (void)gethostname(host, hostmax + 1);
412 len = fprintf(fp, "%s%s%s\n%s%s%s%s\n%s%.40s\n%s\n\n",
413 "From: root@", host, " (Nvi recovery program)",
414 "To: ", pw->pw_name, "@", host,
415 "Subject: Nvi saved the file ", p,
416 "Precedence: bulk"); /* For vacation(1). */
422 if ((qt = quote(t)) == NULL) {
424 msgq(sp, M_SYSERR, NULL);
427 len = asprintf(&buf, "%s%.24s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n\n",
428 "On ", ctime(&now), ", the user ", pw->pw_name,
429 " was editing a file named ", t, " on the machine ",
430 host, ", when it was saved for recovery. ",
431 "You can recover most, if not all, of the changes ",
432 "to this file using the -r option to ", gp->progname, ":\n\n\t",
433 gp->progname, " -r ", qt);
437 msgq(sp, M_SYSERR, NULL);
442 * Format the message. (Yes, I know it's silly.)
443 * Requires that the message end in a <newline>.
446 for (t1 = buf; len > 0; len -= t2 - t1, t1 = t2) {
447 /* Check for a short length. */
448 if (len <= FMTCOLS) {
453 /* Check for a required <newline>. */
454 t2 = strchr(t1, '\n');
455 if (t2 - t1 <= FMTCOLS)
458 /* Find the closest space, if any. */
459 for (t3 = t2; t2 > t1; --t2)
461 if (t2 - t1 <= FMTCOLS)
467 /* t2 points to the last character to display. */
470 /* t2 points one after the last character to display. */
471 if (fwrite(t1, 1, t2 - t1, fp) != t2 - t1) {
479 rcv_email(sp, mpath);
484 werr: msgq(sp, M_SYSERR, "065|Recovery file");
499 * never exactly the same
500 * just like a snowflake
503 * List the files that can be recovered by this user.
505 * PUBLIC: int rcv_list(SCR *);
515 char *p, *file, *path;
519 /* Open the recovery directory for reading. */
520 if (opts_empty(sp, O_RECDIR, 0))
522 p = O_STR(sp, O_RECDIR);
523 if (chdir(p) || (dirp = opendir(".")) == NULL) {
524 msgq_str(sp, M_SYSERR, p, "recdir: %s");
528 /* Read the directory. */
529 for (found = 0; (dp = readdir(dirp)) != NULL;) {
530 if (strncmp(dp->d_name, "recover.", 8))
533 /* If it's readable, it's recoverable. */
534 if ((fp = fopen(dp->d_name, "r")) == NULL)
537 switch (file_lock(sp, NULL, fileno(fp), 1)) {
541 * Assume that a lock can't be acquired, but that we
542 * should permit recovery anyway. If this is wrong,
543 * and someone else is using the file, we're going to
550 /* If it's locked, it's live. */
555 /* Check the headers. */
556 for (file = NULL, path = NULL;
557 file == NULL || path == NULL;) {
558 if ((st = rcv_dlnread(sp, &dtype, &data, fp))) {
560 msgq_str(sp, M_ERR, dp->d_name,
561 "066|%s: malformed recovery file");
566 if (!strcmp(dtype, "file"))
568 else if (!strcmp(dtype, "path"))
575 * If the file doesn't exist, it's an orphaned recovery file,
579 * This can occur if the backup file was deleted and we crashed
580 * before deleting the email file.
583 if (stat(path, &sb) &&
585 (void)unlink(dp->d_name);
589 /* Get the last modification time and display. */
590 (void)fstat(fileno(fp), &sb);
591 (void)printf("%.24s: %s\n",
592 ctime(&sb.st_mtime), file);
595 /* Close, discarding lock. */
596 next: (void)fclose(fp);
603 (void)printf("%s: No files to recover\n", sp->gp->progname);
604 (void)closedir(dirp);
610 * Start a recovered file as the file to edit.
612 * PUBLIC: int rcv_read(SCR *, FREF *);
624 struct timespec rec_mtim = { 0, 0 };
625 int found, locked = 0, requested, sv_fd;
626 char *name, *p, *t, *rp, *recp, *pathp;
627 char *file, *path, *recpath;
631 if (opts_empty(sp, O_RECDIR, 0))
633 rp = O_STR(sp, O_RECDIR);
634 if ((dirp = opendir(rp)) == NULL) {
635 msgq_str(sp, M_SYSERR, rp, "%s");
642 for (found = requested = 0; (dp = readdir(dirp)) != NULL;) {
643 if (strncmp(dp->d_name, "recover.", 8))
645 if ((recpath = join(rp, dp->d_name)) == NULL) {
646 msgq(sp, M_SYSERR, NULL);
650 /* If it's readable, it's recoverable. */
651 if ((fp = fopen(recpath, "r")) == NULL) {
656 switch (file_lock(sp, NULL, fileno(fp), 1)) {
660 * Assume that a lock can't be acquired, but that we
661 * should permit recovery anyway. If this is wrong,
662 * and someone else is using the file, we're going to
671 /* If it's locked, it's live. */
676 /* Check the headers. */
677 for (file = NULL, path = NULL;
678 file == NULL || path == NULL;) {
679 if ((st = rcv_dlnread(sp, &dtype, &data, fp))) {
681 msgq_str(sp, M_ERR, dp->d_name,
682 "067|%s: malformed recovery file");
687 if (!strcmp(dtype, "file"))
689 else if (!strcmp(dtype, "path"))
697 * If the file doesn't exist, it's an orphaned recovery file,
701 * This can occur if the backup file was deleted and we crashed
702 * before deleting the email file.
705 if (stat(path, &sb) &&
707 (void)unlink(dp->d_name);
711 /* Check the file name. */
712 if (strcmp(file, name))
717 /* If we've found more than one, take the most recent. */
718 (void)fstat(fileno(fp), &sb);
720 timespeccmp(&rec_mtim, &sb.st_mtimespec, <)) {
729 rec_mtim = sb.st_mtimespec;
732 sv_fd = dup(fileno(fp));
742 (void)closedir(dirp);
745 msgq_str(sp, M_INFO, name,
746 "068|No files named %s, readable by you, to recover");
752 "069|There are older versions of this file for you to recover");
753 if (found > requested)
755 "070|There are other files for you to recover");
759 * Create the FREF structure, start the btree file.
762 * file_init() is going to set ep->rcv_path.
764 if (file_init(sp, frp, pathp, 0)) {
773 * We keep an open lock on the file so that the recover option can
774 * distinguish between files that are live and those that need to
775 * be recovered. The lock is already acquired, just copy it.
778 ep->rcv_mpath = recp;
781 F_SET(frp, FR_UNLOCKED);
783 /* We believe the file is recoverable. */
790 * Copy a recovery file.
798 int nr, nw, off, rfd;
801 if ((rfd = open(fname, O_RDONLY, 0)) == -1)
803 while ((nr = read(rfd, buf, sizeof(buf))) > 0)
804 for (off = 0; nr; nr -= nw, off += nw)
805 if ((nw = write(wfd, buf + off, nr)) < 0)
810 err: msgq_str(sp, M_SYSERR, fname, "%s");
816 * Paranoid make temporary file routine.
826 if ((fd = mkstemp(path)) == -1)
827 msgq_str(sp, M_SYSERR, dname, "%s");
842 (void)asprintf(&buf, _PATH_SENDMAIL " -odb -t < %s", fname);
844 msgq_str(sp, M_ERR, strerror(errno),
845 "071|not sending email: %s");
854 * Encode a string into an X-vi-data line and write it.
869 dlen = strlen(dtype);
870 GET_SPACE_GOTOC(sp, bp, blen, (len + 2) / 3 * 4 + dlen + 2);
871 (void)memcpy(bp, dtype, dlen);
873 if ((xlen = b64_ntop((u_char *)src,
874 len, bp + dlen + 1, blen)) == -1)
878 /* Output as an MIME folding header. */
879 if ((plen = fprintf(fp, VI_DHEADER " %.*s\n",
880 FMTCOLS - (int)sizeof(VI_DHEADER), bp)) < 0)
882 plen -= (int)sizeof(VI_DHEADER) + 1;
883 for (p = bp, xlen -= plen; xlen > 0; xlen -= plen) {
885 if ((plen = fprintf(fp, " %.*s\n", FMTCOLS - 1, p)) < 0)
889 FREE_SPACE(sp, bp, blen);
892 err: FREE_SPACE(sp, bp, blen);
895 msgq(sp, M_SYSERR, NULL);
901 * Read an X-vi-data line and decode it.
907 char **datap, /* free *datap if != NULL after use. */
912 char *bp = NULL, *p, *src;
914 size_t len, off, dlen;
918 if (fgets(buf, sizeof(buf), fp) == NULL)
920 if (strncmp(buf, VI_DHEADER, sizeof(VI_DHEADER) - 1)) {
926 /* Fetch an MIME folding header. */
927 len = strlen(buf) - sizeof(VI_DHEADER) + 1;
928 GET_SPACE_GOTOC(sp, bp, blen, len);
929 (void)memcpy(bp, buf + sizeof(VI_DHEADER) - 1, len);
931 while ((ch = fgetc(fp)) == ' ') {
932 if (fgets(buf, sizeof(buf), fp) == NULL)
936 ADD_SPACE_GOTOC(sp, bp, blen, len);
938 (void)memcpy(p, buf, off);
941 (void)ungetc(ch, fp);
943 for (p = bp; *p == ' ' || *p == '\n'; p++);
944 if ((src = strchr(p, ';')) == NULL)
950 /* Memory looks like: "<data>\0<dtype>\0". */
951 MALLOC(sp, data, char *, dlen + len / 4 * 3 + 2);
954 if ((xlen = (b64_pton(p + dlen + 1,
955 (u_char *)data, len / 4 * 3 + 1))) == -1) {
960 dtype = data + xlen + 1;
961 (void)memcpy(dtype, p, dlen);
963 FREE_SPACE(sp, bp, blen);
968 err: FREE_SPACE(sp, bp, blen);
971 msgq(sp, M_SYSERR, NULL);