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 }