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.
142 lines
4.6 KiB
142 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:
|
|
}
|
|
}
|
|
}
|
|
|