diff --git a/repos/base-linux/run/lx_fs_notify.run b/repos/base-linux/run/lx_fs_notify.run new file mode 100644 index 0000000000..1944d26856 --- /dev/null +++ b/repos/base-linux/run/lx_fs_notify.run @@ -0,0 +1,436 @@ +# +# \brief Test for using the lx_fs_notify plugin with the Linux file system +# \author Pirmin Duss +# \date 2019-12-05 +# + +assert_spec linux + +# +# Build +# + +create_boot_directory + +import_from_depot [depot_user]/src/[base_src] +import_from_depot [depot_user]/src/chroot +import_from_depot [depot_user]/src/fs_rom +import_from_depot [depot_user]/src/init +import_from_depot [depot_user]/src/libc +import_from_depot [depot_user]/src/stdcxx +import_from_depot [depot_user]/src/posix +import_from_depot [depot_user]/src/vfs + +build { + server/lx_fs + test/lx_fs_notify/rom_log + test/lx_fs_notify/file_writer +} + +# +# init config +# +install_config { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +# +# configurations for the sub init +# +set init_run_fwrite_test { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +set init_run_write_test { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +set test_iterations 10 +set input_file_name "bin/lx_fs_notify/templates/infile.txt" +set output_file_name "bin/lx_fs_notify/test/outfile.txt" + +# +# print text in colors +# +proc color {foreground text} { + # tput is a little Unix utility that lets you use the termcap database + # *much* more easily... + return [exec tput setaf $foreground]$text[exec tput sgr0] +} + +# +# write the desired config for the sub init +# +proc write_init_config { config } { + set fd [open "bin/lx_fs_notify/init.config" "w"] + puts $fd $config + close $fd +} + +# +# clear the content of the init config +# +proc write_empty_init_config { } { + set fd [open "bin/lx_fs_notify/init.config" "w"] + puts $fd "" + close $fd +} + +# +# wait for an update to the test file and check +# +proc wait_file_changed { file_size additional_filter } { + global spawn_id + + puts [color 3 "wait for file change with size=$file_size"] + set timeout 5 + expect { + -i $spawn_id -re ".*init -> test-rom_log.*updated ROM content: size=$file_size.*\n" { } + timeout { + puts [color 1 "ERROR no file change or wrong file size reported. expected size was $file_size"] + exit -4 + } + } + + if { $additional_filter != {} } { + set wait_re ".*$additional_filter\n" + set timeout 3 + expect { + -i $spawn_id -re $wait_re { } + timeout { } + } + } +} + +# +# wait for a defined time +# +proc wait_and_consume_log { delay_sec } { + global spawn_id + + set timeout $delay_sec + expect { + -i $spawn_id -re { text that never is printed during the test } { } + timeout { } + } + after 10 +} + +# +# create the input file for a test +# +proc create_test_file { input_file_name } { + exec seq -w [expr int(rand()*4000)+1] > $input_file_name + set file_size [exec stat -c%s $input_file_name] + return $file_size +} + +# +# create output file +# +proc create_output_file { output_file_name } { + exec seq -w [expr int(rand()*400)+1] > $output_file_name + set file_size [exec stat -c%s $output_file_name] + return $file_size +} + +# +# compute the size of the ROM from the file size +# +proc rom_size { file_size } { + if { [expr $file_size % 4096] == 0 } { + return $file_size + } else { + return [expr $file_size + (4096 - ($file_size % 4096))] + } +} + +# +# Create test-directory structure +# +exec rm -Rf bin/lx_fs_notify +exec mkdir -p bin/lx_fs_notify/templates +exec mkdir -p bin/lx_fs_notify/test +exec mkdir -p bin/lx_fs_notify/mnt + +# +# Boot modules +# +build_boot_image { lx_fs test-rom_log test-file_writer lx_fs_notify } + +# +# build the test program for Linux +# this wil bel located in /tmp/bin +# +exec make -C [genode_dir]/repos/os/src/test/lx_fs_notify/file_writer/ + +# +# Test cases +# +proc test_libc_fwrite_in_genode { test_iterations init_run_fwrite_test input_file_name output_file_name } { + puts [color 2 ">>> run libc fwrite test in Genode ($test_iterations iterations)"] + set size [create_output_file $output_file_name] + for { set it 0 } { $it < $test_iterations } { incr it } { + set size [create_test_file $input_file_name] + wait_and_consume_log 1 + write_init_config $init_run_fwrite_test + wait_file_changed $size {child "test-file_writer" exited with exit value 0} + write_empty_init_config + } +} + +proc test_libc_write_in_genode { test_iterations init_run_write_test input_file_name output_file_name } { + puts [color 2 ">>> run libc write test in Genode ($test_iterations iterations)"] + set size [create_output_file $output_file_name] + for { set it 0 } { $it < $test_iterations } { incr it } { + set size [expr max([create_test_file $input_file_name], $size)] + wait_and_consume_log 1 + write_init_config $init_run_write_test + wait_file_changed $size {child "test-file_writer" exited with exit value 0} + write_empty_init_config + } +} + +proc test_libc_fwrite_on_linux { test_iterations input_file_name output_file_name } { + puts [color 2 ">>> run libc fwrite test on Linux ($test_iterations iterations)"] + write_empty_init_config + create_output_file $output_file_name + for { set it 0 } { $it < $test_iterations } { incr it } { + set size [create_test_file $input_file_name] + wait_and_consume_log 1 + exec /tmp/bin/file_writer --fwrite [run_dir]/genode/lx_fs_notify/templates/infile.txt [run_dir]/genode/lx_fs_notify/test/outfile.txt + wait_file_changed $size {} + } +} + +proc test_libc_write_on_linux { test_iterations input_file_name output_file_name } { + puts [color 2 ">>> run libc write test on Linux ($test_iterations iterations)"] + write_empty_init_config + set size [create_output_file $output_file_name] + for { set it 0 } { $it < $test_iterations } { incr it } { + set size [expr max([create_test_file $input_file_name], $size)] + wait_and_consume_log 1 + exec /tmp/bin/file_writer --write [run_dir]/genode/lx_fs_notify/templates/infile.txt [run_dir]/genode/lx_fs_notify/test/outfile.txt + wait_file_changed $size {} + } +} + +proc test_tcl_file_copy { test_iterations input_file_name output_file_name} { + puts [color 2 ">>> run TCL 'file copy' test ($test_iterations iterations)"] + for { set it 0 } { $it < $test_iterations } { incr it } { + set size [create_test_file $input_file_name] + file copy -force $input_file_name $output_file_name + wait_file_changed $size {} + } +} + +proc test_shell_cp { test_iterations input_file_name output_file_name } { + puts [color 2 ">>> run shell 'cp' test ($test_iterations iterations)"] + for { set it 0 } { $it < $test_iterations } { incr it } { + set size [create_test_file $input_file_name] + exec cp -f $input_file_name $output_file_name + wait_file_changed $size {} + } +} + +proc test_shell_mv_overwrite { test_iterations input_file_name output_file_name } { + create_output_file $output_file_name + puts [color 2 ">>> run shell 'mv' overwrite test ($test_iterations iterations)"] + for { set it 0 } { $it < $test_iterations } { incr it } { + set size [create_test_file $input_file_name] + exec mv $input_file_name $output_file_name + wait_file_changed $size {} + } +} + +proc test_shell_mv_rename { test_iterations } { + global output_file_name + puts [color 2 ">>> run 'mv' rename watched file test ($test_iterations iterations)"] + for { set it 0 } { $it < $test_iterations } { incr it } { + puts "create watched file" + set size [create_output_file $output_file_name] + wait_file_changed $size {} + puts "move watched file away" + exec mv $output_file_name $output_file_name.out + wait_file_changed 0 {} + } +} + +proc test_shell_mv_move_other_dir { test_iterations } { + global input_file_name + global output_file_name + puts [color 2 ">>> run 'mv' move watched file to other directory test ($test_iterations iterations)"] + for { set it 0 } { $it < $test_iterations } { incr it } { + puts "create watched file" + set size [create_output_file $output_file_name] + wait_file_changed $size {} + puts "move watched file away" + exec mv $output_file_name $input_file_name + wait_file_changed 0 {} + } +} + +proc test_shell_rm { test_iterations output_file_name } { + puts [color 2 ">>> run 'rm' remove watched file test ($test_iterations iterations)"] + for { set it 0 } { $it < $test_iterations } { incr it } { + puts "create watched file" + set size [create_output_file $output_file_name] + wait_file_changed $size {} + puts "remove watched file" + exec rm -f $output_file_name + wait_file_changed 0 {} + } +} + +# +# Execute test cases +# +# wait until the test program has started +run_genode_until ".*wait for ROM update.*\n" 6 +set spawn_id [output_spawn_id] +wait_and_consume_log 3 + +test_libc_fwrite_in_genode $test_iterations $init_run_fwrite_test $input_file_name $output_file_name +test_libc_write_in_genode $test_iterations $init_run_write_test $input_file_name $output_file_name +test_libc_fwrite_on_linux $test_iterations $input_file_name $output_file_name +test_libc_write_on_linux $test_iterations $input_file_name $output_file_name +test_tcl_file_copy $test_iterations $input_file_name $output_file_name +test_shell_cp $test_iterations $input_file_name $output_file_name +test_shell_mv_overwrite $test_iterations $input_file_name $output_file_name +test_shell_mv_rename $test_iterations +test_shell_mv_move_other_dir $test_iterations +test_shell_rm $test_iterations $output_file_name + +# +# Cleanup test-directory structure +# +exec rm -Rf bin/lx_fs_notify + +# vi: set ft=tcl : diff --git a/repos/os/src/server/lx_fs/README b/repos/os/src/server/lx_fs/README index 7cce6be14a..b90cc200cc 100644 --- a/repos/os/src/server/lx_fs/README +++ b/repos/os/src/server/lx_fs/README @@ -32,13 +32,12 @@ optional 'writeable' attribute grants the permission to modify the file system. Example ~~~~~~~ -To illustrate the use of lx_fs, refer to the 'base-linux/run/lx_fs.run' -script. +To illustrate the use of lx_fs, refer to the 'base-linux/run/lx_fs.run' or +'base-linux/run/lx_fs_notify.run' scripts. Notes ~~~~~ -If the Linux file system experiences changes from other processes -'inotify' may help to keep the servers cache up-to-date. This is not -implemented yet. +'inotify' is used to track changes to the file system that are caused +by non Genode components. diff --git a/repos/os/src/server/lx_fs/fd_set.h b/repos/os/src/server/lx_fs/fd_set.h new file mode 100644 index 0000000000..18f86a699e --- /dev/null +++ b/repos/os/src/server/lx_fs/fd_set.h @@ -0,0 +1,49 @@ +/* + * \brief Fd_set utlity + * \author Stefan Thöni + * \author Pirmin Duss + * \date 2021-04-08 + */ + +/* + * Copyright (C) 2021 Genode Labs GmbH + * Copyright (C) 2021 gapfruit AG + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _FD_SET_H_ +#define _FD_SET_H_ + +/* libc includes */ +#include + + +namespace Lx_fs { + class Fd_set; +} + + +class Lx_fs::Fd_set +{ + private: + + fd_set _fdset { }; + int _nfds { }; + + public: + + Fd_set(int fd0) + { + FD_ZERO(&_fdset); + FD_SET(fd0, &_fdset); + _nfds = fd0 + 1; + } + + fd_set* fdset() { return &_fdset; } + int nfds() const { return _nfds; } + bool is_set(int fd) const { return FD_ISSET(fd, &_fdset); } +}; + +#endif /* _FD_SET_H_ */ diff --git a/repos/os/src/server/lx_fs/lx_util.cc b/repos/os/src/server/lx_fs/lx_util.cc new file mode 100644 index 0000000000..bec2e8b2dc --- /dev/null +++ b/repos/os/src/server/lx_fs/lx_util.cc @@ -0,0 +1,43 @@ +/* + * \brief Linux utilities + * \author Christian Helmuth + * \author Pirmin Duss + * \date 2013-11-11 + */ + +/* + * Copyright (C) 2013-2020 Genode Labs GmbH + * Copyright (C) 2020 gapfruit AG + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* local includes */ +#include "lx_util.h" + + +int File_system::access_mode(File_system::Mode const &mode) +{ + switch (mode) { + case STAT_ONLY: + case READ_ONLY: return O_RDONLY; + case WRITE_ONLY: return O_WRONLY; + case READ_WRITE: return O_RDWR; + } + + return O_RDONLY; +} + + +Lx_fs::Path_string Lx_fs::absolute_root_dir(char const *root_path) +{ + char cwd[PATH_MAX]; + char real_path[PATH_MAX]; + + getcwd(cwd, PATH_MAX); + + realpath(Path_string { cwd, "/", root_path }.string(), real_path); + + return Path_string { real_path }; +} diff --git a/repos/os/src/server/lx_fs/lx_util.h b/repos/os/src/server/lx_fs/lx_util.h index 8c63c218c5..174fa64307 100644 --- a/repos/os/src/server/lx_fs/lx_util.h +++ b/repos/os/src/server/lx_fs/lx_util.h @@ -49,34 +49,7 @@ namespace Lx_fs { * session. * */ - Path_string absolute_root_directory(char const *root_path); + Path_string absolute_root_dir(char const *root_path); } - -int File_system::access_mode(File_system::Mode const &mode) -{ - switch (mode) { - case STAT_ONLY: - case READ_ONLY: return O_RDONLY; - case WRITE_ONLY: return O_WRONLY; - case READ_WRITE: return O_RDWR; - } - - return O_RDONLY; -} - - -Lx_fs::Path_string Lx_fs::absolute_root_directory(char const *root_path) -{ - char cwd[PATH_MAX]; - char real_path[PATH_MAX]; - - getcwd(cwd, PATH_MAX); - - realpath(Path_string { cwd, "/", root_path }.string(), real_path); - - return Path_string { real_path }; -} - - #endif /* _LX_UTIL_H_ */ diff --git a/repos/os/src/server/lx_fs/main.cc b/repos/os/src/server/lx_fs/main.cc index 320e036273..c0a656eeb1 100644 --- a/repos/os/src/server/lx_fs/main.cc +++ b/repos/os/src/server/lx_fs/main.cc @@ -28,8 +28,13 @@ /* local includes */ #include "directory.h" -#include "node.h" +#include "notifier.h" #include "open_node.h" +#include "watch.h" + +/* libc includes */ +#include +#include namespace Lx_fs { @@ -85,7 +90,8 @@ class Lx_fs::Session_resources class Lx_fs::Session_component : private Session_resources, - public Session_rpc_object + public Session_rpc_object, + private Watch_node::Response_handler { private: @@ -97,9 +103,8 @@ class Lx_fs::Session_component : private Session_resources, Id_space _open_node_registry { }; bool _writable; Absolute_path const _root_dir; - Signal_handler _process_packet_dispatcher; - + Notifier &_notifier; /****************************** ** Packet-stream processing ** @@ -238,6 +243,16 @@ class Lx_fs::Session_component : private Session_resources, } } + /** + * Watch_node::Response_handler interface + */ + void handle_watch_node_response(Lx_fs::Watch_node &node) override + { + using Fs_node = File_system::Open_node; + _process_packet_op(node.acked_packet(), + *(reinterpret_cast(node.open_node()))); + } + public: /** @@ -248,7 +263,8 @@ class Lx_fs::Session_component : private Session_resources, Genode::Cap_quota cap_quota, size_t tx_buf_size, char const *root_dir, - bool writable) + bool writable, + Notifier ¬ifier) : Session_resources { env.pd(), env.rm(), ram_quota, cap_quota, tx_buf_size }, Session_rpc_object {_packet_ds.cap(), env.rm(), env.ep().rpc_ep() }, @@ -256,7 +272,8 @@ class Lx_fs::Session_component : private Session_resources, _root { *new (&_alloc) Directory { _alloc, root_dir, false } }, _writable { writable }, _root_dir { root_dir }, - _process_packet_dispatcher { env.ep(), *this, &Session_component::_process_packets } + _process_packet_dispatcher { env.ep(), *this, &Session_component::_process_packets }, + _notifier { notifier } { /* * Register '_process_packets' dispatch function as signal @@ -383,6 +400,24 @@ class Lx_fs::Session_component : private Session_resources, return open_node->id(); } + Watch_handle watch(Path const &path) override + { + _assert_valid_path(path.string()); + + /* re-root the path */ + Path_string watch_path { _root.path().string(), path.string() }; + + Watch_node *watch = + new (_alloc) Watch_node { _env, watch_path.string(), *this, _notifier }; + Lx_fs::Open_node *open_watch = + new (_alloc) Lx_fs::Open_node(*watch, _open_node_registry); + + Watch_handle handle { open_watch->id().value }; + watch->open_node(open_watch); + + return handle; + } + void close(Node_handle handle) override { auto close_fn = [&] (Open_node &open_node) { @@ -520,6 +555,7 @@ class Lx_fs::Root : public Root_component Genode::Env &_env; Genode::Attached_rom_dataspace _config { _env, "config" }; + Notifier _notifier { _env }; static inline bool writeable_from_args(char const *args) { @@ -603,8 +639,8 @@ class Lx_fs::Root : public Root_component Genode::Ram_quota { ram_quota }, Genode::Cap_quota { cap_quota }, tx_buf_size, - absolute_root_directory(root_dir).string(), - writeable }; + absolute_root_dir(root_dir).string(), + writeable, _notifier }; auto ram_used { _env.pd().used_ram().value - initial_ram_usage }; auto cap_used { _env.pd().used_caps().value - initial_cap_usage }; diff --git a/repos/os/src/server/lx_fs/notifier.cc b/repos/os/src/server/lx_fs/notifier.cc new file mode 100644 index 0000000000..863e4b8f3b --- /dev/null +++ b/repos/os/src/server/lx_fs/notifier.cc @@ -0,0 +1,427 @@ +/* + * \brief inotify handling for underlying file system. + * \author Pirmin Duss + * \date 2020-06-17 + */ + +/* + * Copyright (C) 2013-2021 Genode Labs GmbH + * Copyright (C) 2020-2021 gapfruit AG + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode includes */ +#include "base/exception.h" +#include +#include "base/signal.h" +#include "util/string.h" + +/* libc includes */ +#include +#include +#include +#include /* strerror */ +#include +#include +#include +#include +#include + +/* local includes */ +#include "fd_set.h" +#include "notifier.h" +#include "watch.h" + + +namespace Lx_fs +{ + enum { + STACK_SIZE = 8 * 1024, + EVENT_SIZE = (sizeof (struct inotify_event)), + EVENT_BUF_LEN = (1024 * (EVENT_SIZE + NAME_MAX + 1)), + PARALLEL_NOTIFICATIONS = 4, + INOTIFY_WATCH_MASK = IN_CLOSE_WRITE | /* writtable file was closed */ + IN_MOVED_TO | /* file was moved to Y */ + IN_MOVED_FROM | /* file was moved from X */ + IN_CREATE | /* subfile was created */ + IN_DELETE | /* subfile was deleted */ + IN_IGNORED, /* file was ignored */ + }; + + struct Libc_signal_thread; +} + +#include +#include +#include + +/* do not leak internal function in to global namespace */ +namespace +{ + using namespace Lx_fs; + + ::uint64_t timestamp_us() + { + struct timeval ts { 0, 0 }; + gettimeofday(&ts, nullptr); + + return (ts.tv_sec * 1'000'000) + ts.tv_usec; + } + + + template + T *remove_from_list(Genode::List &list, T *node, Allocator &alloc) + { + T *next { node->next() }; + list.remove(node); + destroy(alloc, node); + return next; + } + + + bool is_dir(char const *path) + { + struct stat s; + int ret = lstat(path, &s); + if (ret == -1) + return false; + + if (S_ISDIR(s.st_mode)) + return true; + + return false; + } + + + Path_string get_directory(Path_string const &path) + { + Path_string directory; + if (is_dir(path.string())) { + // make sure there is a '/' at the end of the path + if (path.string()[path.length() - 2] == '/') + directory = path; + else + directory = Path_string { path, '/' }; + } else { + size_t pos = 0; + for (size_t i = 0; i < path.length() - 1; ++i) { + if (path.string()[i] == '/') + pos = i; + } + directory = Genode::Cstring { path.string(), pos + 1 }; + } + + return directory; + } + + + Path_string get_filename(Path_string const &path) + { + Path_string filename; + if (is_dir(path.string())) + return { }; + + size_t pos = 0; + for (size_t i = 0; i < path.length() - 1; ++i) { + if (path.string()[i] == '/') + pos = i; + } + + /* if '/' is the last symbol we do not need a filename */ + if (pos != path.length() - 2) + filename = Genode::Cstring { path.string() + pos + 1 }; + + return filename; + } + +} /* anonymous namespace */ + + +Lx_fs::Os_path::Os_path(const char *fullname) +: + full_path { fullname }, + directory { get_directory(full_path) }, + filename { get_filename(full_path) } +{ } + + +bool Lx_fs::Notifier::_watched(char const *path) const +{ + for (Watches_list_element const *e = _watched_nodes.first(); e != nullptr; e = e->next()) { + if (e->path.full_path == path) + return true; + } + + return false; +} + + +void Lx_fs::Notifier::_add_to_watched(char const *fullname) +{ + Os_path path { fullname }; + for (Watches_list_element *e = _watched_nodes.first(); e != nullptr; e = e->next()) { + if (e->path.directory == path.directory) { + Watches_list_element *elem { new (_heap) Watches_list_element { e->watch_fd, path } }; + _watched_nodes.insert(elem); + return; + } + } + + auto watch_fd { inotify_add_watch(_fd, path.directory.string(), INOTIFY_WATCH_MASK) }; + + if (watch_fd > 0) { + Watches_list_element *elem { new (_heap) Watches_list_element { watch_fd, path } }; + _watched_nodes.insert(elem); + } +} + + +int Lx_fs::Notifier::_add_node(char const *path, Watch_node &node) +{ + for (Watches_list_element *e = _watched_nodes.first(); e != nullptr; e = e->next()) { + if (e->path.full_path == path) { + Single_watch_list_element *c { new (_heap) Single_watch_list_element { node } }; + e->add_node(c); + return e->watch_fd; + } + }; + + throw File_system::Lookup_failed { }; +} + + +void Lx_fs::Notifier::_add_notify(Watch_node &node) +{ + Mutex::Guard guard { _notify_queue_mutex }; + for (auto const *e = _notify_queue.first(); e != nullptr; e = e->next()) { + if (&e->node == &node) { + return; + } + } + + auto *entry { new (_heap) Single_watch_list_element { node } }; + _notify_queue.insert(entry); +} + + +void Lx_fs::Notifier::_process_notify() +{ + Mutex::Guard guard { _notify_queue_mutex }; + + /** + * limit amount of watch events sent at the same time, + * to prevent an overflow of the packet ack queue of the + * File_system session. + */ + int cnt { 0 }; + Single_watch_list_element *e { _notify_queue.first() }; + for (; e != nullptr && cnt < PARALLEL_NOTIFICATIONS; ++cnt) { + e->node.notify_handler().local_submit(); + e = remove_from_list(_notify_queue, e, _heap); + } +} + + +Lx_fs::Notifier::Notifier(Env &env) +: + Thread { env, "inotify", STACK_SIZE }, + _env { env } +{ + _fd = inotify_init(); + + if (0 > _fd) + throw Init_notify_failed { }; + + start(); +} + + +Lx_fs::Notifier::~Notifier() +{ + /* do not notify the elements */ + for (auto *e = _notify_queue.first(); e != nullptr; ) { + e = remove_from_list(_notify_queue, e, _heap); + } + + for (auto *e = _watched_nodes.first(); e != nullptr; ) { + e = remove_from_list(_watched_nodes, e, _heap); + } + + close(_fd); +} + + +Lx_fs::Notifier::Watches_list_element *Lx_fs::Notifier::_remove_node(Watches_list_element *node) +{ + int watch_fd { node->watch_fd }; + Watches_list_element *next { remove_from_list(_watched_nodes, node, _heap) }; + bool nodes_left { false }; + + Watches_list_element const *e { _watched_nodes.first() }; + for (; e != nullptr && !nodes_left; e = e->next()) { + nodes_left = e->watch_fd == watch_fd; + } + + if (!nodes_left) { + inotify_rm_watch(_fd, watch_fd); + } + + return next; +} + + +void Lx_fs::Notifier::_handle_modify_file(inotify_event *event) +{ + for (Watches_list_element *e = _watched_nodes.first(); e != nullptr; e = e->next()) { + if (e->watch_fd == event->wd && (e->path.filename == event->name || e->path.is_dir())) { + e->notify_all([this] (Watch_node &node) { + _add_notify(node); + }); + } + } +} + + +void Lx_fs::Notifier::_remove_empty_watches() +{ + for (Watches_list_element *e = _watched_nodes.first(); e != nullptr; ) { + if (e->empty()) { + e = _remove_node(e); + } else { + e = e->next(); + } + } +} + + +void Lx_fs::Notifier::entry() +{ + enum { + SELECT_TIMEOUT_US = 5000, /* 5 milliseconds */ + }; + + auto notify_ts { timestamp_us() }; + bool pending_notify { false }; + struct inotify_event *event { nullptr }; + char buffer[EVENT_BUF_LEN] { 0 }; + while (true) { + + int num_select { 0 }; + Fd_set fds { _fd }; + struct timeval tv { .tv_sec = 0, .tv_usec = SELECT_TIMEOUT_US }; + + /* + * if no notifications are pending we wait until a inotify event occurs, + * otherwise we wait with a timeout on which we deliver some notifications + */ + { + Mutex::Guard guard { _notify_queue_mutex }; + pending_notify = _notify_queue.first() == nullptr; + } + if (pending_notify) + num_select = select(fds.nfds(), fds.fdset(), nullptr, nullptr, nullptr); + else + num_select = select(fds.nfds(), fds.fdset(), nullptr, nullptr, &tv); + + /* + * select failed + */ + if (num_select < 0) { + error("select on Linux event queue failed error=", Cstring { strerror(errno) }); + continue; + } + + /* + * select timed out, check if notifications are pending and send a bunch if neccessary + */ + if (num_select == 0) { + _process_notify(); + notify_ts = timestamp_us(); + continue; + } + + /* + * data is ready to be read from the inotify file descriptor + */ + ssize_t const length { read(_fd, buffer, EVENT_BUF_LEN) }; + ssize_t pos { 0 }; + + while (pos < length) { + event = reinterpret_cast(&buffer[pos]); + + /* + * one of the registered events was triggered for + * one of the watched files/directories + */ + if (event->mask & (INOTIFY_WATCH_MASK)) { + Mutex::Guard guard { _watched_nodes_mutex }; + _handle_modify_file(event); + } + + /* Linux kernel watch queue overflow */ + else if (event->mask & IN_Q_OVERFLOW) { + error("Linux event queue overflow"); + break; + } + + pos += EVENT_SIZE + event->len; + } + + /* + * Ensure that notifications are sent even when a lot of changes + * on the file system prevent the timout of select from triggering. + */ + auto const delta { timestamp_us() - notify_ts }; + if (delta > SELECT_TIMEOUT_US) { + _process_notify(); + notify_ts = timestamp_us(); + } + } +} + + +int Lx_fs::Notifier::add_watch(const char* path, Watch_node &node) +{ + { + Mutex::Guard guard { _watched_nodes_mutex }; + + if (!_watched(path)) { + _add_to_watched(path); + } + } + + return _add_node(path, node); +} + + +void Lx_fs::Notifier::remove_watch(char const *path, Watch_node &node) +{ + { + Mutex::Guard guard { _notify_queue_mutex }; + auto *e { _notify_queue.first() }; + while (e != nullptr) { + if (&e->node == &node) { + auto *tmp { e }; + e = e->next(); + _notify_queue.remove(tmp); + destroy(_heap, tmp); + } else { + e = e->next(); + } + } + } + + { + Mutex::Guard guard { _watched_nodes_mutex }; + + for (Watches_list_element *e = _watched_nodes.first(); e != nullptr; e = e->next()) { + if (e->path.full_path == path) { + e->remove_node(_heap, node); + } + } + + _remove_empty_watches(); + } +} diff --git a/repos/os/src/server/lx_fs/notifier.h b/repos/os/src/server/lx_fs/notifier.h new file mode 100644 index 0000000000..7329567768 --- /dev/null +++ b/repos/os/src/server/lx_fs/notifier.h @@ -0,0 +1,156 @@ +/* + * \brief inotify handling for underlying file system. + * \author Pirmin Duss + * \date 2020-06-17 + */ + +/* + * Copyright (C) 2020-2021 Genode Labs GmbH + * Copyright (C) 2020-2021 gapfruit AG + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _NOTIFIER_H_ +#define _NOTIFIER_H_ + +/* Genode includes */ +#include +#include +#include +#include +#include +#include +#include +#include + +/* libc includes */ +#include + +/* local includes */ +#include "lx_util.h" + + +namespace Lx_fs +{ + using namespace Genode; + + using Path_string = Genode::String; + + class Init_notify_failed : public Genode::Exception { }; + + enum { MAX_PATH_SIZE = 1024 }; + struct Os_path; + + class Notifier; + + /* forward decalaration */ + class Watch_node; +} + + +/* + * full_path is always a concatenation of directory and filename + */ +struct Lx_fs::Os_path +{ + Path_string const full_path; + Path_string const directory; /* always ends with '/' */ + Path_string const filename; + + Os_path(char const *path); + + bool is_dir() const { return filename.length() == 0; } + +}; + + +class Lx_fs::Notifier final : public Thread +{ + private: + + struct Single_watch_list_element : public Genode::List::Element + { + Watch_node &node; + + Single_watch_list_element(Watch_node &node) : node { node } {} + }; + + struct Watches_list_element : public Genode::List::Element + { + private: + + List _nodes { }; + + public: + + int const watch_fd; + Os_path const path; + + Watches_list_element(int const watch_fd, Os_path const &path) + : + watch_fd { watch_fd }, path { path } + { } + + ~Watches_list_element() = default; + + bool empty() const { return _nodes.first() == nullptr; } + + void add_node(Single_watch_list_element *cap_entry) + { + _nodes.insert(cap_entry); + } + + template + void notify_all(FN const &fn) + { + for (Single_watch_list_element *e=_nodes.first(); e!=nullptr; e=e->next()) { + fn(e->node); + } + } + + void remove_node(Allocator &alloc, Watch_node &node) + { + for (Single_watch_list_element *e=_nodes.first(); e!=nullptr; e=e->next()) { + if (&e->node == &node) { + _nodes.remove(e); + destroy(alloc, e); + return; + } + } + } + }; + + Env &_env; + Heap _heap { _env.ram(), _env.rm() }; + int _fd { -1 }; + List _watched_nodes { }; + Mutex _watched_nodes_mutex { }; + List _notify_queue { }; + Mutex _notify_queue_mutex { }; + + void entry() override; + + void _add_notify(Watch_node &node); + void _process_notify(); + void _handle_removed_file(inotify_event *event); + void _handle_modify_file(inotify_event *event); + void _remove_empty_watches(); + void _print_watches_list(); + + bool _watched(char const *path) const; + void _add_to_watched(char const *path); + int _add_node(char const *path, Watch_node &node); + Watches_list_element *_remove_node(Watches_list_element *node); + + public: + + Notifier(Env &env); + ~Notifier(); + + int add_watch(const char *path, Watch_node& node); + void remove_watch(char const *path, Watch_node &node); +}; + +#endif /* _NOTIFIER_H_ */ diff --git a/repos/os/src/server/lx_fs/target.mk b/repos/os/src/server/lx_fs/target.mk index 1fb4a91ebb..8ccef42b0a 100644 --- a/repos/os/src/server/lx_fs/target.mk +++ b/repos/os/src/server/lx_fs/target.mk @@ -1,6 +1,6 @@ TARGET = lx_fs REQUIRES = linux -SRC_CC = main.cc +SRC_CC = main.cc notifier.cc lx_util.cc watch.cc LIBS = lx_hybrid INC_DIR += $(PRG_DIR) diff --git a/repos/os/src/server/lx_fs/watch.cc b/repos/os/src/server/lx_fs/watch.cc new file mode 100644 index 0000000000..09052f9afa --- /dev/null +++ b/repos/os/src/server/lx_fs/watch.cc @@ -0,0 +1,65 @@ +/* + * \brief File-system node + * \author Pirmin Duss + * \date 2020-06-17 + */ + +/* + * Copyright (C) 2013-2020 Genode Labs GmbH + * Copyright (C) 2020 gapfruit AG + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + + +/* local includes */ +#include "watch.h" +#include "notifier.h" + + +unsigned long Lx_fs::Watch_node::_inode(char const *path) +{ + struct stat s { }; + int ret { lstat(path, &s) }; + if (ret == -1) + throw Lookup_failed(); + + return s.st_ino; +} + + +Lx_fs::Watch_node::Watch_node(Env &env, + char const *path, + Response_handler &response_handler, + Notifier ¬ifier) +: + Node { _inode(path) }, + _env { env }, + _response_handler { response_handler }, + _notifier { notifier } +{ + name(path); + + if (notifier.add_watch(path, *this) < 0) { + throw Lookup_failed { }; + } +} + +Lx_fs::Watch_node::~Watch_node() +{ + _notifier.remove_watch(name(), *this); +} + + +void Lx_fs::Watch_node::_handle_notify() +{ + mark_as_updated(); + _acked_packet = Packet_descriptor { Packet_descriptor { }, + Node_handle { _open_node->id().value }, + Packet_descriptor::CONTENT_CHANGED, + 0, 0 }; + _acked_packet.succeeded(true); + + _response_handler.handle_watch_node_response(*this); +} diff --git a/repos/os/src/server/lx_fs/watch.h b/repos/os/src/server/lx_fs/watch.h new file mode 100644 index 0000000000..7202849638 --- /dev/null +++ b/repos/os/src/server/lx_fs/watch.h @@ -0,0 +1,95 @@ +/* + * \brief File-system node for watched files/directories + * \author Pirmin Duss + * \date 2020-06-17 + */ + +/* + * Copyright (C) 2013-2020 Genode Labs GmbH + * Copyright (C) 2020 gapfruit AG + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _WATCH_H_ +#define _WATCH_H_ + +/* Genode includes */ +#include +#include +#include +#include + +/* local includes */ +#include "node.h" +#include "open_node.h" + + +namespace Lx_fs { + + using namespace File_system; + + /* forward declaration */ + class Notifier; + + class Watch_node; +} + + +class Lx_fs::Watch_node final : public Lx_fs::Node +{ + public: + + using Packet_descriptor = File_system::Packet_descriptor; + using Fs_open_node = File_system::Open_node; + + struct Response_handler : Genode::Interface + { + virtual void handle_watch_node_response(Watch_node &) = 0; + }; + + private: + + using Signal_handler = Genode::Signal_handler; + + /* + * Noncopyable + */ + Watch_node(Watch_node const &) = delete; + Watch_node &operator = (Watch_node const &) = delete; + + Genode::Env &_env; + Response_handler &_response_handler; + Notifier &_notifier; + Signal_handler _notify_handler { _env.ep(), *this, &Watch_node::_handle_notify }; + Packet_descriptor _acked_packet { }; + Fs_open_node *_open_node { nullptr }; + + void _handle_notify(); + + unsigned long _inode(char const *path); + + public: + + Watch_node(Genode::Env &env, + char const *path, + Response_handler &response_handler, + Notifier ¬ifier); + + ~Watch_node(); + + Signal_handler ¬ify_handler() { return _notify_handler; } + + void update_modification_time(Timestamp const) override { } + size_t read(char *, size_t, seek_off_t) override { return 0; } + size_t write(char const *, size_t, seek_off_t) override { return 0; } + Status status() override { return Status { }; } + + Packet_descriptor &acked_packet() { return _acked_packet; } + + void open_node(Fs_open_node *open_node) { _open_node = open_node; } + Fs_open_node *open_node() { return _open_node; } +}; + +#endif /* _WATCH_H_ */ diff --git a/repos/os/src/test/lx_fs_notify/file_writer/Makefile b/repos/os/src/test/lx_fs_notify/file_writer/Makefile new file mode 100644 index 0000000000..5917d135d8 --- /dev/null +++ b/repos/os/src/test/lx_fs_notify/file_writer/Makefile @@ -0,0 +1,11 @@ + +SRC_C = main.cc +C_WARNINGS = -Wall -Werror +BUILD_DIR = /tmp/bin + +all: + mkdir -p $(BUILD_DIR) + g++ $(C_WARNINGS) $(SRC_C) -o $(BUILD_DIR)/file_writer + +clean: + rm -Rf bin \ No newline at end of file diff --git a/repos/os/src/test/lx_fs_notify/file_writer/main.cc b/repos/os/src/test/lx_fs_notify/file_writer/main.cc new file mode 100644 index 0000000000..c30180efed --- /dev/null +++ b/repos/os/src/test/lx_fs_notify/file_writer/main.cc @@ -0,0 +1,118 @@ +/* + * \brief Test component for the watch feature of the `lx_fs` server. + * \author Pirmin Duss + * \date 2020-06-17 + */ + +/* + * Copyright (C) 2013-2020 Genode Labs GmbH + * Copyright (C) 2020 gapfruit AG + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + + +/* libc includes */ +#include +#include +#include +#include +#include + + +/* stdcxx includes */ +#include +#include +#include +#include + + +void print_usage(const char *prg) +{ + std::cerr << "Usage: " << prg << " <--fwrite|--write> \n\n"; + std::cerr << " --fwrite use fopen/fwrite/fclose functions\n"; + std::cerr << " --write use open/write/close functions\n"; + std::cerr << " input_file_name name of the input file to write.\n"; + std::cerr << " output_file_name name of the file to write to.\n"; + std::cerr << "\n" << std::endl; + + exit(-1); +} + + +void use_fwrite(std::string const &data, char const *out_file_name) +{ + FILE *out_fp { fopen(out_file_name, "w") }; + if (out_fp == nullptr) { + exit(-88); + } + + auto to_write { data.size() }; + size_t pos { 0 }; + + while (to_write > 0) { + + auto written { fwrite(data.c_str()+pos, sizeof(char), to_write, out_fp) }; + + to_write -= written; + pos += written; + } + + fclose(out_fp); +} + + +void use_write(std::string const &data, char const *out_file_name) +{ + auto fd { open(out_file_name, O_WRONLY) }; + if (fd < 0) { + exit(-99); + } + + auto to_write { data.size() }; + size_t pos { 0 }; + + while (to_write > 0) { + + auto written { write(fd, data.c_str()+pos, to_write) }; + + if (written < 0) { + exit(-66); + } + + to_write -= written; + pos += written; + } + + close(fd); +} + + +std::string read_input(char const *file_name) +{ + std::ifstream in_stream { file_name }; + + return std::string(std::istreambuf_iterator(in_stream), + std::istreambuf_iterator()); +} + + +int main(int argc, char *argv[]) +{ + if (argc < 4) { + print_usage(argv[0]); + } + + std::string const data { read_input(argv[2]) }; + + if (strcmp(argv[1], "--fwrite") == 0) { + use_fwrite(data, argv[3]); + } else if (strcmp(argv[1], "--write") == 0) { + use_write(data, argv[3]); + } else { + print_usage(argv[0]); + } + + return 0; +} diff --git a/repos/os/src/test/lx_fs_notify/file_writer/target.mk b/repos/os/src/test/lx_fs_notify/file_writer/target.mk new file mode 100644 index 0000000000..b28f5b81ff --- /dev/null +++ b/repos/os/src/test/lx_fs_notify/file_writer/target.mk @@ -0,0 +1,8 @@ +TARGET := test-file_writer + +LIBS := base +LIBS += libc +LIBS += posix +LIBS += stdcxx + +SRC_CC := main.cc \ No newline at end of file diff --git a/repos/os/src/test/lx_fs_notify/rom_log/main.cc b/repos/os/src/test/lx_fs_notify/rom_log/main.cc new file mode 100644 index 0000000000..bce9e1a729 --- /dev/null +++ b/repos/os/src/test/lx_fs_notify/rom_log/main.cc @@ -0,0 +1,55 @@ +/* + * \brief Test component for the watch feature of the `lx_fs` server. + * \author Pirmin Duss + * \date 2020-06-17 + */ + +/* + * Copyright (C) 2013-2020 Genode Labs GmbH + * Copyright (C) 2020 gapfruit AG + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + + +#include +#include + + +namespace Test_lx_fs_notify +{ + using namespace Genode; + + class Main; +}; + + +class Test_lx_fs_notify::Main +{ + private: + + Env& _env; + Signal_handler
_update_handler { _env.ep(), *this, &Main::_update }; + Attached_rom_dataspace _test_rom { _env, "outfile.txt" }; + + void _update() + { + _test_rom.update(); + log("updated ROM content: size=", strlen(_test_rom.local_addr())); + } + + public: + + Main(Env& env) : _env { env } + { + _test_rom.sigh(_update_handler); + log("wait for ROM update"); + } +}; + + +void Component::construct(Genode::Env& env) +{ + static Test_lx_fs_notify::Main main { env }; +} diff --git a/repos/os/src/test/lx_fs_notify/rom_log/target.mk b/repos/os/src/test/lx_fs_notify/rom_log/target.mk new file mode 100644 index 0000000000..1ca7421ae3 --- /dev/null +++ b/repos/os/src/test/lx_fs_notify/rom_log/target.mk @@ -0,0 +1,5 @@ +TARGET := test-rom_log + +SRC_CC := main.cc + +LIBS := base