diff --git a/repos/gems/recipes/src/fs_query/content.mk b/repos/gems/recipes/src/fs_query/content.mk
new file mode 100644
index 0000000000..f00e374753
--- /dev/null
+++ b/repos/gems/recipes/src/fs_query/content.mk
@@ -0,0 +1,10 @@
+SRC_DIR := src/app/fs_query
+
+include $(GENODE_DIR)/repos/base/recipes/src/content.inc
+
+MIRROR_FROM_REP_DIR := include/gems/vfs.h
+
+content: $(MIRROR_FROM_REP_DIR)
+
+$(MIRROR_FROM_REP_DIR):
+ $(mirror_from_rep_dir)
diff --git a/repos/gems/recipes/src/fs_query/hash b/repos/gems/recipes/src/fs_query/hash
new file mode 100644
index 0000000000..7c2d52582e
--- /dev/null
+++ b/repos/gems/recipes/src/fs_query/hash
@@ -0,0 +1 @@
+2018-08-21 474d21ebb2298e4b411dcef5c32a452c053b09c7
diff --git a/repos/gems/recipes/src/fs_query/used_apis b/repos/gems/recipes/src/fs_query/used_apis
new file mode 100644
index 0000000000..ade9ce3115
--- /dev/null
+++ b/repos/gems/recipes/src/fs_query/used_apis
@@ -0,0 +1,4 @@
+base
+os
+vfs
+report_session
diff --git a/repos/gems/run/fs_query.run b/repos/gems/run/fs_query.run
new file mode 100644
index 0000000000..2e7c965db8
--- /dev/null
+++ b/repos/gems/run/fs_query.run
@@ -0,0 +1,199 @@
+create_boot_directory
+
+import_from_depot \
+ genodelabs/src/[base_src] \
+ genodelabs/src/coreutils \
+ genodelabs/src/bash \
+ genodelabs/src/init \
+ genodelabs/src/libc \
+ genodelabs/src/noux \
+ genodelabs/src/posix \
+ genodelabs/src/report_rom \
+ genodelabs/src/vfs \
+ genodelabs/src/vfs_import
+
+install_config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ first
+ second
+
+
+ fourth
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+build { app/sequence app/fs_query }
+
+build_boot_image { sequence fs_query }
+
+append qemu_args " -nographic -serial mon:stdio "
+
+run_genode_until {.*child "test" exited with exit value 0.*\n} 30
+
+grep_output {\[init -> report_rom\].*}
+
+set num_listings [regexp -all {report 'fs_query -> listing'} $output dummy]
+
+# we expect at least four intermediate reports
+if {$num_listings < 4} {
+ puts "Error: too few reports generated"
+ exit 1
+}
+
+#
+# We cannot reliably compare the full output because some file operations
+# may trigger one or two reports depending on the timing of signal delivery.
+# However, we can at least check the last report for validity.
+#
+regsub {.*report 'fs_query -> listing'} $output {} output
+
+compare_output_to {
+[init -> report_rom]
+[init -> report_rom]
+[init -> report_rom] fourth
+[init -> report_rom] first
+[init -> report_rom] updated
+[init -> report_rom]
+[init -> report_rom]
+[init -> report_rom]
+}
diff --git a/repos/gems/src/app/fs_query/README b/repos/gems/src/app/fs_query/README
new file mode 100644
index 0000000000..1af1b13eca
--- /dev/null
+++ b/repos/gems/src/app/fs_query/README
@@ -0,0 +1,14 @@
+The fs_query component queries and monitors information stored on a file
+system. The file system is configured as a component-local VFS. The component
+accepts any number of '' nodes within its '' node. Each
+'' node must contain a 'path' attribute pointing to a directory to
+watch. The component generates a report labeled "listing". For each
+existing queried directory, the report contains a '' node with the
+list of files as '' nodes featuring the corresponding 'name' as
+attribute value.
+
+A '' can be equipped with a 'content="yes"' attribute. If set, the
+content of the queried files is supplemented as body of the '' nodes.
+The reported content is limited to 4 KiB per file. If the content is valid
+XML, the '' node contains an attribute 'xml="yes"' indicating that
+the XML information is inserted as is. Otherwise, the content is sanitized.
diff --git a/repos/gems/src/app/fs_query/main.cc b/repos/gems/src/app/fs_query/main.cc
new file mode 100644
index 0000000000..3107ec0b9c
--- /dev/null
+++ b/repos/gems/src/app/fs_query/main.cc
@@ -0,0 +1,222 @@
+/*
+ * \brief Tool for querying information from a file system
+ * \author Norman Feske
+ * \date 2018-08-17
+ */
+
+/*
+ * Copyright (C) 2018 Genode Labs GmbH
+ *
+ * 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
+#include
+#include
+#include
+#include
+#include
+
+namespace Fs_query {
+ using namespace Genode;
+ struct Watched_file;
+ struct Watched_directory;
+ struct Main;
+}
+
+
+struct Fs_query::Watched_file
+{
+ File_content::Path const _name;
+
+ Watcher _watcher;
+
+ Watched_file(Directory const &dir, File_content::Path name)
+ : _name(name), _watcher(dir, name) { }
+
+ virtual ~Watched_file() { }
+
+ void _gen_content(Xml_generator &xml, Allocator &alloc, Directory const &dir) const
+ {
+ File_content content(alloc, dir, _name, File_content::Limit{4*1024});
+
+ bool content_is_xml = false;
+
+ content.xml([&] (Xml_node node) {
+ if (!node.has_type("empty")) {
+ xml.attribute("xml", "yes");
+ xml.append("\n");
+ xml.append(node.addr(), node.size());
+ content_is_xml = true;
+ }
+ });
+
+ if (!content_is_xml) {
+ content.bytes([&] (char const *base, size_t len) {
+ xml.append_sanitized(base, len); });
+ }
+ }
+
+ void gen_query_response(Xml_generator &xml, Xml_node query,
+ Allocator &alloc, Directory const &dir) const
+ {
+ try {
+ xml.node("file", [&] () {
+ xml.attribute("name", _name);
+
+ if (query.attribute_value("content", false))
+ _gen_content(xml, alloc, dir);
+ });
+ }
+ /*
+ * File may have disappeared since last traversal. This condition
+ * is detected on the attempt to obtain the file content.
+ */
+ catch (Directory::Nonexistent_file) {
+ warning("could not obtain content of nonexistent file ", _name); }
+ catch (File::Open_failed) {
+ warning("cannot open file ", _name, " for reading"); }
+ catch (Xml_generator::Buffer_exceeded) { throw; }
+ }
+};
+
+
+struct Fs_query::Watched_directory
+{
+ Allocator &_alloc;
+
+ Directory::Path const _rel_path;
+
+ Directory const _dir;
+
+ Registry > _files { };
+
+ Watcher _watcher;
+
+ Watched_directory(Allocator &alloc, Directory &other, Directory::Path const &rel_path)
+ :
+ _alloc(alloc), _rel_path(rel_path),
+ _dir(other, rel_path), _watcher(other, rel_path)
+ {
+ _dir.for_each_entry([&] (Directory::Entry const &entry) {
+ if (entry.type() == Vfs::Directory_service::DIRENT_TYPE_FILE) {
+ try {
+ new (_alloc) Registered(_files, _dir, entry.name());
+ } catch (...) { }
+ }
+ });
+ }
+
+ virtual ~Watched_directory()
+ {
+ _files.for_each([&] (Registered &file) {
+ destroy(_alloc, &file); });
+ }
+
+ bool has_name(Directory::Path const &name) const { return _rel_path == name; }
+
+ void gen_query_response(Xml_generator &xml, Xml_node query) const
+ {
+ xml.node("dir", [&] () {
+ xml.attribute("path", _rel_path);
+ _files.for_each([&] (Watched_file const &file) {
+ file.gen_query_response(xml, query, _alloc, _dir); });
+ });
+ }
+};
+
+
+struct Fs_query::Main : Vfs::Watch_response_handler
+{
+ Env &_env;
+
+ Heap _heap { _env.ram(), _env.rm() };
+
+ Attached_rom_dataspace _config { _env, "config" };
+
+ Vfs::Global_file_system_factory _fs_factory { _heap };
+
+ /**
+ * Vfs::Watch_response_handler interface
+ */
+ void handle_watch_response(Vfs::Vfs_watch_handle::Context*) override
+ {
+ Signal_transmitter(_config_handler).submit();
+ }
+
+ struct Vfs_env : Vfs::Env
+ {
+ Main &_main;
+
+ struct Io_response_dummy : Vfs::Io_response_handler {
+ void handle_io_response(Vfs::Vfs_handle::Context*) override { }
+ } _io_dummy { };
+
+ Vfs_env(Main &main) : _main(main) { }
+
+ Genode::Env &env() override { return _main._env; }
+ Allocator &alloc() override { return _main._heap; }
+ Vfs::File_system &root_dir() override { return _main._root_dir_fs; }
+ Vfs::Io_response_handler &io_handler() override { return _io_dummy; }
+ Vfs::Watch_response_handler &watch_handler() override { return _main; }
+
+ } _vfs_env { *this };
+
+ Vfs::Dir_file_system _root_dir_fs {
+ _vfs_env, _config.xml().sub_node("vfs"), _fs_factory };
+
+ Directory _root_dir { _vfs_env };
+
+ Signal_handler _config_handler {
+ _env.ep(), *this, &Main::_handle_config };
+
+ Expanding_reporter _reporter { _env, "listing", "listing" };
+
+ Registry > _dirs { };
+
+ void _gen_listing(Xml_generator &xml, Xml_node config) const
+ {
+ config.for_each_sub_node("query", [&] (Xml_node query) {
+ Directory::Path const path = query.attribute_value("path", Directory::Path());
+ _dirs.for_each([&] (Watched_directory const &dir) {
+ if (dir.has_name(path))
+ dir.gen_query_response(xml, query);
+ });
+ });
+ }
+
+ void _handle_config()
+ {
+ _config.update();
+
+ Xml_node const config = _config.xml();
+
+ _root_dir_fs.apply_config(config.sub_node("vfs"));
+
+ _dirs.for_each([&] (Registered &dir) {
+ destroy(_heap, &dir); });
+
+ config.for_each_sub_node("query", [&] (Xml_node query) {
+ Directory::Path const path = query.attribute_value("path", Directory::Path());
+ new (_heap) Registered(_dirs, _heap, _root_dir, path);
+ });
+
+ _reporter.generate([&] (Xml_generator &xml) {
+ _gen_listing(xml, config); });
+ }
+
+ Main(Env &env) : _env(env)
+ {
+ _config.sigh(_config_handler);
+ _handle_config();
+ }
+};
+
+
+void Component::construct(Genode::Env &env)
+{
+ static Fs_query::Main main(env);
+}
+
diff --git a/repos/gems/src/app/fs_query/target.mk b/repos/gems/src/app/fs_query/target.mk
new file mode 100644
index 0000000000..73cde1a240
--- /dev/null
+++ b/repos/gems/src/app/fs_query/target.mk
@@ -0,0 +1,3 @@
+TARGET = fs_query
+SRC_CC = main.cc
+LIBS += base vfs
diff --git a/tool/autopilot.list b/tool/autopilot.list
index 3f6ed181ed..c84f1b52f9 100644
--- a/tool/autopilot.list
+++ b/tool/autopilot.list
@@ -18,6 +18,7 @@ fetchurl_lwip
fpu
fs_log
fs_packet
+fs_query
fs_report
gdb_monitor
init