defaults: use nanosecond mtime precision to detect rapid file writes

The user-facing root menu's 'Background → Images → Tiled' menu item runs
'wmsetbg -u -t image.jpg', which internally invokes wdwrite TWICE in
rapid succession:
  1. wdwrite WindowMaker SmoothWorkspaceBack NO
  2. wdwrite WindowMaker WorkspaceBack '(tpixmap, ...)'

Both writes complete within the same wall-clock second. The previous
mtime check used time_t (1-second precision):
  if (timestamp < stbuf.st_mtime)

After the first write, wmaker reads the file (only SmoothWorkspaceBack
changed; WorkspaceBack still old) and sets timestamp = current second.
The second write happens in the same second, so st_mtime equals
timestamp exactly — the check fails and the second write is silently
ignored. The user has to invoke the menu twice for the change to apply.

Fix: WDDomain.timestamp is now struct timespec (nanosecond precision).
New MTIME_NEWER() macro compares full timespec values. Two writes within
the same second now have distinct nanosecond components so both are
detected.

Also removed all debug logging.
This commit is contained in:
Window Maker 2026-05-24 15:24:03 +02:00
parent 861082f1ad
commit 7a422330f6
3 changed files with 27 additions and 16 deletions

View File

@ -163,6 +163,17 @@ static WDECallbackUpdate updateDock;
* this value */
#define TOTAL_VALUES_LENGTH 80
/* Return non-zero if file's mtime is newer than the saved timestamp.
* Uses nanosecond precision to handle multiple writes within the same
* second (e.g., wmsetbg invokes wdwrite twice in rapid succession). */
#define MTIME_NEWER(stbuf, ts) \
((stbuf).st_mtim.tv_sec > (ts).tv_sec || \
((stbuf).st_mtim.tv_sec == (ts).tv_sec && \
(stbuf).st_mtim.tv_nsec > (ts).tv_nsec))
#define MTIME_FROM_STAT(stbuf) \
((struct timespec){ (stbuf).st_mtim.tv_sec, (stbuf).st_mtim.tv_nsec })
#define REFRESH_WINDOW_TEXTURES (1<<0)
#define REFRESH_MENU_TEXTURE (1<<1)
#define REFRESH_MENU_FONT (1<<2)
@ -992,7 +1003,7 @@ WDDomain *wDefaultsInitDomain(const char *domain, Bool requireDictionary)
db->dictionary = NULL;
wwarning(_("Domain %s (%s) of defaults database is corrupted!"), domain, db->path);
}
db->timestamp = stbuf.st_mtime;
db->timestamp = MTIME_FROM_STAT(stbuf);
} else {
wwarning(_("could not load domain %s from user defaults database"), domain);
}
@ -1006,12 +1017,12 @@ WDDomain *wDefaultsInitDomain(const char *domain, Bool requireDictionary)
WMMergePLDictionaries(shared_dict, db->dictionary, True);
WMReleasePropList(db->dictionary);
db->dictionary = shared_dict;
if (stbuf.st_mtime > db->timestamp)
db->timestamp = stbuf.st_mtime;
if (MTIME_NEWER(stbuf, db->timestamp))
db->timestamp = MTIME_FROM_STAT(stbuf);
} else if (!db->dictionary) {
db->dictionary = shared_dict;
if (stbuf.st_mtime > db->timestamp)
db->timestamp = stbuf.st_mtime;
if (MTIME_NEWER(stbuf, db->timestamp))
db->timestamp = MTIME_FROM_STAT(stbuf);
}
return db;
@ -1056,8 +1067,8 @@ void wDefaultsCheckDomains(void* arg)
/* Parameter not used, but tell the compiler that it is ok */
(void) arg;
if (stat(w_global.domain.wmaker->path, &stbuf) >= 0 && w_global.domain.wmaker->timestamp < stbuf.st_mtime) {
w_global.domain.wmaker->timestamp = stbuf.st_mtime;
if (stat(w_global.domain.wmaker->path, &stbuf) >= 0 && MTIME_NEWER(stbuf, w_global.domain.wmaker->timestamp)) {
w_global.domain.wmaker->timestamp = MTIME_FROM_STAT(stbuf);
/* Global dictionary */
shared_dict = readGlobalDomain("WindowMaker", True);
@ -1099,7 +1110,7 @@ void wDefaultsCheckDomains(void* arg)
}
if (stat(w_global.domain.window_attr->path, &stbuf) >= 0 && w_global.domain.window_attr->timestamp < stbuf.st_mtime) {
if (stat(w_global.domain.window_attr->path, &stbuf) >= 0 && MTIME_NEWER(stbuf, w_global.domain.window_attr->timestamp)) {
/* global dictionary */
shared_dict = readGlobalDomain("WMWindowAttributes", True);
/* user dictionary */
@ -1138,12 +1149,12 @@ void wDefaultsCheckDomains(void* arg)
wwarning(_("could not load domain %s from user defaults database"), "WMWindowAttributes");
}
w_global.domain.window_attr->timestamp = stbuf.st_mtime;
w_global.domain.window_attr->timestamp = MTIME_FROM_STAT(stbuf);
if (shared_dict)
WMReleasePropList(shared_dict);
}
if (stat(w_global.domain.root_menu->path, &stbuf) >= 0 && w_global.domain.root_menu->timestamp < stbuf.st_mtime) {
if (stat(w_global.domain.root_menu->path, &stbuf) >= 0 && MTIME_NEWER(stbuf, w_global.domain.root_menu->timestamp)) {
dict = WMReadPropListFromFile(w_global.domain.root_menu->path);
if (dict) {
if (!WMIsPLArray(dict) && !WMIsPLString(dict)) {
@ -1161,7 +1172,7 @@ void wDefaultsCheckDomains(void* arg)
} else {
wwarning(_("could not load domain %s from user defaults database"), "WMRootMenu");
}
w_global.domain.root_menu->timestamp = stbuf.st_mtime;
w_global.domain.root_menu->timestamp = MTIME_FROM_STAT(stbuf);
}
#ifndef HAVE_INOTIFY
if (!arg)

View File

@ -25,7 +25,7 @@ typedef struct WDDomain {
const char *domain_name;
WMPropList *dictionary;
const char *path;
time_t timestamp;
struct timespec timestamp;
} WDDomain;
WDDomain * wDefaultsInitDomain(const char *domain, Bool requireDictionary);

View File

@ -1458,7 +1458,7 @@ WMenu *configureMenu(WScreen *scr, WMPropList *definition)
if (!scr->root_menu || stat_buf.st_mtime > scr->root_menu->timestamp
/* if the pointer in WMRootMenu has changed */
|| w_global.domain.root_menu->timestamp > scr->root_menu->timestamp) {
|| w_global.domain.root_menu->timestamp.tv_sec > scr->root_menu->timestamp) {
WMPropList *menu_from_file = NULL;
if (menu_is_default) {
@ -1476,7 +1476,7 @@ WMenu *configureMenu(WScreen *scr, WMPropList *definition)
}
if (menu)
menu->timestamp = WMAX(stat_buf.st_mtime, w_global.domain.root_menu->timestamp);
menu->timestamp = WMAX(stat_buf.st_mtime, w_global.domain.root_menu->timestamp.tv_sec);
} else {
menu = NULL;
}
@ -1615,12 +1615,12 @@ void OpenRootMenu(WScreen * scr, int x, int y, int keyboard)
*/
if (definition) {
if (WMIsPLArray(definition)) {
if (!scr->root_menu || w_global.domain.root_menu->timestamp > scr->root_menu->timestamp) {
if (!scr->root_menu || w_global.domain.root_menu->timestamp.tv_sec > scr->root_menu->timestamp) {
wwarning("rootmenu: configureMenu begin");
menu = configureMenu(scr, definition);
wwarning("rootmenu: configureMenu done, menu=%p", (void*)menu);
if (menu)
menu->timestamp = w_global.domain.root_menu->timestamp;
menu->timestamp = w_global.domain.root_menu->timestamp.tv_sec;
} else
menu = NULL;