Monorepo for Aesthetic.Computer
aesthetic.computer
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}