This commit is contained in:
Mars 2025-04-28 04:14:13 -04:00
parent 24b6a72614
commit 1a2fba7fb8
Signed by: pupbrained
GPG key ID: 0FF5B8826803F895
29 changed files with 1676 additions and 1401 deletions

View file

@ -1,125 +1,147 @@
#ifdef __linux__
// clang-format off
#include <dbus-cxx.h> // needs to be at top for Success/None
#include <cstring>
#include <fstream>
#include <sys/socket.h>
#include <sys/statvfs.h>
#include <sys/sysinfo.h>
#include <sys/utsname.h>
#include <unistd.h>
#include <wayland-client.h>
#include <xcb/xcb.h>
#include <cstring> // std::strlen
#include <dbus-cxx/callmessage.h> // DBus::CallMessage
#include <dbus-cxx/connection.h> // DBus::Connection
#include <dbus-cxx/dispatcher.h> // DBus::Dispatcher
#include <dbus-cxx/enums.h> // DBus::{DataType, BusType}
#include <dbus-cxx/error.h> // DBus::Error
#include <dbus-cxx/messageappenditerator.h> // DBus::MessageAppendIterator
#include <dbus-cxx/signature.h> // DBus::Signature
#include <dbus-cxx/standalonedispatcher.h> // DBus::StandaloneDispatcher
#include <dbus-cxx/variant.h> // DBus::Variant
#include <expected> // std::{unexpected, expected}
#include <format> // std::{format, format_to_n}
#include <fstream> // std::ifstream
#include <climits> // PATH_MAX
#include <limits> // std::numeric_limits
#include <map> // std::map (Map)
#include <memory> // std::shared_ptr (SharedPointer)
#include <string> // std::{getline, string (String)}
#include <string_view> // std::string_view (StringView)
#include <sys/socket.h> // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED
#include <sys/statvfs.h> // statvfs
#include <sys/sysinfo.h> // sysinfo
#include <sys/utsname.h> // utsname, uname
#include <unistd.h> // readlink
#include <utility> // std::move
#include "os.h"
#include "src/os/linux/display_guards.h"
#include "src/util/macros.h"
#include "src/util/types.h"
#include "src/core/util/logging.hpp"
#include "src/core/util/helpers.hpp"
#include "src/wrappers/wayland.hpp"
#include "src/wrappers/xcb.hpp"
#include "os.hpp"
#include "linux/pkg_count.hpp"
// clang-format on
using namespace std::string_view_literals;
using namespace util::types;
using util::error::DraconisError, util::error::DraconisErrorCode;
namespace {
fn GetX11WindowManager() -> Result<String, OsError> {
using os::linux::XcbReplyGuard;
using os::linux::XorgDisplayGuard;
fn GetX11WindowManager() -> Result<String, DraconisError> {
using namespace xcb;
const XorgDisplayGuard conn;
const DisplayGuard conn;
if (!conn)
if (const i32 err = xcb_connection_has_error(conn.get()); !conn || err != 0)
return Err(OsError(OsErrorCode::ApiUnavailable, [&] -> String {
switch (err) {
case 0: return "Connection object invalid, but no specific XCB error code";
case XCB_CONN_ERROR: return "Stream/Socket/Pipe Error";
case XCB_CONN_CLOSED_EXT_NOTSUPPORTED: return "Closed: Extension Not Supported";
case XCB_CONN_CLOSED_MEM_INSUFFICIENT: return "Closed: Insufficient Memory";
case XCB_CONN_CLOSED_REQ_LEN_EXCEED: return "Closed: Request Length Exceeded";
case XCB_CONN_CLOSED_PARSE_ERR: return "Closed: Display String Parse Error";
case XCB_CONN_CLOSED_INVALID_SCREEN: return "Closed: Invalid Screen";
case XCB_CONN_CLOSED_FDPASSING_FAILED: return "Closed: FD Passing Failed";
default: return std::format("Unknown Error Code ({})", err);
if (const i32 err = connection_has_error(conn.get()))
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, [&] -> String {
if (const Option<ConnError> connErr = getConnError(err)) {
switch (*connErr) {
case Generic: return "Stream/Socket/Pipe Error";
case ExtNotSupported: return "Extension Not Supported";
case MemInsufficient: return "Insufficient Memory";
case ReqLenExceed: return "Request Length Exceeded";
case ParseErr: return "Display String Parse Error";
case InvalidScreen: return "Invalid Screen";
case FdPassingFailed: return "FD Passing Failed";
default: return std::format("Unknown Error Code ({})", err);
}
}
return std::format("Unknown Error Code ({})", err);
}()));
fn internAtom = [&conn](const StringView name) -> Result<xcb_atom_t, OsError> {
const XcbReplyGuard<xcb_intern_atom_reply_t> reply(xcb_intern_atom_reply(
conn.get(), xcb_intern_atom(conn.get(), 0, static_cast<uint16_t>(name.size()), name.data()), nullptr
));
fn internAtom = [&conn](const StringView name) -> Result<atom_t, DraconisError> {
const ReplyGuard<intern_atom_reply_t> reply(
intern_atom_reply(conn.get(), intern_atom(conn.get(), 0, static_cast<u16>(name.size()), name.data()), nullptr)
);
if (!reply)
return Err(OsError(OsErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name)));
return Err(
DraconisError(DraconisErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name))
);
return reply->atom;
};
const Result<xcb_atom_t, OsError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
const Result<xcb_atom_t, OsError> wmNameAtom = internAtom("_NET_WM_NAME");
const Result<xcb_atom_t, OsError> utf8StringAtom = internAtom("UTF8_STRING");
const Result<atom_t, DraconisError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
const Result<atom_t, DraconisError> wmNameAtom = internAtom("_NET_WM_NAME");
const Result<atom_t, DraconisError> utf8StringAtom = internAtom("UTF8_STRING");
if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) {
if (!supportingWmCheckAtom)
ERROR_LOG("Failed to get _NET_SUPPORTING_WM_CHECK atom");
error_log("Failed to get _NET_SUPPORTING_WM_CHECK atom");
if (!wmNameAtom)
ERROR_LOG("Failed to get _NET_WM_NAME atom");
error_log("Failed to get _NET_WM_NAME atom");
if (!utf8StringAtom)
ERROR_LOG("Failed to get UTF8_STRING atom");
error_log("Failed to get UTF8_STRING atom");
return Err(OsError(OsErrorCode::PlatformSpecific, "Failed to get X11 atoms"));
return Err(DraconisError(DraconisErrorCode::PlatformSpecific, "Failed to get X11 atoms"));
}
const XcbReplyGuard<xcb_get_property_reply_t> wmWindowReply(xcb_get_property_reply(
const ReplyGuard<get_property_reply_t> wmWindowReply(get_property_reply(
conn.get(),
xcb_get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, XCB_ATOM_WINDOW, 0, 1),
get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1),
nullptr
));
if (!wmWindowReply || wmWindowReply->type != XCB_ATOM_WINDOW || wmWindowReply->format != 32 ||
xcb_get_property_value_length(wmWindowReply.get()) == 0)
return Err(OsError(OsErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property"));
if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 ||
get_property_value_length(wmWindowReply.get()) == 0)
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property"));
const xcb_window_t wmRootWindow = *static_cast<xcb_window_t*>(xcb_get_property_value(wmWindowReply.get()));
const window_t wmRootWindow = *static_cast<window_t*>(get_property_value(wmWindowReply.get()));
const XcbReplyGuard<xcb_get_property_reply_t> wmNameReply(xcb_get_property_reply(
conn.get(), xcb_get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr
const ReplyGuard<get_property_reply_t> wmNameReply(get_property_reply(
conn.get(), get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr
));
if (!wmNameReply || wmNameReply->type != *utf8StringAtom || xcb_get_property_value_length(wmNameReply.get()) == 0)
return Err(OsError(OsErrorCode::NotFound, "Failed to get _NET_WM_NAME property"));
if (!wmNameReply || wmNameReply->type != *utf8StringAtom || get_property_value_length(wmNameReply.get()) == 0)
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get _NET_WM_NAME property"));
const char* nameData = static_cast<const char*>(xcb_get_property_value(wmNameReply.get()));
const usize length = xcb_get_property_value_length(wmNameReply.get());
const char* nameData = static_cast<const char*>(get_property_value(wmNameReply.get()));
const usize length = get_property_value_length(wmNameReply.get());
return String(nameData, length);
}
fn GetWaylandCompositor() -> Result<String, OsError> {
using os::linux::WaylandDisplayGuard;
const WaylandDisplayGuard display;
fn GetWaylandCompositor() -> Result<String, DraconisError> {
const wl::DisplayGuard display;
if (!display)
return Err(OsError(OsErrorCode::NotFound, "Failed to connect to display (is Wayland running?)"));
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to connect to display (is Wayland running?)"));
const i32 fileDescriptor = display.fd();
if (fileDescriptor < 0)
return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor"));
return Err(DraconisError(DraconisErrorCode::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(OsError::withErrno("Failed to get socket credentials (SO_PEERCRED)"));
return Err(DraconisError::withErrno("Failed to get socket credentials (SO_PEERCRED)"));
Array<char, 128> 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(OsError(OsErrorCode::InternalError, "Failed to format /proc path (PID too large?)"));
return Err(DraconisError(DraconisErrorCode::InternalError, "Failed to format /proc path (PID too large?)"));
*out = '\0';
@ -130,7 +152,7 @@ namespace {
const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1);
if (bytesRead == -1)
return Err(OsError::withErrno(std::format("Failed to read link '{}'", exeLinkPath)));
return Err(DraconisError::withErrno(std::format("Failed to read link '{}'", exeLinkPath)));
exeRealPathBuf.at(bytesRead) = '\0';
@ -153,7 +175,7 @@ namespace {
compositorNameView = filenameView;
if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/")
return Err(OsError(OsErrorCode::NotFound, "Failed to get compositor name from path"));
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get compositor name from path"));
if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() &&
compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) {
@ -161,7 +183,7 @@ namespace {
compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length());
if (cleanedView.empty())
return Err(OsError(OsErrorCode::NotFound, "Compositor name invalid after heuristic"));
return Err(DraconisError(DraconisErrorCode::NotFound, "Compositor name invalid after heuristic"));
return String(cleanedView);
}
@ -169,7 +191,9 @@ namespace {
return String(compositorNameView);
}
fn GetMprisPlayers(const SharedPointer<DBus::Connection>& connection) -> Result<String, OsError> {
fn GetMprisPlayers(const SharedPointer<DBus::Connection>& connection) -> Result<String, DraconisError> {
using namespace std::string_view_literals;
try {
const SharedPointer<DBus::CallMessage> call =
DBus::CallMessage::create("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
@ -177,7 +201,7 @@ namespace {
const SharedPointer<DBus::Message> reply = connection->send_with_reply_blocking(call, 5);
if (!reply || !reply->is_valid())
return Err(OsError(OsErrorCode::Timeout, "Failed to get reply from ListNames"));
return Err(DraconisError(DraconisErrorCode::Timeout, "Failed to get reply from ListNames"));
Vec<String> allNamesStd;
DBus::MessageIterator reader(*reply);
@ -187,14 +211,14 @@ namespace {
if (StringView(name).contains("org.mpris.MediaPlayer2"sv))
return name;
return Err(OsError(OsErrorCode::NotFound, "No MPRIS players found"));
} catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) {
return Err(OsError(OsErrorCode::InternalError, e.what()));
return Err(DraconisError(DraconisErrorCode::NotFound, "No MPRIS players found"));
} catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) {
return Err(DraconisError(DraconisErrorCode::InternalError, e.what()));
}
}
fn GetMediaPlayerMetadata(const SharedPointer<DBus::Connection>& connection, const String& playerBusName)
-> Result<MediaInfo, OsError> {
-> Result<MediaInfo, DraconisError> {
try {
const SharedPointer<DBus::CallMessage> metadataCall =
DBus::CallMessage::create(playerBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
@ -204,7 +228,9 @@ namespace {
const SharedPointer<DBus::Message> metadataReply = connection->send_with_reply_blocking(metadataCall, 1000);
if (!metadataReply || !metadataReply->is_valid()) {
return Err(OsError(OsErrorCode::Timeout, "DBus Get Metadata call timed out or received invalid reply"));
return Err(
DraconisError(DraconisErrorCode::Timeout, "DBus Get Metadata call timed out or received invalid reply")
);
}
DBus::MessageIterator iter(*metadataReply);
@ -213,8 +239,8 @@ namespace {
// MPRIS metadata is variant containing a dict a{sv}
if (metadataVariant.type() != DBus::DataType::DICT_ENTRY && metadataVariant.type() != DBus::DataType::ARRAY) {
return Err(OsError(
OsErrorCode::ParseError,
return Err(DraconisError(
DraconisErrorCode::ParseError,
std::format(
"Inner metadata variant is not the expected type, expected dict/a{{sv}} but got '{}'",
metadataVariant.signature().str()
@ -224,16 +250,14 @@ namespace {
Map<String, DBus::Variant> metadata = metadataVariant.to_map<String, DBus::Variant>(); // Can throw
Option<String> title = None;
Option<String> artist = None;
Option<String> album = None;
Option<String> appName = None; // Try to get app name too
Option<String> title = None;
Option<String> artist = None;
if (auto titleIter = metadata.find("xesam:title");
if (const auto titleIter = metadata.find("xesam:title");
titleIter != metadata.end() && titleIter->second.type() == DBus::DataType::STRING)
title = titleIter->second.to_string();
if (auto artistIter = metadata.find("xesam:artist"); artistIter != metadata.end()) {
if (const auto artistIter = metadata.find("xesam:artist"); artistIter != metadata.end()) {
if (artistIter->second.type() == DBus::DataType::ARRAY) {
if (Vec<String> artists = artistIter->second.to_vector<String>(); !artists.empty())
artist = artists[0];
@ -242,47 +266,27 @@ namespace {
}
}
if (auto albumIter = metadata.find("xesam:album");
albumIter != metadata.end() && albumIter->second.type() == DBus::DataType::STRING)
album = albumIter->second.to_string();
try {
const SharedPointer<DBus::CallMessage> identityCall =
DBus::CallMessage::create(playerBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
*identityCall << "org.mpris.MediaPlayer2" << "Identity";
if (const SharedPointer<DBus::Message> identityReply = connection->send_with_reply_blocking(identityCall, 500);
identityReply && identityReply->is_valid()) {
DBus::MessageIterator identityIter(*identityReply);
DBus::Variant identityVariant;
identityIter >> identityVariant;
if (identityVariant.type() == DBus::DataType::STRING)
appName = identityVariant.to_string();
}
} catch (const DBus::Error& e) {
DEBUG_LOG("Failed to get player Identity property for {}: {}", playerBusName, e.what()); // Non-fatal
}
return MediaInfo(std::move(title), std::move(artist), std::move(album), std::move(appName));
} catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) {
return Err(
OsError(OsErrorCode::InternalError, std::format("Standard exception processing metadata: {}", e.what()))
);
return MediaInfo(std::move(title), std::move(artist));
} catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) {
return Err(DraconisError(
DraconisErrorCode::InternalError, std::format("Standard exception processing metadata: {}", e.what())
));
}
}
} // namespace
fn os::GetOSVersion() -> Result<String, OsError> {
fn os::GetOSVersion() -> Result<String, DraconisError> {
constexpr CStr path = "/etc/os-release";
std::ifstream file(path);
if (!file)
return Err(OsError(OsErrorCode::NotFound, std::format("Failed to open {}", path)));
return Err(DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open {}", path)));
String line;
constexpr StringView prefix = "PRETTY_NAME=";
while (getline(file, line)) {
while (std::getline(file, line)) {
if (StringView(line).starts_with(prefix)) {
String value = line.substr(prefix.size());
@ -291,36 +295,36 @@ fn os::GetOSVersion() -> Result<String, OsError> {
value = value.substr(1, value.length() - 2);
if (value.empty())
return Err(
OsError(OsErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path))
);
return Err(DraconisError(
DraconisErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path)
));
return value;
}
}
return Err(OsError(OsErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path)));
return Err(DraconisError(DraconisErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path)));
}
fn os::GetMemInfo() -> Result<u64, OsError> {
fn os::GetMemInfo() -> Result<u64, DraconisError> {
struct sysinfo info;
if (sysinfo(&info) != 0)
return Err(OsError::fromDBus("sysinfo call failed"));
return Err(DraconisError::fromDBus("sysinfo call failed"));
const u64 totalRam = info.totalram;
const u64 memUnit = info.mem_unit;
if (memUnit == 0)
return Err(OsError(OsErrorCode::InternalError, "sysinfo returned mem_unit of zero"));
return Err(DraconisError(DraconisErrorCode::InternalError, "sysinfo returned mem_unit of zero"));
if (totalRam > std::numeric_limits<u64>::max() / memUnit)
return Err(OsError(OsErrorCode::InternalError, "Potential overflow calculating total RAM"));
return Err(DraconisError(DraconisErrorCode::InternalError, "Potential overflow calculating total RAM"));
return info.totalram * info.mem_unit;
}
fn os::GetNowPlaying() -> Result<MediaInfo, NowPlayingError> {
fn os::GetNowPlaying() -> Result<MediaInfo, DraconisError> {
// Dispatcher must outlive the try-block because 'connection' depends on it later.
// ReSharper disable once CppTooWideScope, CppJoinDeclarationAndAssignment
SharedPointer<DBus::Dispatcher> dispatcher;
@ -330,22 +334,22 @@ fn os::GetNowPlaying() -> Result<MediaInfo, NowPlayingError> {
dispatcher = DBus::StandaloneDispatcher::create();
if (!dispatcher)
return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to create DBus dispatcher"));
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to create DBus dispatcher"));
connection = dispatcher->create_connection(DBus::BusType::SESSION);
if (!connection)
return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to connect to DBus session bus"));
} catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) {
return Err(OsError(OsErrorCode::InternalError, e.what()));
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to connect to DBus session bus"));
} catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) {
return Err(DraconisError(DraconisErrorCode::InternalError, e.what()));
}
Result<String, NowPlayingError> playerBusName = GetMprisPlayers(connection);
Result<String, DraconisError> playerBusName = GetMprisPlayers(connection);
if (!playerBusName)
return Err(playerBusName.error());
Result<MediaInfo, OsError> metadataResult = GetMediaPlayerMetadata(connection, *playerBusName);
Result<MediaInfo, DraconisError> metadataResult = GetMediaPlayerMetadata(connection, *playerBusName);
if (!metadataResult)
return Err(metadataResult.error());
@ -354,37 +358,39 @@ fn os::GetNowPlaying() -> Result<MediaInfo, NowPlayingError> {
}
fn os::GetWindowManager() -> Option<String> {
if (Result<String, OsError> waylandResult = GetWaylandCompositor())
if (Result<String, DraconisError> waylandResult = GetWaylandCompositor())
return *waylandResult;
else
DEBUG_LOG("Could not detect Wayland compositor: {}", waylandResult.error().message);
debug_log("Could not detect Wayland compositor: {}", waylandResult.error().message);
if (Result<String, OsError> x11Result = GetX11WindowManager())
if (Result<String, DraconisError> x11Result = GetX11WindowManager())
return *x11Result;
else
DEBUG_LOG("Could not detect X11 window manager: {}", x11Result.error().message);
debug_log("Could not detect X11 window manager: {}", x11Result.error().message);
return None;
}
fn os::GetDesktopEnvironment() -> Option<String> {
return GetEnv("XDG_CURRENT_DESKTOP")
return util::helpers::GetEnv("XDG_CURRENT_DESKTOP")
.transform([](const String& xdgDesktop) -> String {
if (const usize colon = xdgDesktop.find(':'); colon != String::npos)
return xdgDesktop.substr(0, colon);
return xdgDesktop;
})
.or_else([](const EnvError&) -> Result<String, EnvError> { return GetEnv("DESKTOP_SESSION"); })
.or_else([](const DraconisError&) -> Result<String, DraconisError> {
return util::helpers::GetEnv("DESKTOP_SESSION");
})
.transform([](const String& finalValue) -> Option<String> {
DEBUG_LOG("Found desktop environment: {}", finalValue);
debug_log("Found desktop environment: {}", finalValue);
return finalValue;
})
.value_or(None);
}
fn os::GetShell() -> Option<String> {
if (const Result<String, EnvError> shellPath = GetEnv("SHELL")) {
if (const Result<String, DraconisError> shellPath = util::helpers::GetEnv("SHELL")) {
// clang-format off
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
{ "bash", "Bash" },
@ -405,56 +411,61 @@ fn os::GetShell() -> Option<String> {
return None;
}
fn os::GetHost() -> Result<String, OsError> {
fn os::GetHost() -> Result<String, DraconisError> {
constexpr CStr primaryPath = "/sys/class/dmi/id/product_family";
constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name";
fn readFirstLine = [&](const String& path) -> Result<String, OsError> {
fn readFirstLine = [&](const String& path) -> Result<String, DraconisError> {
std::ifstream file(path);
String line;
if (!file)
return Err(OsError(OsErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path)));
return Err(
DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path))
);
if (!getline(file, line))
return Err(OsError(OsErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)));
if (!std::getline(file, line))
return Err(
DraconisError(DraconisErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path))
);
return line;
};
return readFirstLine(primaryPath).or_else([&](const OsError& primaryError) -> Result<String, OsError> {
return readFirstLine(fallbackPath).or_else([&](const OsError& fallbackError) -> Result<String, OsError> {
return Err(OsError(
OsErrorCode::InternalError,
std::format(
"Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}",
primaryPath,
primaryError.message,
fallbackPath,
fallbackError.message
)
));
});
return readFirstLine(primaryPath).or_else([&](const DraconisError& primaryError) -> Result<String, DraconisError> {
return readFirstLine(fallbackPath)
.or_else([&](const DraconisError& fallbackError) -> Result<String, DraconisError> {
return Err(DraconisError(
DraconisErrorCode::InternalError,
std::format(
"Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}",
primaryPath,
primaryError.message,
fallbackPath,
fallbackError.message
)
));
});
});
}
fn os::GetKernelVersion() -> Result<String, OsError> {
fn os::GetKernelVersion() -> Result<String, DraconisError> {
utsname uts;
if (uname(&uts) == -1)
return Err(OsError::withErrno("uname call failed"));
return Err(DraconisError::withErrno("uname call failed"));
if (strlen(uts.release) == 0)
return Err(OsError(OsErrorCode::ParseError, "uname returned null kernel release"));
if (std::strlen(uts.release) == 0)
return Err(DraconisError(DraconisErrorCode::ParseError, "uname returned null kernel release"));
return uts.release;
}
fn os::GetDiskUsage() -> Result<DiskSpace, OsError> {
fn os::GetDiskUsage() -> Result<DiskSpace, DraconisError> {
struct statvfs stat;
if (statvfs("/", &stat) == -1)
return Err(OsError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)")));
return Err(DraconisError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)")));
return DiskSpace {
.used_bytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize),
@ -462,4 +473,6 @@ fn os::GetDiskUsage() -> Result<DiskSpace, OsError> {
};
}
fn os::GetPackageCount() -> Result<u64, DraconisError> { return linux::GetNixPackageCount(); }
#endif // __linux__

View file

@ -1,70 +0,0 @@
#ifdef __linux__
#include <utility>
#include "display_guards.h"
#include "src/util/macros.h"
namespace os::linux {
XorgDisplayGuard::XorgDisplayGuard(const CStr name) : m_Connection(xcb_connect(name, nullptr)) {}
XorgDisplayGuard::~XorgDisplayGuard() {
if (m_Connection)
xcb_disconnect(m_Connection);
}
XorgDisplayGuard::XorgDisplayGuard(XorgDisplayGuard&& other) noexcept
: m_Connection(std::exchange(other.m_Connection, nullptr)) {}
fn XorgDisplayGuard::operator=(XorgDisplayGuard&& other) noexcept -> XorgDisplayGuard& {
if (this != &other) {
if (m_Connection)
xcb_disconnect(m_Connection);
m_Connection = std::exchange(other.m_Connection, nullptr);
}
return *this;
}
XorgDisplayGuard::operator bool() const { return m_Connection && !xcb_connection_has_error(m_Connection); }
fn XorgDisplayGuard::get() const -> xcb_connection_t* { return m_Connection; }
fn XorgDisplayGuard::setup() const -> const xcb_setup_t* {
return m_Connection ? xcb_get_setup(m_Connection) : nullptr;
}
fn XorgDisplayGuard::rootScreen() const -> xcb_screen_t* {
const xcb_setup_t* setup = this->setup();
return setup ? xcb_setup_roots_iterator(setup).data : nullptr;
}
WaylandDisplayGuard::WaylandDisplayGuard() : m_Display(wl_display_connect(nullptr)) {}
WaylandDisplayGuard::~WaylandDisplayGuard() {
if (m_Display)
wl_display_disconnect(m_Display);
}
WaylandDisplayGuard::WaylandDisplayGuard(WaylandDisplayGuard&& other) noexcept
: m_Display(std::exchange(other.m_Display, nullptr)) {}
fn WaylandDisplayGuard::operator=(WaylandDisplayGuard&& other) noexcept -> WaylandDisplayGuard& {
if (this != &other) {
if (m_Display)
wl_display_disconnect(m_Display);
m_Display = std::exchange(other.m_Display, nullptr);
}
return *this;
}
WaylandDisplayGuard::operator bool() const { return m_Display != nullptr; }
fn WaylandDisplayGuard::get() const -> wl_display* { return m_Display; }
fn WaylandDisplayGuard::fd() const -> i32 { return m_Display ? wl_display_get_fd(m_Display) : -1; }
}
#endif

View file

@ -1,110 +0,0 @@
#pragma once
#ifdef __linux__
#include <wayland-client.h>
#include <xcb/xcb.h>
#include "src/util/macros.h"
namespace os::linux {
/**
* RAII wrapper for X11 Display connections
* Automatically handles resource acquisition and cleanup
*/
class XorgDisplayGuard {
xcb_connection_t* m_Connection = nullptr;
public:
/**
* Opens an XCB connection
* @param name Display name (nullptr for default)
*/
explicit XorgDisplayGuard(CStr name = nullptr);
~XorgDisplayGuard();
// Non-copyable
XorgDisplayGuard(const XorgDisplayGuard&) = delete;
fn operator=(const XorgDisplayGuard&)->XorgDisplayGuard& = delete;
// Movable
XorgDisplayGuard(XorgDisplayGuard&& other) noexcept;
fn operator=(XorgDisplayGuard&& other) noexcept -> XorgDisplayGuard&;
[[nodiscard]] explicit operator bool() const;
[[nodiscard]] fn get() const -> xcb_connection_t*;
[[nodiscard]] fn setup() const -> const xcb_setup_t*;
[[nodiscard]] fn rootScreen() const -> xcb_screen_t*;
};
/**
* RAII wrapper for XCB replies
* Handles automatic cleanup of various XCB reply objects
*/
template <typename T>
class XcbReplyGuard {
T* m_Reply = nullptr;
public:
XcbReplyGuard() = default;
explicit XcbReplyGuard(T* reply) : m_Reply(reply) {}
~XcbReplyGuard() {
if (m_Reply)
free(m_Reply);
}
// Non-copyable
XcbReplyGuard(const XcbReplyGuard&) = delete;
fn operator=(const XcbReplyGuard&)->XcbReplyGuard& = delete;
// Movable
XcbReplyGuard(XcbReplyGuard&& other) noexcept : m_Reply(std::exchange(other.m_Reply, nullptr)) {}
fn operator=(XcbReplyGuard&& other) noexcept -> XcbReplyGuard& {
if (this != &other) {
if (m_Reply)
free(m_Reply);
m_Reply = std::exchange(other.m_Reply, nullptr);
}
return *this;
}
[[nodiscard]] explicit operator bool() const { return m_Reply != nullptr; }
[[nodiscard]] fn get() const -> T* { return m_Reply; }
[[nodiscard]] fn operator->() const->T* { return m_Reply; }
[[nodiscard]] fn operator*() const->T& { return *m_Reply; }
};
/**
* RAII wrapper for Wayland display connections
* Automatically handles resource acquisition and cleanup
*/
class WaylandDisplayGuard {
wl_display* m_Display;
public:
/**
* Opens a Wayland display connection
*/
WaylandDisplayGuard();
~WaylandDisplayGuard();
// Non-copyable
WaylandDisplayGuard(const WaylandDisplayGuard&) = delete;
fn operator=(const WaylandDisplayGuard&)->WaylandDisplayGuard& = delete;
// Movable
WaylandDisplayGuard(WaylandDisplayGuard&& other) noexcept;
fn operator=(WaylandDisplayGuard&& other) noexcept -> WaylandDisplayGuard&;
[[nodiscard]] explicit operator bool() const;
[[nodiscard]] fn get() const -> wl_display*;
[[nodiscard]] fn fd() const -> i32;
};
}
#endif

View file

@ -1,3 +1,4 @@
#include "src/util/macros.h"
#include "src/core/util/defs.hpp"
#include "src/core/util/types.hpp"
extern "C" fn issetugid() -> usize { return 0; } // NOLINT
extern "C" fn issetugid() -> util::types::usize { return 0; } // NOLINT

View file

@ -1 +1,217 @@
#include "src/os/linux/pkg_count.h"
#include "src/os/linux/pkg_count.hpp"
#include <SQLiteCpp/SQLiteCpp.h>
#include <fstream>
#include <glaze/core/common.hpp>
#include <glaze/core/read.hpp>
#include <glaze/core/reflect.hpp>
#include <glaze/json/write.hpp>
#include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp"
using util::error::DraconisError, util::error::DraconisErrorCode;
using util::types::u64, util::types::i64, util::types::Result, util::types::Err, util::types::String,
util::types::Exception;
namespace {
namespace fs = std::filesystem;
using namespace std::chrono;
struct NixPkgCacheData {
u64 count {};
system_clock::time_point timestamp;
// NOLINTBEGIN(readability-identifier-naming) - Needs to specifically use `glaze`
struct [[maybe_unused]] glaze {
using T = NixPkgCacheData;
static constexpr auto value = glz::object("count", &T::count, "timestamp", [](auto& self) -> auto& {
thread_local auto epoch_seconds = duration_cast<seconds>(self.timestamp.time_since_epoch()).count();
return epoch_seconds;
});
};
// NOLINTEND(readability-identifier-naming)
};
fn GetPkgCountCachePath() -> Result<fs::path, DraconisError> {
std::error_code errc;
const fs::path cacheDir = fs::temp_directory_path(errc);
if (errc) {
return Err(DraconisError(DraconisErrorCode::IoError, "Failed to get temp directory: " + errc.message()));
}
return cacheDir / "nix_pkg_count_cache.json";
}
fn ReadPkgCountCache() -> Result<NixPkgCacheData, DraconisError> {
auto cachePathResult = GetPkgCountCachePath();
if (!cachePathResult) {
return Err(cachePathResult.error());
}
const fs::path& cachePath = *cachePathResult;
if (!fs::exists(cachePath)) {
return Err(DraconisError(DraconisErrorCode::NotFound, "Cache file not found: " + cachePath.string()));
}
std::ifstream ifs(cachePath, std::ios::binary);
if (!ifs.is_open()) {
return Err(
DraconisError(DraconisErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string())
);
}
debug_log("Reading Nix package count from cache file: {}", cachePath.string());
try {
const String content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
NixPkgCacheData result;
glz::context ctx {};
if (auto glazeResult = glz::read<glz::opts { .error_on_unknown_keys = false }>(result, content, ctx);
glazeResult.ec != glz::error_code::none) {
return Err(DraconisError(
DraconisErrorCode::ParseError,
std::format("JSON parse error reading cache: {}", glz::format_error(glazeResult, content))
));
}
if (size_t tsPos = content.find("\"timestamp\""); tsPos != String::npos) {
size_t colonPos = content.find(':', tsPos);
if (size_t valueStart = content.find_first_of("0123456789", colonPos); valueStart != String::npos) {
long long timestampSeconds = 0;
char* endPtr = nullptr;
timestampSeconds = std::strtoll(content.c_str() + valueStart, &endPtr, 10);
result.timestamp = system_clock::time_point(seconds(timestampSeconds));
} else {
return Err(DraconisError(DraconisErrorCode::ParseError, "Could not parse timestamp value from cache JSON."));
}
} else {
return Err(DraconisError(DraconisErrorCode::ParseError, "Timestamp field not found in cache JSON."));
}
debug_log("Successfully read package count from cache file.");
return result;
} catch (const Exception& e) {
return Err(
DraconisError(DraconisErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what()))
);
}
}
fn WritePkgCountCache(const NixPkgCacheData& data) -> Result<void, DraconisError> {
auto cachePathResult = GetPkgCountCachePath();
if (!cachePathResult) {
return Err(cachePathResult.error());
}
const fs::path& cachePath = *cachePathResult;
fs::path tempPath = cachePath;
tempPath += ".tmp";
debug_log("Writing Nix package count to cache file: {}", cachePath.string());
try {
{
std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) {
return Err(DraconisError(DraconisErrorCode::IoError, "Failed to open temp cache file: " + tempPath.string()));
}
String jsonStr;
NixPkgCacheData mutableData = data;
if (auto glazeErr = glz::write_json(mutableData, jsonStr); glazeErr.ec != glz::error_code::none) {
return Err(DraconisError(
DraconisErrorCode::ParseError,
std::format("JSON serialization error writing cache: {}", glz::format_error(glazeErr, jsonStr))
));
}
ofs << jsonStr;
if (!ofs) {
return Err(DraconisError(DraconisErrorCode::IoError, "Failed to write to temp cache file"));
}
}
std::error_code errc;
fs::rename(tempPath, cachePath, errc);
if (errc) {
fs::remove(tempPath);
return Err(DraconisError(
DraconisErrorCode::IoError,
std::format("Failed to replace cache file '{}': {}", cachePath.string(), errc.message())
));
}
debug_log("Successfully wrote package count to cache file.");
return {};
} catch (const Exception& e) {
fs::remove(tempPath);
return Err(DraconisError(
DraconisErrorCode::InternalError, std::format("File operation error writing package count cache: {}", e.what())
));
}
}
} // namespace
fn os::linux::GetNixPackageCount() -> Result<u64, DraconisError> {
const fs::path nixDbPath = "/nix/var/nix/db/db.sqlite";
if (Result<NixPkgCacheData, DraconisError> cachedDataResult = ReadPkgCountCache()) {
const auto& [count, timestamp] = *cachedDataResult;
std::error_code errc;
std::filesystem::file_time_type dbModTime = fs::last_write_time(nixDbPath, errc);
if (errc) {
warn_log("Could not get modification time for '{}': {}. Invalidating cache.", nixDbPath.string(), errc.message());
} else {
if (timestamp.time_since_epoch() >= dbModTime.time_since_epoch()) {
debug_log(
"Using valid Nix package count cache (DB file unchanged since {}). Count: {}",
std::format("{:%F %T %Z}", floor<seconds>(timestamp)),
count
);
return count;
}
debug_log("Nix package count cache stale (DB file modified).");
}
} else {
if (cachedDataResult.error().code != DraconisErrorCode::NotFound)
debug_at(cachedDataResult.error());
debug_log("Nix package count cache not found or unreadable.");
}
debug_log("Fetching fresh Nix package count from database: {}", nixDbPath.string());
u64 count = 0;
try {
const SQLite::Database database("/nix/var/nix/db/db.sqlite", SQLite::OPEN_READONLY);
if (SQLite::Statement query(database, "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL");
query.executeStep()) {
const i64 countInt64 = query.getColumn(0).getInt64();
if (countInt64 < 0)
return Err(DraconisError(DraconisErrorCode::ParseError, "Negative count returned by Nix DB COUNT(*) query."));
count = static_cast<u64>(countInt64);
} else {
return Err(DraconisError(DraconisErrorCode::ParseError, "No rows returned by Nix DB COUNT(*) query."));
}
} catch (const SQLite::Exception& e) {
return Err(DraconisError(
DraconisErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing Nix DB: {}", e.what())
));
} catch (const Exception& e) { return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); } catch (...) {
return Err(DraconisError(DraconisErrorCode::Other, "Unknown error occurred accessing Nix DB"));
}
const NixPkgCacheData dataToCache = { .count = count, .timestamp = system_clock::now() };
if (Result<void, DraconisError> writeResult = WritePkgCountCache(dataToCache); !writeResult) {
warn_at(writeResult.error());
warn_log("Failed to write Nix package count to cache.");
}
debug_log("Fetched fresh Nix package count: {}", count);
return count;
}

View file

@ -1,36 +0,0 @@
#pragma once
#include "src/util/macros.h"
// Get package count from dpkg (Debian/Ubuntu)
fn GetDpkgPackageCount() -> Option<usize>;
// Get package count from RPM (Red Hat/Fedora/CentOS)
fn GetRpmPackageCount() -> Option<usize>;
// Get package count from pacman (Arch Linux)
fn GetPacmanPackageCount() -> Option<usize>;
// Get package count from Portage (Gentoo)
fn GetPortagePackageCount() -> Option<usize>;
// Get package count from zypper (openSUSE)
fn GetZypperPackageCount() -> Option<usize>;
// Get package count from apk (Alpine)
fn GetApkPackageCount() -> Option<usize>;
// Get package count from nix
fn GetNixPackageCount() -> Option<usize>;
// Get package count from flatpak
fn GetFlatpakPackageCount() -> Option<usize>;
// Get package count from snap
fn GetSnapPackageCount() -> Option<usize>;
// Get package count from AppImage
fn GetAppimagePackageCount() -> Option<usize>;
// Get total package count from all available package managers
fn GetTotalPackageCount() -> Option<usize>;

View file

@ -0,0 +1,43 @@
#pragma once
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/types.hpp"
namespace os::linux {
using util::error::DraconisError;
using util::types::Result, util::types::u64;
// Get package count from dpkg (Debian/Ubuntu)
fn GetDpkgPackageCount() -> Result<u64, DraconisError>;
// Get package count from RPM (Red Hat/Fedora/CentOS)
fn GetRpmPackageCount() -> Result<u64, DraconisError>;
// Get package count from pacman (Arch Linux)
fn GetPacmanPackageCount() -> Result<u64, DraconisError>;
// Get package count from Portage (Gentoo)
fn GetPortagePackageCount() -> Result<u64, DraconisError>;
// Get package count from zypper (openSUSE)
fn GetZypperPackageCount() -> Result<u64, DraconisError>;
// Get package count from apk (Alpine)
fn GetApkPackageCount() -> Result<u64, DraconisError>;
// Get package count from nix
fn GetNixPackageCount() -> Result<u64, DraconisError>;
// Get package count from flatpak
fn GetFlatpakPackageCount() -> Result<u64, DraconisError>;
// Get package count from snap
fn GetSnapPackageCount() -> Result<u64, DraconisError>;
// Get package count from AppImage
fn GetAppimagePackageCount() -> Result<u64, DraconisError>;
// Get total package count from all available package managers
fn GetTotalPackageCount() -> Result<u64, DraconisError>;
} // namespace os::linux

View file

@ -1,7 +1,8 @@
#pragma once
#include "../util/macros.h"
#include "../util/types.h"
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/types.hpp"
/**
* @namespace os
@ -13,33 +14,35 @@
* (found in linux.cpp, windows.cpp, macos.cpp).
*/
namespace os {
using util::error::DraconisError;
using util::types::u64, util::types::String, util::types::Option, util::types::Result, util::types::MediaInfo,
util::types::DiskSpace;
/**
* @brief Get the total amount of physical RAM installed in the system.
* @return A Result containing the total RAM in bytes (u64) on success,
* or an OsError on failure.
*/
fn GetMemInfo() -> Result<u64, OsError>;
fn GetMemInfo() -> Result<u64, DraconisError>;
/**
* @brief Gets structured metadata about the currently playing media.
* @return A Result containing the media information (MediaInfo struct) on success,
* or a NowPlayingError (indicating player state or system error) on failure.
*/
fn GetNowPlaying() -> Result<MediaInfo, NowPlayingError>;
fn GetNowPlaying() -> Result<MediaInfo, DraconisError>;
/**
* @brief Gets the "pretty" name of the operating system.
* @details Examples: "Ubuntu 24.04.2 LTS", "Windows 11 Pro 24H2", "macOS 15 Sequoia".
* @return A Result containing the OS version String on success,
* or an OsError on failure.
* @return A Result containing the OS version String on success, or an OsError on failure.
*/
fn GetOSVersion() -> Result<String, OsError>;
fn GetOSVersion() -> Result<String, DraconisError>;
/**
* @brief Attempts to retrieve the desktop environment name.
* @details This is most relevant on Linux. May check environment variables (XDG_CURRENT_DESKTOP),
* session files, or running processes. On Windows/macOS, it might return a
* UI theme identifier (e.g., "Fluent", "Aqua") or None.
* @details This is most relevant on Linux. May check environment variables (XDG_CURRENT_DESKTOP), session files,
* or running processes. On Windows/macOS, it might return a UI theme identifier (e.g., "Fluent", "Aqua") or None.
* @return An Option containing the detected DE name String, or None if detection fails or is not applicable.
*/
fn GetDesktopEnvironment() -> Option<String>;
@ -56,8 +59,8 @@ namespace os {
* @brief Attempts to detect the current user shell name.
* @details Checks the SHELL environment variable on Linux/macOS. On Windows, inspects the process tree
* to identify known shells like PowerShell, Cmd, or MSYS2 shells (Bash, Zsh).
* @return An Option containing the detected shell name (e.g., "Bash", "Zsh", "PowerShell", "Fish"), or None if
* detection fails.
* @return An Option containing the detected shell name (e.g., "Bash", "Zsh", "PowerShell", "Fish"),
* or None if detection fails.
*/
fn GetShell() -> Option<String>;
@ -68,7 +71,7 @@ namespace os {
* @return A Result containing the host/product identifier String on success,
* or an OsError on failure (e.g., permission reading DMI/registry, API error).
*/
fn GetHost() -> Result<String, OsError>;
fn GetHost() -> Result<String, DraconisError>;
/**
* @brief Gets the operating system's kernel version string.
@ -77,7 +80,7 @@ namespace os {
* @return A Result containing the kernel version String on success,
* or an OsError on failure.
*/
fn GetKernelVersion() -> Result<String, OsError>;
fn GetKernelVersion() -> Result<String, DraconisError>;
/**
* @brief Gets the number of installed packages (Platform-specific).
@ -86,7 +89,7 @@ namespace os {
* or an OsError on failure (e.g., permission errors, command not found)
* or if not supported (OsErrorCode::NotSupported).
*/
fn GetPackageCount() -> Result<u64, OsError>; // Note: Returns OsError{OsErrorCode::NotSupported} on Win/Mac likely
fn GetPackageCount() -> Result<u64, DraconisError>;
/**
* @brief Gets the disk usage for the primary/root filesystem.
@ -94,5 +97,5 @@ namespace os {
* @return A Result containing the DiskSpace struct (used/total bytes) on success,
* or an OsError on failure (e.g., filesystem not found, permission error).
*/
fn GetDiskUsage() -> Result<DiskSpace, OsError>;
fn GetDiskUsage() -> Result<DiskSpace, DraconisError>;
} // namespace os