diff options
Diffstat (limited to 'sway/ipc.c')
-rw-r--r-- | sway/ipc.c | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/sway/ipc.c b/sway/ipc.c new file mode 100644 index 00000000..39e580cd --- /dev/null +++ b/sway/ipc.c | |||
@@ -0,0 +1,321 @@ | |||
1 | // See https://i3wm.org/docs/ipc.html for protocol information | ||
2 | |||
3 | #include <errno.h> | ||
4 | #include <string.h> | ||
5 | #include <sys/socket.h> | ||
6 | #include <sys/un.h> | ||
7 | #include <stdbool.h> | ||
8 | #include <wlc/wlc.h> | ||
9 | #include <unistd.h> | ||
10 | #include <stdlib.h> | ||
11 | #include <stropts.h> | ||
12 | #include <sys/ioctl.h> | ||
13 | #include <fcntl.h> | ||
14 | #include <ctype.h> | ||
15 | #include "ipc.h" | ||
16 | #include "log.h" | ||
17 | #include "config.h" | ||
18 | #include "commands.h" | ||
19 | #include "list.h" | ||
20 | #include "stringop.h" | ||
21 | |||
22 | static int ipc_socket = -1; | ||
23 | static struct wlc_event_source *ipc_event_source = NULL; | ||
24 | static struct sockaddr_un ipc_sockaddr = { | ||
25 | .sun_family = AF_UNIX, | ||
26 | .sun_path = "/tmp/sway-ipc.sock" | ||
27 | }; | ||
28 | |||
29 | static const char ipc_magic[] = {'i', '3', '-', 'i', 'p', 'c'}; | ||
30 | |||
31 | struct ipc_client { | ||
32 | struct wlc_event_source *event_source; | ||
33 | int fd; | ||
34 | uint32_t payload_length; | ||
35 | enum ipc_command_type current_command; | ||
36 | }; | ||
37 | |||
38 | int ipc_handle_connection(int fd, uint32_t mask, void *data); | ||
39 | int ipc_client_handle_readable(int client_fd, uint32_t mask, void *data); | ||
40 | void ipc_client_disconnect(struct ipc_client *client); | ||
41 | void ipc_client_handle_command(struct ipc_client *client); | ||
42 | bool ipc_send_reply(struct ipc_client *client, const char *payload, uint32_t payload_length); | ||
43 | void ipc_get_workspaces_callback(swayc_t *container, void *data); | ||
44 | void ipc_get_outputs_callback(swayc_t *container, void *data); | ||
45 | |||
46 | char *json_list(list_t *items); | ||
47 | |||
48 | void ipc_init(void) { | ||
49 | ipc_socket = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); | ||
50 | if (ipc_socket == -1) { | ||
51 | sway_abort("Unable to create IPC socket"); | ||
52 | } | ||
53 | |||
54 | if (getenv("SWAYSOCK") != NULL) { | ||
55 | strncpy(ipc_sockaddr.sun_path, getenv("SWAYSOCK"), sizeof(ipc_sockaddr.sun_path)); | ||
56 | } | ||
57 | |||
58 | unlink(ipc_sockaddr.sun_path); | ||
59 | if (bind(ipc_socket, (struct sockaddr *)&ipc_sockaddr, sizeof(ipc_sockaddr)) == -1) { | ||
60 | sway_abort("Unable to bind IPC socket"); | ||
61 | } | ||
62 | |||
63 | if (listen(ipc_socket, 3) == -1) { | ||
64 | sway_abort("Unable to listen on IPC socket"); | ||
65 | } | ||
66 | |||
67 | // Set i3 IPC socket path so that i3-msg works out of the box | ||
68 | setenv("I3SOCK", ipc_sockaddr.sun_path, 1); | ||
69 | |||
70 | ipc_event_source = wlc_event_loop_add_fd(ipc_socket, WLC_EVENT_READABLE, ipc_handle_connection, NULL); | ||
71 | } | ||
72 | |||
73 | void ipc_terminate(void) { | ||
74 | if (ipc_event_source) { | ||
75 | wlc_event_source_remove(ipc_event_source); | ||
76 | } | ||
77 | close(ipc_socket); | ||
78 | unlink(ipc_sockaddr.sun_path); | ||
79 | } | ||
80 | |||
81 | int ipc_handle_connection(int fd, uint32_t mask, void *data) { | ||
82 | (void) fd; (void) data; | ||
83 | sway_log(L_DEBUG, "Event on IPC listening socket"); | ||
84 | assert(mask == WLC_EVENT_READABLE); | ||
85 | |||
86 | int client_fd = accept(ipc_socket, NULL, NULL); | ||
87 | if (client_fd == -1) { | ||
88 | sway_log_errno(L_INFO, "Unable to accept IPC client connection"); | ||
89 | return 0; | ||
90 | } | ||
91 | |||
92 | int flags; | ||
93 | if ((flags=fcntl(client_fd, F_GETFD)) == -1 || fcntl(client_fd, F_SETFD, flags|FD_CLOEXEC) == -1) { | ||
94 | sway_log_errno(L_INFO, "Unable to set CLOEXEC on IPC client socket"); | ||
95 | return 0; | ||
96 | } | ||
97 | |||
98 | struct ipc_client* client = malloc(sizeof(struct ipc_client)); | ||
99 | client->payload_length = 0; | ||
100 | client->fd = client_fd; | ||
101 | client->event_source = wlc_event_loop_add_fd(client_fd, WLC_EVENT_READABLE, ipc_client_handle_readable, client); | ||
102 | |||
103 | return 0; | ||
104 | } | ||
105 | |||
106 | static const int ipc_header_size = sizeof(ipc_magic)+8; | ||
107 | |||
108 | int ipc_client_handle_readable(int client_fd, uint32_t mask, void *data) { | ||
109 | struct ipc_client *client = data; | ||
110 | sway_log(L_DEBUG, "Event on IPC client socket %d", client_fd); | ||
111 | |||
112 | if (mask & WLC_EVENT_ERROR) { | ||
113 | sway_log(L_INFO, "IPC Client socket error, removing client"); | ||
114 | ipc_client_disconnect(client); | ||
115 | return 0; | ||
116 | } | ||
117 | |||
118 | if (mask & WLC_EVENT_HANGUP) { | ||
119 | ipc_client_disconnect(client); | ||
120 | return 0; | ||
121 | } | ||
122 | |||
123 | int read_available; | ||
124 | ioctl(client_fd, FIONREAD, &read_available); | ||
125 | |||
126 | // Wait for the rest of the command payload in case the header has already been read | ||
127 | if (client->payload_length > 0) { | ||
128 | if (read_available >= client->payload_length) { | ||
129 | ipc_client_handle_command(client); | ||
130 | } | ||
131 | else { | ||
132 | sway_log(L_DEBUG, "Too little data to read payload on IPC Client socket, waiting for more (%d < %d)", read_available, client->payload_length); | ||
133 | } | ||
134 | return 0; | ||
135 | } | ||
136 | |||
137 | if (read_available < ipc_header_size) { | ||
138 | sway_log(L_DEBUG, "Too little data to read header on IPC Client socket, waiting for more (%d < %d)", read_available, ipc_header_size); | ||
139 | return 0; | ||
140 | } | ||
141 | |||
142 | char buf[ipc_header_size]; | ||
143 | ssize_t received = recv(client_fd, buf, ipc_header_size, 0); | ||
144 | if (received == -1) { | ||
145 | sway_log_errno(L_INFO, "Unable to receive header from IPC client"); | ||
146 | ipc_client_disconnect(client); | ||
147 | return 0; | ||
148 | } | ||
149 | |||
150 | if (memcmp(buf, ipc_magic, sizeof(ipc_magic)) != 0) { | ||
151 | sway_log(L_DEBUG, "IPC header check failed"); | ||
152 | ipc_client_disconnect(client); | ||
153 | return 0; | ||
154 | } | ||
155 | |||
156 | client->payload_length = *(uint32_t *)&buf[sizeof(ipc_magic)]; | ||
157 | client->current_command = (enum ipc_command_type) *(uint32_t *)&buf[sizeof(ipc_magic)+4]; | ||
158 | |||
159 | if (read_available - received >= client->payload_length) { | ||
160 | ipc_client_handle_command(client); | ||
161 | } | ||
162 | |||
163 | return 0; | ||
164 | } | ||
165 | |||
166 | void ipc_client_disconnect(struct ipc_client *client) | ||
167 | { | ||
168 | if (!sway_assert(client != NULL, "client != NULL")) { | ||
169 | return; | ||
170 | } | ||
171 | |||
172 | sway_log(L_INFO, "IPC Client %d disconnected", client->fd); | ||
173 | wlc_event_source_remove(client->event_source); | ||
174 | close(client->fd); | ||
175 | free(client); | ||
176 | } | ||
177 | |||
178 | void ipc_client_handle_command(struct ipc_client *client) { | ||
179 | if (!sway_assert(client != NULL, "client != NULL")) { | ||
180 | return; | ||
181 | } | ||
182 | |||
183 | char buf[client->payload_length + 1]; | ||
184 | if (client->payload_length > 0) | ||
185 | { | ||
186 | ssize_t received = recv(client->fd, buf, client->payload_length, 0); | ||
187 | if (received == -1) | ||
188 | { | ||
189 | sway_log_errno(L_INFO, "Unable to receive payload from IPC client"); | ||
190 | ipc_client_disconnect(client); | ||
191 | return; | ||
192 | } | ||
193 | } | ||
194 | |||
195 | switch (client->current_command) { | ||
196 | case IPC_COMMAND: | ||
197 | { | ||
198 | buf[client->payload_length] = '\0'; | ||
199 | bool success = handle_command(config, buf); | ||
200 | char reply[64]; | ||
201 | int length = snprintf(reply, sizeof(reply), "{\"success\":%s}", success ? "true" : "false"); | ||
202 | ipc_send_reply(client, reply, (uint32_t) length); | ||
203 | break; | ||
204 | } | ||
205 | case IPC_GET_WORKSPACES: | ||
206 | { | ||
207 | list_t *workspaces = create_list(); | ||
208 | container_map(&root_container, ipc_get_workspaces_callback, workspaces); | ||
209 | char *json = json_list(workspaces); | ||
210 | free_flat_list(workspaces); | ||
211 | ipc_send_reply(client, json, strlen(json)); | ||
212 | free(json); | ||
213 | break; | ||
214 | } | ||
215 | case IPC_GET_OUTPUTS: | ||
216 | { | ||
217 | list_t *outputs = create_list(); | ||
218 | container_map(&root_container, ipc_get_outputs_callback, outputs); | ||
219 | char *json = json_list(outputs); | ||
220 | free_flat_list(outputs); | ||
221 | ipc_send_reply(client, json, strlen(json)); | ||
222 | free(json); | ||
223 | break; | ||
224 | } | ||
225 | default: | ||
226 | sway_log(L_INFO, "Unknown IPC command type %i", client->current_command); | ||
227 | ipc_client_disconnect(client); | ||
228 | break; | ||
229 | } | ||
230 | |||
231 | client->payload_length = 0; | ||
232 | } | ||
233 | |||
234 | bool ipc_send_reply(struct ipc_client *client, const char *payload, uint32_t payload_length) { | ||
235 | assert(payload); | ||
236 | |||
237 | char data[ipc_header_size]; | ||
238 | |||
239 | memcpy(data, ipc_magic, sizeof(ipc_magic)); | ||
240 | *(uint32_t *)&(data[sizeof(ipc_magic)]) = payload_length; | ||
241 | *(uint32_t *)&(data[sizeof(ipc_magic)+4]) = client->current_command; | ||
242 | |||
243 | if (write(client->fd, data, ipc_header_size) == -1) { | ||
244 | sway_log_errno(L_INFO, "Unable to send header to IPC client"); | ||
245 | ipc_client_disconnect(client); | ||
246 | return false; | ||
247 | } | ||
248 | |||
249 | if (write(client->fd, payload, payload_length) == -1) { | ||
250 | sway_log_errno(L_INFO, "Unable to send payload to IPC client"); | ||
251 | ipc_client_disconnect(client); | ||
252 | return false; | ||
253 | } | ||
254 | |||
255 | return true; | ||
256 | } | ||
257 | |||
258 | char *json_list(list_t *items) { | ||
259 | char *json_elements = join_list(items, ","); | ||
260 | size_t len = strlen(json_elements); | ||
261 | char *json = malloc(len + 3); | ||
262 | json[0] = '['; | ||
263 | memcpy(json + 1, json_elements, len); | ||
264 | json[len+1] = ']'; | ||
265 | json[len+2] = '\0'; | ||
266 | free(json_elements); | ||
267 | return json; | ||
268 | } | ||
269 | |||
270 | void ipc_get_workspaces_callback(swayc_t *container, void *data) { | ||
271 | if (container->type == C_WORKSPACE) { | ||
272 | char *json = malloc(512); // Output should usually be around 180 chars | ||
273 | int num = isdigit(container->name[0]) ? atoi(container->name) : -1; | ||
274 | // TODO: escape the name (quotation marks, unicode) | ||
275 | sprintf(json, | ||
276 | "{" | ||
277 | "\"num\":%d," | ||
278 | "\"name\":\"%s\"," | ||
279 | "\"visible\":%s," | ||
280 | "\"focused\":%s," | ||
281 | "\"rect\":{" | ||
282 | "\"x\":%d," | ||
283 | "\"y\":%d," | ||
284 | "\"width\":%d," | ||
285 | "\"height\":%d" | ||
286 | "}," | ||
287 | "\"output\":\"%s\"," | ||
288 | "\"urgent\":%s" | ||
289 | "}", | ||
290 | num, container->name, container->visible ? "true" : "false", container->is_focused ? "true" : "false", | ||
291 | container->x, container->y, container->width, container->height, | ||
292 | container->parent->name, "false" // TODO: urgent hint | ||
293 | ); | ||
294 | list_add((list_t *)data, json); | ||
295 | } | ||
296 | } | ||
297 | |||
298 | void ipc_get_outputs_callback(swayc_t *container, void *data) { | ||
299 | if (container->type == C_OUTPUT) { | ||
300 | char *json = malloc(512); // Output should usually be around 130 chars | ||
301 | // TODO: escape the name (quotation marks, unicode) | ||
302 | sprintf(json, | ||
303 | "{" | ||
304 | "\"name\":\"%s\"," | ||
305 | "\"active\":%s," | ||
306 | "\"primary\":%s," | ||
307 | "\"rect\":{" | ||
308 | "\"x\":%d," | ||
309 | "\"y\":%d," | ||
310 | "\"width\":%d," | ||
311 | "\"height\":%d" | ||
312 | "}," | ||
313 | "\"current_workspace\":\"%s\"" | ||
314 | "}", | ||
315 | container->name, "true", "false", // TODO: active, primary | ||
316 | container->x, container->y, container->width, container->height, | ||
317 | container->focused ? container->focused->name : "" | ||
318 | ); | ||
319 | list_add((list_t *)data, json); | ||
320 | } | ||
321 | } | ||