#include "log_tail.h" #include "client.h" #include "unit.h" #include "log.h" #include #include #include #include #include #include #include #include #define MAX_LOG_WATCHES (MAX_UNITS * 4) typedef struct { int inotify_fd; int watch_wd; int log_fd; Unit *unit; char path[MAX_PATH]; } LogWatch; static LogWatch watches[MAX_LOG_WATCHES]; static int watch_count = 0; void log_tail_init(void) { 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]; 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++) { const char *path = u->log_paths[j]; if (!path[0]) continue; /* Resolve symlinks so inotify watches the real file, not the link */ 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)); continue; } 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); continue; } 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); continue; } if (watch_count >= MAX_LOG_WATCHES) { log_warn("log_tail: max watches reached, skipping %s", open_path); close(ifd); close(log_fd); continue; } watches[watch_count].inotify_fd = ifd; watches[watch_count].watch_wd = wd; watches[watch_count].log_fd = log_fd; watches[watch_count].unit = u; strncpy(watches[watch_count].path, open_path, MAX_PATH - 1); watch_count++; struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = ifd; epoll_ctl(g.epoll_fd, EPOLL_CTL_ADD, ifd, &ev); log_info("log_tail: watching %s for unit %s", open_path, u->name); } } } Unit *log_tail_fd_to_unit(int fd) { 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) { 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) { LogWatch *w = watch_for_inotify_fd(inotify_fd); if (!w) return; /* Parse inotify events — detect log rotation (file moved or deleted) */ char evbuf[4096]; int rotated = 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; p += sizeof(struct inotify_event) + ie->len; } } /* Drain any new data from the current log fd before re-opening */ char buf[RING_BUF_LINE_MAX]; ssize_t n; while ((n = read(w->log_fd, buf, sizeof(buf) - 1)) > 0) { buf[n] = '\0'; ring_push(w->unit->output, buf, (int)n); client_broadcast_output(w->unit->name, buf, 0); } if (!rotated) return; /* Log file was rotated — re-open the path to follow the new file */ inotify_rm_watch(w->inotify_fd, w->watch_wd); 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); } void log_tail_cleanup(void) { for (int i = 0; i < watch_count; i++) { epoll_ctl(g.epoll_fd, EPOLL_CTL_DEL, watches[i].inotify_fd, NULL); inotify_rm_watch(watches[i].inotify_fd, watches[i].watch_wd); close(watches[i].inotify_fd); close(watches[i].log_fd); } watch_count = 0; }