Added command execution support

This commit is contained in:
Marco Cetica 2024-08-20 16:09:42 +02:00
parent 4e25735cd9
commit 77f6db18a5
Signed by: marco
GPG Key ID: 45060A949E90D0FD
3 changed files with 162 additions and 16 deletions

View File

@ -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)

View File

@ -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/)

110
wolf.c
View File

@ -1,3 +1,5 @@
#define _GNU_SOURCE
#ifndef __linux__
#error "Unsupported platform"
#endif
@ -11,6 +13,8 @@
#include <poll.h>
#include <time.h>
#include <sys/inotify.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdint.h>
#include <signal.h>
@ -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;
}