Files
pyxis/patch_filestore.py
torlando-agent[bot] b06b3650ae chore(build): unify pre-script libdeps paths on a shared helper + OTA log
OTA hardening follow-up to the patch_nimble.py hardcoded-"tdeck" bug that broke
tdeck-ota linking (undefined reference to nimble_host_reset_reason):
- Add _build_helpers.env_libdeps_dir(env, *parts): the single per-environment
  libdeps path resolver (.pio/libdeps/<PIOENV>/...). Converted all five pre-scripts
  (patch_nimble/msgpack/filestore/littlefs_paths, sync_file_libdeps) to use it, so
  the env component can no longer be hand-rolled/hardcoded wrong per script.
- "OTA: Ready" log -> "OTA: wireless flash service started (pyxis-tdeck:3232)":
  ArduinoOTA.begin() is void and can't confirm a ready state, so log the target
  instead of claiming readiness we can't verify.

Verified: both tdeck and tdeck-ota build, and the NimBLE patch lands in EACH env's
own libdeps tree (nimble_host_reset_reason count 2 in both).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-23 00:01:17 -04:00

165 lines
7.2 KiB
Python

"""
PlatformIO pre-build script: patches the libdeps copy of microStore
FileStore.h before each build.
Three purposes:
1. Adds diagnostic printfs to `exists()` and `put()` for tracking down
the path-table-store bug where exists() returns false right after a
successful put for the same key. Temporary; remove once that
investigation is closed. Gated behind PYXIS_FILESTORE_DIAG=1.
2. Silences the spammy "[ustore] get: key not found in index" print
that fires on every path-table miss. RNS hits path-store lookups
constantly on every incoming packet — during an active LXST call
that print floods USB CDC at hundreds of lines/sec, saturating the
serial buffer and starving T:CALL_QOS responses (#75). The print
is unconditional in upstream microStore, so we patch it out here.
3. Closes `active_file` before unlinking segment files in
`finalize_compaction()` and `clear()`. Without this, on LittleFS
/ FAT (any FS that doesn't auto-close on unlink) the FD leaks —
over enough compactions the device can't open new files. Local
patch pending the upstream fix landing.
"""
Import("env")
import os, sys
sys.path.insert(0, env.get("PROJECT_DIR", "."))
from _build_helpers import env_libdeps_dir # per-env libdeps path; never hardcode the env
FILESTORE_H = env_libdeps_dir(env, "microStore", "include", "microStore", "FileStore.h")
OLD = """\tbool exists(const uint8_t* key, uint8_t key_len)
\t{
if (!isValid()) return false;
\t\tif(key_len > USTORE_MAX_KEY_LEN) return false;
\t\tIndexValue* e = index_find(key, key_len);
\t\tif (!e) return false;
\t\tif (is_ttl_expired_(e->timestamp, e->ttl)) { index_remove(key, key_len); return false; }
\t\treturn true;
\t}"""
NEW = """\t// pyxis-local diagnostic helper, only compiled under PYXIS_FILESTORE_DIAG
\t// and only used by the printfs below. Hex-encodes a binary key using just
\t// standard C so the diagnostic build is self-contained (no external bin_str).
\t// Static buffer is fine here: each printf evaluates it exactly once.
\tstatic const char* bin_str(const uint8_t* d, uint8_t n) {
\t\tstatic char b[2 * USTORE_MAX_KEY_LEN + 1];
\t\tstatic const char hex[] = "0123456789abcdef";
\t\tif (n > USTORE_MAX_KEY_LEN) n = USTORE_MAX_KEY_LEN;
\t\tfor (uint8_t i = 0; i < n; i++) { b[2 * i] = hex[(d[i] >> 4) & 0xF]; b[2 * i + 1] = hex[d[i] & 0xF]; }
\t\tb[2 * n] = '\\0';
\t\treturn b;
\t}
\tbool exists(const uint8_t* key, uint8_t key_len)
\t{
\t\tif (!isValid()) { printf("[ustore] exists: !isValid len=%u idx_size=%zu store=%p\\n", (unsigned)key_len, _index.size(), (void*)this); return false; }
\t\tif(key_len > USTORE_MAX_KEY_LEN) { printf("[ustore] exists: key too long\\n"); return false; }
\t\tIndexValue* e = index_find(key, key_len);
\t\tif (!e) { printf("[ustore] exists: not_in_index len=%u key=%s idx_size=%zu store=%p\\n", (unsigned)key_len, bin_str(key, key_len), _index.size(), (void*)this); return false; }
\t\tif (is_ttl_expired_(e->timestamp, e->ttl)) { printf("[ustore] exists: ttl_expired ts=%u ttl=%u now=%u\\n", e->timestamp, e->ttl, microStore::time()); index_remove(key, key_len); return false; }
\t\tprintf("[ustore] exists: found key=%s store=%p\\n", bin_str(key, key_len), (void*)this);
\t\treturn true;
\t}"""
PUT_OLD = """\t\tindex_insert(key, key_len, current_segment, offset, ts, ttl);
\t\t// Enforce max_recs:"""
PUT_NEW = """\t\tindex_insert(key, key_len, current_segment, offset, ts, ttl);
\t\tprintf("[ustore] put: index_insert done key=%s idx_size=%zu store=%p\\n", bin_str(key, key_len), _index.size(), (void*)this);
\t\t// Enforce max_recs:"""
SPAMMY_OLD = '\t\t\tprintf("[ustore] get: key not found in index\\n");'
SPAMMY_NEW = '\t\t\t/* silenced — fires on every path-store miss, floods USB CDC */'
# FD-leak fix: close active_file before unlinking segments. Two sites.
FDLEAK_FINALIZE_OLD = """\tvoid finalize_compaction()
\t{
\t\tchar tmp_name[USTORE_MAX_FILENAME_LEN]; snprintf(tmp_name, sizeof(tmp_name), \"%s_compact.tmp\", base_prefix);
\t\tchar seg0[USTORE_MAX_FILENAME_LEN]; segment_name(0, seg0);
\t\t// Remove all existing segments, then rename tmp → seg0.
\t\tfor (uint32_t i = 0; i < _segment_count; i++) {
\t\t\tchar sname[USTORE_MAX_FILENAME_LEN]; segment_name(i, sname);
\t\t\t_filesystem.remove(sname);
\t\t}"""
FDLEAK_FINALIZE_NEW = """\tvoid finalize_compaction()
\t{
\t\tchar tmp_name[USTORE_MAX_FILENAME_LEN]; snprintf(tmp_name, sizeof(tmp_name), \"%s_compact.tmp\", base_prefix);
\t\tchar seg0[USTORE_MAX_FILENAME_LEN]; segment_name(0, seg0);
\t\t// pyxis-local: close active_file before unlinking, otherwise the FD
\t\t// leaks on LittleFS / FAT.
\t\tif (active_file) active_file.close();
\t\t// Remove all existing segments, then rename tmp → seg0.
\t\tfor (uint32_t i = 0; i < _segment_count; i++) {
\t\t\tchar sname[USTORE_MAX_FILENAME_LEN]; segment_name(i, sname);
\t\t\t_filesystem.remove(sname);
\t\t}"""
FDLEAK_CLEAR_OLD = """\t\tif (index_file) index_file.close();
\t\tfor(uint32_t i = 0; i < _segment_count; i++)
\t\t{
\t\t\tsegment_name(i,name);
\t\t\t_filesystem.remove(name);
\t\t}"""
FDLEAK_CLEAR_NEW = """\t\tif (index_file) index_file.close();
\t\t// pyxis-local: same active_file FD-leak fix as finalize_compaction().
\t\tif (active_file) active_file.close();
\t\tfor(uint32_t i = 0; i < _segment_count; i++)
\t\t{
\t\t\tsegment_name(i,name);
\t\t\t_filesystem.remove(name);
\t\t}"""
DIAG_ENABLED = os.environ.get("PYXIS_FILESTORE_DIAG", "0") == "1"
def patch(content):
out = content
# Silence patch always runs.
if SPAMMY_OLD in out:
out = out.replace(SPAMMY_OLD, SPAMMY_NEW)
print("PATCH: FileStore.h: silenced 'key not found in index' spam")
elif "silenced — fires on every path-store miss" in content:
print("PATCH: FileStore.h: 'key not found' spam already silenced")
# FD-leak fix always runs.
if FDLEAK_FINALIZE_OLD in out:
out = out.replace(FDLEAK_FINALIZE_OLD, FDLEAK_FINALIZE_NEW)
print("PATCH: FileStore.h: closed active_file in finalize_compaction()")
elif "pyxis-local: close active_file before unlinking" in content:
print("PATCH: FileStore.h: finalize_compaction() FD-leak fix already applied")
if FDLEAK_CLEAR_OLD in out:
out = out.replace(FDLEAK_CLEAR_OLD, FDLEAK_CLEAR_NEW)
print("PATCH: FileStore.h: closed active_file in clear()")
elif "pyxis-local: same active_file FD-leak fix" in content:
print("PATCH: FileStore.h: clear() FD-leak fix already applied")
# Diagnostic patches only when explicitly requested.
if not DIAG_ENABLED:
return out
if OLD in out:
out = out.replace(OLD, NEW)
print("PATCH: FileStore.h: exists() diagnostics added")
elif "store=%p" in content and "exists:" in content:
print("PATCH: FileStore.h: exists() already patched")
if PUT_OLD in out:
out = out.replace(PUT_OLD, PUT_NEW)
print("PATCH: FileStore.h: put-after-insert diagnostics added")
elif "put: index_insert done" in content:
print("PATCH: FileStore.h: put-after-insert already patched")
return out
if os.path.exists(FILESTORE_H):
with open(FILESTORE_H) as f:
content = f.read()
new = patch(content)
if new != content:
with open(FILESTORE_H, "w") as f:
f.write(new)
else:
print("PATCH: FileStore.h not found, skipping")