Documentation
¶
Overview ¶
Package udsrpc is a tiny newline-delimited JSON-RPC implementation designed for daemon ↔ client communication over Unix domain sockets.
It sits between net/rpc (too rigid, gob-only) and full gRPC (too heavy). Messages are JSON objects separated by newlines; the wire format is language-agnostic, debuggable by hand, and trivial to bridge to any other runtime.
Three message shapes share the wire:
- Request: {"id": N, "method": "...", "params": {...}}
- Response: {"id": N, "result": {...}} or {"id": N, "error": {...}}
- Event: {"type": "...", "data": {...}} (server → client push)
DecodeMessage discriminates between the three by inspecting the keys present in the raw JSON: "type" → Event, "method" → Request, else Response.
Index ¶
- Constants
- func EnsureRuntimeDir(appName string) error
- func HandleSignals(onShutdown, onReload func()) (stop func())
- func IsRunning(path string) (int, bool)
- func PIDPath(appName string) string
- func ReadPID(path string) (int, error)
- func RemovePID(path string) error
- func RuntimeDir(appName string) string
- func SocketPath(appName string) string
- func WritePID(path string) error
- type Conn
- func (c *Conn) Close() error
- func (c *Conn) LocalAddr() net.Addr
- func (c *Conn) ReceiveMessage() (Message, error)
- func (c *Conn) RemoteAddr() net.Addr
- func (c *Conn) Send(v interface{}) error
- func (c *Conn) SendError(id uint64, code int, message string) error
- func (c *Conn) SendEvent(eventType string, data interface{}) error
- func (c *Conn) SendResponse(id uint64, result interface{}) error
- type Error
- type Event
- type HandlerFunc
- type Message
- type Request
- type Response
- type Server
- func (s *Server) Broadcast(eventType string, data any)
- func (s *Server) BroadcastFunc(eventType string, data any, predicate func(*Conn) bool)
- func (s *Server) Clients() []*Conn
- func (s *Server) Handle(method string, fn HandlerFunc)
- func (s *Server) OnConnect(fn func(*Conn))
- func (s *Server) OnDisconnect(fn func(*Conn))
- func (s *Server) Serve(ctx context.Context, l net.Listener) error
Constants ¶
const ( // ErrCodeParse — invalid JSON was received. ErrCodeParse = -32700 // ErrCodeInvalidReq — request was not a valid Request object. ErrCodeInvalidReq = -32600 // ErrCodeNotFound — method does not exist or is not registered. ErrCodeNotFound = -32601 // ErrCodeInvalidParams — method exists but params are invalid. ErrCodeInvalidParams = -32602 // ErrCodeInternal — internal server error. ErrCodeInternal = -32603 )
Standard error codes, borrowed from JSON-RPC 2.0 so existing tooling and client libraries recognize them.
Variables ¶
This section is empty.
Functions ¶
func EnsureRuntimeDir ¶
EnsureRuntimeDir creates the runtime directory if it doesn't exist, with owner-only permissions (0700).
func HandleSignals ¶
func HandleSignals(onShutdown, onReload func()) (stop func())
HandleSignals installs a SIGTERM/SIGINT/SIGHUP handler in a new goroutine and returns a stop function that cancels the handler.
- SIGTERM, SIGINT → onShutdown is called once, the handler returns.
- SIGHUP → onReload is called and the handler keeps running.
Either callback may be nil. Calling the returned stop function detaches the signal handler — it does NOT invoke onShutdown.
func IsRunning ¶
IsRunning returns (pid, true) if a process with the PID stored at path is currently alive, else (pid, false). A signal-0 probe is used on Unix; the call is non-destructive.
func PIDPath ¶
PIDPath returns the conventional PID file path for an app: <RuntimeDir>/<appName>.pid.
func RuntimeDir ¶
RuntimeDir returns the OS-appropriate directory for an app's runtime state (socket, PID file). The directory is per-user and per-app.
- Linux: $XDG_RUNTIME_DIR/<appName>/ (fallback: /tmp/<appName>-<uid>/)
- macOS: ~/Library/Caches/<appName>/
- other: os.TempDir()/<appName>-<uid>/
appName should be a short lowercase identifier — typically the daemon binary name.
func SocketPath ¶
SocketPath returns the conventional Unix socket path for an app: <RuntimeDir>/<appName>.sock.
Types ¶
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn wraps a net.Conn with newline-delimited JSON encoding/decoding. Send is serialized by an internal mutex so concurrent goroutines can push to the same connection without interleaving bytes; ReceiveMessage is not safe for concurrent use and is intended to be driven by a single reader goroutine.
func (*Conn) ReceiveMessage ¶
ReceiveMessage reads and decodes the next JSON message, returning a discriminated Message. Blocks until a full JSON object is read or the underlying connection closes.
func (*Conn) RemoteAddr ¶
RemoteAddr returns the remote address of the underlying connection.
func (*Conn) Send ¶
Send writes a JSON-encoded value followed by a newline. Safe to call from multiple goroutines.
func (*Conn) SendResponse ¶
SendResponse sends a successful response with the given result. The result is marshaled with encoding/json.
type Event ¶
type Event struct {
Type string `json:"type"`
Data json.RawMessage `json:"data,omitempty"`
}
Event is a server-pushed message with no Request. It has no ID and is not acknowledged.
type HandlerFunc ¶
HandlerFunc handles a single Request. The returned value is marshaled as the Response.Result. To return a custom error code, return an *Error (the server forwards Code and Message verbatim). Any other non-nil error is sent as ErrCodeInternal with err.Error() as the message.
type Message ¶
Message is a discriminated union for wire decoding. Exactly one of Request, Response, or Event is non-nil after DecodeMessage succeeds.
func DecodeMessage ¶
func DecodeMessage(raw json.RawMessage) (Message, error)
DecodeMessage inspects raw and returns a discriminated Message.
Discriminator order: "type" present → Event, "method" present → Request, else → Response. This matches how peers should produce messages — never include both "type" and "method", never set "id" on Events.
type Request ¶
type Request struct {
ID uint64 `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
Request from client to server. The ID is echoed in the matching Response so callers can correlate concurrent in-flight requests.
type Response ¶
type Response struct {
ID uint64 `json:"id"`
Result json.RawMessage `json:"result,omitempty"`
Error *Error `json:"error,omitempty"`
}
Response from server to client. Either Result or Error is populated, never both.
type Server ¶
type Server struct {
// Logger receives accept errors and recovered panics. Defaults to
// log.Default(); set to a no-op logger to silence the server.
Logger *log.Logger
// contains filtered or unexported fields
}
Server multiplexes incoming Requests on accepted connections to registered HandlerFuncs and tracks all live clients so events can be broadcast.
A zero-value Server is not usable — call NewServer.
func (*Server) Broadcast ¶
Broadcast sends an Event to every connected client. Errors per connection are logged but do not abort the broadcast.
func (*Server) BroadcastFunc ¶
BroadcastFunc sends an Event to every connected client for which predicate returns true. A nil predicate matches all clients.
func (*Server) Clients ¶
Clients returns a snapshot slice of all currently-connected clients. Safe to call concurrently with Serve.
func (*Server) Handle ¶
func (s *Server) Handle(method string, fn HandlerFunc)
Handle registers fn as the handler for method. Calling Handle with the same method twice replaces the previous handler.
func (*Server) OnConnect ¶
OnConnect installs a hook called once per accepted connection, before any messages are read. The hook runs synchronously on the accept goroutine; expensive work should be dispatched elsewhere.
func (*Server) OnDisconnect ¶
OnDisconnect installs a hook called once per connection when it closes (cleanly or after error).