package dockerexec import "sync" // ── R-863 (v0.294.0): no image clean-up while an image is being pulled ─────────────────────────────── // // MEASURED 2026-10-04 night drill, a fresh box: the controller's one-time image clean-up ran while the // household's first BookStack install was inside `docker compose up -d`. compose had pulled // `mariadb@sha256:…` (stored untagged) and not yet created the container, so no container, installed app // or undo named it; the clean-up deleted it one second before compose created the container, and the // install failed. An app being installed is not "installed" yet, and a restore or undo pulls the same way. // // THE RULE: every compose command that can pull an image and then create a container from it (up, pull, // create, run) holds the image-work lock SHARED for its whole run; an image clean-up pass takes it // EXCLUSIVELY, without waiting (TryLock). So a pass never runs while any such command runs, and a command // that starts during a pass waits the few seconds the pass takes. A pass that cannot take the lock does // not run; its caller tries again later. Pinned by TestR863_* (internal/stacks/image_retention_r863_test.go). var imageWorkMu sync.RWMutex // ImagePulling reports whether compose args are an image-pulling verb (up, pull, create, run). Flags // before the verb (`-p name`, `--profile x`) are skipped. func ImagePulling(args []string) bool { for i := 0; i < len(args); i++ { a := args[i] if a == "compose" { continue } if len(a) > 0 && a[0] == '-' { switch a { case "-p", "--project-name", "-f", "--file", "--profile", "--env-file", "--project-directory": i++ // the flag's value } continue } switch a { case "up", "pull", "create", "run": return true } return false } return false } // BeginImageWork holds the image-work lock shared while compose args pull images; the returned func // releases it. For any other verb it returns a no-op. func BeginImageWork(args []string) (end func()) { if !ImagePulling(args) { return func() {} } imageWorkMu.RLock() return imageWorkMu.RUnlock } // TryImageCleanup takes the image-work lock exclusively if no image work runs now. ok=false: something is // pulling — do not clean up now. func TryImageCleanup() (end func(), ok bool) { if !imageWorkMu.TryLock() { return nil, false } return imageWorkMu.Unlock, true }