v0.311.0: after a power cut, an app repairs the files the cut left empty; the household sees lost photos (R-1007)
gates / gates (push) Successful in 52s
gates / gates (push) Successful in 52s
New template block after_unclean_boot (decision 236): once per controller start that followed an UNCLEAN host boot (crashboot's own verdict, after the 15-minute crash grace; a normal or unknown boot never runs it), each app with the block rebuilds ONLY its zero-byte files (scan: regular files, no symlinks, its own filesystem; ids appended to the app's own command in batches; one retry, then reported) and its own lost-file check is read. The household sees lost files (name, date taken) on the app page until dismissed; the operator gets app_repaired_after_crash with counts only. Skips, with the reason, an unproven image, an unknown key, an app not running. MinAgent 0.155.0 unchanged. Tests red-proofed (14 guards). One new parity fixture (the new card only). Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -310,6 +310,10 @@ type Manager struct {
|
||||
updateUndoHealthFn func(ctx context.Context, name string, timeout time.Duration, meta *Metadata) (bool, string)
|
||||
// afterLoadFn is RunAfterLoad's exec seam (kept.go); nil = compose exec with the app's env.
|
||||
afterLoadFn func(dir string, args ...string) (string, error)
|
||||
// aubSleepFn is RunAfterUncleanBoot's wait seam (tests); nil = time.Sleep.
|
||||
aubSleepFn func(time.Duration)
|
||||
// aubRunningFn is its "is the app up" seam (tests); nil = RefreshStatus + StateRunning.
|
||||
aubRunningFn func(name string) bool
|
||||
// keptUnitFn names the removed app's recovery unit on a drive (backup.RemovedAppUnitFor, wired in
|
||||
// main.go) so a start-fresh moves it in with the files it belongs to. nil = files only.
|
||||
keptUnitFn func(app, drive string) string
|
||||
@@ -1150,6 +1154,11 @@ func deepCopyStack(s *Stack) Stack {
|
||||
acCopy.LockedFields = make([]string, len(s.AppConfig.LockedFields))
|
||||
copy(acCopy.LockedFields, s.AppConfig.LockedFields)
|
||||
}
|
||||
if r := s.AppConfig.AfterUncleanBoot; r != nil { // R-1007: the household's lost list is a slice
|
||||
rc := *r
|
||||
rc.Lost = append([]LostFile(nil), r.Lost...)
|
||||
acCopy.AfterUncleanBoot = &rc
|
||||
}
|
||||
cp.AppConfig = &acCopy
|
||||
}
|
||||
|
||||
@@ -1242,6 +1251,24 @@ func deepCopyStack(s *Stack) Stack {
|
||||
as.Args, as.Command, as.OpenCommand = cloneStrs(as.Args), cloneStrs(as.Command), cloneStrs(as.OpenCommand)
|
||||
cp.Meta.AfterSetup = &as
|
||||
}
|
||||
if s.Meta.AfterUncleanBoot != nil { // R-1007
|
||||
ab := *s.Meta.AfterUncleanBoot
|
||||
ab.Images = cloneStrs(ab.Images)
|
||||
ab.Repair.Command = cloneStrs(ab.Repair.Command)
|
||||
if ab.ReportLost != nil {
|
||||
rl := *ab.ReportLost
|
||||
rl.Command = cloneStrs(rl.Command)
|
||||
ab.ReportLost = &rl
|
||||
}
|
||||
if ab.Extra != nil {
|
||||
ex := make(map[string]any, len(ab.Extra))
|
||||
for k, v := range ab.Extra {
|
||||
ex[k] = v
|
||||
}
|
||||
ab.Extra = ex
|
||||
}
|
||||
cp.Meta.AfterUncleanBoot = &ab
|
||||
}
|
||||
if s.Meta.SetupDoneProbe != nil {
|
||||
sp := *s.Meta.SetupDoneProbe
|
||||
cp.Meta.SetupDoneProbe = &sp
|
||||
|
||||
Reference in New Issue
Block a user