-
Notifications
You must be signed in to change notification settings - Fork 16
/
socket.go
200 lines (177 loc) · 4.77 KB
/
socket.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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
}
}