/wolf

🐺 Configurable file watchdog for Linux platform

Primary LanguageCGNU General Public License v3.0GPL-3.0

Wolf 🐺

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 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:

$> make clean all

This command will produce a statically compiled binary called wolf.

Usage

Wolf - Configurable file watchdog for Linux platform.

Syntax: './wolf [-c|-d|-m|-r|-w|-p|-f|-e] <PATH ...>'
options:
-c, --create              | Add a watchdog for file creation
-d, --delete              | Add a watchdog for file deletion
-m, --move                | Add a watchdog for file movements or file renaming
-r, --read                | Add a watchdog for reading events
-w, --write               | Add a watchdog for writing events
-p, --permission          | Add a watchdog for permissions changes
-f, --full                | Enable all the previous options
-e, --exec                | Execute a command when a watchdog detects a change
--no-timestamp            | Disable timestamp from watchdog output
-v, --version             | Show program version
-h, --help                | Show this helper

General help with the software: https://git.marcocetica.com/marco/wolf
Report bugs to: Marco Cetica(<email@marcocetica.com>)

Wolf is pretty straightforward to use. It requires at least one watchdog option and at least one file/directory to watch as command line arguments. For example, to watch the local files foo, bar and the directory src/ for reading, writing and deletion events, issue the following command:

$> ./wolf -rwd foo bar src

The watchdog will start polling the resources for the specified events and will log on the standard output using the following pattern:

[<timestamp>] <event> '<path>' (<filetype>)

Where <timestamp> is the timestamp of the event, <event> is the event type, <path> is the filename of the watched resource and <filetype> is the type of the resource in the watchlist.

The <event> field is any of the following token: C, D, M, R, W, P.

For instance, if you try to read one of the files of the previous example(cat foo), wolf would produce the following output:

[2024-07-29 20:24:52] R 'foo' (file)

A write syscall to the src/test file would instead produce the following log:

[2024-07-29 20:26:20] W 'src/test' (file)

You can also choose to watch a directory by specifying its path:

$> ./wolf -rwd $PWD

This command will add a watchdog to the current directory for events of the type "read", "write" and "delete" generated for any file or directory on the current path. Do note that this command is NOT recursive(see the caveats section for more information).

Additionally, you can also tell wolf to add a watchdog to any kind of event by using the -f, --full option:

%> ./wolf --full $PWD

Which is equivalent to ./wolf -cdmrwp $PWD. Finally, you can also force wolf to disable the timestamp output by using the --no-timestamp option:

%> ./wolf -f --no-timestamp $PWD

This would produce the following output:

R '/home/marco/wolf' (dir)
R '/home/marco/wolf/foo' (file)
D '/home/marco/wolf/src' (dir)
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:

def square(x):
    return x ** 2

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:

$> ./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:

$> ./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 "technical details" 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.

  • 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 reaping of any child process is delegated to the kernel;
  • 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;
  • 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:
W '/home/marco/wolf/foo' (file)
W '/home/marco/wolf/foo' (file)

This is because the previous command makes two system calls: one to truncate(1) the file at zero length and the other to write(1) the content into the file. The former is issued by the shell itself while the latter is performed by the echo(1) command. You can detect these syscalls by using the strace(1) command:

$> strace sh -c 'echo "hello world" > foo'
execve("/usr/bin/sh", ["sh", "-c", "echo \"hello world\" > foo"], 0xffffd10381d0 /* 23 vars */) = 0
# Suppressed output
openat(AT_FDCWD, "foo", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 # <-- Truncate system call(first event)
fcntl(1, F_DUPFD, 10)                   = 10
close(1)                                = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup3(3, 1, 0)                           = 1
close(3)                                = 0
write(1, "hello world\n", 12)           = 12 # <-- Write system call(second event)
dup3(10, 1, 0)                          = 1
close(10)                               = 0
exit_group(0)                           = ?
+++ exited with 0 +++

Since inotify(1) intercepts both, wolf will also log the same operation twice.

License

GPLv3