okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffix: restore host staging progress and correct configuration597cb84

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