diff --git a/src/OS/Linux.cpp b/src/OS/Linux.cpp new file mode 100644 index 0000000..fe61dda --- /dev/null +++ b/src/OS/Linux.cpp @@ -0,0 +1,656 @@ +#ifdef __linux__ + +// clang-format off +#include // SQLite::{Database, OPEN_READONLY} +#include // SQLite::Exception +#include // SQLite::Statement +#include // PATH_MAX +#include // std::strlen +#include // std::{unexpected, expected} +#include // std::filesystem::{current_path, directory_entry, directory_iterator, etc.} +#include // std::{format, format_to_n} +#include // std::ifstream +#include // glz::read_beve +#include // glz::write_beve +#include // std::numeric_limits +#include // matchit::{is, is_not, is_any, etc.} +#include // pugi::xml_document +#include // std::{getline, string (String)} +#include // std::string_view (StringView) +#include // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED +#include // statvfs +#include // sysinfo +#include // utsname, uname +#include // readlink +#include // std::move + +#include "Services/PackageCounting.hpp" +#include "Util/Caching.hpp" +#include "Util/Definitions.hpp" +#include "Util/Env.hpp" +#include "Util/Error.hpp" +#include "Util/Logging.hpp" +#include "Util/Types.hpp" +#include "Wrappers/DBus.hpp" +#include "Wrappers/Wayland.hpp" +#include "Wrappers/XCB.hpp" + +#include "OperatingSystem.hpp" +// clang-format on + +using util::error::DracError, util::error::DracErrorCode; +using util::types::String, util::types::Result, util::types::Err, util::types::usize; + +namespace { + #ifdef HAVE_XCB + fn GetX11WindowManager() -> Result { + using namespace xcb; + using namespace matchit; + using enum ConnError; + using util::types::StringView; + + const DisplayGuard conn; + + if (!conn) + if (const i32 err = ConnectionHasError(conn.get())) + return Err( + DracError( + DracErrorCode::ApiUnavailable, + match(err)( + is | Generic = "Stream/Socket/Pipe Error", + is | ExtNotSupported = "Extension Not Supported", + is | MemInsufficient = "Insufficient Memory", + is | ReqLenExceed = "Request Length Exceeded", + is | ParseErr = "Display String Parse Error", + is | InvalidScreen = "Invalid Screen", + is | FdPassingFailed = "FD Passing Failed", + is | _ = std::format("Unknown Error Code ({})", err) + ) + ) + ); + + fn internAtom = [&conn](const StringView name) -> Result { + using util::types::u16; + + const ReplyGuard reply(InternAtomReply(conn.get(), InternAtom(conn.get(), 0, static_cast(name.size()), name.data()), nullptr)); + + if (!reply) + return Err(DracError(DracErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name))); + + return reply->atom; + }; + + const Result supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK"); + const Result wmNameAtom = internAtom("_NET_WM_NAME"); + const Result utf8StringAtom = internAtom("UTF8_STRING"); + + if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) { + if (!supportingWmCheckAtom) + error_log("Failed to get _NET_SUPPORTING_WM_CHECK atom"); + + if (!wmNameAtom) + error_log("Failed to get _NET_WM_NAME atom"); + + if (!utf8StringAtom) + error_log("Failed to get UTF8_STRING atom"); + + return Err(DracError(DracErrorCode::PlatformSpecific, "Failed to get X11 atoms")); + } + + const ReplyGuard wmWindowReply(GetPropertyReply( + conn.get(), + GetProperty(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1), + nullptr + )); + + if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 || + GetPropertyValueLength(wmWindowReply.get()) == 0) + return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property")); + + const window_t wmRootWindow = *static_cast(GetPropertyValue(wmWindowReply.get())); + + const ReplyGuard wmNameReply(GetPropertyReply( + conn.get(), GetProperty(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr + )); + + if (!wmNameReply || wmNameReply->type != *utf8StringAtom || GetPropertyValueLength(wmNameReply.get()) == 0) + return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_WM_NAME property")); + + const char* nameData = static_cast(GetPropertyValue(wmNameReply.get())); + const usize length = GetPropertyValueLength(wmNameReply.get()); + + return String(nameData, length); + } + #else + fn GetX11WindowManager() -> Result { + return Err(DracError(DracErrorCode::NotSupported, "XCB (X11) support not available")); + } + #endif + + #ifdef HAVE_WAYLAND + fn GetWaylandCompositor() -> Result { + using util::types::i32, util::types::Array, util::types::isize, util::types::StringView; + + const wl::DisplayGuard display; + + if (!display) + return Err(DracError(DracErrorCode::NotFound, "Failed to connect to display (is Wayland running?)")); + + const i32 fileDescriptor = display.fd(); + if (fileDescriptor < 0) + return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor")); + + ucred cred; + socklen_t len = sizeof(cred); + + if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) + return Err(DracError("Failed to get socket credentials (SO_PEERCRED)")); + + Array exeLinkPathBuf {}; + + auto [out, size] = std::format_to_n(exeLinkPathBuf.data(), exeLinkPathBuf.size() - 1, "/proc/{}/exe", cred.pid); + + if (out >= exeLinkPathBuf.data() + exeLinkPathBuf.size() - 1) + return Err(DracError(DracErrorCode::InternalError, "Failed to format /proc path (PID too large?)")); + + *out = '\0'; + + const char* exeLinkPath = exeLinkPathBuf.data(); + + Array exeRealPathBuf {}; // NOLINT(misc-include-cleaner) - PATH_MAX is in + + const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1); + + if (bytesRead == -1) + return Err(DracError(std::format("Failed to read link '{}'", exeLinkPath))); + + exeRealPathBuf.at(bytesRead) = '\0'; + + StringView compositorNameView; + + const StringView pathView(exeRealPathBuf.data(), bytesRead); + + StringView filenameView; + + if (const usize lastCharPos = pathView.find_last_not_of('/'); lastCharPos != StringView::npos) { + const StringView relevantPart = pathView.substr(0, lastCharPos + 1); + + if (const usize separatorPos = relevantPart.find_last_of('/'); separatorPos == StringView::npos) + filenameView = relevantPart; + else + filenameView = relevantPart.substr(separatorPos + 1); + } + + if (!filenameView.empty()) + compositorNameView = filenameView; + + if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/") + return Err(DracError(DracErrorCode::NotFound, "Failed to get compositor name from path")); + + if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() && + compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) { + const StringView cleanedView = + compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length()); + + if (cleanedView.empty()) + return Err(DracError(DracErrorCode::NotFound, "Compositor name invalid after heuristic")); + + return String(cleanedView); + } + + return String(compositorNameView); + } + #else + fn GetWaylandCompositor() -> Result { + return Err(DracError(DracErrorCode::NotSupported, "Wayland support not available")); + } + #endif +} // namespace + +namespace os { + using util::helpers::GetEnv; + + fn GetOSVersion() -> Result { + using util::types::StringView; + + std::ifstream file("/etc/os-release"); + + if (!file) + return Err(DracError(DracErrorCode::NotFound, std::format("Failed to open /etc/os-release"))); + + String line; + constexpr StringView prefix = "PRETTY_NAME="; + + while (std::getline(file, line)) { + if (StringView(line).starts_with(prefix)) { + String value = line.substr(prefix.size()); + + if ((value.length() >= 2 && value.front() == '"' && value.back() == '"') || + (value.length() >= 2 && value.front() == '\'' && value.back() == '\'')) + value = value.substr(1, value.length() - 2); + + if (value.empty()) + return Err( + DracError(DracErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in /etc/os-release")) + ); + + return value; + } + } + + return Err(DracError(DracErrorCode::NotFound, "PRETTY_NAME line not found in /etc/os-release")); + } + + fn GetMemInfo() -> Result { + struct sysinfo info; + + if (sysinfo(&info) != 0) + return Err(DracError("sysinfo call failed")); + + const u64 totalRam = info.totalram; + const u64 memUnit = info.mem_unit; + + if (memUnit == 0) + return Err(DracError(DracErrorCode::InternalError, "sysinfo returned mem_unit of zero")); + + if (totalRam > std::numeric_limits::max() / memUnit) + return Err(DracError(DracErrorCode::InternalError, "Potential overflow calculating total RAM")); + + return info.totalram * info.mem_unit; + } + + fn GetNowPlaying() -> Result { + #ifdef HAVE_DBUS + using namespace dbus; + + Result connectionResult = Connection::busGet(DBUS_BUS_SESSION); + if (!connectionResult) + return Err(connectionResult.error()); + + const Connection& connection = *connectionResult; + + Option activePlayer = None; + + { + Result listNamesResult = Message::newMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"); + + if (!listNamesResult) + return Err(listNamesResult.error()); + + Result listNamesReplyResult = connection.sendWithReplyAndBlock(*listNamesResult, 100); + + if (!listNamesReplyResult) + return Err(listNamesReplyResult.error()); + + MessageIter iter = listNamesReplyResult->iterInit(); + + if (!iter.isValid() || iter.getArgType() != DBUS_TYPE_ARRAY) + return Err(DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Expected array")); + + MessageIter subIter = iter.recurse(); + + if (!subIter.isValid()) + return Err( + DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Could not recurse into array") + ); + + while (subIter.getArgType() != DBUS_TYPE_INVALID) { + if (Option name = subIter.getString()) + if (name->starts_with("org.mpris.MediaPlayer2.")) { + activePlayer = std::move(*name); + break; + } + if (!subIter.next()) + break; + } + } + + if (!activePlayer) + return Err(DracError(DracErrorCode::NotFound, "No active MPRIS players found")); + + Result msgResult = Message::newMethodCall(activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); + + if (!msgResult) + return Err(msgResult.error()); + + Message& msg = *msgResult; + + if (!msg.appendArgs("org.mpris.MediaPlayer2.Player", "Metadata")) + return Err(DracError(DracErrorCode::InternalError, "Failed to append arguments to Properties.Get message")); + + Result replyResult = connection.sendWithReplyAndBlock(msg, 100); + + if (!replyResult) + return Err(replyResult.error()); + + Option title = None; + Option artist = None; + + MessageIter propIter = replyResult->iterInit(); + + if (!propIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply has no arguments or invalid iterator")); + + if (propIter.getArgType() != DBUS_TYPE_VARIANT) + return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply argument is not a variant")); + + MessageIter variantIter = propIter.recurse(); + + if (!variantIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Could not recurse into variant")); + + if (variantIter.getArgType() != DBUS_TYPE_ARRAY || variantIter.getElementType() != DBUS_TYPE_DICT_ENTRY) + return Err(DracError(DracErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})")); + + MessageIter dictIter = variantIter.recurse(); + + if (!dictIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Could not recurse into metadata dictionary array")); + + while (dictIter.getArgType() == DBUS_TYPE_DICT_ENTRY) { + MessageIter entryIter = dictIter.recurse(); + + if (!entryIter.isValid()) { + debug_log("Warning: Could not recurse into dict entry, skipping."); + if (!dictIter.next()) + break; + continue; + } + + Option key = entryIter.getString(); + + if (!key) { + debug_log("Warning: Could not get key string from dict entry, skipping."); + if (!dictIter.next()) + break; + continue; + } + + if (!entryIter.next() || entryIter.getArgType() != DBUS_TYPE_VARIANT) { + if (!dictIter.next()) + break; + continue; + } + + MessageIter valueVariantIter = entryIter.recurse(); + + if (!valueVariantIter.isValid()) { + if (!dictIter.next()) + break; + continue; + } + + if (*key == "xesam:title") { + title = valueVariantIter.getString(); + } else if (*key == "xesam:artist") { + if (valueVariantIter.getArgType() == DBUS_TYPE_ARRAY && valueVariantIter.getElementType() == DBUS_TYPE_STRING) { + if (MessageIter artistArrayIter = valueVariantIter.recurse(); artistArrayIter.isValid()) + artist = artistArrayIter.getString(); + } else + debug_log("Warning: Artist value was not an array of strings as expected."); + } + + if (!dictIter.next()) + break; + } + + return MediaInfo(std::move(title), std::move(artist)); + #else + return Err(DracError(DracErrorCode::NotSupported, "DBus support not available")); + #endif + } + + fn GetWindowManager() -> Result { + #if !defined(HAVE_WAYLAND) && !defined(HAVE_XCB) + return Err(DracError(DracErrorCode::NotSupported, "Wayland or XCB support not available")); + #else + if (Result waylandResult = GetWaylandCompositor()) + return *waylandResult; + + if (Result x11Result = GetX11WindowManager()) + return *x11Result; + + return Err(DracError(DracErrorCode::NotFound, "Could not detect window manager (Wayland/X11) or both failed")); + #endif + } + + fn GetDesktopEnvironment() -> Result { + return GetEnv("XDG_CURRENT_DESKTOP") + .transform([](String xdgDesktop) -> String { + if (const usize colon = xdgDesktop.find(':'); colon != String::npos) + xdgDesktop.resize(colon); + + return xdgDesktop; + }) + .or_else([](const DracError&) -> Result { return GetEnv("DESKTOP_SESSION"); }); + } + + fn GetShell() -> Result { + using util::types::Pair, util::types::Array, util::types::StringView; + + if (const Result shellPath = GetEnv("SHELL")) { + // clang-format off + constexpr Array, 5> shellMap {{ + { "bash", "Bash" }, + { "zsh", "Zsh" }, + { "fish", "Fish" }, + { "nu", "Nushell" }, + { "sh", "SH" }, // sh last because other shells contain "sh" + }}; + // clang-format on + + for (const auto& [exe, name] : shellMap) + if (shellPath->contains(exe)) + return String(name); + + return *shellPath; // fallback to the raw shell path + } + + return Err(DracError(DracErrorCode::NotFound, "Could not find SHELL environment variable")); + } + + fn GetHost() -> Result { + using util::types::CStr; + + constexpr CStr primaryPath = "/sys/class/dmi/id/product_family"; + constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name"; + + fn readFirstLine = [&](const String& path) -> Result { + std::ifstream file(path); + String line; + + if (!file) + return Err(DracError(DracErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path))); + + if (!std::getline(file, line) || line.empty()) + return Err(DracError(DracErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path))); + + return line; + }; + + Result primaryResult = readFirstLine(primaryPath); + + if (primaryResult) + return primaryResult; + + DracError primaryError = primaryResult.error(); + + Result fallbackResult = readFirstLine(fallbackPath); + + if (fallbackResult) + return fallbackResult; + + DracError fallbackError = fallbackResult.error(); + + return Err(DracError( + DracErrorCode::InternalError, + std::format( + "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", + primaryPath, + primaryError.message, + fallbackPath, + fallbackError.message + ) + )); + } + + fn GetKernelVersion() -> Result { + utsname uts; + + if (uname(&uts) == -1) + return Err(DracError("uname call failed")); + + if (std::strlen(uts.release) == 0) + return Err(DracError(DracErrorCode::ParseError, "uname returned null kernel release")); + + return uts.release; + } + + fn GetDiskUsage() -> Result { + struct statvfs stat; + + if (statvfs("/", &stat) == -1) + return Err(DracError("Failed to get filesystem stats for '/' (statvfs call failed)")); + + return DiskSpace { + .usedBytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize), + .totalBytes = stat.f_blocks * stat.f_frsize, + }; + } +} // namespace os + +namespace package { + using namespace std::string_literals; + + fn CountApk() -> Result { + using namespace util::cache; + + const String pmId = "apk"; + const fs::path apkDbPath = "/lib/apk/db/installed"; + const String cacheKey = "pkg_count_" + pmId; + + std::error_code fsErrCode; + + if (!fs::exists(apkDbPath, fsErrCode)) { + if (fsErrCode) { + warn_log("Filesystem error checking for Apk DB at '{}': {}", apkDbPath.string(), fsErrCode.message()); + return Err(DracError(DracErrorCode::IoError, "Filesystem error checking Apk DB: " + fsErrCode.message())); + } + + return Err(DracError(DracErrorCode::NotFound, std::format("Apk database path '{}' does not exist", apkDbPath.string()))); + } + + if (Result cachedDataResult = ReadCache(cacheKey)) { + const auto& [cachedCount, timestamp] = *cachedDataResult; + std::error_code modTimeErrCode; + const fs::file_time_type dbModTime = fs::last_write_time(apkDbPath, modTimeErrCode); + + if (modTimeErrCode) { + warn_log( + "Could not get modification time for '{}': {}. Invalidating {} cache.", + apkDbPath.string(), + modTimeErrCode.message(), + pmId + ); + } else { + using std::chrono::system_clock, std::chrono::seconds, std::chrono::floor; + const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp)); + + if (cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) { + debug_log("Using valid {} package count cache (DB file unchanged since {}). Count: {}", pmId, std::format("{:%F %T %Z}", floor(cacheTimePoint)), cachedCount); + return cachedCount; + } + + debug_log("{} package count cache stale (DB file modified).", pmId); + } + } else { + if (cachedDataResult.error().code != DracErrorCode::NotFound) + debug_at(cachedDataResult.error()); + debug_log("{} package count cache not found or unreadable.", pmId); + } + + debug_log("Fetching fresh {} package count from file: {}", pmId, apkDbPath.string()); + + std::ifstream file(apkDbPath); + if (!file.is_open()) + return Err(DracError(DracErrorCode::IoError, std::format("Failed to open Apk database file '{}'", apkDbPath.string()))); + + String line; + + u64 count = 0; + + try { + while (std::getline(file, line)) + if (line.empty()) + count++; + } catch (const std::ios_base::failure& e) { + return Err(DracError( + DracErrorCode::IoError, + std::format("Error reading Apk database file '{}': {}", apkDbPath.string(), e.what()) + )); + } + + if (file.bad()) + return Err(DracError(DracErrorCode::IoError, std::format("IO error while reading Apk database file '{}'", apkDbPath.string()))); + + { + using std::chrono::duration_cast, std::chrono::system_clock, std::chrono::seconds; + + const i64 timestampEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); + + const PkgCountCacheData dataToCache(count, timestampEpochSeconds); + + if (Result writeResult = WriteCache(cacheKey, dataToCache); !writeResult) + debug_at(writeResult.error()); + } + + return count; + } + + fn CountDpkg() -> Result { + return GetCountFromDirectory("Dpkg", fs::current_path().root_path() / "var" / "lib" / "dpkg" / "info", ".list"s); + } + + fn CountMoss() -> Result { + Result countResult = GetCountFromDb("moss", "/.moss/db/install", "SELECT COUNT(*) FROM meta"); + + if (countResult) + if (*countResult > 0) + return *countResult - 1; + + return countResult; + } + + fn CountPacman() -> Result { + return GetCountFromDirectory("Pacman", fs::current_path().root_path() / "var" / "lib" / "pacman" / "local", true); + } + + fn CountRpm() -> Result { + return GetCountFromDb("rpm", "/var/lib/rpm/rpmdb.sqlite", "SELECT COUNT(*) FROM Installtid"); + } + + fn CountXbps() -> Result { + using util::types::CStr; + + const CStr xbpsDbPath = "/var/db/xbps"; + + if (!fs::exists(xbpsDbPath)) + return Err(DracError(DracErrorCode::NotFound, std::format("Xbps database path '{}' does not exist", xbpsDbPath))); + + fs::path plistPath; + for (const fs::directory_entry& entry : fs::directory_iterator(xbpsDbPath)) { + const String filename = entry.path().filename().string(); + if (filename.starts_with("pkgdb-") && filename.ends_with(".plist")) { + plistPath = entry.path(); + break; + } + } + + if (plistPath.empty()) + return Err(DracError(DracErrorCode::NotFound, "No Xbps database found")); + + return GetCountFromPlist("xbps", plistPath); + } +} // namespace package + +#endif // __linux__ diff --git a/src/Wrappers/DBus.hpp b/src/Wrappers/DBus.hpp new file mode 100644 index 0000000..3b8cb1d --- /dev/null +++ b/src/Wrappers/DBus.hpp @@ -0,0 +1,416 @@ +#pragma once + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) + +// clang-format off +#include +#include // DBus Library +#include // std::exchange, std::forward +#include // std::format +#include // std::is_convertible_v + +#include "Util/Definitions.hpp" +#include "Util/Error.hpp" +#include "Util/Types.hpp" +// clang-format on + +namespace dbus { + using util::error::DracError, util::error::DracErrorCode; + using util::types::Option, util::types::Result, util::types::Err, util::types::String, util::types::i32, + util::types::u32, util::types::None; + + /** + * @brief RAII wrapper for DBusError. Automatically initializes and frees. + */ + class Error { + DBusError m_err {}; + bool m_isInitialized = false; + + public: + Error() + : m_isInitialized(true) { + dbus_error_init(&m_err); + } + + ~Error() { + if (m_isInitialized) + dbus_error_free(&m_err); + } + + Error(const Error&) = delete; + fn operator=(const Error&)->Error& = delete; + + Error(Error&& other) noexcept + : m_err(other.m_err), m_isInitialized(other.m_isInitialized) { + other.m_isInitialized = false; + dbus_error_init(&other.m_err); + } + + fn operator=(Error&& other) noexcept -> Error& { + if (this != &other) { + if (m_isInitialized) + dbus_error_free(&m_err); + + m_err = other.m_err; + m_isInitialized = other.m_isInitialized; + + other.m_isInitialized = false; + dbus_error_init(&other.m_err); + } + return *this; + } + + /** + * @brief Checks if the D-Bus error is set. + * @return True if an error is set, false otherwise. + */ + [[nodiscard]] fn isSet() const -> bool { + return m_isInitialized && dbus_error_is_set(&m_err); + } + + /** + * @brief Gets the error message. + * @return The error message string, or "" if not set or not initialized. + */ + [[nodiscard]] fn message() const -> const char* { + return isSet() ? m_err.message : ""; + } + + /** + * @brief Gets the error name. + * @return The error name string (e.g., "org.freedesktop.DBus.Error.Failed"), or "" if not set or not initialized. + */ + [[nodiscard]] fn name() const -> const char* { + return isSet() ? m_err.name : ""; + } + + /** + * @brief Gets a pointer to the underlying DBusError. Use with caution. + * @return Pointer to the DBusError struct. + */ + [[nodiscard]] fn get() -> DBusError* { + return &m_err; + } + /** + * @brief Gets a const pointer to the underlying DBusError. + * @return Const pointer to the DBusError struct. + */ + [[nodiscard]] fn get() const -> const DBusError* { + return &m_err; + } + + /** + * @brief Converts the D-Bus error to a DraconisError. + * @param code The DraconisError code to use if the D-Bus error is set. + * @return A DraconisError representing the D-Bus error, or an internal error if called when no D-Bus error is set. + */ + [[nodiscard]] fn toDraconisError(const DracErrorCode code = DracErrorCode::PlatformSpecific) const -> DracError { + if (isSet()) + return { code, std::format("D-Bus Error: {} ({})", message(), name()) }; + + return { DracErrorCode::InternalError, "Attempted to convert non-set ErrorGuard" }; + } + }; + + /** + * @brief RAII wrapper for DBusMessageIter. Encapsulates iterator operations. + * Note: This wrapper does *not* own the message, only the iterator state. + * It's designed to be used within the scope where the MessageGuard is valid. + */ + class MessageIter { + DBusMessageIter m_iter {}; + bool m_isValid = false; + + explicit MessageIter(const DBusMessageIter& iter, const bool isValid) + : m_iter(iter), m_isValid(isValid) {} + + friend class Message; + + /** + * @brief Gets the value of a basic-typed argument. + * Unsafe: Caller must ensure 'value' points to memory suitable for the actual argument type. + * @param value Pointer to store the retrieved value. + */ + fn getBasic(void* value) -> void { + if (m_isValid) + dbus_message_iter_get_basic(&m_iter, value); + } + + public: + MessageIter(const MessageIter&) = delete; + fn operator=(const MessageIter&)->MessageIter& = delete; + MessageIter(MessageIter&&) = delete; + fn operator=(MessageIter&&)->MessageIter& = delete; + ~MessageIter() = default; + + /** + * @brief Checks if the iterator is validly initialized. + */ + [[nodiscard]] fn isValid() const -> bool { + return m_isValid; + } + + /** + * @brief Gets the D-Bus type code of the current argument. + * @return The D-Bus type code, or DBUS_TYPE_INVALID otherwise. + */ + [[nodiscard]] fn getArgType() -> int { + return m_isValid ? dbus_message_iter_get_arg_type(&m_iter) : DBUS_TYPE_INVALID; + } + + /** + * @brief Gets the element type of the container pointed to by the iterator. + * Only valid if the iterator points to an ARRAY or VARIANT. + * @return The D-Bus type code of the elements, or DBUS_TYPE_INVALID otherwise. + */ + [[nodiscard]] fn getElementType() -> int { + return m_isValid ? dbus_message_iter_get_element_type(&m_iter) : DBUS_TYPE_INVALID; + } + + /** + * @brief Advances the iterator to the next argument. + * @return True if successful (moved to a next element), false if at the end or iterator is invalid. + */ + fn next() -> bool { + return m_isValid && dbus_message_iter_next(&m_iter); + } + + /** + * @brief Recurses into a container-type argument (e.g., array, struct, variant). + * @return A new MessageIterGuard for the sub-container. The returned iterator might be invalid + * if the current element is not a container or the main iterator is invalid. + */ + [[nodiscard]] fn recurse() -> MessageIter { + if (!m_isValid) + return MessageIter({}, false); + + DBusMessageIter subIter; + dbus_message_iter_recurse(&m_iter, &subIter); + + return MessageIter(subIter, true); + } + + /** + * @brief Helper to safely get a string argument from the iterator. + * @return An Option containing the string value if the current arg is a valid string, or None otherwise. + */ + [[nodiscard]] fn getString() -> Option { + if (m_isValid && getArgType() == DBUS_TYPE_STRING) { + const char* strPtr = nullptr; + + // ReSharper disable once CppRedundantCastExpression + getBasic(static_cast(&strPtr)); + + if (strPtr) + return String(strPtr); + } + + return None; + } + }; + + /** + * @brief RAII wrapper for DBusMessage. Automatically unrefs. + */ + class Message { + DBusMessage* m_msg = nullptr; + + public: + explicit Message(DBusMessage* msg = nullptr) + : m_msg(msg) {} + + ~Message() { + if (m_msg) + dbus_message_unref(m_msg); + } + + Message(const Message&) = delete; + fn operator=(const Message&)->Message& = delete; + + Message(Message&& other) noexcept + : m_msg(std::exchange(other.m_msg, nullptr)) {} + + fn operator=(Message&& other) noexcept -> Message& { + if (this != &other) { + if (m_msg) + dbus_message_unref(m_msg); + m_msg = std::exchange(other.m_msg, nullptr); + } + return *this; + } + + /** + * @brief Gets the underlying DBusMessage pointer. Use with caution. + * @return The raw DBusMessage pointer, or nullptr if not holding a message. + */ + [[nodiscard]] fn get() const -> DBusMessage* { + return m_msg; + } + + /** + * @brief Initializes a message iterator for reading arguments from this message. + * @return A MessageIterGuard. Check iter.isValid() before use. + */ + [[nodiscard]] fn iterInit() const -> MessageIter { + if (!m_msg) + return MessageIter({}, false); + + DBusMessageIter iter; + const bool isValid = dbus_message_iter_init(m_msg, &iter); + return MessageIter(iter, isValid); + } + + /** + * @brief Appends arguments of basic types to the message. + * @tparam Args Types of the arguments to append. + * @param args The arguments to append. + * @return True if all arguments were appended successfully, false otherwise (e.g., allocation error). + */ + template + [[nodiscard]] fn appendArgs(Args&&... args) -> bool { + if (!m_msg) + return false; + + DBusMessageIter iter; + dbus_message_iter_init_append(m_msg, &iter); + + bool success = true; + ((success = success && appendArgInternal(iter, std::forward(args))), ...); // NOLINT + return success; + } + + /** + * @brief Creates a new D-Bus method call message. + * @param destination Service name (e.g., "org.freedesktop.Notifications"). Can be null. + * @param path Object path (e.g., "/org/freedesktop/Notifications"). Must not be null. + * @param interface Interface name (e.g., "org.freedesktop.Notifications"). Can be null. + * @param method Method name (e.g., "Notify"). Must not be null. + * @return Result containing a MessageGuard on success, or DraconisError on failure. + */ + static fn newMethodCall(const char* destination, const char* path, const char* interface, const char* method) + -> Result { + DBusMessage* rawMsg = dbus_message_new_method_call(destination, path, interface, method); + + if (!rawMsg) + return Err(DracError(DracErrorCode::OutOfMemory, "dbus_message_new_method_call failed (allocation failed?)")); + + return Message(rawMsg); + } + + private: + template + fn appendArgInternal(DBusMessageIter& iter, T&& arg) -> bool { + using DecayedT = std::decay_t; + + if constexpr (std::is_convertible_v) { + const char* valuePtr = static_cast(std::forward(arg)); + return dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, static_cast(&valuePtr)); + } else { + static_assert(!sizeof(T*), "Unsupported type passed to appendArgs"); + return false; + } + } + }; + + /** + * @brief RAII wrapper for DBusConnection. Automatically unrefs. + */ + class Connection { + DBusConnection* m_conn = nullptr; + + public: + explicit Connection(DBusConnection* conn = nullptr) + : m_conn(conn) {} + + ~Connection() { + if (m_conn) + dbus_connection_unref(m_conn); + } + + Connection(const Connection&) = delete; + fn operator=(const Connection&)->Connection& = delete; + + Connection(Connection&& other) noexcept + : m_conn(std::exchange(other.m_conn, nullptr)) {} + + fn operator=(Connection&& other) noexcept -> Connection& { + if (this != &other) { + if (m_conn) + dbus_connection_unref(m_conn); + + m_conn = std::exchange(other.m_conn, nullptr); + } + return *this; + } + + /** + * @brief Gets the underlying DBusConnection pointer. Use with caution. + * @return The raw DBusConnection pointer, or nullptr if not holding a connection. + */ + [[nodiscard]] fn get() const -> DBusConnection* { + return m_conn; + } + + /** + * @brief Sends a message and waits for a reply, blocking execution. + * @param message The D-Bus message guard to send. + * @param timeout_milliseconds Timeout duration in milliseconds. + * @return Result containing the reply MessageGuard on success, or DraconisError on failure. + */ + [[nodiscard]] fn sendWithReplyAndBlock(const Message& message, const i32 timeout_milliseconds = 1000) const + -> Result { + if (!m_conn || !message.get()) + return Err( + DracError(DracErrorCode::InvalidArgument, "Invalid connection or message provided to sendWithReplyAndBlock") + ); + + Error err; + DBusMessage* rawReply = + dbus_connection_send_with_reply_and_block(m_conn, message.get(), timeout_milliseconds, err.get()); + + if (err.isSet()) { + if (const char* errName = err.name()) { + if (strcmp(errName, DBUS_ERROR_TIMEOUT) == 0 || strcmp(errName, DBUS_ERROR_NO_REPLY) == 0) + return Err(err.toDraconisError(DracErrorCode::Timeout)); + + if (strcmp(errName, DBUS_ERROR_SERVICE_UNKNOWN) == 0) + return Err(err.toDraconisError(DracErrorCode::NotFound)); + + if (strcmp(errName, DBUS_ERROR_ACCESS_DENIED) == 0) + return Err(err.toDraconisError(DracErrorCode::PermissionDenied)); + } + + return Err(err.toDraconisError(DracErrorCode::PlatformSpecific)); + } + + if (!rawReply) + return Err(DracError( + DracErrorCode::ApiUnavailable, + "dbus_connection_send_with_reply_and_block returned null without setting error (likely timeout or " + "disconnected)" + )); + + return Message(rawReply); + } + + /** + * @brief Connects to a D-Bus bus type (Session or System). + * @param bus_type The type of bus (DBUS_BUS_SESSION or DBUS_BUS_SYSTEM). + * @return Result containing a ConnectionGuard on success, or DraconisError on failure. + */ + static fn busGet(const DBusBusType bus_type) -> Result { + Error err; + DBusConnection* rawConn = dbus_bus_get(bus_type, err.get()); + + if (err.isSet()) + return Err(err.toDraconisError(DracErrorCode::ApiUnavailable)); + + if (!rawConn) + return Err(DracError(DracErrorCode::ApiUnavailable, "dbus_bus_get returned null without setting error")); + + return Connection(rawConn); + } + }; +} // namespace dbus + +#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__ diff --git a/src/Wrappers/Wayland.hpp b/src/Wrappers/Wayland.hpp new file mode 100644 index 0000000..e91df18 --- /dev/null +++ b/src/Wrappers/Wayland.hpp @@ -0,0 +1,110 @@ +#pragma once + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) + +// clang-format off +#include // Wayland client library + +#include "Util/Definitions.hpp" +#include "Util/Logging.hpp" +#include "Util/Types.hpp" +// clang-format on + +struct wl_display; + +namespace wl { + using display = wl_display; + + inline fn Connect(const char* name) -> display* { + return wl_display_connect(name); + } + inline fn Disconnect(display* display) -> void { + wl_display_disconnect(display); + } + inline fn GetFd(display* display) -> int { + return wl_display_get_fd(display); + } + + /** + * RAII wrapper for Wayland display connections + * Automatically handles resource acquisition and cleanup + */ + class DisplayGuard { + display* m_display; + + public: + /** + * Opens a Wayland display connection + */ + DisplayGuard() { + wl_log_set_handler_client([](const char* fmt, va_list args) -> void { + using util::types::i32, util::types::StringView; + + va_list argsCopy; + va_copy(argsCopy, args); + i32 size = std::vsnprintf(nullptr, 0, fmt, argsCopy); + va_end(argsCopy); + + if (size < 0) { + error_log("Wayland: Internal log formatting error (vsnprintf size check failed)."); + return; + } + + std::vector buffer(static_cast(size) + 1); + + i32 writeSize = std::vsnprintf(buffer.data(), buffer.size(), fmt, args); + + if (writeSize < 0 || writeSize >= static_cast(buffer.size())) { + error_log("Wayland: Internal log formatting error (vsnprintf write failed)."); + return; + } + + StringView msgView(buffer.data(), static_cast(writeSize)); + + if (!msgView.empty() && msgView.back() == '\n') + msgView.remove_suffix(1); + + debug_log("Wayland {}", msgView); + }); + + // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) - needs to come after wl_log_set_handler_client + m_display = Connect(nullptr); + } + + ~DisplayGuard() { + if (m_display) + Disconnect(m_display); + } + + // Non-copyable + DisplayGuard(const DisplayGuard&) = delete; + fn operator=(const DisplayGuard&)->DisplayGuard& = delete; + + // Movable + DisplayGuard(DisplayGuard&& other) noexcept + : m_display(std::exchange(other.m_display, nullptr)) {} + fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& { + if (this != &other) { + if (m_display) + Disconnect(m_display); + + m_display = std::exchange(other.m_display, nullptr); + } + + return *this; + } + + [[nodiscard]] explicit operator bool() const { + return m_display != nullptr; + } + + [[nodiscard]] fn get() const -> display* { + return m_display; + } + [[nodiscard]] fn fd() const -> util::types::i32 { + return GetFd(m_display); + } + }; +} // namespace wl + +#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__ diff --git a/src/Wrappers/XCB.hpp b/src/Wrappers/XCB.hpp new file mode 100644 index 0000000..b6e16e1 --- /dev/null +++ b/src/Wrappers/XCB.hpp @@ -0,0 +1,340 @@ +#pragma once + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) + +// clang-format off +#include // XCB library + +#include "Util/Definitions.hpp" +#include "Util/Types.hpp" +// clang-format on + +namespace xcb { + using util::types::u8, util::types::i32, util::types::CStr, util::types::None; + + using connection_t = xcb_connection_t; + using setup_t = xcb_setup_t; + using screen_t = xcb_screen_t; + using window_t = xcb_window_t; + using atom_t = xcb_atom_t; + + using generic_error_t = xcb_generic_error_t; + using intern_atom_cookie_t = xcb_intern_atom_cookie_t; + using intern_atom_reply_t = xcb_intern_atom_reply_t; + using get_property_cookie_t = xcb_get_property_cookie_t; + using get_property_reply_t = xcb_get_property_reply_t; + + constexpr atom_t ATOM_WINDOW = XCB_ATOM_WINDOW; ///< Window atom + + /** + * @brief Enum representing different types of connection errors + * + * This enum defines the possible types of errors that can occur when + * establishing or maintaining an XCB connection. Each error type + * corresponds to a specific error code defined in the XCB library. + */ + enum ConnError : u8 { + Generic = XCB_CONN_ERROR, ///< Generic connection error + ExtNotSupported = XCB_CONN_CLOSED_EXT_NOTSUPPORTED, ///< Extension not supported + MemInsufficient = XCB_CONN_CLOSED_MEM_INSUFFICIENT, ///< Memory insufficient + ReqLenExceed = XCB_CONN_CLOSED_REQ_LEN_EXCEED, ///< Request length exceed + ParseErr = XCB_CONN_CLOSED_PARSE_ERR, ///< Parse error + InvalidScreen = XCB_CONN_CLOSED_INVALID_SCREEN, ///< Invalid screen + FdPassingFailed = XCB_CONN_CLOSED_FDPASSING_FAILED, ///< FD passing failed + }; + + /** + * @brief Connect to an XCB display + * + * This function establishes a connection to an XCB display. It takes a + * display name and a pointer to an integer that will store the screen + * number. + * + * @param displayname The name of the display to connect to + * @param screenp Pointer to an integer that will store the screen number + * @return A pointer to the connection object + */ + inline fn Connect(const char* displayname, int* screenp) -> connection_t* { + return xcb_connect(displayname, screenp); + } + + /** + * @brief Disconnect from an XCB display + * + * This function disconnects from an XCB display. It takes a pointer to + * the connection object. + * + * @param conn The connection object to disconnect from + */ + inline fn Disconnect(connection_t* conn) -> void { + xcb_disconnect(conn); + } + + /** + * @brief Check if a connection has an error + * + * This function checks if a connection has an error. It takes a pointer + * to the connection object. + * + * @param conn The connection object to check + * @return 1 if the connection has an error, 0 otherwise + */ + inline fn ConnectionHasError(connection_t* conn) -> int { + return xcb_connection_has_error(conn); + } + + /** + * @brief Intern an atom + * + * This function interns an atom. It takes a connection object, a flag + * + * @param conn The connection object to intern the atom on + * @param only_if_exists The flag to check if the atom exists + * @param name_len The length of the atom name + * @param name The name of the atom + * @return The cookie for the atom + */ + inline fn InternAtom(connection_t* conn, const uint8_t only_if_exists, const uint16_t name_len, const char* name) + -> intern_atom_cookie_t { + return xcb_intern_atom(conn, only_if_exists, name_len, name); + } + + /** + * @brief Get the reply for an interned atom + * + * This function gets the reply for an interned atom. It takes a connection + * object, a cookie, and a pointer to a generic error. + * + * @param conn The connection object + * @param cookie The cookie for the atom + * @param err The pointer to the generic error + * @return The reply for the atom + */ + inline fn InternAtomReply(connection_t* conn, const intern_atom_cookie_t cookie, generic_error_t** err) + -> intern_atom_reply_t* { + return xcb_intern_atom_reply(conn, cookie, err); + } + + /** + * @brief Get a property + * + * This function gets a property. It takes a connection object, a flag, + * a window, a property, a type, a long offset, and a long length. + * + * @param conn The connection object + * @param _delete The flag + * @param window The window + * @param property The property + * @param type The type + */ + inline fn GetProperty( + connection_t* conn, + const uint8_t _delete, + const window_t window, + const atom_t property, + const atom_t type, + const uint32_t long_offset, + const uint32_t long_length + ) -> get_property_cookie_t { + return xcb_get_property(conn, _delete, window, property, type, long_offset, long_length); + } + + /** + * @brief Get the reply for a property + * + * This function gets the reply for a property. It takes a connection + * object, a cookie, and a pointer to a generic error. + * + * @param conn The connection object + * @param cookie The cookie for the property + * @param err The pointer to the generic error + * @return The reply for the property + */ + inline fn GetPropertyReply(connection_t* conn, const get_property_cookie_t cookie, generic_error_t** err) + -> get_property_reply_t* { + return xcb_get_property_reply(conn, cookie, err); + } + + /** + * @brief Get the value length for a property + * + * @param reply The reply for the property + * @return The value length for the property + */ + inline fn GetPropertyValueLength(const get_property_reply_t* reply) -> int { + return xcb_get_property_value_length(reply); + } + + /** + * @brief Get the value for a property + * + * @param reply The reply for the property + * @return The value for the property + */ + inline fn GetPropertyValue(const get_property_reply_t* reply) -> void* { + return xcb_get_property_value(reply); + } + + /** + * RAII wrapper for X11 Display connections + * Automatically handles resource acquisition and cleanup + */ + class DisplayGuard { + connection_t* m_connection = nullptr; ///< The connection to the display + + public: + /** + * Opens an XCB connection + * @param name Display name (nullptr for default) + */ + explicit DisplayGuard(const util::types::CStr name = nullptr) + : m_connection(Connect(name, nullptr)) {} + ~DisplayGuard() { + if (m_connection) + Disconnect(m_connection); + } + + // Non-copyable + DisplayGuard(const DisplayGuard&) = delete; + fn operator=(const DisplayGuard&)->DisplayGuard& = delete; + + // Movable + DisplayGuard(DisplayGuard&& other) noexcept + : m_connection(std::exchange(other.m_connection, nullptr)) {} + + /** + * Move assignment operator + * @param other The other display guard + * @return The moved display guard + */ + fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& { + if (this != &other) { + if (m_connection) + Disconnect(m_connection); + + m_connection = std::exchange(other.m_connection, nullptr); + } + return *this; + } + + /** + * @brief Check if the display guard is valid + * @return True if the display guard is valid, false otherwise + */ + [[nodiscard]] explicit operator bool() const { + return m_connection && !ConnectionHasError(m_connection); + } + + /** + * @brief Get the connection to the display + * @return The connection to the display + */ + [[nodiscard]] fn get() const -> connection_t* { + return m_connection; + } + + /** + * @brief Get the setup for the display + * @return The setup for the display + */ + [[nodiscard]] fn setup() const -> const setup_t* { + return m_connection ? xcb_get_setup(m_connection) : nullptr; + } + + /** + * @brief Get the root screen for the display + * @return The root screen for the display + */ + [[nodiscard]] fn rootScreen() const -> screen_t* { + const setup_t* setup = this->setup(); + return setup ? xcb_setup_roots_iterator(setup).data : nullptr; + } + }; + + /** + * RAII wrapper for XCB replies + * Handles automatic cleanup of various XCB reply objects + */ + template + class ReplyGuard { + T* m_reply = nullptr; ///< The reply to the XCB request + + public: + /** + * @brief Default constructor + */ + ReplyGuard() = default; + + /** + * @brief Constructor with a reply + * @param reply The reply to the XCB request + */ + explicit ReplyGuard(T* reply) + : m_reply(reply) {} + + /** + * @brief Destructor + */ + ~ReplyGuard() { + if (m_reply) + free(m_reply); + } + + // Non-copyable + ReplyGuard(const ReplyGuard&) = delete; + fn operator=(const ReplyGuard&)->ReplyGuard& = delete; + + // Movable + ReplyGuard(ReplyGuard&& other) noexcept + : m_reply(std::exchange(other.m_reply, nullptr)) {} + + /** + * @brief Move assignment operator + * @param other The other reply guard + * @return The moved reply guard + */ + fn operator=(ReplyGuard&& other) noexcept -> ReplyGuard& { + if (this != &other) { + if (m_reply) + free(m_reply); + + m_reply = std::exchange(other.m_reply, nullptr); + } + return *this; + } + + /** + * @brief Check if the reply guard is valid + * @return True if the reply guard is valid, false otherwise + */ + [[nodiscard]] explicit operator bool() const { + return m_reply != nullptr; + } + + /** + * @brief Get the reply + * @return The reply + */ + [[nodiscard]] fn get() const -> T* { + return m_reply; + } + + /** + * @brief Get the reply + * @return The reply + */ + [[nodiscard]] fn operator->() const->T* { + return m_reply; + } + + /** + * @brief Dereference the reply + * @return The reply + */ + [[nodiscard]] fn operator*() const->T& { + return *m_reply; + } + }; +} // namespace xcb + +#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__