Fix ScheduleManager start() self-deadlock on cached schedule

start() held mutex_ and then called loadCache(), which re-locks the same
non-recursive mutex. When a schedule cache file exists and parses (the
normal case for a provisioned kiosk), this self-deadlocks and blocks
SDL_AppInit before the first frame, leaving only a black screen and the
compositor cursor.

Narrow the lock to the member setup so loadCache() can take the lock
itself. Add a regression test that starts with a cached payload present.
This commit is contained in:
NeilClack
2026-10-07 08:43:13 -05:00
parent a5a6b5bef0
commit 9b57e48969
2 changed files with 41 additions and 9 deletions
+28
View File
@@ -297,6 +297,34 @@ TEST(ScheduleManagerTest, OfflineFallsBackToCache) {
radiantwave::sync::schedule_cache::clear();
}
// Regression: start() must not deadlock when a cache file is present. It used
// to hold mutex_ and then call loadCache(), which re-locked the same
// non-recursive mutex (self-deadlock), blocking SDL_AppInit before the first
// frame was ever presented — the kiosk sat on a black screen.
TEST(ScheduleManagerTest, StartWithCachedPayloadDoesNotDeadlock) {
const std::string dir = tempCacheDir();
setenv("HOME", dir.c_str(), 1);
SchedulePayload p = dailyPayload();
ASSERT_TRUE(radiantwave::sync::schedule_cache::write(p, "\"cached\""));
const std::string dbPath =
(std::filesystem::temp_directory_path() / "rw_schedule_start_test.db")
.string();
std::filesystem::remove(dbPath);
radiantwave::db::DatabaseManager db;
ASSERT_TRUE(db.initialize(dbPath));
radiantwave::schedule::ScheduleManager manager;
manager.start(db, "UTC");
EXPECT_TRUE(manager.hasSchedule());
manager.shutdown();
db.close();
std::filesystem::remove(dbPath);
radiantwave::sync::schedule_cache::clear();
}
// ─── Phase 5: consume real Portal-generated payloads ───────────────────
//
// These fixtures were captured from a running Portal (GET