1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-10-29 15:02:30 +00:00
Files
go-chasquid-smtp/internal/localrpc/localrpc.go
T
Alberto Bertogli 8b19760f4e test: Use modern testing features instead of custom helpers
Many of our tests rely on helpers that predate features which are now
available in the standard library.

This patch replaces them with the standard equivalents:

- MustTempDir/RemoveIfOk -> t.ArtifactDir() or t.TempDir() (depending on
  whether it is useful to keep the data on request).
- GetFreePort + waitForServer -> opening the listeners before starting
  the servers. To support this, localrpc.Server gains a Serve method. We
  may re-introduce these in the future if more use cases appear.
- WaitFor and real-time timing checks -> testing/synctest.
- context.Background() -> t.Context().
- Benchmark b.N loops -> b.Loop().
- reflect.DeepEqual -> cmp.Diff, and proto.Equal for protobufs.
2026-10-03 10:34:35 +01:00

196 lines
4.1 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 {
// Previous instances of the server may have shut down uncleanly, leaving
// behind the socket file. Remove it just in case.
os.Remove(path)
lis, err := net.Listen("unix", path)
if err != nil {
return err
}
return s.Serve(lis)
}
// Serve connections from the given listener.
func (s *Server) Serve(lis net.Listener) error {
tr := trace.New("LocalRPC.Server", lis.Addr().String())
defer tr.Finish()
s.lis = lis
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
}
name, params, _ := strings.Cut(line, " ")
return name, params, 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)
}