549 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			549 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| #include <ctype.h>
 | |
| #include <errno.h>
 | |
| #include <inttypes.h>
 | |
| #include <stdarg.h>
 | |
| #include <stdint.h>
 | |
| #include <stdio.h>
 | |
| #include <stdlib.h>
 | |
| #include <string.h>
 | |
| #include <sys/socket.h>
 | |
| #include <sys/un.h>
 | |
| #include <unistd.h>
 | |
| #include <yajl/yajl_gen.h>
 | |
| 
 | |
| #define IPC_MAGIC "DWM-IPC"
 | |
| // clang-format off
 | |
| #define IPC_MAGIC_ARR { 'D', 'W', 'M', '-', 'I', 'P', 'C' }
 | |
| // clang-format on
 | |
| #define IPC_MAGIC_LEN 7  // Not including null char
 | |
| 
 | |
| #define IPC_EVENT_TAG_CHANGE "tag_change_event"
 | |
| #define IPC_EVENT_CLIENT_FOCUS_CHANGE "client_focus_change_event"
 | |
| #define IPC_EVENT_LAYOUT_CHANGE "layout_change_event"
 | |
| #define IPC_EVENT_MONITOR_FOCUS_CHANGE "monitor_focus_change_event"
 | |
| #define IPC_EVENT_FOCUSED_TITLE_CHANGE "focused_title_change_event"
 | |
| #define IPC_EVENT_FOCUSED_STATE_CHANGE "focused_state_change_event"
 | |
| 
 | |
| #define YSTR(str) yajl_gen_string(gen, (unsigned char *)str, strlen(str))
 | |
| #define YINT(num) yajl_gen_integer(gen, num)
 | |
| #define YDOUBLE(num) yajl_gen_double(gen, num)
 | |
| #define YBOOL(v) yajl_gen_bool(gen, v)
 | |
| #define YNULL() yajl_gen_null(gen)
 | |
| #define YARR(body)                                                             \
 | |
|   {                                                                            \
 | |
|     yajl_gen_array_open(gen);                                                  \
 | |
|     body;                                                                      \
 | |
|     yajl_gen_array_close(gen);                                                 \
 | |
|   }
 | |
| #define YMAP(body)                                                             \
 | |
|   {                                                                            \
 | |
|     yajl_gen_map_open(gen);                                                    \
 | |
|     body;                                                                      \
 | |
|     yajl_gen_map_close(gen);                                                   \
 | |
|   }
 | |
| 
 | |
| typedef unsigned long Window;
 | |
| 
 | |
| const char *DEFAULT_SOCKET_PATH = "/tmp/dwm.sock";
 | |
| static int sock_fd = -1;
 | |
| static unsigned int ignore_reply = 0;
 | |
| 
 | |
| typedef enum IPCMessageType {
 | |
|   IPC_TYPE_RUN_COMMAND = 0,
 | |
|   IPC_TYPE_GET_MONITORS = 1,
 | |
|   IPC_TYPE_GET_TAGS = 2,
 | |
|   IPC_TYPE_GET_LAYOUTS = 3,
 | |
|   IPC_TYPE_GET_DWM_CLIENT = 4,
 | |
|   IPC_TYPE_SUBSCRIBE = 5,
 | |
|   IPC_TYPE_EVENT = 6
 | |
| } IPCMessageType;
 | |
| 
 | |
| // Every IPC message must begin with this
 | |
| typedef struct dwm_ipc_header {
 | |
|   uint8_t magic[IPC_MAGIC_LEN];
 | |
|   uint32_t size;
 | |
|   uint8_t type;
 | |
| } __attribute((packed)) dwm_ipc_header_t;
 | |
| 
 | |
| static int
 | |
| recv_message(uint8_t *msg_type, uint32_t *reply_size, uint8_t **reply)
 | |
| {
 | |
|   uint32_t read_bytes = 0;
 | |
|   const int32_t to_read = sizeof(dwm_ipc_header_t);
 | |
|   char header[to_read];
 | |
|   char *walk = header;
 | |
| 
 | |
|   // Try to read header
 | |
|   while (read_bytes < to_read) {
 | |
|     ssize_t n = read(sock_fd, header + read_bytes, to_read - read_bytes);
 | |
| 
 | |
|     if (n == 0) {
 | |
|       if (read_bytes == 0) {
 | |
|         fprintf(stderr, "Unexpectedly reached EOF while reading header.");
 | |
|         fprintf(stderr,
 | |
|                 "Read %" PRIu32 " bytes, expected %" PRIu32 " total bytes.\n",
 | |
|                 read_bytes, to_read);
 | |
|         return -2;
 | |
|       } else {
 | |
|         fprintf(stderr, "Unexpectedly reached EOF while reading header.");
 | |
|         fprintf(stderr,
 | |
|                 "Read %" PRIu32 " bytes, expected %" PRIu32 " total bytes.\n",
 | |
|                 read_bytes, to_read);
 | |
|         return -3;
 | |
|       }
 | |
|     } else if (n == -1) {
 | |
|       return -1;
 | |
|     }
 | |
| 
 | |
|     read_bytes += n;
 | |
|   }
 | |
| 
 | |
|   // Check if magic string in header matches
 | |
|   if (memcmp(walk, IPC_MAGIC, IPC_MAGIC_LEN) != 0) {
 | |
|     fprintf(stderr, "Invalid magic string. Got '%.*s', expected '%s'\n",
 | |
|             IPC_MAGIC_LEN, walk, IPC_MAGIC);
 | |
|     return -3;
 | |
|   }
 | |
| 
 | |
|   walk += IPC_MAGIC_LEN;
 | |
| 
 | |
|   // Extract reply size
 | |
|   memcpy(reply_size, walk, sizeof(uint32_t));
 | |
|   walk += sizeof(uint32_t);
 | |
| 
 | |
|   // Extract message type
 | |
|   memcpy(msg_type, walk, sizeof(uint8_t));
 | |
|   walk += sizeof(uint8_t);
 | |
| 
 | |
|   (*reply) = malloc(*reply_size);
 | |
| 
 | |
|   // Extract payload
 | |
|   read_bytes = 0;
 | |
|   while (read_bytes < *reply_size) {
 | |
|     ssize_t n = read(sock_fd, *reply + read_bytes, *reply_size - read_bytes);
 | |
| 
 | |
|     if (n == 0) {
 | |
|       fprintf(stderr, "Unexpectedly reached EOF while reading payload.");
 | |
|       fprintf(stderr, "Read %" PRIu32 " bytes, expected %" PRIu32 " bytes.\n",
 | |
|               read_bytes, *reply_size);
 | |
|       free(*reply);
 | |
|       return -2;
 | |
|     } else if (n == -1) {
 | |
|       if (errno == EINTR || errno == EAGAIN) continue;
 | |
|       free(*reply);
 | |
|       return -1;
 | |
|     }
 | |
| 
 | |
|     read_bytes += n;
 | |
|   }
 | |
| 
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| read_socket(IPCMessageType *msg_type, uint32_t *msg_size, char **msg)
 | |
| {
 | |
|   int ret = -1;
 | |
| 
 | |
|   while (ret != 0) {
 | |
|     ret = recv_message((uint8_t *)msg_type, msg_size, (uint8_t **)msg);
 | |
| 
 | |
|     if (ret < 0) {
 | |
|       // Try again (non-fatal error)
 | |
|       if (ret == -1 && (errno == EINTR || errno == EAGAIN)) continue;
 | |
| 
 | |
|       fprintf(stderr, "Error receiving response from socket. ");
 | |
|       fprintf(stderr, "The connection might have been lost.\n");
 | |
|       exit(2);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static ssize_t
 | |
| write_socket(const void *buf, size_t count)
 | |
| {
 | |
|   size_t written = 0;
 | |
| 
 | |
|   while (written < count) {
 | |
|     const ssize_t n =
 | |
|         write(sock_fd, ((uint8_t *)buf) + written, count - written);
 | |
| 
 | |
|     if (n == -1) {
 | |
|       if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
 | |
|         continue;
 | |
|       else
 | |
|         return n;
 | |
|     }
 | |
|     written += n;
 | |
|   }
 | |
|   return written;
 | |
| }
 | |
| 
 | |
| static void
 | |
| connect_to_socket()
 | |
| {
 | |
|   struct sockaddr_un addr;
 | |
| 
 | |
|   int sock = socket(AF_UNIX, SOCK_STREAM, 0);
 | |
| 
 | |
|   // Initialize struct to 0
 | |
|   memset(&addr, 0, sizeof(struct sockaddr_un));
 | |
| 
 | |
|   addr.sun_family = AF_UNIX;
 | |
|   strcpy(addr.sun_path, DEFAULT_SOCKET_PATH);
 | |
| 
 | |
|   connect(sock, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un));
 | |
| 
 | |
|   sock_fd = sock;
 | |
| }
 | |
| 
 | |
| static int
 | |
| send_message(IPCMessageType msg_type, uint32_t msg_size, uint8_t *msg)
 | |
| {
 | |
|   dwm_ipc_header_t header = {
 | |
|       .magic = IPC_MAGIC_ARR, .size = msg_size, .type = msg_type};
 | |
| 
 | |
|   size_t header_size = sizeof(dwm_ipc_header_t);
 | |
|   size_t total_size = header_size + msg_size;
 | |
| 
 | |
|   uint8_t buffer[total_size];
 | |
| 
 | |
|   // Copy header to buffer
 | |
|   memcpy(buffer, &header, header_size);
 | |
|   // Copy message to buffer
 | |
|   memcpy(buffer + header_size, msg, header.size);
 | |
| 
 | |
|   write_socket(buffer, total_size);
 | |
| 
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| is_float(const char *s)
 | |
| {
 | |
|   size_t len = strlen(s);
 | |
|   int is_dot_used = 0;
 | |
|   int is_minus_used = 0;
 | |
| 
 | |
|   // Floats can only have one decimal point in between or digits
 | |
|   // Optionally, floats can also be below zero (negative)
 | |
|   for (int i = 0; i < len; i++) {
 | |
|     if (isdigit(s[i]))
 | |
|       continue;
 | |
|     else if (!is_dot_used && s[i] == '.' && i != 0 && i != len - 1) {
 | |
|       is_dot_used = 1;
 | |
|       continue;
 | |
|     } else if (!is_minus_used && s[i] == '-' && i == 0) {
 | |
|       is_minus_used = 1;
 | |
|       continue;
 | |
|     } else
 | |
|       return 0;
 | |
|   }
 | |
| 
 | |
|   return 1;
 | |
| }
 | |
| 
 | |
| static int
 | |
| is_unsigned_int(const char *s)
 | |
| {
 | |
|   size_t len = strlen(s);
 | |
| 
 | |
|   // Unsigned int can only have digits
 | |
|   for (int i = 0; i < len; i++) {
 | |
|     if (isdigit(s[i]))
 | |
|       continue;
 | |
|     else
 | |
|       return 0;
 | |
|   }
 | |
| 
 | |
|   return 1;
 | |
| }
 | |
| 
 | |
| static int
 | |
| is_signed_int(const char *s)
 | |
| {
 | |
|   size_t len = strlen(s);
 | |
| 
 | |
|   // Signed int can only have digits and a negative sign at the start
 | |
|   for (int i = 0; i < len; i++) {
 | |
|     if (isdigit(s[i]))
 | |
|       continue;
 | |
|     else if (i == 0 && s[i] == '-') {
 | |
|       continue;
 | |
|     } else
 | |
|       return 0;
 | |
|   }
 | |
| 
 | |
|   return 1;
 | |
| }
 | |
| 
 | |
| static void
 | |
| flush_socket_reply()
 | |
| {
 | |
|   IPCMessageType reply_type;
 | |
|   uint32_t reply_size;
 | |
|   char *reply;
 | |
| 
 | |
|   read_socket(&reply_type, &reply_size, &reply);
 | |
| 
 | |
|   free(reply);
 | |
| }
 | |
| 
 | |
| static void
 | |
| print_socket_reply()
 | |
| {
 | |
|   IPCMessageType reply_type;
 | |
|   uint32_t reply_size;
 | |
|   char *reply;
 | |
| 
 | |
|   read_socket(&reply_type, &reply_size, &reply);
 | |
| 
 | |
|   printf("%.*s\n", reply_size, reply);
 | |
|   fflush(stdout);
 | |
|   free(reply);
 | |
| }
 | |
| 
 | |
| static int
 | |
| run_command(const char *name, char *args[], int argc)
 | |
| {
 | |
|   const unsigned char *msg;
 | |
|   size_t msg_size;
 | |
| 
 | |
|   yajl_gen gen = yajl_gen_alloc(NULL);
 | |
| 
 | |
|   // Message format:
 | |
|   // {
 | |
|   //   "command": "<name>",
 | |
|   //   "args": [ ... ]
 | |
|   // }
 | |
|   // clang-format off
 | |
|   YMAP(
 | |
|     YSTR("command"); YSTR(name);
 | |
|     YSTR("args"); YARR(
 | |
|       for (int i = 0; i < argc; i++) {
 | |
|         if (is_signed_int(args[i])) {
 | |
|           long long num = atoll(args[i]);
 | |
|           YINT(num);
 | |
|         } else if (is_float(args[i])) {
 | |
|           float num = atof(args[i]);
 | |
|           YDOUBLE(num);
 | |
|         } else {
 | |
|           YSTR(args[i]);
 | |
|         }
 | |
|       }
 | |
|     )
 | |
|   )
 | |
|   // clang-format on
 | |
| 
 | |
|   yajl_gen_get_buf(gen, &msg, &msg_size);
 | |
| 
 | |
|   send_message(IPC_TYPE_RUN_COMMAND, msg_size, (uint8_t *)msg);
 | |
| 
 | |
|   if (!ignore_reply)
 | |
|     print_socket_reply();
 | |
|   else
 | |
|     flush_socket_reply();
 | |
| 
 | |
|   yajl_gen_free(gen);
 | |
| 
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| get_monitors()
 | |
| {
 | |
|   send_message(IPC_TYPE_GET_MONITORS, 1, (uint8_t *)"");
 | |
|   print_socket_reply();
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| get_tags()
 | |
| {
 | |
|   send_message(IPC_TYPE_GET_TAGS, 1, (uint8_t *)"");
 | |
|   print_socket_reply();
 | |
| 
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| get_layouts()
 | |
| {
 | |
|   send_message(IPC_TYPE_GET_LAYOUTS, 1, (uint8_t *)"");
 | |
|   print_socket_reply();
 | |
| 
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| get_dwm_client(Window win)
 | |
| {
 | |
|   const unsigned char *msg;
 | |
|   size_t msg_size;
 | |
| 
 | |
|   yajl_gen gen = yajl_gen_alloc(NULL);
 | |
| 
 | |
|   // Message format:
 | |
|   // {
 | |
|   //   "client_window_id": "<win>"
 | |
|   // }
 | |
|   // clang-format off
 | |
|   YMAP(
 | |
|     YSTR("client_window_id"); YINT(win);
 | |
|   )
 | |
|   // clang-format on
 | |
| 
 | |
|   yajl_gen_get_buf(gen, &msg, &msg_size);
 | |
| 
 | |
|   send_message(IPC_TYPE_GET_DWM_CLIENT, msg_size, (uint8_t *)msg);
 | |
| 
 | |
|   print_socket_reply();
 | |
| 
 | |
|   yajl_gen_free(gen);
 | |
| 
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static int
 | |
| subscribe(const char *event)
 | |
| {
 | |
|   const unsigned char *msg;
 | |
|   size_t msg_size;
 | |
| 
 | |
|   yajl_gen gen = yajl_gen_alloc(NULL);
 | |
| 
 | |
|   // Message format:
 | |
|   // {
 | |
|   //   "event": "<event>",
 | |
|   //   "action": "subscribe"
 | |
|   // }
 | |
|   // clang-format off
 | |
|   YMAP(
 | |
|     YSTR("event"); YSTR(event);
 | |
|     YSTR("action"); YSTR("subscribe");
 | |
|   )
 | |
|   // clang-format on
 | |
| 
 | |
|   yajl_gen_get_buf(gen, &msg, &msg_size);
 | |
| 
 | |
|   send_message(IPC_TYPE_SUBSCRIBE, msg_size, (uint8_t *)msg);
 | |
| 
 | |
|   if (!ignore_reply)
 | |
|     print_socket_reply();
 | |
|   else
 | |
|     flush_socket_reply();
 | |
| 
 | |
|   yajl_gen_free(gen);
 | |
| 
 | |
|   return 0;
 | |
| }
 | |
| 
 | |
| static void
 | |
| usage_error(const char *prog_name, const char *format, ...)
 | |
| {
 | |
|   va_list args;
 | |
|   va_start(args, format);
 | |
| 
 | |
|   fprintf(stderr, "Error: ");
 | |
|   vfprintf(stderr, format, args);
 | |
|   fprintf(stderr, "\nusage: %s <command> [...]\n", prog_name);
 | |
|   fprintf(stderr, "Try '%s help'\n", prog_name);
 | |
| 
 | |
|   va_end(args);
 | |
|   exit(1);
 | |
| }
 | |
| 
 | |
| static void
 | |
| print_usage(const char *name)
 | |
| {
 | |
|   printf("usage: %s [options] <command> [...]\n", name);
 | |
|   puts("");
 | |
|   puts("Commands:");
 | |
|   puts("  run_command <name> [args...]    Run an IPC command");
 | |
|   puts("");
 | |
|   puts("  get_monitors                    Get monitor properties");
 | |
|   puts("");
 | |
|   puts("  get_tags                        Get list of tags");
 | |
|   puts("");
 | |
|   puts("  get_layouts                     Get list of layouts");
 | |
|   puts("");
 | |
|   puts("  get_dwm_client <window_id>      Get dwm client proprties");
 | |
|   puts("");
 | |
|   puts("  subscribe [events...]           Subscribe to specified events");
 | |
|   puts("                                  Options: " IPC_EVENT_TAG_CHANGE ",");
 | |
|   puts("                                  " IPC_EVENT_LAYOUT_CHANGE ",");
 | |
|   puts("                                  " IPC_EVENT_CLIENT_FOCUS_CHANGE ",");
 | |
|   puts("                                  " IPC_EVENT_MONITOR_FOCUS_CHANGE ",");
 | |
|   puts("                                  " IPC_EVENT_FOCUSED_TITLE_CHANGE ",");
 | |
|   puts("                                  " IPC_EVENT_FOCUSED_STATE_CHANGE);
 | |
|   puts("");
 | |
|   puts("  help                            Display this message");
 | |
|   puts("");
 | |
|   puts("Options:");
 | |
|   puts("  --ignore-reply                  Don't print reply messages from");
 | |
|   puts("                                  run_command and subscribe.");
 | |
|   puts("");
 | |
| }
 | |
| 
 | |
| int
 | |
| main(int argc, char *argv[])
 | |
| {
 | |
|   const char *prog_name = argv[0];
 | |
| 
 | |
|   connect_to_socket();
 | |
|   if (sock_fd == -1) {
 | |
|     fprintf(stderr, "Failed to connect to socket\n");
 | |
|     return 1;
 | |
|   }
 | |
| 
 | |
|   int i = 1;
 | |
|   if (strcmp(argv[i], "--ignore-reply") == 0) {
 | |
|     ignore_reply = 1;
 | |
|     i++;
 | |
|   }
 | |
| 
 | |
|   // if (i >= argc) usage_error(prog_name, "Expected an argument, got none");
 | |
| 
 | |
|   if (!argc || strcmp(argv[i], "help") == 0)
 | |
|     print_usage(prog_name);
 | |
|   else if (strcmp(argv[i], "run_command") == 0) {
 | |
|     if (++i >= argc) usage_error(prog_name, "No command specified");
 | |
|     // Command name
 | |
|     char *command = argv[i];
 | |
|     // Command arguments are everything after command name
 | |
|     char **command_args = argv + ++i;
 | |
|     // Number of command arguments
 | |
|     int command_argc = argc - i;
 | |
|     run_command(command, command_args, command_argc);
 | |
|   } else if (strcmp(argv[i], "get_monitors") == 0) {
 | |
|     get_monitors();
 | |
|   } else if (strcmp(argv[i], "get_tags") == 0) {
 | |
|     get_tags();
 | |
|   } else if (strcmp(argv[i], "get_layouts") == 0) {
 | |
|     get_layouts();
 | |
|   } else if (strcmp(argv[i], "get_dwm_client") == 0) {
 | |
|     if (++i < argc) {
 | |
|       if (is_unsigned_int(argv[i])) {
 | |
|         Window win = atol(argv[i]);
 | |
|         get_dwm_client(win);
 | |
|       } else
 | |
|         usage_error(prog_name, "Expected unsigned integer argument");
 | |
|     } else
 | |
|       usage_error(prog_name, "Expected the window id");
 | |
|   } else if (strcmp(argv[i], "subscribe") == 0) {
 | |
|     if (++i < argc) {
 | |
|       for (int j = i; j < argc; j++) subscribe(argv[j]);
 | |
|     } else
 | |
|       usage_error(prog_name, "Expected event name");
 | |
|     // Keep listening for events forever
 | |
|     while (1) {
 | |
|       print_socket_reply();
 | |
|     }
 | |
|   } else
 | |
|     usage_error(prog_name, "Invalid argument '%s'", argv[i]);
 | |
| 
 | |
|   return 0;
 | |
| }
 |