Files
vpp-maglev/cmd/maglevc/shell.go
Pim van Pelt 74448cf6d0 Guard pause/resume against disabled backends; clean up CLI errors
- PauseBackend and ResumeBackend return an error (not bool) when the
  backend is disabled, preventing an inconsistent state where the
  health state says "paused" but enabled=false.
- DisableBackend and EnableBackend now log uniform backend-transition
  lines with from/to instead of separate backend-disable/backend-enable
  messages.
- CLI errors strip gRPC boilerplate ("rpc error: code = ... desc = ")
  and display the server message in red (when color is enabled). Both
  the interactive shell and one-shot mode use the same formatError path.
2026-04-11 21:19:10 +02:00

104 lines
2.1 KiB
Go

// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package main
import (
"context"
"errors"
"fmt"
"io"
"strings"
"github.com/chzyer/readline"
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
)
// errQuit is a sentinel returned by runQuit to exit the REPL.
var errQuit = errors.New("quit")
// runShell runs the interactive REPL until the user types quit/exit or EOF.
func runShell(ctx context.Context, client grpcapi.MaglevClient) error {
root := buildTree()
comp := &Completer{root: root, client: client}
ql := &questionListener{root: root, client: client}
cfg := &readline.Config{
Prompt: "maglev> ",
AutoComplete: comp,
InterruptPrompt: "^C",
EOFPrompt: "exit",
Listener: ql,
}
rl, err := readline.NewEx(cfg)
if err != nil {
return fmt.Errorf("readline init: %w", err)
}
ql.rl = rl
defer rl.Close()
for {
line, err := rl.Readline()
if err == readline.ErrInterrupt {
continue
}
if err == io.EOF {
return nil
}
if err != nil {
return err
}
tokens := splitTokens(line)
if len(tokens) == 0 {
continue
}
if err := dispatch(ctx, root, client, tokens); err != nil {
if errors.Is(err, errQuit) {
return nil
}
fmt.Fprintf(rl.Stderr(), "%s\n", formatError(err))
}
}
}
// dispatch walks the tree and executes the matched command.
func dispatch(ctx context.Context, root *Node, client grpcapi.MaglevClient, tokens []string) error {
node, args := Walk(root, tokens)
if node.Run == nil {
showHelp(root, tokens)
return nil
}
return node.Run(ctx, client, args)
}
// showHelp prints all reachable commands from the given token path, birdc-style.
func showHelp(root *Node, tokens []string) {
node, _ := Walk(root, tokens)
prefix := strings.Join(tokens, " ")
lines := expandPaths(node, prefix, make(map[*Node]bool))
maxLen := 0
for _, l := range lines {
if len(l.path) > maxLen {
maxLen = len(l.path)
}
}
if len(lines) == 0 {
fmt.Println(" <no completions>")
return
}
for _, l := range lines {
if l.help != "" {
fmt.Printf("%-*s %s\n", maxLen+2, l.path, l.help)
} else {
fmt.Printf("%s\n", l.path)
}
}
}