wolf/main.c

284 lines
10 KiB
C
Raw Normal View History

2024-07-30 16:48:07 +02:00
#ifndef __linux__
#error "Unsupported platform"
#endif
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <getopt.h>
#include <poll.h>
#include <time.h>
#include <sys/inotify.h>
#include <stdint.h>
#include <signal.h>
#define INOTIFY_EVENT_INC sizeof(struct inotify_event) + event->len
#define WATCHDOG_FORMAT "[%s] %c '%s' (%s)\n" // [<timestamp>] <event> '<path>' (<filetype>)
#define WATCHDOG_FORMAT_NOTS "%c '%s' (%s)\n" // <event> '<path>' (<filetype>)
#define FILE_FMT "%s/%s"
#define DIR_FMT "%s"
typedef enum {
E_CREATE = 'C',
E_DELETE = 'D',
E_MOVE = 'M',
E_READ = 'R',
E_WRITE = 'W',
E_PERM = 'P',
E_UNDEF = 0xB00B5
} watchdog_event;
typedef enum { false, true } bool;
static void handle_inotify_events(int fd, const int *wd, int wd_len, char **watched_files, const bool is_timestamp_enabled);
static void get_timestamp(uint8_t *timestamp, const ssize_t timestamp_len);
volatile sig_atomic_t stop_signal = 0;
void sigint_handler() {
stop_signal = ~stop_signal;
}
void helper(const char *name) {
printf("Wolf - Configurable file watchdog for Linux platform.\n\n"
"Syntax: '%s [-c|-d|-m|-r|-w|-p|-f] <PATH ...>'\n"
"options:\n"
"-c, --create | Add a watchdog for file creation\n"
"-d, --delete | Add a watchdog for file deletion\n"
"-m, --move | Add a watchdog for file movements or file renaming\n"
"-r, --read | Add a watchdog for reading events\n"
"-w, --write | Add a watchdog for writing events\n"
"-p, --permission | Add a watchdog for permissions changes\n"
"-f, --full | Enable all the previous options\n"
"--no-timestamp | Disable timestamp from watchdog output\n"
"-v, --version | Show program version\n"
"-h, --help | Show this helper\n\n"
"General help with the software: https://github.com/ceticamarco/wolf\n"
"Report bugs to: Marco Cetica(<email@marcocetica.com>)\n", name);
}
void version() {
printf("Wolf (v%s, %s) - Configurable file watchdog for Linux platform.\n"
"Copyright (c) 2024 Marco Cetica\n"
"License GPLv3+: GNU GPL version 3 or later\n\n"
"Project homepage: <https://git.marcocetica.com/marco/wolf>.\n"
"Email bug reports to: <email@marcocetica.com>.\n", VERSION, HASH);
}
int main(int argc, char **argv) {
int opt, opt_idx = 0;
const char *short_opts = "cdmrwpfvh";
uint32_t mask = 0;
bool is_timestamp_enabled = true;
struct option long_opts[] = {
{"create", no_argument, NULL, 'c'},
{"delete", no_argument, NULL, 'd'},
{"move", no_argument, NULL, 'm'},
{"read", no_argument, NULL, 'r'},
{"write", no_argument, NULL, 'w'},
{"permission", no_argument, NULL, 'p'},
{"full", no_argument, NULL, 'f'},
{"no-timestamp", no_argument, NULL, 0 },
{"version", no_argument, NULL, 'v'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
// Parse inotify options from command line
while((opt = getopt_long(argc, argv, short_opts, long_opts, &opt_idx)) != -1) {
switch(opt) {
case 'c': mask |= IN_CREATE; break;
case 'd': mask |= (IN_DELETE | IN_DELETE_SELF); break;
case 'm': mask |= (IN_MOVE_SELF | IN_MOVED_FROM); break;
case 'r': mask |= IN_ACCESS; break;
case 'w': mask |= IN_MODIFY; break;
case 'p': mask |= IN_ATTRIB; break;
case 'f': mask = IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM |
IN_ACCESS | IN_MODIFY | IN_ATTRIB; break;
case 0:
if(!strcmp(long_opts[opt_idx].name, "no-timestamp")) {
is_timestamp_enabled = false;
}
break;
case 'v': version(); return 0;
case 'h': helper(argv[0]); return 0;
default: helper(argv[0]); return 1;
}
}
// Retrieve number of non-option arguments
const int number_of_files = (argc - optind);
// Check whether user has provided enough
// non-option arguments(i.e., files to watch)
if(number_of_files == 0) {
puts("Error: provide at least one file/directory to watch");
helper(argv[0]);
return 1;
}
// Check whether user has provided enough watchdog options
if(mask == 0) {
puts("Error: provide at least one watchdog option");
helper(argv[0]);
return 1;
}
// Register SIGINT handler
signal(SIGINT, sigint_handler);
// Initialize inotify API
int fd = inotify_init1(IN_NONBLOCK);
if(fd == -1) {
perror("inotify_init1");
exit(EXIT_FAILURE);
}
// Allocate enough memory for each watch descriptor
int *wd = calloc(number_of_files, sizeof(int));
if(wd == NULL) {
perror("calloc");
exit(EXIT_FAILURE);
}
// Register each file into the watchlist
int file_idx = optind;
for(size_t idx = 0; file_idx < argc; file_idx++, idx++) {
wd[idx] = inotify_add_watch(fd, argv[file_idx], mask);
if(wd[idx] == -1) {
fprintf(stderr, "Cannot watch '%s': %s\n", argv[file_idx], strerror(errno));
exit(EXIT_FAILURE);
}
}
// Prepare polling
nfds_t nfds = 1;
struct pollfd fds[] = {
{ .fd = fd, .events = POLLIN }
};
while(!stop_signal) {
int poll_num = poll(fds, nfds, -1);
if(poll_num == -1) {
if(errno == EINTR) {
continue;
}
perror("poll");
exit(EXIT_FAILURE);
}
if(poll_num > 0) {
// Inotify events are available
if(fds->revents & POLLIN) {
handle_inotify_events(fd, wd, number_of_files, (argv + optind), is_timestamp_enabled);
}
}
}
// Free allocated resources
close(fd);
free(wd);
return 0;
}
static void handle_inotify_events(int fd, const int *wd, int wd_len, char **watched_files, const bool is_timestamp_enabled) {
// Align inotify reading buffer to inotify_event struct
char inotify_read_buf[4096]
__attribute__((aligned((__alignof__(struct inotify_event)))));
const struct inotify_event *event;
while(1) {
// Read events from inotify file descriptor
ssize_t len = read(fd, inotify_read_buf, sizeof(inotify_read_buf));
if(len == -1 && errno != EAGAIN) {
perror("read");
exit(EXIT_FAILURE);
}
// Exit whether read returns nothing
if(len <= 0) {
break;
}
// Handle each event of the buffer
for(char *ptr = inotify_read_buf; ptr < (inotify_read_buf + len); ptr += INOTIFY_EVENT_INC) {
// Retrieve single event
event = (const struct inotify_event*)ptr;
// Set the event type
watchdog_event we = E_UNDEF;
if(event->mask & IN_CREATE) {
we = E_CREATE;
} else if(event->mask & (IN_DELETE | IN_DELETE_SELF)) {
we = E_DELETE;
} else if(event->mask & IN_MOVED_FROM) {
we = E_MOVE;
} else if(event->mask & IN_ACCESS) {
we = E_READ;
} else if(event->mask & IN_MODIFY) {
we = E_WRITE;
} else if(event->mask & IN_ATTRIB) {
we = E_PERM;
} else if(event->mask & IN_IGNORED) {
// This event is generated each time a watched file/directory is
// deleted. The event is ignored since it adds no further information
// about the watched file.
continue;
}
// Print the watchdog event to the standard output
uint8_t *file_name = NULL;
size_t file_name_len;
for(int i = 0; i < wd_len; i++) {
if(wd[i] == event->wd) {
// Build filename
if(event->len) {
file_name_len = snprintf(NULL, 0, FILE_FMT, watched_files[i], event->name);
file_name = malloc((file_name_len+1) * sizeof(char));
if(file_name == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
snprintf((char*)file_name, file_name_len+1, FILE_FMT, watched_files[i], event->name);
} else {
file_name_len = snprintf(NULL, 0, DIR_FMT, watched_files[i]);
file_name = malloc((file_name_len+1) * sizeof(char));
if(file_name == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
snprintf((char*)file_name, file_name_len+1, DIR_FMT, watched_files[i]);
}
// Get file type
const char *file_type = (event->mask & IN_ISDIR ? "dir" : "file");
if(is_timestamp_enabled) {
// Get timestamp
uint8_t timestamp[20];
get_timestamp(timestamp, sizeof(timestamp));
// Print the result
printf(WATCHDOG_FORMAT, (char*)timestamp, we, file_name, file_type);
} else {
// Print the result without timestamp
printf(WATCHDOG_FORMAT_NOTS, we, file_name, file_type);
}
break;
}
}
free(file_name);
}
memset(inotify_read_buf, 0, sizeof(inotify_read_buf));
}
}
static void get_timestamp(uint8_t *timestamp, const ssize_t timestamp_len) {
time_t now = time(NULL);
struct tm *timeinfo = localtime(&now);
strftime((char*)timestamp, timestamp_len, "%Y-%m-%d %H:%M:%S", timeinfo);
}