77 lines
3.7 KiB
Go
77 lines
3.7 KiB
Go
package backup
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// ── D64 = B (operator ruling 2026-10-29 12:10) + R-1343: the operator clears a hold IN THE RUNNING CONTROLLER ──────
|
|
//
|
|
// One door for both of the operator's routes — the hub's „Clear hold" (operator action clear_hold) and the box command
|
|
// `--clear-restore-hold`, which now asks the running controller over its local socket (cmd/controller/holdctl.go).
|
|
//
|
|
// WHY IN THIS PROCESS (R-1343, measured 2026-10-29 on Tester 1): the CLI used to run as a SECOND process that edited
|
|
// settings.json while the running controller kept the hold in its own in-memory Settings — and that controller's next
|
|
// save (after a hub report) wrote the hold back. Clearing through the running Settings removes the hold from memory
|
|
// AND saves the file at once, so no later save of any component can bring it back.
|
|
// Pinned by TestD64_ClearHoldStaysGoneAfterALaterSave (cmd/controller).
|
|
//
|
|
// EVERY REASON (D64 = B): the hold's reason does not matter — R-379 now reads „a person — on the box, or the operator
|
|
// on the hub". One reason needs a second act: a put-back WAITING for its drive (HoldReasonPutBackPending) is also
|
|
// recorded in the app-stop marker, and a controller start with the drive bound would pour that old copy back over
|
|
// whatever the app wrote after the clear. So the clear also drops the app from the marker; the copy stays on disk
|
|
// (nothing is deleted). It runs under resumeMu, so it can never interleave with a put-back of the same app.
|
|
// Pinned by TestD64_ClearPutBackPendingDropsTheMarkerEntry.
|
|
|
|
// ErrHoldBadStack: the argument is not a valid stack name. ErrNotHeld: the app has no hold. Both are refusals —
|
|
// nothing was changed.
|
|
var (
|
|
ErrHoldBadStack = errors.New("not a valid app name")
|
|
ErrNotHeld = errors.New("this app has no hold")
|
|
)
|
|
|
|
// IsValidHoldStack reports whether s may name an app for the hold commands (the backup package's strict
|
|
// isSafeStackName: letters, digits, '-' and '_', at most 64).
|
|
func IsValidHoldStack(s string) bool { return isSafeStackName(s) }
|
|
|
|
// ClearHoldByOperator lifts stack's hold, whatever its reason, in the hold store the running controller uses, and
|
|
// returns the hold it lifted. A waiting put-back is dropped from the app-stop marker too (see above). Refusals
|
|
// (ErrHoldBadStack, ErrNotHeld) change nothing.
|
|
func (g *AppStopGuard) ClearHoldByOperator(stack string) (settings.RestoreHold, error) {
|
|
return g.ClearHoldByOperatorThen(stack, nil)
|
|
}
|
|
|
|
// ClearHoldByOperatorThen is ClearHoldByOperator with beforeLift run once the hold is FOUND and before it is lifted,
|
|
// under the same lock (R-1350: the household's „the stop is wanted" is recorded before the hold stops keeping the app out
|
|
// of the dead-app scan). beforeLift is not run on a refusal.
|
|
func (g *AppStopGuard) ClearHoldByOperatorThen(stack string, beforeLift func()) (settings.RestoreHold, error) {
|
|
if !isSafeStackName(stack) {
|
|
return settings.RestoreHold{}, ErrHoldBadStack
|
|
}
|
|
if g == nil || g.holds == nil {
|
|
return settings.RestoreHold{}, errors.New("no hold store is wired on this box")
|
|
}
|
|
resumeMu.Lock()
|
|
defer resumeMu.Unlock()
|
|
h, ok := g.holds.GetRestoreHold(stack)
|
|
if !ok {
|
|
return settings.RestoreHold{}, ErrNotHeld
|
|
}
|
|
if beforeLift != nil {
|
|
beforeLift()
|
|
}
|
|
cleared, err := g.holds.ClearRestoreHold(stack)
|
|
if err != nil {
|
|
return h, err
|
|
}
|
|
if !cleared {
|
|
return settings.RestoreHold{}, ErrNotHeld
|
|
}
|
|
if h.Reason == settings.HoldReasonPutBackPending {
|
|
g.dropFromMarker(stack)
|
|
g.logger.Printf("[WARN] [appstop] %s: its waiting put-back is DROPPED with the hold — the copy stays on disk at %s and is never poured back by itself", stack, h.SafetyDump)
|
|
}
|
|
return h, nil
|
|
}
|