From 77f6db18a5db11d1f663dc47228f410e712910fa Mon Sep 17 00:00:00 2001 From: Marco Cetica Date: Tue, 20 Aug 2024 16:09:42 +0200 Subject: [PATCH] Added command execution support --- Makefile | 2 +- README.md | 66 ++++++++++++++++++++++++++------ wolf.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 162 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 1eebf24..d82d706 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ DEBUG_CFLAGS = -Wall -Wextra -Werror -pedantic-errors -fstack-protector-strong \ -Wwrite-strings -std=c99 -g CFLAGS = -Wall -Wextra -Werror -pedantic-errors -Wwrite-strings -std=c99 -O3 -BUILD_FLAGS = -DVERSION=\"0.0.1\" +BUILD_FLAGS = -DVERSION=\"0.0.2\" build: $(TARGET) debug: $(DEBUG_TARGET) diff --git a/README.md b/README.md index 94c6acd..44329b0 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,11 @@ **Wolf** is a configurable file watchdog for Linux platform written in C. **Wolf** monitors a set of files or directories and prints out a log event each time the watched resources changes. The watchdog -can be configured to watch for any kind of event, that includes file creation and deletion, file moving, I/O and -permission changing. **Wolf** relies on the `inotify(7)` system call, therefore it is only compatible with Linux-based systems. +can be configured to monitor any kind of event, that includes file creation and deletion, file moving, I/O and +permission changes. Additionally, **Wolf** can execute an user-defined command every time a watchdog detects a +change, thus allowing you to easily build complex pipelines without the need to employ any additional tool. + +**Wolf** relies on the `inotify(7)` system call, therefore it is only compatible with Linux-based systems. ## Building The single source file(`wolf.c`) of the watchdog can be compiled using any C99 compiler. To build it, issue the following command: @@ -98,16 +101,57 @@ R '/home/marco/wolf' (dir) P '/home/marco/wolf/a.out' (file) W '/home/marco/wolf/a.out' (file) ``` +Additionally, if you want to execute a custom command every time a watchdog detects a change, you can do so by +using the `-e,--exec` option. For instance, suppose that you have a Python file(`foo.py`) with the following content: -## Caveats -**Wolf** relies on the Linux `inotify(7)` system call to implement the file tracking mechanism. Before using this -tool you should be aware of the following idiosyncrasies related to the way this system interface works: +```py +def square(x): + return x ** 2 -1. `inotify` is **NOT** recursive. Meaning that you cannot monitor subdirectories of a watched directory; -2. `inotify` can only work within files for which you already have reading and writing permissions; -3. `inotify` removes deleted files from the `inotify_add_watch(2)`, meaning that a watchdog is automatically -removed from a deleted file. To add it again, the program have to be restarted; -4. `inotify` is quite verbose by design. For instance if you try to write to a **non-empty** watched file +print(f"10^2 = {square(10)}") +``` + +and you want to continously evaluate it as soon as you save it to the disk. To do this, you can use **Wolf** as described +below: + +```sh +$> ./wolf -w --exec 'python foo.py' +``` + +Each time a write event is detected by the watchdog, the supplied command will be issued, causing the program +to be automatically evaluated, that is: + +```sh +$> ./wolf -w --exec 'python foo.py' +[2024-08-20 16:24:43] W 'foo.py' (file) +10^2 = 100 +[2024-08-20 16:24:55] W 'foo.py' (file) +10^2 = 100 +5^2 = 25 +[2024-08-20 16:25:10] W 'foo.py' (file) +10^2 = 100 +5^2 = 25 +4^2 = 16 +``` + +Be sure to read the _caveats_ section to learn more about the concurrent aspects of this feature and how **Wolf** +spawns a new process. + +## Technical details +Below there is a brief list of the things you should be aware of when using **Wolf**. + +- `inotify` is **NOT** recursive. Meaning that you cannot monitor subdirectories of a watched directory; +- `inotify` can only work within files for which you already have reading and writing permissions; +- The `-e,--exec` option works by spawning a child process using the `fork(2)` system call; thus, the command is being executed +in a new process; +- The `-e,--exec` option is a **NON-BLOCKING** feature, meaning that the parent process will continue to log new changes while the +child process execute the supplied command; therefore the parent process will **NOT** wait for the child(s) process to terminate; +- Since the parent process does not await for the child process to complete it will also not handle its return code, thus the exit +status of any supplied command is ignored. +- Any `SIGCHLD` signal generated by a child process is ignored, therefore the process reaping of any child is delegated to the kernel; +- `inotify` removes deleted files from the `inotify_add_watch(2)`, meaning that, after a file is being deleted, the watchdog associated with it +is automatically removed as well. To add it again, the program has to be restarted; +- `inotify` is quite verbose by design. For instance if you try to write to a **non-empty** watched file using the `echo(1)` command along with a _redirection_(i.e., `echo 'hello world' > foo`), the watchdog will log two events: @@ -136,7 +180,7 @@ exit_group(0) = ? +++ exited with 0 +++ ``` -Since `inotify(1)` intercepts both, **wolf** will also log twice the same operation. +Since `inotify(1)` intercepts both, **wolf** will also log the same operation twice. ## License [GPLv3](https://choosealicense.com/licenses/gpl-3.0/) diff --git a/wolf.c b/wolf.c index 637d7de..733f9dd 100644 --- a/wolf.c +++ b/wolf.c @@ -1,3 +1,5 @@ +#define _GNU_SOURCE + #ifndef __linux__ #error "Unsupported platform" #endif @@ -11,6 +13,8 @@ #include #include #include +#include +#include #include #include @@ -32,8 +36,10 @@ typedef enum { 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 handle_inotify_events(int fd, const int *wd, int wd_len, char **watched_files, const bool is_timestamp_enabled, const char *watchdog_cmd); static void get_timestamp(uint8_t *timestamp, const ssize_t timestamp_len); +static void exec_command(const char *cmd); +static uint8_t **tokenize_command(const char *cmd); volatile sig_atomic_t stop_signal = 0; void sigint_handler() { @@ -51,6 +57,7 @@ void helper(const char *name) { "-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" + "-e, --exec | Execute a command when a watchdog detects a change\n" "--no-timestamp | Disable timestamp from watchdog output\n" "-v, --version | Show program version\n" "-h, --help | Show this helper\n\n" @@ -68,7 +75,8 @@ void version() { int main(int argc, char **argv) { int opt, opt_idx = 0; - const char *short_opts = "cdmrwpfvh"; + const char *short_opts = "cdmrwpfvhe:"; + char *watchdog_cmd = NULL; uint32_t mask = 0; bool is_timestamp_enabled = true; struct option long_opts[] = { @@ -79,6 +87,7 @@ int main(int argc, char **argv) { {"write", no_argument, NULL, 'w'}, {"permission", no_argument, NULL, 'p'}, {"full", no_argument, NULL, 'f'}, + {"exec", required_argument, NULL, 'e'}, {"no-timestamp", no_argument, NULL, 0 }, {"version", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, @@ -96,6 +105,7 @@ int main(int argc, char **argv) { 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 'e': watchdog_cmd = optarg; break; case 0: if(!strcmp(long_opts[opt_idx].name, "no-timestamp")) { is_timestamp_enabled = false; @@ -171,7 +181,9 @@ int main(int argc, char **argv) { 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); + handle_inotify_events(fd, wd, number_of_files, (argv + optind), is_timestamp_enabled, watchdog_cmd); + + } } } @@ -183,7 +195,7 @@ int main(int argc, char **argv) { return 0; } -static void handle_inotify_events(int fd, const int *wd, int wd_len, char **watched_files, const bool is_timestamp_enabled) { +static void handle_inotify_events(int fd, const int *wd, int wd_len, char **watched_files, const bool is_timestamp_enabled, const char *watchdog_cmd) { // Align inotify reading buffer to inotify_event struct char inotify_read_buf[4096] __attribute__((aligned((__alignof__(struct inotify_event))))); @@ -270,6 +282,11 @@ static void handle_inotify_events(int fd, const int *wd, int wd_len, char **watc break; } } + // If user supplied a command, execute it + if(watchdog_cmd != NULL) { + exec_command(watchdog_cmd); + } + free(file_name); } memset(inotify_read_buf, 0, sizeof(inotify_read_buf)); @@ -281,3 +298,88 @@ static void get_timestamp(uint8_t *timestamp, const ssize_t timestamp_len) { struct tm *timeinfo = localtime(&now); strftime((char*)timestamp, timestamp_len, "%Y-%m-%d %H:%M:%S", timeinfo); } + +static void exec_command(const char *cmd) { + // Ignore SIGCHLD signals. + // This allows us to avoid blocking the parent process until the child completes + // its execution; furthermore, it allows us to prevent the creation of zombie processes + // by delegating the cleanup process to the kernel. By doing so, we lose the ability + // to check the return status of the child process. + signal(SIGCHLD, SIG_IGN); + + // Execute command in a new process + pid_t pid = fork(); + + if(pid == -1) { + perror("fork"); + exit(EXIT_FAILURE); + } else if(pid == 0) { // Child process + // Tokenize command + uint8_t **argv = tokenize_command(cmd); + + // Replace memory of child process with new program + execvp((char*)argv[0], (char**)argv); + + // If execvp returns, it means it has failed + switch(errno) { + case ENOENT: puts("Cannot execute command: no such file or directory"); break; + case EACCES: puts("Cannot execute command: permission denied"); break; + default: puts("Cannot execute command"); break; + } + + // Free allocated resources + for (int i = 0; argv[i] != NULL; i++) { + free(argv[i]); + } + free(argv); + exit(EXIT_FAILURE); + } +} + +static uint8_t **tokenize_command(const char *cmd) { + // Duplicate command + char *cmd_dup = strdup(cmd); + if(cmd_dup == NULL) { + perror("strdup"); + exit(EXIT_FAILURE); + } + + // Count number of arguments + size_t argc = 0; + char *token = strtok(cmd_dup, " "); + while(token != NULL) { + argc++; + token = strtok(NULL, " "); + } + + // Allocate enough memory for arguments(and null terminator) + uint8_t **argv = malloc((argc + 1) * sizeof(uint8_t*)); + if(argv == NULL) { + perror("malloc"); + exit(EXIT_FAILURE); + } + + // Reset command string and tokenize again + strcpy(cmd_dup, cmd); + size_t idx = 0; + token = strtok(cmd_dup, " "); + while(token != NULL) { + argv[idx] = (uint8_t*)strdup(token); + if(argv[idx] == NULL) { + perror("strdup"); + while(idx > 0) { free(argv[--idx]); } + free(argv); + free(cmd_dup); + exit(EXIT_FAILURE); + } + idx++; + token = strtok(NULL, " "); + } + + // Null-terminate the string + argv[idx] = NULL; + // Clear temporary resources + free(cmd_dup); + + return argv; +}