diff --git a/CHANGELOG.md b/CHANGELOG.md index b07f5e616..282914047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ Bugfixes: * Worked around an iTerm bug where the image logo may be moved out of view. (Logo) * Increased the terminal response wait time to 1s to improve compatibility with slower terminals. (General) * Fixed the `PublicIp` and `Weather` modules exiting with "can only be used once due to internal limitations" on the round after a failed request when `--dynamic-interval` is used, instead of retrying. (PublicIp / Weather) +* Fixed the `Display`, `Monitor`, `WM`, `DE`, `Media`, `Player`, `Shell` and `Terminal` modules replaying the first round's result when `--dynamic-interval` is used. The detection results that are cached for the whole run are now dropped between rounds, so these modules detect again on every round. (Display / Monitor / WM / DE / Media / Player / Shell / Terminal) * Some internal cleanups and optimizations. Logos: diff --git a/CMakeLists.txt b/CMakeLists.txt index 814ef080e..81ef1afd9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -462,6 +462,7 @@ set(LIBFASTFETCH_SRC src/common/impl/time.c src/common/impl/edidHelper.c src/common/impl/base64.c + src/common/impl/FFcache.c src/common/impl/FFlist.c src/common/impl/FFstrbuf.c src/common/impl/path.c @@ -2197,6 +2198,13 @@ endif() ################### if (BUILD_TESTS) + add_executable(fastfetch-test-cache + tests/cache.c + ) + target_link_libraries(fastfetch-test-cache + PRIVATE libfastfetch + ) + add_executable(fastfetch-test-strbuf tests/strbuf.c ) diff --git a/src/common/FFcache.h b/src/common/FFcache.h new file mode 100644 index 000000000..a75ab6d55 --- /dev/null +++ b/src/common/FFcache.h @@ -0,0 +1,48 @@ +#pragma once + +#include "fastfetch.h" + +// A cache for a value that is built at most once per generation. +// +// The value itself is owned by the entry owner (usually a file scope `static`); the entry only +// remembers whether that value is still valid and knows how to drop it. `--dynamic-interval` +// starts a new generation for every round, dropping all the values that have been built so far, +// so the modules re-detect them instead of replaying the first round's snapshot. +// +// An entry registers itself the first time it is used. There is therefore no central list to keep +// in sync, and a cache cannot be forgotten in it. Do not use this type for state that is carried +// across calls to derive a rate (cpuusage, diskio, netio, top): that is a baseline rather than a +// snapshot, and dropping it makes the first sample of a round read as zero. +// +// The storage pointed to by `storage` must stay valid for the whole run; only the value behind it +// is dropped by an invalidation. +// +// Not thread safe: entries are only touched from a single thread, on a frame boundary, while the +// modules are not running. +typedef struct FFcacheEntry { + const char* name; // For diagnostics. + void* storage; // Owned by the entry owner. May be nullptr. + void (*init)(void* storage); // Optional. Runs whenever the entry is stale. + void (*destroy)(void* storage); // Optional. Must be idempotent, and tolerate a partially initialized storage. + bool initialized; + bool registered; + struct FFcacheEntry* next; // Internal: registry link. +} FFcacheEntry; + +// Returns `entry->storage`, running `init` on it first when the entry is stale. The entry is marked +// initialized before `init` runs, so a call made from within `init` returns the partially built +// value instead of recursing. +[[gnu::nonnull(1)]] void* ffCacheGet(FFcacheEntry* entry); + +// Staleness test for entries whose initialization needs arguments that are not part of the cache +// identity (such as the cover file request of `ffDetectMedia`) and therefore cannot live behind +// `init(void*)`. Returns true exactly once per generation: the caller must then build the value +// itself. +[[gnu::nonnull(1), nodiscard]] bool ffCacheBeginInit(FFcacheEntry* entry); + +// Drops the cached value of one entry, so that the next request builds it again. Idempotent, and +// safe on an entry that has never been used. +[[gnu::nonnull(1)]] void ffCacheInvalidate(FFcacheEntry* entry); + +// Drops every value that has been built so far. Called once per `--dynamic-interval` round. +void ffCacheInvalidateAll(void); diff --git a/src/common/impl/FFcache.c b/src/common/impl/FFcache.c new file mode 100644 index 000000000..9f1c4f51b --- /dev/null +++ b/src/common/impl/FFcache.c @@ -0,0 +1,53 @@ +#include "common/FFcache.h" + +// Entries are file scope statics owned by the cache owner, so they can be linked into a list +// without any allocation. The list holds every entry that has been used at least once: an entry +// that was never used has nothing to drop. +static FFcacheEntry* entries = nullptr; + +static void registerEntry(FFcacheEntry* entry) { + if (entry->registered) { + return; + } + + entry->registered = true; + entry->next = entries; + entries = entry; +} + +void* ffCacheGet(FFcacheEntry* entry) { + if (ffCacheBeginInit(entry) && entry->init) { + entry->init(entry->storage); + } + + return entry->storage; +} + +bool ffCacheBeginInit(FFcacheEntry* entry) { + registerEntry(entry); + + if (entry->initialized) { + return false; + } + + entry->initialized = true; + return true; +} + +void ffCacheInvalidate(FFcacheEntry* entry) { + if (!entry->initialized) { + return; + } + + entry->initialized = false; + + if (entry->destroy) { + entry->destroy(entry->storage); + } +} + +void ffCacheInvalidateAll(void) { + for (FFcacheEntry* entry = entries; entry; entry = entry->next) { + ffCacheInvalidate(entry); + } +} diff --git a/src/common/impl/netif.c b/src/common/impl/netif.c index 1525a185a..6ca976251 100644 --- a/src/common/impl/netif.c +++ b/src/common/impl/netif.c @@ -1,22 +1,47 @@ #include "common/netif.h" +#include "common/FFcache.h" -#ifndef _WIN32 - #include - #include -#endif +#include + +// netio, localip and dns resolve the default route on every round, and it can change while a +// `--dynamic-interval` run is going on: a laptop switching from Wi-Fi to Ethernet, a VPN coming up. +static FFNetifDefaultRouteResult defaultRouteV4; +static FFNetifDefaultRouteResult defaultRouteV6; + +static void resetDefaultRoute(void* storage) { + // The impls only write the fields they found, so without this a round that finds no default + // route would keep reporting the interface of the previous one. + memset(storage, 0, sizeof(FFNetifDefaultRouteResult)); +} + +static void initDefaultRouteV4(void* storage) { + FFNetifDefaultRouteResult* result = storage; + result->status = ffNetifGetDefaultRouteImplV4(result) ? FF_NETIF_OK : FF_NETIF_INVALID; +} + +static void initDefaultRouteV6(void* storage) { + FFNetifDefaultRouteResult* result = storage; + result->status = ffNetifGetDefaultRouteImplV6(result) ? FF_NETIF_OK : FF_NETIF_INVALID; +} + +static FFcacheEntry ffCacheEntryDefaultRouteV4 = { + .name = "default route V4", + .storage = &defaultRouteV4, + .init = initDefaultRouteV4, + .destroy = resetDefaultRoute, +}; + +static FFcacheEntry ffCacheEntryDefaultRouteV6 = { + .name = "default route V6", + .storage = &defaultRouteV6, + .init = initDefaultRouteV6, + .destroy = resetDefaultRoute, +}; const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV4(void) { - static FFNetifDefaultRouteResult result; - if (result.status == FF_NETIF_UNINITIALIZED) { - result.status = ffNetifGetDefaultRouteImplV4(&result) ? FF_NETIF_OK : FF_NETIF_INVALID; - } - return &result; + return ffCacheGet(&ffCacheEntryDefaultRouteV4); } const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV6(void) { - static FFNetifDefaultRouteResult result; - if (result.status == FF_NETIF_UNINITIALIZED) { - result.status = ffNetifGetDefaultRouteImplV6(&result) ? FF_NETIF_OK : FF_NETIF_INVALID; - } - return &result; + return ffCacheGet(&ffCacheEntryDefaultRouteV6); } diff --git a/src/detection/displayserver/displayserver.c b/src/detection/displayserver/displayserver.c index b5cc8eb7d..5e7265ab7 100644 --- a/src/detection/displayserver/displayserver.c +++ b/src/detection/displayserver/displayserver.c @@ -1,4 +1,5 @@ #include "displayserver.h" +#include "common/FFcache.h" FFDisplayResult* ffdsAppendDisplay( FFDisplayServerResult* result, @@ -50,18 +51,45 @@ FFDisplayResult* ffdsAppendDisplay( void ffConnectDisplayServerImpl(FFDisplayServerResult* ds); -const FFDisplayServerResult* ffConnectDisplayServer() { - static FFDisplayServerResult result; - static bool initialized = false; - if (!initialized) { - initialized = true; - ffStrbufInit(&result.wmProcessName); - ffStrbufInit(&result.wmPrettyName); - ffStrbufInit(&result.wmProtocolName); - ffStrbufInit(&result.deProcessName); - ffStrbufInit(&result.dePrettyName); - ffListInit(&result.displays); - ffConnectDisplayServerImpl(&result); - } - return &result; +static FFDisplayServerResult result; + +static void initDisplayServerResult(void* storage) { + FFDisplayServerResult* ds = storage; + + ffStrbufInit(&ds->wmProcessName); + ffStrbufInit(&ds->wmPrettyName); + ffStrbufInit(&ds->wmProtocolName); + ffStrbufInit(&ds->deProcessName); + ffStrbufInit(&ds->dePrettyName); + ffListInit(&ds->displays); + + ffConnectDisplayServerImpl(ds); +} + +static void destroyDisplayServerResult(void* storage) { + FFDisplayServerResult* ds = storage; + + ffStrbufDestroy(&ds->wmProcessName); + ffStrbufDestroy(&ds->wmPrettyName); + ffStrbufDestroy(&ds->wmProtocolName); + ffStrbufDestroy(&ds->deProcessName); + ffStrbufDestroy(&ds->dePrettyName); + + // Every display owns its name and serial, which are lost if the list is dropped as a whole + FF_LIST_FOR_EACH (FFDisplayResult, display, ds->displays) { + ffStrbufDestroy(&display->name); + ffStrbufDestroy(&display->serial); + } + ffListDestroy(&ds->displays); +} + +static FFcacheEntry ffCacheEntryDisplayServer = { + .name = "displayServer", + .storage = &result, + .init = initDisplayServerResult, + .destroy = destroyDisplayServerResult, +}; + +const FFDisplayServerResult* ffConnectDisplayServer() { + return ffCacheGet(&ffCacheEntryDisplayServer); } diff --git a/src/detection/gtk_qt/gtk.c b/src/detection/gtk_qt/gtk.c index 7c9638dbd..11b2a26b8 100644 --- a/src/detection/gtk_qt/gtk.c +++ b/src/detection/gtk_qt/gtk.c @@ -1,7 +1,7 @@ #include "fastfetch.h" #include "common/properties.h" -#include "common/thread.h" #include "common/settings.h" +#include "common/FFcache.h" #include "detection/gtk_qt/gtk_qt.h" #include "detection/displayserver/displayserver.h" @@ -11,29 +11,42 @@ static inline bool allPropertiesSet(FFGTKResult* result) { result->font.length > 0; } -static inline void applyGTKSettings(FFGTKResult* result, const char* themeName, const char* iconsName, const char* fontName, const char* cursorTheme, int cursorSize, const char* wallpaper) { +// The values read from the desktop settings daemons, shared by the GTK2/3/4 results. They are +// owned copies on purpose: `settings.c` hands out strings it leaks deliberately (see the +// "Leaks value.chars" notes there), so borrowing them past a `--dynamic-interval` round would +// leak one set per round. Copying them also lets `destroy` release them. +typedef struct FFGTKSettings { + FFstrbuf theme; + FFstrbuf icons; + FFstrbuf font; + FFstrbuf cursor; + FFstrbuf wallpaper; + int32_t cursorSize; +} FFGTKSettings; + +static inline void applyGTKSettings(FFGTKResult* result, const FFGTKSettings* settings) { if (result->theme.length == 0) { - ffStrbufAppendS(&result->theme, themeName); + ffStrbufAppend(&result->theme, &settings->theme); } if (result->icons.length == 0) { - ffStrbufAppendS(&result->icons, iconsName); + ffStrbufAppend(&result->icons, &settings->icons); } if (result->font.length == 0) { - ffStrbufAppendS(&result->font, fontName); + ffStrbufAppend(&result->font, &settings->font); } if (result->cursor.length == 0) { - ffStrbufAppendS(&result->cursor, cursorTheme); + ffStrbufAppend(&result->cursor, &settings->cursor); } - if (result->cursorSize.length == 0 && cursorSize > 0) { - ffStrbufAppendF(&result->cursorSize, "%i", cursorSize); + if (result->cursorSize.length == 0 && settings->cursorSize > 0) { + ffStrbufAppendSInt(&result->cursorSize, settings->cursorSize); } if (result->wallpaper.length == 0) { - ffStrbufAppendS(&result->wallpaper, wallpaper); + ffStrbufAppend(&result->wallpaper, &settings->wallpaper); } } @@ -43,22 +56,23 @@ static bool testXfconfWallpaperPropKey([[maybe_unused]] void* data, const char* return count == 0; } -static void detectGTKFromSettings(FFGTKResult* result) { - static const char* themeName = nullptr; - static const char* iconsName = nullptr; - static const char* fontName = nullptr; - static const char* cursorTheme = nullptr; - static int cursorSize = 0; - static const char* wallpaper = nullptr; +static FFGTKSettings gtkSettings; - static bool init = false; +static void initGTKSettings(void* storage) { + FFGTKSettings* settings = storage; - if (init) { - applyGTKSettings(result, themeName, iconsName, fontName, cursorTheme, cursorSize, wallpaper); - return; - } + ffStrbufInit(&settings->theme); + ffStrbufInit(&settings->icons); + ffStrbufInit(&settings->font); + ffStrbufInit(&settings->cursor); + ffStrbufInit(&settings->wallpaper); - init = true; + const char* themeName = nullptr; + const char* iconsName = nullptr; + const char* fontName = nullptr; + const char* cursorTheme = nullptr; + int cursorSize = 0; + const char* wallpaper = nullptr; const FFDisplayServerResult* wmde = ffConnectDisplayServer(); @@ -98,14 +112,14 @@ static void detectGTKFromSettings(FFGTKResult* result) { wallpaper = ffSettingsGetGnome("/org/gnome/desktop/background/picture-uri", "org.gnome.desktop.background", nullptr, "picture-uri", FF_VARIANT_TYPE_STRING).strValue; } else if ( ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_ENLIGHTENMENT)) { - ffEnlightenmentSettings settings = {}; - if (ffSettingsGetEnlightenmentProperty(&settings)) { - themeName = settings.theme; - iconsName = settings.icon_theme; - fontName = settings.font; - cursorTheme = settings.use_e_cursor ? "Enlightenment" : "Application"; - cursorSize = settings.cursor_size; - wallpaper = settings.desktop_default_background; + ffEnlightenmentSettings enlightenmentSettings = {}; + if (ffSettingsGetEnlightenmentProperty(&enlightenmentSettings)) { + themeName = enlightenmentSettings.theme; + iconsName = enlightenmentSettings.icon_theme; + fontName = enlightenmentSettings.font; + cursorTheme = enlightenmentSettings.use_e_cursor ? "Enlightenment" : "Application"; + cursorSize = enlightenmentSettings.cursor_size; + wallpaper = enlightenmentSettings.desktop_default_background; } } } else { @@ -121,9 +135,34 @@ static void detectGTKFromSettings(FFGTKResult* result) { cursorSize = ffSettingsGetDConf("/org/gnome/desktop/interface/cursor-size", FF_VARIANT_TYPE_INT).intValue; } - applyGTKSettings(result, themeName, iconsName, fontName, cursorTheme, cursorSize, wallpaper); + // The strings above are owned by `settings.c`, which leaks them on purpose, so copy them into + // the cache instead of borrowing them: this runs again on every `--dynamic-interval` round. + ffStrbufSetS(&settings->theme, themeName); + ffStrbufSetS(&settings->icons, iconsName); + ffStrbufSetS(&settings->font, fontName); + ffStrbufSetS(&settings->cursor, cursorTheme); + ffStrbufSetS(&settings->wallpaper, wallpaper); + settings->cursorSize = cursorSize; } +static void destroyGTKSettings(void* storage) { + FFGTKSettings* settings = storage; + + ffStrbufDestroy(&settings->theme); + ffStrbufDestroy(&settings->icons); + ffStrbufDestroy(&settings->font); + ffStrbufDestroy(&settings->cursor); + ffStrbufDestroy(&settings->wallpaper); + settings->cursorSize = 0; +} + +static FFcacheEntry ffCacheEntryGTKSettings = { + .name = "gtk-settings", + .storage = >kSettings, + .init = initGTKSettings, + .destroy = destroyGTKSettings, +}; + static void detectGTKFromConfigFile(const char* filename, FFGTKResult* result) { ffParsePropFileValues(filename, 5, (FFpropquery[]) { { "gtk-theme-name =", &result->theme }, { "gtk-icon-theme-name =", &result->icons }, { "gtk-font-name =", &result->font }, { "gtk-cursor-theme-name =", &result->cursor }, { "gtk-cursor-theme-size =", &result->cursorSize } }); } @@ -172,7 +211,7 @@ static void detectGTKFromConfigDir(FFstrbuf* configDir, const char* version, FFG static void detectGTK(const char* version, FFGTKResult* result) { // Mate, Cinnamon, GNOME, Unity, Budgie use dconf to save theme config // On other DEs, this will do nothing - detectGTKFromSettings(result); + applyGTKSettings(result, ffCacheGet(&ffCacheEntryGTKSettings)); if (allPropertiesSet(result)) { return; } @@ -189,20 +228,51 @@ static void detectGTK(const char* version, FFGTKResult* result) { } } -#define FF_DETECT_GTK_IMPL(version) \ - static FFGTKResult result; \ - static bool init = false; \ - if (init) \ - return &result; \ - init = true; \ - ffStrbufInit(&result.theme); \ - ffStrbufInit(&result.icons); \ - ffStrbufInit(&result.font); \ - ffStrbufInit(&result.cursor); \ - ffStrbufInit(&result.cursorSize); \ - ffStrbufInit(&result.wallpaper); \ - detectGTK(#version, &result); \ - return &result; +static void initGTKResult(const char* version, FFGTKResult* result) { + ffStrbufInit(&result->theme); + ffStrbufInit(&result->icons); + ffStrbufInit(&result->font); + ffStrbufInit(&result->cursor); + ffStrbufInit(&result->cursorSize); + ffStrbufInit(&result->wallpaper); + detectGTK(version, result); +} + +static void destroyGTKResult(void* storage) { + FFGTKResult* result = storage; + + ffStrbufDestroy(&result->theme); + ffStrbufDestroy(&result->icons); + ffStrbufDestroy(&result->font); + ffStrbufDestroy(&result->cursor); + ffStrbufDestroy(&result->cursorSize); + ffStrbufDestroy(&result->wallpaper); +} + +static void initGTK2Result(void* storage) { + initGTKResult("2", storage); +} + +static void initGTK3Result(void* storage) { + initGTKResult("3", storage); +} + +static void initGTK4Result(void* storage) { + initGTKResult("4", storage); +} + +// Each version gets its own result and its own entry, so a refresh drops all three (each one is +// then rebuilt on demand, in the same lazy order as a single-shot run). They share the single +// `gtk-settings` entry above, which keeps a round at one settings query instead of three. +#define FF_DETECT_GTK_IMPL(version) \ + static FFGTKResult result; \ + static FFcacheEntry entry = { \ + .name = "gtk" #version, \ + .storage = &result, \ + .init = initGTK##version##Result, \ + .destroy = destroyGTKResult, \ + }; \ + return ffCacheGet(&entry); const FFGTKResult* ffDetectGTK2(void) { FF_DETECT_GTK_IMPL(2) @@ -216,4 +286,4 @@ const FFGTKResult* ffDetectGTK4(void) { FF_DETECT_GTK_IMPL(4) } -#undef FF_CALCULATE_GTK_IMPL +#undef FF_DETECT_GTK_IMPL diff --git a/src/detection/gtk_qt/qt.c b/src/detection/gtk_qt/qt.c index f7eb22bc4..05317fff6 100644 --- a/src/detection/gtk_qt/qt.c +++ b/src/detection/gtk_qt/qt.c @@ -1,7 +1,7 @@ #include "fastfetch.h" #include "common/properties.h" -#include "common/thread.h" #include "common/strutil.h" +#include "common/FFcache.h" #include "detection/gtk_qt/gtk_qt.h" #include "detection/displayserver/displayserver.h" @@ -172,38 +172,53 @@ static void detectKvantum(FFQtResult* result) { }); } -const FFQtResult* ffDetectQt(void) { - static FFQtResult result; +static FFQtResult result; - static bool init = false; - if (init) { - return &result; - } - init = true; +static void initQtResult(void* storage) { + FFQtResult* qt = storage; - ffStrbufInit(&result.widgetStyle); - ffStrbufInit(&result.colorScheme); - ffStrbufInit(&result.icons); - ffStrbufInit(&result.font); - ffStrbufInit(&result.wallpaper); + ffStrbufInit(&qt->widgetStyle); + ffStrbufInit(&qt->colorScheme); + ffStrbufInit(&qt->icons); + ffStrbufInit(&qt->font); + ffStrbufInit(&qt->wallpaper); const FFDisplayServerResult* wmde = ffConnectDisplayServer(); if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_PLASMA)) { - detectPlasma(&result); + detectPlasma(qt); } else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_LXQT)) { - detectLXQt(&result); + detectLXQt(qt); } else { const char* qPlatformTheme = getenv("QT_QPA_PLATFORMTHEME"); if (qPlatformTheme && (ffStrEquals(qPlatformTheme, "qt5ct") || ffStrEquals(qPlatformTheme, "qt6ct"))) { - detectQtCt(qPlatformTheme[2], &result); + detectQtCt(qPlatformTheme[2], qt); } } - if (ffStrbufEqualS(&result.widgetStyle, "kvantum") || ffStrbufEqualS(&result.widgetStyle, "kvantum-dark")) { - ffStrbufClear(&result.widgetStyle); - detectKvantum(&result); + if (ffStrbufEqualS(&qt->widgetStyle, "kvantum") || ffStrbufEqualS(&qt->widgetStyle, "kvantum-dark")) { + ffStrbufClear(&qt->widgetStyle); + detectKvantum(qt); } - - return &result; +} + +static void destroyQtResult(void* storage) { + FFQtResult* qt = storage; + + ffStrbufDestroy(&qt->widgetStyle); + ffStrbufDestroy(&qt->colorScheme); + ffStrbufDestroy(&qt->icons); + ffStrbufDestroy(&qt->font); + ffStrbufDestroy(&qt->wallpaper); +} + +static FFcacheEntry ffCacheEntryQt = { + .name = "qt", + .storage = &result, + .init = initQtResult, + .destroy = destroyQtResult, +}; + +const FFQtResult* ffDetectQt(void) { + return ffCacheGet(&ffCacheEntryQt); } diff --git a/src/detection/media/media.c b/src/detection/media/media.c index 2dfd28a61..93ce674d2 100644 --- a/src/detection/media/media.c +++ b/src/detection/media/media.c @@ -1,19 +1,53 @@ #include "media.h" +#include "common/FFcache.h" #include "common/io.h" void ffDetectMediaImpl(FFMediaResult* media, bool saveCover); static FFMediaResult result; +// The cover file of this run, if there is one. It is tracked here rather than in `result` because +// the cached result is dropped between `--dynamic-interval` rounds, while the file has to stay on +// disk for the whole run: the `kitty-direct` logo type lets the terminal open it by path, and the +// logo is replayed from the line cache on every round. +static FFstrbuf coverFile; +static bool coverFileRegistered = false; + static void removeMediaCoverFile(void) { - if (result.cover.length > 0) { - ffRemoveFile(result.cover.chars); - ffStrbufDestroy(&result.cover); + if (coverFile.length > 0) { + ffRemoveFile(coverFile.chars); + ffStrbufDestroy(&coverFile); } } +static void destroyMediaResult(void* storage) { + FFMediaResult* media = storage; + + ffStrbufDestroy(&media->error); + ffStrbufDestroy(&media->playerId); + ffStrbufDestroy(&media->player); + ffStrbufDestroy(&media->song); + ffStrbufDestroy(&media->artist); + ffStrbufDestroy(&media->album); + ffStrbufDestroy(&media->url); + ffStrbufDestroy(&media->status); + ffStrbufDestroy(&media->cover); + media->length = 0; + media->position = 0; + media->removeCoverAfterUse = false; +} + +// `saveCover` asks for a side effect and is not part of the cache identity, so the entry cannot be +// initialized through `init(void*)`: the first caller of a generation decides, and the callers that +// follow reuse its result, as they did when the result was cached for the whole run. +static FFcacheEntry ffCacheEntryMedia = { + .name = "media", + .storage = &result, + .destroy = destroyMediaResult, +}; + const FFMediaResult* ffDetectMedia(bool saveCover) { - if (result.error.chars == nullptr) { + if (ffCacheBeginInit(&ffCacheEntryMedia)) { ffStrbufInit(&result.error); ffStrbufInit(&result.playerId); ffStrbufInit(&result.player); @@ -37,7 +71,11 @@ const FFMediaResult* ffDetectMedia(bool saveCover) { ffStrbufTrimRightSpace(&result.player); if (saveCover && result.removeCoverAfterUse) { - atexit(removeMediaCoverFile); + ffStrbufSet(&coverFile, &result.cover); + if (!coverFileRegistered) { + coverFileRegistered = true; + atexit(removeMediaCoverFile); + } } } diff --git a/src/detection/os/os.c b/src/detection/os/os.c index 0690574e4..baea6a418 100644 --- a/src/detection/os/os.c +++ b/src/detection/os/os.c @@ -1,21 +1,48 @@ #include "os.h" +#include "common/FFcache.h" void ffDetectOSImpl(FFOSResult* os); -const FFOSResult* ffDetectOS(void) { - static FFOSResult result; - if (result.name.chars == nullptr) { - ffStrbufInit(&result.name); - ffStrbufInit(&result.prettyName); - ffStrbufInit(&result.id); - ffStrbufInit(&result.version); - ffStrbufInit(&result.versionID); - ffStrbufInit(&result.codename); - ffStrbufInit(&result.buildID); - ffStrbufInit(&result.idLike); - ffStrbufInit(&result.variant); - ffStrbufInit(&result.variantID); - ffDetectOSImpl(&result); - } - return &result; +static FFOSResult result; + +static void initOSResult(void* storage) { + FFOSResult* os = storage; + + ffStrbufInit(&os->name); + ffStrbufInit(&os->prettyName); + ffStrbufInit(&os->id); + ffStrbufInit(&os->version); + ffStrbufInit(&os->versionID); + ffStrbufInit(&os->codename); + ffStrbufInit(&os->buildID); + ffStrbufInit(&os->idLike); + ffStrbufInit(&os->variant); + ffStrbufInit(&os->variantID); + ffDetectOSImpl(os); +} + +static void destroyOSResult(void* storage) { + FFOSResult* os = storage; + + ffStrbufDestroy(&os->name); + ffStrbufDestroy(&os->prettyName); + ffStrbufDestroy(&os->id); + ffStrbufDestroy(&os->version); + ffStrbufDestroy(&os->versionID); + ffStrbufDestroy(&os->codename); + ffStrbufDestroy(&os->buildID); + ffStrbufDestroy(&os->idLike); + ffStrbufDestroy(&os->variant); + ffStrbufDestroy(&os->variantID); +} + +static FFcacheEntry ffCacheEntryOS = { + .name = "os", + .storage = &result, + .init = initOSResult, + .destroy = destroyOSResult, +}; + +const FFOSResult* ffDetectOS(void) { + return ffCacheGet(&ffCacheEntryOS); } diff --git a/src/detection/terminalshell/terminalshell_linux.c b/src/detection/terminalshell/terminalshell_linux.c index ae7e07b9c..b6a4d71bc 100644 --- a/src/detection/terminalshell/terminalshell_linux.c +++ b/src/detection/terminalshell/terminalshell_linux.c @@ -1,4 +1,5 @@ #include "terminalshell.h" +#include "common/FFcache.h" #include "common/io.h" #include "common/parsing.h" #include "common/processing.h" @@ -376,22 +377,43 @@ static void setTerminalInfoDetails(FFTerminalResult* result) { #define FF_EXE_PATH_LEN 260 #endif -const FFShellResult* ffDetectShell() { - static FFShellResult result; - static bool init = false; - if (init) { - return &result; - } - init = true; +static FFShellResult shellResult; - ffStrbufInit(&result.processName); - ffStrbufInitA(&result.exe, FF_EXE_PATH_LEN); - result.exeName = result.exe.chars; - ffStrbufInit(&result.exePath); - ffStrbufInit(&result.version); - result.pid = 0; - result.ppid = 0; - result.tty = -1; +static void destroyShellResult(void* storage) { + FFShellResult* result = storage; + + ffStrbufDestroy(&result->processName); + ffStrbufDestroy(&result->exe); + result->exeName = result->exe.chars; // `exeName` points into `exe`, which is empty again + ffStrbufDestroy(&result->exePath); + ffStrbufDestroy(&result->prettyName); + ffStrbufDestroy(&result->version); + result->pid = 0; + result->ppid = 0; + result->tty = -1; +} + +static FFcacheEntry ffCacheEntryShell = { + .name = "shell", + .storage = &shellResult, + .destroy = destroyShellResult, +}; + +const FFShellResult* ffDetectShell() { + FFShellResult* result = &shellResult; + + if (!ffCacheBeginInit(&ffCacheEntryShell)) { + return result; + } + + ffStrbufInit(&result->processName); + ffStrbufInitA(&result->exe, FF_EXE_PATH_LEN); + result->exeName = result->exe.chars; + ffStrbufInit(&result->exePath); + ffStrbufInit(&result->version); + result->pid = 0; + result->ppid = 0; + result->tty = -1; pid_t ppid = getppid(); @@ -401,49 +423,70 @@ const FFShellResult* ffDetectShell() { ffProcessGetBasicInfoLinux(ppid, &_, &ppid, nullptr); } - ppid = getShellInfo(&result, ppid); - getUserShellFromEnv(&result); + ppid = getShellInfo(result, ppid); + getUserShellFromEnv(result); - if (result.processName.length > 0) { - setShellInfoDetails(&result); + if (result->processName.length > 0) { + setShellInfoDetails(result); if (instance.config.general.detectVersion) { - fftsGetShellVersion(result.exePath.length > 0 ? &result.exePath : &result.exe, result.exeName, &result.version); + fftsGetShellVersion(result->exePath.length > 0 ? &result->exePath : &result->exe, result->exeName, &result->version); } } - return &result; + return result; } -const FFTerminalResult* ffDetectTerminal() { - static FFTerminalResult result; - static bool init = false; - if (init) { - return &result; - } - init = true; +static FFTerminalResult terminalResult; - ffStrbufInit(&result.processName); - ffStrbufInitA(&result.exe, FF_EXE_PATH_LEN); - result.exeName = result.exe.chars; - ffStrbufInit(&result.exePath); - ffStrbufInit(&result.version); - ffStrbufInitS(&result.tty, ttyname(STDOUT_FILENO)); - result.pid = 0; - result.ppid = 0; +static void destroyTerminalResult(void* storage) { + FFTerminalResult* result = storage; + + ffStrbufDestroy(&result->processName); + ffStrbufDestroy(&result->exe); + result->exeName = result->exe.chars; // `exeName` points into `exe`, which is empty again + ffStrbufDestroy(&result->exePath); + ffStrbufDestroy(&result->prettyName); + ffStrbufDestroy(&result->version); + ffStrbufDestroy(&result->tty); + result->pid = 0; + result->ppid = 0; +} + +static FFcacheEntry ffCacheEntryTerminal = { + .name = "terminal", + .storage = &terminalResult, + .destroy = destroyTerminalResult, +}; + +const FFTerminalResult* ffDetectTerminal() { + FFTerminalResult* result = &terminalResult; + + if (!ffCacheBeginInit(&ffCacheEntryTerminal)) { + return result; + } + + ffStrbufInit(&result->processName); + ffStrbufInitA(&result->exe, FF_EXE_PATH_LEN); + result->exeName = result->exe.chars; + ffStrbufInit(&result->exePath); + ffStrbufInit(&result->version); + ffStrbufInitS(&result->tty, ttyname(STDOUT_FILENO)); + result->pid = 0; + result->ppid = 0; pid_t ppid = (pid_t) ffDetectShell()->ppid; if (ppid) { - ppid = getTerminalInfo(&result, ppid); + ppid = getTerminalInfo(result, ppid); } - getTerminalFromEnv(&result); + getTerminalFromEnv(result); - if (result.processName.length > 0) { - setTerminalInfoDetails(&result); + if (result->processName.length > 0) { + setTerminalInfoDetails(result); if (instance.config.general.detectVersion) { - fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version); + fftsGetTerminalVersion(&result->processName, result->exePath.length > 0 ? &result->exePath : &result->exe, &result->version); } } - return &result; + return result; } diff --git a/src/detection/terminalshell/terminalshell_windows.c b/src/detection/terminalshell/terminalshell_windows.c index 748944347..d4b33c19f 100644 --- a/src/detection/terminalshell/terminalshell_windows.c +++ b/src/detection/terminalshell/terminalshell_windows.c @@ -1,4 +1,5 @@ #include "terminalshell.h" +#include "common/FFcache.h" #include "common/io.h" #include "common/processing.h" #include "common/thread.h" @@ -293,27 +294,48 @@ static void setTerminalInfoDetails(FFTerminalResult* result) { } } -const FFShellResult* ffDetectShell(void) { - static FFShellResult result; - static bool init = false; - if (init) { - return &result; - } - init = true; +static FFShellResult shellResult; - ffStrbufInit(&result.processName); - ffStrbufInitA(&result.exe, MAX_PATH); - result.exeName = ""; - ffStrbufInit(&result.exePath); - ffStrbufInit(&result.prettyName); - ffStrbufInit(&result.version); - result.pid = 0; - result.ppid = 0; - result.tty = -1; +static void destroyShellResult(void* storage) { + FFShellResult* result = storage; + + ffStrbufDestroy(&result->processName); + ffStrbufDestroy(&result->exe); + result->exeName = ""; // The `exe` buffer is empty again, so nothing points into it + ffStrbufDestroy(&result->exePath); + ffStrbufDestroy(&result->prettyName); + ffStrbufDestroy(&result->version); + result->pid = 0; + result->ppid = 0; + result->tty = -1; +} + +static FFcacheEntry ffCacheEntryShell = { + .name = "shell", + .storage = &shellResult, + .destroy = destroyShellResult, +}; + +const FFShellResult* ffDetectShell(void) { + FFShellResult* result = &shellResult; + + if (!ffCacheBeginInit(&ffCacheEntryShell)) { + return result; + } + + ffStrbufInit(&result->processName); + ffStrbufInitA(&result->exe, MAX_PATH); + result->exeName = ""; + ffStrbufInit(&result->exePath); + ffStrbufInit(&result->prettyName); + ffStrbufInit(&result->version); + result->pid = 0; + result->ppid = 0; + result->tty = -1; uint32_t ppid; if (!ffProcessGetInfoWindows(0, &ppid, nullptr, nullptr, nullptr, nullptr, nullptr)) { - return &result; + return result; } const char* ignoreParent = getenv("FFTS_IGNORE_PARENT"); @@ -321,60 +343,81 @@ const FFShellResult* ffDetectShell(void) { ffProcessGetInfoWindows(ppid, &ppid, nullptr, nullptr, nullptr, nullptr, nullptr); } - ppid = getShellInfo(&result, ppid); + ppid = getShellInfo(result, ppid); - if (result.processName.length > 0) { - setShellInfoDetails(&result); + if (result->processName.length > 0) { + setShellInfoDetails(result); char tmp[MAX_PATH]; - strcpy(tmp, result.exeName); + strcpy(tmp, result->exeName); char* ext = strrchr(tmp, '.'); if (ext) { *ext = '\0'; } if (instance.config.general.detectVersion) { - fftsGetShellVersion(result.exePath.length > 0 ? &result.exePath : &result.exe, tmp, &result.version); + fftsGetShellVersion(result->exePath.length > 0 ? &result->exePath : &result->exe, tmp, &result->version); } } - return &result; + return result; } -const FFTerminalResult* ffDetectTerminal(void) { - static FFTerminalResult result; - static bool init = false; - if (init) { - return &result; - } - init = true; +static FFTerminalResult terminalResult; - ffStrbufInit(&result.processName); - ffStrbufInitA(&result.exe, MAX_PATH); - result.exeName = ""; - ffStrbufInit(&result.exePath); - ffStrbufInit(&result.prettyName); - ffStrbufInit(&result.version); - ffStrbufInit(&result.tty); - result.pid = 0; - result.ppid = 0; +static void destroyTerminalResult(void* storage) { + FFTerminalResult* result = storage; + + ffStrbufDestroy(&result->processName); + ffStrbufDestroy(&result->exe); + result->exeName = ""; // The `exe` buffer is empty again, so nothing points into it + ffStrbufDestroy(&result->exePath); + ffStrbufDestroy(&result->prettyName); + ffStrbufDestroy(&result->version); + ffStrbufDestroy(&result->tty); + result->pid = 0; + result->ppid = 0; +} + +static FFcacheEntry ffCacheEntryTerminal = { + .name = "terminal", + .storage = &terminalResult, + .destroy = destroyTerminalResult, +}; + +const FFTerminalResult* ffDetectTerminal(void) { + FFTerminalResult* result = &terminalResult; + + if (!ffCacheBeginInit(&ffCacheEntryTerminal)) { + return result; + } + + ffStrbufInit(&result->processName); + ffStrbufInitA(&result->exe, MAX_PATH); + result->exeName = ""; + ffStrbufInit(&result->exePath); + ffStrbufInit(&result->prettyName); + ffStrbufInit(&result->version); + ffStrbufInit(&result->tty); + result->pid = 0; + result->ppid = 0; uint32_t ppid = ffDetectShell()->ppid; if (ppid) { - getTerminalInfo(&result, ppid); + getTerminalInfo(result, ppid); } - if (result.processName.length == 0) { - getTerminalFromEnv(&result); + if (result->processName.length == 0) { + getTerminalFromEnv(result); } - if (result.processName.length == 0) { - detectDefaultTerminal(&result); + if (result->processName.length == 0) { + detectDefaultTerminal(result); } - if (result.processName.length > 0) { - setTerminalInfoDetails(&result); + if (result->processName.length > 0) { + setTerminalInfoDetails(result); if (instance.config.general.detectVersion) { - fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version); + fftsGetTerminalVersion(&result->processName, result->exePath.length > 0 ? &result->exePath : &result->exe, &result->version); } } - return &result; + return result; } diff --git a/src/fastfetch.c b/src/fastfetch.c index 980bf7c06..715991977 100644 --- a/src/fastfetch.c +++ b/src/fastfetch.c @@ -2,6 +2,7 @@ #include "common/ffdata.h" #include "detection/version/version.h" #include "logo/logo.h" +#include "common/FFcache.h" #include "common/commandoption.h" #include "common/genconfig.h" #include "common/init.h" @@ -815,6 +816,7 @@ static void run(FFdata* data) { ffTimeSleep(instance.state.dynamicInterval); fputs("\e[H", stdout); // Move cursor to the top left corner to overwrite the previous output instance.state.keysHeight = 0; // Reset keysHeight so `ffLogoPrintRemaining` will recalculate it + ffCacheInvalidateAll(); // Drop the detection results cached for the previous round, so that this round re-detects them } else { break; } diff --git a/tests/cache.c b/tests/cache.c new file mode 100644 index 000000000..b9e54c8b4 --- /dev/null +++ b/tests/cache.c @@ -0,0 +1,120 @@ +#include "common/FFcache.h" +#include "common/textModifier.h" + +#include +#include +#include + +typedef struct TestStorage { + int value; + int initCalls; + int destroyCalls; +} TestStorage; + +static TestStorage storageA, storageB, storageC, storageD, storageUnused; + +static void initStorage(void* storage) { + TestStorage* test = storage; + ++test->initCalls; + test->value = 42; +} + +static void destroyStorage(void* storage) { + TestStorage* test = storage; + ++test->destroyCalls; + test->value = -1; +} + +static FFcacheEntry entryA = { .name = "A", .storage = &storageA, .init = initStorage, .destroy = destroyStorage }; +static FFcacheEntry entryB = { .name = "B", .storage = &storageB, .init = initStorage, .destroy = destroyStorage }; +static FFcacheEntry entryC = { .name = "C", .storage = &storageC, .init = initStorage, .destroy = destroyStorage }; +static FFcacheEntry entryD = { .name = "D", .storage = &storageD }; +static FFcacheEntry entryNoStorage = { .name = "no storage" }; +static FFcacheEntry entryUnused = { .name = "unused", .storage = &storageUnused, .init = initStorage, .destroy = destroyStorage }; + +noreturn static void testFailed(const char* expression, int lineNo) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expression); + exit(1); +} + +noreturn static void testFailedInt(int expected, int actual, const char* what, int lineNo) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s: expected %d, got %d\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, what, expected, actual); + exit(1); +} + +#define VERIFY(expression) \ + if (!(expression)) testFailed(#expression, __LINE__) + +#define VERIFY_INT(expected, actual, what) \ + if ((expected) != (actual)) testFailedInt((expected), (actual), (what), __LINE__) + +int main(void) { + // A stale entry is built on the first get, and only then + VERIFY(ffCacheGet(&entryA) == &storageA); + VERIFY_INT(1, storageA.initCalls, "A init calls after the first get"); + VERIFY_INT(42, storageA.value, "A value after the first get"); + + VERIFY(ffCacheGet(&entryA) == &storageA); + VERIFY_INT(1, storageA.initCalls, "A init calls after the second get"); + + // Nothing happens to the entry that has not been used yet + VERIFY_INT(0, storageUnused.initCalls, "unused init calls"); + + // Invalidating drops the value and rearms the entry, and doing it twice is a no-op + ffCacheInvalidate(&entryA); + VERIFY_INT(1, storageA.destroyCalls, "A destroy calls after invalidating"); + ffCacheInvalidate(&entryA); + VERIFY_INT(1, storageA.destroyCalls, "A destroy calls after invalidating twice"); + + VERIFY(ffCacheGet(&entryA) == &storageA); + VERIFY_INT(2, storageA.initCalls, "A init calls after rebuilding"); + VERIFY_INT(42, storageA.value, "A value after rebuilding"); + + // An entry that has never been used has nothing to drop + ffCacheInvalidate(&entryC); + VERIFY_INT(0, storageC.destroyCalls, "C destroy calls after invalidating an unused entry"); + VERIFY(ffCacheGet(&entryC) == &storageC); + VERIFY_INT(1, storageC.initCalls, "C init calls"); + + // An entry without an initializer tracks staleness on its own + VERIFY(ffCacheGet(&entryD) == &storageD); + VERIFY(!ffCacheBeginInit(&entryD)); + ffCacheInvalidate(&entryD); + VERIFY(ffCacheBeginInit(&entryD)); + VERIFY(!ffCacheBeginInit(&entryD)); + + // An entry without storage and without callbacks is tolerated + VERIFY(ffCacheGet(&entryNoStorage) == nullptr); + ffCacheInvalidate(&entryNoStorage); + VERIFY(ffCacheGet(&entryNoStorage) == nullptr); + + // An entry whose owner builds the value itself (which is what `ffCacheBeginInit` is for) is only + // marked, never built by the cache; and it must survive a `destroy` on a storage that the owner + // never filled in + VERIFY(ffCacheBeginInit(&entryB)); + VERIFY_INT(0, storageB.initCalls, "B init calls (the owner builds the value itself)"); + + // Invalidating everything drops exactly the entries that have been used so far + ffCacheInvalidateAll(); + VERIFY_INT(2, storageA.destroyCalls, "A destroy calls after invalidating all"); + VERIFY_INT(1, storageC.destroyCalls, "C destroy calls after invalidating all"); + VERIFY_INT(0, storageD.destroyCalls, "D destroy calls"); // D has no callback + VERIFY_INT(1, storageB.destroyCalls, "B destroy calls after invalidating a value that was never built"); + VERIFY_INT(0, entryB.initialized, "B must be stale again after invalidating all"); + VERIFY_INT(0, storageUnused.initCalls, "unused init calls after invalidating all"); + + // Rebuilding after invalidating all + VERIFY(ffCacheGet(&entryA) == &storageA); + VERIFY_INT(3, storageA.initCalls, "A init calls after invalidating all"); + VERIFY_INT(42, storageA.value, "A value after invalidating all"); + + // An entry joins the registry when it is first used, not when it is created + VERIFY(ffCacheGet(&entryUnused) == &storageUnused); + VERIFY_INT(1, storageUnused.initCalls, "unused init calls after its first get"); + ffCacheInvalidateAll(); + VERIFY_INT(1, storageUnused.destroyCalls, "unused destroy calls after invalidating all"); + VERIFY_INT(3, storageA.destroyCalls, "A destroy calls after the second invalidating all"); + + // Success + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); +}