#include "log_tail.h" #include "filter.h" #include "client.h" #include "unit.h" #include "log.h" #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_LOG_WATCHES (MAX_UNITS * 4) typedef struct { int inotify_fd; int watch_wd; /* file watch descriptor, or -1 if no current file */ int log_fd; /* log file fd, or -1 if no current file */ Unit *unit; char path[MAX_PATH]; /* directory-mode fields (all -1 / "" when not in dir mode) */ int dir_wd; /* directory watch descriptor, or -1 */ char dir_path[MAX_PATH]; /* watched directory */ char pattern[MAX_PATH]; /* fnmatch pattern copy */ } LogWatch; static LogWatch watches[MAX_LOG_WATCHES]; static int watch_count = 0; /* Drain any available data from w->log_fd into ring + clients. */ static void log_tail_drain(LogWatch *w) { assert(w != NULL); assert(w->unit != NULL); if (w->log_fd < 0) return; char buf[RING_BUF_LINE_MAX]; ssize_t n; while ((n = read(w->log_fd, buf, sizeof(buf) - 1)) > 0) { buf[n] = '\0'; int nf = (int)n; filter_apply(w->unit, buf, sizeof(buf), &nf); if (nf <= 0) continue; ring_push(w->unit->output, buf, nf); client_broadcast_output(w->unit->name, buf, 0); } } /* * Find the most-recently-modified file in dir matching pattern. * Writes filename (not full path) into out_name[out_size]. * Returns 0 on success, -1 if nothing found. */ static int log_tail_scan_dir(const char *dir, const char *pattern, char *out_name, int out_size) { assert(dir != NULL); assert(pattern != NULL); DIR *d = opendir(dir); if (!d) { log_warn("log_tail: opendir %s: %s", dir, strerror(errno)); return -1; } struct dirent *ent; char best_name[MAX_PATH] = {0}; time_t best_sec = -1; long best_nsec = -1; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; if (fnmatch(pattern, ent->d_name, 0) != 0) continue; char full[MAX_PATH]; int r = snprintf(full, sizeof(full), "%s/%s", dir, ent->d_name); if (r < 0 || r >= (int)sizeof(full)) continue; struct stat st; if (stat(full, &st) != 0) continue; if (best_sec < 0 || st.st_mtim.tv_sec > best_sec || (st.st_mtim.tv_sec == best_sec && st.st_mtim.tv_nsec > best_nsec)) { best_sec = st.st_mtim.tv_sec; best_nsec = st.st_mtim.tv_nsec; strncpy(best_name, ent->d_name, sizeof(best_name) - 1); best_name[sizeof(best_name) - 1] = '\0'; } } closedir(d); if (!best_name[0]) return -1; strncpy(out_name, best_name, out_size - 1); out_name[out_size - 1] = '\0'; return 0; } /* * Drain old file, close its watch descriptor, open new_fname (relative to * w->dir_path), and add a new file watch descriptor on the same inotify_fd. * seek_end=1 at init (skip existing content), seek_end=0 for new files. * Returns 0 on success, -1 on error. */ static int log_tail_switch_file(LogWatch *w, const char *new_fname, int seek_end) { assert(w != NULL); assert(new_fname != NULL); log_tail_drain(w); if (w->watch_wd >= 0) { inotify_rm_watch(w->inotify_fd, w->watch_wd); w->watch_wd = -1; } if (w->log_fd >= 0) { close(w->log_fd); w->log_fd = -1; } char full[MAX_PATH]; int r = snprintf(full, sizeof(full), "%s/%s", w->dir_path, new_fname); if (r < 0 || r >= (int)sizeof(full)) { log_warn("log_tail: path too long for %s/%s", w->dir_path, new_fname); return -1; } int fd = open(full, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (fd < 0) { log_warn("log_tail: cannot open %s: %s", full, strerror(errno)); return -1; } if (seek_end) lseek(fd, 0, SEEK_END); int wd = inotify_add_watch(w->inotify_fd, full, IN_MODIFY | IN_MOVE_SELF | IN_DELETE_SELF); if (wd < 0) { log_warn("log_tail: inotify_add_watch %s: %s", full, strerror(errno)); close(fd); return -1; } w->log_fd = fd; w->watch_wd = wd; strncpy(w->path, full, MAX_PATH - 1); w->path[MAX_PATH - 1] = '\0'; log_info("log_tail: switched to %s for unit %s", full, w->unit->name); return 0; } /* Open a fixed-path log file and register it with inotify + epoll. */ static void log_tail_open_fixed_watch(Unit *u, const char *path) { assert(u != NULL); assert(path != NULL); if (watch_count >= MAX_LOG_WATCHES) { log_warn("log_tail: max watches reached, skipping %s", path); return; } char resolved[MAX_PATH]; const char *open_path = path; if (realpath(path, resolved) != NULL) open_path = resolved; int log_fd = open(open_path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (log_fd < 0) { log_warn("log_tail: cannot open %s: %s", open_path, strerror(errno)); return; } lseek(log_fd, 0, SEEK_END); int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (ifd < 0) { log_error("inotify_init1: %s", strerror(errno)); close(log_fd); return; } int wd = inotify_add_watch(ifd, open_path, IN_MODIFY | IN_MOVE_SELF | IN_DELETE_SELF); if (wd < 0) { log_warn("log_tail: inotify_add_watch %s: %s", open_path, strerror(errno)); close(ifd); close(log_fd); return; } LogWatch *w = &watches[watch_count++]; w->inotify_fd = ifd; w->watch_wd = wd; w->log_fd = log_fd; w->unit = u; w->dir_wd = -1; w->dir_path[0] = '\0'; w->pattern[0] = '\0'; strncpy(w->path, open_path, MAX_PATH - 1); w->path[MAX_PATH - 1] = '\0'; struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = ifd; if (epoll_ctl(g.epoll_fd, EPOLL_CTL_ADD, ifd, &ev) < 0) log_warn("log_tail: epoll_ctl ADD %s: %s", open_path, strerror(errno)); log_info("log_tail: watching %s for unit %s", open_path, u->name); } /* Open a directory watch for a unit using log_dir mode. */ static void log_tail_open_dir_watch(Unit *u) { assert(u != NULL); assert(u->log_dir[0] != '\0'); if (watch_count >= MAX_LOG_WATCHES) { log_warn("log_tail: max watches reached, skipping dir %s", u->log_dir); return; } int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (ifd < 0) { log_error("inotify_init1 for dir %s: %s", u->log_dir, strerror(errno)); return; } int dwd = inotify_add_watch(ifd, u->log_dir, IN_CREATE | IN_MOVED_TO); if (dwd < 0) { log_warn("log_tail: inotify_add_watch dir %s: %s", u->log_dir, strerror(errno)); close(ifd); return; } LogWatch *w = &watches[watch_count++]; w->inotify_fd = ifd; w->dir_wd = dwd; w->watch_wd = -1; w->log_fd = -1; w->unit = u; w->path[0] = '\0'; strncpy(w->dir_path, u->log_dir, MAX_PATH - 1); w->dir_path[MAX_PATH - 1] = '\0'; strncpy(w->pattern, u->log_dir_pattern, MAX_PATH - 1); w->pattern[MAX_PATH - 1] = '\0'; struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = ifd; if (epoll_ctl(g.epoll_fd, EPOLL_CTL_ADD, ifd, &ev) < 0) log_warn("log_tail: epoll_ctl ADD dir %s: %s", u->log_dir, strerror(errno)); char best[MAX_PATH]; if (log_tail_scan_dir(w->dir_path, w->pattern, best, sizeof(best)) == 0) log_tail_switch_file(w, best, 1); else log_info("log_tail: no matching file yet in %s for unit %s", u->log_dir, u->name); log_info("log_tail: watching dir %s (pattern %s) for unit %s", u->log_dir, u->log_dir_pattern, u->name); } /* Re-open a fixed-path watch after log rotation. */ static void log_tail_reopen_fixed(LogWatch *w) { assert(w != NULL); assert(w->dir_wd < 0); if (w->watch_wd >= 0) { inotify_rm_watch(w->inotify_fd, w->watch_wd); w->watch_wd = -1; } if (w->log_fd >= 0) { close(w->log_fd); w->log_fd = -1; } int log_fd = open(w->path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (log_fd < 0) { log_warn("log_tail: cannot re-open %s after rotation: %s", w->path, strerror(errno)); return; } int wd = inotify_add_watch(w->inotify_fd, w->path, IN_MODIFY | IN_MOVE_SELF | IN_DELETE_SELF); if (wd < 0) { log_warn("log_tail: inotify_add_watch after rotation %s: %s", w->path, strerror(errno)); close(log_fd); return; } w->log_fd = log_fd; w->watch_wd = wd; log_info("log_tail: re-opened %s after rotation", w->path); } /* Handle IN_MOVE_SELF / IN_DELETE_SELF on the current file in dir mode. */ static void log_tail_handle_rotation_dir(LogWatch *w) { assert(w != NULL); assert(w->dir_wd >= 0); if (w->watch_wd >= 0) { inotify_rm_watch(w->inotify_fd, w->watch_wd); w->watch_wd = -1; } if (w->log_fd >= 0) { close(w->log_fd); w->log_fd = -1; } w->path[0] = '\0'; char best[MAX_PATH]; if (log_tail_scan_dir(w->dir_path, w->pattern, best, sizeof(best)) == 0) log_tail_switch_file(w, best, 0); } void log_tail_init(void) { assert(g.unit_count >= 0); assert(g.unit_count <= MAX_UNITS); watch_count = 0; log_info("log_tail_init: checking %d unit(s) for log paths", g.unit_count); for (int i = 0; i < g.unit_count; i++) { Unit *u = &g.units[i]; if (u->log_dir[0]) { if (!u->log_dir_pattern[0]) strncpy(u->log_dir_pattern, "*", MAX_PATH - 1); log_tail_open_dir_watch(u); continue; } log_info("log_tail_init: unit '%s' has %d log path(s)", u->name, u->log_count); for (int j = 0; j < u->log_count; j++) if (u->log_paths[j][0]) log_tail_open_fixed_watch(u, u->log_paths[j]); } } Unit *log_tail_fd_to_unit(int fd) { assert(fd >= 0); assert(watch_count >= 0); for (int i = 0; i < watch_count; i++) { if (watches[i].inotify_fd == fd) return watches[i].unit; } return NULL; } static LogWatch *watch_for_inotify_fd(int fd) { assert(fd >= 0); assert(watch_count >= 0); for (int i = 0; i < watch_count; i++) { if (watches[i].inotify_fd == fd) return &watches[i]; } return NULL; } void log_tail_handle(int inotify_fd) { assert(inotify_fd >= 0); LogWatch *w = watch_for_inotify_fd(inotify_fd); if (!w) return; assert(w->unit != NULL); char evbuf[4096]; int rotated = 0; int new_file = 0; char new_fname[MAX_PATH] = {0}; ssize_t nread; while ((nread = read(inotify_fd, evbuf, sizeof(evbuf))) > 0) { char *p = evbuf; while (p < evbuf + nread) { struct inotify_event *ie = (struct inotify_event *)p; if (ie->mask & (IN_MOVE_SELF | IN_DELETE_SELF)) rotated = 1; if (w->dir_wd >= 0 && ie->wd == w->dir_wd && (ie->mask & (IN_CREATE | IN_MOVED_TO)) && ie->len > 0 && fnmatch(w->pattern, ie->name, 0) == 0) { new_file = 1; strncpy(new_fname, ie->name, MAX_PATH - 1); new_fname[MAX_PATH - 1] = '\0'; } p += sizeof(struct inotify_event) + ie->len; } } log_tail_drain(w); if (new_file) { /* Re-scan instead of using new_fname directly: handles the race * where multiple files appear in one event batch — scan finds * the definitively newest one. */ char best[MAX_PATH]; if (log_tail_scan_dir(w->dir_path, w->pattern, best, sizeof(best)) == 0) log_tail_switch_file(w, best, 0); return; } if (!rotated) return; if (w->dir_wd >= 0) log_tail_handle_rotation_dir(w); else log_tail_reopen_fixed(w); } void log_tail_cleanup(void) { assert(watch_count >= 0); assert(watch_count <= MAX_LOG_WATCHES); for (int i = 0; i < watch_count; i++) { epoll_ctl(g.epoll_fd, EPOLL_CTL_DEL, watches[i].inotify_fd, NULL); close(watches[i].inotify_fd); /* removes dir_wd and watch_wd */ if (watches[i].log_fd >= 0) close(watches[i].log_fd); } watch_count = 0; }