ough
This commit is contained in:
parent
3ad961a571
commit
3b16fee5f4
28 changed files with 1242 additions and 1047 deletions
353
src/os/linux.cpp
353
src/os/linux.cpp
|
@ -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__
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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
40
src/os/shared.cpp
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue