Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539// machines.c — System-level AC Machines monitoring daemon// Manages a WebSocket connection to session-server for device registration,// heartbeats, log upload, and remote command reception.
#include "machines.h"#include "drm-display.h"#include "swank-bridge.h"
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <time.h>#include <unistd.h>#include <sys/reboot.h>#include <linux/reboot.h>
extern void ac_log(const char *fmt, ...);extern char g_machine_id[64];
// Compile-time build info (set by Makefile)#ifndef AC_BUILD_NAME#define AC_BUILD_NAME "dev"#endif#ifndef AC_GIT_HASH#define AC_GIT_HASH "unknown"#endif#ifndef AC_BUILD_TS#define AC_BUILD_TS "unknown"#endif
#define WS_URL "wss://session-server.aesthetic.computer/machines"
// ── Helpers ──────────────────────────────────────────────────
static int read_file(const char *path, char *buf, int bufsize) { FILE *f = fopen(path, "r"); if (!f) return -1; int len = (int)fread(buf, 1, bufsize - 1, f); fclose(f); if (len > 0 && buf[len - 1] == '\n') len--; buf[len] = 0; return len;}
static void read_battery(int *percent, int *charging) { char buf[64]; *percent = -1; *charging = 0; const char *bat_names[] = {"BAT0", "BAT1", NULL}; for (int i = 0; bat_names[i]; i++) { char path[128]; snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity", bat_names[i]); if (read_file(path, buf, sizeof(buf)) > 0) { *percent = atoi(buf); snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status", bat_names[i]); if (read_file(path, buf, sizeof(buf)) > 0) *charging = (strcmp(buf, "Charging") == 0); return; } }}
// Simple JSON string extraction: find "key":"value" and copy value to out.static int json_get_str(const char *json, const char *key, char *out, int out_sz) { char needle[128]; snprintf(needle, sizeof(needle), "\"%s\":\"", key); const char *p = strstr(json, needle); if (!p) return -1; p += strlen(needle); const char *end = strchr(p, '"'); if (!end) return -1; int len = (int)(end - p); if (len >= out_sz) len = out_sz - 1; memcpy(out, p, len); out[len] = 0; return len;}
// Escape-aware JSON string extraction: respects \", \\, \n, \r, \t so values// like `(format t "hi")` survive transit. Stops at the first un-escaped quote.static int json_get_str_unesc(const char *json, const char *key, char *out, int out_sz) { char needle[128]; snprintf(needle, sizeof(needle), "\"%s\":\"", key); const char *p = strstr(json, needle); if (!p) return -1; p += strlen(needle); int j = 0; while (*p && j < out_sz - 1) { if (*p == '"') break; if (*p == '\\' && p[1]) { char c = p[1]; if (c == '"') out[j++] = '"'; else if (c == '\\') out[j++] = '\\'; else if (c == 'n') out[j++] = '\n'; else if (c == 'r') out[j++] = '\r'; else if (c == 't') out[j++] = '\t'; else if (c == '/') out[j++] = '/'; else { // unknown escape — keep verbatim out[j++] = '\\'; if (j < out_sz - 1) out[j++] = c; } p += 2; } else { out[j++] = *p++; } } out[j] = 0; return j;}
// Escape a string for JSON embedding (backslash, quotes, control chars).// Returns number of bytes written (excluding null terminator).static int json_escape(const char *in, int in_len, char *out, int out_sz) { int j = 0; for (int i = 0; i < in_len && j < out_sz - 2; i++) { unsigned char c = (unsigned char)in[i]; if (c == '"' || c == '\\') { if (j + 2 >= out_sz) break; out[j++] = '\\'; out[j++] = c; } else if (c == '\n') { if (j + 2 >= out_sz) break; out[j++] = '\\'; out[j++] = 'n'; } else if (c == '\r') { if (j + 2 >= out_sz) break; out[j++] = '\\'; out[j++] = 'r'; } else if (c == '\t') { if (j + 2 >= out_sz) break; out[j++] = '\\'; out[j++] = 't'; } else if (c < 0x20) { // Skip other control chars } else { out[j++] = c; } } out[j] = 0; return j;}
// ── Send queue (drains one per frame when ws slot is free) ────
static void sq_push(ACMachines *m, const char *msg) { if (m->sq_count >= MACHINES_SEND_QUEUE_SIZE) { ac_log("[machines] send queue full, dropping message\n"); return; } int slot = (m->sq_head + m->sq_count) % MACHINES_SEND_QUEUE_SIZE; strncpy(m->send_queue[slot], msg, MACHINES_SEND_MSG_SIZE - 1); m->send_queue[slot][MACHINES_SEND_MSG_SIZE - 1] = 0; m->sq_count++;}
static void sq_drain_one(ACMachines *m) { if (m->sq_count <= 0 || !m->ws) return; // Only send if ws send slot is free if (m->ws->send_pending) return; ws_send(m->ws, m->send_queue[m->sq_head]); m->sq_head = (m->sq_head + 1) % MACHINES_SEND_QUEUE_SIZE; m->sq_count--;}
// ── Connection ───────────────────────────────────────────────
static void machines_connect(ACMachines *m) { char url[768]; const char *mid = g_machine_id[0] ? g_machine_id : "unknown"; if (m->device_token[0]) { snprintf(url, sizeof(url), "%s?role=device&machineId=%s&token=%s", WS_URL, mid, m->device_token); } else { snprintf(url, sizeof(url), "%s?role=device&machineId=%s", WS_URL, mid); } ws_connect(m->ws, url); m->connected = 0; ac_log("[machines] connecting: %s\n", mid);}
// ── Register (sent on connect) ───────────────────────────────
static void send_register(ACMachines *m, ACWifi *wifi) { int bat_pct, bat_chg; read_battery(&bat_pct, &bat_chg);
char hw[128] = ""; read_file("/sys/devices/virtual/dmi/id/product_name", hw, sizeof(hw));
char hostname[64] = ""; read_file("/etc/hostname", hostname, sizeof(hostname));
// Display driver extern void *g_display; const char *drv = "unknown"; if (g_display) { drv = drm_display_driver((ACDisplay *)g_display); } else if (getenv("WAYLAND_DISPLAY")) { drv = "wayland"; }
// GPU name from sysfs char gpu_name[128] = "unknown"; { char tmp[128] = ""; if (read_file("/sys/kernel/debug/dri/0/name", tmp, sizeof(tmp)) > 0 || read_file("/sys/class/drm/card0/device/label", tmp, sizeof(tmp)) > 0) { snprintf(gpu_name, sizeof(gpu_name), "%s", tmp); } }
char msg[2048]; snprintf(msg, sizeof(msg), "{\"type\":\"register\"," "\"version\":\"%s %s-%s\"," "\"buildName\":\"%s\"," "\"gitHash\":\"%s\"," "\"buildTs\":\"%s\"," "\"currentPiece\":\"%s\"," "\"ip\":\"%s\"," "\"wifiSSID\":\"%s\"," "\"battery\":%d," "\"charging\":%s," "\"hostname\":\"%s\"," "\"hw\":{\"display\":\"%s\",\"displayDriver\":\"%s\",\"gpu\":\"%s\"}}", AC_BUILD_NAME, AC_GIT_HASH, AC_BUILD_TS, AC_BUILD_NAME, AC_GIT_HASH, AC_BUILD_TS, m->current_piece, wifi ? wifi->ip_address : "", wifi ? wifi->connected_ssid : "", bat_pct, bat_chg ? "true" : "false", hostname, hw, drv, gpu_name); sq_push(m, msg); ac_log("[machines] registered\n");}
// ── Session log upload (on connect) ──────────────────────────// Sends full boot log in chunks (up to 32KB total, 8KB per message)
#define LOG_CHUNK_SIZE 7000 // raw bytes per chunk (leaves room for JSON overhead)#define LOG_MAX_TOTAL 32000 // max total log bytes to upload
static void upload_log_file(ACMachines *m, const char *path, const char *logType) { char raw[LOG_MAX_TOTAL + 1]; memset(raw, 0, sizeof(raw)); int len = read_file(path, raw, LOG_MAX_TOTAL); if (len <= 0) return;
int chunk = 0; int offset = 0; while (offset < len) { int clen = len - offset; if (clen > LOG_CHUNK_SIZE) clen = LOG_CHUNK_SIZE;
char escaped[LOG_CHUNK_SIZE * 2]; json_escape(raw + offset, clen, escaped, sizeof(escaped));
char msg[LOG_CHUNK_SIZE * 2 + 256]; snprintf(msg, sizeof(msg), "{\"type\":\"log\",\"logType\":\"%s\"," "\"data\":{\"chunk\":%d,\"offset\":%d,\"total\":%d}," "\"message\":\"%s\"}", logType, chunk, offset, len, escaped); sq_push(m, msg);
offset += clen; chunk++; } ac_log("[machines] %s log queued (%d bytes, %d chunks)\n", logType, len, chunk);}
static void upload_session_log(ACMachines *m) { upload_log_file(m, "/mnt/ac-native.log", "session"); upload_log_file(m, "/mnt/ac-audio.log", "audio");}
// ── Crash report upload ──────────────────────────────────────
static void upload_crash_report(ACMachines *m) { char raw[2048] = ""; int len = read_file("/mnt/crash.json", raw, sizeof(raw)); if (len <= 0) return;
// crash.json is already JSON, embed it as data char msg[3072]; snprintf(msg, sizeof(msg), "{\"type\":\"log\",\"logType\":\"crash\"," "\"data\":%s}", raw); sq_push(m, msg);
// Clear crash file FILE *f = fopen("/mnt/crash.json", "w"); if (f) fclose(f); ac_log("[machines] crash report queued\n");}
// ── Heartbeat ────────────────────────────────────────────────
static void send_heartbeat(ACMachines *m, int frame, int fps) { int bat_pct, bat_chg; read_battery(&bat_pct, &bat_chg);
char msg[512]; snprintf(msg, sizeof(msg), "{\"type\":\"heartbeat\"," "\"currentPiece\":\"%s\"," "\"battery\":%d," "\"charging\":%s," "\"uptime\":%d," "\"fps\":%d}", m->current_piece, bat_pct, bat_chg ? "true" : "false", frame, fps); sq_push(m, msg);}
// ── Command ack ──────────────────────────────────────────────
static void send_ack(ACMachines *m, const char *cmd_id, const char *cmd) { char msg[256]; snprintf(msg, sizeof(msg), "{\"type\":\"command-ack\",\"commandId\":\"%s\",\"command\":\"%s\"}", cmd_id, cmd); sq_push(m, msg);}
// ── Process incoming messages ────────────────────────────────
static void process_messages(ACMachines *m) { int count = ws_poll(m->ws); for (int i = 0; i < count; i++) { const char *raw = m->ws->messages[i];
// Handle swank:eval — evaluate CL via local Swank server if (strstr(raw, "\"type\":\"swank:eval\"")) { char expr[2048] = "", eval_id[32] = ""; json_get_str(raw, "expr", expr, sizeof(expr)); json_get_str(raw, "evalId", eval_id, sizeof(eval_id)); if (expr[0]) { ac_log("[machines] swank:eval id=%s expr=%.80s\n", eval_id, expr); char result[4096] = ""; int rc = swank_eval(expr, result, sizeof(result)); // Send result back char escaped_result[8192]; json_escape(result, strlen(result), escaped_result, sizeof(escaped_result)); char msg[12288]; snprintf(msg, sizeof(msg), "{\"type\":\"swank:result\",\"evalId\":\"%s\",\"ok\":%s,\"result\":\"%s\"}", eval_id, rc == 0 ? "true" : "false", escaped_result); ws_send(m->ws, msg); } continue; }
// Check if it's a command message if (!strstr(raw, "\"type\":\"command\"")) continue;
char cmd[32] = "", target[128] = "", cmd_id[32] = ""; json_get_str(raw, "command", cmd, sizeof(cmd)); json_get_str(raw, "commandId", cmd_id, sizeof(cmd_id)); json_get_str(raw, "target", target, sizeof(target));
ac_log("[machines] command: %s target=%s id=%s\n", cmd, target, cmd_id);
// Send ack immediately send_ack(m, cmd_id, cmd);
if (strcmp(cmd, "reboot") == 0) { ac_log("[machines] rebooting by remote command\n"); sync(); reboot(0x01234567); // LINUX_REBOOT_CMD_RESTART } else if (strcmp(cmd, "prompt") == 0 || strcmp(cmd, "prompt-bg") == 0) { // Free-text remote prompt: feed `text` straight into prompt.mjs's // execute() on the device. "prompt" force-jumps to the prompt // piece; "prompt-bg" runs in the background and bounces back to // whatever was on screen so notepat etc. aren't disrupted. char text[2048] = ""; json_get_str_unesc(raw, "text", text, sizeof(text)); if (text[0] && !m->cmd_pending) { strncpy(m->cmd_type, cmd, sizeof(m->cmd_type) - 1); m->cmd_type[sizeof(m->cmd_type) - 1] = 0; strncpy(m->cmd_target, text, sizeof(m->cmd_target) - 1); m->cmd_target[sizeof(m->cmd_target) - 1] = 0; strncpy(m->cmd_id, cmd_id, sizeof(m->cmd_id) - 1); m->cmd_id[sizeof(m->cmd_id) - 1] = 0; m->cmd_pending = 1; } } else if (strcmp(cmd, "jump") == 0 || strcmp(cmd, "update") == 0 || strcmp(cmd, "request-logs") == 0) { // Forward to main loop via cmd_pending if (!m->cmd_pending) { strncpy(m->cmd_type, cmd, sizeof(m->cmd_type) - 1); strncpy(m->cmd_target, target, sizeof(m->cmd_target) - 1); strncpy(m->cmd_id, cmd_id, sizeof(m->cmd_id) - 1); m->cmd_pending = 1; } } }}
// ── Public API ───────────────────────────────────────────────
void machines_init(ACMachines *m) { memset(m, 0, sizeof(*m)); m->ws = ws_create(); read_file("/mnt/.device-token", m->device_token, sizeof(m->device_token)); strncpy(m->current_piece, "notepat", sizeof(m->current_piece) - 1); ac_log("[machines] init: id=%s token=%s\n", g_machine_id, m->device_token[0] ? "yes" : "no");}
void machines_tick(ACMachines *m, ACWifi *wifi, int frame, int fps, const char *current_piece) { if (!m->ws) return;
// Track current piece if (current_piece && current_piece[0]) { strncpy(m->current_piece, current_piece, sizeof(m->current_piece) - 1); }
int wifi_up = (wifi && wifi->state == WIFI_STATE_CONNECTED);
// Auto-connect when wifi is up and ws isn't active if (wifi_up && !m->ws->connected && !m->ws->connecting && !m->connected && m->reconnect_frame == 0) { m->reconnect_frame = frame + 60; // ~1s delay }
// Reconnect timer if (m->reconnect_frame > 0 && frame >= m->reconnect_frame) { m->reconnect_frame = 0; if (!m->ws->connected && !m->ws->connecting && wifi_up) { machines_connect(m); } }
// Just connected → register + upload logs if (m->ws->connected && !m->connected) { m->connected = 1; m->last_heartbeat_frame = frame; send_register(m, wifi); upload_session_log(m); upload_crash_report(m); }
// Disconnected → schedule reconnect if (!m->ws->connected && !m->ws->connecting && m->connected) { m->connected = 0; ac_log("[machines] disconnected\n"); if (wifi_up) m->reconnect_frame = frame + MACHINES_RECONNECT_FRAMES; }
// Heartbeat every ~30s if (m->connected && m->ws->connected && (frame - m->last_heartbeat_frame) >= MACHINES_HEARTBEAT_FRAMES) { m->last_heartbeat_frame = frame; send_heartbeat(m, frame, fps); }
// Process incoming messages (commands) if (m->connected) { process_messages(m); }
// Drain send queue (one message per frame) if (m->connected && m->ws->connected) { sq_drain_one(m); }}
void machines_send_response(ACMachines *m, const char *cmd_id, const char *cmd, const char *output, int ok) { if (!m || !cmd_id || !cmd) return; // Cap raw output at ~7KB so the worst-case escaped form (~14KB if every // byte gets backslash-encoded) plus the JSON envelope still fits below // MACHINES_SEND_MSG_SIZE (16384). Append a "…[truncated]" marker so the // viewer knows there was more. char buf[7168]; int olen = output ? (int)strlen(output) : 0; static const char TRUNC[] = " …[truncated]"; const int trunc_len = (int)sizeof(TRUNC) - 1; if (olen > (int)sizeof(buf) - 1) { const int copy_len = (int)sizeof(buf) - trunc_len - 1; memcpy(buf, output, copy_len); memcpy(buf + copy_len, TRUNC, trunc_len); buf[copy_len + trunc_len] = 0; olen = copy_len + trunc_len; } else if (output) { memcpy(buf, output, olen); buf[olen] = 0; } else { buf[0] = 0; }
char escaped[14336]; json_escape(buf, olen, escaped, sizeof(escaped)); char msg[15800]; snprintf(msg, sizeof(msg), "{\"type\":\"command-response\"," "\"commandId\":\"%s\"," "\"command\":\"%s\"," "\"data\":{\"ok\":%s,\"output\":\"%s\"}}", cmd_id, cmd, ok ? "true" : "false", escaped); sq_push(m, msg);}
void machines_flush_logs(ACMachines *m) { if (!m->ws || !m->ws->connected) return;
ac_log("[machines] flushing final shutdown logs\n"); upload_log_file(m, "/mnt/ac-native.log", "shutdown"); upload_log_file(m, "/mnt/ac-audio.log", "audio-shutdown");
// Drain the entire send queue (blocking — we're shutting down) int attempts = 0; while (m->sq_count > 0 && attempts < 300) { // up to ~3 seconds if (m->ws->send_pending) { usleep(10000); // 10ms wait for send completion ws_poll(m->ws); } else { sq_drain_one(m); } attempts++; } ac_log("[machines] shutdown flush done (queue=%d)\n", m->sq_count);}
void machines_destroy(ACMachines *m) { if (m->ws) { ws_close(m->ws); ws_destroy(m->ws); m->ws = NULL; }}