Monorepo for Aesthetic.Computer aesthetic.computer
at main 454 lines 16 kB view raw
1// machines.c — System-level AC Machines monitoring daemon 2// Manages a WebSocket connection to session-server for device registration, 3// heartbeats, log upload, and remote command reception. 4 5#include "machines.h" 6#include "drm-display.h" 7#include "swank-bridge.h" 8 9#include <stdio.h> 10#include <stdlib.h> 11#include <string.h> 12#include <time.h> 13#include <unistd.h> 14#include <sys/reboot.h> 15#include <linux/reboot.h> 16 17extern void ac_log(const char *fmt, ...); 18extern char g_machine_id[64]; 19 20// Compile-time build info (set by Makefile) 21#ifndef AC_BUILD_NAME 22#define AC_BUILD_NAME "dev" 23#endif 24#ifndef AC_GIT_HASH 25#define AC_GIT_HASH "unknown" 26#endif 27#ifndef AC_BUILD_TS 28#define AC_BUILD_TS "unknown" 29#endif 30 31#define WS_URL "wss://session-server.aesthetic.computer/machines" 32 33// ── Helpers ────────────────────────────────────────────────── 34 35static int read_file(const char *path, char *buf, int bufsize) { 36 FILE *f = fopen(path, "r"); 37 if (!f) return -1; 38 int len = (int)fread(buf, 1, bufsize - 1, f); 39 fclose(f); 40 if (len > 0 && buf[len - 1] == '\n') len--; 41 buf[len] = 0; 42 return len; 43} 44 45static void read_battery(int *percent, int *charging) { 46 char buf[64]; 47 *percent = -1; 48 *charging = 0; 49 const char *bat_names[] = {"BAT0", "BAT1", NULL}; 50 for (int i = 0; bat_names[i]; i++) { 51 char path[128]; 52 snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity", bat_names[i]); 53 if (read_file(path, buf, sizeof(buf)) > 0) { 54 *percent = atoi(buf); 55 snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status", bat_names[i]); 56 if (read_file(path, buf, sizeof(buf)) > 0) 57 *charging = (strcmp(buf, "Charging") == 0); 58 return; 59 } 60 } 61} 62 63// Simple JSON string extraction: find "key":"value" and copy value to out. 64static int json_get_str(const char *json, const char *key, char *out, int out_sz) { 65 char needle[128]; 66 snprintf(needle, sizeof(needle), "\"%s\":\"", key); 67 const char *p = strstr(json, needle); 68 if (!p) return -1; 69 p += strlen(needle); 70 const char *end = strchr(p, '"'); 71 if (!end) return -1; 72 int len = (int)(end - p); 73 if (len >= out_sz) len = out_sz - 1; 74 memcpy(out, p, len); 75 out[len] = 0; 76 return len; 77} 78 79// Escape a string for JSON embedding (backslash, quotes, control chars). 80// Returns number of bytes written (excluding null terminator). 81static int json_escape(const char *in, int in_len, char *out, int out_sz) { 82 int j = 0; 83 for (int i = 0; i < in_len && j < out_sz - 2; i++) { 84 unsigned char c = (unsigned char)in[i]; 85 if (c == '"' || c == '\\') { 86 if (j + 2 >= out_sz) break; 87 out[j++] = '\\'; 88 out[j++] = c; 89 } else if (c == '\n') { 90 if (j + 2 >= out_sz) break; 91 out[j++] = '\\'; 92 out[j++] = 'n'; 93 } else if (c == '\r') { 94 if (j + 2 >= out_sz) break; 95 out[j++] = '\\'; 96 out[j++] = 'r'; 97 } else if (c == '\t') { 98 if (j + 2 >= out_sz) break; 99 out[j++] = '\\'; 100 out[j++] = 't'; 101 } else if (c < 0x20) { 102 // Skip other control chars 103 } else { 104 out[j++] = c; 105 } 106 } 107 out[j] = 0; 108 return j; 109} 110 111// ── Send queue (drains one per frame when ws slot is free) ──── 112 113static void sq_push(ACMachines *m, const char *msg) { 114 if (m->sq_count >= MACHINES_SEND_QUEUE_SIZE) { 115 ac_log("[machines] send queue full, dropping message\n"); 116 return; 117 } 118 int slot = (m->sq_head + m->sq_count) % MACHINES_SEND_QUEUE_SIZE; 119 strncpy(m->send_queue[slot], msg, MACHINES_SEND_MSG_SIZE - 1); 120 m->send_queue[slot][MACHINES_SEND_MSG_SIZE - 1] = 0; 121 m->sq_count++; 122} 123 124static void sq_drain_one(ACMachines *m) { 125 if (m->sq_count <= 0 || !m->ws) return; 126 // Only send if ws send slot is free 127 if (m->ws->send_pending) return; 128 ws_send(m->ws, m->send_queue[m->sq_head]); 129 m->sq_head = (m->sq_head + 1) % MACHINES_SEND_QUEUE_SIZE; 130 m->sq_count--; 131} 132 133// ── Connection ─────────────────────────────────────────────── 134 135static void machines_connect(ACMachines *m) { 136 char url[768]; 137 const char *mid = g_machine_id[0] ? g_machine_id : "unknown"; 138 if (m->device_token[0]) { 139 snprintf(url, sizeof(url), "%s?role=device&machineId=%s&token=%s", 140 WS_URL, mid, m->device_token); 141 } else { 142 snprintf(url, sizeof(url), "%s?role=device&machineId=%s", WS_URL, mid); 143 } 144 ws_connect(m->ws, url); 145 m->connected = 0; 146 ac_log("[machines] connecting: %s\n", mid); 147} 148 149// ── Register (sent on connect) ─────────────────────────────── 150 151static void send_register(ACMachines *m, ACWifi *wifi) { 152 int bat_pct, bat_chg; 153 read_battery(&bat_pct, &bat_chg); 154 155 char hw[128] = ""; 156 read_file("/sys/devices/virtual/dmi/id/product_name", hw, sizeof(hw)); 157 158 char hostname[64] = ""; 159 read_file("/etc/hostname", hostname, sizeof(hostname)); 160 161 // Display driver 162 extern void *g_display; 163 const char *drv = "unknown"; 164 if (g_display) { 165 drv = drm_display_driver((ACDisplay *)g_display); 166 } else if (getenv("WAYLAND_DISPLAY")) { 167 drv = "wayland"; 168 } 169 170 // GPU name from sysfs 171 char gpu_name[128] = "unknown"; 172 { 173 char tmp[128] = ""; 174 if (read_file("/sys/kernel/debug/dri/0/name", tmp, sizeof(tmp)) > 0 || 175 read_file("/sys/class/drm/card0/device/label", tmp, sizeof(tmp)) > 0) { 176 snprintf(gpu_name, sizeof(gpu_name), "%s", tmp); 177 } 178 } 179 180 char msg[2048]; 181 snprintf(msg, sizeof(msg), 182 "{\"type\":\"register\"," 183 "\"version\":\"%s %s-%s\"," 184 "\"buildName\":\"%s\"," 185 "\"gitHash\":\"%s\"," 186 "\"buildTs\":\"%s\"," 187 "\"currentPiece\":\"%s\"," 188 "\"ip\":\"%s\"," 189 "\"wifiSSID\":\"%s\"," 190 "\"battery\":%d," 191 "\"charging\":%s," 192 "\"hostname\":\"%s\"," 193 "\"hw\":{\"display\":\"%s\",\"displayDriver\":\"%s\",\"gpu\":\"%s\"}}", 194 AC_BUILD_NAME, AC_GIT_HASH, AC_BUILD_TS, 195 AC_BUILD_NAME, AC_GIT_HASH, AC_BUILD_TS, 196 m->current_piece, 197 wifi ? wifi->ip_address : "", 198 wifi ? wifi->connected_ssid : "", 199 bat_pct, 200 bat_chg ? "true" : "false", 201 hostname, 202 hw, drv, gpu_name); 203 sq_push(m, msg); 204 ac_log("[machines] registered\n"); 205} 206 207// ── Session log upload (on connect) ────────────────────────── 208// Sends full boot log in chunks (up to 32KB total, 8KB per message) 209 210#define LOG_CHUNK_SIZE 7000 // raw bytes per chunk (leaves room for JSON overhead) 211#define LOG_MAX_TOTAL 32000 // max total log bytes to upload 212 213static void upload_log_file(ACMachines *m, const char *path, const char *logType) { 214 char raw[LOG_MAX_TOTAL + 1]; 215 memset(raw, 0, sizeof(raw)); 216 int len = read_file(path, raw, LOG_MAX_TOTAL); 217 if (len <= 0) return; 218 219 int chunk = 0; 220 int offset = 0; 221 while (offset < len) { 222 int clen = len - offset; 223 if (clen > LOG_CHUNK_SIZE) clen = LOG_CHUNK_SIZE; 224 225 char escaped[LOG_CHUNK_SIZE * 2]; 226 json_escape(raw + offset, clen, escaped, sizeof(escaped)); 227 228 char msg[LOG_CHUNK_SIZE * 2 + 256]; 229 snprintf(msg, sizeof(msg), 230 "{\"type\":\"log\",\"logType\":\"%s\"," 231 "\"data\":{\"chunk\":%d,\"offset\":%d,\"total\":%d}," 232 "\"message\":\"%s\"}", logType, chunk, offset, len, escaped); 233 sq_push(m, msg); 234 235 offset += clen; 236 chunk++; 237 } 238 ac_log("[machines] %s log queued (%d bytes, %d chunks)\n", logType, len, chunk); 239} 240 241static void upload_session_log(ACMachines *m) { 242 upload_log_file(m, "/mnt/ac-native.log", "session"); 243 upload_log_file(m, "/mnt/ac-audio.log", "audio"); 244} 245 246// ── Crash report upload ────────────────────────────────────── 247 248static void upload_crash_report(ACMachines *m) { 249 char raw[2048] = ""; 250 int len = read_file("/mnt/crash.json", raw, sizeof(raw)); 251 if (len <= 0) return; 252 253 // crash.json is already JSON, embed it as data 254 char msg[3072]; 255 snprintf(msg, sizeof(msg), 256 "{\"type\":\"log\",\"logType\":\"crash\"," 257 "\"data\":%s}", raw); 258 sq_push(m, msg); 259 260 // Clear crash file 261 FILE *f = fopen("/mnt/crash.json", "w"); 262 if (f) fclose(f); 263 ac_log("[machines] crash report queued\n"); 264} 265 266// ── Heartbeat ──────────────────────────────────────────────── 267 268static void send_heartbeat(ACMachines *m, int frame, int fps) { 269 int bat_pct, bat_chg; 270 read_battery(&bat_pct, &bat_chg); 271 272 char msg[512]; 273 snprintf(msg, sizeof(msg), 274 "{\"type\":\"heartbeat\"," 275 "\"currentPiece\":\"%s\"," 276 "\"battery\":%d," 277 "\"charging\":%s," 278 "\"uptime\":%d," 279 "\"fps\":%d}", 280 m->current_piece, 281 bat_pct, 282 bat_chg ? "true" : "false", 283 frame, 284 fps); 285 sq_push(m, msg); 286} 287 288// ── Command ack ────────────────────────────────────────────── 289 290static void send_ack(ACMachines *m, const char *cmd_id, const char *cmd) { 291 char msg[256]; 292 snprintf(msg, sizeof(msg), 293 "{\"type\":\"command-ack\",\"commandId\":\"%s\",\"command\":\"%s\"}", 294 cmd_id, cmd); 295 sq_push(m, msg); 296} 297 298// ── Process incoming messages ──────────────────────────────── 299 300static void process_messages(ACMachines *m) { 301 int count = ws_poll(m->ws); 302 for (int i = 0; i < count; i++) { 303 const char *raw = m->ws->messages[i]; 304 305 // Handle swank:eval — evaluate CL via local Swank server 306 if (strstr(raw, "\"type\":\"swank:eval\"")) { 307 char expr[2048] = "", eval_id[32] = ""; 308 json_get_str(raw, "expr", expr, sizeof(expr)); 309 json_get_str(raw, "evalId", eval_id, sizeof(eval_id)); 310 if (expr[0]) { 311 ac_log("[machines] swank:eval id=%s expr=%.80s\n", eval_id, expr); 312 char result[4096] = ""; 313 int rc = swank_eval(expr, result, sizeof(result)); 314 // Send result back 315 char escaped_result[8192]; 316 json_escape(result, strlen(result), escaped_result, sizeof(escaped_result)); 317 char msg[12288]; 318 snprintf(msg, sizeof(msg), 319 "{\"type\":\"swank:result\",\"evalId\":\"%s\",\"ok\":%s,\"result\":\"%s\"}", 320 eval_id, rc == 0 ? "true" : "false", escaped_result); 321 ws_send(m->ws, msg); 322 } 323 continue; 324 } 325 326 // Check if it's a command message 327 if (!strstr(raw, "\"type\":\"command\"")) continue; 328 329 char cmd[32] = "", target[128] = "", cmd_id[32] = ""; 330 json_get_str(raw, "command", cmd, sizeof(cmd)); 331 json_get_str(raw, "commandId", cmd_id, sizeof(cmd_id)); 332 json_get_str(raw, "target", target, sizeof(target)); 333 334 ac_log("[machines] command: %s target=%s id=%s\n", cmd, target, cmd_id); 335 336 // Send ack immediately 337 send_ack(m, cmd_id, cmd); 338 339 if (strcmp(cmd, "reboot") == 0) { 340 ac_log("[machines] rebooting by remote command\n"); 341 sync(); 342 reboot(0x01234567); // LINUX_REBOOT_CMD_RESTART 343 } else if (strcmp(cmd, "jump") == 0 || 344 strcmp(cmd, "update") == 0 || 345 strcmp(cmd, "request-logs") == 0) { 346 // Forward to main loop via cmd_pending 347 if (!m->cmd_pending) { 348 strncpy(m->cmd_type, cmd, sizeof(m->cmd_type) - 1); 349 strncpy(m->cmd_target, target, sizeof(m->cmd_target) - 1); 350 strncpy(m->cmd_id, cmd_id, sizeof(m->cmd_id) - 1); 351 m->cmd_pending = 1; 352 } 353 } 354 } 355} 356 357// ── Public API ─────────────────────────────────────────────── 358 359void machines_init(ACMachines *m) { 360 memset(m, 0, sizeof(*m)); 361 m->ws = ws_create(); 362 read_file("/mnt/.device-token", m->device_token, sizeof(m->device_token)); 363 strncpy(m->current_piece, "notepat", sizeof(m->current_piece) - 1); 364 ac_log("[machines] init: id=%s token=%s\n", 365 g_machine_id, m->device_token[0] ? "yes" : "no"); 366} 367 368void machines_tick(ACMachines *m, ACWifi *wifi, int frame, int fps, 369 const char *current_piece) { 370 if (!m->ws) return; 371 372 // Track current piece 373 if (current_piece && current_piece[0]) { 374 strncpy(m->current_piece, current_piece, sizeof(m->current_piece) - 1); 375 } 376 377 int wifi_up = (wifi && wifi->state == WIFI_STATE_CONNECTED); 378 379 // Auto-connect when wifi is up and ws isn't active 380 if (wifi_up && !m->ws->connected && !m->ws->connecting && 381 !m->connected && m->reconnect_frame == 0) { 382 m->reconnect_frame = frame + 60; // ~1s delay 383 } 384 385 // Reconnect timer 386 if (m->reconnect_frame > 0 && frame >= m->reconnect_frame) { 387 m->reconnect_frame = 0; 388 if (!m->ws->connected && !m->ws->connecting && wifi_up) { 389 machines_connect(m); 390 } 391 } 392 393 // Just connected → register + upload logs 394 if (m->ws->connected && !m->connected) { 395 m->connected = 1; 396 m->last_heartbeat_frame = frame; 397 send_register(m, wifi); 398 upload_session_log(m); 399 upload_crash_report(m); 400 } 401 402 // Disconnected → schedule reconnect 403 if (!m->ws->connected && !m->ws->connecting && m->connected) { 404 m->connected = 0; 405 ac_log("[machines] disconnected\n"); 406 if (wifi_up) m->reconnect_frame = frame + MACHINES_RECONNECT_FRAMES; 407 } 408 409 // Heartbeat every ~30s 410 if (m->connected && m->ws->connected && 411 (frame - m->last_heartbeat_frame) >= MACHINES_HEARTBEAT_FRAMES) { 412 m->last_heartbeat_frame = frame; 413 send_heartbeat(m, frame, fps); 414 } 415 416 // Process incoming messages (commands) 417 if (m->connected) { 418 process_messages(m); 419 } 420 421 // Drain send queue (one message per frame) 422 if (m->connected && m->ws->connected) { 423 sq_drain_one(m); 424 } 425} 426 427void machines_flush_logs(ACMachines *m) { 428 if (!m->ws || !m->ws->connected) return; 429 430 ac_log("[machines] flushing final shutdown logs\n"); 431 upload_log_file(m, "/mnt/ac-native.log", "shutdown"); 432 upload_log_file(m, "/mnt/ac-audio.log", "audio-shutdown"); 433 434 // Drain the entire send queue (blocking — we're shutting down) 435 int attempts = 0; 436 while (m->sq_count > 0 && attempts < 300) { // up to ~3 seconds 437 if (m->ws->send_pending) { 438 usleep(10000); // 10ms wait for send completion 439 ws_poll(m->ws); 440 } else { 441 sq_drain_one(m); 442 } 443 attempts++; 444 } 445 ac_log("[machines] shutdown flush done (queue=%d)\n", m->sq_count); 446} 447 448void machines_destroy(ACMachines *m) { 449 if (m->ws) { 450 ws_close(m->ws); 451 ws_destroy(m->ws); 452 m->ws = NULL; 453 } 454}