fix(i18n): thread-safe translation for the daemon monitor thread (don't race loadLanguage)

Concurrency audit: I18n::translate() returns a const char* pointing INTO strings_, and loadLanguage()
clears/rebuilds strings_ on the main thread at a language switch. The EmbeddedDaemon state/monitor callback
builds its status strings via TR() off the main thread, so a concurrent language switch could free the
map node out from under that pointer — a use-after-free data race.

- Add a shared_mutex guarding strings_: loadLanguage() takes it exclusively for the whole clear+rebuild
  (its helpers loadBuiltinEnglish/overlayTranslations are called only from there and don't lock, so no
  recursion); a new translateCopy()/trCopy() takes a shared lock and returns an owned std::string.
- The lock-free translate()/TR() stays for main-thread UI (same thread as the writer, so never races).
- The four daemon-state setDaemonStatus(TR(...)) calls in the monitor callback now use util::trCopy(...).

Synchronization only; no fund-path call touched. Threading fix — clean build + ctest; the race is runtime-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 18:38:46 -05:00
co-authored by Claude Opus 4.8
parent 6301cd5984
commit 5563becad2
3 changed files with 40 additions and 5 deletions
+4 -4
View File
@@ -6089,16 +6089,16 @@ bool App::startEmbeddedDaemon()
// NB: this callback runs on EmbeddedDaemon's monitor thread — write via the locked setter.
switch (state) {
case daemon::EmbeddedDaemon::State::Starting:
setDaemonStatus(TR("sb_starting_daemon"));
setDaemonStatus(util::trCopy("sb_starting_daemon"));
break;
case daemon::EmbeddedDaemon::State::Running:
setDaemonStatus(TR("sb_dragonxd_running"));
setDaemonStatus(util::trCopy("sb_dragonxd_running"));
break;
case daemon::EmbeddedDaemon::State::Stopping:
setDaemonStatus(TR("sb_dragonxd_stopping"));
setDaemonStatus(util::trCopy("sb_dragonxd_stopping"));
break;
case daemon::EmbeddedDaemon::State::Stopped:
setDaemonStatus(TR("sb_dragonxd_stopped"));
setDaemonStatus(util::trCopy("sb_dragonxd_stopped"));
break;
case daemon::EmbeddedDaemon::State::Error:
setDaemonStatus("Error: " + msg);