-
Notifications
You must be signed in to change notification settings - Fork 103
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
7 changed files
with
910 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.