summaryrefslogtreecommitdiff
path: root/clients/umbrella-cli/main.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/umbrella-cli/main.c')
-rw-r--r--clients/umbrella-cli/main.c410
1 files changed, 410 insertions, 0 deletions
diff --git a/clients/umbrella-cli/main.c b/clients/umbrella-cli/main.c
new file mode 100644
index 0000000..69357e4
--- /dev/null
+++ b/clients/umbrella-cli/main.c
@@ -0,0 +1,410 @@
+/*
+ * umbrella-cli: command-line client for the umbrella daemon.
+ *
+ * Usage:
+ * umbrella-cli list
+ * umbrella-cli status <unit>
+ * umbrella-cli attach <unit> # interactive console session
+ * umbrella-cli input <unit> <cmd> # send a single command
+ * umbrella-cli action <unit> <action>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/select.h>
+#include <arpa/inet.h>
+
+#define SOCK_PATH "/run/umbrella/umbrella.sock"
+#define PROTO_MAX 65536
+#define HDR_SIZE 4
+
+/* ── Connection ──────────────────────────────────────────────────────────── */
+
+static int connect_daemon(void) {
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0) { perror("socket"); return -1; }
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
+
+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
+ if (errno == ENOENT)
+ fprintf(stderr, "umbrella: daemon not running "
+ "(socket not found: %s)\n", SOCK_PATH);
+ else
+ perror("connect");
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+/* ── Protocol ────────────────────────────────────────────────────────────── */
+
+static int send_msg(int fd, const char *json) {
+ uint32_t len = (uint32_t)strlen(json);
+ uint32_t net = htonl(len);
+ if (write(fd, &net, 4) != 4) return -1;
+ if (write(fd, json, len) != (ssize_t)len) return -1;
+ return 0;
+}
+
+static int recv_msg(int fd, char *buf, int buf_size) {
+ uint32_t net;
+ int got = 0;
+ while (got < 4) {
+ int n = read(fd, (uint8_t*)&net + got, 4 - got);
+ if (n <= 0) return -1;
+ got += n;
+ }
+ uint32_t len = ntohl(net);
+ if (!len || (int)len >= buf_size) return -1;
+ got = 0;
+ while (got < (int)len) {
+ int n = read(fd, buf + got, len - got);
+ if (n <= 0) return -1;
+ got += n;
+ }
+ buf[len] = '\0';
+ return (int)len;
+}
+
+/* ── Minimal JSON helpers ────────────────────────────────────────────────── */
+
+static int jget(const char *json, const char *key, char *out, int size) {
+ char needle[128];
+ snprintf(needle, sizeof(needle), "\"%s\":", key);
+ const char *p = strstr(json, needle);
+ if (!p) return 0;
+ p += strlen(needle);
+ while (*p == ' ') p++;
+ if (*p == '"') {
+ p++;
+ int i = 0;
+ while (*p && *p != '"' && i < size - 1) {
+ if (*p == '\\' && *(p+1)) {
+ p++;
+ switch (*p) {
+ case 'n': out[i++] = '\n'; break;
+ case 'r': out[i++] = '\r'; break;
+ case 't': out[i++] = '\t'; break;
+ case '"': out[i++] = '"'; break;
+ default: out[i++] = *p; break;
+ }
+ p++;
+ } else {
+ out[i++] = *p++;
+ }
+ }
+ out[i] = '\0';
+ return 1;
+ } else {
+ int i = 0;
+ while (*p && *p != ',' && *p != '}' && i < size - 1)
+ out[i++] = *p++;
+ out[i] = '\0';
+ return i > 0;
+ }
+}
+
+/* ── Commands ────────────────────────────────────────────────────────────── */
+
+static int cmd_list(int fd) {
+ if (send_msg(fd, "{\"cmd\":\"list\"}") != 0) return 1;
+
+ char buf[PROTO_MAX];
+ if (recv_msg(fd, buf, sizeof(buf)) <= 0) return 1;
+
+ /* Simple display: find all "name"/"display"/"state" triplets */
+ printf("%-24s %-32s %s\n", "NAME", "DISPLAY", "STATE");
+ printf("%-24s %-32s %s\n",
+ "────────────────────────",
+ "────────────────────────────────",
+ "───────");
+
+ /* Walk the units array manually */
+ const char *p = buf;
+ while ((p = strstr(p, "\"name\":")) != NULL) {
+ char name[64] = {0}, display[128] = {0}, state[32] = {0};
+
+ /* Extract from this position forward */
+ char snippet[512];
+ strncpy(snippet, p, sizeof(snippet) - 1);
+
+ jget(snippet, "name", name, sizeof(name));
+ jget(snippet, "display", display, sizeof(display));
+ jget(snippet, "state", state, sizeof(state));
+
+ if (name[0])
+ printf("%-24s %-32s %s\n", name, display, state);
+
+ p++; /* advance past current match */
+ }
+ return 0;
+}
+
+static int cmd_status(int fd, const char *unit) {
+ char msg[256];
+ snprintf(msg, sizeof(msg),
+ "{\"cmd\":\"status\",\"unit\":\"%s\"}", unit);
+ if (send_msg(fd, msg) != 0) return 1;
+
+ char buf[PROTO_MAX];
+ if (recv_msg(fd, buf, sizeof(buf)) <= 0) return 1;
+
+ char type[32] = {0};
+ jget(buf, "type", type, sizeof(type));
+ if (strcmp(type, "error") == 0) {
+ char errmsg[256] = {0};
+ jget(buf, "message", errmsg, sizeof(errmsg));
+ fprintf(stderr, "Error: %s\n", errmsg);
+ return 1;
+ }
+
+ char display[128] = {0}, state[32] = {0}, pid[16] = {0};
+ jget(buf, "display", display, sizeof(display));
+ jget(buf, "state", state, sizeof(state));
+ jget(buf, "pid", pid, sizeof(pid));
+
+ printf("Unit : %s\n", unit);
+ printf("Display : %s\n", display);
+ printf("State : %s\n", state);
+ if (pid[0] && strcmp(pid, "0") != 0)
+ printf("PID : %s\n", pid);
+ return 0;
+}
+
+static int cmd_input(int fd, const char *unit, const char *input) {
+ char msg[1024];
+ snprintf(msg, sizeof(msg),
+ "{\"cmd\":\"input\",\"unit\":\"%s\",\"data\":\"%s\\n\"}",
+ unit, input);
+ if (send_msg(fd, msg) != 0) return 1;
+
+ char buf[PROTO_MAX];
+ if (recv_msg(fd, buf, sizeof(buf)) <= 0) return 1;
+
+ /* Print any output that came back */
+ char type[32] = {0};
+ jget(buf, "type", type, sizeof(type));
+ if (strcmp(type, "output") == 0) {
+ char data[4096] = {0};
+ jget(buf, "data", data, sizeof(data));
+ printf("%s\n", data);
+ } else if (strcmp(type, "error") == 0) {
+ char errmsg[256] = {0};
+ jget(buf, "message", errmsg, sizeof(errmsg));
+ fprintf(stderr, "Error: %s\n", errmsg);
+ return 1;
+ }
+ return 0;
+}
+
+static int cmd_action(int fd, const char *unit, const char *action) {
+ char msg[256];
+ snprintf(msg, sizeof(msg),
+ "{\"cmd\":\"action\",\"unit\":\"%s\",\"action\":\"%s\"}",
+ unit, action);
+ if (send_msg(fd, msg) != 0) return 1;
+
+ char buf[PROTO_MAX];
+ if (recv_msg(fd, buf, sizeof(buf)) <= 0) return 1;
+
+ char type[32] = {0};
+ jget(buf, "type", type, sizeof(type));
+ if (strcmp(type, "error") == 0) {
+ char errmsg[256] = {0};
+ jget(buf, "message", errmsg, sizeof(errmsg));
+ fprintf(stderr, "Error: %s\n", errmsg);
+ return 1;
+ }
+ printf("Action '%s' dispatched for unit '%s'\n", action, unit);
+ return 0;
+}
+
+/*
+ * cmd_attach: interactive console session.
+ * Attaches to the unit, receives live output, sends typed input.
+ * Ctrl+D or Ctrl+C to detach.
+ */
+static int cmd_attach(int fd, const char *unit) {
+ char msg[256];
+ snprintf(msg, sizeof(msg),
+ "{\"cmd\":\"attach\",\"unit\":\"%s\"}", unit);
+ if (send_msg(fd, msg) != 0) return 1;
+
+ /* Set terminal to raw mode so we get input character by character */
+ struct termios orig, raw;
+ int is_tty = isatty(STDIN_FILENO);
+ if (is_tty) {
+ tcgetattr(STDIN_FILENO, &orig);
+ raw = orig;
+ raw.c_lflag &= ~(ICANON | ECHO);
+ raw.c_cc[VMIN] = 1;
+ raw.c_cc[VTIME] = 0;
+ tcsetattr(STDIN_FILENO, TCSANOW, &raw);
+ }
+
+ printf("── Attached to %s (Ctrl+D to detach) ──\n", unit);
+ char drain[PROTO_MAX];
+ recv_msg(fd, drain, sizeof(drain));
+ fflush(stdout);
+
+ char input_buf[1024];
+ int input_pos = 0;
+ int ret = 0;
+
+ while (1) {
+ fd_set rfds;
+ FD_ZERO(&rfds);
+ FD_SET(fd, &rfds); /* daemon messages */
+ FD_SET(STDIN_FILENO, &rfds); /* keyboard input */
+ int maxfd = fd > STDIN_FILENO ? fd : STDIN_FILENO;
+
+ int n = select(maxfd + 1, &rfds, NULL, NULL, NULL);
+ if (n < 0) {
+ if (errno == EINTR) continue;
+ break;
+ }
+
+ /* Daemon sent us something */
+ if (FD_ISSET(fd, &rfds)) {
+ char buf[PROTO_MAX];
+ int r = recv_msg(fd, buf, sizeof(buf));
+ if (r <= 0) break;
+
+ char type[32] = {0};
+ jget(buf, "type", type, sizeof(type));
+
+ if (strcmp(type, "output") == 0) {
+ char data[PROTO_MAX] = {0};
+ jget(buf, "data", data, sizeof(data));
+ /* Print output, restore cursor line for input */
+ printf("%s", data);
+ fflush(stdout);
+ } else if (strcmp(type, "error") == 0) {
+ char errmsg[256] = {0};
+ jget(buf, "message", errmsg, sizeof(errmsg));
+ fprintf(stderr, "\nError: %s\n", errmsg);
+ ret = 1;
+ break;
+ }
+ }
+
+ /* User typed something */
+ if (FD_ISSET(STDIN_FILENO, &rfds)) {
+ char ch;
+ ssize_t r = read(STDIN_FILENO, &ch, 1);
+ if (r <= 0) break;
+
+ if (ch == 4) break; /* Ctrl+D — detach */
+
+ if (ch == '\n' || ch == '\r') {
+ /* Send the buffered line to the unit */
+ input_buf[input_pos] = '\0';
+ printf("\n");
+
+ char send_buf[1200];
+ /* JSON-escape the input */
+ char escaped[1100];
+ int j = 0;
+ for (int i = 0; input_buf[i] && j < 1090; i++) {
+ if (input_buf[i] == '"') { escaped[j++] = '\\'; escaped[j++] = '"'; }
+ else if (input_buf[i] == '\\') { escaped[j++] = '\\'; escaped[j++] = '\\'; }
+ else escaped[j++] = input_buf[i];
+ }
+ escaped[j] = '\0';
+
+ snprintf(send_buf, sizeof(send_buf),
+ "{\"cmd\":\"input\",\"unit\":\"%s\","
+ "\"data\":\"%s\\n\"}", unit, escaped);
+ send_msg(fd, send_buf);
+ input_pos = 0;
+ } else if (ch == 127 || ch == '\b') {
+ /* Backspace */
+ if (input_pos > 0) {
+ input_pos--;
+ printf("\b \b");
+ fflush(stdout);
+ }
+ } else {
+ if (input_pos < (int)sizeof(input_buf) - 1) {
+ input_buf[input_pos++] = ch;
+ /* Echo the character */
+ write(STDOUT_FILENO, &ch, 1);
+ }
+ }
+ }
+ }
+
+ /* Restore terminal */
+ if (is_tty)
+ tcsetattr(STDIN_FILENO, TCSANOW, &orig);
+
+ /* Send detach */
+ send_msg(fd, "{\"cmd\":\"detach\"}");
+ printf("\n── Detached ──\n");
+ return ret;
+}
+
+/* ── Entry point ─────────────────────────────────────────────────────────── */
+
+static void usage(void) {
+ fprintf(stderr,
+ "Usage: umbrella-cli <command> [args]\n"
+ "\n"
+ "Commands:\n"
+ " list List all units\n"
+ " status <unit> Show unit status\n"
+ " attach <unit> Interactive console session\n"
+ " input <unit> <cmd> Send a single command\n"
+ " action <unit> <action> Run a named action\n"
+ "\n"
+ "Examples:\n"
+ " umbrella-cli list\n"
+ " umbrella-cli attach tf2-novemen\n"
+ " umbrella-cli input tf2-novemen \"say Server restarting soon\"\n"
+ " umbrella-cli action tf2-novemen update\n");
+}
+
+int main(int argc, char *argv[]) {
+ if (argc < 2) { usage(); return 1; }
+
+ int fd = connect_daemon();
+ if (fd < 0) return 1;
+
+ int ret = 0;
+ const char *cmd = argv[1];
+
+ if (strcmp(cmd, "list") == 0) {
+ ret = cmd_list(fd);
+ } else if (strcmp(cmd, "status") == 0) {
+ if (argc < 3) { fprintf(stderr, "status: need unit name\n"); ret = 1; }
+ else ret = cmd_status(fd, argv[2]);
+ } else if (strcmp(cmd, "attach") == 0) {
+ if (argc < 3) { fprintf(stderr, "attach: need unit name\n"); ret = 1; }
+ else ret = cmd_attach(fd, argv[2]);
+ } else if (strcmp(cmd, "input") == 0) {
+ if (argc < 4) { fprintf(stderr, "input: need unit and command\n"); ret = 1; }
+ else ret = cmd_input(fd, argv[2], argv[3]);
+ } else if (strcmp(cmd, "action") == 0) {
+ if (argc < 4) { fprintf(stderr, "action: need unit and action name\n"); ret = 1; }
+ else ret = cmd_action(fd, argv[2], argv[3]);
+ } else {
+ fprintf(stderr, "Unknown command: %s\n\n", cmd);
+ usage();
+ ret = 1;
+ }
+
+ close(fd);
+ return ret;
+}