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.

143 lines
4.6 KiB

// tera-serial-lease-check verifies that CH02-CH48 can acquire and release the
// production tera-seriald stream path. It sends no test payload and never opens
// a raw TTY directly.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"time"
"tera-clone/services/tera-api/internal/serial"
)
const (
firstCheckedChannel = 2
lastCheckedChannel = 48
)
type leaseController interface {
Open(context.Context, string) (io.ReadWriteCloser, error)
Status(context.Context) (serial.StreamSnapshot, error)
}
type channelResult struct {
Channel string `json:"channel"`
Status string `json:"status"`
OpenCloseMS int64 `json:"open_close_ms"`
ErrorMessage string `json:"error,omitempty"`
}
type report struct {
Kind string `json:"kind"`
Status string `json:"status"`
GeneratedAt time.Time `json:"generated_at"`
FirstChannel string `json:"first_channel"`
LastChannel string `json:"last_channel"`
ChannelsRequested int `json:"channels_requested"`
ChannelsPassed int `json:"channels_passed"`
ChannelsFailed int `json:"channels_failed"`
ActiveLeasesAfter int `json:"active_leases_after"`
IdleStatusError string `json:"idle_status_error,omitempty"`
TestPayloadSent bool `json:"test_payload_sent"`
DynamicDeviceScan bool `json:"dynamic_device_scan"`
FixedChannelList bool `json:"fixed_channel_list"`
Results []channelResult `json:"results"`
}
func main() {
controller := serial.NewStreamSocketClient(serial.DefaultStreamSocketPath, "tera-serial-lease-check")
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr, controller))
}
func run(args []string, stdout, stderr io.Writer, controller leaseController) int {
flags := flag.NewFlagSet("tera-serial-lease-check", flag.ContinueOnError)
flags.SetOutput(stderr)
perChannelTimeout := flags.Duration("per-channel-timeout", 3*time.Second, "maximum time for one open/close")
confirmed := flags.Bool("operator-confirmed-ch02-ch48-open-close", false, "confirm a no-test-payload CH02-CH48 open/close check")
if err := flags.Parse(args); err != nil {
return 2
}
if flags.NArg() != 0 || !*confirmed || *perChannelTimeout <= 0 || *perChannelTimeout > 10*time.Second {
fmt.Fprintln(stderr, "require explicit CH02-CH48 open/close confirmation and a timeout up to 10s")
return 2
}
if controller == nil {
fmt.Fprintln(stderr, "serial lease controller is required")
return 2
}
result := report{
Kind: "tera-serial-lease-check", Status: "pass", GeneratedAt: time.Now().UTC(),
FirstChannel: "CH02", LastChannel: "CH48",
ChannelsRequested: lastCheckedChannel - firstCheckedChannel + 1,
TestPayloadSent: false, DynamicDeviceScan: false, FixedChannelList: true,
Results: make([]channelResult, 0, lastCheckedChannel-firstCheckedChannel+1),
}
for number := firstCheckedChannel; number <= lastCheckedChannel; number++ {
channel := fmt.Sprintf("CH%02d", number)
started := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), *perChannelTimeout)
stream, err := controller.Open(ctx, channel)
if err == nil {
err = stream.Close()
}
cancel()
entry := channelResult{Channel: channel, Status: "pass", OpenCloseMS: time.Since(started).Milliseconds()}
if err != nil {
entry.Status = "fail"
entry.ErrorMessage = err.Error()
result.ChannelsFailed++
} else {
result.ChannelsPassed++
}
result.Results = append(result.Results, entry)
}
statusContext, cancelStatus := context.WithTimeout(context.Background(), 3*time.Second)
status, statusErr := waitForIdle(statusContext, controller)
cancelStatus()
result.ActiveLeasesAfter = status.ActiveLeases
if statusErr != nil {
result.Status = "fail"
result.IdleStatusError = statusErr.Error()
fmt.Fprintln(stderr, statusErr)
}
if result.ChannelsFailed != 0 || result.ChannelsPassed != result.ChannelsRequested {
result.Status = "fail"
}
if err := json.NewEncoder(stdout).Encode(result); err != nil {
fmt.Fprintln(stderr, err)
return 1
}
if result.Status != "pass" {
return 1
}
return 0
}
func waitForIdle(ctx context.Context, controller leaseController) (serial.StreamSnapshot, error) {
ticker := time.NewTicker(25 * time.Millisecond)
defer ticker.Stop()
var latest serial.StreamSnapshot
for {
status, err := controller.Status(ctx)
if err != nil {
return status, err
}
latest = status
if status.ActiveLeases == 0 {
return status, nil
}
select {
case <-ctx.Done():
return latest, fmt.Errorf("serial leases did not return to idle: %w", ctx.Err())
case <-ticker.C:
}
}
}