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.

167 lines
4.5 KiB

package systemcontrol
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"io"
"log"
"net"
"sync"
"time"
)
const DefaultSocketPath = "/run/tera-hwd/system-control.sock"
type request struct {
Action string `json:"action"`
}
type response struct {
State string `json:"state,omitempty"`
Pressed *bool `json:"pressed,omitempty"`
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
}
type Server struct {
Controller Controller
ResetButton ResetButtonReader
RebootDelay time.Duration
rebootOnce sync.Once
}
func (s *Server) Serve(listener net.Listener) error {
if s == nil || s.Controller == nil {
return errors.New("system controller is required")
}
for {
connection, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return nil
}
continue
}
go s.handle(connection)
}
}
func (s *Server) handle(connection net.Conn) {
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(5 * time.Second))
payload, err := bufio.NewReader(io.LimitReader(connection, 4097)).ReadBytes('\n')
if err != nil && !errors.Is(err, io.EOF) || len(payload) == 0 || len(payload) > 4096 || payload[len(payload)-1] != '\n' {
s.write(connection, response{Code: "validation_failed", Error: "invalid system control request"})
return
}
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.DisallowUnknownFields()
var command request
if err := decoder.Decode(&command); err != nil {
s.write(connection, response{Code: "validation_failed", Error: "invalid system control request"})
return
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
s.write(connection, response{Code: "validation_failed", Error: "system control request must contain one JSON value"})
return
}
if command.Action == "reset_button_state" {
if s.ResetButton == nil {
s.write(connection, response{Code: "unavailable", Error: "reset button is not configured"})
return
}
pressed, err := s.ResetButton.ResetButtonPressed(context.Background())
if err != nil {
s.write(connection, response{Code: "unavailable", Error: err.Error()})
return
}
s.write(connection, response{State: "ready", Pressed: &pressed})
return
}
if command.Action != "reboot" {
s.write(connection, response{Code: "validation_failed", Error: "system control action is not supported"})
return
}
delay := s.RebootDelay
if delay <= 0 {
delay = 2 * time.Second
}
s.write(connection, response{State: "scheduled"})
s.rebootOnce.Do(func() {
time.AfterFunc(delay, func() {
if err := s.Controller.Reboot(context.Background()); err != nil {
log.Printf("tera-hwd scheduled reboot failed: %v", err)
}
})
})
}
func (*Server) write(connection net.Conn, value response) {
_ = json.NewEncoder(connection).Encode(value)
}
type Client struct {
path string
}
func NewClient(path string) *Client {
if path == "" {
path = DefaultSocketPath
}
return &Client{path: path}
}
func (c *Client) Reboot(ctx context.Context) error {
result, err := c.request(ctx, "reboot")
if err != nil {
return err
}
if result.State != "scheduled" {
return errors.New("system reboot was not scheduled")
}
return nil
}
func (c *Client) ResetButtonPressed(ctx context.Context) (bool, error) {
result, err := c.request(ctx, "reset_button_state")
if err != nil {
return false, err
}
if result.State != "ready" || result.Pressed == nil {
return false, errors.New("reset button state is unavailable")
}
return *result.Pressed, nil
}
func (c *Client) request(ctx context.Context, action string) (response, error) {
dialer := net.Dialer{Timeout: 3 * time.Second}
connection, err := dialer.DialContext(ctx, "unix", c.path)
if err != nil {
return response{}, errors.New("privileged system control is not reachable")
}
defer connection.Close()
deadline := time.Now().Add(5 * time.Second)
if value, ok := ctx.Deadline(); ok && value.Before(deadline) {
deadline = value
}
_ = connection.SetDeadline(deadline)
if err := json.NewEncoder(connection).Encode(request{Action: action}); err != nil {
return response{}, errors.New("system control request could not be sent")
}
var result response
if err := json.NewDecoder(io.LimitReader(connection, 4096)).Decode(&result); err != nil {
return response{}, errors.New("privileged system control returned no response")
}
if result.Code != "" {
if result.Error != "" {
return response{}, errors.New(result.Error)
}
return response{}, errors.New("system control request failed")
}
return result, nil
}