dwm-msg.c (13408B)
1 #include <ctype.h> 2 #include <errno.h> 3 #include <inttypes.h> 4 #include <stdarg.h> 5 #include <stdint.h> 6 #include <stdio.h> 7 #include <stdlib.h> 8 #include <string.h> 9 #include <sys/socket.h> 10 #include <sys/un.h> 11 #include <unistd.h> 12 #include <yajl/yajl_gen.h> 13 14 #define IPC_MAGIC "DWM-IPC" 15 // clang-format off 16 #define IPC_MAGIC_ARR { 'D', 'W', 'M', '-', 'I', 'P', 'C' } 17 // clang-format on 18 #define IPC_MAGIC_LEN 7 // Not including null char 19 20 #define IPC_EVENT_TAG_CHANGE "tag_change_event" 21 #define IPC_EVENT_CLIENT_FOCUS_CHANGE "client_focus_change_event" 22 #define IPC_EVENT_LAYOUT_CHANGE "layout_change_event" 23 #define IPC_EVENT_MONITOR_FOCUS_CHANGE "monitor_focus_change_event" 24 #define IPC_EVENT_FOCUSED_TITLE_CHANGE "focused_title_change_event" 25 #define IPC_EVENT_FOCUSED_STATE_CHANGE "focused_state_change_event" 26 27 #define YSTR(str) yajl_gen_string(gen, (unsigned char *)str, strlen(str)) 28 #define YINT(num) yajl_gen_integer(gen, num) 29 #define YDOUBLE(num) yajl_gen_double(gen, num) 30 #define YBOOL(v) yajl_gen_bool(gen, v) 31 #define YNULL() yajl_gen_null(gen) 32 #define YARR(body) \ 33 { \ 34 yajl_gen_array_open(gen); \ 35 body; \ 36 yajl_gen_array_close(gen); \ 37 } 38 #define YMAP(body) \ 39 { \ 40 yajl_gen_map_open(gen); \ 41 body; \ 42 yajl_gen_map_close(gen); \ 43 } 44 45 typedef unsigned long Window; 46 47 const char *DEFAULT_SOCKET_PATH = "/tmp/dwm.sock"; 48 static int sock_fd = -1; 49 static unsigned int ignore_reply = 0; 50 51 typedef enum IPCMessageType { 52 IPC_TYPE_RUN_COMMAND = 0, 53 IPC_TYPE_GET_MONITORS = 1, 54 IPC_TYPE_GET_TAGS = 2, 55 IPC_TYPE_GET_LAYOUTS = 3, 56 IPC_TYPE_GET_DWM_CLIENT = 4, 57 IPC_TYPE_SUBSCRIBE = 5, 58 IPC_TYPE_EVENT = 6 59 } IPCMessageType; 60 61 // Every IPC message must begin with this 62 typedef struct dwm_ipc_header { 63 uint8_t magic[IPC_MAGIC_LEN]; 64 uint32_t size; 65 uint8_t type; 66 } __attribute((packed)) dwm_ipc_header_t; 67 68 static int 69 recv_message(uint8_t *msg_type, uint32_t *reply_size, uint8_t **reply) 70 { 71 uint32_t read_bytes = 0; 72 const int32_t to_read = sizeof(dwm_ipc_header_t); 73 char header[to_read]; 74 char *walk = header; 75 76 // Try to read header 77 while (read_bytes < to_read) { 78 ssize_t n = read(sock_fd, header + read_bytes, to_read - read_bytes); 79 80 if (n == 0) { 81 if (read_bytes == 0) { 82 fprintf(stderr, "Unexpectedly reached EOF while reading header."); 83 fprintf(stderr, 84 "Read %" PRIu32 " bytes, expected %" PRIu32 " total bytes.\n", 85 read_bytes, to_read); 86 return -2; 87 } else { 88 fprintf(stderr, "Unexpectedly reached EOF while reading header."); 89 fprintf(stderr, 90 "Read %" PRIu32 " bytes, expected %" PRIu32 " total bytes.\n", 91 read_bytes, to_read); 92 return -3; 93 } 94 } else if (n == -1) { 95 return -1; 96 } 97 98 read_bytes += n; 99 } 100 101 // Check if magic string in header matches 102 if (memcmp(walk, IPC_MAGIC, IPC_MAGIC_LEN) != 0) { 103 fprintf(stderr, "Invalid magic string. Got '%.*s', expected '%s'\n", 104 IPC_MAGIC_LEN, walk, IPC_MAGIC); 105 return -3; 106 } 107 108 walk += IPC_MAGIC_LEN; 109 110 // Extract reply size 111 memcpy(reply_size, walk, sizeof(uint32_t)); 112 walk += sizeof(uint32_t); 113 114 // Extract message type 115 memcpy(msg_type, walk, sizeof(uint8_t)); 116 walk += sizeof(uint8_t); 117 118 (*reply) = malloc(*reply_size); 119 120 // Extract payload 121 read_bytes = 0; 122 while (read_bytes < *reply_size) { 123 ssize_t n = read(sock_fd, *reply + read_bytes, *reply_size - read_bytes); 124 125 if (n == 0) { 126 fprintf(stderr, "Unexpectedly reached EOF while reading payload."); 127 fprintf(stderr, "Read %" PRIu32 " bytes, expected %" PRIu32 " bytes.\n", 128 read_bytes, *reply_size); 129 free(*reply); 130 return -2; 131 } else if (n == -1) { 132 if (errno == EINTR || errno == EAGAIN) continue; 133 free(*reply); 134 return -1; 135 } 136 137 read_bytes += n; 138 } 139 140 return 0; 141 } 142 143 static int 144 read_socket(IPCMessageType *msg_type, uint32_t *msg_size, char **msg) 145 { 146 int ret = -1; 147 148 while (ret != 0) { 149 ret = recv_message((uint8_t *)msg_type, msg_size, (uint8_t **)msg); 150 151 if (ret < 0) { 152 // Try again (non-fatal error) 153 if (ret == -1 && (errno == EINTR || errno == EAGAIN)) continue; 154 155 fprintf(stderr, "Error receiving response from socket. "); 156 fprintf(stderr, "The connection might have been lost.\n"); 157 exit(2); 158 } 159 } 160 161 return 0; 162 } 163 164 static ssize_t 165 write_socket(const void *buf, size_t count) 166 { 167 size_t written = 0; 168 169 while (written < count) { 170 const ssize_t n = 171 write(sock_fd, ((uint8_t *)buf) + written, count - written); 172 173 if (n == -1) { 174 if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) 175 continue; 176 else 177 return n; 178 } 179 written += n; 180 } 181 return written; 182 } 183 184 static void 185 connect_to_socket() 186 { 187 struct sockaddr_un addr; 188 189 int sock = socket(AF_UNIX, SOCK_STREAM, 0); 190 191 // Initialize struct to 0 192 memset(&addr, 0, sizeof(struct sockaddr_un)); 193 194 addr.sun_family = AF_UNIX; 195 strcpy(addr.sun_path, DEFAULT_SOCKET_PATH); 196 197 connect(sock, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)); 198 199 sock_fd = sock; 200 } 201 202 static int 203 send_message(IPCMessageType msg_type, uint32_t msg_size, uint8_t *msg) 204 { 205 dwm_ipc_header_t header = { 206 .magic = IPC_MAGIC_ARR, .size = msg_size, .type = msg_type}; 207 208 size_t header_size = sizeof(dwm_ipc_header_t); 209 size_t total_size = header_size + msg_size; 210 211 uint8_t buffer[total_size]; 212 213 // Copy header to buffer 214 memcpy(buffer, &header, header_size); 215 // Copy message to buffer 216 memcpy(buffer + header_size, msg, header.size); 217 218 write_socket(buffer, total_size); 219 220 return 0; 221 } 222 223 static int 224 is_float(const char *s) 225 { 226 size_t len = strlen(s); 227 int is_dot_used = 0; 228 int is_minus_used = 0; 229 230 // Floats can only have one decimal point in between or digits 231 // Optionally, floats can also be below zero (negative) 232 for (int i = 0; i < len; i++) { 233 if (isdigit(s[i])) 234 continue; 235 else if (!is_dot_used && s[i] == '.' && i != 0 && i != len - 1) { 236 is_dot_used = 1; 237 continue; 238 } else if (!is_minus_used && s[i] == '-' && i == 0) { 239 is_minus_used = 1; 240 continue; 241 } else 242 return 0; 243 } 244 245 return 1; 246 } 247 248 static int 249 is_unsigned_int(const char *s) 250 { 251 size_t len = strlen(s); 252 253 // Unsigned int can only have digits 254 for (int i = 0; i < len; i++) { 255 if (isdigit(s[i])) 256 continue; 257 else 258 return 0; 259 } 260 261 return 1; 262 } 263 264 static int 265 is_signed_int(const char *s) 266 { 267 size_t len = strlen(s); 268 269 // Signed int can only have digits and a negative sign at the start 270 for (int i = 0; i < len; i++) { 271 if (isdigit(s[i])) 272 continue; 273 else if (i == 0 && s[i] == '-') { 274 continue; 275 } else 276 return 0; 277 } 278 279 return 1; 280 } 281 282 static void 283 flush_socket_reply() 284 { 285 IPCMessageType reply_type; 286 uint32_t reply_size; 287 char *reply; 288 289 read_socket(&reply_type, &reply_size, &reply); 290 291 free(reply); 292 } 293 294 static void 295 print_socket_reply() 296 { 297 IPCMessageType reply_type; 298 uint32_t reply_size; 299 char *reply; 300 301 read_socket(&reply_type, &reply_size, &reply); 302 303 printf("%.*s\n", reply_size, reply); 304 fflush(stdout); 305 free(reply); 306 } 307 308 static int 309 run_command(const char *name, char *args[], int argc) 310 { 311 const unsigned char *msg; 312 size_t msg_size; 313 314 yajl_gen gen = yajl_gen_alloc(NULL); 315 316 // Message format: 317 // { 318 // "command": "<name>", 319 // "args": [ ... ] 320 // } 321 // clang-format off 322 YMAP( 323 YSTR("command"); YSTR(name); 324 YSTR("args"); YARR( 325 for (int i = 0; i < argc; i++) { 326 if (is_signed_int(args[i])) { 327 long long num = atoll(args[i]); 328 YINT(num); 329 } else if (is_float(args[i])) { 330 float num = atof(args[i]); 331 YDOUBLE(num); 332 } else { 333 YSTR(args[i]); 334 } 335 } 336 ) 337 ) 338 // clang-format on 339 340 yajl_gen_get_buf(gen, &msg, &msg_size); 341 342 send_message(IPC_TYPE_RUN_COMMAND, msg_size, (uint8_t *)msg); 343 344 if (!ignore_reply) 345 print_socket_reply(); 346 else 347 flush_socket_reply(); 348 349 yajl_gen_free(gen); 350 351 return 0; 352 } 353 354 static int 355 get_monitors() 356 { 357 send_message(IPC_TYPE_GET_MONITORS, 1, (uint8_t *)""); 358 print_socket_reply(); 359 return 0; 360 } 361 362 static int 363 get_tags() 364 { 365 send_message(IPC_TYPE_GET_TAGS, 1, (uint8_t *)""); 366 print_socket_reply(); 367 368 return 0; 369 } 370 371 static int 372 get_layouts() 373 { 374 send_message(IPC_TYPE_GET_LAYOUTS, 1, (uint8_t *)""); 375 print_socket_reply(); 376 377 return 0; 378 } 379 380 static int 381 get_dwm_client(Window win) 382 { 383 const unsigned char *msg; 384 size_t msg_size; 385 386 yajl_gen gen = yajl_gen_alloc(NULL); 387 388 // Message format: 389 // { 390 // "client_window_id": "<win>" 391 // } 392 // clang-format off 393 YMAP( 394 YSTR("client_window_id"); YINT(win); 395 ) 396 // clang-format on 397 398 yajl_gen_get_buf(gen, &msg, &msg_size); 399 400 send_message(IPC_TYPE_GET_DWM_CLIENT, msg_size, (uint8_t *)msg); 401 402 print_socket_reply(); 403 404 yajl_gen_free(gen); 405 406 return 0; 407 } 408 409 static int 410 subscribe(const char *event) 411 { 412 const unsigned char *msg; 413 size_t msg_size; 414 415 yajl_gen gen = yajl_gen_alloc(NULL); 416 417 // Message format: 418 // { 419 // "event": "<event>", 420 // "action": "subscribe" 421 // } 422 // clang-format off 423 YMAP( 424 YSTR("event"); YSTR(event); 425 YSTR("action"); YSTR("subscribe"); 426 ) 427 // clang-format on 428 429 yajl_gen_get_buf(gen, &msg, &msg_size); 430 431 send_message(IPC_TYPE_SUBSCRIBE, msg_size, (uint8_t *)msg); 432 433 if (!ignore_reply) 434 print_socket_reply(); 435 else 436 flush_socket_reply(); 437 438 yajl_gen_free(gen); 439 440 return 0; 441 } 442 443 static void 444 usage_error(const char *prog_name, const char *format, ...) 445 { 446 va_list args; 447 va_start(args, format); 448 449 fprintf(stderr, "Error: "); 450 vfprintf(stderr, format, args); 451 fprintf(stderr, "\nusage: %s <command> [...]\n", prog_name); 452 fprintf(stderr, "Try '%s help'\n", prog_name); 453 454 va_end(args); 455 exit(1); 456 } 457 458 static void 459 print_usage(const char *name) 460 { 461 printf("usage: %s [options] <command> [...]\n", name); 462 puts(""); 463 puts("Commands:"); 464 puts(" run_command <name> [args...] Run an IPC command"); 465 puts(""); 466 puts(" get_monitors Get monitor properties"); 467 puts(""); 468 puts(" get_tags Get list of tags"); 469 puts(""); 470 puts(" get_layouts Get list of layouts"); 471 puts(""); 472 puts(" get_dwm_client <window_id> Get dwm client proprties"); 473 puts(""); 474 puts(" subscribe [events...] Subscribe to specified events"); 475 puts(" Options: " IPC_EVENT_TAG_CHANGE ","); 476 puts(" " IPC_EVENT_LAYOUT_CHANGE ","); 477 puts(" " IPC_EVENT_CLIENT_FOCUS_CHANGE ","); 478 puts(" " IPC_EVENT_MONITOR_FOCUS_CHANGE ","); 479 puts(" " IPC_EVENT_FOCUSED_TITLE_CHANGE ","); 480 puts(" " IPC_EVENT_FOCUSED_STATE_CHANGE); 481 puts(""); 482 puts(" help Display this message"); 483 puts(""); 484 puts("Options:"); 485 puts(" --ignore-reply Don't print reply messages from"); 486 puts(" run_command and subscribe."); 487 puts(""); 488 } 489 490 int 491 main(int argc, char *argv[]) 492 { 493 const char *prog_name = argv[0]; 494 495 connect_to_socket(); 496 if (sock_fd == -1) { 497 fprintf(stderr, "Failed to connect to socket\n"); 498 return 1; 499 } 500 501 int i = 1; 502 if (i < argc && strcmp(argv[i], "--ignore-reply") == 0) { 503 ignore_reply = 1; 504 i++; 505 } 506 507 if (i >= argc) usage_error(prog_name, "Expected an argument, got none"); 508 509 if (!argc || strcmp(argv[i], "help") == 0) 510 print_usage(prog_name); 511 else if (strcmp(argv[i], "run_command") == 0) { 512 if (++i >= argc) usage_error(prog_name, "No command specified"); 513 // Command name 514 char *command = argv[i]; 515 // Command arguments are everything after command name 516 char **command_args = argv + ++i; 517 // Number of command arguments 518 int command_argc = argc - i; 519 run_command(command, command_args, command_argc); 520 } else if (strcmp(argv[i], "get_monitors") == 0) { 521 get_monitors(); 522 } else if (strcmp(argv[i], "get_tags") == 0) { 523 get_tags(); 524 } else if (strcmp(argv[i], "get_layouts") == 0) { 525 get_layouts(); 526 } else if (strcmp(argv[i], "get_dwm_client") == 0) { 527 if (++i < argc) { 528 if (is_unsigned_int(argv[i])) { 529 Window win = atol(argv[i]); 530 get_dwm_client(win); 531 } else 532 usage_error(prog_name, "Expected unsigned integer argument"); 533 } else 534 usage_error(prog_name, "Expected the window id"); 535 } else if (strcmp(argv[i], "subscribe") == 0) { 536 if (++i < argc) { 537 for (int j = i; j < argc; j++) subscribe(argv[j]); 538 } else 539 usage_error(prog_name, "Expected event name"); 540 // Keep listening for events forever 541 while (1) { 542 print_socket_reply(); 543 } 544 } else 545 usage_error(prog_name, "Invalid argument '%s'", argv[i]); 546 547 return 0; 548 } 549