201 lines
4.8 KiB
Go
201 lines
4.8 KiB
Go
package i3
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// If your computer takes more than 10s to restart i3, it must be seriously
|
|
// overloaded, in which case we are probably doing you a favor by erroring out.
|
|
const reconnectTimeout = 10 * time.Second
|
|
|
|
// remote is a singleton containing the socket path and auto-detected byte order
|
|
// which i3 is using. It is lazily initialized by getIPCSocket.
|
|
var remote struct {
|
|
path string
|
|
order binary.ByteOrder
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// SocketPathHook Provides a way to override the default socket path lookup mechanism. Overriding this is unsupported.
|
|
var SocketPathHook = func() (string, error) {
|
|
out, err := exec.Command("i3", "--get-socketpath").CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting i3 socketpath: %v (output: %s)", err, out)
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
func getIPCSocket(updateSocketPath bool) (*socket, net.Conn, error) {
|
|
remote.mu.Lock()
|
|
defer remote.mu.Unlock()
|
|
path := remote.path
|
|
if (!wasRestart && updateSocketPath) || remote.path == "" {
|
|
out, err := SocketPathHook()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
path = strings.TrimSpace(string(out))
|
|
}
|
|
conn, err := net.Dial("unix", path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
remote.path = path
|
|
if remote.order == nil {
|
|
remote.order, err = detectByteOrder(conn)
|
|
if err != nil {
|
|
conn.Close()
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
return &socket{conn: conn, order: remote.order}, conn, err
|
|
}
|
|
|
|
type messageType uint32
|
|
|
|
const (
|
|
messageTypeRunCommand messageType = iota
|
|
messageTypeGetWorkspaces
|
|
messageTypeSubscribe
|
|
messageTypeGetOutputs
|
|
messageTypeGetTree
|
|
messageTypeGetMarks
|
|
messageTypeGetBarConfig
|
|
messageTypeGetVersion
|
|
messageTypeGetBindingModes
|
|
messageTypeGetConfig
|
|
messageTypeSendTick
|
|
messageTypeSync
|
|
messageTypeGetBindingState
|
|
)
|
|
|
|
var messageAtLeast = map[messageType]majorMinor{
|
|
messageTypeRunCommand: {4, 0},
|
|
messageTypeGetWorkspaces: {4, 0},
|
|
messageTypeSubscribe: {4, 0},
|
|
messageTypeGetOutputs: {4, 0},
|
|
messageTypeGetTree: {4, 0},
|
|
messageTypeGetMarks: {4, 1},
|
|
messageTypeGetBarConfig: {4, 1},
|
|
messageTypeGetVersion: {4, 3},
|
|
messageTypeGetBindingModes: {4, 13},
|
|
messageTypeGetConfig: {4, 14},
|
|
messageTypeSendTick: {4, 15},
|
|
messageTypeSync: {4, 16},
|
|
messageTypeGetBindingState: {4, 19},
|
|
}
|
|
|
|
const (
|
|
messageReplyTypeCommand messageType = iota
|
|
messageReplyTypeWorkspaces
|
|
messageReplyTypeSubscribe
|
|
)
|
|
|
|
var magic = [6]byte{'i', '3', '-', 'i', 'p', 'c'}
|
|
|
|
type header struct {
|
|
Magic [6]byte
|
|
Length uint32
|
|
Type messageType
|
|
}
|
|
|
|
type message struct {
|
|
Type messageType
|
|
Payload []byte
|
|
}
|
|
|
|
type socket struct {
|
|
conn io.ReadWriter
|
|
order binary.ByteOrder
|
|
}
|
|
|
|
func (s *socket) recvMsg() (message, error) {
|
|
if s == nil {
|
|
return message{}, fmt.Errorf("not connected")
|
|
}
|
|
var h header
|
|
if err := binary.Read(s.conn, s.order, &h); err != nil {
|
|
return message{}, err
|
|
}
|
|
msg := message{
|
|
Type: h.Type,
|
|
Payload: make([]byte, h.Length),
|
|
}
|
|
_, err := io.ReadFull(s.conn, msg.Payload)
|
|
return msg, err
|
|
}
|
|
|
|
func (s *socket) roundTrip(t messageType, payload []byte) (message, error) {
|
|
if s == nil {
|
|
return message{}, fmt.Errorf("not connected")
|
|
}
|
|
|
|
if err := binary.Write(s.conn, s.order, &header{magic, uint32(len(payload)), t}); err != nil {
|
|
return message{}, err
|
|
}
|
|
if len(payload) > 0 { // skip empty Write()s for net.Pipe
|
|
_, err := s.conn.Write(payload)
|
|
if err != nil {
|
|
return message{}, err
|
|
}
|
|
}
|
|
return s.recvMsg()
|
|
}
|
|
|
|
// defaultSock is a singleton, lazily initialized by roundTrip. All
|
|
// request/response messages are sent to i3 via this socket, whereas
|
|
// subscriptions use their own connection.
|
|
var defaultSock struct {
|
|
sock *socket
|
|
conn net.Conn
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// roundTrip sends a message to i3 and returns the received result in a
|
|
// concurrency-safe fashion.
|
|
func roundTrip(t messageType, payload []byte) (message, error) {
|
|
// Error out early in case the message type is not yet supported by the
|
|
// running i3 version.
|
|
if t != messageTypeGetVersion {
|
|
if err := AtLeast(messageAtLeast[t].major, messageAtLeast[t].minor); err != nil {
|
|
return message{}, err
|
|
}
|
|
}
|
|
|
|
defaultSock.mu.Lock()
|
|
defer defaultSock.mu.Unlock()
|
|
|
|
Outer:
|
|
for {
|
|
msg, err := defaultSock.sock.roundTrip(t, payload)
|
|
if err == nil {
|
|
return msg, nil // happy path: success
|
|
}
|
|
|
|
// reconnect
|
|
start := time.Now()
|
|
for time.Since(start) < reconnectTimeout && (defaultSock.sock == nil || i3Running()) {
|
|
if defaultSock.sock != nil {
|
|
defaultSock.conn.Close()
|
|
}
|
|
defaultSock.sock, defaultSock.conn, err = getIPCSocket(defaultSock.sock != nil)
|
|
if err == nil {
|
|
continue Outer
|
|
}
|
|
|
|
// Reconnect within [10, 20) ms to prevent CPU-starving i3.
|
|
time.Sleep(time.Duration(10+rand.Int63n(10)) * time.Millisecond)
|
|
}
|
|
return msg, err
|
|
}
|
|
}
|