okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
f6da61d
main
1package storage 2 3import ( 4"albedo/cli/internal/daemon" 5"bytes" 6"context" 7"encoding/json" 8"errors" 9"fmt" 10"io" 11"os/exec" 12"strings" 13) 14 15// maintenance owns both mutations and the lock. Closing input revokes ownership. 16type maintenance struct { 17command * exec.Cmd 18input io.WriteCloser 19output * json.Decoder 20stderr bytes.Buffer 21waited bool 22} 23 24type maintenanceMessage struct { 25Result * CleanupResult `json:"result"` 26Ready bool `json:"ready"` 27} 28 29func (s * Service )startMaintenance (ctx context.Context ,files []daemon.StorageFile ) (* maintenance ,error ) { 30command ,err := s .command (ctx ,"storage" ,"maintain" ,s .Home ) 31if err != nil { 32return nil ,err 33 } 34helper := & maintenance {command :command } 35helper .command .Stderr = & helper .stderr 36input ,err := helper .command .StdinPipe () 37if err != nil { 38return nil ,err 39 } 40helper .input = input 41output ,err := helper .command .StdoutPipe () 42if err != nil { 43_ = input .Close () 44return nil ,err 45 } 46helper .output = json .NewDecoder (output ) 47helper .output .DisallowUnknownFields () 48if err := helper .command .Start ();err != nil { 49_ = input .Close () 50return nil ,err 51 } 52paths := make ([]string ,0 ,len (files )) 53for _ ,file := range files { 54paths = append (paths ,file .Path ) 55 } 56if err := json .NewEncoder (input ).Encode (struct { 57Paths []string `json:"paths"` 58 }{paths });err != nil { 59helper .close () 60return nil ,helper .failure (ctx ,err ) 61 } 62var message maintenanceMessage 63if err := helper .output .Decode (& message );err != nil { 64helper .close () 65return nil ,helper .failure (ctx ,err ) 66 } 67if ! message .Ready || message .Result != nil { 68helper .close () 69return nil ,helper .failure (ctx ,errors .New ("invalid maintenance readiness" )) 70 } 71return helper ,nil 72} 73 74// close always reaps the child, including revalidation failures before authorization. 75func (m * maintenance )close () { 76_ = m .input .Close () 77if ! m .waited { 78_ = m .command .Process .Kill () 79_ = m .command .Wait () 80m .waited = true 81 } 82} 83 84func (m * maintenance )failure (ctx context.Context ,err error )error { 85if ctx .Err ()!= nil { 86return ctx .Err () 87 } 88return fmt .Errorf ("storage maintenance: %w: %s" ,err ,strings .TrimSpace (m .stderr .String ())) 89} 90 91func (m * maintenance )apply (ctx context.Context ,vacuum bool ,fresh daemon.StorageReport ) (CleanupResult ,error ) { 92var result CleanupResult 93err := json .NewEncoder (m .input ).Encode (struct { 94Apply bool `json:"apply"` 95Vacuum bool `json:"vacuum"` 96Before int64 `json:"before"` 97FreePages int64 `json:"free_pages"` 98 }{true ,vacuum ,fresh .Database ,fresh .DB .FreePages }) 99if err != nil { 100m .close () 101return result ,m .failure (ctx ,err ) 102 } 103var message maintenanceMessage 104if err = m .output .Decode (& message );err != nil { 105m .close () 106return result ,m .failure (ctx ,err ) 107 } 108if message .Ready || message .Result == nil { 109m .close () 110return result ,m .failure (ctx ,errors .New ("invalid maintenance result" )) 111 } 112// Keep input open until the helper finishes, so its EOF watcher cannot 113// interrupt successful work. A valid result alone does not prove success. 114err = m .command .Wait () 115m .waited = true 116if ctx .Err ()!= nil { 117return result ,ctx .Err () 118 } 119if err != nil { 120return result ,m .failure (ctx ,err ) 121 } 122return * message .Result ,nil 123}