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
167 lines
4.5 KiB
|
2 months ago
|
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
|
||
|
|
}
|