// Local RPC package.
//
// This is a simple RPC package that uses a line-oriented protocol for
// encoding and decoding, and Unix sockets for transport. It is meant to be
// used for lightweight occasional communication between processes on the
// same machine.
package localrpc
import (
"errors"
"net"
"net/textproto"
"net/url"
"os"
"strings"
"time"
"blitiri.com.ar/go/chasquid/internal/trace"
)
// Handler is the type of RPC request handlers.
type Handler func(tr *trace.Trace, input url.Values) (url.Values, error)
//
// Server
//
// Server represents the RPC server.
type Server struct {
handlers map[string]Handler
lis net.Listener
}
// NewServer creates a new local RPC server.
func NewServer() *Server {
return &Server{
handlers: make(map[string]Handler),
}
}
var errUnknownMethod = errors.New("unknown method")
// Register a handler for the given name.
func (s *Server) Register(name string, handler Handler) {
s.handlers[name] = handler
}
// ListenAndServe starts the server.
func (s *Server) ListenAndServe(path string) error {
tr := trace.New("LocalRPC.Server", path)
defer tr.Finish()
// Previous instances of the server may have shut down uncleanly, leaving
// behind the socket file. Remove it just in case.
os.Remove(path)
var err error
s.lis, err = net.Listen("unix", path)
if err != nil {
return err
}
tr.Printf("Listening")
for {
conn, err := s.lis.Accept()
if err != nil {
tr.Errorf("Accept error: %v", err)
return err
}
go s.handleConn(tr, conn)
}
}
// Close stops the server.
func (s *Server) Close() error {
return s.lis.Close()
}
func (s *Server) handleConn(tr *trace.Trace, conn net.Conn) {
tr = tr.NewChild("LocalRPC.Handle", conn.RemoteAddr().String())
defer tr.Finish()
// Set a generous deadline to prevent client issues from tying up a server
// goroutine indefinitely.
conn.SetDeadline(time.Now().Add(5 * time.Second))
tconn := textproto.NewConn(conn)
defer tconn.Close()
// Read the request.
name, inS, err := readRequest(&tconn.Reader)
if err != nil {
tr.Debugf("error reading request: %v", err)
return
}
tr.Debugf("<- %s %s", name, inS)
// Find the handler.
handler, ok := s.handlers[name]
if !ok {
writeError(tr, tconn, errUnknownMethod)
return
}
// Unmarshal the input.
inV, err := url.ParseQuery(inS)
if err != nil {
writeError(tr, tconn, err)
return
}
// Call the handler.
outV, err := handler(tr, inV)
if err != nil {
writeError(tr, tconn, err)
return
}
// Send the response.
outS := outV.Encode()
tr.Debugf("-> 200 %s", outS)
tconn.PrintfLine("200 %s", outS)
}
func readRequest(r *textproto.Reader) (string, string, error) {
line, err := r.ReadLine()
if err != nil {
return "", "", err
}
sp := strings.SplitN(line, " ", 2)
if len(sp) == 1 {
return sp[0], "", nil
}
return sp[0], sp[1], nil
}
func writeError(tr *trace.Trace, tconn *textproto.Conn, err error) {
tr.Errorf("-> 500 %s", err.Error())
tconn.PrintfLine("500 %s", err.Error())
}
// Default server. This is a singleton server that can be used for
// convenience.
var DefaultServer = NewServer()
//
// Client
//
// Client for the localrpc server.
type Client struct {
path string
}
// NewClient creates a new client for the given path.
func NewClient(path string) *Client {
return &Client{path: path}
}
// CallWithValues calls the given method.
func (c *Client) CallWithValues(name string, input url.Values) (url.Values, error) {
conn, err := textproto.Dial("unix", c.path)
if err != nil {
return nil, err
}
defer conn.Close()
err = conn.PrintfLine("%s %s", name, input.Encode())
if err != nil {
return nil, err
}
code, msg, err := conn.ReadCodeLine(0)
if err != nil {
return nil, err
}
if code != 200 {
return nil, errors.New(msg)
}
return url.ParseQuery(msg)
}
// Call the given method. The arguments are key-value strings, and must be
// provided in pairs.
func (c *Client) Call(name string, args ...string) (url.Values, error) {
v := url.Values{}
for i := 0; i < len(args); i += 2 {
v.Set(args[i], args[i+1])
}
return c.CallWithValues(name, v)
}