package hooks
import (
"context"
"fmt"
"net"
"time"
)
const (
// DefaultDialTimeout bounds finding the daemon. A unix socket connect is
// immediate when the daemon is listening, so this is generous enough to
// survive a loaded box and short enough that a push against a dead daemon
// fails while the human is still looking at the terminal.
DefaultDialTimeout = 5 * time.Second
// DefaultTimeout bounds one call end to end. Validation walks the pushed
// tree and asks Postgres about every document id in it, so it is not
// instantaneous; but a hook that hangs holds the push open indefinitely,
// and a rejected push is the better failure.
DefaultTimeout = 60 * time.Second
)
// Client is a hook's end of the RPC: one connection, one request, one
// response, no reuse. Pushes are rare and serial, so a pool would be state to
// get wrong for no gain.
type Client struct {
// Socket is the daemon's unix socket.
Socket string
// DialTimeout and Timeout default to the constants above when zero.
DialTimeout time.Duration
Timeout time.Duration
}
// Call sends one request and returns the daemon's answer.
//
// Every failure here — cannot connect, cannot write, cannot parse — is
// returned as an error, and every caller on the rejecting path turns it into a
// rejection. That is the fail-closed rule: the daemon not answering is never
// permission to proceed.
func (c Client) Call(ctx context.Context, req Request) (Response, error) {
if c.Socket == "" {
return Response{}, fmt.Errorf("hooks: no daemon socket to call")
}
timeout := c.Timeout
if timeout <= 0 {
timeout = DefaultTimeout
}
dialTimeout := c.DialTimeout
if dialTimeout <= 0 {
dialTimeout = DefaultDialTimeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
dialer := net.Dialer{Timeout: dialTimeout}
conn, err := dialer.DialContext(ctx, "unix", c.Socket)
if err != nil {
return Response{}, fmt.Errorf("hooks: reach the spec.sr.ht daemon on %s: %w", c.Socket, err)
}
defer conn.Close()
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return Response{}, fmt.Errorf("hooks: set deadline on %s: %w", c.Socket, err)
}
}
if err := WriteRequest(conn, req); err != nil {
return Response{}, fmt.Errorf("hooks: send %s to %s: %w", req.Method, c.Socket, err)
}
// Half-close so a daemon that reads to EOF is not left waiting. The
// response still arrives on the read half.
if uc, ok := conn.(*net.UnixConn); ok {
if err := uc.CloseWrite(); err != nil {
return Response{}, fmt.Errorf("hooks: finish sending %s to %s: %w", req.Method, c.Socket, err)
}
}
resp, err := ReadResponse(conn)
if err != nil {
return Response{}, fmt.Errorf("hooks: read the answer to %s from %s: %w", req.Method, c.Socket, err)
}
if err := resp.Validate(); err != nil {
return Response{}, fmt.Errorf("hooks: %w", err)
}
return resp, nil
}