touch: fix SD message history flush reliability

Ensure the tile-fetch worker is running before arming a message-ring flush,
fall back to a synchronous write if spawn fails, write ui_msgs_v1.bin via a
.tmp rename for crash-safe replacement, and retry when the worker snapshot is
missing instead of treating the flush as successful.

Signed-off-by: Steve Glenner <steveglenner@hotmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Steve GlennerandCursor authored and Kaj committed 2026-07-08 16:21:16 +02:00
1 parent 1eeeb267d2
commit 862ec32f80
1 file changed
+27 -3
+27 -3
View File
@@ -157,6 +157,7 @@ constexpr uint16_t k_ui_history_min_version = 6; // v4/v5 used 96-char records
// reducing flash write pressure when the ring is large.
constexpr const char* k_ui_threads_path = "/ui_threads_v1.bin";
constexpr const char* k_ui_msgs_path = "/ui_msgs_v1.bin";
constexpr const char* k_ui_msgs_tmp_path = "/ui_msgs_v1.bin.tmp";
constexpr uint32_t k_ui_threads_magic = 0x55495448; // "UITH"
constexpr uint32_t k_ui_msgs_magic = 0x55494D53; // "UIMS"
@@ -34479,6 +34480,10 @@ void UITask::flushHistoryIfDue(unsigned long now) {
}
if (_msgs_dirty && now >= _next_msgs_flush_ms) {
if (s_hist_flush_busy || s_hist_flush_req) {
// The worker only exists while the tile-fetch task is spawned (map / Wi-Fi
// scan / LOS / …). Without this kick a pending flush can sit armed forever
// while ui_threads_v1.bin keeps updating — messages live only in RAM.
ensureTileFetchTaskRunning();
_next_msgs_flush_ms = now + 1000; // one flush in flight; new messages ride the next one
return;
}
@@ -34497,6 +34502,14 @@ void UITask::flushHistoryIfDue(unsigned long now) {
s_hist_snap_count = (uint16_t)_ui_msg_count;
s_hist_snap_head = (uint16_t)_ui_msg_head;
s_hist_snap_msgcount = (uint32_t)_msgcount;
if (!ensureTileFetchTaskRunning()) {
// No worker — fall back to a synchronous write instead of clearing dirty
// and losing the snapshot (the whole point of the worker is to stay off
// the loop thread; a rare spawn failure must not drop hours of chat).
if (saveMsgsToStorage()) _msgs_dirty = false;
else _next_msgs_flush_ms = now + 2000;
return;
}
s_hist_flush_req = true; // worker picks it up
_msgs_dirty = false;
return;
@@ -34586,6 +34599,16 @@ static void uiDataRemove(const char* name) {
char p[80]; snprintf(p, sizeof p, "%s%s", s_ui_data_root, name);
s_ui_data_fs->remove(p);
}
// Atomically replace a ui-data file (write tmp, then rename) so a panic mid-write
// cannot leave a truncated header that quarantines the whole chat on next boot.
static bool uiDataReplaceFile(const char* final_name, const char* tmp_name) {
if (!uiDataFsReady()) return false;
char fin[80], tmp[80];
snprintf(fin, sizeof fin, "%s%s", s_ui_data_root, final_name);
snprintf(tmp, sizeof tmp, "%s%s", s_ui_data_root, tmp_name);
s_ui_data_fs->remove(fin);
return s_ui_data_fs->rename(tmp, fin);
}
// Shared helper: read one on-disk record into a zero-initialized current-layout
// struct, skipping any surplus tail bytes from a newer (longer) record.
@@ -34878,7 +34901,7 @@ static bool uiWriteMsgsFile(const UITask::UIMessage* msgs, int cap,
uint16_t count, uint16_t head, uint32_t msgcount) {
#if defined(ESP32)
WdtHeavyGuard _wg; // a fragmenting write can trigger a multi-second SPIFFS GC
File f = uiDataOpen(k_ui_msgs_path, "w");
File f = uiDataOpen(k_ui_msgs_tmp_path, "w");
if (!f) return false;
UiMsgFileHeader hdr{};
@@ -34944,7 +34967,8 @@ static bool uiWriteMsgsFile(const UITask::UIMessage* msgs, int cap,
}
}
f.close();
return ok;
if (!ok) { uiDataRemove(k_ui_msgs_tmp_path); return false; }
return uiDataReplaceFile(k_ui_msgs_path, k_ui_msgs_tmp_path);
#else
(void)msgs; (void)cap; (void)count; (void)head; (void)msgcount;
return false;
@@ -34954,7 +34978,7 @@ static bool uiWriteMsgsFile(const UITask::UIMessage* msgs, int cap,
// Worker-side entry: write the snapshot the loop thread armed (core-0 task; the
// loop thread never waits on this, which is the whole point).
static bool uiHistWorkerFlush() {
if (!s_hist_snap || s_hist_snap_cap <= 0) return true; // nothing armed
if (!s_hist_snap || s_hist_snap_cap <= 0) return false; // armed without a snapshot — retry
return uiWriteMsgsFile(s_hist_snap, s_hist_snap_cap,
s_hist_snap_count, s_hist_snap_head, s_hist_snap_msgcount);
}