okami.mom/albedo

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

Aly Raffaufrefactor(cli): extract local daemon lifecycled9c1a13

main
4.4 KiB146 linesraw
1package daemon
2
3import (
4	"encoding/json"
5	"errors"
6	"fmt"
7	"net/http"
8)
9
10// UpgradeRequiredError identifies a feature missing from the running daemon.
11type UpgradeRequiredError struct {
12	Feature string
13}
14
15func (e *UpgradeRequiredError) Error() string {
16	return "the running copy of Albedo needs an update " + e.Feature + "; restarting will interrupt work in all sessions and clear their Python variables; when you are ready, run albedo daemon --stop, then launch your updated copy of Albedo"
17}
18
19// APIError retains the HTTP status and daemon error code independently of copy.
20type APIError struct {
21	Cause      error
22	Code       string
23	Message    string
24	StatusCode int
25	Decision   json.RawMessage
26}
27
28func (e *APIError) Error() string {
29	if e.Message != "" {
30		return e.Message
31	}
32	if e.Cause != nil {
33		return fmt.Sprintf("HTTP %d: %v", e.StatusCode, e.Cause)
34	}
35	if status := http.StatusText(e.StatusCode); status != "" {
36		return fmt.Sprintf("HTTP %d: %s", e.StatusCode, status)
37	}
38	return fmt.Sprintf("HTTP %d", e.StatusCode)
39}
40
41func (e *APIError) Unwrap() error { return e.Cause }
42
43// DaemonRestarting reports whether the daemon refused because it is going
44// away or not serving yet, so the one that serves next may answer.
45func (e *APIError) DaemonRestarting() bool {
46	return e.Code == "daemon_stopping" || e.Code == "daemon_unavailable"
47}
48
49func decodeAPIError(status int, body []byte) error {
50	var data struct {
51		Code     string          `json:"code"`
52		Detail   string          `json:"detail"`
53		Title    string          `json:"title"`
54		Decision json.RawMessage `json:"decision"`
55	}
56	apiErr := &APIError{StatusCode: status}
57	if err := json.Unmarshal(body, &data); err != nil {
58		return apiErr
59	}
60	apiErr.Code, apiErr.Message, apiErr.Decision = data.Code, data.Detail, data.Decision
61	if apiErr.Message == "" {
62		apiErr.Message = data.Title
63	}
64	return apiErr
65}
66
67// UncertainOutcomeError means the daemon may have applied a mutation. Callers
68// must reconcile its result before asking the user to submit it again.
69type UncertainOutcomeError struct {
70	Cause     error
71	Operation string
72	Handle    *OperationHandle
73}
74
75func (e *UncertainOutcomeError) Error() string {
76	if e.Handle != nil {
77		return e.Operation + " admission is uncertain; operation " + e.Handle.ID() + " can be queried"
78	}
79	return e.Operation + " may have been accepted; check its result before trying again"
80}
81
82func (e *UncertainOutcomeError) Unwrap() error { return e.Cause }
83
84// StreamFailureKind tells subscribers whether reconnecting can make progress.
85type StreamFailureKind uint8
86
87const (
88	StreamTransient StreamFailureKind = iota
89	StreamTerminal
90	StreamProtocol
91)
92
93// StreamError preserves the cause while defining subscription recovery.
94type StreamError struct {
95	Kind  StreamFailureKind
96	Cause error
97}
98
99func (e *StreamError) Error() string { return e.Cause.Error() }
100
101func (e *StreamError) Unwrap() error { return e.Cause }
102
103// CompatibilityError describes a live daemon that cannot serve this client.
104type CompatibilityError struct {
105	Version             int
106	MissingCapabilities []string
107}
108
109func (e *CompatibilityError) Error() string {
110	if e.Version != ProtocolVersion {
111		return fmt.Sprintf("daemon protocol %d is incompatible with this client; required protocol is %d. Stop the daemon with a matching client (albedo daemon --stop), then relaunch this client", e.Version, ProtocolVersion)
112	}
113	return fmt.Sprintf("daemon protocol %d is incompatible with this client; required protocol is %d; missing capabilities: %v", e.Version, ProtocolVersion, e.MissingCapabilities)
114}
115
116// ProtocolError identifies an invalid successful API response.
117type ProtocolError struct {
118	Cause     error
119	Code      string
120	Operation string
121	Field     string
122}
123
124func (e *ProtocolError) Error() string {
125	if e.Field == "" {
126		return fmt.Sprintf("invalid response for %s: %v", e.Operation, e.Cause)
127	}
128	return fmt.Sprintf("invalid response for %s (%s): %v", e.Operation, e.Field, e.Cause)
129}
130
131func (e *ProtocolError) Unwrap() error { return e.Cause }
132
133func invalidResponse(operation operation, field string, cause error) error {
134	return uncertainOperation(operation, &ProtocolError{Code: "invalid_response", Operation: operation.Name, Field: field, Cause: cause})
135}
136
137type responseFieldError struct {
138	cause error
139	field string
140}
141
142func (e *responseFieldError) Error() string { return e.field + ": " + e.cause.Error() }
143
144func fieldError(field string) error {
145	return &responseFieldError{field: field, cause: errors.New("required field is missing or invalid")}
146}