From 4838efdb1d5a746432a30ef0b86b090aab52fa7a Mon Sep 17 00:00:00 2001 From: Moritz Poldrack Date: Sat, 4 Mar 2023 10:56:45 +0100 Subject: ipc: change protocol to JSON In overhauling the IPC, it has become necessary to switch to a more extendable message format, to ensure more complex commands can be sent. Messages have the following basic structure and must not contain linebreaks, as these are used to delimit separate messages from one another. {"arguments": ["mailto:moritz@poldrack.dev"]} The responses have the following structure: {"error": "epic fail"} If the IPC request was successful, "error" will be empty. {"error": ""} Signed-off-by: Moritz Poldrack Signed-off-by: Robin Jarry --- aerc.go | 11 +++-------- lib/ipc/message.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/ipc/receive.go | 57 +++++++++++++++++++++++++++++++----------------------- lib/ipc/send.go | 24 +++++++++++++++++++---- 4 files changed, 108 insertions(+), 36 deletions(-) create mode 100644 lib/ipc/message.go diff --git a/aerc.go b/aerc.go index 9ed4a2ca..f4247ed5 100644 --- a/aerc.go +++ b/aerc.go @@ -159,12 +159,8 @@ func main() { } retryExec := false args := os.Args[optind:] - if len(args) > 1 { - usage("error: invalid arguments") - return - } else if len(args) == 1 { - arg := args[0] - err := ipc.ConnectAndExec(arg) + if len(args) > 0 { + err := ipc.ConnectAndExec(args) if err == nil { return // other aerc instance takes over } @@ -232,8 +228,7 @@ func main() { if retryExec { // retry execution - arg := args[0] - err := ipc.ConnectAndExec(arg) + err := ipc.ConnectAndExec(args) if err != nil { fmt.Fprintf(os.Stderr, "Failed to communicate to aerc: %v\n", err) err = aerc.CloseBackends() diff --git a/lib/ipc/message.go b/lib/ipc/message.go new file mode 100644 index 00000000..3bd1e85c --- /dev/null +++ b/lib/ipc/message.go @@ -0,0 +1,52 @@ +package ipc + +import "encoding/json" + +// Request constains all parameters needed for the main instance to respond to +// a request. +type Request struct { + // Arguments contains the commandline arguments. The detection of what + // action to take is left to the receiver. + Arguments []string `json:"arguments"` +} + +// Response is used to report the results of a command. +type Response struct { + // Error contains the success-state of the command. Error is an empty + // string if everything ran successfully. + Error string `json:"error"` +} + +// Encode transforms the message in an easier to transfer format +func (msg *Request) Encode() ([]byte, error) { + return json.Marshal(msg) +} + +// DecodeMessage consumes a raw message and returns the message contained +// within. +func DecodeMessage(data []byte) (*Request, error) { + msg := new(Request) + err := json.Unmarshal(data, msg) + return msg, err +} + +// Encode transforms the message in an easier to transfer format +func (msg *Response) Encode() ([]byte, error) { + return json.Marshal(msg) +} + +// DecodeRequest consumes a raw message and returns the message contained +// within. +func DecodeRequest(data []byte) (*Request, error) { + msg := new(Request) + err := json.Unmarshal(data, msg) + return msg, err +} + +// DecodeResponse consumes a raw message and returns the message contained +// within. +func DecodeResponse(data []byte) (*Response, error) { + msg := new(Response) + err := json.Unmarshal(data, msg) + return msg, err +} diff --git a/lib/ipc/receive.go b/lib/ipc/receive.go index c074b116..11a96e30 100644 --- a/lib/ipc/receive.go +++ b/lib/ipc/receive.go @@ -3,7 +3,6 @@ package ipc import ( "bufio" "errors" - "fmt" "net" "net/url" "os" @@ -26,7 +25,7 @@ type AercServer struct { func StartServer() (*AercServer, error) { sockpath := path.Join(xdg.RuntimeDir(), "aerc.sock") // remove the socket if it is not connected to a session - if err := ConnectAndExec(""); err != nil { + if err := ConnectAndExec(nil); err != nil { os.Remove(sockpath) } log.Debugf("Starting Unix server: %s", sockpath) @@ -69,14 +68,25 @@ func (as *AercServer) Serve() { log.Errorf("unix:%d failed to set deadline: %v", clientId, err) } for scanner.Scan() { + // allow up to 1 minute between commands err = conn.SetDeadline(time.Now().Add(1 * time.Minute)) if err != nil { log.Errorf("unix:%d failed to update deadline: %v", clientId, err) } - msg := scanner.Text() - log.Tracef("unix:%d got message %s", clientId, msg) + msg, err := DecodeRequest(scanner.Bytes()) + log.Tracef("unix:%d got message %s", clientId, scanner.Text()) + if err != nil { + log.Errorf("unix:%d failed to parse request: %v", clientId, err) + continue + } - _, err = conn.Write([]byte(as.handleMessage(msg))) + response := as.handleMessage(msg) + result, err := response.Encode() + if err != nil { + log.Errorf("unix:%d failed to encode result: %v", clientId, err) + continue + } + _, err = conn.Write(append(result, '\n')) if err != nil { log.Errorf("unix:%d failed to send response: %v", clientId, err) break @@ -86,31 +96,30 @@ func (as *AercServer) Serve() { } } -func (as *AercServer) handleMessage(msg string) string { - if !strings.ContainsRune(msg, ':') { - return "error: invalid command\n" +func (as *AercServer) handleMessage(req *Request) *Response { + if len(req.Arguments) == 0 { + return &Response{} // send noop success message, i.e. ping } - prefix := msg[:strings.IndexRune(msg, ':')] var err error - switch prefix { - case "mailto": - mailto, err := url.Parse(msg) + switch { + case strings.HasPrefix(req.Arguments[0], "mailto:"): + mailto, err := url.Parse(req.Arguments[0]) if err != nil { - return fmt.Sprintf("error: %v\n", err) + return &Response{Error: err.Error()} } - if as.OnMailto != nil { - err = as.OnMailto(mailto) - if err != nil { - return fmt.Sprintf("mailto failed: %v\n", err) + err = as.OnMailto(mailto) + if err != nil { + return &Response{ + Error: err.Error(), } } - case "mbox": - if as.OnMbox != nil { - err = as.OnMbox(msg) - if err != nil { - return fmt.Sprintf("mbox failed: %v\n", err) - } + case strings.HasPrefix(req.Arguments[0], "mbox:"): + err = as.OnMbox(req.Arguments[0]) + if err != nil { + return &Response{Error: err.Error()} } + default: + return &Response{Error: "command not understood"} } - return "result: success\n" + return &Response{} } diff --git a/lib/ipc/send.go b/lib/ipc/send.go index 5cc97cc0..522e944a 100644 --- a/lib/ipc/send.go +++ b/lib/ipc/send.go @@ -10,14 +10,20 @@ import ( "github.com/kyoh86/xdg" ) -func ConnectAndExec(msg string) error { +func ConnectAndExec(args []string) error { sockpath := path.Join(xdg.RuntimeDir(), "aerc.sock") conn, err := net.Dial("unix", sockpath) if err != nil { return err } defer conn.Close() - _, err = conn.Write([]byte(msg + "\n")) + + req, err := (&Request{Arguments: args}).Encode() + if err != nil { + return fmt.Errorf("failed to encode request: %w", err) + } + + _, err = conn.Write(append(req, '\n')) if err != nil { return fmt.Errorf("failed to send message: %w", err) } @@ -25,7 +31,17 @@ func ConnectAndExec(msg string) error { if !scanner.Scan() { return errors.New("No response from server") } - result := scanner.Text() - fmt.Println(result) + resp, err := DecodeResponse(scanner.Bytes()) + if err != nil { + return err + } + + // TODO: handle this in a more elegant manner + if resp.Error == "" { + fmt.Println("result: success") + } else { + fmt.Println("result: ", resp.Error) + } + return nil } -- cgit v1.2.3-54-g00ecf