Skip to content

Commit

Permalink
miral::ConfigFile (#3544)
Browse files Browse the repository at this point in the history
A utility class to locate and (re)load configuration files when they
change. The actual `Loader` processing of the file is one customization
point, the reloading strategy another.
  • Loading branch information
Saviq authored Aug 21, 2024
2 parents 8887bcb + 4dfe34d commit a026b51
Show file tree
Hide file tree
Showing 7 changed files with 910 additions and 1 deletion.
2 changes: 2 additions & 0 deletions debian/libmiral7.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ libmiral.so.7 libmiral7 #MINVER#
(c++)"vtable for miral::MinimalWindowManager@MIRAL_5.0" 5.0.0
(c++)"vtable for miral::WindowManagementPolicy@MIRAL_5.0" 5.0.0
MIRAL_5.1@MIRAL_5.1 5.1.0
(c++)"miral::ConfigFile::ConfigFile(miral::MirRunner&, std::filesystem::__cxx11::path, miral::ConfigFile::Mode, std::function<void (std::basic_istream<char, std::char_traits<char> >&, std::filesystem::__cxx11::path const&)>)@MIRAL_5.1" 5.1.0
(c++)"miral::ConfigFile::~ConfigFile()@MIRAL_5.1" 5.1.0
(c++)"miral::Decorations::Decorations(std::shared_ptr<miral::Decorations::Self>)@MIRAL_5.1" 5.1.0
(c++)"miral::Decorations::always_csd()@MIRAL_5.1" 5.1.0
(c++)"miral::Decorations::always_ssd()@MIRAL_5.1" 5.1.0
Expand Down
66 changes: 66 additions & 0 deletions include/miral/miral/config_file.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright © Canonical Ltd.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 or 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef MIRAL_CONFIG_FILE_H
#define MIRAL_CONFIG_FILE_H

#include <mir/fd.h>

#include <filesystem>
#include <istream>
#include <functional>

namespace miral { class MirRunner; class FdHandle; }

namespace miral
{
/**
* Utility to locate and monitor a configuration file via the XDG Base Directory
* Specification. Vis: ($XDG_CONFIG_HOME or $HOME/.config followed by
* $XDG_CONFIG_DIRS). If, instead of a filename, a path is given, then the base
* directories are not applied.
*
* If mode is `no_reloading`, then the file is loaded on startup and not reloaded
*
* If mode is `reload_on_change`, then the file is loaded on startup and either
* the user-specific configuration file base ($XDG_CONFIG_HOME or $HOME/.config),
* or the supplied path is monitored for changes.
* \remark MirAL 5.1
*/
class ConfigFile
{
public:
/// Loader functor is passed both the open stream and the actual path (for use in reporting problems)
using Loader = std::function<void(std::istream& istream, std::filesystem::path const& path)>;

/// Mode of reloading
enum class Mode
{
no_reloading,
reload_on_change
};

ConfigFile(MirRunner& runner, std::filesystem::path file, Mode mode, Loader load_config);
~ConfigFile();

private:

class Self;
std::shared_ptr<Self> self;
};
}

#endif //MIRAL_CONFIG_FILE_H
1 change: 1 addition & 0 deletions src/miral/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ add_library(miral-external OBJECT
application_authorizer.cpp ${miral_include}/miral/application_authorizer.h
application_info.cpp ${miral_include}/miral/application_info.h
canonical_window_manager.cpp ${miral_include}/miral/canonical_window_manager.h
config_file.cpp ${miral_include}/miral/config_file.h
configuration_option.cpp ${miral_include}/miral/configuration_option.h
${miral_include}/miral/command_line_option.h
cursor_theme.cpp ${miral_include}/miral/cursor_theme.h
Expand Down
221 changes: 221 additions & 0 deletions src/miral/config_file.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* Copyright © Canonical Ltd.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 or 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include <miral/config_file.h>

#include <miral/runner.h>

#define MIR_LOG_COMPONENT "ReloadingConfigFile"
#include <mir/log.h>

#include <boost/throw_exception.hpp>

#include <sys/inotify.h>
#include <unistd.h>

#include <fstream>
#include <optional>
#include <string>
#include <vector>

using namespace std::filesystem;

namespace
{
auto config_directory(path const& file) -> std::optional<path>
{
if (file.has_parent_path())
{
return file.parent_path();
}
else if (auto config_home = getenv("XDG_CONFIG_HOME"))
{
return config_home;
}
else if (auto home = getenv("HOME"))
{
return path(home) / ".config";
}
else
{
return std::nullopt;
}
}

auto watch_descriptor(mir::Fd const& inotify_fd, std::optional<path> const& path) -> std::optional<int>
{
if (!path.has_value())
return std::nullopt;

if (inotify_fd < 0)
BOOST_THROW_EXCEPTION((std::system_error{errno, std::system_category(), "Failed to initialize inotify_fd"}));

return inotify_add_watch(inotify_fd, path.value().c_str(), IN_CLOSE_WRITE | IN_CREATE | IN_MOVED_TO);
}

class Watcher
{
public:
using Loader = miral::ConfigFile::Loader;
Watcher(miral::MirRunner& runner, path file, miral::ConfigFile::Loader load_config);

mir::Fd const inotify_fd;
Loader const load_config;
path const filename;
std::optional<path> const directory;
std::optional<int> const directory_watch_descriptor;

void register_handler(miral::MirRunner& runner);
std::unique_ptr<miral::FdHandle> fd_handle;
};
}

class miral::ConfigFile::Self
{
public:
Self(MirRunner& runner, path file, Mode mode, Loader load_config);

private:
std::unique_ptr<Watcher> watcher;
};

Watcher::Watcher(miral::MirRunner& runner, path file, miral::ConfigFile::Loader load_config) :
inotify_fd{inotify_init1(IN_CLOEXEC)},
load_config{load_config},
filename{file.filename()},
directory{config_directory(file)},
directory_watch_descriptor{watch_descriptor(inotify_fd, directory)}
{
register_handler(runner);

if (directory_watch_descriptor.has_value())
{
mir::log_debug("Monitoring %s for configuration changes", (directory.value()/filename).c_str());
}
}

miral::ConfigFile::Self::Self(MirRunner& runner, path file, Mode mode, Loader load_config)
{
auto const filename{file.filename()};
auto const directory{config_directory(file)};

// With C++26 we should be able to use the optional directory as a range to
// initialize config_roots. Until then, we'll just do it the long way...
std::vector<path> config_roots;

if (directory)
{
config_roots.push_back(directory.value());
}

if (auto config_dirs = getenv("XDG_CONFIG_DIRS"))
{
std::istringstream config_stream{config_dirs};
for (std::string config_root; getline(config_stream, config_root, ':');)
{
config_roots.push_back(config_root);
}
}
else
{
config_roots.push_back("/etc/xdg");
}

/* Read config file */
for (auto const& config_root : config_roots)
{
auto filepath = config_root / filename;
if (std::ifstream config_file{filepath})
{
load_config(config_file, filepath);
mir::log_debug("Loaded %s", filepath.c_str());
break;
}
}

switch (mode)
{
case Mode::no_reloading:
break;

case Mode::reload_on_change:
watcher = std::make_unique<Watcher>(runner, file, std::move(load_config));
break;
}
}

miral::ConfigFile::ConfigFile(MirRunner& runner, path file, Mode mode, Loader load_config) :
self{std::make_shared<Self>(runner, file, mode, load_config)}
{
}

miral::ConfigFile::~ConfigFile() = default;

void Watcher::register_handler(miral::MirRunner& runner)
{
if (directory_watch_descriptor)
{
fd_handle = runner.register_fd_handler(inotify_fd, [this] (int)
{
static size_t const sizeof_inotify_event = sizeof(inotify_event);

// A union ensures buffer is aligned for inotify_event
union
{
char buffer[sizeof_inotify_event + NAME_MAX + 1];
inotify_event unused [[maybe_unused]];
} inotify_magic;

auto const readsize = read(inotify_fd, &inotify_magic, sizeof(inotify_magic));
if (readsize < static_cast<ssize_t>(sizeof_inotify_event))
{
return;
}

auto raw_buffer = inotify_magic.buffer;
while (raw_buffer != inotify_magic.buffer + readsize)
{
// This is safe because inotify_magic.buffer is aligned and event.len includes padding for alignment
auto& event = reinterpret_cast<inotify_event&>(*raw_buffer);
if (event.mask & (IN_CLOSE_WRITE | IN_MOVED_TO) && event.wd == directory_watch_descriptor.value())
try
{
if (event.name == filename)
{
auto const& file = directory.value() / filename;

if (std::ifstream config_file{file})
{
load_config(config_file, file);
mir::log_debug("(Re)loaded %s", file.c_str());
}
else
{
mir::log_debug("Failed to open %s", file.c_str());
}
}
}
catch (...)
{
mir::log(mir::logging::Severity::warning, MIR_LOG_COMPONENT, std::current_exception(),
"Failed to reload configuration");
}

raw_buffer += sizeof_inotify_event+event.len;
}
});
}
}
6 changes: 5 additions & 1 deletion src/miral/symbols.map
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ local: *;
MIRAL_5.1 {
global:
extern "C++" {
miral::ConfigFile::?ConfigFile*;
miral::ConfigFile::ConfigFile*;
miral::Decorations::Decorations*;
miral::Decorations::always_csd*;
miral::Decorations::always_ssd*;
Expand All @@ -469,7 +471,9 @@ global:
miral::IdleListener::on_wake*;
miral::IdleListener::operator*;
miral::WindowManagerTools::move_cursor_to*;
typeinfo?for?miral::IdleListener;
typeinfo?for?miral::ConfigFile;
typeinfo?for?miral::Decorations;
typeinfo?for?miral::IdleListener;
};
} MIRAL_5.0;

1 change: 1 addition & 0 deletions tests/miral/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ target_link_libraries(miral-test-internal

mir_add_wrapped_executable(miral-test NOINSTALL
external_client.cpp
config_file.cpp
runner.cpp
wayland_extensions.cpp
zone.cpp
Expand Down
Loading

0 comments on commit a026b51

Please sign in to comment.