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.
267 lines
8.4 KiB
267 lines
8.4 KiB
package serial
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
const DefaultSocketPath = "/run/tera-seriald/serial-ports.sock"
|
|
|
|
type socketRequest struct {
|
|
Action string `json:"action"`
|
|
Configs []ChannelConfig `json:"configs,omitempty"`
|
|
Config *ChannelConfig `json:"config,omitempty"`
|
|
Captures []CaptureConfig `json:"captures,omitempty"`
|
|
}
|
|
|
|
type socketResponse struct {
|
|
States []State `json:"states,omitempty"`
|
|
Links []LinkState `json:"links,omitempty"`
|
|
CRProbes []CRProbeState `json:"cr_probes,omitempty"`
|
|
Probe *ProbeResult `json:"probe,omitempty"`
|
|
Code string `json:"code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
const maxSocketRequestBytes = 16 * 1024
|
|
|
|
// SocketServer exposes the seriald-only controller on a local Unix socket.
|
|
// It accepts only bounded state and line-configuration requests, never data.
|
|
type SocketServer struct {
|
|
Controller Controller
|
|
Capture CaptureController
|
|
CRProbe CRProbeStateController
|
|
Links LinkStateController
|
|
}
|
|
|
|
func (s SocketServer) Serve(listener net.Listener) error {
|
|
if s.Controller == nil {
|
|
return errors.New("serial controller is required")
|
|
}
|
|
for {
|
|
connection, err := listener.Accept()
|
|
if err != nil {
|
|
if errors.Is(err, net.ErrClosed) {
|
|
return nil
|
|
}
|
|
if temporary, ok := err.(interface{ Temporary() bool }); ok && temporary.Temporary() {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
go s.handleConnection(connection)
|
|
}
|
|
}
|
|
|
|
func (s SocketServer) handleConnection(connection net.Conn) {
|
|
defer connection.Close()
|
|
_ = connection.SetDeadline(time.Now().Add(10 * time.Second))
|
|
payload, err := bufio.NewReader(io.LimitReader(connection, maxSocketRequestBytes+1)).ReadBytes('\n')
|
|
if err != nil || len(payload) > maxSocketRequestBytes || !bytes.HasSuffix(payload, []byte{'\n'}) {
|
|
s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "invalid serial control request"})
|
|
return
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
var command socketRequest
|
|
if err := decoder.Decode(&command); err != nil {
|
|
s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "invalid serial control request"})
|
|
return
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
|
s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "serial control request must contain a single JSON value"})
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
switch command.Action {
|
|
case "states":
|
|
states, err := s.Controller.States(ctx)
|
|
if err != nil {
|
|
s.writeError(connection, err)
|
|
return
|
|
}
|
|
s.writeResponse(connection, socketResponse{States: states})
|
|
case "link_states":
|
|
controller := s.Links
|
|
if controller == nil {
|
|
controller, _ = s.Controller.(LinkStateController)
|
|
}
|
|
if controller == nil {
|
|
s.writeResponse(connection, socketResponse{Code: "hardware_unavailable", Error: "serial modem status is unavailable"})
|
|
return
|
|
}
|
|
links, err := controller.LinkStates(ctx)
|
|
if err != nil {
|
|
s.writeError(connection, err)
|
|
return
|
|
}
|
|
s.writeResponse(connection, socketResponse{Links: links})
|
|
case "cr_probe_states":
|
|
if s.CRProbe == nil {
|
|
s.writeResponse(connection, socketResponse{Code: "hardware_unavailable", Error: "serial CR response probe is unavailable"})
|
|
return
|
|
}
|
|
states, err := s.CRProbe.CRProbeStates(ctx)
|
|
if err != nil {
|
|
s.writeError(connection, err)
|
|
return
|
|
}
|
|
s.writeResponse(connection, socketResponse{CRProbes: states})
|
|
case "apply":
|
|
states, err := s.Controller.Apply(ctx, command.Configs)
|
|
if err != nil {
|
|
s.writeError(connection, err)
|
|
return
|
|
}
|
|
s.writeResponse(connection, socketResponse{States: states})
|
|
case "apply_capture":
|
|
if s.Capture == nil {
|
|
s.writeResponse(connection, socketResponse{Code: "hardware_unavailable", Error: "serial payload capture control is unavailable"})
|
|
return
|
|
}
|
|
if err := s.Capture.ApplyCapture(ctx, command.Captures); err != nil {
|
|
s.writeError(connection, err)
|
|
return
|
|
}
|
|
s.writeResponse(connection, socketResponse{})
|
|
case "probe":
|
|
if command.Config == nil {
|
|
s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "serial line probe requires one config"})
|
|
return
|
|
}
|
|
prober, ok := s.Controller.(interface {
|
|
Probe(context.Context, ChannelConfig) (ProbeResult, error)
|
|
})
|
|
if !ok {
|
|
s.writeResponse(connection, socketResponse{Code: "hardware_unavailable", Error: "serial line probe is unavailable"})
|
|
return
|
|
}
|
|
result, err := prober.Probe(ctx, *command.Config)
|
|
if err != nil {
|
|
s.writeError(connection, err)
|
|
return
|
|
}
|
|
s.writeResponse(connection, socketResponse{Probe: &result})
|
|
default:
|
|
s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "serial control action is not supported"})
|
|
}
|
|
}
|
|
|
|
func (s SocketServer) writeError(connection net.Conn, err error) {
|
|
code := "hardware_failed"
|
|
if errors.Is(err, ErrStreamLeaseBusy) {
|
|
code = "stream_lease_busy"
|
|
} else if IsUnavailable(err) {
|
|
code = "hardware_unavailable"
|
|
}
|
|
s.writeResponse(connection, socketResponse{Code: code, Error: err.Error()})
|
|
}
|
|
|
|
func (s SocketServer) writeResponse(connection net.Conn, value socketResponse) {
|
|
_ = json.NewEncoder(connection).Encode(value)
|
|
}
|
|
|
|
// SocketClient implements Controller for the unprivileged tera-api process.
|
|
// Unix-socket permissions are the boundary between the API and seriald.
|
|
type SocketClient struct {
|
|
path string
|
|
timeout time.Duration
|
|
}
|
|
|
|
func NewSocketClient(path string) *SocketClient {
|
|
if path == "" {
|
|
path = DefaultSocketPath
|
|
}
|
|
return &SocketClient{path: path, timeout: 10 * time.Second}
|
|
}
|
|
|
|
func (c *SocketClient) States(ctx context.Context) ([]State, error) {
|
|
response, err := c.exchange(ctx, socketRequest{Action: "states"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return response.States, nil
|
|
}
|
|
|
|
func (c *SocketClient) LinkStates(ctx context.Context) ([]LinkState, error) {
|
|
response, err := c.exchange(ctx, socketRequest{Action: "link_states"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return response.Links, nil
|
|
}
|
|
|
|
func (c *SocketClient) CRProbeStates(ctx context.Context) ([]CRProbeState, error) {
|
|
response, err := c.exchange(ctx, socketRequest{Action: "cr_probe_states"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return response.CRProbes, nil
|
|
}
|
|
|
|
func (c *SocketClient) Apply(ctx context.Context, configs []ChannelConfig) ([]State, error) {
|
|
response, err := c.exchange(ctx, socketRequest{Action: "apply", Configs: configs})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return response.States, nil
|
|
}
|
|
|
|
func (c *SocketClient) ApplyCapture(ctx context.Context, configs []CaptureConfig) error {
|
|
_, err := c.exchange(ctx, socketRequest{Action: "apply_capture", Captures: configs})
|
|
return err
|
|
}
|
|
|
|
func (c *SocketClient) Probe(ctx context.Context, config ChannelConfig) (ProbeResult, error) {
|
|
response, err := c.exchange(ctx, socketRequest{Action: "probe", Config: &config})
|
|
if err != nil {
|
|
return ProbeResult{}, err
|
|
}
|
|
if response.Probe == nil {
|
|
return ProbeResult{}, errors.New("tera-seriald returned no probe result")
|
|
}
|
|
return *response.Probe, nil
|
|
}
|
|
|
|
func (c *SocketClient) exchange(ctx context.Context, command socketRequest) (socketResponse, error) {
|
|
dialer := net.Dialer{Timeout: c.timeout}
|
|
connection, err := dialer.DialContext(ctx, "unix", c.path)
|
|
if err != nil {
|
|
return socketResponse{}, &UnavailableError{Reason: "tera-seriald is not reachable"}
|
|
}
|
|
defer connection.Close()
|
|
deadline := time.Now().Add(c.timeout)
|
|
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
|
|
deadline = contextDeadline
|
|
}
|
|
_ = connection.SetDeadline(deadline)
|
|
if err := json.NewEncoder(connection).Encode(command); err != nil {
|
|
return socketResponse{}, fmt.Errorf("serial control request failed")
|
|
}
|
|
var response socketResponse
|
|
if err := json.NewDecoder(io.LimitReader(connection, 65536)).Decode(&response); err != nil {
|
|
return socketResponse{}, &UnavailableError{Reason: "tera-seriald returned no response"}
|
|
}
|
|
if response.Error != "" {
|
|
if response.Code == "hardware_unavailable" {
|
|
return socketResponse{}, &UnavailableError{Reason: response.Error}
|
|
}
|
|
// The exact-message fallback keeps a newer API compatible with a
|
|
// seriald that predates the stream_lease_busy protocol code.
|
|
if response.Code == "stream_lease_busy" || response.Error == ErrStreamLeaseBusy.Error() {
|
|
return socketResponse{}, ErrStreamLeaseBusy
|
|
}
|
|
return socketResponse{}, errors.New(response.Error)
|
|
}
|
|
return response, nil
|
|
}
|
|
|