This patch fixes a variety of typos in documentation and comments, found by running `codespell`.
194 lines
4.0 KiB
Go
194 lines
4.0 KiB
Go
// 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)
|
|
}
|