mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-28 07:44:06 +01:00
Packages (Windows): caches winget packages count; enables winget by default
This commit is contained in:
@@ -15,3 +15,4 @@ fastfetch.kdev4
|
||||
*.log
|
||||
/*.zh.md
|
||||
/.work*
|
||||
/doc/*.md
|
||||
|
||||
+3
-1
@@ -59,7 +59,9 @@ Features:
|
||||
* The version is now read from the `COSMIC_VERSION` environment variable when it is set.
|
||||
* Improved accuracy and performance of process name detection in the Top module. (Top, macOS)
|
||||
* Improved Packages detection on Windows (Packages, Windows)
|
||||
* `winget list` is now invoked with `--source winget`, so only packages installed by winget itself are counted, and the slow msstore HTTP round trips are skipped.
|
||||
* `winget` is now detected by default, instead of requiring a build-time opt-in, and the source agreements are accepted non-interactively on a machine that has never accepted them. Opt out by setting `packages.disabled` to `["winget"]`, or by configuring with `cmake -DPACKAGES_DISABLE_WINGET=ON`.
|
||||
* The count reports the packages the `winget` source knows about, which is not limited to the packages `winget` itself installed. Microsoft Store apps are no longer counted.
|
||||
* `winget list` takes about 1.5 seconds, so its result is cached. The cache is refreshed when a program is installed or removed, not on a timer, so only the first run after such a change is slow.
|
||||
* Improved Wallpaper detection on macOS Sonoma and later (#2559, Wallpaper, macOS)
|
||||
* The image path is now also extracted from the `Configuration` field of the wallpaper plist, and the `NSWorkspace` fallback is used only as a last resort.
|
||||
* Removed the `kvm` dependency on OpenBSD by using `sysctl` directly. (General, OpenBSD)
|
||||
|
||||
+1
-5
@@ -161,11 +161,7 @@ string(REGEX MATCHALL "FF_PACKAGES_FLAG_[A-Z]+_BIT" PACKAGE_MANAGERS "${PACKAGE_
|
||||
string(REGEX REPLACE "FF_PACKAGES_FLAG_([A-Z]+)_BIT" "\\1" PACKAGE_MANAGERS "${PACKAGE_MANAGERS}")
|
||||
list(SORT PACKAGE_MANAGERS)
|
||||
foreach(package_manager ${PACKAGE_MANAGERS})
|
||||
if(package_manager STREQUAL "WINGET")
|
||||
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" ON)
|
||||
else()
|
||||
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" OFF)
|
||||
endif()
|
||||
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" OFF)
|
||||
endforeach()
|
||||
option(PACKAGES_REMOVE_DISABLED "Remove disabled package managers from the build entirely, instead of just skipping them at runtime" OFF)
|
||||
|
||||
|
||||
@@ -3812,7 +3812,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": ["winget"]
|
||||
"default": []
|
||||
},
|
||||
"combined": {
|
||||
"description": "Whether to combine related package managers into single counts (e.g., nix-system + nix-user = nix)",
|
||||
|
||||
+17
-13
@@ -1307,19 +1307,23 @@ typedef struct _KEY_BASIC_INFORMATION {
|
||||
_Field_size_bytes_(NameLength) WCHAR Name[]; // The name of the registry key. This string is not null-terminated.
|
||||
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
|
||||
|
||||
typedef struct _KEY_FULL_INFORMATION {
|
||||
LARGE_INTEGER LastWriteTime;
|
||||
ULONG TitleIndex;
|
||||
ULONG ClassOffset;
|
||||
ULONG ClassLength;
|
||||
ULONG SubKeys;
|
||||
ULONG MaxNameLength;
|
||||
ULONG MaxClassLength;
|
||||
ULONG Values;
|
||||
ULONG MaxValueNameLength;
|
||||
ULONG MaxValueDataLength;
|
||||
WCHAR Class[];
|
||||
} KEY_FULL_INFORMATION, *PKEY_FULL_INFORMATION;
|
||||
typedef struct _KEY_CACHED_INFORMATION {
|
||||
LARGE_INTEGER LastWriteTime; // Number of 100-nanosecond intervals since this key or any of its values changed.
|
||||
ULONG TitleIndex; // Reserved // A legacy field originally intended for use with localization such as an index of a resource table.
|
||||
ULONG SubKeys; // The number of subkeys for a key.
|
||||
ULONG MaxNameLen; // The maximum length, in bytes, of a subkey name.
|
||||
ULONG Values; // The number of value entries.
|
||||
ULONG MaxValueNameLen; // The maximum length, in bytes, of a value entry name.
|
||||
ULONG MaxValueDataLen; // The maximum length, in bytes, of a value entry data field.
|
||||
ULONG NameLength; // Size, in bytes, of the key's own (leaf) name, without the terminator. The name itself is not stored.
|
||||
} KEY_CACHED_INFORMATION, *PKEY_CACHED_INFORMATION;
|
||||
// 36 bytes of members, but LARGE_INTEGER makes the struct 8-byte aligned, and that is exactly what
|
||||
// the kernel demands: a 36-byte buffer is rejected with STATUS_BUFFER_TOO_SMALL (measured).
|
||||
// The kernel writes all 40 bytes -- the 4 padding bytes included, zeroed -- and reports
|
||||
// ResultLength = 40 whatever size you pass, so a larger buffer buys nothing (measured 40..4096).
|
||||
// This is the only information class that never appends a string: NameLength is the length of the
|
||||
// key's own (leaf) name in bytes without the terminator, and the name itself is not returned.
|
||||
static_assert(sizeof(KEY_CACHED_INFORMATION) == 40, "KEY_CACHED_INFORMATION should be 40 bytes");
|
||||
|
||||
NTSYSAPI NTSTATUS NTAPI NtQueryKey(
|
||||
_In_ HANDLE KeyHandle,
|
||||
|
||||
@@ -355,6 +355,10 @@ bool ffRegGetSubKey(HANDLE hKey, uint32_t index, FFstrbuf* result, FFstrbuf* err
|
||||
ULONG bufSize = (ULONG) sizeof(buffer);
|
||||
KEY_BASIC_INFORMATION* keyInfo = (KEY_BASIC_INFORMATION*) buffer;
|
||||
|
||||
// Every failure mode fails NT_SUCCESS, and the read below depends on that: on a too-small buffer
|
||||
// the kernel returns STATUS_BUFFER_OVERFLOW, writes the *full* NameLength but copies only part of
|
||||
// the name (measured), so treating that status as success would read past what was written.
|
||||
// The longest name the CM accepts, 255 characters, still fits: 255 * 2 + 16 <= sizeof(buffer).
|
||||
if (!NT_SUCCESS(NtEnumerateKey(hKey, index, KeyBasicInformation, keyInfo, bufSize, &bufSize))) {
|
||||
FF_DEBUG("NtEnumerateKey(hKey=%p, index=%u) failed", hKey, (unsigned) index);
|
||||
if (error) {
|
||||
@@ -367,22 +371,26 @@ bool ffRegGetSubKey(HANDLE hKey, uint32_t index, FFstrbuf* result, FFstrbuf* err
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ffRegGetNSubKeys(HANDLE hKey, uint32_t* result, FFstrbuf* error) {
|
||||
// Reads the counters the key object already caches. KeyFullInformation would return the same numbers
|
||||
// but also marshals the key's class string out; measured per query on the ARP and MSIX keys:
|
||||
// 837 ns vs 895 ns (interleaved, 200k queries each). The open dominates the total -- 22.10 us vs
|
||||
// 22.40 us for open + query -- so this is a small win, not a big one.
|
||||
//
|
||||
// The kernel writes exactly sizeof(KEY_CACHED_INFORMATION) bytes and rejects a smaller buffer with
|
||||
// STATUS_BUFFER_TOO_SMALL without writing anything, so pass a whole KEY_CACHED_INFORMATION.
|
||||
bool ffRegQueryKey(HANDLE hKey, KEY_CACHED_INFORMATION* result, FFstrbuf* error) {
|
||||
assert(hKey);
|
||||
assert(result);
|
||||
|
||||
alignas(KEY_FULL_INFORMATION) uint8_t buffer[sizeof(KEY_FULL_INFORMATION) + MAX_PATH * sizeof(wchar_t)];
|
||||
ULONG bufSize = sizeof(buffer);
|
||||
KEY_FULL_INFORMATION* keyInfo = (KEY_FULL_INFORMATION*) buffer;
|
||||
ULONG bufSize = (ULONG) sizeof(*result);
|
||||
|
||||
if (!NT_SUCCESS(NtQueryKey(hKey, KeyFullInformation, keyInfo, bufSize, &bufSize))) {
|
||||
FF_DEBUG("NtQueryKey(hKey=%p, KeyFullInformation) failed", hKey);
|
||||
if (!NT_SUCCESS(NtQueryKey(hKey, KeyCachedInformation, result, bufSize, &bufSize))) {
|
||||
FF_DEBUG("NtQueryKey(hKey=%p, KeyCachedInformation) failed", hKey);
|
||||
if (error) {
|
||||
ffStrbufAppendF(error, "NtQueryKey(hKey=%p, KeyFullInformation, keyInfo) failed", hKey);
|
||||
ffStrbufAppendF(error, "NtQueryKey(hKey=%p, KeyCachedInformation, keyInfo) failed", hKey);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
*result = (uint32_t) keyInfo->SubKeys;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "fastfetch.h"
|
||||
#include "common/argType.h"
|
||||
#include "common/io.h"
|
||||
#include "common/windows/nt.h"
|
||||
|
||||
#ifndef HKEY_CURRENT_USER
|
||||
#define HKEY_CLASSES_ROOT ((HKEY) (ULONG_PTR) ((LONG) 0x80000000))
|
||||
@@ -26,7 +27,9 @@ bool ffRegOpenSubkeyForRead(HANDLE hKey, const wchar_t* subKeyW, HANDLE* result,
|
||||
bool ffRegReadValue(HANDLE hKey, const FFRegValueArg* arg, FFstrbuf* error);
|
||||
bool ffRegReadValues(HANDLE hKey, uint32_t argc, const FFRegValueArg argv[], FFstrbuf* error);
|
||||
bool ffRegGetSubKey(HANDLE hKey, uint32_t index, FFstrbuf* result, FFstrbuf* error);
|
||||
bool ffRegGetNSubKeys(HANDLE hKey, uint32_t* result, FFstrbuf* error);
|
||||
// Fills `result` with the counters the key object caches, including LastWriteTime: the number of
|
||||
// 100-nanosecond intervals since this key or any of its values was last changed (FILETIME epoch).
|
||||
bool ffRegQueryKey(HANDLE hKey, KEY_CACHED_INFORMATION* result, FFstrbuf* error);
|
||||
|
||||
static inline bool ffRegOpenKeyForRead(HKEY hRootKey, const wchar_t* subKeyW, HANDLE* result, FFstrbuf* error) {
|
||||
return ffRegOpenSubkeyForRead(ffRegGetRootKeyHandle(hRootKey), subKeyW, result, error);
|
||||
|
||||
@@ -16,6 +16,33 @@ const char* ffDetectPackages(FFPackagesResult* result, FFPackagesOptions* option
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ffPackagesReadCacheKey(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint64_t cacheKey, const char* packageId, uint32_t* result) {
|
||||
if (__builtin_expect(cacheKey == 0, false)) {
|
||||
// The caller could not compute a cache key (its data source is unreadable). Nothing may be
|
||||
// cached under an unknown key, and reporting 0 would be a lie. Leaving `cacheContent` empty
|
||||
// makes the matching ffPackagesWriteCache() call a no-op.
|
||||
return false;
|
||||
}
|
||||
|
||||
ffStrbufSet(cacheDir, &instance.state.platform.cacheDir);
|
||||
ffStrbufEnsureEndsWithC(cacheDir, '/');
|
||||
ffStrbufAppendF(cacheDir, "fastfetch/packages/%s.txt", packageId);
|
||||
|
||||
if (ffReadFileBuffer(cacheDir->chars, cacheContent)) {
|
||||
uint64_t key_cached;
|
||||
uint32_t num_cached;
|
||||
if (sscanf(cacheContent->chars, "%" SCNu64 " %" SCNu32, &key_cached, &num_cached) == 2 &&
|
||||
key_cached == cacheKey && num_cached > 0) {
|
||||
*result = num_cached;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ffStrbufSetF(cacheContent, "%" PRIu64 " ", cacheKey);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ffPackagesReadCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, const char* filePath, const char* packageId, uint32_t* result) {
|
||||
const uint64_t mtime_current = ffPathGetMtime(filePath);
|
||||
if (__builtin_expect(mtime_current == 0, false)) {
|
||||
@@ -31,23 +58,7 @@ bool ffPackagesReadCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, const char*
|
||||
return true;
|
||||
}
|
||||
|
||||
ffStrbufSet(cacheDir, &instance.state.platform.cacheDir);
|
||||
ffStrbufEnsureEndsWithC(cacheDir, '/');
|
||||
ffStrbufAppendF(cacheDir, "fastfetch/packages/%s.txt", packageId);
|
||||
|
||||
if (ffReadFileBuffer(cacheDir->chars, cacheContent)) {
|
||||
uint64_t mtime_cached;
|
||||
uint32_t num_cached;
|
||||
if (sscanf(cacheContent->chars, "%" SCNu64 " %" SCNu32, &mtime_cached, &num_cached) == 2 &&
|
||||
mtime_cached == mtime_current && num_cached > 0) {
|
||||
*result = num_cached;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ffStrbufSetF(cacheContent, "%" PRIu64 " ", mtime_current);
|
||||
|
||||
return false;
|
||||
return ffPackagesReadCacheKey(cacheDir, cacheContent, mtime_current, packageId, result);
|
||||
}
|
||||
|
||||
bool ffPackagesWriteCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint32_t num_elements) {
|
||||
|
||||
@@ -65,6 +65,7 @@ typedef struct FFPackagesResult {
|
||||
|
||||
const char* ffDetectPackages(FFPackagesResult* result, FFPackagesOptions* options);
|
||||
bool ffPackagesReadCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, const char* filePath, const char* packageId, uint32_t* result);
|
||||
bool ffPackagesReadCacheKey(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint64_t cacheKey, const char* packageId, uint32_t* result);
|
||||
bool ffPackagesWriteCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint32_t num_elements);
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__) || defined(__GNU__)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "common/strutil.h"
|
||||
#include "common/path.h"
|
||||
#include "common/windows/unicode.h"
|
||||
#include "common/windows/registry.h"
|
||||
#include "common/mallocHelper.h"
|
||||
#include "common/io.h"
|
||||
|
||||
@@ -156,6 +157,61 @@ static void detectPacman(FFPackagesResult* result) {
|
||||
result->pacman = getNumElements(pacmanPath, FILE_ATTRIBUTE_DIRECTORY, nullptr);
|
||||
}
|
||||
|
||||
// The newest last-write time among the registry keys that hold the set of installed programs.
|
||||
//
|
||||
// winget is the only package manager on Windows whose count cannot be read from a local index:
|
||||
// `winget list` has to spawn a process (~1.5 s), so its result is cached. The obvious cache key --
|
||||
// the modification time of winget's own database -- does not work. `installed.db` is a
|
||||
// `PackageTrackingCatalog`: it only records the installs winget performed itself. Measured on a
|
||||
// machine with 101 listed packages: it holds 46 ids, and 33 of the listed ones are absent from it.
|
||||
// Uninstalling a package through Settings > Apps leaves the file completely untouched (same mtime,
|
||||
// same size, same row count) while `winget list` loses a row.
|
||||
//
|
||||
// The sources `winget list` correlates against are the ARP registry and the MSIX package registry.
|
||||
// Adding or removing a program bumps the last-write time of the parent key it lives under, so the
|
||||
// newest of these four changes exactly when the count may have changed.
|
||||
// All four must be read: a program may register in either ARP view, or be an MSIX package.
|
||||
//
|
||||
// The keys are read through registry.h, like every other Windows detection module does. There the
|
||||
// 32-bit view has to be addressed as a path: NtOpenKey ignores KEY_WOW64_32KEY -- it hands back the
|
||||
// 64-bit key with the flag set just as with it cleared (measured), the redirect is done by the Win32
|
||||
// Reg* wrappers. What the redirector resolves to is the physical `SOFTWARE\WOW6432Node\...`, so
|
||||
// opening that path is equivalent to asking for KEY_WOW64_32KEY.
|
||||
static uint64_t getInstalledProgramsFingerprint(void) {
|
||||
static const struct {
|
||||
HKEY root;
|
||||
const wchar_t* path;
|
||||
} locations[] = {
|
||||
{ HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" },
|
||||
#ifdef _WIN64
|
||||
// A 64-bit program may still register in the 32-bit view -- the installer decides. Geekbench 6
|
||||
// does, and reading only the 64-bit view misses its uninstall entirely.
|
||||
//
|
||||
// A 32-bit fastfetch is supported on 32-bit Windows only (a 32-bit process on 64-bit Windows
|
||||
// would need KEY_WOW64_64KEY, which this API cannot express): there the key above is the one
|
||||
// and only ARP view, and this one does not exist.
|
||||
{ HKEY_LOCAL_MACHINE, L"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall" },
|
||||
#endif
|
||||
{ HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" },
|
||||
{ HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModel\\StateRepository\\Cache\\Package\\Data" },
|
||||
};
|
||||
|
||||
uint64_t newest = 0;
|
||||
for (size_t i = 0; i < ARRAY_SIZE(locations); ++i) {
|
||||
FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
|
||||
if (!ffRegOpenKeyForRead(locations[i].root, locations[i].path, &hKey, nullptr)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KEY_CACHED_INFORMATION info = {};
|
||||
if (ffRegQueryKey(hKey, &info, nullptr) && (uint64_t) info.LastWriteTime.QuadPart > newest) {
|
||||
newest = (uint64_t) info.LastWriteTime.QuadPart;
|
||||
}
|
||||
}
|
||||
|
||||
return newest;
|
||||
}
|
||||
|
||||
static void detectWinget(FFPackagesResult* result) {
|
||||
// Why not read winget's own database instead of shelling out?
|
||||
// `%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\<source>\installed.db`
|
||||
@@ -166,6 +222,20 @@ static void detectWinget(FFPackagesResult* result) {
|
||||
// and reading it would drag in a SQLite dependency.
|
||||
// The authoritative set of installed packages comes from the ARP registry and MSIX, which is exactly
|
||||
// what `winget list` enumerates before correlating it against the read-only source index.
|
||||
|
||||
// The fingerprint is read once, before the process runs, and reused as the cache key on a miss.
|
||||
// `winget list` writes to none of those keys, so a stored key stays valid until a program changes.
|
||||
const uint64_t cacheKey = getInstalledProgramsFingerprint();
|
||||
|
||||
FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate();
|
||||
FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate();
|
||||
|
||||
uint32_t count;
|
||||
if (ffPackagesReadCacheKey(&cacheDir, &cacheContent, cacheKey, "winget", &count)) {
|
||||
result->winget = count;
|
||||
return;
|
||||
}
|
||||
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
if (ffProcessAppendStdOut(&buffer, (char*[]) {
|
||||
"winget.exe",
|
||||
@@ -177,8 +247,13 @@ static void detectWinget(FFPackagesResult* result) {
|
||||
"--source",
|
||||
"winget",
|
||||
"--disable-interactivity",
|
||||
// Accepting is a no-op once it has been done. Without it, `winget list`
|
||||
// exits with code 1 on a machine that has never accepted the source
|
||||
// agreements, and this module would report no packages at all.
|
||||
"--accept-source-agreements",
|
||||
nullptr,
|
||||
})) {
|
||||
// A failed detection is not cached: only ffPackagesWriteCache() writes, and it is not reached.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,7 +262,7 @@ static void detectWinget(FFPackagesResult* result) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t count = 0;
|
||||
count = 0;
|
||||
for (
|
||||
index += strlen("--\r\n");
|
||||
(index = ffStrbufNextIndexC(&buffer, index, '\n')) < buffer.length;
|
||||
@@ -200,6 +275,8 @@ static void detectWinget(FFPackagesResult* result) {
|
||||
}
|
||||
|
||||
result->winget = count;
|
||||
|
||||
ffPackagesWriteCache(&cacheDir, &cacheContent, count);
|
||||
}
|
||||
|
||||
void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) {
|
||||
|
||||
Reference in New Issue
Block a user