// 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) }