Files
mesh-host/internal/meshcli/ask.go
T
jochen 4a5dcb0209
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Say the login session a mesh-cli line comes from, and pin the SDK at go/v0.1.9 (hq ADR 0272)
A service running as the operator — the tool runner, a unit of the
account's user manager — shares the operator's user id, and was as much the
terminal on the control-node as a person typing. The engine now reads the
asking process's cgroup and sends its login session's scope
(session-<id>.scope under the account's own user-<uid>.slice), or nothing;
the controller makes nothing else the terminal. The SDK is pinned at its
tag now that go/v0.1.9 exists.
2026-10-09 13:40:17 +02:00

93 lines
3.3 KiB
Go

package meshcli
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"git.novox.be/novox/mesh-sdk/go/cli"
"github.com/novox/mesh-host/internal/link"
)
// The controller answers a line as it answers every call (novox/hq ADR 0272, review): in an envelope, `result` or
// `error`, and within its window for an answer — so a line still running is answered `running` with its call, and
// followed by the same account on the same node until it ends. Its side holds the same field names (mesh-controller
// internal/link, the mesh-cli field names' test).
// ErrNotAsked is the link's: a request it knows it never delivered.
var ErrNotAsked = link.ErrNotAsked
// AskFunc asks the controller one request on this node's subject and returns its reply.
type AskFunc func(ctx context.Context, body []byte) ([]byte, error)
// followEvery is how often a line still running is asked about again.
var followEvery = 2 * time.Second
// envelope is a call's answer: its result, or its error.
type envelope struct {
Result json.RawMessage `json:"result"`
Error string `json:"error"`
}
// runningResult is what a call still running answers in its result.
type runningResult struct {
Running bool `json:"running"`
Call string `json:"call"`
}
// AskTheController asks the line and follows it to its answer, within ctx. What it says is one of three things,
// never confused: what the line printed; that nothing ran, and why (the request was never taken); or that what came
// of it is not known — it may have run — naming the call to look at when one is known.
func AskTheController(ctx context.Context, ask AskFunc, asked Asked) cli.Answer {
body, _ := json.Marshal(asked)
reply, err := ask(ctx, body)
if err != nil {
// **Only a request the link knows it never asked ran nothing**: no link open, nobody listening. Any other
// failure may come after the request was delivered, and the line may have run (review of ADR 0272).
if errors.Is(err, ErrNotAsked) {
return cli.Refusal("the controller was not reached, so nothing ran: " + err.Error())
}
return cli.NotKnown("", "no answer from the controller ("+err.Error()+")")
}
call := ""
for {
var env envelope
if err := json.Unmarshal(reply, &env); err != nil {
return cli.NotKnown(call, "the controller's answer could not be read")
}
if env.Error != "" {
return cli.NotKnown(call, "the controller answered "+env.Error)
}
var r runningResult
_ = json.Unmarshal(env.Result, &r)
if !r.Running {
var a cli.Answer
if err := json.Unmarshal(env.Result, &a); err != nil {
return cli.NotKnown(call, "the controller's answer could not be read")
}
return a
}
if r.Call != "" {
call = r.Call
}
if call == "" {
return cli.NotKnown("", "the controller says the line is running and names no call to follow")
}
// Asked again until it ends, through a link that may drop and come back: a failed follow is tried
// again, and only ctx ending stops it.
for {
select {
case <-ctx.Done():
return cli.NotKnown(call, fmt.Sprintf("no answer within the time mesh-cli waits (%s)", cli.AskWithin))
case <-time.After(followEvery):
}
body, _ := json.Marshal(Asked{Follow: call, Account: asked.Account, UID: asked.UID, Session: asked.Session})
if reply, err = ask(ctx, body); err == nil {
break
}
}
}
}