This commit is contained in:
Mars 2025-05-01 02:06:05 -04:00
parent 3ad961a571
commit 3b16fee5f4
Signed by: pupbrained
GPG key ID: 0FF5B8826803F895
28 changed files with 1242 additions and 1047 deletions

View file

@ -1,22 +1,28 @@
#ifdef __linux__
// clang-format off
#include <cstring> // std::strlen
#include <dbus/dbus.h> // DBus::{DBusConnection, DBusMessage, DBusMessageIter, etc.}
#include <expected> // std::{unexpected, expected}
#include <format> // std::{format, format_to_n}
#include <fstream> // std::ifstream
#include <limits> // std::numeric_limits
#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 <cstring> // std::strlen
#include <dbus/dbus-shared.h> // DBUS_BUS_SESSION
#include <dbus/dbus-protocol.h> // DBUS_TYPE_*
#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 <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 "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/helpers.hpp"
#include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp"
#include "src/wrappers/dbus.hpp"
#include "src/wrappers/wayland.hpp"
#include "src/wrappers/xcb.hpp"
@ -26,17 +32,18 @@
// clang-format on
using namespace util::types;
using util::error::DraconisError, util::error::DraconisErrorCode;
using util::error::DracError, util::error::DracErrorCode;
using util::helpers::GetEnv;
namespace {
fn GetX11WindowManager() -> Result<String, DraconisError> {
fn GetX11WindowManager() -> Result<String, DracError> {
using namespace xcb;
const DisplayGuard conn;
if (!conn)
if (const i32 err = connection_has_error(conn.get()))
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, [&] -> String {
return Err(DracError(DracErrorCode::ApiUnavailable, [&] -> String {
if (const Option<ConnError> connErr = getConnError(err)) {
switch (*connErr) {
case Generic: return "Stream/Socket/Pipe Error";
@ -53,22 +60,22 @@ namespace {
return std::format("Unknown Error Code ({})", err);
}()));
fn internAtom = [&conn](const StringView name) -> Result<atom_t, DraconisError> {
fn internAtom = [&conn](const StringView name) -> Result<atom_t, DracError> {
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(
DraconisError(DraconisErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name))
DracError(DracErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name))
);
return reply->atom;
};
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");
const Result<atom_t, DracError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
const Result<atom_t, DracError> wmNameAtom = internAtom("_NET_WM_NAME");
const Result<atom_t, DracError> utf8StringAtom = internAtom("UTF8_STRING");
if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) {
if (!supportingWmCheckAtom)
@ -80,7 +87,7 @@ namespace {
if (!utf8StringAtom)
error_log("Failed to get UTF8_STRING atom");
return Err(DraconisError(DraconisErrorCode::PlatformSpecific, "Failed to get X11 atoms"));
return Err(DracError(DracErrorCode::PlatformSpecific, "Failed to get X11 atoms"));
}
const ReplyGuard<get_property_reply_t> wmWindowReply(get_property_reply(
@ -91,7 +98,7 @@ namespace {
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"));
return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property"));
const window_t wmRootWindow = *static_cast<window_t*>(get_property_value(wmWindowReply.get()));
@ -100,7 +107,7 @@ namespace {
));
if (!wmNameReply || wmNameReply->type != *utf8StringAtom || get_property_value_length(wmNameReply.get()) == 0)
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get _NET_WM_NAME property"));
return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_WM_NAME property"));
const char* nameData = static_cast<const char*>(get_property_value(wmNameReply.get()));
const usize length = get_property_value_length(wmNameReply.get());
@ -108,39 +115,39 @@ namespace {
return String(nameData, length);
}
fn GetWaylandCompositor() -> Result<String, DraconisError> {
fn GetWaylandCompositor() -> Result<String, DracError> {
const wl::DisplayGuard display;
if (!display)
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to connect to display (is Wayland running?)"));
return Err(DracError(DracErrorCode::NotFound, "Failed to connect to display (is Wayland running?)"));
const i32 fileDescriptor = display.fd();
if (fileDescriptor < 0)
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor"));
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(DraconisError::withErrno("Failed to get socket credentials (SO_PEERCRED)"));
return Err(DracError::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(DraconisError(DraconisErrorCode::InternalError, "Failed to format /proc path (PID too large?)"));
return Err(DracError(DracErrorCode::InternalError, "Failed to format /proc path (PID too large?)"));
*out = '\0';
const char* exeLinkPath = exeLinkPathBuf.data();
Array<char, PATH_MAX> exeRealPathBuf;
Array<char, PATH_MAX> exeRealPathBuf; // NOLINT(misc-include-cleaner) - PATH_MAX is in <climits>
const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1);
if (bytesRead == -1)
return Err(DraconisError::withErrno(std::format("Failed to read link '{}'", exeLinkPath)));
return Err(DracError::withErrno(std::format("Failed to read link '{}'", exeLinkPath)));
exeRealPathBuf.at(bytesRead) = '\0';
@ -163,7 +170,7 @@ namespace {
compositorNameView = filenameView;
if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/")
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get compositor name from path"));
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)) {
@ -171,7 +178,7 @@ namespace {
compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length());
if (cleanedView.empty())
return Err(DraconisError(DraconisErrorCode::NotFound, "Compositor name invalid after heuristic"));
return Err(DracError(DracErrorCode::NotFound, "Compositor name invalid after heuristic"));
return String(cleanedView);
}
@ -181,13 +188,13 @@ namespace {
} // namespace
namespace os {
fn GetOSVersion() -> Result<String, DraconisError> {
fn GetOSVersion() -> Result<String, DracError> {
constexpr CStr path = "/etc/os-release";
std::ifstream file(path);
if (!file)
return Err(DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open {}", path)));
return Err(DracError(DracErrorCode::NotFound, std::format("Failed to open {}", path)));
String line;
constexpr StringView prefix = "PRETTY_NAME=";
@ -201,206 +208,197 @@ namespace os {
value = value.substr(1, value.length() - 2);
if (value.empty())
return Err(DraconisError(
DraconisErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path)
));
return Err(
DracError(DracErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path))
);
return value;
}
}
return Err(DraconisError(DraconisErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path)));
return Err(DracError(DracErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path)));
}
fn GetMemInfo() -> Result<u64, DraconisError> {
fn GetMemInfo() -> Result<u64, DracError> {
struct sysinfo info;
if (sysinfo(&info) != 0)
return Err(DraconisError::withErrno("sysinfo call failed"));
return Err(DracError::withErrno("sysinfo call failed"));
const u64 totalRam = info.totalram;
const u64 memUnit = info.mem_unit;
if (memUnit == 0)
return Err(DraconisError(DraconisErrorCode::InternalError, "sysinfo returned mem_unit of zero"));
return Err(DracError(DracErrorCode::InternalError, "sysinfo returned mem_unit of zero"));
if (totalRam > std::numeric_limits<u64>::max() / memUnit)
return Err(DraconisError(DraconisErrorCode::InternalError, "Potential overflow calculating total RAM"));
return Err(DracError(DracErrorCode::InternalError, "Potential overflow calculating total RAM"));
return info.totalram * info.mem_unit;
}
fn GetNowPlaying() -> Result<MediaInfo, DraconisError> {
Result<dbus::ConnectionGuard, DraconisError> connectionResult = dbus::BusGet(DBUS_BUS_SESSION);
fn GetNowPlaying() -> Result<MediaInfo, DracError> {
using namespace dbus;
Result<Connection, DracError> connectionResult = Connection::busGet(DBUS_BUS_SESSION);
if (!connectionResult)
return Err(connectionResult.error());
dbus::ConnectionGuard& connection = *connectionResult;
const Connection& connection = *connectionResult;
Option<String> activePlayer = None;
{
Result<dbus::MessageGuard, DraconisError> listNamesResult = dbus::MessageNewMethodCall(
"org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"
);
Result<Message, DracError> listNamesResult =
Message::newMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
if (!listNamesResult)
return Err(listNamesResult.error());
dbus::MessageGuard& listNames = *listNamesResult;
Result<dbus::MessageGuard, DraconisError> listNamesReplyResult =
dbus::ConnectionSendWithReplyAndBlock(connection, listNames, 100);
Result<Message, DracError> listNamesReplyResult = connection.sendWithReplyAndBlock(*listNamesResult, 100);
if (!listNamesReplyResult)
return Err(listNamesReplyResult.error());
dbus::MessageGuard& listNamesReply = *listNamesReplyResult;
MessageIter iter = listNamesReplyResult->iterInit();
if (!iter.isValid() || iter.getArgType() != DBUS_TYPE_ARRAY)
return Err(DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Expected array"));
dbus::MessageIter iter;
MessageIter subIter = iter.recurse();
if (!subIter.isValid())
return Err(
DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Could not recurse into array")
);
if (dbus::MessageIterInit(listNamesReply, &iter) && dbus::MessageIterGetArgType(&iter) == DBUS_TYPE_ARRAY) {
dbus::MessageIter subIter;
dbus::MessageIterRecurse(&iter, &subIter);
while (dbus::MessageIterGetArgType(&subIter) != DBUS_TYPE_INVALID) {
if (Option<String> name = dbus::MessageIterGetString(&subIter))
if (name->find("org.mpris.MediaPlayer2") != String::npos) {
activePlayer = std::move(*name);
break;
}
dbus::MessageIterNext(&subIter);
}
} else {
return Err(DraconisError(DraconisErrorCode::ParseError, "Invalid DBus ListNames reply format"));
while (subIter.getArgType() != DBUS_TYPE_INVALID) {
if (Option<String> name = subIter.getString())
if (name->starts_with("org.mpris.MediaPlayer2.")) {
activePlayer = std::move(*name);
break;
}
if (!subIter.next())
break;
}
}
if (!activePlayer)
return Err(DraconisError(DraconisErrorCode::NotFound, "No active MPRIS players found"));
return Err(DracError(DracErrorCode::NotFound, "No active MPRIS players found"));
Result<dbus::MessageGuard, DraconisError> msgResult = dbus::MessageNewMethodCall(
Result<Message, DracError> msgResult = Message::newMethodCall(
activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"
);
if (!msgResult)
return Err(msgResult.error());
dbus::MessageGuard& msg = *msgResult;
Message& msg = *msgResult;
if (!dbus::MessageAppendArgs(
msg, DBUS_TYPE_STRING, "org.mpris.MediaPlayer2.Player", DBUS_TYPE_STRING, "Metadata", DBUS_TYPE_INVALID
))
return Err(DraconisError(DraconisErrorCode::InternalError, "Failed to append arguments to DBus message"));
if (!msg.appendArgs("org.mpris.MediaPlayer2.Player", "Metadata"))
return Err(DracError(DracErrorCode::InternalError, "Failed to append arguments to Properties.Get message"));
Result<dbus::MessageGuard, DraconisError> replyResult =
dbus::ConnectionSendWithReplyAndBlock(connection, msg, 100);
Result<Message, DracError> replyResult = connection.sendWithReplyAndBlock(msg, 100);
if (!replyResult)
return Err(replyResult.error());
dbus::MessageGuard& reply = *replyResult;
Option<String> title = None;
Option<String> artist = None;
dbus::MessageIter propIter;
if (!dbus::MessageIterInit(reply, &propIter))
return Err(DraconisError(DraconisErrorCode::ParseError, "Properties.Get reply has no arguments"));
MessageIter propIter = replyResult->iterInit();
if (!propIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply has no arguments or invalid iterator"));
if (dbus::MessageIterGetArgType(&propIter) != DBUS_TYPE_VARIANT)
return Err(DraconisError(DraconisErrorCode::ParseError, "Properties.Get reply argument is not a variant"));
if (propIter.getArgType() != DBUS_TYPE_VARIANT)
return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply argument is not a variant"));
dbus::MessageIter variantIter;
dbus::MessageIterRecurse(&propIter, &variantIter);
MessageIter variantIter = propIter.recurse();
if (!variantIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Could not recurse into variant"));
if (dbus::MessageIterGetArgType(&variantIter) != DBUS_TYPE_ARRAY ||
dbus_message_iter_get_element_type(&variantIter) != DBUS_TYPE_DICT_ENTRY)
return Err(
DraconisError(DraconisErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})")
);
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})"));
dbus::MessageIter dictIter;
dbus::MessageIterRecurse(&variantIter, &dictIter);
MessageIter dictIter = variantIter.recurse();
if (!dictIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Could not recurse into metadata dictionary array"));
while (dbus::MessageIterGetArgType(&dictIter) == DBUS_TYPE_DICT_ENTRY) {
dbus::MessageIter entryIter;
dbus::MessageIterRecurse(&dictIter, &entryIter);
Option<String> key = dbus::MessageIterGetString(&entryIter);
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<String> key = entryIter.getString();
if (!key) {
dbus::MessageIterNext(&dictIter);
debug_log("Warning: Could not get key string from dict entry, skipping.");
if (!dictIter.next())
break;
continue;
}
if (!dbus::MessageIterNext(&entryIter) || dbus::MessageIterGetArgType(&entryIter) != DBUS_TYPE_VARIANT) {
dbus::MessageIterNext(&dictIter);
if (!entryIter.next() || entryIter.getArgType() != DBUS_TYPE_VARIANT) {
if (!dictIter.next())
break;
continue;
}
dbus::MessageIter valueVariantIter;
dbus::MessageIterRecurse(&entryIter, &valueVariantIter);
MessageIter valueVariantIter = entryIter.recurse();
if (!valueVariantIter.isValid()) {
if (!dictIter.next())
break;
continue;
}
if (*key == "xesam:title")
title = dbus::MessageIterGetString(&valueVariantIter);
else if (*key == "xesam:artist")
if (dbus::MessageIterGetArgType(&valueVariantIter) == DBUS_TYPE_ARRAY &&
dbus_message_iter_get_element_type(&valueVariantIter) == DBUS_TYPE_STRING) {
dbus::MessageIter artistArrayIter;
dbus::MessageIterRecurse(&valueVariantIter, &artistArrayIter);
artist = dbus::MessageIterGetString(&artistArrayIter);
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.");
}
}
dbus::MessageIterNext(&dictIter);
if (!dictIter.next())
break;
}
return MediaInfo(std::move(title), std::move(artist));
}
fn GetWindowManager() -> Option<String> {
if (Result<String, DraconisError> waylandResult = GetWaylandCompositor())
fn GetWindowManager() -> Result<String, DracError> {
if (Result<String, DracError> waylandResult = GetWaylandCompositor())
return *waylandResult;
else
debug_log("Could not detect Wayland compositor: {}", waylandResult.error().message);
if (Result<String, DraconisError> x11Result = GetX11WindowManager())
if (Result<String, DracError> x11Result = GetX11WindowManager())
return *x11Result;
else
debug_log("Could not detect X11 window manager: {}", x11Result.error().message);
return None;
return Err(DracError(DracErrorCode::NotFound, "Could not detect window manager (Wayland/X11) or both failed"));
}
fn GetDesktopEnvironment() -> Option<String> {
return util::helpers::GetEnv("XDG_CURRENT_DESKTOP")
.transform([](const String& xdgDesktop) -> String {
fn GetDesktopEnvironment() -> Result<String, DracError> {
return GetEnv("XDG_CURRENT_DESKTOP")
.transform([](String xdgDesktop) -> String {
if (const usize colon = xdgDesktop.find(':'); colon != String::npos)
return xdgDesktop.substr(0, colon);
xdgDesktop.resize(colon);
return xdgDesktop;
})
.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);
return finalValue;
})
.value_or(None);
.or_else([](const DracError&) -> Result<String, DracError> { return GetEnv("DESKTOP_SESSION"); });
}
fn GetShell() -> Option<String> {
if (const Result<String, DraconisError> shellPath = util::helpers::GetEnv("SHELL")) {
fn GetShell() -> Result<String, DracError> {
if (const Result<String, DracError> shellPath = GetEnv("SHELL")) {
// clang-format off
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
{ "bash", "Bash" },
{ "zsh", "Zsh" },
{ "fish", "Fish" },
{ "nu", "Nushell" },
{ "sh", "SH" }, // sh last because other shells contain "sh"
}};
constexpr Array<Pair<StringView, StringView>, 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)
@ -410,64 +408,63 @@ namespace os {
return *shellPath; // fallback to the raw shell path
}
return None;
return Err(DracError(DracErrorCode::NotFound, "Could not find SHELL environment variable"));
}
fn GetHost() -> Result<String, DraconisError> {
fn GetHost() -> Result<String, DracError> {
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, DraconisError> {
fn readFirstLine = [&](const String& path) -> Result<String, DracError> {
std::ifstream file(path);
String line;
if (!file)
return Err(DraconisError(
DraconisErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path)
));
return Err(
DracError(DracErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path))
);
if (!std::getline(file, line))
return Err(
DraconisError(DraconisErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path))
DracError(DracErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path))
);
return line;
};
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
)
));
});
return readFirstLine(primaryPath).or_else([&](const DracError& primaryError) -> Result<String, DracError> {
return readFirstLine(fallbackPath).or_else([&](const DracError& fallbackError) -> Result<String, DracError> {
return Err(DracError(
DracErrorCode::InternalError,
std::format(
"Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}",
primaryPath,
primaryError.message,
fallbackPath,
fallbackError.message
)
));
});
});
}
fn GetKernelVersion() -> Result<String, DraconisError> {
fn GetKernelVersion() -> Result<String, DracError> {
utsname uts;
if (uname(&uts) == -1)
return Err(DraconisError::withErrno("uname call failed"));
return Err(DracError::withErrno("uname call failed"));
if (std::strlen(uts.release) == 0)
return Err(DraconisError(DraconisErrorCode::ParseError, "uname returned null kernel release"));
return Err(DracError(DracErrorCode::ParseError, "uname returned null kernel release"));
return uts.release;
}
fn GetDiskUsage() -> Result<DiskSpace, DraconisError> {
fn GetDiskUsage() -> Result<DiskSpace, DracError> {
struct statvfs stat;
if (statvfs("/", &stat) == -1)
return Err(DraconisError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)")));
return Err(DracError::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),
@ -475,7 +472,21 @@ namespace os {
};
}
fn GetPackageCount() -> Result<u64, DraconisError> { return linux::GetTotalPackageCount(); }
fn GetPackageCount() -> Result<u64, DracError> {
u64 count = 0;
if (Result<u64, DracError> linuxCount = linux::GetTotalPackageCount())
count += *linuxCount;
else
return Err(linuxCount.error());
if (Result<u64, DracError> sharedCount = shared::GetPackageCount())
count += *sharedCount;
else
return Err(sharedCount.error());
return count;
}
} // namespace os
#endif // __linux__

View file

@ -3,53 +3,53 @@
// clang-format off
#include "src/os/linux/pkg_count.hpp"
#include <SQLiteCpp/SQLiteCpp.h>
#include <fstream>
#include <glaze/beve/read.hpp>
#include <glaze/beve/write.hpp>
#include <glaze/core/common.hpp>
#include <glaze/core/read.hpp>
#include <SQLiteCpp/Database.h> // SQLite::{Database, OPEN_READONLY}
#include <SQLiteCpp/Exception.h> // SQLite::Exception
#include <SQLiteCpp/Statement.h> // SQLite::Statement
#include <chrono> // std::chrono::{duration_cast, seconds, system_clock}
#include <filesystem> // std::filesystem::{current_path, directory_entry, directory_iterator, etc.}
#include <format> // std::format
#include <fstream> // std::{ifstream, ofstream}
#include <future> // std::{async, launch}
#include <ios> // std::ios::{binary, trunc}, std::ios_base
#include <iterator> // std::istreambuf_iterator
#include <glaze/beve/read.hpp> // glz::read_beve
#include <glaze/beve/write.hpp> // glz::write_beve
#include <glaze/core/context.hpp> // glz::{context, error_code, error_ctx}
#include <system_error> // std::error_code
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp"
// clang-format on
using util::error::DraconisError, util::error::DraconisErrorCode;
using util::error::DracError, util::error::DracErrorCode;
using util::types::u64, util::types::i64, util::types::Result, util::types::Err, util::types::String,
util::types::Exception;
util::types::StringView, util::types::Exception;
namespace {
namespace fs = std::filesystem;
using namespace std::chrono;
using os::linux::PkgCountCacheData, os::linux::PackageManagerInfo;
struct PkgCountCacheData {
u64 count {};
i64 timestamp_epoch_seconds {};
constexpr StringView ALLOWED_PMID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
// NOLINTBEGIN(readability-identifier-naming)
struct [[maybe_unused]] glaze {
using T = PkgCountCacheData;
static constexpr auto value = glz::object("count", &T::count, "timestamp", &T::timestamp_epoch_seconds);
};
// NOLINTEND(readability-identifier-naming)
};
fn GetPkgCountCachePath(const String& pm_id) -> Result<fs::path, DraconisError> {
fn GetPkgCountCachePath(const String& pmId) -> Result<fs::path, DracError> {
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 Err(DracError(DracErrorCode::IoError, "Failed to get temp directory: " + errc.message()));
if (pm_id.empty() ||
pm_id.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") != String::npos)
return Err(DraconisError(DraconisErrorCode::ParseError, "Invalid package manager ID for cache path: " + pm_id));
if (pmId.empty() || pmId.find_first_not_of(ALLOWED_PMID_CHARS) != String::npos)
return Err(DracError(DracErrorCode::ParseError, "Invalid package manager ID for cache path: " + pmId));
return cacheDir / (pm_id + "_pkg_count_cache.beve");
return cacheDir / (pmId + "_pkg_count_cache.beve");
}
fn ReadPkgCountCache(const String& pm_id) -> Result<PkgCountCacheData, DraconisError> {
Result<fs::path, DraconisError> cachePathResult = GetPkgCountCachePath(pm_id);
fn ReadPkgCountCache(const String& pmId) -> Result<PkgCountCacheData, DracError> {
Result<fs::path, DracError> cachePathResult = GetPkgCountCachePath(pmId);
if (!cachePathResult)
return Err(cachePathResult.error());
@ -57,57 +57,45 @@ namespace {
const fs::path& cachePath = *cachePathResult;
if (!fs::exists(cachePath))
return Err(DraconisError(DraconisErrorCode::NotFound, "Cache file not found: " + cachePath.string()));
return Err(DracError(DracErrorCode::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())
);
// Update log message
debug_log("Reading {} package count from cache file: {}", pm_id, cachePath.string());
return Err(DracError(DracErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string()));
try {
// Read the entire binary content
// Using std::string buffer is fine, it can hold arbitrary binary data
const String content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
ifs.close(); // Close the file stream after reading
ifs.close();
if (content.empty()) {
return Err(DraconisError(DraconisErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string()));
}
if (content.empty())
return Err(DracError(DracErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string()));
PkgCountCacheData result;
const glz::context ctx {};
if (auto glazeResult = glz::read_beve(result, content); glazeResult.ec != glz::error_code::none)
return Err(DraconisError(
DraconisErrorCode::ParseError,
if (glz::error_ctx glazeResult = glz::read_beve(result, content); glazeResult.ec != glz::error_code::none)
return Err(DracError(
DracErrorCode::ParseError,
std::format(
"BEVE parse error reading cache (code {}): {}", static_cast<int>(glazeResult.ec), cachePath.string()
)
));
debug_log("Successfully read {} package count from BEVE cache file.", pm_id);
return result;
} catch (const std::ios_base::failure& e) {
return Err(DraconisError(
DraconisErrorCode::IoError,
std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what())
return Err(DracError(
DracErrorCode::IoError, std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what())
));
} catch (const Exception& e) {
return Err(
DraconisError(DraconisErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what()))
return Err(DracError(DracErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what()))
);
}
}
// Modified to take pm_id and PkgCountCacheData
fn WritePkgCountCache(const String& pm_id, const PkgCountCacheData& data) -> Result<void, DraconisError> {
fn WritePkgCountCache(const String& pmId, const PkgCountCacheData& data) -> Result<void, DracError> {
using util::types::isize;
Result<fs::path, DraconisError> cachePathResult = GetPkgCountCachePath(pm_id);
Result<fs::path, DracError> cachePathResult = GetPkgCountCachePath(pmId);
if (!cachePathResult)
return Err(cachePathResult.error());
@ -116,80 +104,65 @@ namespace {
fs::path tempPath = cachePath;
tempPath += ".tmp";
debug_log("Writing {} package count to BEVE cache file: {}", pm_id, cachePath.string());
try {
String binaryBuffer;
PkgCountCacheData mutableData = data;
if (auto glazeErr = glz::write_beve(mutableData, binaryBuffer); glazeErr) {
return Err(DraconisError(
DraconisErrorCode::ParseError,
if (glz::error_ctx glazeErr = glz::write_beve(mutableData, binaryBuffer); glazeErr)
return Err(DracError(
DracErrorCode::ParseError,
std::format("BEVE serialization error writing cache (code {})", static_cast<int>(glazeErr.ec))
));
}
{
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()));
}
if (!ofs.is_open())
return Err(DracError(DracErrorCode::IoError, "Failed to open temp cache file: " + tempPath.string()));
ofs.write(binaryBuffer.data(), static_cast<isize>(binaryBuffer.size()));
if (!ofs) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(
DraconisError(DraconisErrorCode::IoError, "Failed to write to temp cache file: " + tempPath.string())
);
return Err(DracError(DracErrorCode::IoError, "Failed to write to temp cache file: " + tempPath.string()));
}
}
// Atomically replace the old cache file with the new one
std::error_code errc;
fs::rename(tempPath, cachePath, errc);
if (errc) {
fs::remove(tempPath, errc); // Clean up temp file on failure (ignore error)
return Err(DraconisError(
DraconisErrorCode::IoError,
fs::remove(tempPath, errc);
return Err(DracError(
DracErrorCode::IoError,
std::format("Failed to replace cache file '{}': {}", cachePath.string(), errc.message())
));
}
debug_log("Successfully wrote {} package count to BEVE cache file.", pm_id);
return {};
} catch (const std::ios_base::failure& e) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(DraconisError(
DraconisErrorCode::IoError,
std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what())
return Err(DracError(
DracErrorCode::IoError, std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what())
));
} catch (const Exception& e) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(
DraconisError(DraconisErrorCode::InternalError, std::format("Error writing package count cache: {}", e.what()))
return Err(DracError(DracErrorCode::InternalError, std::format("Error writing package count cache: {}", e.what()))
);
} catch (...) {
std::error_code removeEc;
fs::remove(tempPath, removeEc);
return Err(
DraconisError(DraconisErrorCode::Other, std::format("Unknown error writing cache file: {}", tempPath.string()))
return Err(DracError(DracErrorCode::Other, std::format("Unknown error writing cache file: {}", tempPath.string()))
);
}
}
fn GetPackageCountInternal(const os::linux::PackageManagerInfo& pmInfo) -> Result<u64, DraconisError> {
// Use info from the struct
const fs::path& dbPath = pmInfo.db_path;
const String& pmId = pmInfo.id;
const String& queryStr = pmInfo.count_query;
fn GetPackageCountInternalDb(const PackageManagerInfo& pmInfo) -> Result<u64, DracError> {
const auto& [pmId, dbPath, countQuery] = pmInfo;
// Try reading from cache using pm_id
if (Result<PkgCountCacheData, DraconisError> cachedDataResult = ReadPkgCountCache(pmId)) {
if (Result<PkgCountCacheData, DracError> cachedDataResult = ReadPkgCountCache(pmId)) {
const auto& [count, timestamp] = *cachedDataResult;
std::error_code errc;
const std::filesystem::file_time_type dbModTime = fs::last_write_time(dbPath, errc);
@ -199,9 +172,8 @@ namespace {
"Could not get modification time for '{}': {}. Invalidating {} cache.", dbPath.string(), errc.message(), pmId
);
} else {
if (const auto cacheTimePoint = system_clock::time_point(seconds(timestamp));
if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp));
cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) {
// Use cacheTimePoint for logging as well
debug_log(
"Using valid {} package count cache (DB file unchanged since {}).",
pmId,
@ -212,7 +184,7 @@ namespace {
debug_log("{} package count cache stale (DB file modified).", pmId);
}
} else {
if (cachedDataResult.error().code != DraconisErrorCode::NotFound)
if (cachedDataResult.error().code != DracErrorCode::NotFound)
debug_at(cachedDataResult.error());
debug_log("{} package count cache not found or unreadable.", pmId);
}
@ -222,121 +194,202 @@ namespace {
try {
const SQLite::Database database(dbPath.string(), SQLite::OPEN_READONLY);
if (SQLite::Statement query(database, queryStr); query.executeStep()) {
if (SQLite::Statement query(database, countQuery); query.executeStep()) {
const i64 countInt64 = query.getColumn(0).getInt64();
if (countInt64 < 0)
return Err(DraconisError(
DraconisErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId)
));
return Err(
DracError(DracErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId))
);
count = static_cast<u64>(countInt64);
} else {
return Err(
DraconisError(DraconisErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId))
);
return Err(DracError(DracErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId)));
}
} catch (const SQLite::Exception& e) {
return Err(DraconisError(
DraconisErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what())
return Err(DracError(
DracErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what())
));
} catch (const Exception& e) {
return Err(DraconisError(DraconisErrorCode::InternalError, e.what()));
} catch (...) {
return Err(DraconisError(DraconisErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId)));
} catch (const Exception& e) { return Err(DracError(DracErrorCode::InternalError, e.what())); } catch (...) {
return Err(DracError(DracErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId)));
}
const i64 nowEpochSeconds = duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
const PkgCountCacheData dataToCache = { .count = count, .timestamp_epoch_seconds = nowEpochSeconds };
const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds };
if (Result<void, DraconisError> writeResult = WritePkgCountCache(pmId, dataToCache); !writeResult) {
warn_at(writeResult.error());
warn_log("Failed to write {} package count to cache.", pmId);
if (Result<void, DracError> writeResult = WritePkgCountCache(pmId, dataToCache); !writeResult)
error_at(writeResult.error());
return count;
}
fn GetPackageCountInternalDir(
const String& pmId,
const fs::path& dirPath,
const String& file_extension_filter = "",
const bool subtract_one = false
) -> Result<u64, DracError> {
debug_log("Attempting to get {} package count.", pmId);
std::error_code errc;
if (!fs::exists(dirPath, errc)) {
if (errc)
return Err(DracError(
DracErrorCode::IoError, std::format("Filesystem error checking {} directory: {}", pmId, errc.message())
));
return Err(
DracError(DracErrorCode::ApiUnavailable, std::format("{} directory not found: {}", pmId, dirPath.string()))
);
}
debug_log("Fetched fresh {} package count: {}", pmId, count);
if (!fs::is_directory(dirPath, errc)) {
if (errc)
return Err(DracError(
DracErrorCode::IoError, std::format("Filesystem error checking {} path type: {}", pmId, errc.message())
));
warn_log("Expected {} directory at '{}', but it's not a directory.", pmId, dirPath.string());
return Err(
DracError(DracErrorCode::IoError, std::format("{} path is not a directory: {}", pmId, dirPath.string()))
);
}
u64 count = 0;
try {
const fs::directory_iterator dirIter(dirPath, fs::directory_options::skip_permission_denied, errc);
if (errc)
return Err(
DracError(DracErrorCode::IoError, std::format("Failed to iterate {} directory: {}", pmId, errc.message()))
);
for (const fs::directory_entry& entry : dirIter) {
if (!file_extension_filter.empty()) {
if (std::error_code fileErrc; !entry.is_regular_file(fileErrc) || fileErrc) {
if (fileErrc)
warn_log(
"Error checking file status in {} directory for '{}': {}",
pmId,
entry.path().string(),
fileErrc.message()
);
continue;
}
if (entry.path().extension().string() == file_extension_filter)
count++;
} else {
count++;
}
}
} catch (const fs::filesystem_error& e) {
return Err(DracError(
DracErrorCode::IoError,
std::format("Filesystem error iterating {} directory '{}': {}", pmId, dirPath.string(), e.what())
));
} catch (...) {
return Err(DracError(
DracErrorCode::Other, std::format("Unknown error iterating {} directory '{}'", pmId, dirPath.string())
));
}
if (subtract_one && count > 0)
count--;
return count;
}
} // namespace
namespace os::linux {
fn GetMossPackageCount() -> Result<u64, DraconisError> {
fn GetDpkgPackageCount() -> Result<u64, DracError> {
return GetPackageCountInternalDir(
"Dpkg", fs::current_path().root_path() / "var" / "lib" / "dpkg" / "info", String(".list")
);
}
fn GetMossPackageCount() -> Result<u64, DracError> {
debug_log("Attempting to get Moss package count.");
const PackageManagerInfo mossInfo = {
.id = "moss",
.db_path = "/.moss/db/install",
.count_query = "SELECT COUNT(*) FROM meta",
.id = "moss",
.dbPath = "/.moss/db/install",
.countQuery = "SELECT COUNT(*) FROM meta",
};
if (std::error_code errc; !fs::exists(mossInfo.db_path, errc)) {
if (std::error_code errc; !fs::exists(mossInfo.dbPath, errc)) {
if (errc) {
warn_log("Filesystem error checking for Moss DB at '{}': {}", mossInfo.db_path.string(), errc.message());
return Err(DraconisError(DraconisErrorCode::IoError, "Filesystem error checking Moss DB: " + errc.message()));
warn_log("Filesystem error checking for Moss DB at '{}': {}", mossInfo.dbPath.string(), errc.message());
return Err(DracError(DracErrorCode::IoError, "Filesystem error checking Moss DB: " + errc.message()));
}
debug_log("Moss database not found at '{}'. Assuming 0 Moss packages.", mossInfo.db_path.string());
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Moss db not found: " + mossInfo.db_path.string()));
return Err(DracError(DracErrorCode::ApiUnavailable, "Moss db not found: " + mossInfo.dbPath.string()));
}
debug_log("Moss database found at '{}'. Proceeding with count.", mossInfo.db_path.string());
Result<u64, DracError> countResult = GetPackageCountInternalDb(mossInfo);
return GetPackageCountInternal(mossInfo);
if (!countResult) {
if (countResult.error().code != DracErrorCode::ParseError)
debug_at(countResult.error());
return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get package count from Moss DB"));
}
return *countResult - 1;
}
fn GetNixPackageCount() -> Result<u64, DraconisError> {
fn GetNixPackageCount() -> Result<u64, DracError> {
debug_log("Attempting to get Nix package count.");
const PackageManagerInfo nixInfo = {
.id = "nix",
.db_path = "/nix/var/nix/db/db.sqlite",
.count_query = "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL",
.id = "nix",
.dbPath = "/nix/var/nix/db/db.sqlite",
.countQuery = "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL",
};
if (std::error_code errc; !fs::exists(nixInfo.db_path, errc)) {
if (std::error_code errc; !fs::exists(nixInfo.dbPath, errc)) {
if (errc) {
warn_log("Filesystem error checking for Nix DB at '{}': {}", nixInfo.db_path.string(), errc.message());
return Err(DraconisError(DraconisErrorCode::IoError, "Filesystem error checking Nix DB: " + errc.message()));
warn_log("Filesystem error checking for Nix DB at '{}': {}", nixInfo.dbPath.string(), errc.message());
return Err(DracError(DracErrorCode::IoError, "Filesystem error checking Nix DB: " + errc.message()));
}
debug_log("Nix database not found at '{}'. Assuming 0 Nix packages.", nixInfo.db_path.string());
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Nix db not found: " + nixInfo.db_path.string()));
return Err(DracError(DracErrorCode::ApiUnavailable, "Nix db not found: " + nixInfo.dbPath.string()));
}
debug_log("Nix database found at '{}'. Proceeding with count.", nixInfo.db_path.string());
return GetPackageCountInternal(nixInfo);
return GetPackageCountInternalDb(nixInfo);
}
fn GetTotalPackageCount() -> Result<u64, DraconisError> {
debug_log("Attempting to get total package count from all package managers.");
fn GetPacmanPackageCount() -> Result<u64, DracError> {
return GetPackageCountInternalDir(
"Pacman", fs::current_path().root_path() / "var" / "lib" / "pacman" / "local", "", true
);
}
const PackageManagerInfo mossInfo = {
.id = "moss",
.db_path = "/.moss/db/install",
.count_query = "SELECT COUNT(*) FROM meta",
};
fn GetTotalPackageCount() -> Result<u64, DracError> {
using util::types::Array, util::types::Future;
const PackageManagerInfo nixInfo = {
.id = "nix",
.db_path = "/nix/var/nix/db/db.sqlite",
.count_query = "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL",
Array<Future<Result<u64, DracError>>, 4> futures = {
std::async(std::launch::async, GetDpkgPackageCount),
std::async(std::launch::async, GetMossPackageCount),
std::async(std::launch::async, GetNixPackageCount),
std::async(std::launch::async, GetPacmanPackageCount),
};
u64 totalCount = 0;
if (Result<u64, DraconisError> mossCountResult = GetMossPackageCount(); mossCountResult) {
// `moss list installed` returns 1 less than the db count,
// so we subtract 1 for consistency.
totalCount += (*mossCountResult - 1);
} else {
debug_at(mossCountResult.error());
}
if (Result<u64, DraconisError> nixCountResult = GetNixPackageCount(); nixCountResult) {
totalCount += *nixCountResult;
} else {
debug_at(nixCountResult.error());
}
for (Future<Result<u64, DracError>>& fut : futures) try {
if (Result<u64, DracError> result = fut.get()) {
totalCount += *result;
} else {
if (result.error().code != DracErrorCode::ApiUnavailable) {
error_at(result.error());
} else {
debug_at(result.error());
}
}
} catch (const Exception& e) {
error_log("Caught exception while getting package count future: {}", e.what());
} catch (...) { error_log("Caught unknown exception while getting package count future."); }
return totalCount;
}

View file

@ -3,7 +3,9 @@
#ifdef __linux__
// clang-format off
#include <filesystem>
#include <filesystem> // std::filesystem::path
#include <glaze/core/common.hpp> // glz::object
#include <glaze/core/meta.hpp> // glz::detail::Object
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
@ -11,50 +13,65 @@
// clang-format on
namespace os::linux {
using util::error::DraconisError;
using util::types::Result, util::types::u64;
using util::error::DracError;
using util::types::Result, util::types::u64, util::types::i64, util::types::String;
namespace fs = std::filesystem;
struct PackageManagerInfo {
util::types::String id;
std::filesystem::path db_path;
util::types::String count_query;
String id;
fs::path dbPath;
String countQuery;
};
struct PkgCountCacheData {
u64 count {};
i64 timestampEpochSeconds {};
// NOLINTBEGIN(readability-identifier-naming)
struct [[maybe_unused]] glaze {
using T = PkgCountCacheData;
static constexpr glz::detail::Object value =
glz::object("count", &T::count, "timestamp", &T::timestampEpochSeconds);
};
// NOLINTEND(readability-identifier-naming)
};
// Get package count from dpkg (Debian/Ubuntu)
fn GetDpkgPackageCount() -> Result<u64, DraconisError>;
fn GetDpkgPackageCount() -> Result<u64, DracError>;
// Get package count from RPM (Red Hat/Fedora/CentOS)
fn GetRpmPackageCount() -> Result<u64, DraconisError>;
fn GetRpmPackageCount() -> Result<u64, DracError>;
// Get package count from pacman (Arch Linux)
fn GetPacmanPackageCount() -> Result<u64, DraconisError>;
fn GetPacmanPackageCount() -> Result<u64, DracError>;
// Get package count from Portage (Gentoo)
fn GetPortagePackageCount() -> Result<u64, DraconisError>;
fn GetPortagePackageCount() -> Result<u64, DracError>;
// Get package count from zypper (openSUSE)
fn GetZypperPackageCount() -> Result<u64, DraconisError>;
fn GetZypperPackageCount() -> Result<u64, DracError>;
// Get package count from apk (Alpine)
fn GetApkPackageCount() -> Result<u64, DraconisError>;
fn GetApkPackageCount() -> Result<u64, DracError>;
// Get package count from moss (AerynOS)
fn GetMossPackageCount() -> Result<u64, DraconisError>;
fn GetMossPackageCount() -> Result<u64, DracError>;
// Get package count from nix
fn GetNixPackageCount() -> Result<u64, DraconisError>;
fn GetNixPackageCount() -> Result<u64, DracError>;
// Get package count from flatpak
fn GetFlatpakPackageCount() -> Result<u64, DraconisError>;
fn GetFlatpakPackageCount() -> Result<u64, DracError>;
// Get package count from snap
fn GetSnapPackageCount() -> Result<u64, DraconisError>;
fn GetSnapPackageCount() -> Result<u64, DracError>;
// Get package count from AppImage
fn GetAppimagePackageCount() -> Result<u64, DraconisError>;
fn GetAppimagePackageCount() -> Result<u64, DracError>;
// Get total package count from all available package managers
fn GetTotalPackageCount() -> Result<u64, DraconisError>;
fn GetTotalPackageCount() -> Result<u64, DracError>;
} // namespace os::linux
#endif // __linux__

View file

@ -14,88 +14,100 @@
* (found in linux.cpp, windows.cpp, macos.cpp).
*/
namespace os {
using util::error::DraconisError;
using util::error::DracError;
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.
* or a DracError on failure.
*/
fn GetMemInfo() -> Result<u64, DraconisError>;
fn GetMemInfo() -> Result<u64, DracError>;
/**
* @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, DraconisError>;
fn GetNowPlaying() -> Result<MediaInfo, DracError>;
/**
* @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 a DracError on failure.
*/
fn GetOSVersion() -> Result<String, DraconisError>;
fn GetOSVersion() -> Result<String, DracError>;
/**
* @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.
* @return An Option containing the detected DE name String, or None if detection fails or is not applicable.
* @return A Result containing the DE name String on success,
* or a DracError on failure (e.g., permission error, API error).
*/
fn GetDesktopEnvironment() -> Option<String>;
fn GetDesktopEnvironment() -> Result<String, DracError>;
/**
* @brief Attempts to retrieve the window manager name.
* @details On Linux, checks Wayland compositor or X11 WM properties. On Windows, returns "DWM" or similar.
* On macOS, might return "Quartz Compositor" or a specific tiling WM name if active.
* @return An Option containing the detected WM name String, or None if detection fails.
* @return A Result containing the detected WM name String on success,
* or a DracError on failure (e.g., permission error, API error).
*/
fn GetWindowManager() -> Option<String>;
fn GetWindowManager() -> Result<String, DracError>;
/**
* @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 A Result containing the shell name String on success,
* or a DracError on failure (e.g., permission error, API error).
*/
fn GetShell() -> Option<String>;
fn GetShell() -> Result<String, DracError>;
/**
* @brief Gets a system identifier, often the hardware model or product family.
* @details Examples: "MacBookPro18,3", "Latitude 5420", "ThinkPad T490".
* Implementation varies: reads DMI info on Linux, registry on Windows, sysctl on macOS.
* @return A Result containing the host/product identifier String on success,
* or an OsError on failure (e.g., permission reading DMI/registry, API error).
* or a DracError on failure (e.g., permission reading DMI/registry, API error).
*/
fn GetHost() -> Result<String, DraconisError>;
fn GetHost() -> Result<String, DracError>;
/**
* @brief Gets the operating system's kernel version string.
* @details Examples: "5.15.0-76-generic", "10.0.22621", "23.1.0".
* Uses uname() on Linux/macOS, WinRT/registry on Windows.
* @return A Result containing the kernel version String on success,
* or an OsError on failure.
* or a DracError on failure.
*/
fn GetKernelVersion() -> Result<String, DraconisError>;
fn GetKernelVersion() -> Result<String, DracError>;
/**
* @brief Gets the number of installed packages (Platform-specific).
* @details On Linux, sums counts from various package managers. On other platforms, behavior may vary.
* @return A Result containing the package count (u64) on success,
* or an OsError on failure (e.g., permission errors, command not found)
* or if not supported (OsErrorCode::NotSupported).
* or a DracError on failure (e.g., permission errors, command not found)
* or if not supported (DracErrorCode::NotSupported).
*/
fn GetPackageCount() -> Result<u64, DraconisError>;
fn GetPackageCount() -> Result<u64, DracError>;
/**
* @brief Gets the disk usage for the primary/root filesystem.
* @details Uses statvfs on Linux/macOS, GetDiskFreeSpaceExW on Windows.
* @return A Result containing the DiskSpace struct (used/total bytes) on success,
* or an OsError on failure (e.g., filesystem not found, permission error).
* or a DracError on failure (e.g., filesystem not found, permission error).
*/
fn GetDiskUsage() -> Result<DiskSpace, DraconisError>;
fn GetDiskUsage() -> Result<DiskSpace, DracError>;
namespace shared {
/**
* @brief Gets the number of installed packages from OS-agnostic package managers.
* @details Currently only supports Cargo package manager.
* @return A Result containing the package count (u64) on success,
* or a DracError on failure (e.g., permission errors, command not found).
*/
fn GetPackageCount() -> Result<u64, DracError>;
} // namespace shared
} // namespace os

40
src/os/shared.cpp Normal file
View file

@ -0,0 +1,40 @@
#include <filesystem>
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/helpers.hpp"
#include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp"
#include "os.hpp"
namespace os::shared {
using util::error::DracError, util::error::DracErrorCode;
using util::types::u64, util::types::String, util::types::Result, util::types::Err;
namespace fs = std::filesystem;
fn GetPackageCount() -> Result<u64, DracError> {
using util::helpers::GetEnv;
fs::path cargoPath {};
if (Result<String, DracError> cargoHome = GetEnv("CARGO_HOME"))
cargoPath = fs::path(*cargoHome) / "bin";
else if (Result<String, DracError> homeDir = GetEnv("HOME"))
cargoPath = fs::path(*homeDir) / ".cargo" / "bin";
if (cargoPath.empty() || !fs::exists(cargoPath))
return Err(DracError(DracErrorCode::NotFound, "Could not find cargo directory"));
u64 count = 0;
for (const fs::directory_entry& entry : fs::directory_iterator(cargoPath))
if (entry.is_regular_file())
++count;
debug_log("Found {} packages in cargo directory: {}", count, cargoPath.string());
return count;
}
} // namespace os::shared