With the daemon shipped as a systemd user unit, the CLI must not fight systemd for it: - Auto-start (DaemonClient::startDaemon): when the unit is installed but not running and would run exactly the daemon the client expects (same socket, data dir and config as the default resolution, default binary), start it with `systemctl --user start` instead of forking a copy that a later login would race for the socket. Flag or environment overrides (--socket, --data-dir, YAMS_DAEMON_SOCKET, YAMS_DATA_DIR, YAMS_CONFIG, YAMS_DAEMON_BIN) keep the fork path. - `yams daemon start|stop|restart` use `systemctl --user` for an installed/active unit (start/restart without custom path options, stop/restart without --socket), falling back to IPC if systemctl fails. - `yams daemon install --user` with no options enables the packaged unit (`systemctl --user enable --now`) instead of writing a copy into ~/.config/systemd/user that would shadow it; with options it still writes that override and says so. `uninstall --user` reloads the manager after removing an override and explains how to mask the globally enabled packaged unit for one account. The decision is a pure function (userUnitMatchesPaths) with Catch2 coverage; systemctl is only run with whitelisted verbs and only when a user manager is reachable ($XDG_RUNTIME_DIR set, Linux). Signed-off-by: trvon <me@trevon.dev>
60 lines
2.4 KiB
C++
60 lines
2.4 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// When may a client hand daemon start-up to the packaged systemd user unit?
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <yams/daemon/client/systemd_user_unit.h>
|
|
|
|
namespace {
|
|
|
|
using yams::config::ResolvedRuntimePaths;
|
|
using yams::config::RuntimePathSource;
|
|
|
|
ResolvedRuntimePaths defaultPaths() {
|
|
ResolvedRuntimePaths p;
|
|
p.configFile = {"/home/u/.config/yams/config.toml", RuntimePathSource::PlatformDefault, ""};
|
|
p.dataDir = {"/home/u/.local/share/yams", RuntimePathSource::PlatformDefault, ""};
|
|
p.runtimeDir = {"/run/user/1000/yams", RuntimePathSource::PlatformDefault, ""};
|
|
p.socketPath = {"/run/user/1000/yams-daemon.sock", RuntimePathSource::PlatformDefault, ""};
|
|
return p;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("user unit: default paths match the unit", "[daemon][systemd-user-unit]") {
|
|
CHECK(yams::daemon::client::userUnitMatchesPaths(defaultPaths()));
|
|
}
|
|
|
|
TEST_CASE("user unit: paths from the shared config file still match",
|
|
"[daemon][systemd-user-unit]") {
|
|
// The unit reads the same ~/.config/yams/config.toml, e.g. data_dir from `yams init`.
|
|
auto p = defaultPaths();
|
|
p.dataDir.source = RuntimePathSource::ConfigFile;
|
|
p.socketPath.source = RuntimePathSource::ConfigFile;
|
|
CHECK(yams::daemon::client::userUnitMatchesPaths(p));
|
|
}
|
|
|
|
TEST_CASE("user unit: flag or environment overrides do not match", "[daemon][systemd-user-unit]") {
|
|
for (auto source : {RuntimePathSource::Explicit, RuntimePathSource::Environment}) {
|
|
auto data = defaultPaths();
|
|
data.dataDir.source = source;
|
|
CHECK_FALSE(yams::daemon::client::userUnitMatchesPaths(data));
|
|
|
|
auto socket = defaultPaths();
|
|
socket.socketPath.source = source;
|
|
CHECK_FALSE(yams::daemon::client::userUnitMatchesPaths(socket));
|
|
|
|
auto config = defaultPaths();
|
|
config.configFile.source = source;
|
|
CHECK_FALSE(yams::daemon::client::userUnitMatchesPaths(config));
|
|
}
|
|
auto runtime = defaultPaths();
|
|
runtime.runtimeDir.source = RuntimePathSource::Explicit;
|
|
CHECK_FALSE(yams::daemon::client::userUnitMatchesPaths(runtime));
|
|
}
|
|
|
|
TEST_CASE("user unit: only whitelisted systemctl verbs run", "[daemon][systemd-user-unit]") {
|
|
// Rejected before any process is spawned, on every platform.
|
|
CHECK_FALSE(yams::daemon::client::runUserUnit("kill"));
|
|
CHECK_FALSE(yams::daemon::client::runUserUnit("start; rm -rf /"));
|
|
}
|