okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
597cb84
main
1package daemon 2 3import ( 4"context" 5"encoding/json" 6"errors" 7"io" 8"net/http" 9"time" 10 11"albedo/cli/internal/daemon/protocol" 12) 13 14type FolderList struct { 15Path string `json:"path"` 16Home string `json:"home"` 17Entries []FolderEntry `json:"entries"` 18Truncated bool `json:"truncated"` 19} 20 21type FolderEntry struct { 22Name string `json:"name"` 23// VCS is "jj", "git", or empty when the folder is no repository root. 24VCS string `json:"vcs"` 25Modified int64 `json:"modified"` // Unix seconds; zero means unknown. 26Hidden bool `json:"hidden"` 27} 28 29// Repo is the repository a folder is in. Fields whose command failed are 30// empty; Kind and Root are always there. 31type Repo struct { 32Bookmark * Bookmark `json:"bookmark"` 33Changed * int `json:"changed"` 34Touched * int64 `json:"touched"` // Unix seconds, when observed. 35Kind string `json:"kind"` 36Root string `json:"root"` 37Branch string `json:"branch"` 38Commit string `json:"commit"` 39Change string `json:"change"` 40} 41 42type Bookmark struct { 43Name string `json:"name"` 44Ahead int `json:"ahead"` 45} 46 47type FolderPreview struct { 48Path string `json:"path"` 49Repo * Repo `json:"repo"` 50Languages []LanguageShare `json:"languages"` 51Tree []FolderNode `json:"tree"` 52More int `json:"more"` 53} 54 55type LanguageShare struct { 56Color * string `json:"color"` 57Name string `json:"name"` 58Share float64 `json:"share"` 59} 60 61type FolderNode struct { 62Name string `json:"name"` 63Language string `json:"language"` 64Children []FolderNode `json:"children"` 65Changed int `json:"changed"` 66More int `json:"more"` 67Dir bool `json:"dir"` 68} 69 70type HostStatus struct { 71Host string `json:"host"` 72State string `json:"state"` 73Detail string `json:"detail"` 74Step string `json:"step"` 75OS string `json:"os"` 76Arch string `json:"arch"` 77Home string `json:"home"` 78ControlPath string `json:"control_path"` 79} 80 81// KnownHost is a host worth offering: one sessions worked on ("recent"), or 82// a Host entry of the daemon's ssh config ("config"). State is the cached 83// probe's, empty when there is none. 84type KnownHost struct { 85Host string `json:"host"` 86Label string `json:"label"` 87Source string `json:"source"` 88State string `json:"state"` 89} 90 91func workspaceDirectory (ctx context.Context ,conn * Connection ,location string ,preview bool ) (protocol.WorkspaceDirectory ,error ) { 92params := protocol.GetWorkspacesParams {Location :& location ,Limit :new (int64 (200 ))} 93if preview { 94params .Include = new ("preview ") 95 } 96var result protocol.WorkspaceDirectory 97err := walkPages (func (next * string ) (protocol.WorkspaceDirectory ,* string ,error ) { 98params .Next = next 99var page protocol.WorkspaceDirectory 100pageErr := executeRead (ctx ,conn ,operation {Capability :"workspace_browsing" ,Name :"browse workspace" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 101return protocol .NewGetWorkspacesRequest (base ,& params ) 102 },Policy :readRecovery },func (data []byte )error { 103return decodeRequired (data ,& page ) 104 }) 105return page ,page .Next ,pageErr 106 },func (page protocol.WorkspaceDirectory )error { 107if page .Items == nil { 108return fieldError ("workspace entries" ) 109 } 110if result .Directory == "" { 111result = page 112 }else { 113if result .Directory != page .Directory { 114return fieldError ("workspace page" ) 115 } 116result .Items = append (result .Items ,page .Items ... ) 117 } 118return nil 119 },"workspace page cursor" ) 120if err != nil { 121return result ,err 122 } 123return result ,nil 124} 125 126func ListFolders (ctx context.Context ,conn * Connection ,path string ) (FolderList ,error ) { 127w ,err := workspaceDirectory (ctx ,conn ,path ,false ) 128result := FolderList {Path :w .Directory ,Home :value (w .Home ),Entries : []FolderEntry {}} 129for _ ,row := range w .Items { 130var modified int64 131if t := timestampSeconds (value (row .ModifiedAt ));t != nil { 132modified = * t 133 } 134result .Entries = append (result .Entries ,FolderEntry {Name :row .Name ,VCS :value (row .Vcs ),Modified :modified ,Hidden :row .Hidden }) 135 } 136return result ,err 137} 138func previewValue (w protocol.WorkspacePreview ,path string ) (FolderPreview ,error ) { 139result := FolderPreview {Path :path ,More :int (w .More ),Languages : []LanguageShare {},Tree : []FolderNode {}} 140if len (w .Repository )> 0 && string (w .Repository )!= "null" { 141var kind struct { 142Kind string `json:"kind"` 143 } 144if err := json .Unmarshal (w .Repository ,& kind );err != nil { 145return result ,err 146 } 147switch kind .Kind { 148case "git" : 149var r protocol.GitFacts 150if err := decodeRequired (w .Repository ,& r );err != nil { 151return result ,err 152 } 153result .Repo = & Repo {Kind :r .Kind ,Root :r .Root ,Branch :value (r .Branch ),Commit :value (r .Revision ),Changed :intPointer (r .Changed ),Touched :timestampSeconds (value (r .TouchedAt ))} 154case "jj" : 155var r protocol.JJFacts 156if err := decodeRequired (w .Repository ,& r );err != nil { 157return result ,err 158 } 159result .Repo = & Repo {Kind :r .Kind ,Root :r .Root ,Change :value (r .ChangeID ),Commit :value (r .Revision ),Changed :intPointer (r .Changed ),Touched :timestampSeconds (value (r .TouchedAt ))} 160if r .Bookmark != nil { 161result .Repo .Bookmark = & Bookmark {Name :r .Bookmark .Name ,Ahead :int (r .Bookmark .Ahead )} 162 } 163default : 164return result ,fieldError ("repository kind" ) 165 } 166 } 167for _ ,row := range w .Languages { 168result .Languages = append (result .Languages ,LanguageShare {Name :row .Name ,Color :row .Color ,Share :row .Share }) 169 } 170for _ ,row := range w .Tree { 171node := FolderNode {Name :row .Name ,Language :value (row .Language ),Dir :row .Kind == "directory" ,Changed :int (row .Changed ),More :int (row .More )} 172for _ ,child := range row .Children { 173node .Children = append (node .Children ,FolderNode {Name :child .Name ,Dir :child .Kind == "directory" ,Language :value (child .Language ),Changed :int (child .Changed )}) 174 } 175result .Tree = append (result .Tree ,node ) 176 } 177return result ,nil 178} 179func PreviewFolder (ctx context.Context ,conn * Connection ,path string ) (FolderPreview ,error ) { 180w ,err := workspaceDirectory (ctx ,conn ,path ,true ) 181if err != nil { 182return FolderPreview {},err 183 } 184if w .Preview == nil { 185return FolderPreview {},fieldError ("workspace preview" ) 186 } 187return previewValue (* w .Preview ,w .Directory ) 188} 189func FolderRepo (ctx context.Context ,conn * Connection ,path string ) (* Repo ,error ) { 190preview ,err := PreviewFolder (ctx ,conn ,path ) 191return preview .Repo ,err 192} 193func MoveSession (ctx context.Context ,conn * Connection ,id ,workspace string ,condition SessionCondition ) (Session ,error ) { 194return patchSession (ctx ,conn ,id ,condition .ETag ,map [string ]any {"workspace" :workspace ,"family_revision" :condition .FamilyRevision }) 195} 196func hostValue (w protocol.Host )HostStatus { 197r := HostStatus {Host :w .Target ,State :w .State ,Step :value (w .Step ),OS :value (w .Os ),Arch :value (w .Architecture ),Home :value (w .Home )} 198if w .Detail != nil { 199r .Detail = w .Detail .Detail 200 } 201if w .Authentication != nil { 202r .ControlPath = value (w .Authentication .ControlPath ) 203 } 204return r 205} 206func hosts (ctx context.Context ,conn * Connection ,target * string ) ([]protocol.Host ,error ) { 207result := []protocol.Host {} 208params := protocol.ListHostsParams {Limit :new (int64 (200 )),Target :target } 209err := walkPages (func (next * string ) (protocol.HostPage ,* string ,error ) { 210params .Next = next 211var page protocol.HostPage 212pageErr := executeRead (ctx ,conn ,operation {Capability :"host_probes" ,Name :"list hosts" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 213return protocol .NewListHostsRequest (base ,& params ) 214 },Policy :readRecovery },func (data []byte )error {return decodeRequired (data ,& page ) }) 215return page ,page .Next ,pageErr 216 },func (page protocol.HostPage )error { 217result = append (result ,page .Items ... ) 218return nil 219 },"host page cursor" ) 220if err != nil { 221return nil ,err 222 } 223return result ,nil 224} 225 226func ListHosts (ctx context.Context ,conn * Connection ) ([]KnownHost ,error ) { 227rows ,err := hosts (ctx ,conn ,nil ) 228result := []KnownHost {} 229for _ ,row := range rows { 230result = append (result ,KnownHost {Host :row .Target ,Label :row .Target ,State :row .State }) 231 } 232return result ,err 233} 234func GetHost (ctx context.Context ,conn * Connection ,host string ) (HostStatus ,error ) { 235rows ,err := hosts (ctx ,conn ,& host ) 236if err != nil { 237return HostStatus {},err 238 } 239for _ ,row := range rows { 240if row .Target == host { 241return hostValue (row ),nil 242 } 243 } 244return HostStatus {Host :host ,State :"unknown" },nil 245} 246func WarmHost (ctx context.Context ,conn * Connection ,host string ) (HostStatus ,error ) { 247if ctx == nil { 248ctx = context .Background () 249 } 250ctx ,cancel := context .WithTimeout (ctx ,90 * time .Second ) 251defer cancel () 252var w protocol.Host 253op := operation {Capability :"host_probes" ,Name :"probe host" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 254return protocol .NewProbeHostRequestWithBody (base ,host ,"application/json" ,body ) 255 },Body :struct {}{},Policy :noRecovery } 256body ,err := requestBytes (ctx ,conn ,op ,responseLimits {successStatus :http .StatusAccepted ,bodyBytes :1024 * 1024 ,errorBytes :64 * 1024 }) 257if err != nil { 258return HostStatus {},err 259 } 260if err = decodeRequired (body ,& w );err != nil { 261return HostStatus {},invalidResponse (op ,"" ,err ) 262 } 263if w .Target != host { 264return HostStatus {},errors .New ("host probe returned another host" ) 265 } 266status := hostValue (w ) 267for status .State == "probing" || status .State == "unknown" { 268timer := time .NewTimer (200 * time .Millisecond ) 269select { 270case <- ctx .Done (): 271timer .Stop () 272return HostStatus {},ctx .Err () 273case <- timer .C : 274 } 275status ,err = GetHost (ctx ,conn ,host ) 276if err != nil { 277return HostStatus {},err 278 } 279 } 280return status ,nil 281} 282func (c * ChatClient )WarmHost (ctx context.Context ,host string ) (HostStatus ,error ) { 283return WarmHost (ctx ,c .conn ,host ) 284} 285func (c * ChatClient )Host (ctx context.Context ,host string ) (HostStatus ,error ) { 286return GetHost (ctx ,c .conn ,host ) 287} 288func (c * ChatClient )LocalDaemon ()bool {return c .conn .Local () }