controller v0.72.0: FileBrowser converges on boot-recreate

processGuestBootChange recreated the drive-backed app stacks but never re-synced
FileBrowser (base-infra, no HDD_PATH), so its drive mounts went stale after a
reboot. Now, AFTER pollLiveBinds confirms the live binds and the apps are
recreated, trigger go s.SyncFileBrowserMounts() so FileBrowser converges against
the now-live drives. Refactored into pure recreateDriveBackedApps(stacks, present,
recreate, syncFB). Tests: FB sync runs once after recreate (red-proofed companion);
runs even when nothing recreated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 17:37:12 +02:00
parent 14f200c9a1
commit 6ea25388d7
3 changed files with 92 additions and 11 deletions
+42 -11
View File
@@ -370,24 +370,26 @@ func (s *Server) processGuestBootChange() {
s.logger.Printf("[INFO] [gate] boot %s: waiting (≤%s) for live drive bind(s) %v before recreating drive-backed apps", resp.GuestBootID, bootBindWait, paths)
presentStable := pollLiveBinds(paths, driveBindLive, time.Sleep, time.Now, bootBindWait, bootBindPoll)
skipped := 0
var bootStacks []bootStack
for _, st := range s.stackMgr.GetStacks() {
cfg := s.stackMgr.LoadAppConfigByName(st.Name)
if cfg == nil {
continue
}
if !shouldRecreateOnBoot(cfg.Deployed, cfg.Env["HDD_PATH"], presentStable) {
if cfg.Deployed && strings.HasPrefix(cfg.Env["HDD_PATH"], StableParentDir+"/") {
skipped++ // a deployed drive-backed app whose bind never went live → gate's job
}
continue
}
s.logger.Printf("[INFO] [gate] boot %s: live bind confirmed — recreating drive-backed app %s (state=%s) onto %s", resp.GuestBootID, st.Name, st.State, cfg.Env["HDD_PATH"])
_ = s.stackMgr.StopStack(st.Name)
if serr := s.stackMgr.StartStack(st.Name); serr != nil {
s.logger.Printf("[WARN] [gate] boot recreate %s: %v", st.Name, serr)
bootStacks = append(bootStacks, bootStack{name: st.Name, deployed: cfg.Deployed, hdd: cfg.Env["HDD_PATH"], state: string(st.State)})
}
recreate := func(bs bootStack) {
s.logger.Printf("[INFO] [gate] boot %s: live bind confirmed — recreating drive-backed app %s (state=%s) onto %s", resp.GuestBootID, bs.name, bs.state, bs.hdd)
_ = s.stackMgr.StopStack(bs.name)
if serr := s.stackMgr.StartStack(bs.name); serr != nil {
s.logger.Printf("[WARN] [gate] boot recreate %s: %v", bs.name, serr)
}
}
syncFB := func() {
s.logger.Printf("[INFO] [gate] boot %s: re-syncing FileBrowser mounts against the live binds", resp.GuestBootID)
go s.SyncFileBrowserMounts()
}
_, skipped := recreateDriveBackedApps(bootStacks, presentStable, recreate, syncFB)
if skipped > 0 {
s.logger.Printf("[WARN] [gate] boot %s: %d drive-backed app(s) had no live bind within %s — leaving to the drive gate", resp.GuestBootID, skipped, bootBindWait)
}
@@ -396,6 +398,35 @@ func (s *Server) processGuestBootChange() {
}
}
// bootStack is one deployed stack's boot-recreate inputs (decoupled from stacks.Manager for testing).
type bootStack struct {
name string
deployed bool
hdd string
state string
}
// recreateDriveBackedApps recreates every deployed drive-backed app whose drive bind is live, then
// triggers the FileBrowser sync. FileBrowser binds the drives' userdata but is base-infra (no HDD_PATH),
// so it is NOT in the recreate set — it must be converged HERE, AFTER the recreate (which itself only
// ran once pollLiveBinds confirmed the live binds), so FileBrowser's mounts reflect the now-live drives
// instead of going stale (the gap a host/guest reboot left before this fix). syncFB is always called so
// FileBrowser reflects the current bind state even if no app needed recreating. Pure (ops injected).
func recreateDriveBackedApps(stacks []bootStack, presentStable map[string]bool, recreate func(bootStack), syncFB func()) (recreated, skipped int) {
for _, bs := range stacks {
if !shouldRecreateOnBoot(bs.deployed, bs.hdd, presentStable) {
if bs.deployed && strings.HasPrefix(bs.hdd, StableParentDir+"/") {
skipped++ // a deployed drive-backed app whose bind never went live → gate's job
}
continue
}
recreate(bs)
recreated++
}
syncFB()
return
}
// ---- H1 endpoints (the UI's settings.js calls these; previously 404/unrouted) -----------------
// handleStorageDisconnect EJECTS a drive without restart: stop its apps (gate-stopped), agent-detach the