okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
a229862
main
1package daemon 2 3import ( 4"context" 5"encoding/json" 6"errors" 7"io" 8"maps" 9"net/http" 10 11"albedo/cli/internal/daemon/protocol" 12) 13 14const ProtocolVersion = 3 15 16type ServerInfo struct { 17State string 18Protocol int 19InstanceID string 20Capabilities map [string ]int64 21Build string 22Digest string 23Notices []ServerNotice 24} 25type ServerNotice struct {ID ,Kind ,Message string } 26 27func decodeServer (body []byte ) (ServerInfo ,error ) { 28var server protocol.Server 29if err := decodeRequired (body ,& server );err != nil { 30return ServerInfo {},& ProtocolError {Code :"invalid_server" ,Operation :"read server" ,Cause :err } 31 32 } 33if server .InstanceID == "" || server .Protocol <= 0 || server .Capabilities == nil || (server .State != "ready" && server .State != "draining" ) { 34return ServerInfo {},& ProtocolError {Code :"invalid_server" ,Operation :"read server" ,Cause :errors .New ("missing daemon identity, protocol, state, or capabilities" )} 35 } 36return ServerInfo {State :server .State ,Protocol :int (server .Protocol ),InstanceID :server .InstanceID ,Capabilities :server .Capabilities ,Build :value (server .Build ),Digest :value (server .Digest ),Notices :noticeValues (server .Notices )},nil 37 38} 39 40func ProbeServer (ctx context.Context ,conn * Connection ) (ServerInfo ,error ) { 41if conn == nil { 42return ServerInfo {},errors .New ("not connected to Albedo" ) 43 } 44if ctx == nil { 45ctx = context .Background () 46 } 47body ,err := requestBytes (ctx ,conn ,operation {Name :"read server" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 48return protocol .NewGetServerRequest (base ,nil ) 49 },Policy :noRecovery },responseLimits {successStatus :200 ,bodyBytes :1048576 ,errorBytes :65536 }) 50if err != nil { 51return ServerInfo {},err 52 } 53return decodeServer (body ) 54} 55 56func CheckCompatible (server ServerInfo )error { 57failure := & CompatibilityError {Version :server .Protocol } 58for _ ,name := range []string {"durable_inputs" ,"session_replay" ,"collection_invalidation" ,"tool_progress" } { 59minimum := int64 (1 ) 60if name == "session_replay" { 61minimum = 2 62 } 63if server .Capabilities [name ]< minimum { 64failure .MissingCapabilities = append (failure .MissingCapabilities ,name ) 65 } 66 } 67if server .Protocol != ProtocolVersion || len (failure .MissingCapabilities )> 0 { 68return failure 69 } 70if server .State != "ready" { 71return & APIError {StatusCode :503 ,Code :"server_draining" ,Message :"the daemon is draining" } 72 } 73return nil 74} 75 76func Attach (ctx context.Context ,snapshot ConnectionSnapshot ,rediscover Rediscovery ) (* Connection ,error ) { 77if snapshot .Port < 1 || snapshot .Port > 65535 || snapshot .Token == "" { 78return nil ,& ProtocolError {Code :"invalid_endpoint" ,Operation :"attach daemon" ,Cause :errors .New ("daemon endpoint requires a valid port and token" )} 79 } 80conn := NewConnection (snapshot ,rediscover ) 81server ,err := ProbeServer (ctx ,conn ) 82if err == nil { 83err = CheckCompatible (server ) 84 } 85if err != nil { 86conn .HTTPClient ().CloseIdleConnections () 87return nil ,err 88 } 89snapshot .Version ,snapshot .Build ,snapshot .Digest ,snapshot .InstanceID = server .Protocol ,server .Build ,server .Digest ,server .InstanceID 90conn .state .Store (& connectionState {endpoint :snapshot ,capabilities :maps .Clone (server .Capabilities )}) 91 92return conn ,nil 93} 94 95func StopDaemon (ctx context.Context ,conn * Connection )error { 96instanceID := conn .Snapshot ().InstanceID 97if instanceID == "" { 98server ,err := ProbeServer (ctx ,conn ) 99if err != nil { 100return err 101 } 102instanceID = server .InstanceID 103 } 104return executeMutation (ctx ,conn ,operation {Name :"stop daemon" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 105return protocol .NewShutdownServerRequestWithBody (base ,"application/json" ,body ) 106 },Body : protocol.ShutdownRequest {InstanceID :instanceID },Policy :noRecovery }, []int {202 },func (data []byte ,_ int )error { 107var reply protocol.Shutdown 108if err := json .Unmarshal (data ,& reply );err != nil { 109return err 110 } 111if reply .InstanceID != instanceID || reply .State != "draining" { 112return fieldError ("shutdown" ) 113 } 114return nil 115 }) 116} 117 118func noticeValues (rows []struct { 119ID string `json:"id"` 120Kind string `json:"kind"` 121Message string `json:"message"` 122}) []ServerNotice { 123result := []ServerNotice {} 124for _ ,row := range rows { 125result = append (result ,ServerNotice {ID :row .ID ,Kind :row .Kind ,Message :row .Message }) 126 } 127return result 128} 129func DismissServerNotices (ctx context.Context ,conn * Connection ,etag string ,ids []string )error { 130var change protocol.UISettingsChange 131err := patchSettingsGroup (ctx ,conn ,"ui" ,etag ,map [string ]any {"dismissed_notices" :ids },& change ) 132return err 133} 134 135// BuildIdentity identifies daemon code by its label and content digest. 136type BuildIdentity struct { 137Build string 138Digest string 139} 140 141// BuildMismatch offers a restart unless matching digests, or matching labels 142// when digests are unavailable, establish that the code is the same. 143func BuildMismatch (running ,selected BuildIdentity )bool { 144if running .Digest != "" && selected .Digest != "" { 145return running .Digest != selected .Digest 146 } 147if running .Build != "" && selected .Build != "" { 148return running .Build != selected .Build 149 } 150return true 151}