You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

137 lines
3.9 KiB

// tera-serial-stream-check performs one bounded CH01/COM3 data-plane check.
// It never opens a raw TTY and requires an explicit operator confirmation.
package main
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"time"
"tera-clone/services/tera-api/internal/serial"
)
const maximumPayloadBytes = 64
type streamOpener interface {
Open(context.Context, string) (io.ReadWriteCloser, error)
}
type report struct {
Kind string `json:"kind"`
Status string `json:"status"`
Direction string `json:"direction"`
Channel string `json:"channel"`
Device string `json:"device"`
PCPort string `json:"pc_port"`
Bytes int `json:"bytes"`
SHA256 string `json:"sha256"`
DataSent bool `json:"data_sent"`
}
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr, serial.NewStreamSocketClient(serial.DefaultStreamSocketPath, "tera-serial-stream-check")))
}
func run(args []string, stdout, stderr io.Writer, opener streamOpener) int {
flags := flag.NewFlagSet("tera-serial-stream-check", flag.ContinueOnError)
flags.SetOutput(stderr)
direction := flags.String("direction", "", "tx or rx relative to the target")
payloadBase64 := flags.String("payload-base64", "", "bounded payload for tx")
byteCount := flags.Int("bytes", 0, "expected byte count for rx")
timeout := flags.Duration("timeout", 10*time.Second, "bounded stream timeout")
confirmed := flags.Bool("operator-confirmed-ch01-com3", false, "confirm CH01 /dev/ttyWCH4 is connected to PC COM3")
if err := flags.Parse(args); err != nil {
return 2
}
if flags.NArg() != 0 || !*confirmed || (*direction != "tx" && *direction != "rx") || *timeout <= 0 {
fmt.Fprintln(stderr, "require tx/rx, a positive timeout, and explicit CH01-COM3 confirmation")
return 2
}
var payload []byte
var err error
if *direction == "tx" {
payload, err = base64.StdEncoding.Strict().DecodeString(*payloadBase64)
if err != nil || len(payload) < 1 || len(payload) > maximumPayloadBytes || *byteCount != 0 {
fmt.Fprintln(stderr, "tx requires a strict Base64 payload of 1 to 64 bytes")
return 2
}
} else if *payloadBase64 != "" || *byteCount < 1 || *byteCount > maximumPayloadBytes {
fmt.Fprintln(stderr, "rx requires an expected byte count of 1 to 64")
return 2
}
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
stream, err := opener.Open(ctx, "CH01")
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
defer stream.Close()
if *direction == "tx" {
if err := writeAll(stream, payload); err != nil {
fmt.Fprintln(stderr, err)
return 1
}
} else {
payload, err = readExact(ctx, stream, *byteCount)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
}
digest := sha256.Sum256(payload)
result := report{
Kind: "tera-serial-stream-check", Status: "pass", Direction: *direction,
Channel: "CH01", Device: "/dev/ttyWCH4", PCPort: "COM3",
Bytes: len(payload), SHA256: hex.EncodeToString(digest[:]), DataSent: true,
}
if err := json.NewEncoder(stdout).Encode(result); err != nil {
fmt.Fprintln(stderr, err)
return 1
}
return 0
}
func writeAll(writer io.Writer, payload []byte) error {
for len(payload) > 0 {
written, err := writer.Write(payload)
if err != nil {
return err
}
if written <= 0 {
return io.ErrShortWrite
}
payload = payload[written:]
}
return nil
}
func readExact(ctx context.Context, stream io.ReadWriteCloser, count int) ([]byte, error) {
type outcome struct {
payload []byte
err error
}
done := make(chan outcome, 1)
go func() {
payload := make([]byte, count)
_, err := io.ReadFull(stream, payload)
done <- outcome{payload: payload, err: err}
}()
select {
case result := <-done:
return result.payload, result.err
case <-ctx.Done():
_ = stream.Close()
result := <-done
return result.payload, errors.Join(ctx.Err(), result.err)
}
}