Utilities: Introduce the watchfs utility

This utility is meant mainly for debugging purposes, to watch regular
files and directories being modified.
This commit is contained in:
Liav A. 2024-05-31 12:10:17 +03:00
parent 925bea444b
commit b38dbe9c3e
3 changed files with 226 additions and 1 deletions

View file

@ -0,0 +1,55 @@
## Name
watchfs - watch a file or directory being changed
## Synopsis
```**sh
$ watchfs [options...] [path...]
```
## Description
`watchfs` watches files and directories being changed.
## Options
* `--help`: Display this message
* `-E`, `--exit-after-change`: Wait for first change and exit
* `-a`, `--watch-all-events`: Watch all types of events
* `-d`, `--watch-delete-events`: Watch file deletion events
* `-m`, `--watch-file-modify-events`: Watch file content being modified
* `-M`, `--watch-file-metadata-events`: Watch file metadata being modified
* `-c`, `--watch-directory-child-creation-events`: Watch directory child creation events
* `-D`, `--watch-directory-child-deletion-events`: Watch directory child deletion events
## Arguments
* `path`: Files and/or directories to watch
## Examples
```sh
# watch /tmp with all events being handled (child creation and deletion)
$ watchfs -a /tmp/
# watch /tmp with child creation events being handled
$ watchfs -c /tmp/
# watch /tmp with child creation events being handled
$ watchfs -D /tmp/
# watch /tmp with all events being handled (child creation and deletion) and exit after first change
$ watchfs -E /tmp/
# watch /tmp/test_file with all events being handled (file being deleted, metadata being modified or content modified)
$ watchfs -a /tmp/test_file
# watch /tmp/test_file being deleted
$ watchfs -d /tmp/test_file
# watch /tmp/test_file being metadata-modified
$ watchfs -M /tmp/test_file
# watch /tmp/test_file being content-modified
$ watchfs -m /tmp/test_file
```
## See also
* [`listdir`(1)](help://man/1/listdir) to list directory entries
* [`ls`(1)](help://man/1/ls) to list directory contents

View file

@ -4,7 +4,7 @@ list(APPEND REQUIRED_TARGETS
arp base64 basename cat chmod chown clear comm cp cut date dd df diff dirname dmesg du echo env expr false
file find grep groups head host hostname id ifconfig kill killall ln logout ls mkdir mount mv network-settings nproc
patch pgrep pidof ping pkill pmap ps readlink realpath reboot rm rmdir sed route seq shutdown sleep sort stat stty su tail test
touch tr true umount uname uniq uptime w wc which whoami xargs yes
touch tr true umount uname uniq uptime w watchfs wc which whoami xargs yes
)
list(APPEND RECOMMENDED_TARGETS
aconv adjtime aplay abench asctl bt checksum chres cksum copy fortune gzip init install keymap lsirq lsof lspci lzcat man mkfs.fat mknod mktemp

View file

@ -0,0 +1,170 @@
/*
* Copyright (c) 2024, Liav A. <liavalb@hotmail.co.il>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Function.h>
#include <AK/StringView.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/DirIterator.h>
#include <LibCore/DirectoryEntry.h>
#include <LibCore/EventLoop.h>
#include <LibCore/FileWatcher.h>
#include <LibCore/System.h>
#include <LibMain/Main.h>
static ErrorOr<NonnullRefPtr<Core::FileWatcher>> watch_directory_child_creation(StringView path, bool exit_after_first_change)
{
auto watcher = TRY(Core::FileWatcher::create());
watcher->on_change = [path, exit_after_first_change](auto&) {
outln("{} has new file", path);
if (exit_after_first_change)
exit(1);
};
TRY(watcher->add_watch(path, Core::FileWatcherEvent::Type::ChildCreated));
return watcher;
}
static ErrorOr<NonnullRefPtr<Core::FileWatcher>> watch_directory_child_deletion(StringView path, bool exit_after_first_change)
{
auto watcher = TRY(Core::FileWatcher::create());
watcher->on_change = [path, exit_after_first_change](auto&) {
outln("{} has file being deleted", path);
if (exit_after_first_change)
exit(1);
};
TRY(watcher->add_watch(path, Core::FileWatcherEvent::Type::ChildDeleted));
return watcher;
}
static ErrorOr<NonnullRefPtr<Core::FileWatcher>> watch_file_content_modified(StringView path, bool exit_after_first_change)
{
auto watcher = TRY(Core::FileWatcher::create());
watcher->on_change = [path, exit_after_first_change](auto&) {
outln("{} content is modified", path);
if (exit_after_first_change)
exit(1);
};
TRY(watcher->add_watch(path, Core::FileWatcherEvent::Type::ContentModified));
return watcher;
}
static ErrorOr<NonnullRefPtr<Core::FileWatcher>> watch_file_metadata_modified(StringView path, bool exit_after_first_change)
{
auto watcher = TRY(Core::FileWatcher::create());
watcher->on_change = [path, exit_after_first_change](auto&) {
outln("{} metadata is modified", path);
if (exit_after_first_change)
exit(1);
};
TRY(watcher->add_watch(path, Core::FileWatcherEvent::Type::MetadataModified));
return watcher;
}
static ErrorOr<NonnullRefPtr<Core::FileWatcher>> watch_file_being_deleted(StringView path, bool exit_after_first_change)
{
auto watcher = TRY(Core::FileWatcher::create());
watcher->on_change = [path, exit_after_first_change](auto&) {
outln("{} is deleted", path);
if (exit_after_first_change)
exit(1);
};
TRY(watcher->add_watch(path, Core::FileWatcherEvent::Type::Deleted));
return watcher;
}
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
TRY(Core::System::pledge("stdio rpath"));
Vector<StringView> paths;
bool flag_exit_after_first_change = false;
bool flag_watch_all_events = false;
bool flag_watch_file_being_deleted = false;
bool flag_watch_file_being_content_modified = false;
bool flag_watch_file_being_metadata_modified = false;
bool flag_watch_directory_child_creation = false;
bool flag_watch_directory_child_deletion = false;
Core::ArgsParser args_parser;
args_parser.set_general_help("Watch for filesystem activity in a directory.");
args_parser.add_option(flag_exit_after_first_change, "Wait for first change and exit", "exit-after-change", 'E');
args_parser.add_option(flag_watch_all_events, "Watch all types of events", "watch-all-events", 'a');
args_parser.add_option(flag_watch_file_being_deleted, "Watch file deletion events", "watch-delete-events", 'd');
args_parser.add_option(flag_watch_file_being_content_modified, "Watch file content being modified", "watch-file-modify-events", 'm');
args_parser.add_option(flag_watch_file_being_metadata_modified, "Watch file metadata being modified", "watch-file-metadata-events", 'M');
args_parser.add_option(flag_watch_directory_child_creation, "Watch directory child creation events", "watch-directory-child-creation-events", 'c');
args_parser.add_option(flag_watch_directory_child_deletion, "Watch directory child deletion events", "watch-directory-child-deletion-events", 'D');
args_parser.add_positional_argument(paths, "Path to watch", "path", Core::ArgsParser::Required::No);
args_parser.parse(arguments);
if (flag_watch_all_events) {
flag_watch_file_being_deleted = true;
flag_watch_file_being_content_modified = true;
flag_watch_file_being_metadata_modified = true;
flag_watch_directory_child_creation = true;
flag_watch_directory_child_deletion = true;
}
if (paths.is_empty())
paths.append("."sv);
Vector<NonnullRefPtr<Core::FileWatcher>> watchers;
Core::EventLoop event_loop;
for (auto& path : paths) {
auto st = TRY(Core::System::stat(path));
auto directory_entry_type = Core::DirectoryEntry::directory_entry_type_from_stat(st.st_mode);
switch (directory_entry_type) {
case Core::DirectoryEntry::Type::Directory: {
if (flag_watch_directory_child_creation) {
auto watcher = TRY(watch_directory_child_creation(path, flag_exit_after_first_change));
TRY(watchers.try_append(watcher));
}
if (flag_watch_directory_child_deletion) {
auto watcher = TRY(watch_directory_child_deletion(path, flag_exit_after_first_change));
TRY(watchers.try_append(watcher));
}
break;
}
case Core::DirectoryEntry::Type::File: {
if (flag_watch_file_being_content_modified) {
auto watcher = TRY(watch_file_content_modified(path, flag_exit_after_first_change));
TRY(watchers.try_append(watcher));
}
if (flag_watch_file_being_metadata_modified) {
auto watcher = TRY(watch_file_metadata_modified(path, flag_exit_after_first_change));
TRY(watchers.try_append(watcher));
}
if (flag_watch_file_being_deleted) {
auto watcher = TRY(watch_file_being_deleted(path, flag_exit_after_first_change));
TRY(watchers.try_append(watcher));
}
break;
}
default:
warnln("Trying to watch unsupported file type");
break;
}
}
if (watchers.is_empty())
return Error::from_string_literal("Watchers list is empty");
event_loop.exec();
return 0;
}