package hostexec import ( "bytes" "encoding/json" "errors" "io" "strings" "time" "github.com/carlosframework/platform/internal/controlname" "github.com/carlosframework/platform/internal/rootparticipation" ) const TTL = 90 * time.Second const Renewal = 20 * time.Second const Margin = 5 * time.Second var ErrControllerBusy = errors.New("hostexec: controller already holds lifetime lock") var ErrLost = errors.New("hostexec: lease lost") var ErrInactive = errors.New("hostexec: no active participation") var ErrState = errors.New("hostexec: invalid local state") var ErrUnsupported = errors.New("hostexec: suspend-aware clock unavailable") // Record is the complete remote wire object. It grants no claim authority. type Record struct { Op string `json:"op"` Expires time.Time `json:"expires"` } type Binding struct { Box string `json:"box"` Enrollment string `json:"enrollment"` Host string `json:"host"` Incarnation string `json:"incarnation"` Op string `json:"op"` BootID string `json:"boot_id"` ControllerID string `json:"controller_id"` } type ControllerProcess = rootparticipation.ControllerProcess type State struct { Process ControllerProcess `json:"process,omitzero"` V int `json:"v"` Binding LeaseETag string `json:"lease_etag"` Expires time.Time `json:"expires"` SafeUntil int64 `json:"safe_until_boottime_ns"` State string `json:"state"` } func validID(s string) bool { return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\\x00\r\n\t ") } func (b Binding) valid() bool { return controlname.ValidHostShape(b.Host) && validID(b.Box) && validID(b.Enrollment) && validID(b.Incarnation) && validID(b.Op) && validID(b.BootID) && validID(b.ControllerID) } func (s State) validate() error { if s.V != 1 || !s.Binding.valid() || s.SafeUntil <= 0 || s.Expires.IsZero() { return ErrState } switch s.State { case "pending", "lost": case "active": if s.LeaseETag == "" { return ErrState } default: return ErrState } return nil } func decode(b []byte, dst any) error { d := json.NewDecoder(bytes.NewReader(b)) d.DisallowUnknownFields() if err := d.Decode(dst); err != nil { return err } var extra any if err := d.Decode(&extra); err != io.EOF { return ErrState } return nil } func Key(host, box string) (string, error) { if !controlname.ValidHostShape(host) || !validID(box) { return "", ErrState } return "control/hosts/" + host + "/exec/" + box + ".json", nil } // BootNanos includes time spent suspended. Wall time is used only on the wire. type Clock interface { Now() (wall time.Time, bootNanos int64, err error) After(time.Duration) <-chan time.Time }