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.
138 lines
3.9 KiB
138 lines
3.9 KiB
|
2 months ago
|
// 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)
|
||
|
|
}
|
||
|
|
}
|