mirror of
https://github.com/labwc/labwc.git
synced 2026-06-13 14:33:18 -04:00
390 lines
8.7 KiB
C
390 lines
8.7 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* labmsg - IPC client for labwc (swaymsg-compatible interface)
|
|
*/
|
|
#define _POSIX_C_SOURCE 200809L
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <getopt.h>
|
|
#include <json-c/json.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
|
|
#define IPC_MAGIC "labwc-ipc"
|
|
#define IPC_MAGIC_LEN 9
|
|
#define IPC_HEADER_SIZE (IPC_MAGIC_LEN + 4 + 4)
|
|
|
|
/* Message types */
|
|
enum ipc_msg_type {
|
|
IPC_RUN_COMMAND = 0,
|
|
IPC_GET_WORKSPACES = 1,
|
|
IPC_SUBSCRIBE = 2,
|
|
IPC_GET_OUTPUTS = 3,
|
|
IPC_GET_TREE = 4,
|
|
IPC_GET_BAR_CONFIG = 6,
|
|
IPC_GET_VERSION = 7,
|
|
IPC_GET_CONFIG = 9,
|
|
IPC_SEND_TICK = 10,
|
|
IPC_SYNC = 11,
|
|
IPC_GET_INPUTS = 100,
|
|
IPC_GET_SEATS = 101,
|
|
};
|
|
|
|
static const char *version_str = "labmsg 0.1";
|
|
|
|
static const struct option long_options[] = {{"help", no_argument, NULL, 'h'},
|
|
{"monitor", no_argument, NULL, 'm'}, {"pretty", no_argument, NULL, 'p'},
|
|
{"quiet", no_argument, NULL, 'q'}, {"raw", no_argument, NULL, 'r'},
|
|
{"socket", required_argument, NULL, 's'},
|
|
{"type", required_argument, NULL, 't'},
|
|
{"version", no_argument, NULL, 'v'}, {0, 0, 0, 0}};
|
|
|
|
static const char usage_str[] =
|
|
"Usage: labmsg [options] [message]\n"
|
|
"\n"
|
|
"Options:\n"
|
|
" -h, --help Show help and exit\n"
|
|
" -m, --monitor Monitor for events (subscribe mode)\n"
|
|
" -p, --pretty Force pretty-printed JSON output\n"
|
|
" -q, --quiet Suppress response output\n"
|
|
" -r, --raw Force raw JSON output\n"
|
|
" -s, --socket <path> Override socket path\n"
|
|
" -t, --type <type> Message type (default: run_command)\n"
|
|
" -v, --version Show version and exit\n"
|
|
"\n"
|
|
"Message types:\n"
|
|
" run_command, get_workspaces, subscribe, get_outputs,\n"
|
|
" get_tree, get_bar_config, get_version, get_config,\n"
|
|
" send_tick, get_inputs, get_seats\n";
|
|
|
|
static int
|
|
parse_msg_type(const char *name)
|
|
{
|
|
if (!name || !strcmp(name, "run_command")) {
|
|
return IPC_RUN_COMMAND;
|
|
} else if (!strcmp(name, "get_workspaces")) {
|
|
return IPC_GET_WORKSPACES;
|
|
} else if (!strcmp(name, "subscribe")) {
|
|
return IPC_SUBSCRIBE;
|
|
} else if (!strcmp(name, "get_outputs")) {
|
|
return IPC_GET_OUTPUTS;
|
|
} else if (!strcmp(name, "get_tree")) {
|
|
return IPC_GET_TREE;
|
|
} else if (!strcmp(name, "get_bar_config")) {
|
|
return IPC_GET_BAR_CONFIG;
|
|
} else if (!strcmp(name, "get_version")) {
|
|
return IPC_GET_VERSION;
|
|
} else if (!strcmp(name, "get_config")) {
|
|
return IPC_GET_CONFIG;
|
|
} else if (!strcmp(name, "send_tick")) {
|
|
return IPC_SEND_TICK;
|
|
} else if (!strcmp(name, "get_inputs")) {
|
|
return IPC_GET_INPUTS;
|
|
} else if (!strcmp(name, "get_seats")) {
|
|
return IPC_GET_SEATS;
|
|
}
|
|
fprintf(stderr, "Unknown message type: %s\n", name);
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
ipc_connect(const char *socket_path)
|
|
{
|
|
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (fd < 0) {
|
|
perror("socket");
|
|
return -1;
|
|
}
|
|
|
|
struct sockaddr_un addr = {0};
|
|
addr.sun_family = AF_UNIX;
|
|
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
|
|
|
|
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
|
perror("connect");
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
static bool
|
|
ipc_send(int fd, uint32_t type, const char *payload, uint32_t len)
|
|
{
|
|
char header[IPC_HEADER_SIZE];
|
|
memcpy(header, IPC_MAGIC, IPC_MAGIC_LEN);
|
|
memcpy(header + IPC_MAGIC_LEN, &len, sizeof(uint32_t));
|
|
memcpy(header + IPC_MAGIC_LEN + 4, &type, sizeof(uint32_t));
|
|
|
|
if (write(fd, header, IPC_HEADER_SIZE) != IPC_HEADER_SIZE) {
|
|
return false;
|
|
}
|
|
if (len > 0 && write(fd, payload, len) != (ssize_t)len) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
read_exact(int fd, void *buf, size_t count)
|
|
{
|
|
size_t total = 0;
|
|
while (total < count) {
|
|
ssize_t n = read(fd, (char *)buf + total, count - total);
|
|
if (n <= 0) {
|
|
return false;
|
|
}
|
|
total += n;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
ipc_recv(int fd, uint32_t *type, char **payload, uint32_t *len)
|
|
{
|
|
char header[IPC_HEADER_SIZE];
|
|
if (!read_exact(fd, header, IPC_HEADER_SIZE)) {
|
|
return false;
|
|
}
|
|
|
|
if (memcmp(header, IPC_MAGIC, IPC_MAGIC_LEN) != 0) {
|
|
fprintf(stderr, "Invalid IPC response magic\n");
|
|
return false;
|
|
}
|
|
|
|
memcpy(len, header + IPC_MAGIC_LEN, sizeof(uint32_t));
|
|
memcpy(type, header + IPC_MAGIC_LEN + 4, sizeof(uint32_t));
|
|
|
|
if (*len > 0) {
|
|
*payload = malloc(*len + 1);
|
|
if (!*payload) {
|
|
return false;
|
|
}
|
|
if (!read_exact(fd, *payload, *len)) {
|
|
free(*payload);
|
|
*payload = NULL;
|
|
return false;
|
|
}
|
|
(*payload)[*len] = '\0';
|
|
} else {
|
|
*payload = NULL;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
print_json(const char *data, bool pretty)
|
|
{
|
|
struct json_object *obj = json_tokener_parse(data);
|
|
if (!obj) {
|
|
/* Not valid JSON, print as-is */
|
|
printf("%s\n", data);
|
|
return;
|
|
}
|
|
|
|
int flags = JSON_C_TO_STRING_NOSLASHESCAPE
|
|
| (pretty ? (JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED)
|
|
: JSON_C_TO_STRING_PLAIN);
|
|
|
|
printf("%s\n", json_object_to_json_string_ext(obj, flags));
|
|
json_object_put(obj);
|
|
}
|
|
|
|
/* Check if any command in the result array failed */
|
|
static bool
|
|
check_command_success(const char *data)
|
|
{
|
|
struct json_object *obj = json_tokener_parse(data);
|
|
if (!obj) {
|
|
return false;
|
|
}
|
|
|
|
bool success = true;
|
|
if (json_object_get_type(obj) == json_type_array) {
|
|
int len = json_object_array_length(obj);
|
|
for (int i = 0; i < len; i++) {
|
|
struct json_object *item =
|
|
json_object_array_get_idx(obj, i);
|
|
struct json_object *s = NULL;
|
|
if (json_object_object_get_ex(item, "success", &s)) {
|
|
if (!json_object_get_boolean(s)) {
|
|
success = false;
|
|
/* Print error to stderr */
|
|
struct json_object *err = NULL;
|
|
if (json_object_object_get_ex(item,
|
|
"error", &err)) {
|
|
fprintf(stderr, "Error: %s\n",
|
|
json_object_get_string(
|
|
err));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (json_object_get_type(obj) == json_type_object) {
|
|
struct json_object *s = NULL;
|
|
if (json_object_object_get_ex(obj, "success", &s)) {
|
|
success = json_object_get_boolean(s);
|
|
}
|
|
}
|
|
|
|
json_object_put(obj);
|
|
return success;
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
setvbuf(stdout, NULL, _IOLBF, 0);
|
|
|
|
bool monitor = false;
|
|
bool pretty = false;
|
|
bool raw = false;
|
|
bool quiet = false;
|
|
const char *socket_path = NULL;
|
|
const char *type_str = NULL;
|
|
bool force_pretty = false;
|
|
|
|
int c;
|
|
while (1) {
|
|
int index = 0;
|
|
c = getopt_long(argc, argv, "hmpqrs:t:v", long_options, &index);
|
|
if (c == -1) {
|
|
break;
|
|
}
|
|
switch (c) {
|
|
case 'h':
|
|
printf("%s", usage_str);
|
|
return 0;
|
|
case 'm':
|
|
monitor = true;
|
|
break;
|
|
case 'p':
|
|
pretty = true;
|
|
force_pretty = true;
|
|
break;
|
|
case 'q':
|
|
quiet = true;
|
|
break;
|
|
case 'r':
|
|
raw = true;
|
|
break;
|
|
case 's':
|
|
socket_path = optarg;
|
|
break;
|
|
case 't':
|
|
type_str = optarg;
|
|
break;
|
|
case 'v':
|
|
printf("%s\n", version_str);
|
|
return 0;
|
|
default:
|
|
fprintf(stderr, "%s", usage_str);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* Auto-detect pretty mode */
|
|
if (!force_pretty && !raw) {
|
|
pretty = isatty(STDOUT_FILENO);
|
|
}
|
|
|
|
/* Determine message type */
|
|
int msg_type = parse_msg_type(type_str);
|
|
if (msg_type < 0) {
|
|
return 1;
|
|
}
|
|
|
|
/* Build payload from remaining args */
|
|
char *payload = NULL;
|
|
if (optind < argc) {
|
|
/* Concatenate remaining args with spaces */
|
|
size_t total = 0;
|
|
for (int i = optind; i < argc; i++) {
|
|
total += strlen(argv[i]) + 1;
|
|
}
|
|
payload = malloc(total);
|
|
payload[0] = '\0';
|
|
for (int i = optind; i < argc; i++) {
|
|
if (i > optind) {
|
|
strcat(payload, " ");
|
|
}
|
|
strcat(payload, argv[i]);
|
|
}
|
|
}
|
|
|
|
/* Get socket path */
|
|
if (!socket_path) {
|
|
socket_path = getenv("LABWC_IPC_SOCK");
|
|
}
|
|
if (!socket_path) {
|
|
fprintf(stderr, "LABWC_IPC_SOCK not set and no -s option\n");
|
|
free(payload);
|
|
return 1;
|
|
}
|
|
|
|
int fd = ipc_connect(socket_path);
|
|
if (fd < 0) {
|
|
fprintf(stderr, "Failed to connect to %s\n", socket_path);
|
|
free(payload);
|
|
return 1;
|
|
}
|
|
|
|
/* Send message */
|
|
uint32_t payload_len = payload ? strlen(payload) : 0;
|
|
if (!ipc_send(fd, msg_type, payload, payload_len)) {
|
|
fprintf(stderr, "Failed to send IPC message\n");
|
|
close(fd);
|
|
free(payload);
|
|
return 1;
|
|
}
|
|
free(payload);
|
|
|
|
/* Receive response */
|
|
uint32_t resp_type = 0;
|
|
char *resp_payload = NULL;
|
|
uint32_t resp_len = 0;
|
|
|
|
if (!ipc_recv(fd, &resp_type, &resp_payload, &resp_len)) {
|
|
fprintf(stderr, "Failed to receive IPC response\n");
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
|
|
int exit_code = 0;
|
|
|
|
if (!quiet && resp_payload) {
|
|
print_json(resp_payload, pretty);
|
|
}
|
|
|
|
/* Check for server-reported errors */
|
|
if (resp_payload && msg_type == IPC_RUN_COMMAND) {
|
|
if (!check_command_success(resp_payload)) {
|
|
exit_code = 2;
|
|
}
|
|
}
|
|
|
|
/* Monitor mode: keep reading events */
|
|
if (monitor) {
|
|
free(resp_payload);
|
|
while (1) {
|
|
resp_payload = NULL;
|
|
if (!ipc_recv(fd, &resp_type, &resp_payload,
|
|
&resp_len)) {
|
|
break;
|
|
}
|
|
if (!quiet && resp_payload) {
|
|
print_json(resp_payload, pretty);
|
|
}
|
|
free(resp_payload);
|
|
}
|
|
}
|
|
|
|
free(resp_payload);
|
|
close(fd);
|
|
return exit_code;
|
|
}
|