feat(dm): keep each user's desktop background for the login screen

main
reionwong 1 week ago
parent 578d0f9ff1
commit 876d8cf1b1

@ -26,6 +26,7 @@ add_library(cutefish-dm-common STATIC common/wire.cpp)
target_link_libraries(cutefish-dm-common PUBLIC Cutefish::ToolkitBase)
add_executable(cutefish-dm
daemon/backgrounds.cpp
daemon/config.cpp
daemon/daemon.cpp
daemon/greeterserver.cpp

@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "backgrounds.h"
#include <cerrno>
#include <filesystem>
#include <optional>
#include <string_view>
#include <pwd.h>
#include <sys/stat.h>
#include <unistd.h>
#include "base/file.h"
#include "base/settings.h"
using namespace cutefish;
namespace dm {
static constexpr std::string_view kSettingsSuffix = ".conf";
static constexpr std::string_view kPictureSuffix = ".picture";
static bool userExists(const std::string &user)
{
return getpwnam(user.c_str()) != nullptr;
}
static bool isColor(const std::string &color)
{
if (color.size() != 4 && color.size() != 7 && color.size() != 9)
return false;
if (color.front() != '#')
return false;
return color.find_first_not_of("0123456789abcdefABCDEF", 1) == std::string::npos;
}
// The whole file from its start, or nothing for anything but a regular file
// within the limit. The descriptor is the caller's, so its offset is left alone.
static std::optional<std::string> readPicture(int fd)
{
struct stat info = {};
if (fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || info.st_size > Backgrounds::kMaximumPictureSize)
return std::nullopt;
std::string bytes;
bytes.resize(size_t(info.st_size));
size_t done = 0;
while (done < bytes.size()) {
const ssize_t count = pread(fd, bytes.data() + done, bytes.size() - done, off_t(done));
if (count < 0 && errno == EINTR)
continue;
if (count <= 0)
return std::nullopt;
done += size_t(count);
}
return bytes.empty() ? std::nullopt : std::optional<std::string>(std::move(bytes));
}
Backgrounds::Backgrounds(std::string directory)
: m_directory(std::move(directory))
{
}
std::string Backgrounds::settingsPath(const std::string &user) const
{
return m_directory + '/' + user + std::string(kSettingsSuffix);
}
std::string Backgrounds::picturePath(const std::string &user) const
{
return m_directory + '/' + user + std::string(kPictureSuffix);
}
void Backgrounds::removeStale()
{
std::error_code error;
for (const auto &entry : std::filesystem::directory_iterator(m_directory, error)) {
const std::string name = entry.path().filename().string();
std::string user;
if (name.ends_with(kSettingsSuffix))
user = name.substr(0, name.size() - kSettingsSuffix.size());
else if (name.ends_with(kPictureSuffix))
user = name.substr(0, name.size() - kPictureSuffix.size());
if (user.empty() || !userExists(user))
std::filesystem::remove(entry.path(), error);
}
}
void Backgrounds::setPicture(const std::string &user, base::dbus::UnixFd file, std::function<void(bool)> done)
{
const uint64_t request = ++m_lastRequest;
m_latest[user] = request;
const std::string pending = picturePath(user) + '.' + std::to_string(request);
m_copies[request] = base::ThreadPool::global().run(
[file, pending] {
const std::optional<std::string> bytes = readPicture(file.fd());
return bytes && base::file::writeAtomically(pending, *bytes);
},
[this, user, request, pending, done = std::move(done)](bool copied) {
m_copies.erase(request);
if (!copied) {
unlink(pending.c_str());
done(false);
return;
}
// A later request took over; it has its own answer.
if (m_latest[user] != request) {
unlink(pending.c_str());
done(true);
return;
}
const std::string picture = picturePath(user);
if (rename(pending.c_str(), picture.c_str()) != 0) {
unlink(pending.c_str());
done(false);
return;
}
base::Settings settings(settingsPath(user));
settings.setValue("Appearance/BackgroundType", 0);
settings.setValue("Appearance/Wallpaper", picture);
done(settings.sync());
});
}
bool Backgrounds::setColor(const std::string &user, const std::string &color)
{
if (!isColor(color))
return false;
m_latest[user] = ++m_lastRequest;
base::Settings settings(settingsPath(user));
settings.setValue("Appearance/BackgroundType", 1);
settings.setValue("Appearance/BackgroundColor", color);
return settings.sync();
}
} // namespace dm

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
#include <functional>
#include <map>
#include <string>
#include "base/dbus_types.h"
#include "base/thread_pool.h"
namespace dm {
// Each user's desktop background as the login screen shows it, kept where the
// greeter can read it: the user's home may be closed to it or not mounted yet.
// <user>.conf has services.conf's [Appearance] keys, and <user>.picture is a
// copy of the picture it names.
class Backgrounds
{
public:
// The largest picture copied, in bytes.
static constexpr int64_t kMaximumPictureSize = 64 * 1024 * 1024;
explicit Backgrounds(std::string directory);
// Removes what belongs to no existing user, and copies left unfinished.
void removeStale();
// Copies the picture `file` reads, on a pool thread. done(false) for
// anything but a readable regular file within kMaximumPictureSize. Of two
// requests for one user, the later one wins whichever finishes first.
void setPicture(const std::string &user, cutefish::base::dbus::UnixFd file, std::function<void(bool)> done);
// #RGB, #RRGGBB or #AARRGGBB.
bool setColor(const std::string &user, const std::string &color);
std::string settingsPath(const std::string &user) const;
std::string picturePath(const std::string &user) const;
private:
std::string m_directory;
uint64_t m_lastRequest = 0;
// The latest request for each user.
std::map<std::string, uint64_t> m_latest;
std::map<uint64_t, cutefish::base::Job> m_copies;
};
} // namespace dm

@ -16,6 +16,7 @@ struct Config {
std::vector<std::string> sessionDirectories = {"/usr/local/share/wayland-sessions", "/usr/share/wayland-sessions"};
std::string defaultSession = "cutefish-wayland";
std::string statePath = "/var/lib/cutefish-dm/state.conf";
std::string backgroundDirectory = "/var/lib/cutefish-dm/backgrounds";
std::string runtimeDirectory = "/run/cutefish-dm";
// Defaults, overridden by whatever `path` sets.

@ -58,6 +58,7 @@ static bool bootSplashRunning()
Daemon::Daemon(Config config)
: m_config(std::move(config))
, m_state(m_config.statePath)
, m_backgrounds(m_config.backgroundDirectory)
{
m_greeterRestart.setSingleShot(true);
m_greeterRestart.onTimeout = [this] { startGreeter(); };
@ -110,6 +111,10 @@ bool Daemon::start()
"com.cutefish.DisplayManager");
m_object->addMethod("SwitchToGreeter", "", "",
[this](base::dbus::MethodCall &call) { switchToGreeter(call); });
m_object->addMethod("SetBackgroundPicture", "h", "",
[this](base::dbus::MethodCall &call) { setBackgroundPicture(call); });
m_object->addMethod("SetBackgroundColor", "s", "",
[this](base::dbus::MethodCall &call) { setBackgroundColor(call); });
}
if (!m_bus || !m_object->publish() || !m_bus->requestName("com.cutefish.DisplayManager"))
CF_WARNING(base::LogCategory::defaultCategory())
@ -120,6 +125,8 @@ bool Daemon::start()
execute("plymouth", {"deactivate"});
}
m_backgrounds.removeStale();
// A run that ended mid-login may have left it in KD_GRAPHICS with nobody on it.
restoreTextMode(m_config.sessionVt);
startGreeter();
@ -254,6 +261,58 @@ void Daemon::switchToGreeter(base::dbus::MethodCall &call)
returnToGreeter();
}
std::string Daemon::callerName(base::dbus::MethodCall &call)
{
const base::dbus::Proxy driver(m_bus, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus");
const std::optional<uint32_t> caller =
driver.call("GetConnectionUnixUser", call.message().sender()).value<uint32_t>();
const passwd *entry = caller ? getpwuid(*caller) : nullptr;
if (!entry || !entry->pw_name || m_config.greeterUser == entry->pw_name) {
call.replyError("org.freedesktop.DBus.Error.AccessDenied", "The caller is not a user who logs in");
return {};
}
return entry->pw_name;
}
void Daemon::setBackgroundPicture(base::dbus::MethodCall &call)
{
base::dbus::UnixFd file;
if (!call.read(file) || !file.isValid()) {
call.replyError("org.freedesktop.DBus.Error.InvalidArgs", "Expected the picture's file descriptor");
return;
}
const std::string user = callerName(call);
if (user.empty())
return;
m_backgrounds.setPicture(user, file, [call](bool copied) mutable {
if (copied)
call.reply();
else
call.replyError("org.freedesktop.DBus.Error.InvalidArgs", "The picture cannot be copied");
});
}
void Daemon::setBackgroundColor(base::dbus::MethodCall &call)
{
std::string color;
if (!call.read(color)) {
call.replyError("org.freedesktop.DBus.Error.InvalidArgs", "Expected a colour");
return;
}
const std::string user = callerName(call);
if (user.empty())
return;
if (m_backgrounds.setColor(user, color))
call.reply();
else
call.replyError("org.freedesktop.DBus.Error.InvalidArgs", "Not a colour that can be stored");
}
void Daemon::createSession(const std::string &username)
{
release(m_login);

@ -9,6 +9,7 @@
#include "base/dbus.h"
#include "base/timer.h"
#include "backgrounds.h"
#include "config.h"
#include "greeterserver.h"
#include "helperprocess.h"
@ -31,6 +32,10 @@ public:
// com.cutefish.DisplayManager.SwitchToGreeter: called by the session
// before it ends; replies once the login screen is up.
void switchToGreeter(cutefish::base::dbus::MethodCall &call);
// com.cutefish.DisplayManager.SetBackgroundPicture and SetBackgroundColor:
// the caller's own desktop background, for the login screen to show.
void setBackgroundPicture(cutefish::base::dbus::MethodCall &call);
void setBackgroundColor(cutefish::base::dbus::MethodCall &call);
private:
struct PendingStart {
@ -44,6 +49,8 @@ private:
void returnToGreeter();
void onGreeterFinished();
void replySwitchRequests(bool switched);
// The calling user's name; empty, with the call refused, when it cannot be told.
std::string callerName(cutefish::base::dbus::MethodCall &call);
void createSession(const std::string &username);
void postAuthMessageResponse(const Json &response);
@ -61,6 +68,7 @@ private:
Config m_config;
State m_state;
Backgrounds m_backgrounds;
GreeterServer m_server;
cutefish::base::dbus::Connection *m_bus = nullptr;
std::unique_ptr<cutefish::base::dbus::Object> m_object;

@ -4,7 +4,8 @@
<policy user="root">
<allow own="com.cutefish.DisplayManager"/>
</policy>
<!-- The daemon checks that the caller owns the session it is asked to end. -->
<!-- The daemon checks that the caller owns the session it is asked to end,
and keeps each caller's background under the caller's own name. -->
<policy context="default">
<allow send_destination="com.cutefish.DisplayManager"
send_interface="com.cutefish.DisplayManager"/>

@ -3,6 +3,7 @@ find_package(GTest REQUIRED)
add_executable(dm-test
dm_test.cpp
terminal_stub.cpp
../daemon/backgrounds.cpp
../daemon/config.cpp
../daemon/daemon.cpp
../daemon/greeterserver.cpp

@ -4,7 +4,10 @@
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <functional>
#include <optional>
#include <poll.h>
#include <pwd.h>
#include <sys/stat.h>
@ -16,6 +19,7 @@
#include "base/settings.h"
#include "common/wire.h"
#include "daemon/backgrounds.h"
#include "daemon/config.h"
#include "daemon/daemon.h"
#include "daemon/greeterserver.h"
@ -301,6 +305,119 @@ TEST_F(DisplayManagerTest, ConfigFallsBackToDefaults)
EXPECT_EQ(config.greeterUser, "cutefish-greeter");
}
static std::string currentUser()
{
const passwd *entry = getpwuid(getuid());
return entry && entry->pw_name ? entry->pw_name : "";
}
static base::dbus::UnixFd openForReading(const std::string &path)
{
return base::dbus::UnixFd::adopt(open(path.c_str(), O_RDONLY | O_CLOEXEC));
}
// Runs the loop until the copy answers; its answer.
static std::optional<bool> copyPicture(dm::Backgrounds &backgrounds, const std::string &user,
base::dbus::UnixFd file)
{
std::optional<bool> answer;
backgrounds.setPicture(user, std::move(file), [&answer](bool copied) { answer = copied; });
runUntil([&answer] { return answer.has_value(); });
return answer;
}
TEST_F(DisplayManagerTest, BackgroundsKeepTheUsersPictureAndColour)
{
const std::string user = currentUser();
ASSERT_FALSE(user.empty());
dm::Backgrounds backgrounds(directory.path() + "/backgrounds");
writeFile(directory.path() + "/sky.jpg", "picture bytes");
base::dbus::UnixFd file = openForReading(directory.path() + "/sky.jpg");
// The caller's offset is not where the copy starts.
ASSERT_EQ(lseek(file.fd(), 4, SEEK_SET), 4);
EXPECT_EQ(copyPicture(backgrounds, user, file), true);
EXPECT_EQ(lseek(file.fd(), 0, SEEK_CUR), 4);
EXPECT_EQ(base::file::read(backgrounds.picturePath(user)).value_or(""), "picture bytes");
// The keys the login screen reads, as services.conf has them.
{
const base::Settings settings(backgrounds.settingsPath(user));
EXPECT_EQ(settings.intValue("Appearance/BackgroundType", -1), 0);
EXPECT_EQ(settings.stringValue("Appearance/Wallpaper"), backgrounds.picturePath(user));
}
// The login screen reads both as its own user.
struct stat info = {};
ASSERT_EQ(stat(backgrounds.picturePath(user).c_str(), &info), 0);
EXPECT_TRUE(info.st_mode & S_IROTH);
ASSERT_EQ(stat(backgrounds.settingsPath(user).c_str(), &info), 0);
EXPECT_TRUE(info.st_mode & S_IROTH);
EXPECT_TRUE(backgrounds.setColor(user, "#2B8ADA"));
{
const base::Settings settings(backgrounds.settingsPath(user));
EXPECT_EQ(settings.intValue("Appearance/BackgroundType", -1), 1);
EXPECT_EQ(settings.stringValue("Appearance/BackgroundColor"), "#2B8ADA");
}
EXPECT_FALSE(backgrounds.setColor(user, "red"));
EXPECT_FALSE(backgrounds.setColor(user, "#12345"));
EXPECT_FALSE(backgrounds.setColor(user, "#12x456"));
}
TEST_F(DisplayManagerTest, BackgroundsRefuseWhatIsNotARegularFile)
{
const std::string user = currentUser();
dm::Backgrounds backgrounds(directory.path() + "/backgrounds");
EXPECT_EQ(copyPicture(backgrounds, user, openForReading(directory.path())), false);
int pipeFds[2];
ASSERT_EQ(pipe(pipeFds), 0);
close(pipeFds[1]);
EXPECT_EQ(copyPicture(backgrounds, user, base::dbus::UnixFd::adopt(pipeFds[0])), false);
writeFile(directory.path() + "/empty.jpg", "");
EXPECT_EQ(copyPicture(backgrounds, user, openForReading(directory.path() + "/empty.jpg")), false);
EXPECT_NE(access(backgrounds.settingsPath(user).c_str(), F_OK), 0);
EXPECT_NE(access(backgrounds.picturePath(user).c_str(), F_OK), 0);
}
TEST_F(DisplayManagerTest, BackgroundsKeepTheLatestRequest)
{
const std::string user = currentUser();
dm::Backgrounds backgrounds(directory.path() + "/backgrounds");
// A colour chosen while a picture is still being copied wins.
writeFile(directory.path() + "/sky.jpg", "picture bytes");
std::optional<bool> answer;
backgrounds.setPicture(user, openForReading(directory.path() + "/sky.jpg"),
[&answer](bool copied) { answer = copied; });
EXPECT_TRUE(backgrounds.setColor(user, "#000"));
runUntil([&answer] { return answer.has_value(); });
EXPECT_EQ(answer, true);
const base::Settings settings(backgrounds.settingsPath(user));
EXPECT_EQ(settings.intValue("Appearance/BackgroundType", -1), 1);
EXPECT_NE(access(backgrounds.picturePath(user).c_str(), F_OK), 0);
// No copy is left behind.
std::error_code error;
for (const auto &entry : std::filesystem::directory_iterator(directory.path() + "/backgrounds", error))
EXPECT_EQ(entry.path().filename().string(), user + ".conf");
}
TEST_F(DisplayManagerTest, BackgroundsOfRemovedUsersAreDropped)
{
const std::string user = currentUser();
const std::string path = directory.path() + "/backgrounds";
dm::Backgrounds backgrounds(path);
EXPECT_TRUE(backgrounds.setColor(user, "#000"));
writeFile(path + "/no-such-user-dm-test.conf", "");
writeFile(path + "/no-such-user-dm-test.picture", "");
writeFile(path + "/" + user + ".picture.7", "");
backgrounds.removeStale();
EXPECT_EQ(access(backgrounds.settingsPath(user).c_str(), F_OK), 0);
EXPECT_NE(access((path + "/no-such-user-dm-test.conf").c_str(), F_OK), 0);
EXPECT_NE(access((path + "/no-such-user-dm-test.picture").c_str(), F_OK), 0);
EXPECT_NE(access((path + "/" + user + ".picture.7").c_str(), F_OK), 0);
}
TEST(ConfigTest, CommandsSplitAsTheConfigurationAlwaysHas)
{
EXPECT_EQ(dm::splitCommand("a b\tc"), (std::vector<std::string> {"a", "b", "c"}));

Loading…
Cancel
Save