diff options
Diffstat (limited to 'inotail.c')
-rw-r--r-- | inotail.c | 464 |
1 files changed, 464 insertions, 0 deletions
diff --git a/inotail.c b/inotail.c new file mode 100644 index 0000000..7b58477 --- /dev/null +++ b/inotail.c @@ -0,0 +1,464 @@ +/* + * inotail.c + * A fast implementation of tail which uses the inotify API present in + * recent versions of the Linux kernel. + * + * Copyright (C) 2005-2006, Tobias Klauser <tklauser@distanz.ch> + * + * The idea was taken from turbotail. + * + * 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, 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. + */ + +#define _GNU_SOURCE + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <getopt.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include "inotify.h" +#include "inotify-syscalls.h" + +#include "inotail.h" + +#define PROGRAM_NAME "inotail" +#define BUFFER_SIZE 4096 + +/* (ino)tail works on these file types */ +#define IS_TAILABLE(mode) \ + (S_ISREG(mode) || S_ISFIFO(mode) || S_ISSOCK(mode) || S_ISCHR(mode)) + +/* Print header with filename before tailing the file? */ +static char verbose = 0; + +/* Tailing relative to begin or end of file */ +static char from_begin = 0; + +/* Command line options */ +static const struct option long_opts[] = { + {"bytes", required_argument, NULL, 'c'}, + {"follow", optional_argument, NULL, 'f'}, + {"lines", required_argument, NULL, 'n'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {"version", no_argument, NULL, 'V'}, + {NULL, 0, NULL, 0} +}; + +static void usage(const int status) +{ + fprintf(stderr, "Usage: %s [OPTION]... [FILE]...\n\n" + " -c N, --bytes=N output the last N bytes\n" + " -f, --follow output as the file grows\n" + " -n N, --lines=N output the last N lines (default: %d)\n" + " -v, --verbose print headers with file names\n" + " -h, --help show this help and exit\n" + " -V, --version show version and exit\n\n" + "If the first character of N (the number of bytes or lines) is a `+',\n" + "begin printing with the Nth item from the start of each file, otherwise,\n" + "print the last N items in the file.\n", PROGRAM_NAME, DEFAULT_N_LINES); + + exit(status); +} + +static void setup_file(struct file_struct *f) +{ + f->fd = -1; + f->st_size = 0; + f->ignore = 0; + f->i_watch = -1; +} + +static inline char *pretty_name(char *filename) +{ + return (strncmp(filename, "-", 1) == 0) ? "standard input" : filename; +} + +static void write_header(char *filename) +{ + static unsigned short first_file = 1; + + fprintf(stdout, "%s==> %s <==\n", (first_file ? "" : "\n"), pretty_name(filename)); + first_file = 0; +} + +static off_t lines_to_offset_from_end(struct file_struct *f, unsigned int n_lines) +{ + char buf[BUFFER_SIZE]; + off_t offset = f->st_size; + + n_lines++; /* We also count the last \n */ + memset(&buf, 0, sizeof(buf)); + + while (offset > 0 && n_lines > 0) { + int i, rc; + int block_size = BUFFER_SIZE; /* Size of the current block we're reading */ + + if (offset < BUFFER_SIZE) + block_size = offset; + + /* Start of current block */ + offset -= block_size; + + lseek(f->fd, offset, SEEK_SET); + + rc = read(f->fd, &buf, block_size); + if (rc < 0) { + fprintf(stderr, "Error: Could not read from file '%s' (%s)\n", f->name, strerror(errno)); + return -1; + } + + for (i = block_size; i > 0; i--) { + if (buf[i] == '\n') { + n_lines--; + + if (n_lines == 0) { + offset += i + 1; /* We don't want the first \n */ + break; + } + } + } + } + + return offset; + +} + +static off_t lines_to_offset_from_begin(struct file_struct *f, unsigned int n_lines) +{ + char buf[BUFFER_SIZE]; + off_t offset = 0; + + /* tail everything for 'inotail -n +0' */ + if (n_lines == 0) + return 0; + + n_lines--; + memset(&buf, 0, sizeof(buf)); + + while (offset <= f->st_size && n_lines > 0) { + int i, rc; + int block_size = BUFFER_SIZE; + + lseek(f->fd, offset, SEEK_SET); + + rc = read(f->fd, &buf, block_size); + if (rc < 0) { + fprintf(stderr, "Error: Could not read from file '%s' (%s)\n", f->name, strerror(errno)); + return -1; + } + + for (i = 0; i < block_size; i++) { + if (buf[i] == '\n') { + n_lines--; + if (n_lines == 0) { + offset += i + 1; + return offset; + } + } + } + + offset += block_size; + } + + return offset; +} + +static off_t lines_to_offset(struct file_struct *f, unsigned int n_lines) +{ + if (from_begin) + return lines_to_offset_from_begin(f, n_lines); + else + return lines_to_offset_from_end(f, n_lines); +} + +static inline off_t bytes_to_offset(struct file_struct *f, unsigned int n_bytes) +{ + /* tail everything for 'inotail -c +0' */ + if (from_begin && n_bytes == 0) + return 0; + else + return (from_begin ? ((off_t) n_bytes - 1) : (f->st_size - (off_t) n_bytes)); +} + +static int tail_pipe(struct file_struct *f) +{ + int rc; + char buf[BUFFER_SIZE]; + + if (verbose) + write_header(f->name); + + /* We will just tail everything here */ + while ((rc = read(f->fd, &buf, BUFFER_SIZE)) > 0) + write(STDOUT_FILENO, buf, (size_t) rc); + + return rc; +} + +static int tail_file(struct file_struct *f, unsigned int n_units, char mode) +{ + int ret = -1; + ssize_t bytes_read = 0; + off_t offset = 0; + char buf[BUFFER_SIZE]; + struct stat finfo; + + if (strncmp(f->name, "-", 1) == 0) + f->fd = STDIN_FILENO; + else { + f->fd = open(f->name, O_RDONLY); + if (f->fd < 0) { + fprintf(stderr, "Error: Could not open file '%s' (%s)\n", f->name, strerror(errno)); + return -1; + } + } + + if (fstat(f->fd, &finfo) < 0) { + fprintf(stderr, "Error: Could not stat file '%s' (%s)\n", f->name, strerror(errno)); + goto out; + } + + if (!IS_TAILABLE(finfo.st_mode)) { + fprintf(stderr, "Error: '%s' of unsupported file type\n", f->name); + goto out; + } + + /* We cannot seek on these */ + if (S_ISFIFO(finfo.st_mode) || S_ISSOCK(finfo.st_mode) || f->fd == STDIN_FILENO) + return tail_pipe(f); + + f->st_size = finfo.st_size; + + if (mode == M_LINES) + offset = lines_to_offset(f, n_units); + else + offset = bytes_to_offset(f, n_units); + + /* We only get negative offsets on errors */ + if (offset < 0) + goto out; + + if (verbose) + write_header(f->name); + + lseek(f->fd, offset, SEEK_SET); + + while ((bytes_read = read(f->fd, &buf, BUFFER_SIZE)) > 0) + write(STDOUT_FILENO, buf, (size_t) bytes_read); + + ret = 0; +out: + if (close(f->fd) < 0) { + fprintf(stderr, "Error: Could not close file '%s' (%s)\n", f->name, strerror(errno)); + ret = -1; + } + + return ret; +} + +static int handle_inotify_event(struct inotify_event *inev, struct file_struct *fil, int n_ignored) +{ + off_t offset; + + if (inev->mask & IN_MODIFY) { + int rc; + char fbuf[BUFFER_SIZE]; + struct stat finfo; + + fil->fd = open(fil->name, O_RDONLY); + if (fil->fd < 0) { + fprintf(stderr, "Error: Could not open file '%s' (%s)\n", fil->name, strerror(errno)); + goto ignore; + } + + if (fstat(fil->fd, &finfo) < 0) { + fprintf(stderr, "Error: Could not stat file '%s' (%s)\n", fil->name, strerror(errno)); + close(fil->fd); + goto ignore; + } + + offset = fil->st_size; + fil->st_size = finfo.st_size; + + if (verbose) + write_header(fil->name); + + memset(&fbuf, 0, sizeof(fbuf)); + + lseek(fil->fd, offset, SEEK_SET); + while ((rc = read(fil->fd, &fbuf, BUFFER_SIZE)) != 0) + write(STDOUT_FILENO, fbuf, rc); + + close(fil->fd); + + return n_ignored; + } else if (inev->mask & IN_DELETE_SELF) { + fprintf(stderr, "File '%s' deleted.\n", fil->name); + } else if (inev->mask & IN_MOVE_SELF) { + /* TODO: Try to follow file/fd */ + fprintf(stderr, "File '%s' moved.\n", fil->name); + } else if (inev->mask & IN_UNMOUNT) { + fprintf(stderr, "Device containing file '%s' unmounted.\n", fil->name); + } + +ignore: + fil->ignore = 1; + n_ignored++; + + return n_ignored; +} + +static int watch_files(struct file_struct *f, int n_files) +{ + int ifd, i, n_ignored = 0; + unsigned buflen = 2 * n_files * sizeof(struct inotify_event); + struct inotify_event *inev; + char *buf; + + ifd = inotify_init(); + if (ifd < 0) { + fprintf(stderr, "Error: Inotify is not supported by the kernel you're currently running.\n"); + exit(EXIT_FAILURE); + } + + for (i = 0; i < n_files; i++) { + if (!f[i].ignore) + f[i].i_watch = inotify_add_watch(ifd, f[i].name, + IN_MODIFY|IN_DELETE_SELF|IN_MOVE_SELF|IN_UNMOUNT); + else + n_ignored++; + } + + buf = malloc(buflen); + memset(buf, 0, buflen); + + while (n_ignored < n_files) { + int len; + + len = read(ifd, buf, buflen); + inev = (struct inotify_event *) buf; + + while (len > 0) { + struct file_struct *fil = NULL; + + /* Which file has produced the event? */ + for (i = 0; i < n_files; i++) { + if (!f[i].ignore && f[i].fd >= 0 && f[i].i_watch == inev->wd) { + fil = &f[i]; + break; + } + } + + if (unlikely(!fil)) + break; + + n_ignored = handle_inotify_event(inev, fil, n_ignored); + len -= sizeof(struct inotify_event) + inev->len; + inev = (struct inotify_event *) ((char *) inev + sizeof(struct inotify_event) + inev->len); + } + } + + free(buf); + close(ifd); + + return n_ignored; +} + +int main(int argc, char **argv) +{ + int i, c, ret = 0; + int n_files = 0; + int n_units = DEFAULT_N_LINES; + char forever = 0, mode = M_LINES; + char **filenames; + struct file_struct *files; + + while ((c = getopt_long(argc, argv, "c:n:fvVh", long_opts, NULL)) != -1) { + switch (c) { + case 'c': + mode = M_BYTES; + /* fall through */ + case 'n': + if (*optarg == '+') + from_begin = 1; + else if (*optarg == '-') + optarg++; + n_units = strtoul(optarg, NULL, 0); + if (n_units < 0) + n_units = 0; + break; + case 'f': + forever = 1; + break; + case 'v': + verbose = 1; + break; + case 'V': + fprintf(stderr, "%s %s\n", PROGRAM_NAME, VERSION); + return 0; + case 'h': + usage(EXIT_SUCCESS); + default: + usage(EXIT_FAILURE); + } + } + + /* Do we have some files to read from? */ + if (optind < argc) { + n_files = argc - optind; + filenames = argv + optind; + } else { + /* It must be stdin then */ + static char *dummy_stdin = "-"; + n_files++; + filenames = &dummy_stdin; + + /* POSIX says that -f is ignored if no file operand is + specified and standard input is a pipe. */ + if (forever) { + struct stat finfo; + if (fstat(STDIN_FILENO, &finfo) == 0 + && (S_ISFIFO(finfo.st_mode) || S_ISSOCK(finfo.st_mode))) + forever = 0; + } + } + + files = malloc(n_files * sizeof(struct file_struct)); + if (!files) { + fprintf(stderr, "Error: Not enough memory (%s)\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + for (i = 0; i < n_files; i++) { + files[i].name = filenames[i]; + setup_file(&files[i]); + ret = tail_file(&files[i], n_units, mode); + if (ret < 0) + files[i].ignore = 1; + } + + if (forever) + ret = watch_files(files, n_files); + + free(files); + + return ret; +} |