diff --git a/meson.build b/meson.build index 8770a76..a0e5ce8 100644 --- a/meson.build +++ b/meson.build @@ -88,7 +88,7 @@ add_project_arguments(common_cpp_args, language : 'cpp') # ------- # # Files # # ------- # -base_sources = files('src/core/system_data.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', 'src/main.cpp') +base_sources = files('src/core/system_data.cpp', 'src/core/package.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', 'src/main.cpp') platform_sources = { 'darwin' : ['src/os/macos.cpp', 'src/os/macos/bridge.mm'], diff --git a/src/core/package.cpp b/src/core/package.cpp new file mode 100644 index 0000000..5df0fdd --- /dev/null +++ b/src/core/package.cpp @@ -0,0 +1,335 @@ +#include "package.hpp" + +#include // SQLite::{Database, OPEN_READONLY} +#include // SQLite::Exception +#include // SQLite::Statement +#include // std::chrono +#include // std::filesystem +#include // std::format +#include // std::{async, future, launch} +#include // std::error_code + +#include "src/util/cache.hpp" +#include "src/util/error.hpp" +#include "src/util/helpers.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" + +namespace fs = std::filesystem; +using namespace std::chrono; +using util::cache::ReadCache, util::cache::WriteCache; +using util::error::DracError, util::error::DracErrorCode; +using util::types::Err, util::types::Exception, util::types::Future, util::types::Result, util::types::String, + util::types::Vec, util::types::i64, util::types::u64; + +namespace { + fn GetCountFromDirectoryImpl( + const String& pmId, + const fs::path& dirPath, + const String& fileExtensionFilter, + const bool subtractOne + ) -> Result { + debug_log("Counting packages for '{}' in directory: {}", pmId, dirPath.string()); + + std::error_code errc; + + if (!fs::exists(dirPath, errc)) { + if (errc) + warn_log("Filesystem error checking {} directory '{}': {}", pmId, dirPath.string(), errc.message()); + + return Err(DracError(DracErrorCode::NotFound, std::format("{} directory not found: {}", pmId, dirPath.string()))); + } + + errc.clear(); + + if (!fs::is_directory(dirPath, errc)) { + if (errc) + return Err(DracError( + DracErrorCode::IoError, + std::format("Filesystem error checking if '{}' is a directory: {}", dirPath.string(), errc.message()) + )); + + return Err( + DracError(DracErrorCode::IoError, std::format("{} path is not a directory: {}", pmId, dirPath.string())) + ); + } + + errc.clear(); + + u64 count = 0; + bool filterActive = !fileExtensionFilter.empty(); + + 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 create iterator for {} directory '{}': {}", pmId, dirPath.string(), errc.message()) + )); + } + + errc.clear(); + + for (const fs::directory_entry& entry : dirIter) { + if (entry.path().empty()) + continue; + + std::error_code entryStatErr; + bool isFile = false; + + if (filterActive) { + isFile = entry.is_regular_file(entryStatErr); + if (entryStatErr) { + warn_log( + "Error stating entry '{}' in {} directory: {}", entry.path().string(), pmId, entryStatErr.message() + ); + entryStatErr.clear(); + continue; + } + } + + if (filterActive) { + if (isFile && entry.path().extension().string() == fileExtensionFilter) + count++; + } else + count++; + } + + } catch (const fs::filesystem_error& e) { + return Err(DracError(DracErrorCode::IoError, std::format("Filesystem error during {} directory iteration", pmId)) + ); + } catch (const Exception& e) { return Err(DracError(DracErrorCode::InternalError, e.what())); } catch (...) { + return Err(DracError(DracErrorCode::Other, std::format("Unknown error iterating {} directory", pmId))); + } + + if (subtractOne && count > 0) + count--; + + debug_log("Successfully counted {} packages for '{}': {}", std::to_string(count), pmId, dirPath.string()); + return count; + } +} // namespace + +namespace package { + fn GetCountFromDirectory( + const String& pmId, + const fs::path& dirPath, + const String& fileExtensionFilter, + const bool subtractOne + ) -> Result { + return GetCountFromDirectoryImpl(pmId, dirPath, fileExtensionFilter, subtractOne); + } + + fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath, const String& fileExtensionFilter) + -> Result { + return GetCountFromDirectoryImpl(pmId, dirPath, fileExtensionFilter, false); + } + + fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath, const bool subtractOne) + -> Result { + const String noFilter; + return GetCountFromDirectoryImpl(pmId, dirPath, noFilter, subtractOne); + } + + fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath) -> Result { + const String noFilter; + return GetCountFromDirectoryImpl(pmId, dirPath, noFilter, false); + } + + fn GetCountFromDb(const PackageManagerInfo& pmInfo) -> Result { + const auto& [pmId, dbPath, countQuery] = pmInfo; + const String cacheKey = "pkg_count_" + pmId; // More specific cache key + + if (Result cachedDataResult = ReadCache(cacheKey)) { + const auto& [count, timestamp] = *cachedDataResult; + std::error_code errc; + const std::filesystem::file_time_type dbModTime = fs::last_write_time(dbPath, errc); + + if (errc) { + warn_log( + "Could not get modification time for '{}': {}. Invalidating {} cache.", dbPath.string(), errc.message(), pmId + ); + } else { + if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp)); + cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) { + debug_log( + "Using valid {} package count cache (DB file unchanged since {}). Count: {}", + pmId, + std::format("{:%F %T %Z}", floor(cacheTimePoint)), + count + ); + return count; + } + debug_log("{} package count cache stale (DB file modified).", pmId); + } + } else { + if (cachedDataResult.error().code != DracErrorCode::NotFound) + debug_at(cachedDataResult.error()); + debug_log("{} package count cache not found or unreadable.", pmId); + } + + debug_log("Fetching fresh {} package count from database: {}", pmId, dbPath.string()); + u64 count = 0; + + try { + // Ensure database file exists before trying to open + std::error_code existsErr; + if (!fs::exists(dbPath, existsErr) || existsErr) { + if (existsErr) { + warn_log("Error checking existence of {} DB '{}': {}", pmId, dbPath.string(), existsErr.message()); + } + return Err( + DracError(DracErrorCode::NotFound, std::format("{} database not found at '{}'", pmId, dbPath.string())) + ); + } + + const SQLite::Database database(dbPath.string(), SQLite::OPEN_READONLY); + SQLite::Statement queryStmt(database, countQuery); // Use query directly + + if (queryStmt.executeStep()) { + const i64 countInt64 = queryStmt.getColumn(0).getInt64(); + if (countInt64 < 0) + return Err( + DracError(DracErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId)) + ); + count = static_cast(countInt64); + } else { + // It's possible a query legitimately returns 0 rows (e.g., no packages) + debug_log("No rows returned by {} DB COUNT query for '{}', assuming count is 0.", pmId, dbPath.string()); + count = 0; + // return Err(DracError(DracErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", + // pmId))); + } + } catch (const SQLite::Exception& e) { + // Log specific SQLite errors but return a more general error type + error_log("SQLite error occurred accessing {} DB '{}': {}", pmId, dbPath.string(), e.what()); + return Err(DracError( + DracErrorCode::ApiUnavailable, // Or IoError? + std::format("Failed to query {} database: {}", pmId, dbPath.string()) + )); + } catch (const Exception& e) { + error_log("Standard exception accessing {} DB '{}': {}", pmId, dbPath.string(), e.what()); + return Err(DracError(DracErrorCode::InternalError, e.what())); + } catch (...) { + error_log("Unknown error occurred accessing {} DB '{}'", pmId, dbPath.string()); + return Err(DracError(DracErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId))); + } + + debug_log("Successfully fetched {} package count: {}.", pmId, count); + + const i64 nowEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); + const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds }; + + if (Result writeResult = WriteCache(cacheKey, dataToCache); !writeResult) + error_at(writeResult.error()); // Log cache write error but return the count anyway + + return count; + } + +#if defined(__linux__) || defined(__APPLE__) + fn GetNixCount() -> Result { + const PackageManagerInfo nixInfo = { + .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.dbPath, errc)) { + if (errc) { + 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())); + } + + return Err(DracError(DracErrorCode::ApiUnavailable, "Nix db not found: " + nixInfo.dbPath.string())); + } + + return GetCountFromDb(nixInfo); + } +#endif + + fn GetCargoCount() -> Result { + using util::helpers::GetEnv; + + fs::path cargoPath {}; + + if (const Result cargoHome = GetEnv("CARGO_HOME")) + cargoPath = fs::path(*cargoHome) / "bin"; + else if (const Result 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; + } + + fn GetTotalCount() -> Result { + Vec>> futures; + +#ifdef __linux__ + futures.push_back(std::async(std::launch::async, GetDpkgCount)); + futures.push_back(std::async(std::launch::async, GetPacmanCount)); + // futures.push_back(std::async(std::launch::async, GetRpmCount)); + // futures.push_back(std::async(std::launch::async, GetPortageCount)); + // futures.push_back(std::async(std::launch::async, GetZypperCount)); + // futures.push_back(std::async(std::launch::async, GetApkCount)); + futures.push_back(std::async(std::launch::async, GetMossCount)); +#elifdef __APPLE__ + futures.push_back(std::async(std::launch::async, GetHomebrewCount)); + futures.push_back(std::async(std::launch::async, GetMacPortsCount)); +#elifdef _WIN32 + futures.push_back(std::async(std::launch::async, GetWinRTCount)); + futures.push_back(std::async(std::launch::async, GetChocolateyCount)); + futures.push_back(std::async(std::launch::async, GetScoopCount)); +#elif defined(__FreeBSD__) || defined(__DragonFly__) + futures.push_back(std::async(std::launch::async, GetPkgNgCount)); +#elifdef __NetBSD__ + futures.push_back(std::async(std::launch::async, GetPkgSrcCount)); +#elifdef __HAIKU__ + futures.push_back(std::async(std::launch::async, GetHaikuCount)); +#elifdef __serenity__ + futures.push_back(std::async(std::launch::async, GetSerenityCount)); +#endif + +#if defined(__linux__) || defined(__APPLE__) + futures.push_back(std::async(std::launch::async, GetNixCount)); +#endif + futures.push_back(std::async(std::launch::async, GetCargoCount)); + + u64 totalCount = 0; + bool oneSucceeded = false; + + for (Future>& fut : futures) { + try { + if (Result result = fut.get()) { + totalCount += *result; + oneSucceeded = true; + debug_log("Added {} packages. Current total: {}", *result, totalCount); + } else { + if (result.error().code != DracErrorCode::NotFound && result.error().code != DracErrorCode::ApiUnavailable && + result.error().code != DracErrorCode::NotSupported) { + 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."); } + } + + if (!oneSucceeded && totalCount == 0) + return Err(DracError(DracErrorCode::NotFound, "No package managers found or none reported counts.")); + + debug_log("Final total package count: {}", totalCount); + return totalCount; + } +} // namespace package diff --git a/src/core/package.hpp b/src/core/package.hpp index 2d810fc..a2acc24 100644 --- a/src/core/package.hpp +++ b/src/core/package.hpp @@ -1,34 +1,35 @@ #pragma once #include // std::filesystem::path -#include // std::future #include // glz::object #include // glz::detail::Object +// Include necessary type headers used in declarations #include "src/util/defs.hpp" #include "src/util/error.hpp" -#include "src/util/types.hpp" +#include "src/util/types.hpp" // Brings in Result, u64, etc. -namespace packages { +namespace package { namespace fs = std::filesystem; using util::error::DracError; - using util::types::Result, util::types::u64, util::types::i64, util::types::String, util::types::Vec, - util::types::Future; + using util::types::Future; + using util::types::i64; + using util::types::Result; + using util::types::String; + using util::types::u64; /** * @struct PkgCountCacheData * @brief Structure for caching package count results along with a timestamp. - * - * Used to avoid redundant lookups in package manager databases or directories - * if the underlying data source hasn't changed recently. */ struct PkgCountCacheData { - u64 count {}; ///< The cached package count. - i64 timestampEpochSeconds {}; ///< The UNIX timestamp (seconds since epoch) when the count was cached. + 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); }; @@ -40,31 +41,67 @@ namespace packages { * @brief Holds information needed to query a database-backed package manager. */ struct PackageManagerInfo { - String id; ///< Unique identifier for the package manager (used for cache key). - fs::path dbPath; ///< Filesystem path to the package manager's database. - String countQuery; ///< SQL query string to count the packages. + String id; ///< Unique identifier (e.g., "pacman", "dpkg", used for cache key). + fs::path dbPath; ///< Filesystem path to the database or primary directory. + String countQuery; ///< Query string (e.g., SQL) or specific file/pattern if not DB. }; /** - * @brief Gets the total package count by querying all relevant package managers for the current OS. - * @details This function orchestrates the process: - * 1. Determines the set of relevant package managers (platform-specific + shared). - * 2. Launches asynchronous tasks to query each manager. - * 3. Aggregates the results, summing counts and logging errors. + * @brief Gets the total package count by querying all relevant package managers. * @return Result containing the total package count (u64) on success, - * or a DracError if the aggregation fails (though individual manager errors are logged). + * or a DracError if aggregation fails (individual errors logged). */ fn GetTotalCount() -> Result; + /** + * @brief Gets package count from a database using SQLite. + * @param pmInfo Information about the package manager database. + * @return Result containing the count (u64) or a DracError. + */ fn GetCountFromDb(const PackageManagerInfo& pmInfo) -> Result; + /** + * @brief Gets package count by iterating entries in a directory, optionally filtering and subtracting. + * @param pmId Identifier for the package manager (for logging/cache). + * @param dirPath Path to the directory to iterate. + * @param fileExtensionFilter Only count files with this extension (e.g., ".list"). + * @param subtractOne Subtract one from the final count. + * @return Result containing the count (u64) or a DracError. + */ fn GetCountFromDirectory( const String& pmId, const fs::path& dirPath, - const String& fileExtensionFilter = "", - bool subtractOne = false + const String& fileExtensionFilter, + bool subtractOne ) -> Result; + /** + * @brief Gets package count by iterating entries in a directory, filtering by extension. + * @param pmId Identifier for the package manager (for logging/cache). + * @param dirPath Path to the directory to iterate. + * @param fileExtensionFilter Only count files with this extension (e.g., ".list"). + * @return Result containing the count (u64) or a DracError. Defaults subtractOne to false. + */ + fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath, const String& fileExtensionFilter) + -> Result; + + /** + * @brief Gets package count by iterating entries in a directory, optionally subtracting one. + * @param pmId Identifier for the package manager (for logging/cache). + * @param dirPath Path to the directory to iterate. + * @param subtractOne Subtract one from the final count. + * @return Result containing the count (u64) or a DracError. Defaults fileExtensionFilter to "". + */ + fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath, bool subtractOne) -> Result; + + /** + * @brief Gets package count by iterating all entries in a directory. + * @param pmId Identifier for the package manager (for logging/cache). + * @param dirPath Path to the directory to iterate. + * @return Result containing the count (u64) or a DracError. Defaults filter to "" and subtractOne to false. + */ + fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath) -> Result; + #ifdef __linux__ fn GetDpkgCount() -> Result; fn GetPacmanCount() -> Result; @@ -73,17 +110,26 @@ namespace packages { fn GetZypperCount() -> Result; fn GetPortageCount() -> Result; fn GetApkCount() -> Result; -#elif defined(__APPLE__) +#elifdef __APPLE__ fn GetHomebrewCount() -> Result; fn GetMacPortsCount() -> Result; -#elif defined(_WIN32) +#elifdef _WIN32 fn GetWinRTCount() -> Result; fn GetChocolateyCount() -> Result; fn GetScoopCount() -> Result; +#elif defined(__FreeBSD__) || defined(__DragonFly__) + fn GetPkgNgCount() -> Result; +#elifdef __NetBSD__ + fn GetPkgSrcCount() -> Result; +#elifdef __HAIKU__ + fn GetHaikuCount() -> Result; +#elifdef __serenity__ + fn GetSerenityCount() -> Result; #endif + // Common (potentially cross-platform) #ifndef _WIN32 fn GetNixCount() -> Result; #endif fn GetCargoCount() -> Result; -} // namespace packages \ No newline at end of file +} // namespace package diff --git a/src/core/system_data.cpp b/src/core/system_data.cpp index 70adeb7..6760c3e 100644 --- a/src/core/system_data.cpp +++ b/src/core/system_data.cpp @@ -7,6 +7,7 @@ #include "src/config/config.hpp" #include "src/config/weather.hpp" +#include "src/core/package.hpp" #include "src/os/os.hpp" #include "src/util/defs.hpp" #include "src/util/error.hpp" @@ -78,7 +79,7 @@ namespace os { Future> wmFut = std::async(async, GetWindowManager); Future> diskFut = std::async(async, GetDiskUsage); Future> shellFut = std::async(async, GetShell); - Future> pkgFut = std::async(async, GetPackageCount); + Future> pkgFut = std::async(async, package::GetTotalCount); Future> npFut = std::async(config.nowPlaying.enabled ? async : deferred, GetNowPlaying); Future> wthrFut = diff --git a/src/os/bsd/pkg_count.cpp b/src/os/bsd/pkg_count.cpp index c30c23b..de2501c 100644 --- a/src/os/bsd/pkg_count.cpp +++ b/src/os/bsd/pkg_count.cpp @@ -26,155 +26,22 @@ 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::StringView, util::types::Exception; -namespace { - using namespace std::chrono; - using namespace util::cache; - - using os::bsd::PackageManagerInfo, os::bsd::PkgCountCacheData; - - #ifdef __NetBSD__ - fn GetPackageCountInternalDir(const String& pmId, const fs::path& dirPath) -> Result { - 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())) - ); - } - - 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) { 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 (count > 0) - count--; - - return count; - } - #else - fn GetPackageCountInternalDb(const PackageManagerInfo& pmInfo) -> Result { - const auto& [pmId, dbPath, countQuery] = pmInfo; - - if (Result cachedDataResult = ReadCache(pmId)) { - const auto& [count, timestamp] = *cachedDataResult; - std::error_code errc; - const std::filesystem::file_time_type dbModTime = fs::last_write_time(dbPath, errc); - - if (errc) { - warn_log( - "Could not get modification time for '{}': {}. Invalidating {} cache.", dbPath.string(), errc.message(), pmId - ); - } else { - if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp)); - cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) { - debug_log( - "Using valid {} package count cache (DB file unchanged since {}).", - pmId, - std::format("{:%F %T %Z}", floor(cacheTimePoint)) - ); - return count; - } - debug_log("{} package count cache stale (DB file modified).", pmId); - } - } else { - if (cachedDataResult.error().code != DracErrorCode::NotFound) - debug_at(cachedDataResult.error()); - debug_log("{} package count cache not found or unreadable.", pmId); - } - - debug_log("Fetching fresh {} package count from database: {}", pmId, dbPath.string()); - u64 count = 0; - - try { - const SQLite::Database database(dbPath.string(), SQLite::OPEN_READONLY); - if (SQLite::Statement query(database, countQuery); query.executeStep()) { - const i64 countInt64 = query.getColumn(0).getInt64(); - if (countInt64 < 0) - return Err( - DracError(DracErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId)) - ); - count = static_cast(countInt64); - } else { - return Err(DracError(DracErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId))); - } - } catch (const SQLite::Exception& e) { - return Err(DracError( - DracErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what()) - )); - } 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(system_clock::now().time_since_epoch()).count(); - const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds }; - - if (Result writeResult = WriteCache(pmId, dataToCache); !writeResult) - error_at(writeResult.error()); - - return count; - } - #endif -} // namespace - -namespace os { - fn GetPackageCount() -> Result { - #ifdef __NetBSD__ - return GetPackageCountInternalDir("PkgSrc", fs::current_path().root_path() / "usr" / "pkg" / "pkgdb"); - #else +namespace package { + #if defined(__FreeBSD__) || defined(__DragonFly__) + fn GetPkgNgCount() -> Result { const PackageManagerInfo pkgInfo = { - .id = "pkg_count", + .id = "pkgng", // Use core struct .dbPath = "/var/db/pkg/local.sqlite", .countQuery = "SELECT COUNT(*) FROM packages", }; - if (std::error_code errc; !fs::exists(pkgInfo.dbPath, errc)) { - if (errc) { - warn_log("Filesystem error checking for pkg DB at '{}': {}", pkgInfo.dbPath.string(), errc.message()); - return Err(DracError(DracErrorCode::IoError, "Filesystem error checking Nix DB: " + errc.message())); - } - - return Err(DracError(DracErrorCode::ApiUnavailable, "pkg db not found: " + pkgInfo.dbPath.string())); - } - - return GetPackageCountInternalDb(pkgInfo); - #endif + return GetCountFromDb(pkgInfo); } -} // namespace os + #elif defined(__NetBSD__) + fn GetPkgSrcCount() -> Result { + return GetCountFromDirectory("pkgsrc", fs::current_path().root_path() / "usr" / "pkg" / "pkgdb", true); + } + #endif +} // namespace package #endif // __FreeBSD__ || __DragonFly__ || __NetBSD__ diff --git a/src/os/bsd/pkg_count.hpp b/src/os/bsd/pkg_count.hpp deleted file mode 100644 index 4f7515a..0000000 --- a/src/os/bsd/pkg_count.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -// clang-format off -#include // std::filesystem::path -#include // glz::object -#include // glz::detail::Object - -#include "src/util/error.hpp" -#include "src/util/types.hpp" -// clang-format on - -namespace os::bsd { - using util::error::DracError; - using util::types::Result, util::types::u64, util::types::i64, util::types::String; - namespace fs = std::filesystem; - - struct PackageManagerInfo { - 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) - }; -} // namespace os::bsd diff --git a/src/os/haiku.cpp b/src/os/haiku.cpp index 9dc2ee6..9b71d80 100644 --- a/src/os/haiku.cpp +++ b/src/os/haiku.cpp @@ -259,18 +259,6 @@ namespace os { .total_bytes = stat.f_blocks * stat.f_frsize, }; } - - fn GetPackageCount() -> Result { - BPackageKit::BPackageRoster roster; - BPackageKit::BPackageInfoSet packageList; - - const status_t status = roster.GetActivePackages(BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, packageList); - - if (status != B_OK) - return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get active package list")); - - return static_cast(packageList.CountInfos()); - } } // namespace os #endif // __HAIKU__ diff --git a/src/os/haiku/pkg_count.cpp b/src/os/haiku/pkg_count.cpp new file mode 100644 index 0000000..ecbd660 --- /dev/null +++ b/src/os/haiku/pkg_count.cpp @@ -0,0 +1,23 @@ +#ifdef __HAIKU__ + +// clang-format off +#include // BPackageKit::BPackageInfoSet +#include // BPackageKit::BPackageInfo +#include // BPackageKit::BPackageRoster +// clang-format on + +namespace package { + fn GetPackageCount() -> Result { + BPackageKit::BPackageRoster roster; + BPackageKit::BPackageInfoSet packageList; + + const status_t status = roster.GetActivePackages(BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, packageList); + + if (status != B_OK) + return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get active package list")); + + return static_cast(packageList.CountInfos()); + } +} // namespace package + +#endif // __HAIKU__ diff --git a/src/os/linux.cpp b/src/os/linux.cpp index 1b22c9a..dffcabc 100644 --- a/src/os/linux.cpp +++ b/src/os/linux.cpp @@ -27,7 +27,6 @@ #include "src/wrappers/wayland.hpp" #include "src/wrappers/xcb.hpp" -#include "linux/pkg_count.hpp" #include "os.hpp" // clang-format on @@ -471,17 +470,6 @@ namespace os { .total_bytes = stat.f_blocks * stat.f_frsize, }; } - - fn GetPackageCount() -> Result { - u64 count = 0; - - if (Result linuxCount = linux::GetTotalPackageCount()) - count += *linuxCount; - else - debug_at(linuxCount.error()); - - return count; - } } // namespace os #endif // __linux__ diff --git a/src/os/linux/pkg_count.cpp b/src/os/linux/pkg_count.cpp index 5ce2b7c..fa207d0 100644 --- a/src/os/linux/pkg_count.cpp +++ b/src/os/linux/pkg_count.cpp @@ -1,248 +1,49 @@ #ifdef __linux__ // clang-format off -#include "src/os/linux/pkg_count.hpp" +#include "src/core/package.hpp" #include // SQLite::{Database, OPEN_READONLY} #include // SQLite::Exception #include // SQLite::Statement -#include // std::chrono::{duration_cast, seconds, system_clock} #include // std::filesystem::{current_path, directory_entry, directory_iterator, etc.} -#include // std::format -#include // std::{async, launch} #include // glz::read_beve #include // glz::write_beve -#include // std::error_code -#include "src/util/cache.hpp" #include "src/util/defs.hpp" #include "src/util/error.hpp" -#include "src/util/logging.hpp" #include "src/util/types.hpp" // clang-format on -using util::error::DracError, util::error::DracErrorCode; +using util::error::DracError; using util::types::u64, util::types::i64, util::types::Result, util::types::Err, util::types::String, util::types::StringView, util::types::Exception; +using namespace std::string_literals; -namespace { - namespace fs = std::filesystem; - using namespace std::chrono; - using namespace util::cache; - using os::linux::PkgCountCacheData, os::linux::PackageManagerInfo; - - fn GetPackageCountInternalDir( - const String& pmId, - const fs::path& dirPath, - const String& file_extension_filter = "", - const bool subtract_one = false - ) -> Result { - 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())) - ); - } - - 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 package { + fn GetDpkgCount() -> Result { + return GetCountFromDirectory("Dpkg", fs::current_path().root_path() / "var" / "lib" / "dpkg" / "info", ".list"s); } - fn GetPackageCountInternalDb(const PackageManagerInfo& pmInfo) -> Result { - const auto& [pmId, dbPath, countQuery] = pmInfo; - - if (Result cachedDataResult = ReadCache(pmId)) { - const auto& [count, timestamp] = *cachedDataResult; - std::error_code errc; - const std::filesystem::file_time_type dbModTime = fs::last_write_time(dbPath, errc); - - if (errc) { - warn_log( - "Could not get modification time for '{}': {}. Invalidating {} cache.", dbPath.string(), errc.message(), pmId - ); - } else { - if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp)); - cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) { - debug_log( - "Using valid {} package count cache (DB file unchanged since {}).", - pmId, - std::format("{:%F %T %Z}", floor(cacheTimePoint)) - ); - return count; - } - debug_log("{} package count cache stale (DB file modified).", pmId); - } - } else { - if (cachedDataResult.error().code != DracErrorCode::NotFound) - debug_at(cachedDataResult.error()); - debug_log("{} package count cache not found or unreadable.", pmId); - } - - debug_log("Fetching fresh {} package count from database: {}", pmId, dbPath.string()); - u64 count = 0; - - try { - const SQLite::Database database(dbPath.string(), SQLite::OPEN_READONLY); - if (SQLite::Statement query(database, countQuery); query.executeStep()) { - const i64 countInt64 = query.getColumn(0).getInt64(); - if (countInt64 < 0) - return Err( - DracError(DracErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId)) - ); - count = static_cast(countInt64); - } else { - return Err(DracError(DracErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId))); - } - } catch (const SQLite::Exception& e) { - return Err(DracError( - DracErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what()) - )); - } 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(system_clock::now().time_since_epoch()).count(); - const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds }; - - if (Result writeResult = WriteCache(pmId, dataToCache); !writeResult) - error_at(writeResult.error()); - - return count; - } -} // namespace - -namespace os::linux { - fn GetDpkgPackageCount() -> Result { - return GetPackageCountInternalDir( - "Dpkg", fs::current_path().root_path() / "var" / "lib" / "dpkg" / "info", String(".list") - ); - } - - fn GetMossPackageCount() -> Result { - debug_log("Attempting to get Moss package count."); - + fn GetMossCount() -> Result { const PackageManagerInfo mossInfo = { .id = "moss", .dbPath = "/.moss/db/install", .countQuery = "SELECT COUNT(*) FROM meta", }; - if (std::error_code errc; !fs::exists(mossInfo.dbPath, errc)) { - if (errc) { - 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())); - } + Result countResult = GetCountFromDb(mossInfo); - return Err(DracError(DracErrorCode::ApiUnavailable, "Moss db not found: " + mossInfo.dbPath.string())); - } + if (countResult) + if (*countResult > 0) + return *countResult - 1; - Result countResult = GetPackageCountInternalDb(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; + return countResult; } - fn GetPacmanPackageCount() -> Result { - return GetPackageCountInternalDir( - "Pacman", fs::current_path().root_path() / "var" / "lib" / "pacman" / "local", "", true - ); + fn GetPacmanCount() -> Result { + return GetCountFromDirectory("Pacman", fs::current_path().root_path() / "var" / "lib" / "pacman" / "local", true); } - - fn GetTotalPackageCount() -> Result { - using util::types::Array, util::types::Future; - - Array>, 3> futures = { - std::async(std::launch::async, GetDpkgPackageCount), - std::async(std::launch::async, GetMossPackageCount), - std::async(std::launch::async, GetPacmanPackageCount), - }; - - u64 totalCount = 0; - - for (Future>& fut : futures) try { - if (Result 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; - } -} // namespace os::linux +} // namespace package #endif // __linux__ diff --git a/src/os/linux/pkg_count.hpp b/src/os/linux/pkg_count.hpp deleted file mode 100644 index 89fc7f9..0000000 --- a/src/os/linux/pkg_count.hpp +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once - -#ifdef __linux__ - -// clang-format off -#include // std::filesystem::path -#include // glz::object -#include // glz::detail::Object - -#include "src/util/defs.hpp" -#include "src/util/error.hpp" -#include "src/util/types.hpp" -// clang-format on - -namespace os::linux { - using util::error::DracError; - using util::types::Result, util::types::u64, util::types::i64, util::types::String; - namespace fs = std::filesystem; - - struct PackageManagerInfo { - 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; - - // Get package count from RPM (Red Hat/Fedora/CentOS) - fn GetRpmPackageCount() -> Result; - - // Get package count from pacman (Arch Linux) - fn GetPacmanPackageCount() -> Result; - - // Get package count from Portage (Gentoo) - fn GetPortagePackageCount() -> Result; - - // Get package count from zypper (openSUSE) - fn GetZypperPackageCount() -> Result; - - // Get package count from apk (Alpine) - fn GetApkPackageCount() -> Result; - - // Get package count from moss (AerynOS) - fn GetMossPackageCount() -> Result; - - // Get package count from nix - fn GetNixPackageCount() -> Result; - - // Get package count from flatpak - fn GetFlatpakPackageCount() -> Result; - - // Get package count from snap - fn GetSnapPackageCount() -> Result; - - // Get package count from AppImage - fn GetAppimagePackageCount() -> Result; - - // Get total package count from all available package managers - fn GetTotalPackageCount() -> Result; -} // namespace os::linux - -#endif // __linux__ diff --git a/src/os/os.hpp b/src/os/os.hpp index ad9609e..4ca8cf9 100644 --- a/src/os/os.hpp +++ b/src/os/os.hpp @@ -84,15 +84,6 @@ namespace os { */ fn GetKernelVersion() -> Result; - /** - * @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 a DracError on failure (e.g., permission errors, command not found) - * or if not supported (DracErrorCode::NotSupported). - */ - fn GetPackageCount() -> Result; - /** * @brief Gets the disk usage for the primary/root filesystem. * @details Uses statvfs on Linux/macOS, GetDiskFreeSpaceExW on Windows. diff --git a/src/util/cache.hpp b/src/util/cache.hpp index c42e9d7..b43e9d1 100644 --- a/src/util/cache.hpp +++ b/src/util/cache.hpp @@ -6,7 +6,6 @@ #include // glz::write_beve #include // glz::{context, error_code, error_ctx} #include // std::istreambuf_iterator -#include // std::string #include // std::error_code #include // std::decay_t