rpcfetch/io.go
Ophestra Umiker b3325f56b1
library: io: handle retryable errors
Discord client being absent/disconnecting is not fatal to the RPC sender, in cases like that we return ErrAgain and in the case of broken pipe (Discord client going away) also reset the Client state so the caller can choose to retry the operation and therefore initiate a new connection attempt.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-06-20 01:14:04 +09:00

120 lines
2.4 KiB
Go

package rpcfetch
import (
"encoding/binary"
"encoding/json"
"errors"
"net"
"os"
"strconv"
"syscall"
)
type Client struct {
id string
config *Config
user *User
dialed bool
active bool
conn net.Conn
}
// Raw wraps around the Raw method to provide generic json parsing
func Raw[T any](d *Client, opcode uint32, payload any) (uint32, T, error) {
var p T
opcodeResp, payloadResp, err := d.Raw(opcode, payload)
if err != nil {
return opcodeResp, p, err
}
return opcodeResp, p, json.Unmarshal(payloadResp, &p)
}
// Raw writes a raw payload to Discord and returns the response opcode and payload
func (d *Client) Raw(opcode uint32, payload any) (uint32, []byte, error) {
opcodeResp, payloadResp, err := d.raw(opcode, payload)
if errors.Is(err, syscall.EPIPE) {
// clean up as much as possible
_ = d.Close()
// advise retry
err = ErrAgain
}
return opcodeResp, payloadResp, err
}
func (d *Client) raw(opcode uint32, payload any) (uint32, []byte, error) {
if err := binary.Write(d.conn, binary.LittleEndian, opcode); err != nil {
return 0, nil, err
}
if p, err := json.Marshal(payload); err != nil {
return 0, nil, err
} else {
if err = binary.Write(d.conn, binary.LittleEndian, uint32(len(p))); err != nil {
return 0, nil, err
}
if _, err = d.conn.Write(p); err != nil {
return 0, nil, err
}
}
var (
opcodeResp uint32
lengthResp uint32
)
if err := binary.Read(d.conn, binary.LittleEndian, &opcodeResp); err != nil {
return 0, nil, err
}
if err := binary.Read(d.conn, binary.LittleEndian, &lengthResp); err != nil {
return 0, nil, err
}
payloadResp := make([]byte, lengthResp)
_, err := d.conn.Read(payloadResp)
return opcodeResp, payloadResp, err
}
// Close the client, this is required before exit
func (d *Client) Close() error {
if !d.dialed {
// silently succeed because client activation is implicit
return nil
}
d.active = false
d.dialed = false
return d.conn.Close()
}
// New sets up and returns the reference to a new Client
func New(id string) *Client {
d := &Client{
id: id,
}
return d
}
func sockPath() string {
snap := "/run/user/" + strconv.Itoa(os.Getuid()) + "/snap.discord"
if _, err := os.Stat(snap); err == nil {
return snap
}
for _, env := range []string{"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"} {
if val, ok := os.LookupEnv(env); ok {
return val
}
}
// fallback
return "/tmp"
}