v1.0.0 — first release

Bump VERSION to 1.0.0 and cut the first tagged release of vpp-maglev.

Also in this commit:

- maglevc: MAGLEV_SERVER env var as an alternative to the --server
  flag, matching the MAGLEV_CONFIG / MAGLEV_GRPC_ADDR convention on
  the other binaries. The flag takes precedence when both are set.
- Rename cmd/maglevd -> cmd/server and cmd/maglevc -> cmd/client so
  the source directory names are decoupled from binary names (the
  frontend and tester commands already followed this convention).
  Build outputs and the Debian packages are unchanged.
This commit is contained in:
2026-04-15 15:23:46 +02:00
parent 177d81cca1
commit bc6ccaa844
15 changed files with 33 additions and 17 deletions

174
cmd/client/watch.go Normal file
View File

@@ -0,0 +1,174 @@
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package main
import (
"context"
"fmt"
"os"
"os/signal"
"strconv"
"syscall"
"golang.org/x/sys/unix"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
)
func dynWatchEventOpts(_ context.Context, _ grpcapi.MaglevClient) []string {
return []string{"num", "log", "backend", "frontend"}
}
// runWatchEvents implements 'watch events [num <n>] [log [level <level>]] [backend] [frontend]'.
// All tokens after 'events' are captured as args by the circular slot node in the tree.
// If none of log/backend/frontend are mentioned, all three default to true.
func runWatchEvents(ctx context.Context, client grpcapi.MaglevClient, args []string) error {
var maxEvents int // 0 = unlimited
var wantLog, wantBackend, wantFrontend bool
logLevel := ""
anyExplicit := false
for i := 0; i < len(args); {
switch args[i] {
case "num":
if i+1 >= len(args) {
return fmt.Errorf("num requires a count argument")
}
n, err := strconv.Atoi(args[i+1])
if err != nil || n < 1 {
return fmt.Errorf("num: invalid count %q", args[i+1])
}
maxEvents = n
i += 2
case "log":
wantLog = true
anyExplicit = true
if i+1 < len(args) && args[i+1] == "level" {
if i+2 >= len(args) {
return fmt.Errorf("log level requires a level argument")
}
logLevel = args[i+2]
i += 3
} else {
i++
}
case "backend":
wantBackend = true
anyExplicit = true
i++
case "frontend":
wantFrontend = true
anyExplicit = true
i++
default:
return fmt.Errorf("unknown watch option %q; expected: num, log, backend, frontend", args[i])
}
}
if !anyExplicit {
wantLog, wantBackend, wantFrontend = true, true, true
}
boolp := func(b bool) *bool { v := b; return &v }
req := &grpcapi.WatchRequest{
Log: boolp(wantLog),
LogLevel: logLevel,
Backend: boolp(wantBackend),
Frontend: boolp(wantFrontend),
}
// Cancel the stream on keypress or signal.
watchCtx, cancel := context.WithCancel(ctx)
defer cancel()
go watchStopOnKeypress(watchCtx, cancel)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigCh)
go func() {
select {
case <-sigCh:
cancel()
case <-watchCtx.Done():
}
}()
stream, err := client.WatchEvents(watchCtx, req)
if err != nil {
return err
}
marshaler := protojson.MarshalOptions{}
count := 0
for {
ev, err := stream.Recv()
if err != nil {
if watchCtx.Err() != nil {
return nil // stopped by keypress or signal
}
if st, ok := status.FromError(err); ok && st.Code() == codes.Canceled {
return nil
}
return err
}
data, err := marshaler.Marshal(ev)
if err != nil {
return fmt.Errorf("marshal event: %w", err)
}
fmt.Printf("%s\n", data)
count++
if maxEvents > 0 && count >= maxEvents {
return nil
}
}
}
// watchStopOnKeypress puts stdin into cbreak mode (when it is a terminal) and
// calls cancel when any byte arrives. Cbreak mode disables canonical (line)
// input so a single keypress is sufficient, while preserving output
// post-processing (OPOST/ONLCR) so that fmt.Printf("\n") still produces the
// expected carriage-return+newline on screen. Falls back gracefully when stdin
// is not a tty. The goroutine exits when ctx is cancelled.
func watchStopOnKeypress(ctx context.Context, cancel context.CancelFunc) {
fd := int(os.Stdin.Fd())
if old, err := stdinCbreak(fd); err == nil {
defer unix.IoctlSetTermios(fd, unix.TCSETSF, old) //nolint:errcheck
}
readDone := make(chan struct{})
go func() {
defer close(readDone)
buf := make([]byte, 1)
os.Stdin.Read(buf) //nolint:errcheck
}()
select {
case <-readDone:
cancel()
case <-ctx.Done():
}
}
// stdinCbreak sets the terminal referred to by fd into cbreak mode: canonical
// input and echo are disabled (so single keystrokes are immediately available)
// but output post-processing is left untouched (so \n still maps to \r\n).
// Returns the previous termios so the caller can restore it, or an error if fd
// is not a terminal.
func stdinCbreak(fd int) (*unix.Termios, error) {
old, err := unix.IoctlGetTermios(fd, unix.TCGETS)
if err != nil {
return nil, err // not a terminal
}
t := *old
t.Lflag &^= unix.ICANON | unix.ECHO | unix.ECHOE | unix.ECHOK | unix.ECHONL
t.Cc[unix.VMIN] = 1
t.Cc[unix.VTIME] = 0
if err := unix.IoctlSetTermios(fd, unix.TCSETS, &t); err != nil {
return nil, err
}
return old, nil
}