Global: adds general cache layer

This commit is contained in:
李通洲
2026-09-21 22:46:40 +08:00
parent 2f74d6be4d
commit 7f3209181e
14 changed files with 728 additions and 207 deletions
+1
View File
@@ -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:
+8
View File
@@ -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
)
+48
View File
@@ -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);
+53
View File
@@ -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);
}
}
+39 -14
View File
@@ -1,22 +1,47 @@
#include "common/netif.h"
#include "common/FFcache.h"
#ifndef _WIN32
#include <net/if.h>
#include <netinet/in.h>
#endif
#include <string.h>
// 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);
}
+42 -14
View File
@@ -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);
}
+117 -47
View File
@@ -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 = &gtkSettings,
.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
+36 -21
View File
@@ -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);
}
+43 -5
View File
@@ -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);
}
}
}
+43 -16
View File
@@ -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);
}
@@ -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;
}
@@ -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;
}
+2
View File
@@ -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;
}
+120
View File
@@ -0,0 +1,120 @@
#include "common/FFcache.h"
#include "common/textModifier.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
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);
}