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}) if reply, err = ask(ctx, body); err == nil { break } } } }