Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

miral::ConfigFile #3544

Merged
merged 21 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading