/* * bootlogd.c Store output from the console during bootup into a file. * The file is usually located on the /var partition, and * gets written (and fsynced) as soon as possible. * * Version: @(#)bootlogd 2.86pre 12-Jan-2004 miquels@cistron.nl * * Bugs: Uses openpty(), only available in glibc. Sorry. * * This file is part of the sysvinit suite, * Copyright (C) 1991-2004 Miquel van Smoorenburg. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * *NOTE* *NOTE* *NOTE* * This is a PROOF OF CONCEPT IMPLEMENTATION * * I have bigger plans for Debian, but for now * this has to do ;) * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include #endif char *Version = "@(#) bootlogd 2.86 03-Jun-2004 miquels@cistron.nl"; #define LOGFILE "/var/log/boot" char ringbuf[32768]; char *endptr = ringbuf + sizeof(ringbuf); char *inptr = ringbuf; char *outptr = ringbuf; int got_signal = 0; int didnl = 1; int createlogfile = 0; int syncalot = 0; /* * Console devices as listed on the kernel command line and * the mapping to actual devices in /dev */ struct consdev { char *cmdline; char *dev1; char *dev2; } consdev[] = { { "ttyB", "/dev/ttyB%s", NULL }, { "ttySC", "/dev/ttySC%s", "/dev/ttsc/%s" }, { "ttyS", "/dev/ttyS%s", "/dev/tts/%s" }, { "tty", "/dev/tty%s", "/dev/vc/%s" }, { "hvc", "/dev/hvc%s", "/dev/hvc/%s" }, { NULL, NULL, NULL }, }; /* * Devices to try as console if not found on kernel command line. * Tried from left to right (as opposed to kernel cmdline). */ char *defcons[] = { "tty0", "hvc0", "ttyS0", "ttySC0", "ttyB0", NULL }; /* * Catch signals. */ void handler(int sig) { got_signal = sig; } /* * Scan /dev and find the device name. */ static int findtty(char *res, const char *startdir, int rlen, dev_t dev) { DIR *dir; struct dirent *ent; struct stat st; int r = -1; char *olddir = getcwd(NULL, 0); if (chdir(startdir) < 0 || (dir = opendir(".")) == NULL) { int msglen = strlen(startdir) + 11; char *msg = malloc(msglen); snprintf(msg, msglen, "bootlogd: %s", startdir); perror(msg); free(msg); chdir(olddir); return -1; } while ((ent = readdir(dir)) != NULL) { if (lstat(ent->d_name, &st) != 0) continue; if (S_ISDIR(st.st_mode) && 0 != strcmp(".", ent->d_name) && 0 != strcmp("..", ent->d_name)) { char *path = malloc(rlen); snprintf(path, rlen, "%s/%s", startdir, ent->d_name); r = findtty(res, path, rlen, dev); free(path); if (0 == r) { /* device found, return */ closedir(dir); chdir(olddir); return 0; } continue; } if (!S_ISCHR(st.st_mode)) continue; if (st.st_rdev == dev) { if ( (int) (strlen(ent->d_name) + strlen(startdir) + 1) >= rlen) { fprintf(stderr, "bootlogd: console device name too long\n"); closedir(dir); chdir(olddir); return -1; } else { snprintf(res, rlen, "%s/%s", startdir, ent->d_name); closedir(dir); chdir(olddir); return 0; } } } closedir(dir); chdir(olddir); return r; } /* * For some reason, openpty() in glibc sometimes doesn't * work at boot-time. It must be a bug with old-style pty * names, as new-style (/dev/pts) is not available at that * point. So, we find a pty/tty pair ourself if openpty() * fails for whatever reason. */ int findpty(int *master, int *slave, char *name) { char pty[16]; char tty[16]; int i, j; int found; if (openpty(master, slave, name, NULL, NULL) >= 0) return 0; found = 0; for (i = 'p'; i <= 'z'; i++) { for (j = '0'; j <= 'f'; j++) { if (j == '9' + 1) j = 'a'; sprintf(pty, "/dev/pty%c%c", i, j); sprintf(tty, "/dev/tty%c%c", i, j); if ((*master = open(pty, O_RDWR|O_NOCTTY)) >= 0) { *slave = open(tty, O_RDWR|O_NOCTTY); if (*slave >= 0) { found = 1; break; } } } if (found) break; } if (!found) return -1; if (name) strcpy(name, tty); return 0; } /* * See if a console taken from the kernel command line maps * to a character device we know about, and if we can open it. */ int isconsole(char *s, char *res, int rlen) { struct consdev *c; int l, sl, i, fd; char *p, *q; sl = strlen(s); for (c = consdev; c->cmdline; c++) { l = strlen(c->cmdline); if (sl <= l) continue; p = s + l; if (strncmp(s, c->cmdline, l) != 0 || !isdigit(*p)) continue; for (i = 0; i < 2; i++) { snprintf(res, rlen, i ? c->dev1 : c->dev2, p); if ((q = strchr(res, ',')) != NULL) *q = 0; if ((fd = open(res, O_RDONLY|O_NONBLOCK)) >= 0) { close(fd); return 1; } } } return 0; } /* * Find out the _real_ console. Assume that stdin is connected to * the console device (/dev/console). */ int consolename(char *res, int rlen) { #ifdef TIOCGDEV unsigned int kdev; #endif struct stat st, st2; char buf[256]; char *p; int didmount = 0; int n, r; int fd; fstat(0, &st); if (major(st.st_rdev) != 5 || minor(st.st_rdev) != 1) { /* * Old kernel, can find real device easily. */ int r = findtty(res, "/dev", rlen, st.st_rdev); if (0 != r) fprintf(stderr, "bootlogd: cannot find console device " "%d:%d under /dev\n", major(st.st_rdev), minor(st.st_rdev)); return r; } #ifdef TIOCGDEV # ifndef ENOIOCTLCMD # define ENOIOCTLCMD 515 # endif if (ioctl(0, TIOCGDEV, &kdev) == 0) { int r = findtty(res, "/dev", rlen, (dev_t)kdev); if (0 != r) fprintf(stderr, "bootlogd: cannot find console device " "%d:%d under /dev\n", major(kdev), minor(kdev)); return r; } if (errno != ENOIOCTLCMD) return -1; #endif #ifdef __linux__ /* * Read /proc/cmdline. */ stat("/", &st); if (stat("/proc", &st2) < 0) { perror("bootlogd: /proc"); return -1; } if (st.st_dev == st2.st_dev) { if (mount("proc", "/proc", "proc", 0, NULL) < 0) { perror("bootlogd: mount /proc"); return -1; } didmount = 1; } n = 0; r = -1; if ((fd = open("/proc/cmdline", O_RDONLY)) < 0) { perror("bootlogd: /proc/cmdline"); } else { buf[0] = 0; if ((n = read(fd, buf, sizeof(buf) - 1)) >= 0) r = 0; else perror("bootlogd: /proc/cmdline"); close(fd); } if (didmount) umount("/proc"); if (r < 0) return r; /* * OK, so find console= in /proc/cmdline. * Parse in reverse, opening as we go. */ p = buf + n; *p-- = 0; r = -1; while (p >= buf) { if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') { *p-- = 0; continue; } if (strncmp(p, "console=", 8) == 0 && isconsole(p + 8, res, rlen)) { r = 0; break; } p--; } if (r == 0) return r; #endif /* * Okay, no console on the command line - * guess the default console. */ for (n = 0; defcons[n]; n++) if (isconsole(defcons[n], res, rlen)) return 0; fprintf(stderr, "bootlogd: cannot deduce real console device\n"); return -1; } /* * Write data and make sure it's on disk. */ void writelog(FILE *fp, unsigned char *ptr, int len) { int dosync = 0; int i; static int first_run = 1; static int inside_esc = 0; for (i = 0; i < len; i++) { int ignore = 0; /* prepend date to every line */ if (*(ptr-1) == '\n' || first_run) { time_t t; char *s; time(&t); s = ctime(&t); fprintf(fp, "%.24s: ", s); dosync = 1; first_run = 0; } /* remove escape sequences, but do it in a way that allows us to stop * in the middle in case the string was cut off */ if (inside_esc == 1) { /* first '[' is special because if we encounter it again, it should be considered the final byte */ if (*ptr == '[') { /* multi char sequence */ ignore = 1; inside_esc = 2; } else { /* single char sequence */ if (*ptr >= 64 && *ptr <= 95) { ignore = 1; } inside_esc = 0; } } else if (inside_esc == 2) { switch (*ptr) { case '0' ... '9': /* intermediate chars of escape sequence */ case ';': case 32 ... 47: if (inside_esc) { ignore = 1; } break; case 64 ... 126: /* final char of escape sequence */ if (inside_esc) { ignore = 1; inside_esc = 0; } break; } } else { switch (*ptr) { case '\r': ignore = 1; break; case 27: /* ESC */ ignore = 1; inside_esc = 1; break; } } if (!ignore) { fwrite(ptr, sizeof(char), 1, fp); } ptr++; } if (dosync) { fflush(fp); if (syncalot) { fdatasync(fileno(fp)); } } outptr += len; if (outptr >= endptr) outptr = ringbuf; } /* * Print usage message and exit. */ void usage(void) { fprintf(stderr, "Usage: bootlogd [-v] [-r] [-d] [-s] [-c] [-p pidfile] [-l logfile]\n"); exit(1); } int open_nb(char *buf) { int fd, n; if ((fd = open(buf, O_WRONLY|O_NONBLOCK|O_NOCTTY)) < 0) return -1; n = fcntl(fd, F_GETFL); n &= ~(O_NONBLOCK); fcntl(fd, F_SETFL, n); return fd; } /* * We got a write error on the real console. If its an EIO, * somebody hung up our filedescriptor, so try to re-open it. */ int write_err(int pts, int realfd, char *realcons, int e) { int fd; if (e != EIO) { werr: close(pts); fprintf(stderr, "bootlogd: writing to console: %s\n", strerror(e)); return -1; } close(realfd); if ((fd = open_nb(realcons)) < 0) goto werr; return fd; } int main(int argc, char **argv) { FILE *fp; struct timeval tv; fd_set fds; char buf[1024]; char realcons[1024]; char *p; char *logfile; char *pidfile; int rotate; int dontfork; int ptm, pts; int realfd; int n, m, i; int todo; fp = NULL; logfile = LOGFILE; pidfile = NULL; rotate = 0; dontfork = 0; while ((i = getopt(argc, argv, "cdsl:p:rv")) != EOF) switch(i) { case 'l': logfile = optarg; break; case 'r': rotate = 1; break; case 'v': printf("%s\n", Version); exit(0); break; case 'p': pidfile = optarg; break; case 'c': createlogfile = 1; break; case 'd': dontfork = 1; break; case 's': syncalot = 1; break; default: usage(); break; } if (optind < argc) usage(); signal(SIGTERM, handler); signal(SIGQUIT, handler); signal(SIGINT, handler); signal(SIGTTIN, SIG_IGN); signal(SIGTTOU, SIG_IGN); signal(SIGTSTP, SIG_IGN); /* * Open console device directly. */ if (consolename(realcons, sizeof(realcons)) < 0) return 1; if (strcmp(realcons, "/dev/tty0") == 0) strcpy(realcons, "/dev/tty1"); if (strcmp(realcons, "/dev/vc/0") == 0) strcpy(realcons, "/dev/vc/1"); if ((realfd = open_nb(realcons)) < 0) { fprintf(stderr, "bootlogd: %s: %s\n", buf, strerror(errno)); return 1; } /* * Grab a pty, and redirect console messages to it. */ ptm = -1; pts = -1; buf[0] = 0; if (findpty(&ptm, &pts, buf) < 0) { fprintf(stderr, "bootlogd: cannot allocate pseudo tty: %s\n", strerror(errno)); return 1; } (void)ioctl(0, TIOCCONS, NULL); #if 1 /* Work around bug in 2.1/2.2 kernels. Fixed in 2.2.13 and 2.3.18 */ if ((n = open("/dev/tty0", O_RDWR)) >= 0) { (void)ioctl(n, TIOCCONS, NULL); close(n); } #endif if (ioctl(pts, TIOCCONS, NULL) < 0) { fprintf(stderr, "bootlogd: ioctl(%s, TIOCCONS): %s\n", buf, strerror(errno)); return 1; } /* * Fork and write pidfile if needed. */ if (!dontfork) { pid_t child_pid = fork(); switch (child_pid) { case -1: /* I am parent and the attempt to create a child failed */ fprintf(stderr, "bootlogd: fork failed: %s\n", strerror(errno)); exit(1); break; case 0: /* I am the child */ break; default: /* I am parent and got child's pid */ exit(0); break; } setsid(); } if (pidfile) { unlink(pidfile); if ((fp = fopen(pidfile, "w")) != NULL) { fprintf(fp, "%d\n", (int)getpid()); fclose(fp); } fp = NULL; } /* * Read the console messages from the pty, and write * to the real console and the logfile. */ while (!got_signal) { /* * We timeout after 5 seconds if we still need to * open the logfile. There might be buffered messages * we want to write. */ tv.tv_sec = 0; tv.tv_usec = 500000; FD_ZERO(&fds); FD_SET(ptm, &fds); if (select(ptm + 1, &fds, NULL, NULL, &tv) == 1) { /* * See how much space there is left, read. */ if ((n = read(ptm, inptr, endptr - inptr)) >= 0) { /* * Write data (in chunks if needed) * to the real output device. */ m = n; p = inptr; while (m > 0) { i = write(realfd, p, m); if (i >= 0) { m -= i; p += i; continue; } /* * Handle EIO (somebody hung * up our filedescriptor) */ realfd = write_err(pts, realfd, realcons, errno); if (realfd >= 0) continue; got_signal = 1; /* Not really */ break; } /* * Increment buffer position. Handle * wraps, and also drag output pointer * along if we cross it. */ inptr += n; if (inptr - n < outptr && inptr > outptr) outptr = inptr; if (inptr >= endptr) inptr = ringbuf; if (outptr >= endptr) outptr = ringbuf; } } /* * Perhaps we need to open the logfile. */ if (fp == NULL && access(logfile, F_OK) == 0) { if (rotate) { snprintf(buf, sizeof(buf), "%s~", logfile); rename(logfile, buf); } fp = fopen(logfile, "a"); } if (fp == NULL && createlogfile) fp = fopen(logfile, "a"); if (inptr >= outptr) todo = inptr - outptr; else todo = endptr - outptr; if (fp && todo) writelog(fp, (unsigned char *)outptr, todo); } if (fp) { if (!didnl) fputc('\n', fp); fclose(fp); } close(pts); close(ptm); close(realfd); return 0; }