aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMoritz Poldrack <git@moritz.sh>2023-03-04 10:56:45 +0100
committerRobin Jarry <robin@jarry.cc>2023-03-07 00:12:47 +0100
commit4838efdb1d5a746432a30ef0b86b090aab52fa7a (patch)
tree10c1c6954e47395ae9b310ce268a77cf9924a4d8
parent3dbf33bb4c8988851eeed0292fcdc170eb0ee6c7 (diff)
downloadaerc-4838efdb1d5a746432a30ef0b86b090aab52fa7a.tar.gz
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 <git@moritz.sh> Signed-off-by: Robin Jarry <robin@jarry.cc>
-rw-r--r--aerc.go11
-rw-r--r--lib/ipc/message.go52
-rw-r--r--lib/ipc/receive.go57
-rw-r--r--lib/ipc/send.go24
4 files changed, 108 insertions, 36 deletions
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
}