Merge pull request 'mesh-cli: carry what a pipe or a file gives standard input, for the controller's terminal (hq ADR 0259 §10, ADR 0272)' (#164) from feat/mesh-cli-carries-standard-input into main
This commit was merged in pull request #164.
This commit is contained in:
@@ -23,12 +23,42 @@ import (
|
||||
// socket is where the node-engine answers; a variable so a test can name its own.
|
||||
var socket = cli.Socket
|
||||
|
||||
// standardInput is mesh-cli's own standard input, and whether it is a terminal (or a device such as /dev/null),
|
||||
// which is never read; a variable so a test can say.
|
||||
var standardInput = func() (io.Reader, bool) {
|
||||
info, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return os.Stdin, true
|
||||
}
|
||||
return os.Stdin, info.Mode()&os.ModeCharDevice != 0
|
||||
}
|
||||
|
||||
// carried is what mesh-cli carries of its standard input for the line: what a pipe or a file held, at most
|
||||
// cli.MaxStdin, for the controller's terminal alone (a secret given with `secret accept … --from -`). A terminal is
|
||||
// never read: the line is asked without. More than the bound is refused, and nothing is asked.
|
||||
func carried() ([]byte, error) {
|
||||
in, terminal := standardInput()
|
||||
if terminal {
|
||||
return nil, nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(in, cli.MaxStdin+1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("standard input could not be read (%v); nothing was asked", err)
|
||||
}
|
||||
if len(body) > cli.MaxStdin {
|
||||
return nil, fmt.Errorf("standard input is more than %d bytes, which mesh-cli does not carry; nothing was asked", cli.MaxStdin)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
const usage = `mesh-cli — the controller's command lines, from this node (novox/hq ADR 0272)
|
||||
|
||||
mesh-cli <command line> e.g. mesh-cli status, mesh-cli settings show zsh --node <node>
|
||||
nox <command line> the same, in interactive zsh
|
||||
|
||||
Run it as yourself, never under sudo: the node-engine tells the controller which account asks.
|
||||
What a pipe or a file gives its standard input (at most 64 KiB) goes with the line to the controller's
|
||||
terminal alone, e.g. a secret: mesh-cli secret accept <node> <module> <name> --from - < file
|
||||
On the control-node, from its operator's account, the line runs as the controller's terminal.
|
||||
Anywhere else it is an ordinary call, and a terminal-only change is refused with the reason.
|
||||
'mesh-cli help' asks the controller for its own list of command lines.
|
||||
@@ -52,7 +82,12 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
"Nothing was asked\n", verb)
|
||||
return 1
|
||||
}
|
||||
a, err := cli.Ask(ctx, socket, args)
|
||||
in, err := carried()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "mesh-cli: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
a, err := cli.AskWith(ctx, socket, cli.Request{Line: args, Stdin: in})
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
fmt.Fprintln(stderr, "mesh-cli: stopped waiting; the line may still be running on the controller")
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -11,6 +12,13 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/cli"
|
||||
)
|
||||
|
||||
// The tests' own standard input is the runner's, which may be a pipe nobody closes: read as a terminal unless a test
|
||||
// says otherwise.
|
||||
func init() { standardInput = func() (io.Reader, bool) { return nil, true } }
|
||||
|
||||
// lastStdin is the standard input the engine was last given.
|
||||
var lastStdin []byte
|
||||
|
||||
// engine answers one connection on a socket of the test's own as the node-engine would, and says what it was asked.
|
||||
func engine(t *testing.T, answer cli.Answer) <-chan []string {
|
||||
t.Helper()
|
||||
@@ -24,6 +32,7 @@ func engine(t *testing.T, answer cli.Answer) <-chan []string {
|
||||
socket = path
|
||||
t.Cleanup(func() { socket = before })
|
||||
asked := make(chan []string, 1)
|
||||
lastStdin = nil
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
@@ -34,6 +43,7 @@ func engine(t *testing.T, answer cli.Answer) <-chan []string {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lastStdin = req.Stdin
|
||||
asked <- req.Line
|
||||
_ = cli.WriteAnswer(conn, answer)
|
||||
}()
|
||||
@@ -104,3 +114,42 @@ func TestAnOutcomeNotKnownIsSaid(t *testing.T) {
|
||||
t.Fatalf("exit %d, said %q", code, errOut.String())
|
||||
}
|
||||
}
|
||||
|
||||
// A pipe's or a file's standard input goes with the line, whole; a terminal's is never read; more than the bound is
|
||||
// refused and nothing is asked (novox/hq ADR 0259 §10).
|
||||
func TestStandardInputGoesWithTheLineAndATerminalIsNeverRead(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "")
|
||||
was := standardInput
|
||||
t.Cleanup(func() { standardInput = was })
|
||||
secret := "123456789:the-bot-token\n"
|
||||
|
||||
standardInput = func() (io.Reader, bool) { return strings.NewReader(secret), false }
|
||||
asked := engine(t, cli.Answer{Terminal: true})
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"},
|
||||
&out, &errOut); code != 0 {
|
||||
t.Fatalf("exit %d: %s", code, errOut.String())
|
||||
}
|
||||
<-asked
|
||||
if string(lastStdin) != secret {
|
||||
t.Fatalf("the engine was given %q", lastStdin)
|
||||
}
|
||||
if strings.Contains(out.String()+errOut.String(), "bot-token") {
|
||||
t.Fatal("mesh-cli printed what it carried")
|
||||
}
|
||||
|
||||
standardInput = func() (io.Reader, bool) { return strings.NewReader("typed"), true }
|
||||
asked = engine(t, cli.Answer{})
|
||||
_ = run(context.Background(), []string{"status"}, &out, &errOut)
|
||||
<-asked
|
||||
if len(lastStdin) != 0 {
|
||||
t.Fatalf("a terminal's input was read: %q", lastStdin)
|
||||
}
|
||||
|
||||
standardInput = func() (io.Reader, bool) { return bytes.NewReader(make([]byte, cli.MaxStdin+1)), false }
|
||||
errOut.Reset()
|
||||
if code := run(context.Background(), []string{"secret", "accept", "a", "b", "c", "--from", "-"}, &out, &errOut); code == 0 ||
|
||||
!strings.Contains(errOut.String(), "nothing was asked") {
|
||||
t.Fatalf("more than the bound was asked: %d %s", code, errOut.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ module mesh-cli
|
||||
|
||||
go 1.22
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.9
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.9 h1:2zq9tbwoXEYi+dDBxPoeNmn39KdRRKRp8CK33F4LWd0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.9/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970 h1:9tFDQsgmI+4X7/BpZGXIr+HemPKE7YddYGqWV0lINAI=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
|
||||
Reference in New Issue
Block a user