Monorepo for Aesthetic.Computer aesthetic.computer
at main 4484 lines 208 kB view raw
1// ac-native.c — Sub-second boot AC piece runner 2// Runs as PID 1 in a minimal initramfs. 3// UEFI → EFI stub kernel → this binary → piece.mjs 4 5#include <stdio.h> 6#include <stdlib.h> 7#include <stdarg.h> 8#include <string.h> 9#include <signal.h> 10#include <time.h> 11#include <math.h> 12#include <unistd.h> 13#include <sys/wait.h> 14#include <fcntl.h> 15#include <dirent.h> 16#include <errno.h> 17#include <sys/mount.h> 18#include <sys/statvfs.h> 19#include <sys/stat.h> 20#include <sys/reboot.h> 21#include <linux/reboot.h> 22#include <linux/input.h> 23#include <linux/fs.h> // BLKRRPART for forced partition re-read (install) 24#include <sys/ioctl.h> 25#include <pthread.h> 26 27#include "drm-display.h" 28#include "framebuffer.h" 29#include "graph.h" 30#include "font.h" 31#include "input.h" 32#include "audio.h" 33#include "wifi.h" 34#include "tts.h" 35#include "js-bindings.h" 36#include "machines.h" 37#include "recorder.h" 38#ifdef USE_WAYLAND 39#include "wayland-display.h" 40#endif 41 42static volatile int running = 1; 43static FILE *logfile = NULL; 44static volatile int log_dirty = 0; 45int ac_log_stderr_muted = 0; // When set, ac_log skips stderr (PTY active) 46int voice_off = 1; // Keystroke TTS disabled by default (enable with voice:on in config) 47static int is_removable(const char *blkname); 48static void get_parent_block(const char *part, char *out, int out_sz); 49 50// ── Performance logger (crash-resilient chunked files) ── 51// Writes /mnt/perf/NNNN.csv every 30s, each chunk fsync'd and closed. 52// On hard crash you lose at most 30 seconds. Keeps last 5 minutes (10 chunks). 53#define PERF_CHUNK_SECS 30 54#define PERF_CHUNK_FRAMES (60 * PERF_CHUNK_SECS) // 1800 frames per chunk 55#define PERF_MAX_CHUNKS 10 // 10 × 30s = 5 minutes 56#define PERF_BUF_SIZE PERF_CHUNK_FRAMES 57 58typedef struct { 59 uint32_t frame; 60 uint16_t total_us; // total frame time in microseconds (capped at 65535) 61 uint16_t act_us; 62 uint16_t sim_us; 63 uint16_t paint_us; 64 uint16_t present_us; 65 uint8_t voices; // active synth voices 66 uint8_t events; // input events this frame 67 uint8_t js_heap_mb; // QuickJS heap in MB (capped at 255) 68 uint8_t flags; // bit0=trackpadFX, bit1=cursor_visible 69} PerfRecord; 70 71static PerfRecord *perf_buf = NULL; 72static int perf_buf_count = 0; // records in current chunk buffer 73static int perf_chunk_seq = 0; // monotonic chunk sequence number 74static int perf_flush_frame = 0; // last frame we flushed 75 76static void perf_init(void) { 77 perf_buf = calloc(PERF_BUF_SIZE, sizeof(PerfRecord)); 78 if (!perf_buf) fprintf(stderr, "[perf] alloc failed\n"); 79 mkdir("/mnt/perf", 0755); // ensure directory exists 80} 81 82static void perf_record(PerfRecord *r) { 83 if (!perf_buf || perf_buf_count >= PERF_BUF_SIZE) return; 84 perf_buf[perf_buf_count++] = *r; 85} 86 87// Write current buffer as a numbered chunk file, fsync, close, then 88// delete the oldest chunk if we exceed PERF_MAX_CHUNKS. 89void perf_flush(void) { 90 if (!perf_buf || perf_buf_count == 0) return; 91 92 char path[128]; 93 snprintf(path, sizeof(path), "/mnt/perf/%04d.csv", perf_chunk_seq); 94 95 int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); 96 if (fd < 0) return; 97 98 FILE *f = fdopen(fd, "w"); 99 if (!f) { close(fd); return; } 100 101 fprintf(f, "frame,total_us,act_us,sim_us,paint_us,present_us,voices,events,heap_mb,flags\n"); 102 for (int i = 0; i < perf_buf_count; i++) { 103 PerfRecord *r = &perf_buf[i]; 104 fprintf(f, "%u,%u,%u,%u,%u,%u,%u,%u,%u,%u\n", 105 r->frame, r->total_us, r->act_us, r->sim_us, 106 r->paint_us, r->present_us, r->voices, r->events, 107 r->js_heap_mb, r->flags); 108 } 109 fflush(f); 110 fsync(fd); 111 fclose(f); // also closes fd 112 113 perf_buf_count = 0; 114 115 // Delete oldest chunk beyond retention window 116 int old_seq = perf_chunk_seq - PERF_MAX_CHUNKS; 117 if (old_seq >= 0) { 118 char old_path[128]; 119 snprintf(old_path, sizeof(old_path), "/mnt/perf/%04d.csv", old_seq); 120 unlink(old_path); 121 } 122 123 perf_chunk_seq++; 124} 125 126static void perf_destroy(void) { 127 perf_flush(); 128 free(perf_buf); 129 perf_buf = NULL; 130} 131 132// Forward declaration — defined after init_log_mount() 133extern char g_machine_id[64]; 134 135// DRM handoff for xdg-open browser popup 136// SIGUSR1 = release DRM master (browser takes over display) 137// SIGUSR2 = reboot request from cage child (or reclaim DRM in DRM mode) 138static volatile int drm_handoff_release = 0; 139static volatile int drm_handoff_reclaim = 0; 140static volatile int reboot_requested = 0; 141volatile int poweroff_requested = 0; // extern'd in js-bindings.c 142 143static void sigusr_handler(int sig) { 144 if (sig == SIGUSR1) drm_handoff_release = 1; 145 if (sig == SIGUSR2) reboot_requested = 1; 146} 147 148static void sigterm_handler(int sig) { 149 (void)sig; 150 poweroff_requested = 1; 151} 152 153// Shutdown/reboot that works in all three contexts: 154// - PID 1 (bare metal, direct DRM boot): reboot() syscall works directly. 155// - Child of init script (bare metal): exit with special code so the init 156// shell script can invoke `poweroff -f` and the reboot syscall itself. 157// We ALSO try the reboot syscall directly since we're running as root 158// with CAP_SYS_BOOT — that's faster than round-tripping through init. 159// - Under systemd (NixOS): fall back to systemctl. 160static void ac_poweroff(void) { 161 sync(); 162 usleep(500000); 163 sync(); 164 // Try the kernel syscall first — works as root with CAP_SYS_BOOT, which 165 // we always have on bare metal (PID 1 or child of init). Only non-root 166 // contexts (NixOS under systemd) need to shell out. 167 if (reboot(LINUX_REBOOT_CMD_POWER_OFF) == 0) { 168 // Shouldn't reach here — syscall succeeds → kernel halts. 169 _exit(0); 170 } 171 // Syscall failed (likely EPERM on systemd) — fall back. 172 system("systemctl poweroff || /sbin/poweroff -f || /bin/poweroff -f || poweroff -f"); 173 // If the initramfs init script is our parent, exit(0) tells it we've 174 // finished; it will do its own `poweroff -f` + sysrq as a last resort. 175 _exit(0); 176} 177 178static void ac_reboot(void) { 179 sync(); 180 usleep(500000); 181 sync(); 182 if (reboot(LINUX_REBOOT_CMD_RESTART) == 0) { 183 _exit(2); 184 } 185 system("systemctl reboot || /sbin/reboot -f || /bin/reboot -f || reboot -f"); 186 _exit(2); 187} 188 189static void signal_handler(int sig) { 190 running = 0; 191 192 // Best-effort crash report to /mnt/crash.json for next-boot upload 193 if (sig == SIGSEGV || sig == SIGBUS || sig == SIGABRT || sig == SIGFPE) { 194 const char *signame = "UNKNOWN"; 195 switch (sig) { 196 case SIGSEGV: signame = "SIGSEGV"; break; 197 case SIGBUS: signame = "SIGBUS"; break; 198 case SIGABRT: signame = "SIGABRT"; break; 199 case SIGFPE: signame = "SIGFPE"; break; 200 } 201 FILE *f = fopen("/mnt/crash.json", "w"); 202 if (f) { 203 time_t now = time(NULL); 204 fprintf(f, "{\"signal\":\"%s\",\"machineId\":\"%s\",\"time\":%ld}\n", 205 signame, g_machine_id, (long)now); 206 fclose(f); 207 sync(); 208 } 209 // Re-raise to get default behavior (core dump / termination) 210 signal(sig, SIG_DFL); 211 raise(sig); 212 } 213} 214 215// Log to stderr (when unmuted) and logfile 216void ac_log(const char *fmt, ...) { 217 va_list args, args2; 218 va_start(args, fmt); 219 va_copy(args2, args); 220 if (!ac_log_stderr_muted) vfprintf(stderr, fmt, args); 221 va_end(args); 222 if (logfile) { 223 vfprintf(logfile, fmt, args2); 224 fflush(logfile); 225 log_dirty = 1; 226 } 227 va_end(args2); 228} 229 230// Flush log file to disk without closing it 231void ac_log_flush(void) { 232 if (logfile) { 233 fflush(logfile); 234 if (log_dirty) { 235 fsync(fileno(logfile)); 236 log_dirty = 0; 237 } 238 } 239} 240 241// Temporarily close the log file (e.g. before flash writes to same partition) 242void ac_log_pause(void) { 243 if (logfile) { 244 fflush(logfile); 245 if (log_dirty) { 246 fsync(fileno(logfile)); 247 log_dirty = 0; 248 } 249 fclose(logfile); 250 logfile = NULL; 251 } 252} 253 254// Reopen the log file in append mode after a pause 255void ac_log_resume(void) { 256 if (!logfile) { 257 logfile = fopen("/mnt/ac-native.log", "a"); 258 // If reopen fails, logging continues to stderr only 259 } 260} 261 262// Mount minimal filesystems (PID 1 only) 263static void mount_minimal_fs(void) { 264 mkdir("/proc", 0755); 265 mkdir("/sys", 0755); 266 mkdir("/dev", 0755); 267 mkdir("/tmp", 0755); 268 269 mount("proc", "/proc", "proc", 0, NULL); 270 mount("sysfs", "/sys", "sysfs", 0, NULL); 271 // Re-mount devtmpfs — safe here because by DRM fallback time i915 is fully loaded, 272 // so the fresh devtmpfs will have /dev/dri/card0. (Init does NOT re-mount devtmpfs 273 // so cage can see the kernel's original card0 early.) 274 mount("devtmpfs", "/dev", "devtmpfs", 0, NULL); 275 mkdir("/dev/pts", 0755); 276 mount("devpts", "/dev/pts", "devpts", 0, "ptmxmode=0666"); 277 mkdir("/dev/shm", 0755); 278 mount("tmpfs", "/dev/shm", "tmpfs", 0, NULL); 279 // Don't re-mount /tmp — init already mounted it and may have put logs there. 280 281 // Enable zram swap (compressed RAM — effectively doubles available memory) 282 // Firefox + GTK needs significant memory beyond the initramfs tmpfs 283 system("(modprobe zram 2>/dev/null || true); " 284 "[ -e /sys/block/zram0/disksize ] && [ -b /dev/zram0 ] && " 285 "echo 1G > /sys/block/zram0/disksize && " 286 "mkswap /dev/zram0 >/dev/null 2>&1 && " 287 "swapon /dev/zram0 2>/dev/null"); 288 289 // Bring up loopback interface (needed for Claude OAuth callback server) 290 system("/bin/ip link set lo up 2>/dev/null || /usr/sbin/ip link set lo up 2>/dev/null || ifconfig lo up 2>/dev/null"); 291 292 // Wait for display device (up to 10s — Gemini Lake GPUs can be slow) 293 for (int i = 0; i < 1000; i++) { 294 if (access("/dev/dri/card0", F_OK) == 0 || 295 access("/dev/dri/card1", F_OK) == 0 || 296 access("/dev/fb0", F_OK) == 0) break; 297 usleep(10000); 298 } 299 300 // Set performance power mode 301 FILE *gov = fopen("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", "w"); 302 if (gov) { fputs("performance", gov); fclose(gov); } 303 // Set all CPUs to performance 304 for (int c = 1; c < 16; c++) { 305 char path[128]; 306 snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor", c); 307 gov = fopen(path, "w"); 308 if (gov) { fputs("performance", gov); fclose(gov); } 309 } 310} 311 312// Try to mount boot USB for log writing (non-blocking, best-effort) 313char log_dev[32] = ""; // non-static: accessed by js-bindings.c for flash target check 314static void try_mount_log(void) { 315 mkdir("/mnt", 0755); 316 // Wait for USB block devices to appear (up to 2s after EFI handoff) 317 fprintf(stderr, "[ac-native] Waiting for USB block devices...\n"); 318 for (int w = 0; w < 100; w++) { 319 if (access("/dev/sda1", F_OK) == 0 || access("/dev/sda2", F_OK) == 0 || 320 access("/dev/sdb1", F_OK) == 0 || access("/dev/sdb2", F_OK) == 0) break; 321 usleep(20000); 322 } 323 fprintf(stderr, "[ac-native] sda1=%s sda2=%s sdb1=%s sdb2=%s\n", 324 access("/dev/sda1", F_OK) == 0 ? "yes" : "no", 325 access("/dev/sda2", F_OK) == 0 ? "yes" : "no", 326 access("/dev/sdb1", F_OK) == 0 ? "yes" : "no", 327 access("/dev/sdb2", F_OK) == 0 ? "yes" : "no"); 328 const char *devs[] = { 329 "/dev/sda1", "/dev/sda2", 330 "/dev/sdb1", "/dev/sdb2", 331 "/dev/sdc1", "/dev/sdc2", 332 "/dev/sdd1", "/dev/sdd2", 333 "/dev/nvme0n1p1", "/dev/nvme0n1p2", 334 "/dev/nvme1n1p1", "/dev/nvme1n1p2", 335 "/dev/mmcblk0p1", "/dev/mmcblk0p2", 336 "/dev/mmcblk1p1", "/dev/mmcblk1p2", 337 NULL 338 }; 339 340 // Pass 0: removable media first (USB install source). 341 // Pass 1: fallback to internal ESP (ensures config.json loads on disk boots). 342 for (int pass = 0; pass < 2; pass++) { 343 for (int i = 0; devs[i]; i++) { 344 if (access(devs[i], F_OK) != 0) continue; 345 346 char blk[32] = ""; 347 get_parent_block(devs[i] + 5, blk, sizeof(blk)); // skip "/dev/" 348 int rem = blk[0] ? is_removable(blk) : -1; 349 if (pass == 0 && rem != 1) continue; 350 if (pass == 1 && rem == 1) continue; 351 352 int mr = mount(devs[i], "/mnt", "vfat", 0, NULL); 353 if (mr != 0) 354 fprintf(stderr, "[ac-native] mount %s failed: %s\n", devs[i], strerror(errno)); 355 if (mr == 0) { 356 logfile = fopen("/mnt/ac-native.log", "a"); 357 if (logfile) { 358 // Separator between boots for multi-boot log history 359 fprintf(logfile, "\n=== BOOT %s ===\n", devs[i]); 360 fprintf(logfile, "[ac-native] Log opened on %s (removable=%d)\n", devs[i], rem); 361 fflush(logfile); 362 fsync(fileno(logfile)); 363 strncpy(log_dev, devs[i], sizeof(log_dev) - 1); 364 fprintf(stderr, "[ac-native] Log: %s -> /mnt/ac-native.log (removable=%d)\n", devs[i], rem); 365 // Log available block devices for storage diagnostics 366 { 367 const char *bdevs[] = {"sda","sdb","sdc","sdd","nvme0n1","nvme1n1","mmcblk0","mmcblk1",NULL}; 368 for (int b = 0; bdevs[b]; b++) { 369 char bp[48]; snprintf(bp, sizeof(bp), "/sys/block/%s", bdevs[b]); 370 if (access(bp, F_OK) == 0) { 371 int brem = is_removable(bdevs[b]); 372 ac_log("[storage] /dev/%s removable=%d\n", bdevs[b], brem); 373 } 374 } 375 } 376 // Dump init debug lines to USB from /tmp/ac-init.log (written by init) 377 // and also try kmsg as backup 378 { 379 FILE *initlog = fopen("/mnt/init.log", "w"); 380 if (initlog) { 381 // Diagnostics: what does /tmp look like? 382 fprintf(initlog, "diag: pid=%d\n", getpid()); 383 fprintf(initlog, "diag: /tmp/ac-init.log access=%d\n", 384 access("/tmp/ac-init.log", F_OK)); 385 fprintf(initlog, "diag: /tmp/cage-stderr.log access=%d\n", 386 access("/tmp/cage-stderr.log", F_OK)); 387 // List /tmp contents 388 DIR *tmpdir = opendir("/tmp"); 389 if (tmpdir) { 390 struct dirent *te; 391 while ((te = readdir(tmpdir)) != NULL) { 392 if (te->d_name[0] != '.') 393 fprintf(initlog, "diag: /tmp/%s\n", te->d_name); 394 } 395 closedir(tmpdir); 396 } else { 397 fprintf(initlog, "diag: opendir /tmp failed\n"); 398 } 399 // Primary: read tmpfs log written by init script 400 FILE *tmplog = fopen("/tmp/ac-init.log", "r"); 401 if (tmplog) { 402 char kbuf[512]; 403 while (fgets(kbuf, sizeof(kbuf), tmplog)) 404 fputs(kbuf, initlog); 405 fclose(tmplog); 406 } 407 // Also dump cage stderr if it exists 408 FILE *cage_err = fopen("/tmp/cage-stderr.log", "r"); 409 if (cage_err) { 410 fprintf(initlog, "--- cage stderr ---\n"); 411 char kbuf[512]; 412 while (fgets(kbuf, sizeof(kbuf), cage_err)) 413 fputs(kbuf, initlog); 414 fclose(cage_err); 415 } 416 // Backup: scan kmsg for ac-init lines 417 int kmsg = open("/dev/kmsg", O_RDONLY | O_NONBLOCK); 418 if (kmsg >= 0) { 419 lseek(kmsg, 0, SEEK_SET); 420 char kbuf[512]; 421 ssize_t r; 422 int found = 0; 423 while ((r = read(kmsg, kbuf, sizeof(kbuf) - 1)) > 0) { 424 kbuf[r] = 0; 425 if (strstr(kbuf, "ac-init:")) { 426 if (!found) { fprintf(initlog, "--- kmsg ---\n"); found = 1; } 427 char *msg = strstr(kbuf, "ac-init:"); 428 fprintf(initlog, "%s\n", msg); 429 } 430 } 431 close(kmsg); 432 } 433 fflush(initlog); 434 fsync(fileno(initlog)); 435 fclose(initlog); 436 } 437 } 438 return; 439 } 440 umount("/mnt"); 441 } 442 fprintf(stderr, "[ac-native] Log mount failed: %s\n", devs[i]); 443 } 444 } 445 // Fallback: log to tmpfs (won't survive reboot but stderr goes to console) 446 fprintf(stderr, "[ac-native] No USB log mount available\n"); 447} 448 449// ── Persistent machine ID ── 450// Generated on first boot, read back on subsequent boots. 451// Accessible from js-bindings.c via extern. 452char g_machine_id[64] = {0}; 453static ACMachines g_machines = {0}; 454 455static void init_machine_id(void) { 456 FILE *f = fopen("/mnt/.machine-id", "r"); 457 if (f) { 458 if (fgets(g_machine_id, sizeof(g_machine_id), f)) { 459 char *nl = strchr(g_machine_id, '\n'); 460 if (nl) *nl = '\0'; 461 } 462 fclose(f); 463 ac_log("[machine] ID loaded: %s\n", g_machine_id); 464 } else { 465 unsigned int rval = 0; 466 FILE *urand = fopen("/dev/urandom", "r"); 467 if (urand) { 468 fread(&rval, sizeof(rval), 1, urand); 469 fclose(urand); 470 } else { 471 rval = (unsigned int)(time(NULL) ^ getpid()); 472 } 473 snprintf(g_machine_id, sizeof(g_machine_id), "ac-%08x", rval); 474 f = fopen("/mnt/.machine-id", "w"); 475 if (f) { 476 fprintf(f, "%s\n", g_machine_id); 477 fclose(f); 478 ac_log("[machine] New ID generated: %s\n", g_machine_id); 479 } else { 480 ac_log("[machine] WARNING: Could not write /mnt/.machine-id\n"); 481 } 482 } 483} 484 485// Forward declarations for time-of-day functions (defined later) 486static int get_la_offset(void); 487static int get_la_hour(void); 488 489// Global display pointer — exposed to js-bindings for browser DRM handoff 490void *g_display = NULL; 491 492#ifdef USE_WAYLAND 493// Global Wayland display — used by ac_display_present dispatch 494static ACWaylandDisplay *g_wayland_display = NULL; 495#endif 496 497// Unified display present — dispatches to Wayland or DRM backend 498static void ac_display_present(ACDisplay *display, ACFramebuffer *screen, int scale) { 499#ifdef USE_WAYLAND 500 if (g_wayland_display) { 501 wayland_display_present(g_wayland_display, screen, scale); 502 return; 503 } 504#endif 505 display_present(display, screen, scale); 506} 507 508// DRM master release/acquire (defined in drm-display.c) 509extern int drm_release_master(void *display); 510extern int drm_acquire_master(void *display); 511 512// Boot title — defaults to "notepat", overridden by config.json handle 513int wifi_disabled = 0; // set from config.json "wifi":false (extern'd in js-bindings.c) 514static char boot_title[80] = "notepat"; 515static ACColor boot_title_colors[80]; 516static int boot_title_colors_len = 0; 517 518static uint8_t clamp_u8(int v) { 519 if (v < 0) return 0; 520 if (v > 255) return 255; 521 return (uint8_t)v; 522} 523 524static int parse_config_string(const char *json, const char *key, char *out, int out_sz) { 525 if (!json || !key || !out || out_sz < 2) return 0; 526 const char *kp = strstr(json, key); 527 if (!kp) return 0; 528 const char *colon = strchr(kp, ':'); 529 if (!colon) return 0; 530 const char *q1 = strchr(colon, '"'); 531 if (!q1) return 0; 532 const char *q2 = strchr(q1 + 1, '"'); 533 if (!q2) return 0; 534 int len = (int)(q2 - q1 - 1); 535 if (len <= 0 || len >= out_sz) return 0; 536 memcpy(out, q1 + 1, len); 537 out[len] = 0; 538 return 1; 539} 540 541static int parse_config_bool(const char *json, const char *key, int *out) { 542 if (!json || !key || !out) return 0; 543 const char *kp = strstr(json, key); 544 if (!kp) return 0; 545 const char *colon = strchr(kp, ':'); 546 if (!colon) return 0; 547 const char *p = colon + 1; 548 while (*p == ' ' || *p == '\t') p++; 549 if (strncmp(p, "true", 4) == 0) { *out = 1; return 1; } 550 if (strncmp(p, "false", 5) == 0) { *out = 0; return 1; } 551 return 0; 552} 553 554static int parse_json_int_field(const char *start, const char *limit, const char *key, int *out) { 555 if (!start || !limit || !key || !out || start >= limit) return 0; 556 const char *kp = strstr(start, key); 557 if (!kp || kp >= limit) return 0; 558 const char *colon = strchr(kp, ':'); 559 if (!colon || colon >= limit) return 0; 560 const char *p = colon + 1; 561 while (p < limit && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; 562 if (p >= limit) return 0; 563 char *endp = NULL; 564 long v = strtol(p, &endp, 10); 565 if (!endp || endp == p || endp > limit) return 0; 566 *out = (int)v; 567 return 1; 568} 569 570static void parse_boot_title_colors(const char *json) { 571 boot_title_colors_len = 0; 572 if (!json) return; 573 574 // Prefer explicit title_colors, fallback to handle colors from API payload. 575 const char *cp = strstr(json, "\"title_colors\""); 576 if (!cp) cp = strstr(json, "\"colors\""); 577 if (!cp) return; 578 579 const char *arr0 = strchr(cp, '['); 580 if (!arr0) return; 581 const char *arr1 = strchr(arr0 + 1, ']'); 582 if (!arr1) return; 583 584 const char *p = arr0 + 1; 585 while (p < arr1 && boot_title_colors_len < (int)(sizeof(boot_title_colors) / sizeof(boot_title_colors[0]))) { 586 const char *obj0 = strchr(p, '{'); 587 if (!obj0 || obj0 >= arr1) break; 588 const char *obj1 = strchr(obj0, '}'); 589 if (!obj1 || obj1 > arr1) break; 590 591 int r = -1, g = -1, b = -1; 592 if (parse_json_int_field(obj0, obj1, "\"r\"", &r) && 593 parse_json_int_field(obj0, obj1, "\"g\"", &g) && 594 parse_json_int_field(obj0, obj1, "\"b\"", &b)) { 595 int i = boot_title_colors_len++; 596 boot_title_colors[i] = (ACColor){clamp_u8(r), clamp_u8(g), clamp_u8(b), 255}; 597 } 598 p = obj1 + 1; 599 } 600} 601 602static void load_boot_visual_config(void) { 603 // Try USB/HD config first, fall back to initramfs-baked default 604 FILE *cfg = fopen("/mnt/config.json", "r"); 605 if (!cfg) cfg = fopen("/default-config.json", "r"); 606 if (!cfg) return; 607 608 char buf[32768] = {0}; 609 size_t n = fread(buf, 1, sizeof(buf) - 1, cfg); 610 fclose(cfg); 611 buf[n] = '\0'; 612 613 // Skip identity block marker line if present ("AC_IDENTITY_BLOCK_V1\n") 614 char *json = buf; 615 if (strncmp(buf, "AC_IDENTITY_BLOCK_V1", 20) == 0) { 616 char *nl = strchr(buf, '\n'); 617 if (nl) json = nl + 1; 618 } 619 620 char handle[64] = {0}; 621 if (parse_config_string(json, "\"handle\"", handle, sizeof(handle))) { 622 setenv("AC_HANDLE", handle, 1); 623 if ((int)strlen(handle) < (int)sizeof(boot_title) - 20) { 624 // Time-of-day greeting based on LA time 625 int hour = get_la_hour(); 626 const char *greeting; 627 if (hour >= 5 && hour < 12) greeting = "good morning"; 628 else if (hour >= 12 && hour < 17) greeting = "good afternoon"; 629 else greeting = "good evening"; 630 snprintf(boot_title, sizeof(boot_title), "%s @%s", greeting, handle); 631 } 632 } 633 parse_boot_title_colors(json); 634 635 // Read wifi flag (default: enabled) 636 int wifi_val = 1; 637 if (parse_config_bool(json, "\"wifi\"", &wifi_val)) { 638 wifi_disabled = !wifi_val; 639 ac_log("[config] wifi: %s\n", wifi_disabled ? "disabled" : "enabled"); 640 } 641 642 // Bake Claude/GitHub tokens early so boot-fade badge check (access()) 643 // succeeds while the fade is animating. The duplicate in main() runs 644 // after the fade and was leaving the badges invisible. 645 { 646 char ct[512] = {0}, gp[256] = {0}; 647 if (parse_config_string(json, "\"claudeToken\"", ct, sizeof(ct)) && ct[0]) { 648 FILE *tf = fopen("/claude-token", "w"); 649 if (tf) { fputs(ct, tf); fclose(tf); } 650 ac_log("[tokens] claude token from config (%d bytes)\n", (int)strlen(ct)); 651 } 652 if (parse_config_string(json, "\"githubPat\"", gp, sizeof(gp)) && gp[0]) { 653 FILE *gf = fopen("/github-pat", "w"); 654 if (gf) { fputs(gp, gf); fclose(gf); } 655 ac_log("[tokens] github pat from config (%d bytes)\n", (int)strlen(gp)); 656 } 657 } 658 659 // Extract claudeCreds JSON and write to /tmp for PTY to pick up 660 const char *cc = strstr(buf, "\"claudeCreds\""); 661 if (cc) { 662 const char *start = strchr(cc, '{'); 663 if (start) { 664 int depth = 0; 665 const char *end = start; 666 while (*end) { 667 if (*end == '{') depth++; 668 else if (*end == '}') { depth--; if (depth == 0) { end++; break; } } 669 end++; 670 } 671 if (depth == 0 && end > start) { 672 mkdir("/tmp/.claude", 0755); 673 FILE *cf = fopen("/tmp/.claude/.credentials.json", "w"); 674 if (cf) { 675 fwrite(start, 1, end - start, cf); 676 fclose(cf); 677 ac_log("[ac-native] Claude credentials written (%d bytes)\n", (int)(end - start)); 678 } 679 } 680 } 681 } 682 683 // Extract claudeState JSON and write to /tmp/.claude.json 684 const char *cs = strstr(buf, "\"claudeState\""); 685 if (cs) { 686 const char *start = strchr(cs, '{'); 687 if (start) { 688 int depth = 0; 689 const char *end = start; 690 while (*end) { 691 if (*end == '{') depth++; 692 else if (*end == '}') { depth--; if (depth == 0) { end++; break; } } 693 end++; 694 } 695 if (depth == 0 && end > start) { 696 FILE *sf = fopen("/tmp/.claude.json", "w"); 697 if (sf) { 698 fwrite(start, 1, end - start, sf); 699 fclose(sf); 700 ac_log("[ac-native] Claude state written (%d bytes)\n", (int)(end - start)); 701 } 702 } 703 } 704 } 705 706 ac_log("[ac-native] Boot title: %s (colors=%d)\n", boot_title, boot_title_colors_len); 707} 708 709static ACColor rainbow_title_color(int ci, int frame, int alpha) { 710 double hue = fmod((double)ci / 7.0 * 360.0 + frame * 2.0, 360.0); 711 double h6 = hue / 60.0; 712 int hi = (int)h6 % 6; 713 double fr = h6 - (int)h6; 714 double sv = 0.7, vv = 1.0; 715 double p = vv * (1.0 - sv), q = vv * (1.0 - sv * fr), tt = vv * (1.0 - sv * (1.0 - fr)); 716 double cr, cg, cb; 717 switch (hi) { 718 case 0: cr = vv; cg = tt; cb = p; break; 719 case 1: cr = q; cg = vv; cb = p; break; 720 case 2: cr = p; cg = vv; cb = tt; break; 721 case 3: cr = p; cg = q; cb = vv; break; 722 case 4: cr = tt; cg = p; cb = vv; break; 723 default: cr = vv; cg = p; cb = q; break; 724 } 725 return (ACColor){(uint8_t)(cr * 255), (uint8_t)(cg * 255), (uint8_t)(cb * 255), clamp_u8(alpha)}; 726} 727 728static ACColor title_char_color(int ci, int frame, int alpha) { 729 if (boot_title_colors_len <= 0) return rainbow_title_color(ci, frame, alpha); 730 731 int title_len = (int)strlen(boot_title); 732 int idx = ci; 733 // Map palette to the handle portion (everything after @) 734 const char *at = strchr(boot_title, '@'); 735 int handle_start = at ? (int)(at - boot_title) + 1 : 0; // char after @ 736 if (handle_start > 0 && ci >= handle_start && boot_title_colors_len > 0) { 737 idx = ci - handle_start; 738 } else if (ci < handle_start) { 739 // greeting prefix and "@" get rainbow colors 740 return rainbow_title_color(ci, frame, alpha); 741 } 742 if (idx < 0) idx = 0; 743 if (boot_title_colors_len > 0) idx %= boot_title_colors_len; 744 ACColor c = boot_title_colors[idx]; 745 746 // Keep custom colors visible over dark boot backgrounds. 747 int pulse = (int)(18.0 * sin((double)(frame + ci * 6) * 0.08)); 748 int r = (c.r * 7 + 255 * 3) / 10 + pulse; 749 int g = (c.g * 7 + 255 * 3) / 10 + pulse; 750 int b = (c.b * 7 + 255 * 3) / 10 + pulse; 751 return (ACColor){clamp_u8(r), clamp_u8(g), clamp_u8(b), clamp_u8(alpha)}; 752} 753 754// Forward declarations 755static void draw_boot_status(ACGraph *graph, ACFramebuffer *screen, 756 ACDisplay *display, const char *status, int pixel_scale); 757 758// Check if a block device is removable (USB = 1, internal = 0) 759static int is_removable(const char *blkname) { 760 char path[128]; 761 snprintf(path, sizeof(path), "/sys/block/%s/removable", blkname); 762 FILE *f = fopen(path, "r"); 763 if (!f) return -1; // unknown 764 int val = 0; 765 if (fscanf(f, "%d", &val) != 1) val = -1; 766 fclose(f); 767 return val; 768} 769 770// Copy a file from src to dst path, returns bytes copied or -1 on error 771static long copy_file(const char *src, const char *dst) { 772 FILE *in = fopen(src, "rb"); 773 if (!in) { 774 ac_log("[copy_file] cannot open src %s: errno=%d\n", src, errno); 775 return -1; 776 } 777 FILE *out = fopen(dst, "wb"); 778 if (!out) { 779 ac_log("[copy_file] cannot open dst %s: errno=%d\n", dst, errno); 780 fclose(in); 781 return -1; 782 } 783 char buf[65536]; 784 long total = 0; 785 size_t n; 786 while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { 787 size_t written = fwrite(buf, 1, n, out); 788 if (written != n) { 789 ac_log("[copy_file] write failed at offset %ld: wanted %zu got %zu errno=%d\n", 790 total, n, written, errno); 791 fclose(out); 792 fclose(in); 793 return -1; 794 } 795 total += n; 796 } 797 if (ferror(in)) { 798 ac_log("[copy_file] read failed from %s: errno=%d\n", src, errno); 799 fclose(out); 800 fclose(in); 801 return -1; 802 } 803 if (fflush(out) != 0) { 804 ac_log("[copy_file] fflush failed: errno=%d\n", errno); 805 fclose(out); 806 fclose(in); 807 return -1; 808 } 809 if (fsync(fileno(out)) != 0) { 810 ac_log("[copy_file] fsync failed: errno=%d\n", errno); 811 fclose(out); 812 fclose(in); 813 return -1; 814 } 815 fclose(out); 816 fclose(in); 817 return total; 818} 819 820// Install failure reason — set by auto_install_to_hd, displayed on failure screen 821static char install_fail_reason[256] = ""; 822static char install_fail_detail[256] = ""; 823 824// Unmount every entry in /proc/mounts whose SOURCE device matches 825// /dev/<parent> or any of its partitions. Loops until /proc/mounts no longer 826// shows any matching entry — necessary for mount-stacked /mnt where the 827// topmost fs might not be the one we want to unmount. Each iteration finds 828// ONE matching target, umount2()'s it (MNT_DETACH pops the topmost mount at 829// that path, which may or may not be the one we targeted — but after enough 830// iterations the stack empties of any matching entries). Returns total 831// successful umounts. 832static int force_unmount_disk(const char *parent_blk, const char *dlog_path) { 833 int unmounted = 0; 834 FILE *dl = dlog_path ? fopen(dlog_path, "a") : NULL; 835 if (dl) fprintf(dl, "--- force_unmount_disk(%s) start ---\n", parent_blk); 836 837 // First pass: log the full /proc/mounts so we can see the initial stack 838 { 839 FILE *mp = fopen("/proc/mounts", "r"); 840 if (mp && dl) { 841 fprintf(dl, "--- initial /proc/mounts ---\n"); 842 char line[512]; 843 while (fgets(line, sizeof(line), mp)) fputs(line, dl); 844 fprintf(dl, "--- end /proc/mounts ---\n"); 845 fclose(mp); 846 } else if (mp) { 847 fclose(mp); 848 } 849 } 850 851 const int MAX_ITER = 16; // belt-and-suspenders against mount-stack depth 852 for (int iter = 0; iter < MAX_ITER; iter++) { 853 // Find ONE target whose source starts with /dev/<parent> 854 char target[128] = ""; 855 char source[128] = ""; 856 FILE *mp = fopen("/proc/mounts", "r"); 857 if (!mp) break; 858 char line[512]; 859 while (fgets(line, sizeof(line), mp)) { 860 char src[128], tgt[128], fst[64]; 861 if (sscanf(line, "%127s %127s %63s", src, tgt, fst) != 3) continue; 862 if (strncmp(src, "/dev/", 5) != 0) continue; 863 if (strncmp(src + 5, parent_blk, strlen(parent_blk)) != 0) continue; 864 strncpy(target, tgt, sizeof(target) - 1); 865 target[sizeof(target) - 1] = 0; 866 strncpy(source, src, sizeof(source) - 1); 867 source[sizeof(source) - 1] = 0; 868 break; 869 } 870 fclose(mp); 871 if (!target[0]) { 872 if (dl) fprintf(dl, "iter=%d: no matching mounts remain — done\n", iter); 873 break; 874 } 875 // Umount by path (MNT_DETACH pops the topmost mount, which may not be 876 // the one we scanned — that's ok, we loop and re-scan). 877 int r = umount2(target, MNT_DETACH); 878 if (dl) fprintf(dl, "iter=%d: umount2(%s, MNT_DETACH) src=%s = %d errno=%d\n", 879 iter, target, source, r, r < 0 ? errno : 0); 880 ac_log("[install] iter=%d umount2(%s) src=%s rc=%d errno=%d\n", 881 iter, target, source, r, r < 0 ? errno : 0); 882 if (r == 0) { 883 unmounted++; 884 } else { 885 // Try without MNT_DETACH as a fallback (plain umount). 886 r = umount2(target, 0); 887 if (dl) fprintf(dl, "iter=%d: umount2(%s, 0) fallback = %d errno=%d\n", 888 iter, target, r, r < 0 ? errno : 0); 889 if (r != 0) break; 890 unmounted++; 891 } 892 // Tiny yield so the kernel can finish detaching before we re-scan. 893 usleep(50000); 894 } 895 896 if (dl) { 897 fprintf(dl, "--- force_unmount_disk(%s) total=%d ---\n", parent_blk, unmounted); 898 // Log post-unmount /proc/mounts so we can verify the stack is clean. 899 FILE *mp = fopen("/proc/mounts", "r"); 900 if (mp) { 901 fprintf(dl, "--- post /proc/mounts ---\n"); 902 char line[512]; 903 while (fgets(line, sizeof(line), mp)) fputs(line, dl); 904 fprintf(dl, "--- end /proc/mounts ---\n"); 905 fclose(mp); 906 } 907 fclose(dl); 908 } 909 return unmounted; 910} 911 912// Try BLKRRPART on a disk in a retry loop. Lazy unmounts take time to release 913// the device fully — the kernel keeps the block device referenced until all 914// superblock cleanups finish. We retry with backoff: 0 → 250ms → 500ms → 1s → 915// 2s. Returns 0 on success, -1 on failure (errno set by last attempt). 916// Also logs each attempt to dlog_path if provided. 917static int blkrrpart_with_retry(const char *disk_path, const char *dlog_path) { 918 int delays_ms[] = {0, 250, 500, 1000, 2000}; 919 FILE *dl = dlog_path ? fopen(dlog_path, "a") : NULL; 920 int last_errno = 0; 921 for (int i = 0; i < (int)(sizeof(delays_ms) / sizeof(delays_ms[0])); i++) { 922 if (delays_ms[i] > 0) { 923 sync(); 924 usleep(delays_ms[i] * 1000); 925 } 926 int dfd = open(disk_path, O_RDONLY | O_CLOEXEC); 927 if (dfd < 0) { 928 last_errno = errno; 929 if (dl) fprintf(dl, "BLKRRPART try %d: open failed errno=%d\n", i, last_errno); 930 continue; 931 } 932 int ri = ioctl(dfd, BLKRRPART); 933 last_errno = ri < 0 ? errno : 0; 934 close(dfd); 935 if (dl) fprintf(dl, "BLKRRPART try %d (delay=%dms) rc=%d errno=%d (%s)\n", 936 i, delays_ms[i], ri, last_errno, 937 ri < 0 ? strerror(last_errno) : "ok"); 938 ac_log("[install] BLKRRPART %s try=%d delay=%dms rc=%d errno=%d\n", 939 disk_path, i, delays_ms[i], ri, last_errno); 940 if (ri == 0) { 941 if (dl) fclose(dl); 942 return 0; 943 } 944 } 945 // Last resort: sysfs rescan (NVMe has /sys/class/nvme/nvme*/rescan_controller 946 // and every block device has /sys/block/<dev>/uevent which triggers a 947 // udev change event that forces partition rescan). 948 char sys_uevent[128]; 949 snprintf(sys_uevent, sizeof(sys_uevent), "/sys/block/%s/uevent", 950 disk_path + 5); // skip "/dev/" 951 FILE *ue = fopen(sys_uevent, "w"); 952 if (ue) { 953 fprintf(ue, "change\n"); 954 fclose(ue); 955 if (dl) fprintf(dl, "wrote 'change' to %s\n", sys_uevent); 956 ac_log("[install] triggered sysfs change on %s\n", sys_uevent); 957 // Give udev time to process 958 usleep(500000); 959 } else if (dl) { 960 fprintf(dl, "sysfs fallback open(%s) failed errno=%d\n", sys_uevent, errno); 961 } 962 if (dl) fclose(dl); 963 errno = last_errno; 964 return -1; 965} 966 967// Score removable install sources by how much of the current boot payload they 968// contain. Higher scores are preferred during W-to-install so we choose the 969// universal ACEFI partition over the simpler ACBOOT fallback when both exist. 970// 3 = universal layout (BOOTX64 + LOADER + KERNEL + initramfs + loader entry) 971// 2 = chainloader layout (BOOTX64 + KERNEL) 972// 1 = monolithic layout (BOOTX64 only) 973// 0 = not a usable install source 974static int install_source_layout_score(const char *mountpoint) { 975 char bootx64[128]; 976 char loader[128]; 977 char kernel[128]; 978 char initramfs_gz[128]; 979 char initramfs_lz4[128]; 980 char loader_entry[160]; 981 982 snprintf(bootx64, sizeof(bootx64), "%s/EFI/BOOT/BOOTX64.EFI", mountpoint); 983 snprintf(loader, sizeof(loader), "%s/EFI/BOOT/LOADER.EFI", mountpoint); 984 snprintf(kernel, sizeof(kernel), "%s/EFI/BOOT/KERNEL.EFI", mountpoint); 985 snprintf(initramfs_gz, sizeof(initramfs_gz), "%s/initramfs.cpio.gz", mountpoint); 986 snprintf(initramfs_lz4, sizeof(initramfs_lz4), "%s/initramfs.cpio.lz4", mountpoint); 987 snprintf(loader_entry, sizeof(loader_entry), "%s/loader/entries/ac-native.conf", mountpoint); 988 989 if (access(bootx64, F_OK) == 0 && 990 access(loader, F_OK) == 0 && 991 access(kernel, F_OK) == 0 && 992 (access(initramfs_gz, F_OK) == 0 || access(initramfs_lz4, F_OK) == 0) && 993 access(loader_entry, F_OK) == 0) { 994 return 3; 995 } 996 if (access(bootx64, F_OK) == 0 && access(kernel, F_OK) == 0) return 2; 997 if (access(bootx64, F_OK) == 0) return 1; 998 return 0; 999} 1000 1001// Auto-install kernel to internal drive's EFI System Partition 1002// Returns 1 on success, 0 on failure (sets install_fail_reason/detail). 1003static int auto_install_to_hd(ACGraph *graph, ACFramebuffer *screen, 1004 ACDisplay *display, int pixel_scale) { 1005 char source_mount[32] = "/mnt"; 1006 char source_dev[32] = ""; 1007 int source_mounted_tmp = 0; 1008 char kernel_src[96] = ""; 1009 install_fail_reason[0] = '\0'; 1010 install_fail_detail[0] = '\0'; 1011 char bootloader_src[96] = ""; 1012 char loader_src[96] = ""; 1013 char chain_kernel_src[96] = ""; 1014 char initramfs_src[96] = ""; 1015 char install_kernel_src[96] = ""; 1016 char config_src[64] = ""; 1017 char loader_conf_src[96] = ""; 1018 char loader_entry_src[128] = ""; 1019 1020 ac_log("[install] auto_install_to_hd starting\n"); 1021 if (display) 1022 draw_boot_status(graph, screen, display, "installing to disk...", pixel_scale); 1023 1024 // Detect source layout: monolithic (BOOTX64.EFI is kernel), chainloader 1025 // (BOOTX64.EFI boots KERNEL.EFI), or universal systemd-boot 1026 // (BOOTX64.EFI + LOADER.EFI + KERNEL.EFI + initramfs + loader entry). 1027 int systemd_boot_layout = 0; 1028 int chainloader_layout = 0; 1029 int source_score = 0; 1030 1031 // Prefer current /mnt only when it is removable and actually bootable. 1032 if (log_dev[0]) { 1033 char blk[32] = ""; 1034 get_parent_block(log_dev + 5, blk, sizeof(blk)); 1035 if (blk[0] && is_removable(blk) == 1) { 1036 source_score = install_source_layout_score("/mnt"); 1037 if (source_score > 0) { 1038 ac_log("[install] current /mnt source score=%d (%s)\n", source_score, log_dev); 1039 strncpy(source_dev, log_dev, sizeof(source_dev) - 1); 1040 source_dev[sizeof(source_dev) - 1] = '\0'; 1041 } 1042 } 1043 } 1044 1045 // Scan removable partitions and prefer the richest boot layout. This lets 1046 // W-install source from ACEFI on the new hybrid USB instead of blindly 1047 // using whichever removable partition happened to get mounted at /mnt. 1048 { 1049 const char *src_candidates[] = { 1050 "/dev/sda1", "/dev/sda2", 1051 "/dev/sdb1", "/dev/sdb2", 1052 "/dev/sdc1", "/dev/sdc2", 1053 "/dev/sdd1", "/dev/sdd2", 1054 NULL 1055 }; 1056 char best_dev[32] = ""; 1057 int best_score = source_score; 1058 mkdir("/tmp/src", 0755); 1059 for (int i = 0; src_candidates[i]; i++) { 1060 if (access(src_candidates[i], F_OK) != 0) continue; 1061 if (source_dev[0] && strcmp(src_candidates[i], source_dev) == 0) continue; 1062 char blk[32] = ""; 1063 get_parent_block(src_candidates[i] + 5, blk, sizeof(blk)); 1064 if (!blk[0] || is_removable(blk) != 1) continue; 1065 if (mount(src_candidates[i], "/tmp/src", "vfat", 0, NULL) != 0) continue; 1066 int score = install_source_layout_score("/tmp/src"); 1067 umount("/tmp/src"); 1068 if (score > best_score) { 1069 best_score = score; 1070 strncpy(best_dev, src_candidates[i], sizeof(best_dev) - 1); 1071 best_dev[sizeof(best_dev) - 1] = '\0'; 1072 } 1073 } 1074 if (best_dev[0]) { 1075 if (mount(best_dev, "/tmp/src", "vfat", 0, NULL) == 0) { 1076 strncpy(source_dev, best_dev, sizeof(source_dev) - 1); 1077 source_dev[sizeof(source_dev) - 1] = '\0'; 1078 strncpy(source_mount, "/tmp/src", sizeof(source_mount) - 1); 1079 source_mount[sizeof(source_mount) - 1] = '\0'; 1080 source_mounted_tmp = 1; 1081 source_score = best_score; 1082 ac_log("[install] selected richer removable source %s score=%d\n", 1083 source_dev, source_score); 1084 } else { 1085 ac_log("[install] failed to mount preferred source %s errno=%d\n", 1086 best_dev, errno); 1087 } 1088 } 1089 } 1090 1091 systemd_boot_layout = (source_score >= 3); 1092 chainloader_layout = (!systemd_boot_layout && source_score >= 2); 1093 1094 snprintf(bootloader_src, sizeof(bootloader_src), "%s/EFI/BOOT/BOOTX64.EFI", source_mount); 1095 snprintf(loader_src, sizeof(loader_src), "%s/EFI/BOOT/LOADER.EFI", source_mount); 1096 snprintf(chain_kernel_src, sizeof(chain_kernel_src), "%s/EFI/BOOT/KERNEL.EFI", source_mount); 1097 snprintf(kernel_src, sizeof(kernel_src), "%s/EFI/BOOT/BOOTX64.EFI", source_mount); 1098 snprintf(config_src, sizeof(config_src), "%s/config.json", source_mount); 1099 snprintf(loader_conf_src, sizeof(loader_conf_src), "%s/loader/loader.conf", source_mount); 1100 snprintf(loader_entry_src, sizeof(loader_entry_src), "%s/loader/entries/ac-native.conf", source_mount); 1101 if (access(source_dev, F_OK) == 0 && systemd_boot_layout) { 1102 char initramfs_gz[96]; 1103 char initramfs_lz4[96]; 1104 snprintf(initramfs_gz, sizeof(initramfs_gz), "%s/initramfs.cpio.gz", source_mount); 1105 snprintf(initramfs_lz4, sizeof(initramfs_lz4), "%s/initramfs.cpio.lz4", source_mount); 1106 if (access(initramfs_gz, F_OK) == 0) { 1107 strncpy(initramfs_src, initramfs_gz, sizeof(initramfs_src) - 1); 1108 initramfs_src[sizeof(initramfs_src) - 1] = '\0'; 1109 } else if (access(initramfs_lz4, F_OK) == 0) { 1110 strncpy(initramfs_src, initramfs_lz4, sizeof(initramfs_src) - 1); 1111 initramfs_src[sizeof(initramfs_src) - 1] = '\0'; 1112 } 1113 } 1114 1115 if (systemd_boot_layout) { 1116 strncpy(install_kernel_src, chain_kernel_src, sizeof(install_kernel_src) - 1); 1117 install_kernel_src[sizeof(install_kernel_src) - 1] = '\0'; 1118 ac_log("[install] detected universal systemd-boot layout\n"); 1119 } else if (chainloader_layout) { 1120 strncpy(install_kernel_src, chain_kernel_src, sizeof(install_kernel_src) - 1); 1121 install_kernel_src[sizeof(install_kernel_src) - 1] = '\0'; 1122 ac_log("[install] detected chainloader layout\n"); 1123 } else { 1124 strncpy(install_kernel_src, kernel_src, sizeof(install_kernel_src) - 1); 1125 install_kernel_src[sizeof(install_kernel_src) - 1] = '\0'; 1126 if (source_score == 1) ac_log("[install] detected monolithic layout\n"); 1127 } 1128 1129 if (!source_dev[0] || source_score == 0 || 1130 access(install_kernel_src, F_OK) != 0 || 1131 (systemd_boot_layout && 1132 (access(loader_src, F_OK) != 0 || 1133 initramfs_src[0] == '\0' || 1134 access(loader_entry_src, F_OK) != 0))) { 1135 ac_log("[install] No removable install source with kernel found\n"); 1136 snprintf(install_fail_reason, sizeof(install_fail_reason), 1137 "no USB boot source found"); 1138 // Log available block devices for diagnostics 1139 ac_log("[install] Block devices:\n"); 1140 int dpos = 0; 1141 const char *scan[] = {"sda","sdb","sdc","sdd","nvme0n1","nvme1n1","mmcblk0",NULL}; 1142 for (int i = 0; scan[i]; i++) { 1143 char dp[32]; snprintf(dp, sizeof(dp), "/sys/block/%s", scan[i]); 1144 if (access(dp, F_OK) == 0) { 1145 int rem = is_removable(scan[i]); 1146 ac_log("[install] /dev/%s removable=%d\n", scan[i], rem); 1147 dpos += snprintf(install_fail_detail + dpos, 1148 sizeof(install_fail_detail) - dpos, 1149 "/dev/%s %s ", scan[i], rem == 1 ? "(USB)" : rem == 0 ? "(int)" : "(?)"); 1150 } 1151 } 1152 if (source_mounted_tmp) umount("/tmp/src"); 1153 return 0; 1154 } 1155 1156 mkdir("/tmp/hd", 0755); 1157 1158 // Determine which block device install source is on (skip it as destination) 1159 char usb_blk[16] = ""; 1160 if (source_dev[0]) { 1161 const char *p = source_dev + 5; // skip "/dev/" 1162 int len = 0; 1163 while (p[len] && (p[len] < '0' || p[len] > '9')) len++; 1164 // For nvme: "nvme0n1p1" → parent "nvme0n1" 1165 // For sd: "sda1" → parent "sda" 1166 if (len > 0) { 1167 if (len > (int)sizeof(usb_blk) - 1) len = sizeof(usb_blk) - 1; 1168 memcpy(usb_blk, p, len); 1169 usb_blk[len] = '\0'; 1170 } 1171 } 1172 1173 // Scan for internal (non-removable) block devices with partitions. 1174 // NVMe first (always internal), then eMMC (always internal — common 1175 // on Chromebooks + budget laptops), then SATA/USB last. 1176 const char *part_candidates[] = { 1177 "nvme0n1", "nvme1n1", // NVMe SSDs 1178 "mmcblk0", "mmcblk1", // eMMC (Chromebooks, budget laptops) 1179 "sda", "sdb", "sdc", "sdd", // SATA/USB 1180 NULL 1181 }; 1182 1183 int installed = 0; 1184 for (int i = 0; part_candidates[i] && !installed; i++) { 1185 const char *blk = part_candidates[i]; 1186 1187 // Skip the USB boot device 1188 if (usb_blk[0] && strcmp(blk, usb_blk) == 0) continue; 1189 1190 // For sd* devices, skip if removable 1191 if (blk[0] == 's' && blk[1] == 'd') { 1192 int rem = is_removable(blk); 1193 if (rem == 1) continue; // removable = USB 1194 } 1195 1196 // Two-pass partition scan. Pass 0: probe p1..p16 for an existing 1197 // vfat partition (non-destructive) — finds the Chromebook ESP at 1198 // p12 before we would otherwise reformat p1 (stateful/ext4) on 1199 // Chromebooks and clobber user data. Pass 1: fall back to the 1200 // p=1 rescue reformat for stock Linux layouts where no ESP exists. 1201 for (int pass = 0; pass < 2 && !installed; pass++) { 1202 int allow_rescue_mkfs = (pass == 1); 1203 for (int p = 1; p <= 16 && !installed; p++) { 1204 char devpath[32]; 1205 // NVMe + eMMC use "p<N>" suffix (name ends in a digit); SATA 1206 // just appends the number to the base name. 1207 if (blk[0] == 'n' || strncmp(blk, "mmcblk", 6) == 0) 1208 snprintf(devpath, sizeof(devpath), "/dev/%sp%d", blk, p); 1209 else // SATA: sda1 1210 snprintf(devpath, sizeof(devpath), "/dev/%s%d", blk, p); 1211 1212 if (access(devpath, F_OK) != 0) continue; 1213 1214 // Try mounting as FAT (ESP is always FAT32) 1215 ac_log("[install] trying %s\n", devpath); 1216 if (mount(devpath, "/tmp/hd", "vfat", 0, NULL) != 0) { 1217 int mount_errno = errno; 1218 ac_log("[install] mount failed: %s (errno=%d)\n", devpath, mount_errno); 1219 // If the partition exists but has no filesystem (e.g. a prior 1220 // sfdisk created it but mkfs never succeeded), try to format 1221 // it now. Only attempt this on the first partition (which is 1222 // the ESP slot) and only if the partition is large enough. 1223 // Gated to pass==1 so we never clobber Chromebook p1 STATE 1224 // when an existing vfat ESP is available elsewhere (e.g. p12). 1225 if (p == 1 && allow_rescue_mkfs) { 1226 long long part_bytes = 0; 1227 int pfd = open(devpath, O_RDONLY | O_CLOEXEC); 1228 if (pfd >= 0) { 1229 unsigned long long sz = 0; 1230 if (ioctl(pfd, BLKGETSIZE64, &sz) == 0) part_bytes = (long long)sz; 1231 close(pfd); 1232 } 1233 long part_mb = (long)(part_bytes / 1048576LL); 1234 ac_log("[install] %s size=%ldMB — try format to rescue unmountable partition\n", 1235 devpath, part_mb); 1236 if (part_mb >= 512) { 1237 // Flush buffer cache, then mkfs 1238 int fpfd = open(devpath, O_RDONLY | O_CLOEXEC); 1239 if (fpfd >= 0) { ioctl(fpfd, BLKFLSBUF); close(fpfd); } 1240 system("echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true"); 1241 sync(); 1242 usleep(500000); 1243 char mkfs_cmd[256]; 1244 snprintf(mkfs_cmd, sizeof(mkfs_cmd), 1245 "mkfs.vfat -F 32 -n AC-NATIVE %s 2>&1", devpath); 1246 int mkrc = system(mkfs_cmd); 1247 ac_log("[install] rescue mkfs rc=%d\n", mkrc); 1248 if (WIFEXITED(mkrc) && WEXITSTATUS(mkrc) == 0) { 1249 usleep(500000); 1250 if (mount(devpath, "/tmp/hd", "vfat", 0, NULL) == 0) { 1251 ac_log("[install] rescue format + mount OK\n"); 1252 // Fall through to normal install flow below 1253 } else { 1254 ac_log("[install] rescue mount still failed\n"); 1255 continue; 1256 } 1257 } else { 1258 continue; 1259 } 1260 } else { 1261 continue; 1262 } 1263 } else { 1264 continue; 1265 } 1266 } 1267 1268 // Create EFI boot directories 1269 mkdir("/tmp/hd/EFI", 0755); 1270 mkdir("/tmp/hd/EFI/BOOT", 0755); 1271 1272 // Check free space against the full payload we plan to copy. 1273 { 1274 struct stat src_st; 1275 struct statvfs hd_vfs; 1276 long long install_bytes = 0; 1277 long long free_bytes = 0; 1278 if (stat(install_kernel_src, &src_st) == 0) install_bytes += src_st.st_size; 1279 if (chainloader_layout && stat(bootloader_src, &src_st) == 0) install_bytes += src_st.st_size; 1280 if (systemd_boot_layout) { 1281 if (stat(bootloader_src, &src_st) == 0) install_bytes += src_st.st_size; 1282 if (stat(loader_src, &src_st) == 0) install_bytes += src_st.st_size; 1283 if (stat(initramfs_src, &src_st) == 0) install_bytes += src_st.st_size; 1284 if (stat(loader_conf_src, &src_st) == 0) install_bytes += src_st.st_size; 1285 if (stat(loader_entry_src, &src_st) == 0) install_bytes += src_st.st_size; 1286 } 1287 if (stat(config_src, &src_st) == 0) install_bytes += src_st.st_size; 1288 if (statvfs("/tmp/hd", &hd_vfs) == 0) 1289 free_bytes = (long long)hd_vfs.f_bavail * (long long)hd_vfs.f_bsize; 1290 long need_mb = (long)((install_bytes + (1048576 - 1)) / 1048576) + 10; 1291 long free_mb = (long)(free_bytes / 1048576); 1292 ac_log("[install] payload=%ldMB free=%ldMB on %s\n", 1293 (long)((install_bytes + (1048576 - 1)) / 1048576), free_mb, devpath); 1294 if (install_bytes > 0 && free_bytes < install_bytes + 10LL * 1048576LL) { 1295 ac_log("[install] NOT ENOUGH SPACE — need %ldMB, have %ldMB\n", need_mb, free_mb); 1296 // Check the PARTITION SIZE (not free space). If a previous 1297 // sfdisk already expanded it to 1024MB but mkfs failed, the 1298 // partition is big enough — we just need to unmount + reformat, 1299 // no repartitioning needed (which avoids the EBUSY nightmare). 1300 long long part_size_bytes = 0; 1301 { 1302 int pfd = open(devpath, O_RDONLY | O_CLOEXEC); 1303 if (pfd >= 0) { 1304 unsigned long long sz = 0; 1305 if (ioctl(pfd, BLKGETSIZE64, &sz) == 0) 1306 part_size_bytes = (long long)sz; 1307 close(pfd); 1308 } 1309 } 1310 long part_mb = (long)(part_size_bytes / 1048576LL); 1311 ac_log("[install] partition %s size=%ldMB (need %ldMB)\n", devpath, part_mb, need_mb); 1312 1313 if (part_mb >= need_mb) { 1314 // Partition already large enough — just unmount + reformat 1315 ac_log("[install] partition big enough, skip repartition → direct reformat\n"); 1316 umount("/tmp/hd"); 1317 umount2("/tmp/hd", MNT_DETACH); 1318 // Unmount all mounts on this disk 1319 char parent_blk_tmp[32] = ""; 1320 { 1321 const char *d = devpath + 5; 1322 strncpy(parent_blk_tmp, d, sizeof(parent_blk_tmp) - 1); 1323 char *pp = strstr(parent_blk_tmp, "p"); 1324 if (pp && pp > parent_blk_tmp && *(pp-1) >= '0' && *(pp-1) <= '9' && *(pp+1) >= '1' && *(pp+1) <= '9') 1325 *pp = 0; 1326 else { 1327 int len = strlen(parent_blk_tmp); 1328 while (len > 0 && parent_blk_tmp[len-1] >= '0' && parent_blk_tmp[len-1] <= '9') len--; 1329 parent_blk_tmp[len] = 0; 1330 } 1331 } 1332 const char *DLOG = "/tmp/install-debug.log"; 1333 FILE *__dl = fopen(DLOG, "w"); 1334 if (__dl) { fprintf(__dl, "=== direct-reformat (no repartition) ===\n"); fclose(__dl); } 1335 force_unmount_disk(parent_blk_tmp, DLOG); 1336 sync(); 1337 // Flush block device cache 1338 { 1339 int pfd = open(devpath, O_RDONLY | O_CLOEXEC); 1340 if (pfd >= 0) { ioctl(pfd, BLKFLSBUF); close(pfd); } 1341 char dd[64]; 1342 snprintf(dd, sizeof(dd), "/dev/%s", parent_blk_tmp); 1343 int dfd = open(dd, O_RDONLY | O_CLOEXEC); 1344 if (dfd >= 0) { ioctl(dfd, BLKFLSBUF); close(dfd); } 1345 } 1346 system("echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true"); 1347 sync(); 1348 usleep(2000000); 1349 // Format directly 1350 char rcmd[512]; 1351 const char *MKFS_ERR = "/tmp/mkfs-err.log"; 1352 int mkfs_exit = -1; 1353 for (int mkfs_try = 1; mkfs_try <= 5 && mkfs_exit != 0; mkfs_try++) { 1354 if (mkfs_try > 1) { 1355 int pfd = open(devpath, O_RDONLY | O_CLOEXEC); 1356 if (pfd >= 0) { ioctl(pfd, BLKFLSBUF); close(pfd); } 1357 system("echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true"); 1358 sync(); 1359 usleep(3000000); 1360 } 1361 snprintf(rcmd, sizeof(rcmd), 1362 "echo '--- mkfs attempt %d ---' >> %s; " 1363 "(mkfs.vfat -F 32 -n AC-NATIVE %s > %s 2>&1; rc=$?; " 1364 "cat %s >> %s; exit $rc)", 1365 mkfs_try, DLOG, devpath, MKFS_ERR, MKFS_ERR, DLOG); 1366 int rrc = system(rcmd); 1367 mkfs_exit = WIFEXITED(rrc) ? WEXITSTATUS(rrc) : -1; 1368 ac_log("[install] direct mkfs rc=%d attempt=%d\n", rrc, mkfs_try); 1369 FILE *mf = fopen(MKFS_ERR, "r"); 1370 if (mf) { 1371 char line[512]; 1372 while (fgets(line, sizeof(line), mf)) { 1373 size_t len = strlen(line); 1374 if (len > 0 && line[len-1] == '\n') line[len-1] = '\0'; 1375 ac_log("[mkfs/%d] %s\n", mkfs_try, line); 1376 } 1377 fclose(mf); 1378 } 1379 } 1380 if (mkfs_exit == 0) { 1381 ac_log("[install] direct reformat succeeded\n"); 1382 goto install_copy_phase; 1383 } 1384 ac_log("[install] direct reformat failed, falling through to repartition\n"); 1385 } 1386 1387 // Repartition: expand ESP to 1024MB 1388 char parent_blk[32] = ""; 1389 // Extract parent device: /dev/nvme0n1p1 → nvme0n1, /dev/sda1 → sda 1390 { 1391 const char *d = devpath + 5; // skip "/dev/" 1392 strncpy(parent_blk, d, sizeof(parent_blk) - 1); 1393 // Remove partition suffix: "nvme0n1p1" → "nvme0n1", "sda1" → "sda" 1394 char *pp = strstr(parent_blk, "p"); 1395 if (pp && pp > parent_blk && *(pp-1) >= '0' && *(pp-1) <= '9' && *(pp+1) >= '1' && *(pp+1) <= '9') 1396 *pp = 0; // NVMe: nvme0n1p1 → nvme0n1 1397 else { 1398 // SATA: sda1 → sda (strip trailing digits) 1399 int len = strlen(parent_blk); 1400 while (len > 0 && parent_blk[len-1] >= '0' && parent_blk[len-1] <= '9') len--; 1401 parent_blk[len] = 0; 1402 } 1403 } 1404 ac_log("[install] repartitioning /dev/%s → 1024MB ESP\n", parent_blk); 1405 if (display) { 1406 char msg[80]; 1407 snprintf(msg, sizeof(msg), "expanding to 1024MB..."); 1408 draw_boot_status(graph, screen, display, msg, pixel_scale); 1409 } 1410 // Release every handle on the target disk BEFORE sfdisk. 1411 // The previous version relied on a shell `umount -l` loop 1412 // that (a) couldn't umount partitions by device path under 1413 // busybox, and (b) left enough lazy-mount residue that 1414 // BLKRRPART returned EBUSY every time. The new helpers 1415 // parse /proc/mounts in C and umount2 each target 1416 // explicitly, then retry BLKRRPART with backoff. 1417 umount("/tmp/hd"); 1418 umount2("/tmp/hd", MNT_DETACH); 1419 // Write install-debug.log to TMPFS so it survives even 1420 // if the unmount loop pops the whole /mnt stack. We copy 1421 // it back to /mnt/install-debug.log at the end so the 1422 // USB has a post-mortem trace. 1423 const char *DLOG = "/tmp/install-debug.log"; 1424 // Fresh log per attempt 1425 FILE *__dl = fopen(DLOG, "w"); 1426 if (__dl) { 1427 fprintf(__dl, "=== install-debug starting ===\n"); 1428 fprintf(__dl, "parent_blk=%s devpath=%s\n", parent_blk, devpath); 1429 fclose(__dl); 1430 } 1431 // Unmount every mount currently referencing this disk. 1432 int n_unmounted = force_unmount_disk(parent_blk, DLOG); 1433 ac_log("[install] force_unmount_disk(%s) released %d mounts\n", 1434 parent_blk, n_unmounted); 1435 sync(); 1436 1437 // CRITICAL: After MNT_DETACH (lazy unmount), the kernel 1438 // VFS still holds block device references from cached 1439 // dentries/inodes/superblock. We MUST flush buffer cache 1440 // on both the partition and whole disk via BLKFLSBUF, 1441 // then drop page caches, before the block layer will 1442 // release its exclusive hold. Without this, BLKRRPART 1443 // and mkfs both fail with EBUSY. 1444 { 1445 // Flush buffer cache on partition 1446 int pfd = open(devpath, O_RDONLY | O_CLOEXEC); 1447 if (pfd >= 0) { 1448 ioctl(pfd, BLKFLSBUF); 1449 close(pfd); 1450 ac_log("[install] BLKFLSBUF on %s: ok\n", devpath); 1451 } 1452 // Flush buffer cache on whole disk 1453 char disk_dev[64]; 1454 snprintf(disk_dev, sizeof(disk_dev), "/dev/%s", parent_blk); 1455 int dfd = open(disk_dev, O_RDONLY | O_CLOEXEC); 1456 if (dfd >= 0) { 1457 ioctl(dfd, BLKFLSBUF); 1458 close(dfd); 1459 ac_log("[install] BLKFLSBUF on %s: ok\n", disk_dev); 1460 } 1461 // Drop all page/dentry/inode caches 1462 system("echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true"); 1463 sync(); 1464 ac_log("[install] caches flushed + dropped\n"); 1465 } 1466 usleep(1000000); // 1s settle after cache flush 1467 1468 // Nuke the old filesystem signatures BEFORE sfdisk. This 1469 // is critical: the old Fedora ESP's FAT boot sector and 1470 // GPT partition UUID are still on disk, and mkfs.vfat 1471 // later hits EBUSY trying to get O_EXCL because the 1472 // kernel's block device cache still maps to the old FS. 1473 // dd'ing zeros over the first 64KB removes the old FAT 1474 // signature + GPT primary header. The backup GPT at the 1475 // end of the disk also needs clearing but sfdisk --force 1476 // will overwrite it. busybox dd is in initramfs. 1477 char rcmd[512]; 1478 snprintf(rcmd, sizeof(rcmd), 1479 "echo '--- wiping old FS signatures ---' >> %s; " 1480 "dd if=/dev/zero of=/dev/%s bs=512 count=128 conv=fsync 2>&1 | tee -a %s; " 1481 "sync", 1482 DLOG, parent_blk, DLOG); 1483 system(rcmd); 1484 usleep(500000); 1485 1486 // Repartition: create 1024MB EFI System Partition. 1487 // CRITICAL: --no-reread tells sfdisk NOT to call BLKRRPART 1488 // itself. The kernel was failing BLKRRPART with EBUSY 1489 // because the block device cache still holds references 1490 // from the old filesystem, and that blocked sfdisk too. 1491 // With --no-reread, sfdisk writes the partition table 1492 // and returns cleanly; we refresh partitions explicitly 1493 // via partx -u below, which is non-destructive and 1494 // works even when the device is "in use" at the kernel 1495 // level. 1496 snprintf(rcmd, sizeof(rcmd), 1497 "{ echo 'label: gpt'; echo 'type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, size=1024M'; } | sfdisk --force --no-reread /dev/%s >> %s 2>&1", 1498 parent_blk, DLOG); 1499 int rrc = system(rcmd); 1500 ac_log("[install] sfdisk --no-reread rc=%d\n", rrc); 1501 // Give the kernel time to process the new GPT 1502 sync(); 1503 usleep(500000); 1504 1505 // Refresh the kernel's partition table. Primary method: 1506 // BLKRRPART ioctl (no external binary needed). Falls back 1507 // to partx/partprobe if available for belt-and-suspenders. 1508 { 1509 char disk_dev[64]; 1510 snprintf(disk_dev, sizeof(disk_dev), "/dev/%s", parent_blk); 1511 ac_log("[install] BLKRRPART on %s...\n", disk_dev); 1512 int brr = blkrrpart_with_retry(disk_dev, DLOG); 1513 ac_log("[install] BLKRRPART result=%d\n", brr); 1514 } 1515 // Also try partx/partprobe as secondary refresh (may not 1516 // be in initramfs — that's OK, || true swallows the error). 1517 snprintf(rcmd, sizeof(rcmd), 1518 "echo '--- partx refresh ---' >> %s; " 1519 "partx -u /dev/%s >> %s 2>&1 || true; " 1520 "partprobe /dev/%s >> %s 2>&1 || true; " 1521 "sfdisk --verify /dev/%s >> %s 2>&1 || true; " 1522 "ls -l /dev/%s* >> %s 2>&1 || true", 1523 DLOG, parent_blk, DLOG, parent_blk, DLOG, 1524 parent_blk, DLOG, parent_blk, DLOG); 1525 system(rcmd); 1526 // sfdisk wrote a single partition → the new ESP is p1 on 1527 // the parent disk, regardless of what partition index we 1528 // entered this branch with. Chromebook case: we arrived 1529 // via devpath=/dev/mmcblk1p12 (existing 16MB EFI-SYSTEM), 1530 // sfdisk rewrote the table to a single 1024MB entry at 1531 // p1 and wiped p12 entirely — so mkfs + mount need to 1532 // target the new partition, not the old devpath. 1533 char new_devpath[48]; 1534 if (parent_blk[0] == 'n' || strncmp(parent_blk, "mmcblk", 6) == 0) { 1535 snprintf(new_devpath, sizeof(new_devpath), "/dev/%sp1", parent_blk); 1536 } else { 1537 snprintf(new_devpath, sizeof(new_devpath), "/dev/%s1", parent_blk); 1538 } 1539 if (strcmp(new_devpath, devpath) != 0) { 1540 ac_log("[install] canonicalizing devpath %s → %s after sfdisk\n", 1541 devpath, new_devpath); 1542 strncpy(devpath, new_devpath, sizeof(devpath) - 1); 1543 devpath[sizeof(devpath) - 1] = '\0'; 1544 } 1545 // Wait for partition device node to appear (up to 10s) 1546 int devpath_ready = 0; 1547 for (int wait = 0; wait < 20; wait++) { 1548 usleep(500000); 1549 struct stat st; 1550 if (stat(devpath, &st) == 0 && S_ISBLK(st.st_mode)) { 1551 ac_log("[install] device %s ready after %dms\n", devpath, (wait+1)*500); 1552 devpath_ready = 1; 1553 break; 1554 } 1555 if (wait % 4 == 0) ac_log("[install] waiting for %s... (%d)\n", devpath, wait); 1556 } 1557 if (!devpath_ready) { 1558 ac_log("[install] device %s never appeared after sfdisk — see %s\n", devpath, DLOG); 1559 } 1560 // Wipe any remaining FS signature. Try wipefs first; if 1561 // it's missing from initramfs, fall back to dd'ing zeros 1562 // over the first 4KB of the partition (covers FAT BPB, 1563 // ext superblock, and any other FS magic). 1564 snprintf(rcmd, sizeof(rcmd), 1565 "echo '--- wipefs partition ---' >> %s; " 1566 "if command -v wipefs >/dev/null 2>&1; then " 1567 " wipefs -a %s >> %s 2>&1; " 1568 "else " 1569 " echo 'wipefs not found, using dd fallback' >> %s; " 1570 " dd if=/dev/zero of=%s bs=512 count=8 conv=fsync >> %s 2>&1; " 1571 "fi", 1572 DLOG, devpath, DLOG, DLOG, devpath, DLOG); 1573 system(rcmd); 1574 // Aggressively flush block device buffer cache + page cache 1575 // to release the kernel's exclusive hold on the partition. 1576 { 1577 int pfd = open(devpath, O_RDONLY | O_CLOEXEC); 1578 if (pfd >= 0) { ioctl(pfd, BLKFLSBUF); close(pfd); } 1579 char disk_dev2[64]; 1580 snprintf(disk_dev2, sizeof(disk_dev2), "/dev/%s", parent_blk); 1581 int dfd = open(disk_dev2, O_RDONLY | O_CLOEXEC); 1582 if (dfd >= 0) { ioctl(dfd, BLKFLSBUF); close(dfd); } 1583 } 1584 system("echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true"); 1585 sync(); 1586 ac_log("[install] pre-mkfs cache flush done\n"); 1587 // Long settle — let the kernel + nvme driver fully release 1588 // the device. 5 seconds to be safe. 1589 usleep(5000000); 1590 1591 // Reformat. Retry up to 3 times if mkfs fails — the 1592 // kernel sometimes needs multiple passes after a fresh 1593 // GPT to release exclusive holds on the block device. 1594 // 1595 // CRITICAL: run mkfs in a subshell that PRESERVES its 1596 // exit code. The previous implementation used 1597 // mkfs ... > err 2>&1; cat err >> dlog 1598 // which made system() always return cat's exit code (0), 1599 // silently masking every mkfs failure and causing the 1600 // install to proceed onto an unformatted partition. 1601 // 1602 // The subshell pattern below captures mkfs's rc, tees 1603 // the output, and `exit $rc` propagates it back through 1604 // system(). 1605 const char *MKFS_ERR = "/tmp/mkfs-err.log"; 1606 snprintf(rcmd, sizeof(rcmd), 1607 "echo '--- mkfs attempt 1 ---' >> %s; " 1608 "(mkfs.vfat -F 32 -n AC-NATIVE %s > %s 2>&1; rc=$?; " 1609 "cat %s >> %s; exit $rc)", 1610 DLOG, devpath, MKFS_ERR, MKFS_ERR, DLOG); 1611 rrc = system(rcmd); 1612 int mkfs_exit = WIFEXITED(rrc) ? WEXITSTATUS(rrc) : -1; 1613 ac_log("[install] mkfs rc=%d attempt=1 (WIFEXITED=%d status=%d)\n", 1614 rrc, WIFEXITED(rrc), mkfs_exit); 1615 // Inline dump mkfs output so we see the exact error 1616 { 1617 FILE *mf = fopen(MKFS_ERR, "r"); 1618 if (mf) { 1619 char line[512]; 1620 while (fgets(line, sizeof(line), mf)) { 1621 size_t len = strlen(line); 1622 if (len > 0 && line[len-1] == '\n') line[len-1] = '\0'; 1623 ac_log("[mkfs/1] %s\n", line); 1624 } 1625 fclose(mf); 1626 } 1627 } 1628 // Attempt 2–5 if needed (more retries with aggressive flush) 1629 for (int mkfs_try = 2; mkfs_try <= 5 && mkfs_exit != 0; mkfs_try++) { 1630 // Flush block device buffer cache before each retry 1631 { 1632 int pfd = open(devpath, O_RDONLY | O_CLOEXEC); 1633 if (pfd >= 0) { ioctl(pfd, BLKFLSBUF); close(pfd); } 1634 char dd2[64]; 1635 snprintf(dd2, sizeof(dd2), "/dev/%s", parent_blk); 1636 int dfd = open(dd2, O_RDONLY | O_CLOEXEC); 1637 if (dfd >= 0) { ioctl(dfd, BLKFLSBUF); close(dfd); } 1638 } 1639 sync(); 1640 system("echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true"); 1641 usleep(3000000); // 3s settle between retries 1642 snprintf(rcmd, sizeof(rcmd), 1643 "echo '--- mkfs attempt %d ---' >> %s; " 1644 "(mkfs.vfat -F 32 -n AC-NATIVE %s > %s 2>&1; rc=$?; " 1645 "cat %s >> %s; exit $rc)", 1646 mkfs_try, DLOG, devpath, MKFS_ERR, MKFS_ERR, DLOG); 1647 rrc = system(rcmd); 1648 mkfs_exit = WIFEXITED(rrc) ? WEXITSTATUS(rrc) : -1; 1649 ac_log("[install] mkfs rc=%d attempt=%d (WIFEXITED=%d status=%d)\n", 1650 rrc, mkfs_try, WIFEXITED(rrc), mkfs_exit); 1651 FILE *mf = fopen(MKFS_ERR, "r"); 1652 if (mf) { 1653 char line[512]; 1654 while (fgets(line, sizeof(line), mf)) { 1655 size_t len = strlen(line); 1656 if (len > 0 && line[len-1] == '\n') line[len-1] = '\0'; 1657 ac_log("[mkfs/%d] %s\n", mkfs_try, line); 1658 } 1659 fclose(mf); 1660 } 1661 } 1662 rrc = (mkfs_exit == 0) ? 0 : -1; 1663 if (rrc != 0) { 1664 snprintf(install_fail_reason, sizeof(install_fail_reason), 1665 "format failed on %s", devpath); 1666 snprintf(install_fail_detail, sizeof(install_fail_detail), 1667 "mkfs.vfat rc=%d after repartition (see %s)", rrc, DLOG); 1668 if (display) 1669 draw_boot_status(graph, screen, display, "format failed!", pixel_scale); 1670 usleep(2000000); 1671 continue; 1672 } 1673 usleep(500000); 1674 // Remount and retry 1675 install_copy_phase: 1676 if (mount(devpath, "/tmp/hd", "vfat", 0, NULL) != 0) { 1677 ac_log("[install] remount failed after repartition\n"); 1678 snprintf(install_fail_reason, sizeof(install_fail_reason), 1679 "repartition failed on %s", devpath); 1680 snprintf(install_fail_detail, sizeof(install_fail_detail), 1681 "sfdisk=%d mkfs then mount failed", rrc); 1682 if (display) 1683 draw_boot_status(graph, screen, display, "repartition failed!", pixel_scale); 1684 usleep(2000000); 1685 continue; 1686 } 1687 mkdir("/tmp/hd/EFI", 0755); 1688 mkdir("/tmp/hd/EFI/BOOT", 0755); 1689 ac_log("[install] repartitioned OK, retrying copy\n"); 1690 } 1691 } 1692 // Copy kernel (and initramfs/loader for systemd-boot layout) 1693 long sz = 0; 1694 if (systemd_boot_layout) { 1695 // Universal layout: splash BOOTX64.EFI + LOADER.EFI + 1696 // KERNEL.EFI + initramfs + loader config. 1697 long bsz = copy_file(bootloader_src, "/tmp/hd/EFI/BOOT/BOOTX64.EFI"); 1698 long lsz = copy_file(loader_src, "/tmp/hd/EFI/BOOT/LOADER.EFI"); 1699 long ksz = copy_file(chain_kernel_src, "/tmp/hd/EFI/BOOT/KERNEL.EFI"); 1700 char initramfs_dst[128]; 1701 const char *initramfs_name = strstr(initramfs_src, ".lz4") ? "initramfs.cpio.lz4" : "initramfs.cpio.gz"; 1702 snprintf(initramfs_dst, sizeof(initramfs_dst), "/tmp/hd/%s", initramfs_name); 1703 long isz = copy_file(initramfs_src, initramfs_dst); 1704 mkdir("/tmp/hd/loader", 0755); 1705 mkdir("/tmp/hd/loader/entries", 0755); 1706 long csz = copy_file(loader_conf_src, "/tmp/hd/loader/loader.conf"); 1707 long esz = copy_file(loader_entry_src, "/tmp/hd/loader/entries/ac-native.conf"); 1708 ac_log("[install] bootloader: %ld bytes\n", bsz); 1709 ac_log("[install] loader: %ld bytes\n", lsz); 1710 ac_log("[install] kernel: %ld bytes\n", ksz); 1711 ac_log("[install] initramfs: %ld bytes\n", isz); 1712 ac_log("[install] loader.conf: %ld bytes\n", csz); 1713 ac_log("[install] loader entry: %ld bytes\n", esz); 1714 sz = (bsz > 0 && lsz > 0 && ksz > 0 && isz > 0 && csz > 0 && esz > 0) 1715 ? (bsz + lsz + ksz + isz + csz + esz) 1716 : -1; 1717 } else if (chainloader_layout) { 1718 long bsz = copy_file(bootloader_src, "/tmp/hd/EFI/BOOT/BOOTX64.EFI"); 1719 long ksz = copy_file(chain_kernel_src, "/tmp/hd/EFI/BOOT/KERNEL.EFI"); 1720 ac_log("[install] chainloader: %ld bytes\n", bsz); 1721 ac_log("[install] kernel: %ld bytes\n", ksz); 1722 sz = (bsz > 0 && ksz > 0) ? (bsz + ksz) : -1; 1723 } else { 1724 // Monolithic: single BOOTX64.EFI is the kernel 1725 sz = copy_file(kernel_src, "/tmp/hd/EFI/BOOT/BOOTX64.EFI"); 1726 ac_log("[install] copy result: %ld bytes\n", sz); 1727 } 1728 1729 if (sz <= 0) { 1730 ac_log("[install] copy failed on %s (sz=%ld)\n", devpath, sz); 1731 snprintf(install_fail_reason, sizeof(install_fail_reason), 1732 "copy failed on %s", devpath); 1733 snprintf(install_fail_detail, sizeof(install_fail_detail), 1734 "install payload copy returned %ld bytes", sz); 1735 umount("/tmp/hd"); 1736 continue; 1737 } 1738 1739 { 1740 // Also overwrite Windows Boot Manager path (ThinkPad BIOS often 1741 // boots this first regardless of boot order) — but only if space permits 1742 struct statvfs vfs; 1743 long free_bytes = 0; 1744 if (statvfs("/tmp/hd", &vfs) == 0) 1745 free_bytes = (long)vfs.f_bavail * (long)vfs.f_bsize; 1746 if (!systemd_boot_layout && free_bytes > sz + 1048576) { 1747 mkdir("/tmp/hd/EFI/Microsoft", 0755); 1748 mkdir("/tmp/hd/EFI/Microsoft/Boot", 0755); 1749 copy_file(chainloader_layout ? bootloader_src : kernel_src, 1750 "/tmp/hd/EFI/Microsoft/Boot/bootmgfw.efi"); 1751 } 1752 1753 // Preserve user config + wifi creds on installed disk 1754 if (access(config_src, F_OK) == 0) { 1755 copy_file(config_src, "/tmp/hd/config.json"); 1756 ac_log("[install] copied config.json\n"); 1757 } 1758 { 1759 char wifi_src[64]; 1760 snprintf(wifi_src, sizeof(wifi_src), "%s/wifi_creds.json", 1761 source_mount); 1762 if (access(wifi_src, F_OK) == 0) { 1763 copy_file(wifi_src, "/tmp/hd/wifi_creds.json"); 1764 ac_log("[install] copied wifi_creds.json\n"); 1765 } 1766 } 1767 1768 // Write install marker so next boot knows it's installed 1769 FILE *marker = fopen("/tmp/hd/EFI/BOOT/ac-installed", "w"); 1770 if (marker) { fputs("1\n", marker); fclose(marker); } 1771 1772 sync(); 1773 installed = 1; 1774 ac_log("[install] Installed from %s to %s (systemd-boot=%d chainloader=%d)\n", 1775 source_dev, devpath, systemd_boot_layout, chainloader_layout); 1776 } 1777 1778 umount("/tmp/hd"); 1779 } 1780 } 1781 } 1782 1783 if (installed && display) { 1784 draw_boot_status(graph, screen, display, "installed to disk!", pixel_scale); 1785 usleep(800000); 1786 } else if (!installed) { 1787 ac_log("[install] No suitable HD partition found for install\n"); 1788 if (!install_fail_reason[0]) { 1789 snprintf(install_fail_reason, sizeof(install_fail_reason), 1790 "no internal FAT32/ESP partition found"); 1791 // Build detail: list what we tried 1792 int dpos = 0; 1793 dpos += snprintf(install_fail_detail + dpos, 1794 sizeof(install_fail_detail) - dpos, "usb=%s ", usb_blk[0] ? usb_blk : "?"); 1795 for (int i = 0; part_candidates[i]; i++) { 1796 const char *blk = part_candidates[i]; 1797 char dp[32]; snprintf(dp, sizeof(dp), "/sys/block/%s", blk); 1798 if (access(dp, F_OK) != 0) continue; 1799 int rem = (blk[0] == 's' && blk[1] == 'd') ? is_removable(blk) : 0; 1800 const char *skip = ""; 1801 if (usb_blk[0] && strcmp(blk, usb_blk) == 0) skip = "=boot"; 1802 else if (rem == 1) skip = "=USB"; 1803 else skip = "=tried"; 1804 dpos += snprintf(install_fail_detail + dpos, 1805 sizeof(install_fail_detail) - dpos, 1806 "%s%s ", blk, skip); 1807 } 1808 } 1809 } 1810 if (source_mounted_tmp) umount("/tmp/src"); 1811 1812 // Dump the full install-debug.log inline to ac_log so the trace ALWAYS 1813 // lands in ac-native.log (which we know reliably survives to the USB). 1814 // This is the primary diagnostic channel — the parallel copy to 1815 // /mnt/install-debug.log below is a best-effort secondary that may fail 1816 // silently if /mnt got trashed by the repartition attempt. 1817 { 1818 FILE *src = fopen("/tmp/install-debug.log", "r"); 1819 if (src) { 1820 ac_log("[install] === /tmp/install-debug.log BEGIN ===\n"); 1821 char line[1024]; 1822 int line_count = 0; 1823 while (fgets(line, sizeof(line), src)) { 1824 // Strip trailing newline for consistent ac_log formatting 1825 size_t len = strlen(line); 1826 if (len > 0 && line[len-1] == '\n') line[len-1] = '\0'; 1827 ac_log("[install-debug] %s\n", line); 1828 line_count++; 1829 if (line_count > 500) { 1830 ac_log("[install] ... (truncated at 500 lines)\n"); 1831 break; 1832 } 1833 } 1834 ac_log("[install] === /tmp/install-debug.log END (%d lines) ===\n", line_count); 1835 fclose(src); 1836 } else { 1837 ac_log("[install] /tmp/install-debug.log not present (errno=%d)\n", errno); 1838 } 1839 } 1840 1841 // Parallel copy of the tmpfs log to /mnt (best effort). Even if the 1842 // inline dump above succeeded, keep this so the file is also visible 1843 // as a standalone artifact when /mnt is intact. 1844 { 1845 FILE *src = fopen("/tmp/install-debug.log", "rb"); 1846 if (src) { 1847 FILE *dst = fopen("/mnt/install-debug.log", "wb"); 1848 if (dst) { 1849 char buf[4096]; size_t n; 1850 while ((n = fread(buf, 1, sizeof(buf), src)) > 0) 1851 fwrite(buf, 1, n, dst); 1852 fflush(dst); 1853 fsync(fileno(dst)); 1854 fclose(dst); 1855 ac_log("[install] copied /tmp/install-debug.log → /mnt/install-debug.log\n"); 1856 } else { 1857 ac_log("[install] /mnt/install-debug.log copy failed (errno=%d: %s)\n", 1858 errno, strerror(errno)); 1859 } 1860 fclose(src); 1861 } 1862 } 1863 1864 return installed; 1865} 1866 1867static void frame_sync_60fps(struct timespec *next) { 1868 next->tv_nsec += 16666667; 1869 if (next->tv_nsec >= 1000000000) { 1870 next->tv_nsec -= 1000000000; 1871 next->tv_sec++; 1872 } 1873 clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, next, NULL); 1874} 1875 1876// Check if a specific key is currently held using pre-opened fds (fast path) 1877static int check_key_held_fds(int keycode, int *fds, int nfds) { 1878 for (int i = 0; i < nfds; i++) { 1879 unsigned long bits[(KEY_MAX + 7) / 8 / sizeof(unsigned long) + 1] = {0}; 1880 if (ioctl(fds[i], EVIOCGKEY(sizeof(bits)), bits) >= 0) { 1881 if (bits[keycode / (sizeof(unsigned long) * 8)] & 1882 (1UL << (keycode % (sizeof(unsigned long) * 8)))) 1883 return 1; 1884 } 1885 } 1886 return 0; 1887} 1888 1889// Get LA offset from UTC: 7 for PDT, 8 for PST 1890// DST: second Sunday of March 2am PT → first Sunday of November 2am PT 1891static int get_la_offset(void) { 1892 time_t now = time(NULL); 1893 struct tm *utc = gmtime(&now); 1894 int m = utc->tm_mon, y = utc->tm_year + 1900; 1895 if (m > 2 && m < 10) return 7; // Apr-Oct: PDT 1896 if (m < 2 || m > 10) return 8; // Jan-Feb, Dec: PST 1897 if (m == 2) { // March: find second Sunday 1898 struct tm mar1 = {0}; mar1.tm_year = y - 1900; mar1.tm_mon = 2; mar1.tm_mday = 1; 1899 mktime(&mar1); 1900 int secondSun = 8 + (7 - mar1.tm_wday) % 7; 1901 // DST starts at 2am PST = 10:00 UTC on that day 1902 if (utc->tm_mday > secondSun) return 7; 1903 if (utc->tm_mday < secondSun) return 8; 1904 return (utc->tm_hour >= 10) ? 7 : 8; 1905 } 1906 // November: find first Sunday 1907 struct tm nov1 = {0}; nov1.tm_year = y - 1900; nov1.tm_mon = 10; nov1.tm_mday = 1; 1908 mktime(&nov1); 1909 int firstSun = 1 + (7 - nov1.tm_wday) % 7; 1910 // DST ends at 2am PDT = 9:00 UTC on that day 1911 if (utc->tm_mday < firstSun) return 7; 1912 if (utc->tm_mday > firstSun) return 8; 1913 return (utc->tm_hour < 9) ? 7 : 8; 1914} 1915 1916static int get_la_hour(void) { 1917 time_t now = time(NULL); 1918 struct tm *utc = gmtime(&now); 1919 return (utc->tm_hour - get_la_offset() + 24) % 24; 1920} 1921 1922static void play_install_prompt_beep(ACAudio *audio) { 1923 if (!audio || !audio->pcm) return; 1924 audio_synth(audio, WAVE_SINE, 620.0, 0.06, 0.40, 0.001, 0.05, 0.0); 1925 usleep(35000); 1926 audio_synth(audio, WAVE_SINE, 760.0, 0.06, 0.40, 0.001, 0.05, 0.0); 1927} 1928 1929static void play_install_accept_beep(ACAudio *audio) { 1930 if (!audio || !audio->pcm) return; 1931 audio_synth(audio, WAVE_TRIANGLE, 880.0, 0.08, 0.55, 0.001, 0.06, -0.1); 1932 usleep(45000); 1933 audio_synth(audio, WAVE_TRIANGLE, 1175.0, 0.10, 0.65, 0.001, 0.08, 0.1); 1934} 1935 1936static void play_install_reject_beep(ACAudio *audio) { 1937 if (!audio || !audio->pcm) return; 1938 audio_synth(audio, WAVE_SAWTOOTH, 260.0, 0.08, 0.45, 0.001, 0.06, 0.0); 1939 usleep(45000); 1940 audio_synth(audio, WAVE_SAWTOOTH, 180.0, 0.12, 0.45, 0.001, 0.09, 0.0); 1941} 1942 1943static void play_install_success_beep(ACAudio *audio) { 1944 if (!audio || !audio->pcm) return; 1945 audio_synth(audio, WAVE_TRIANGLE, 659.25, 0.12, 0.55, 0.001, 0.08, -0.2); 1946 usleep(50000); 1947 audio_synth(audio, WAVE_TRIANGLE, 783.99, 0.12, 0.60, 0.001, 0.08, 0.0); 1948 usleep(50000); 1949 audio_synth(audio, WAVE_TRIANGLE, 987.77, 0.16, 0.68, 0.001, 0.12, 0.2); 1950} 1951 1952static void play_install_failure_beep(ACAudio *audio) { 1953 if (!audio || !audio->pcm) return; 1954 audio_synth(audio, WAVE_SAWTOOTH, 180.0, 0.11, 0.50, 0.001, 0.08, 0.0); 1955 usleep(55000); 1956 audio_synth(audio, WAVE_SAWTOOTH, 140.0, 0.15, 0.50, 0.001, 0.11, 0.0); 1957} 1958 1959// Draw y/n install confirmation screen 1960// Returns 1 if user confirms with Y, 0 if N/Escape 1961static int draw_install_confirm(ACGraph *graph, ACFramebuffer *screen, 1962 ACDisplay *display, int *fds, int nfds, 1963 ACTts *tts, ACAudio *audio, int pixel_scale) { 1964 int is_dark = 1; // always dark 1965 struct timespec anim_time; 1966 clock_gettime(CLOCK_MONOTONIC, &anim_time); 1967 1968 if (tts) tts_speak(tts, "install to disk? press Y for yes, N for no"); 1969 play_install_prompt_beep(audio); 1970 1971 for (;;) { 1972 uint8_t bg = is_dark ? 20 : 255; 1973 graph_wipe(graph, (ACColor){bg, bg, (uint8_t)(bg + (is_dark ? 5 : 0)), 255}); 1974 1975 uint8_t fg = is_dark ? 220 : 0; 1976 graph_ink(graph, (ACColor){fg, fg, fg, 255}); 1977 1978 int tw = font_measure_matrix("install to disk?", 2); 1979 font_draw_matrix(graph, "install to disk?", 1980 (screen->width - tw) / 2, screen->height / 2 - 24, 2); 1981 1982 // Warning text 1983 graph_ink(graph, (ACColor){220, 80, 80, 255}); 1984 const char *warn = "this will overwrite EFI boot!"; 1985 int ww = font_measure_matrix(warn, 1); 1986 font_draw_matrix(graph, warn, (screen->width - ww) / 2, 1987 screen->height / 2 + 2, 1); 1988 1989 // Y/N prompt 1990 graph_ink(graph, (ACColor){60, 200, 60, 255}); 1991 const char *yn = "Y = yes N = no"; 1992 int yw = font_measure_matrix(yn, 1); 1993 font_draw_matrix(graph, yn, (screen->width - yw) / 2, 1994 screen->height / 2 + 20, 1); 1995 1996 ac_display_present(display, screen, pixel_scale); 1997 frame_sync_60fps(&anim_time); 1998 1999 // Read key events 2000 struct input_event ev; 2001 for (int ki = 0; ki < nfds; ki++) { 2002 while (read(fds[ki], &ev, sizeof(ev)) == sizeof(ev)) { 2003 if (ev.type == EV_KEY && ev.value == 1) { 2004 if (ev.code == KEY_Y) { 2005 play_install_accept_beep(audio); 2006 return 1; 2007 } 2008 if (ev.code == KEY_N || ev.code == KEY_ESC) { 2009 play_install_reject_beep(audio); 2010 return 0; 2011 } 2012 } 2013 } 2014 } 2015 } 2016} 2017 2018// Pause after install attempt so USB boots do not continue into the piece. 2019// Returns 1 if reboot requested, 0 if user chose to continue boot (failure-only path). 2020static int draw_install_reboot_prompt(ACGraph *graph, ACFramebuffer *screen, 2021 ACDisplay *display, ACInput *input, 2022 ACTts *tts, ACAudio *audio, 2023 int install_ok, int pixel_scale) { 2024 struct timespec anim_time; 2025 clock_gettime(CLOCK_MONOTONIC, &anim_time); 2026 2027 if (install_ok) { 2028 if (tts) tts_speak(tts, "install complete. remove USB stick. press R to reboot."); 2029 play_install_success_beep(audio); 2030 } else { 2031 if (tts) tts_speak(tts, "install failed. press R to reboot or escape to continue."); 2032 play_install_failure_beep(audio); 2033 } 2034 2035 int blink = 0; 2036 int no_input_frames = 0; 2037 while (running) { 2038 blink++; 2039 graph_wipe(graph, (ACColor){20, 20, 25, 255}); 2040 2041 const char *title = install_ok ? "disk install complete" : "disk install failed"; 2042 ACColor title_color = install_ok 2043 ? (ACColor){90, 220, 120, 255} 2044 : (ACColor){220, 100, 90, 255}; 2045 graph_ink(graph, title_color); 2046 int tw = font_measure_matrix(title, 2); 2047 font_draw_matrix(graph, title, (screen->width - tw) / 2, screen->height / 2 - 34, 2); 2048 2049 graph_ink(graph, (ACColor){220, 220, 220, 255}); 2050 const char *line1 = install_ok 2051 ? "remove USB stick now" 2052 : "check EFI target and try again"; 2053 int l1w = font_measure_matrix(line1, 1); 2054 font_draw_matrix(graph, line1, (screen->width - l1w) / 2, screen->height / 2 + 0, 1); 2055 2056 // Show failure diagnostics 2057 if (!install_ok && install_fail_reason[0]) { 2058 graph_ink(graph, (ACColor){200, 160, 100, 255}); 2059 int rw = font_measure_matrix(install_fail_reason, 1); 2060 font_draw_matrix(graph, install_fail_reason, 2061 (screen->width - rw) / 2, screen->height / 2 + 16, 1); 2062 2063 if (install_fail_detail[0]) { 2064 graph_ink(graph, (ACColor){140, 140, 160, 255}); 2065 int dw = font_measure_matrix(install_fail_detail, 1); 2066 font_draw_matrix(graph, install_fail_detail, 2067 (screen->width - dw) / 2, screen->height / 2 + 28, 1); 2068 } 2069 } 2070 2071 int yoff = (!install_ok && install_fail_reason[0]) ? 16 : 0; 2072 graph_ink(graph, (ACColor){180, 180, 210, 255}); 2073 const char *line2 = "R/Enter = reboot"; 2074 int l2w = font_measure_matrix(line2, 1); 2075 font_draw_matrix(graph, line2, (screen->width - l2w) / 2, screen->height / 2 + 32 + yoff, 1); 2076 2077 if (!install_ok) { 2078 graph_ink(graph, (ACColor){150, 150, 170, 255}); 2079 const char *line3 = "Esc = continue USB boot"; 2080 int l3w = font_measure_matrix(line3, 1); 2081 font_draw_matrix(graph, line3, (screen->width - l3w) / 2, screen->height / 2 + 46 + yoff, 1); 2082 } 2083 2084 if ((blink % 60) < 36) { 2085 graph_ink(graph, (ACColor){120, 120, 140, 255}); 2086 const char *line4 = install_ok ? "waiting for reboot..." : "waiting for key..."; 2087 int l4w = font_measure_matrix(line4, 1); 2088 font_draw_matrix(graph, line4, (screen->width - l4w) / 2, screen->height / 2 + 62 + yoff, 1); 2089 } 2090 2091 ac_display_present(display, screen, pixel_scale); 2092 frame_sync_60fps(&anim_time); 2093 2094 if (!input) { 2095 no_input_frames++; 2096 if (no_input_frames > 900) return install_ok ? 1 : 0; // ~15s fallback 2097 continue; 2098 } 2099 input_poll(input); 2100 for (int i = 0; i < input->event_count; i++) { 2101 ACEvent *ev = &input->events[i]; 2102 if (ev->type != AC_EVENT_KEYBOARD_DOWN) continue; 2103 2104 if (ev->key_code == KEY_R || ev->key_code == KEY_ENTER || ev->key_code == KEY_KPENTER) { 2105 if (tts) tts_wait(tts); // let "install complete" TTS finish 2106 play_install_accept_beep(audio); 2107 usleep(300000); // let beep play 2108 return 1; 2109 } 2110 if (!install_ok && (ev->key_code == KEY_ESC || ev->key_code == KEY_SPACE)) { 2111 play_install_reject_beep(audio); 2112 return 0; 2113 } 2114 } 2115 } 2116 return install_ok ? 1 : 0; 2117} 2118 2119// Check if we booted from an installed (non-removable) disk 2120// by looking for ac-installed marker on an internal ESP 2121// Extract parent block device name from a partition path: 2122// "nvme0n1p1" → "nvme0n1" 2123// "mmcblk0p1" → "mmcblk0" 2124// "sda1" → "sda" 2125// NVMe and eMMC both use the "<dev>p<N>" partition-suffix scheme (because 2126// the parent name ends in a digit), so strip the trailing "pN" for both. 2127static void get_parent_block(const char *part, char *out, int out_sz) { 2128 out[0] = 0; 2129 int len = (int)strlen(part); 2130 if (len >= out_sz) return; 2131 // NVMe / eMMC: strip trailing "pN" (e.g. nvme0n1p1 → nvme0n1, 2132 // mmcblk0p1 → mmcblk0). 2133 if (strncmp(part, "nvme", 4) == 0 || strncmp(part, "mmcblk", 6) == 0) { 2134 int base_min = (part[0] == 'n') ? 4 : 6; 2135 for (int i = len - 1; i > base_min; i--) { 2136 if (part[i - 1] == 'p' && part[i] >= '0' && part[i] <= '9') { 2137 memcpy(out, part, i - 1); 2138 out[i - 1] = 0; 2139 return; 2140 } 2141 } 2142 } 2143 // SATA/USB: strip trailing digits (e.g. sda1 → sda) 2144 int i = len; 2145 while (i > 0 && part[i - 1] >= '0' && part[i - 1] <= '9') i--; 2146 if (i > 0 && i < out_sz) { 2147 memcpy(out, part, i); 2148 out[i] = 0; 2149 } 2150} 2151 2152static int is_installed_on_hd(void) { 2153 if (getpid() != 1) return 0; // not bare metal 2154 mkdir("/tmp/chk", 0755); 2155 const char *parts[] = { 2156 "/dev/nvme0n1p1", "/dev/nvme0n1p2", 2157 "/dev/mmcblk0p1", "/dev/mmcblk0p2", 2158 "/dev/sda1", "/dev/sdb1", NULL 2159 }; 2160 for (int i = 0; parts[i]; i++) { 2161 // Extract parent block device and skip removable (USB) drives 2162 char blk[32] = ""; 2163 get_parent_block(parts[i] + 5, blk, sizeof(blk)); 2164 fprintf(stderr, "[install-check] %s → parent=%s\n", parts[i], blk); 2165 if (blk[0] && is_removable(blk) == 1) { 2166 fprintf(stderr, "[install-check] → removable, skipping\n"); 2167 continue; 2168 } 2169 if (mount(parts[i], "/tmp/chk", "vfat", MS_RDONLY, NULL) != 0) { 2170 fprintf(stderr, "[install-check] → mount failed\n"); 2171 continue; 2172 } 2173 int found = access("/tmp/chk/EFI/BOOT/ac-installed", F_OK) == 0; 2174 umount("/tmp/chk"); 2175 fprintf(stderr, "[install-check] → mounted, ac-installed=%s\n", found ? "YES" : "no"); 2176 if (found) return 1; 2177 } 2178 fprintf(stderr, "[install-check] not installed on HD\n"); 2179 return 0; 2180} 2181 2182// Draw startup fade animation (black → white with title) 2183// Returns 1 if user pressed W and confirmed install, 0 otherwise 2184static int draw_startup_fade(ACGraph *graph, ACFramebuffer *screen, 2185 ACDisplay *display, ACTts *tts, ACAudio *audio, 2186 int pixel_scale) { 2187 struct timespec anim_time; 2188 clock_gettime(CLOCK_MONOTONIC, &anim_time); 2189 // Show install option whenever booting from USB (even if already installed — 2190 // user may want to update). Detect USB by checking if boot device is removable. 2191 int show_install = 0; 2192 if (log_dev[0]) { 2193 char boot_blk[32] = ""; 2194 get_parent_block(log_dev + 5, boot_blk, sizeof(boot_blk)); 2195 show_install = (boot_blk[0] && is_removable(boot_blk) == 1) ? 1 : 0; 2196 ac_log("[boot-anim] boot_dev=%s parent=%s removable=%d show_install=%d\n", 2197 log_dev, boot_blk, is_removable(boot_blk), show_install); 2198 } else { 2199 // No log_dev — check if any removable block device has our EFI boot file 2200 // If not, we're booted from internal disk (post-install) — don't show install 2201 show_install = 0; 2202 DIR *blkdir = opendir("/sys/block"); 2203 if (blkdir) { 2204 struct dirent *bent; 2205 while ((bent = readdir(blkdir)) != NULL) { 2206 if (bent->d_name[0] == '.') continue; 2207 if (is_removable(bent->d_name) == 1) { 2208 show_install = 1; 2209 ac_log("[boot-anim] removable device %s found, show_install=1\n", bent->d_name); 2210 break; 2211 } 2212 } 2213 closedir(blkdir); 2214 } 2215 if (!show_install) 2216 ac_log("[boot-anim] no removable media, show_install=0\n"); 2217 } 2218 2219 // Open evdev fds for key checking — retry until devices appear 2220 int key_fds[24]; 2221 int key_fd_count = 0; 2222 for (int retry = 0; retry < 20 && key_fd_count == 0; retry++) { 2223 DIR *dir = opendir("/dev/input"); 2224 if (dir) { 2225 struct dirent *ent; 2226 while ((ent = readdir(dir)) && key_fd_count < 24) { 2227 if (strncmp(ent->d_name, "event", 5) != 0) continue; 2228 char path[64]; 2229 snprintf(path, sizeof(path), "/dev/input/%s", ent->d_name); 2230 int fd = open(path, O_RDONLY | O_NONBLOCK); 2231 if (fd >= 0) { 2232 key_fds[key_fd_count++] = fd; 2233 fprintf(stderr, "[boot-anim] opened %s (fd %d)\n", path, fd); 2234 } 2235 } 2236 closedir(dir); 2237 } 2238 if (key_fd_count == 0) { 2239 fprintf(stderr, "[boot-anim] no input devices yet, waiting... (%d/20)\n", retry + 1); 2240 usleep(100000); // 100ms 2241 } 2242 } 2243 fprintf(stderr, "[boot-anim] %d event devices\n", key_fd_count); 2244 2245 // Start with a solid frame immediately (hides any kernel text) 2246 // White for daytime, black for evening/night 2247 int boot_hour = get_la_hour(); 2248 int boot_is_day = (boot_hour >= 7 && boot_hour < 18); 2249 graph_wipe(graph, boot_is_day 2250 ? (ACColor){255, 255, 255, 255} 2251 : (ACColor){0, 0, 0, 255}); 2252 ac_display_present(display, screen, pixel_scale); 2253 2254 // Check if this is a fresh boot of a new version 2255 int is_new_version = 0; 2256#ifdef AC_GIT_HASH 2257#ifdef AC_BUILD_TS 2258 { 2259 const char *current_ver = AC_GIT_HASH "-" AC_BUILD_TS; 2260 char prev_ver[128] = ""; 2261 FILE *vf = fopen("/mnt/booted-version", "r"); 2262 if (vf) { 2263 if (fgets(prev_ver, sizeof(prev_ver), vf)) { 2264 // Strip trailing newline 2265 char *nl = strchr(prev_ver, '\n'); 2266 if (nl) *nl = 0; 2267 } 2268 fclose(vf); 2269 } 2270 is_new_version = (strcmp(prev_ver, current_ver) != 0); 2271 ac_log("[boot] version=%s prev=%s fresh=%s\n", current_ver, prev_ver, is_new_version ? "YES" : "no"); 2272 // Write current version immediately so next boot sees it 2273 vf = fopen("/mnt/booted-version", "w"); 2274 if (vf) { 2275 fprintf(vf, "%s", current_ver); 2276 fclose(vf); 2277 } 2278 // Append to boot history log (persists across reboots) 2279 vf = fopen("/mnt/boot-history.log", "a"); 2280 if (vf) { 2281 time_t now_t = time(NULL); 2282 struct tm *tm = gmtime(&now_t); 2283 char ts[32]; 2284 strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%SZ", tm); 2285 fprintf(vf, "%s %s %s\n", ts, current_ver, is_new_version ? "FRESH" : "same"); 2286 fclose(vf); 2287 } 2288 sync(); 2289 } 2290#endif 2291#endif 2292 2293 // Generate or load persistent machine ID (needs /mnt mounted) 2294 init_machine_id(); 2295 2296 // Initialize machines monitoring daemon 2297 machines_init(&g_machines); 2298 2299 // 180 frames = 3 second animation. 2300 // W press → halt and show y/n confirmation 2301 // Any other key → skip animation and start playing 2302 int total_frames = 180; // 3 seconds 2303 int skip_anim = 0; 2304 int w_pressed = 0; 2305 for (int f = 0; f < total_frames && !skip_anim && !w_pressed; f++) { 2306 double t = (double)f / (double)total_frames; 2307 2308 // Drain all key events — detect W press or other-key skip 2309 { 2310 struct input_event ev; 2311 for (int ki = 0; ki < key_fd_count; ki++) { 2312 while (read(key_fds[ki], &ev, sizeof(ev)) == sizeof(ev)) { 2313 if (ev.type == EV_KEY && ev.value == 1) { 2314 // First 60 frames (1 second): ignore all keys 2315 // Ensures W hint is visible before accepting input 2316 if (f < 60) { 2317 fprintf(stderr, "[boot-anim] drained key %d at f=%d (hold period)\n", ev.code, f); 2318 continue; 2319 } 2320 fprintf(stderr, "[boot-anim] key %d at f=%d\n", ev.code, f); 2321 if (ev.code == KEY_W && show_install) { 2322 w_pressed = 1; 2323 } else if (ev.code != KEY_RESERVED) { 2324 skip_anim = 1; 2325 } 2326 } 2327 } 2328 } 2329 } 2330 2331 // Startup greeting — time-of-day + handle, or boot melody if no handle 2332 if (f == 10) { 2333 const char *at = strchr(boot_title, '@'); 2334 if (at && tts) { 2335 // Personalized TTS greeting 2336 char greet[256]; 2337 int hour = get_la_hour(); 2338 const char *tod; 2339 if (hour >= 5 && hour < 12) tod = "good morning"; 2340 else if (hour >= 12 && hour < 17) tod = "good afternoon"; 2341 else tod = "good evening"; 2342#ifdef AC_BUILD_NAME 2343 char name_tts[64]; 2344 strncpy(name_tts, AC_BUILD_NAME, sizeof(name_tts) - 1); 2345 name_tts[sizeof(name_tts) - 1] = 0; 2346 for (char *p = name_tts; *p; p++) { if (*p == '-') *p = ' '; } 2347 snprintf(greet, sizeof(greet), "%s %s. enjoy Los Angeles! %s.", tod, at + 1, name_tts); 2348#else 2349 snprintf(greet, sizeof(greet), "%s %s. enjoy Los Angeles!", tod, at + 1); 2350#endif 2351 tts_speak(tts, greet); 2352 } else if (audio) { 2353 // No handle — play a short ascending arpeggio (C E G C') 2354 audio_synth(audio, WAVE_SINE, 523.3, 0.15, 0.6, 0.003, 0.10, -0.3); // C5 2355 } 2356 } 2357 // Stagger the arpeggio notes across frames for no-handle boot 2358 if (!strchr(boot_title, '@') && audio) { 2359 if (f == 20) audio_synth(audio, WAVE_SINE, 659.3, 0.15, 0.6, 0.003, 0.10, 0.0); // E5 2360 if (f == 30) audio_synth(audio, WAVE_SINE, 784.0, 0.15, 0.6, 0.003, 0.10, 0.3); // G5 2361 if (f == 42) audio_synth(audio, WAVE_SINE, 1047.0, 0.20, 0.5, 0.003, 0.15, 0.0); // C6 2362 } 2363 // W hint is visual only — no TTS 2364 2365 // Fade from black/white to time-of-day themed bg (complete in first 0.3s) 2366 double fade_t = t * 3.33; 2367 if (fade_t > 1.0) fade_t = 1.0; 2368 int hour = get_la_hour(); 2369 int is_day = (hour >= 7 && hour < 18); // light mode 7am-6pm 2370 int target_r, target_g, target_b; 2371 if (is_day) { 2372 // Light mode: soft warm backgrounds 2373 if (hour >= 7 && hour < 12) { 2374 target_r = 235; target_g = 230; target_b = 220; // morning cream 2375 } else { 2376 target_r = 240; target_g = 235; target_b = 215; // afternoon warm white 2377 } 2378 } else { 2379 // Dark mode: rich atmospheric backgrounds 2380 if (hour >= 5 && hour < 7) { 2381 target_r = 100; target_g = 45; target_b = 20; // sunrise orange 2382 } else if (hour >= 18 && hour < 20) { 2383 target_r = 80; target_g = 25; target_b = 60; // sunset purple 2384 } else { 2385 target_r = 15; target_g = 15; target_b = 40; // night deep blue 2386 } 2387 } 2388 int start_r = is_day ? 255 : 0; 2389 int start_g = is_day ? 255 : 0; 2390 int start_b = is_day ? 255 : 0; 2391 int bg_r = start_r + (int)((target_r - start_r) * fade_t); 2392 int bg_g = start_g + (int)((target_g - start_g) * fade_t); 2393 int bg_b = start_b + (int)((target_b - start_b) * fade_t); 2394 graph_wipe(graph, (ACColor){(uint8_t)bg_r, (uint8_t)bg_g, (uint8_t)bg_b, 255}); 2395 2396 // Title — per-handle palette (fallback rainbow), animated pulse 2397 int alpha = (int)(255.0 * fade_t); 2398 if (alpha > 0) { 2399 const char *title = boot_title; 2400 // Auto-scale: use 3x unless title is too wide, then 2x 2401 int scale = 3; 2402 int tw = font_measure_matrix(title, scale); 2403 if (tw > screen->width - 20) { 2404 scale = 2; 2405 tw = font_measure_matrix(title, scale); 2406 } 2407 int tx = (screen->width - tw) / 2; 2408 int ty = screen->height / 2 - 20; 2409 for (int ci = 0; title[ci]; ci++) { 2410 ACColor cc = title_char_color(ci, f, alpha); 2411 graph_ink(graph, cc); 2412 char ch[2] = { title[ci], 0 }; 2413 tx = font_draw_matrix(graph, ch, tx, ty, scale); 2414 } 2415 } 2416 2417 // Version + build name + build date (high-contrast panel, top-right) 2418#ifdef AC_GIT_HASH 2419 if (alpha > 40) { 2420 char ver[64]; 2421 char bts[64]; 2422 char bname[64] = ""; 2423 char ddrv[64] = ""; 2424 snprintf(ver, sizeof(ver), "version %s", AC_GIT_HASH); 2425#ifdef AC_BUILD_TS 2426 snprintf(bts, sizeof(bts), "%s", AC_BUILD_TS); 2427#else 2428 snprintf(bts, sizeof(bts), "build unknown"); 2429#endif 2430#ifdef AC_BUILD_NAME 2431 snprintf(bname, sizeof(bname), "%s", AC_BUILD_NAME); 2432#endif 2433 const char *driver = drm_display_driver(display); 2434 snprintf(ddrv, sizeof(ddrv), "display %s", driver); 2435 int wv = font_measure_matrix(ver, 1); 2436 int wt = font_measure_matrix(bts, 1); 2437 int wn = bname[0] ? font_measure_matrix(bname, 1) : 0; 2438 int wd = font_measure_matrix(ddrv, 1); 2439 int max_w = wv; 2440 if (wt > max_w) max_w = wt; 2441 if (wn > max_w) max_w = wn; 2442 if (wd > max_w) max_w = wd; 2443 int panel_w = max_w + 8; 2444 int panel_h = (bname[0] ? 28 : 20) + 8; 2445 int panel_x = screen->width - panel_w - 3; 2446 int panel_y = 3; 2447 graph_ink(graph, is_day 2448 ? (ACColor){255, 255, 255, (uint8_t)(alpha * 0.7)} 2449 : (ACColor){0, 0, 0, (uint8_t)(alpha * 0.82)}); 2450 graph_box(graph, panel_x, panel_y, panel_w, panel_h, 1); 2451 // Build name (top line) 2452 if (bname[0]) { 2453 graph_ink(graph, is_day 2454 ? (ACColor){140, 100, 0, (uint8_t)alpha} 2455 : (ACColor){255, 200, 60, (uint8_t)alpha}); 2456 font_draw_matrix(graph, bname, panel_x + 4, panel_y + 3, 1); 2457 } 2458 int line_y = panel_y + (bname[0] ? 11 : 3); 2459 graph_ink(graph, is_day 2460 ? (ACColor){60, 60, 60, (uint8_t)alpha} 2461 : (ACColor){255, 255, 255, (uint8_t)alpha}); 2462 font_draw_matrix(graph, ver, panel_x + 4, line_y, 1); 2463 graph_ink(graph, is_day 2464 ? (ACColor){80, 100, 90, (uint8_t)alpha} 2465 : (ACColor){210, 235, 220, (uint8_t)alpha}); 2466 font_draw_matrix(graph, bts, panel_x + 4, line_y + 8, 1); 2467 // Display driver line 2468 graph_ink(graph, is_day 2469 ? (ACColor){90, 60, 120, (uint8_t)alpha} 2470 : (ACColor){180, 160, 255, (uint8_t)alpha}); 2471 font_draw_matrix(graph, ddrv, panel_x + 4, line_y + 16, 1); 2472 // "FRESH" badge when first boot of this version 2473 if (is_new_version) { 2474 graph_ink(graph, is_day 2475 ? (ACColor){0, 140, 60, (uint8_t)alpha} 2476 : (ACColor){80, 255, 120, (uint8_t)alpha}); 2477 font_draw_matrix(graph, "FRESH", panel_x - font_measure_matrix("FRESH", 1) - 4, panel_y + 6, 1); 2478 } 2479 } 2480#endif 2481 2482 // Subtitle: "enjoy Los Angeles!" appears after frame 130 2483 if (f > 130) { 2484 double sub_t = (double)(f - 130) / 30.0; 2485 if (sub_t > 1.0) sub_t = 1.0; 2486 int sub_alpha = (int)(180.0 * sub_t); 2487 graph_ink(graph, is_day 2488 ? (ACColor){120, 100, 80, (uint8_t)sub_alpha} 2489 : (ACColor){220, 180, 140, (uint8_t)sub_alpha}); 2490 const char *subtitle = "enjoy Los Angeles!"; 2491 int sw = font_measure_matrix(subtitle, 1); 2492 font_draw_matrix(graph, subtitle, 2493 (screen->width - sw) / 2, screen->height / 2 + 10, 1); 2494 } 2495 2496 // Auth badges (bottom-left): pixel crab = Claude, pixel octocat = GitHub 2497 if (f > 60 && alpha > 80) { 2498 int badge_x = 6; 2499 int badge_y = screen->height - 22; 2500 double badge_t = (double)(f - 60) / 40.0; 2501 if (badge_t > 1.0) badge_t = 1.0; 2502 int ba = (int)(220.0 * badge_t); // badge alpha 2503 2504 // 11x9 pixel crab (Claude/Anthropic) 2505 if (access("/claude-token", F_OK) == 0 || getenv("CLAUDE_CODE_OAUTH_TOKEN")) { 2506 static const char crab[9][12] = { 2507 " . . ", 2508 " . . ", 2509 " ..##.##.. ", 2510 ".# #### #.", 2511 ". ####### .", 2512 " ####### ", 2513 " ## . ## ", 2514 " . . ", 2515 " . . ", 2516 }; 2517 for (int cy = 0; cy < 9; cy++) 2518 for (int cx = 0; cx < 11; cx++) { 2519 char c = crab[cy][cx]; 2520 if (c == '#') 2521 graph_ink(graph, (ACColor){255, 120, 50, (uint8_t)ba}); 2522 else if (c == '.') 2523 graph_ink(graph, (ACColor){200, 90, 30, (uint8_t)(ba*2/3)}); 2524 else continue; 2525 graph_box(graph, badge_x + cx*2, badge_y + cy*2, 2, 2, 1); 2526 } 2527 badge_x += 28; 2528 } 2529 // 11x11 pixel octocat (GitHub) 2530 if (access("/github-pat", F_OK) == 0 || getenv("GH_TOKEN")) { 2531 static const char octo[11][12] = { 2532 " .###. ", 2533 " ####### ", 2534 " ## o#o ## ", 2535 " ######### ", 2536 " ## ### ## ", 2537 " ####### ", 2538 " ##### ", 2539 " .# . #. ", 2540 " .# . #. ", 2541 " . . . ", 2542 ". . .", 2543 }; 2544 for (int cy = 0; cy < 11; cy++) 2545 for (int cx = 0; cx < 11; cx++) { 2546 char c = octo[cy][cx]; 2547 if (c == '#') 2548 graph_ink(graph, (ACColor){180, 210, 255, (uint8_t)ba}); 2549 else if (c == 'o') 2550 graph_ink(graph, (ACColor){60, 80, 120, (uint8_t)ba}); 2551 else if (c == '.') 2552 graph_ink(graph, (ACColor){120, 150, 200, (uint8_t)(ba*2/3)}); 2553 else continue; 2554 graph_box(graph, badge_x + cx*2, badge_y + cy*2, 2, 2, 1); 2555 } 2556 badge_x += 28; 2557 } 2558 } 2559 2560 // Animated triangles — geometric decoration 2561 if (alpha > 30) { 2562 int tri_alpha = (int)(alpha * 0.15); 2563 int W = screen->width; 2564 int H = screen->height; 2565 // Drifting triangles based on frame counter 2566 for (int ti = 0; ti < 6; ti++) { 2567 double phase = (double)f * 0.02 + ti * 1.047; // 60° apart 2568 int cx = (int)(W * 0.5 + W * 0.35 * sin(phase + 1.5708)); 2569 int cy = (int)(H * 0.5 + H * 0.3 * sin(phase * 0.7)); 2570 int sz = 8 + ti * 3 + (int)(4.0 * sin(f * 0.05 + ti)); 2571 ACColor tc = is_day 2572 ? (ACColor){180 - ti*15, 140 - ti*10, 120, (uint8_t)tri_alpha} 2573 : (ACColor){80 + ti*20, 60 + ti*15, 120 + ti*10, (uint8_t)tri_alpha}; 2574 graph_ink(graph, tc); 2575 // Draw triangle as 3 lines 2576 int x0 = cx, y0 = cy - sz; 2577 int x1 = cx - sz, y1 = cy + sz/2; 2578 int x2 = cx + sz, y2 = cy + sz/2; 2579 graph_line(graph, x0, y0, x1, y1); 2580 graph_line(graph, x1, y1, x2, y2); 2581 graph_line(graph, x2, y2, x0, y0); 2582 } 2583 } 2584 2585 // Bottom: shrinking time bar 2586 int bar_full = screen->width - 40; 2587 int bar_remaining = (int)((1.0 - t) * bar_full); 2588 if (bar_remaining > 0) { 2589 graph_ink(graph, (ACColor){200, 150, 180, (uint8_t)(80 * (1.0 - t))}); 2590 graph_box(graph, 20, screen->height - 6, bar_remaining, 3, 1); 2591 } 2592 2593 // Animated W install prompt 2594 if (alpha > 100 && show_install) { 2595 // Pulsing box behind the text 2596 double pulse = 0.5 + 0.5 * sin(f * 0.1); 2597 int pa = (int)(40 + 30 * pulse); 2598 const char *hint = is_installed_on_hd() 2599 ? "W: update" : "W: install to disk"; 2600 int hw = font_measure_matrix(hint, 1); 2601 int hx = (screen->width - hw) / 2; 2602 int hy = screen->height - 20; 2603 // Pulsing background pill 2604 graph_ink(graph, is_day 2605 ? (ACColor){200, 160, 120, (uint8_t)pa} 2606 : (ACColor){60, 40, 80, (uint8_t)pa}); 2607 graph_box(graph, hx - 4, hy - 2, hw + 8, 12, 1); 2608 // Triangle arrow pointing down at the text 2609 int ax = hx - 10; 2610 int ay = hy + 3; 2611 graph_ink(graph, is_day 2612 ? (ACColor){180, 120, 60, (uint8_t)(alpha / 2)} 2613 : (ACColor){200, 150, 255, (uint8_t)(alpha / 2)}); 2614 graph_line(graph, ax, ay - 3, ax, ay + 3); 2615 graph_line(graph, ax, ay + 3, ax - 3, ay); 2616 // Text with higher contrast 2617 graph_ink(graph, is_day 2618 ? (ACColor){120, 60, 0, (uint8_t)(alpha * 2 / 3)} 2619 : (ACColor){220, 180, 255, (uint8_t)(alpha * 2 / 3)}); 2620 font_draw_matrix(graph, hint, hx, hy, 1); 2621 } 2622 2623 ac_display_present(display, screen, pixel_scale); 2624 frame_sync_60fps(&anim_time); 2625 } 2626 2627 // If W was pressed, show y/n confirmation 2628 int result = 0; 2629 if (w_pressed) { 2630 result = draw_install_confirm(graph, screen, display, key_fds, key_fd_count, tts, audio, pixel_scale); 2631 } 2632 2633 for (int i = 0; i < key_fd_count; i++) close(key_fds[i]); 2634 return result; 2635} 2636 2637// Draw a status frame during boot (white bg, bouncy title + status text) 2638// Renders multiple frames with a bounce animation 2639static void draw_boot_status(ACGraph *graph, ACFramebuffer *screen, 2640 ACDisplay *display, const char *status, int pixel_scale) { 2641 static int boot_frame = 0; 2642 // Time-of-day themed background (light for day, dark for night) 2643 int la_hour = get_la_hour(); 2644 int is_day = (la_hour >= 7 && la_hour < 18); 2645 uint8_t bg_r, bg_g, bg_b; 2646 if (is_day) { 2647 if (la_hour < 12) { 2648 bg_r = 235; bg_g = 230; bg_b = 220; // morning cream 2649 } else { 2650 bg_r = 240; bg_g = 235; bg_b = 215; // afternoon warm white 2651 } 2652 } else { 2653 if (la_hour >= 5 && la_hour < 7) { 2654 bg_r = 100; bg_g = 45; bg_b = 20; // sunrise orange 2655 } else if (la_hour >= 18 && la_hour < 20) { 2656 bg_r = 80; bg_g = 25; bg_b = 60; // sunset purple 2657 } else { 2658 bg_r = 15; bg_g = 15; bg_b = 40; // night deep blue 2659 } 2660 } 2661 2662 struct timespec anim_time; 2663 clock_gettime(CLOCK_MONOTONIC, &anim_time); 2664 2665 // Animate for 20 frames (~333ms) per status change 2666 for (int af = 0; af < 20; af++) { 2667 boot_frame++; 2668 graph_wipe(graph, (ACColor){bg_r, bg_g, bg_b, 255}); 2669 2670 // Rainbow stripes at top and bottom 2671 { 2672 int stripe_h = 3; 2673 int num_stripes = 8; 2674 int band = num_stripes * stripe_h; 2675 for (int s = 0; s < num_stripes; s++) { 2676 double hue = fmod((double)s / num_stripes * 360.0 + boot_frame * 3.0, 360.0); 2677 double h6 = hue / 60.0; 2678 int hi = (int)h6 % 6; 2679 double fr = h6 - (int)h6; 2680 double cr, cg, cb; 2681 switch (hi) { 2682 case 0: cr = 1; cg = fr; cb = 0; break; 2683 case 1: cr = 1-fr; cg = 1; cb = 0; break; 2684 case 2: cr = 0; cg = 1; cb = fr; break; 2685 case 3: cr = 0; cg = 1-fr; cb = 1; break; 2686 case 4: cr = fr; cg = 0; cb = 1; break; 2687 default: cr = 1; cg = 0; cb = 1-fr; break; 2688 } 2689 int y_top = s * stripe_h; 2690 int y_bot = screen->height - band + s * stripe_h; 2691 graph_ink(graph, (ACColor){(uint8_t)(cr*200), (uint8_t)(cg*200), (uint8_t)(cb*200), 80}); 2692 graph_box(graph, 0, y_top, screen->width, stripe_h, 1); 2693 graph_box(graph, 0, y_bot, screen->width, stripe_h, 1); 2694 } 2695 } 2696 2697 // Bounce: title oscillates with a decaying sine 2698 double bounce_t = (double)af / 20.0; 2699 int bounce_y = (int)(6.0 * sin(bounce_t * 3.14159 * 2) * (1.0 - bounce_t)); 2700 2701 // Title: boot_title with per-handle colors 2702 int tw = font_measure_matrix(boot_title, 3); 2703 int tx = (screen->width - tw) / 2; 2704 int ty = screen->height / 2 - 20 + bounce_y; 2705 for (int ci = 0; boot_title[ci]; ci++) { 2706 ACColor cc = title_char_color(ci, boot_frame, 255); 2707 graph_ink(graph, cc); 2708 char ch[2] = { boot_title[ci], 0 }; 2709 tx = font_draw_matrix(graph, ch, tx, ty, 3); 2710 } 2711 2712 // Subtitle (slight counter-bounce) 2713 uint8_t sub = is_day ? 100 : 140; 2714 graph_ink(graph, (ACColor){sub, sub, sub, 255}); 2715 int sw = font_measure_matrix("aesthetic.computer", 1); 2716 font_draw_matrix(graph, "aesthetic.computer", 2717 (screen->width - sw) / 2, 2718 screen->height / 2 + 10 - bounce_y / 3, 1); 2719 2720 // Status text — slides in from right 2721 if (status) { 2722 int slide = (int)((1.0 - bounce_t) * 40); 2723 if (slide < 0) slide = 0; 2724 uint8_t sc = is_day ? 80 : 120; 2725 graph_ink(graph, (ACColor){sc, sc, sc, (uint8_t)(255 * bounce_t)}); 2726 int stw = font_measure_matrix(status, 1); 2727 font_draw_matrix(graph, status, 2728 (screen->width - stw) / 2 + slide, 2729 screen->height / 2 + 26, 1); 2730 } 2731 2732 // Spinning dot indicator (rotates each boot_frame) 2733 { 2734 int cx = screen->width / 2; 2735 int cy = screen->height / 2 + 42; 2736 double angle = boot_frame * 0.15; 2737 for (int d = 0; d < 4; d++) { 2738 double a = angle + d * 1.5708; // 90° apart 2739 int dx = (int)(6.0 * cos(a)); 2740 int dy = (int)(3.0 * sin(a)); 2741 uint8_t bright = (d == 0) ? 200 : 80; 2742 graph_ink(graph, (ACColor){bright, bright, bright, 255}); 2743 graph_box(graph, cx + dx - 1, cy + dy - 1, 2, 2, 1); 2744 } 2745 } 2746 2747 ac_display_present(display, screen, pixel_scale); 2748 frame_sync_60fps(&anim_time); 2749 } 2750} 2751 2752// Audio recording tap callback (adapts rec_callback signature to recorder_submit_audio) 2753static void rec_audio_tap(const int16_t *pcm, int frames, void *userdata) { 2754 recorder_submit_audio((ACRecorder *)userdata, pcm, frames); 2755} 2756 2757// Tape recording state — the red "rolling" overlay in the top-left and 2758// the elapsed-time counter both read from these globals, which are set 2759// by the PrintScreen key handler when a tape starts. 2760static volatile int g_tape_recording = 0; // 1 while actively recording 2761static char g_tape_current_path[256] = {0}; // /mnt/tapes/<slug>.mp4 2762static time_t g_tape_start_sec = 0; // wall-clock start for elapsed display 2763 2764// Fire-and-forget tape upload: shells out a curl script that 2765// 1. requests a presigned PUT URL from /api/presigned-upload-url/mp4/<slug>/user 2766// 2. PUTs the MP4 directly to DO Spaces 2767// 3. POSTs /api/track-media with ext=mp4 + metadata 2768// Runs in a child process so the audio+render loop never blocks on network. 2769// The script reads config.json for {handle, token}. 2770static void tape_upload_async(const char *tape_path) { 2771 if (!tape_path || !tape_path[0]) return; 2772 // Sanity: only upload MP4 tapes, only if config.json has a token 2773 FILE *cf = fopen("/mnt/config.json", "r"); 2774 if (!cf) { ac_log("[tape] upload skipped — no /mnt/config.json\n"); return; } 2775 char cbuf[4096] = {0}; 2776 fread(cbuf, 1, sizeof(cbuf) - 1, cf); 2777 fclose(cf); 2778 char handle[64] = {0}, actoken[1024] = {0}; 2779 parse_config_string(cbuf, "\"handle\"", handle, sizeof(handle)); 2780 parse_config_string(cbuf, "\"token\"", actoken, sizeof(actoken)); 2781 if (!handle[0] || !actoken[0]) { 2782 ac_log("[tape] upload skipped — no handle/token in config.json\n"); 2783 return; 2784 } 2785 // Extract slug: "/mnt/tapes/2026.04.11.12.34.56.789.mp4" → "2026.04.11.12.34.56.789" 2786 const char *base = strrchr(tape_path, '/'); 2787 base = base ? base + 1 : tape_path; 2788 char slug[128] = {0}; 2789 strncpy(slug, base, sizeof(slug) - 1); 2790 char *dot = strrchr(slug, '.'); 2791 if (dot) *dot = '\0'; 2792 ac_log("[tape] uploading %s as slug %s for @%s\n", tape_path, slug, handle); 2793 // Shell out to curl. Runs in background (&) so we don't block. 2794 // Step 1: GET presigned URL. Step 2: PUT mp4. Step 3: POST track-media. 2795 char cmd[4096]; 2796 snprintf(cmd, sizeof(cmd), 2797 "( " 2798 "URL=$(curl -fsSL -H 'Authorization: Bearer %s' " 2799 " 'https://aesthetic.computer/api/presigned-upload-url/mp4/%s/user' " 2800 " 2>/dev/null | jq -r '.uploadURL' 2>/dev/null); " 2801 "[ -z \"$URL\" ] && echo '[tape] no presigned url' && exit 1; " 2802 "curl -fsSL -X PUT -H 'Content-Type: video/mp4' -H 'x-amz-acl: public-read' " 2803 " --data-binary @'%s' \"$URL\" 2>/dev/null; " 2804 "curl -fsSL -X POST -H 'Authorization: Bearer %s' -H 'Content-Type: application/json' " 2805 " 'https://aesthetic.computer/api/track-tape' " 2806 " -d '{\"slug\":\"%s\",\"ext\":\"mp4\",\"metadata\":{\"totalDuration\":30,\"audioOnly\":false,\"device\":\"ac-native\"}}' " 2807 " >/dev/null 2>&1; " 2808 "echo '[tape] upload finished %s'; " 2809 ") >> /tmp/tape-upload.log 2>&1 &", 2810 actoken, slug, tape_path, actoken, slug, slug); 2811 system(cmd); 2812} 2813 2814int main(int argc, char *argv[]) { 2815 struct timespec boot_start; 2816 clock_gettime(CLOCK_MONOTONIC, &boot_start); 2817 2818 // Mount filesystems if PID 1 (direct DRM boot) 2819 if (getpid() == 1) { 2820 mount_minimal_fs(); 2821 // Ensure PATH includes all standard binary directories 2822 setenv("PATH", "/bin:/sbin:/usr/bin:/usr/sbin", 1); 2823 // Keep curl/OpenSSL trust lookup stable in initramfs. 2824 setenv("SSL_CERT_FILE", "/etc/pki/tls/certs/ca-bundle.crt", 1); 2825 setenv("CURL_CA_BUNDLE", "/etc/pki/tls/certs/ca-bundle.crt", 1); 2826 setenv("SSL_CERT_DIR", "/etc/ssl/certs", 1); 2827 } else { 2828 // Under cage: filesystems already mounted by init script, 2829 // but still need USB log mount and PATH 2830 if (!getenv("PATH")) 2831 setenv("PATH", "/bin:/sbin:/usr/bin:/usr/sbin", 1); 2832 setenv("SSL_CERT_FILE", "/etc/pki/tls/certs/ca-bundle.crt", 1); 2833 setenv("CURL_CA_BUNDLE", "/etc/pki/tls/certs/ca-bundle.crt", 1); 2834 setenv("SSL_CERT_DIR", "/etc/ssl/certs", 1); 2835 // Log to USB directly (parent has /mnt mounted, we inherit it) 2836 // Use a separate file so we don't truncate parent's log 2837 logfile = fopen("/mnt/cage-child.log", "w"); 2838 if (!logfile) logfile = fopen("/tmp/ac-native-cage.log", "w"); 2839 if (logfile) { 2840 fprintf(logfile, "[ac-native] Running under cage (pid=%d)\n", getpid()); 2841 fflush(logfile); 2842 } 2843 } 2844 2845 signal(SIGINT, signal_handler); 2846 signal(SIGTERM, signal_handler); 2847 signal(SIGSEGV, signal_handler); 2848 signal(SIGBUS, signal_handler); 2849 signal(SIGABRT, signal_handler); 2850 signal(SIGFPE, signal_handler); 2851 // Ignore SIGPIPE so writes to a closed socket (WebSocket, UDP peer, 2852 // session-server log uploads) return EPIPE instead of killing the 2853 // process. Without this, any server-side close during an in-flight 2854 // SSL_write kills ac-native with exit=141 (observed in crash logs). 2855 signal(SIGPIPE, SIG_IGN); 2856#ifdef USE_WAYLAND 2857 // Under Wayland: no DRM handoff signals needed (browser is sibling client) 2858 if (!getenv("WAYLAND_DISPLAY")) 2859#endif 2860 { 2861 signal(SIGUSR1, sigusr_handler); 2862 signal(SIGUSR2, sigusr_handler); 2863 signal(SIGTERM, sigterm_handler); 2864 } 2865 2866 // Determine piece path (ignore kernel cmdline args passed to init) 2867 const char *piece_path = "/piece.mjs"; 2868 int headless = 0; 2869 for (int i = 1; i < argc; i++) { 2870 if (strcmp(argv[i], "--headless") == 0) headless = 1; 2871 else if (argv[i][0] == '/' || argv[i][0] == '.') piece_path = argv[i]; 2872 } 2873 2874 ACDisplay *display = NULL; 2875 extern void *g_display; // expose to js-bindings for browser DRM handoff 2876 ACFramebuffer *screen = NULL; 2877 ACInput *input = NULL; 2878 int pixel_scale = 3; // Default: computed below to target ~300px wide 2879#ifdef USE_WAYLAND 2880 ACWaylandDisplay *wayland_display = NULL; 2881 int is_wayland = 0; 2882#endif 2883 2884 if (!headless) { 2885#ifdef USE_WAYLAND 2886 // Prefer Wayland if running under cage compositor 2887 ac_log("[ac-native] WAYLAND_DISPLAY=%s XDG_RUNTIME_DIR=%s\n", 2888 getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "(null)", 2889 getenv("XDG_RUNTIME_DIR") ? getenv("XDG_RUNTIME_DIR") : "(null)"); 2890 if (getenv("WAYLAND_DISPLAY")) { 2891 wayland_display = wayland_display_init(); 2892 if (wayland_display) { 2893 is_wayland = 1; 2894 g_wayland_display = wayland_display; 2895 // Create a minimal ACDisplay for code that needs width/height 2896 display = calloc(1, sizeof(ACDisplay)); 2897 display->width = wayland_display->width; 2898 display->height = wayland_display->height; 2899 g_display = display; 2900 fprintf(stderr, "[ac-native] Wayland display: %dx%d\n", 2901 display->width, display->height); 2902 } else { 2903 ac_log("[ac-native] Wayland init failed — exiting (cage will restart or fallback)\n"); 2904 return 1; // exit so cage exits and init falls through to DRM 2905 } 2906 } 2907 if (!is_wayland) 2908#endif 2909 { 2910 display = drm_init(); 2911 g_display = display; 2912 } 2913 if (!display) { 2914 fprintf(stderr, "[ac-native] FATAL: No display\n"); 2915 // Dump device diagnostics for debugging 2916 fprintf(stderr, "[ac-native] /dev/dri contents:\n"); 2917 system("ls -la /dev/dri/ 2>&1 || echo ' /dev/dri does not exist'"); 2918 fprintf(stderr, "[ac-native] /dev/fb contents:\n"); 2919 system("ls -la /dev/fb* 2>&1 || echo ' no framebuffer devices'"); 2920 fprintf(stderr, "[ac-native] PCI GPU devices:\n"); 2921 system("cat /sys/bus/pci/devices/*/class 2>/dev/null | grep -c 0x03 || echo ' 0'"); 2922 system("for d in /sys/bus/pci/devices/*; do c=$(cat $d/class 2>/dev/null); [ \"${c#0x03}\" != \"$c\" ] && echo \" $(basename $d) $c $(cat $d/vendor $d/device 2>/dev/null)\"; done"); 2923 fprintf(stderr, "[ac-native] dmesg DRM/i915:\n"); 2924 system("dmesg 2>/dev/null | grep -i 'drm\\|i915\\|display\\|error' | tail -20"); 2925 // Write diagnostics to USB too 2926 system("cat /proc/cmdline >> /mnt/ac-init.log 2>/dev/null"); 2927 system("dmesg 2>/dev/null | grep -i 'drm\\|i915\\|display' >> /mnt/ac-init.log 2>/dev/null"); 2928 sleep(30); ac_poweroff(); 2929 return 1; 2930 } 2931 2932 // Target ~300px wide — just divide and let fb_copy_scaled handle the remainder 2933 { 2934 int target = display->width / 300; 2935 if (target < 1) target = 1; 2936 if (target > 16) target = 16; 2937 // Prefer clean divisors, but accept any scale 2938 pixel_scale = target; 2939 for (int delta = 0; delta <= 3; delta++) { 2940 int s = target + delta; 2941 if (s >= 1 && s <= 16 && display->width % s == 0 && display->height % s == 0) { 2942 pixel_scale = s; break; 2943 } 2944 s = target - delta; 2945 if (s >= 1 && display->width % s == 0 && display->height % s == 0) { 2946 pixel_scale = s; break; 2947 } 2948 } 2949 } 2950 ac_log("pixel_scale=%d (display %dx%d -> screen %dx%d)\n", 2951 pixel_scale, display->width, display->height, 2952 display->width / pixel_scale, display->height / pixel_scale); 2953 screen = fb_create(display->width / pixel_scale, display->height / pixel_scale); 2954 if (!screen) { 2955#ifdef USE_WAYLAND 2956 if (is_wayland) wayland_display_destroy(wayland_display); 2957 else 2958#endif 2959 drm_destroy(display); 2960 return 1; 2961 } 2962 } else { 2963 screen = fb_create(320, 200); 2964 } 2965 2966 // Init graphics + font 2967 ACGraph graph; 2968 graph_init(&graph, screen); 2969 if (display) graph_init_gpu(&graph, display); 2970 font_init(); 2971 2972 // Cursor overlay buffer — drawn separately so KidLisp effects don't smear it 2973 ACFramebuffer *cursor_fb = fb_create(screen->width, screen->height); 2974 2975 // Mount USB log early so boot animation can detect USB boot 2976#ifdef USE_WAYLAND 2977 if (!is_wayland) // Under cage: parent already has USB mounted at /mnt 2978#endif 2979 try_mount_log(); 2980 2981 // Read boot visuals (handle + optional per-char colors) from /mnt/config.json 2982 load_boot_visual_config(); 2983 2984 // Init audio + TTS early (needed for boot animation speech) 2985 ACAudio *audio = audio_init(); 2986 ACTts *tts = NULL; 2987 ACWifi *wifi = NULL; 2988 ACSecondaryDisplay *hdmi = NULL; 2989 char bl_path[128] = ""; 2990 int bl_max = 0; 2991 2992#ifdef USE_WAYLAND 2993 if (is_wayland) { 2994 // ── Cage session ── 2995 ac_log("[ac-native] Wayland session\n"); 2996 2997 // Input (Wayland path) 2998 if (!headless) 2999 input = input_init_wayland(wayland_display, display->width, display->height, pixel_scale); 3000 3001 // Boot animation with install prompt (same as DRM path) 3002 audio_boot_beep(audio); 3003 tts = tts_init(audio); 3004 int want_install = 0; 3005 if (!headless) { 3006 want_install = draw_startup_fade(&graph, screen, display, tts, audio, pixel_scale); 3007 if (!want_install) 3008 draw_boot_status(&graph, screen, display, "starting...", pixel_scale); 3009 } 3010 if (want_install) { 3011 int install_ok = auto_install_to_hd(&graph, screen, display, pixel_scale); 3012 if (display) { 3013 int should_reboot = draw_install_reboot_prompt(&graph, screen, display, input, tts, audio, install_ok, pixel_scale); 3014 if (should_reboot) { 3015 if (tts) tts_wait(tts); // let TTS finish before reboot 3016 // Use the shared ac_reboot() path — it tries reboot(2) 3017 // syscall first (reboot=efi,cold cmdline steers this 3018 // to UEFI ResetSystem which is the only consistently 3019 // working path on coreboot Chromebooks), falls back 3020 // to systemctl / busybox reboot -f, then finally 3021 // _exit(2) so init.sh sees "reboot requested" and 3022 // re-enters its own multi-tiered reboot loop 3023 // (reboot -f → sysrq-b → halt -f → sysrq-c panic). 3024 // The old path here did `_exit(0)` which triggered 3025 // init.sh's POWEROFF branch, so a failing reboot(2) 3026 // would hang waiting to power off instead of retrying. 3027 ac_reboot(); 3028 } 3029 } 3030 } 3031 3032 // WiFi is already running from parent — just connect to it 3033 if (!wifi_disabled) 3034 wifi = wifi_init(); 3035 } else 3036#endif 3037 { 3038 // ── DRM boot: full boot sequence ── 3039 // Load persisted sample (overrides default seed if file exists) 3040 if (audio && audio_sample_load(audio, "/mnt/ac-sample.raw") > 0) { 3041 ac_log("[audio] loaded persisted sample (%d samples)\n", audio->sample_len); 3042 } 3043 audio_boot_beep(audio); 3044 tts = tts_init(audio); 3045 // Skip tts_precache here — defer to background after piece loads 3046 3047 // Startup fade animation (black → white, hides kernel text) 3048 ac_log("[ac-native] pre-fade: display=%p screen=%p w=%d h=%d\n", 3049 (void*)display, (void*)screen, 3050 display ? display->width : -1, display ? display->height : -1); 3051 int want_install = 0; 3052 if (!headless) { 3053 want_install = draw_startup_fade(&graph, screen, display, tts, audio, pixel_scale); 3054 if (!want_install) 3055 draw_boot_status(&graph, screen, display, "starting input...", pixel_scale); 3056 } 3057 3058 // Init input (DRM path) 3059 if (!headless) 3060 input = input_init(display->width, display->height, pixel_scale); 3061 3062 // Find backlight path 3063 { 3064 DIR *bldir = opendir("/sys/class/backlight"); 3065 if (bldir) { 3066 struct dirent *ent; 3067 while ((ent = readdir(bldir)) && !bl_path[0]) { 3068 if (ent->d_name[0] == '.') continue; 3069 char tmp[160]; 3070 snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/max_brightness", ent->d_name); 3071 FILE *f = fopen(tmp, "r"); 3072 if (f) { 3073 if (fscanf(f, "%d", &bl_max) == 1 && bl_max > 0) 3074 snprintf(bl_path, sizeof(bl_path), "/sys/class/backlight/%s", ent->d_name); 3075 fclose(f); 3076 } 3077 } 3078 closedir(bldir); 3079 } 3080 } 3081 3082 // Install kernel to internal drive (only if user pressed W during boot) 3083 if (want_install) { 3084 int install_ok = auto_install_to_hd(&graph, screen, display, pixel_scale); 3085 int should_reboot = 1; 3086 if (!headless && display) 3087 should_reboot = draw_install_reboot_prompt(&graph, screen, display, input, tts, audio, install_ok, pixel_scale); 3088 if (install_ok) should_reboot = 1; 3089 if (should_reboot) { 3090 if (tts) { tts_speak(tts, "rebooting"); tts_wait(tts); } 3091 audio_shutdown_sound(audio); 3092 usleep(500000); 3093 sync(); 3094 ac_reboot(); 3095 while (running) sleep(1); 3096 } 3097 } 3098 3099 // Init WiFi 3100 if (!wifi_disabled) { 3101 if (!headless && display) 3102 draw_boot_status(&graph, screen, display, "starting wifi...", pixel_scale); 3103 wifi = wifi_init(); 3104 // Don't autoconnect from C — notepat.mjs handles scanning 3105 // at 30s intervals (reduced from 2s to avoid 65ms frame drops). 3106 } else { 3107 if (!headless && display) 3108 draw_boot_status(&graph, screen, display, "wifi disabled", pixel_scale); 3109 ac_log("[ac-native] WiFi disabled by config\n"); 3110 } 3111 3112 // Init secondary HDMI display (if connected) 3113 if (display && !display->is_fbdev) { 3114 hdmi = drm_init_secondary(display); 3115 if (hdmi) ac_log("[ac-native] HDMI secondary: %dx%d\n", hdmi->width, hdmi->height); 3116 } 3117 } 3118 3119 // Init JS 3120 ACRuntime *rt = js_init(&graph, input, audio, wifi, tts); 3121 if (!rt) { 3122 fprintf(stderr, "[ac-native] FATAL: Cannot init JS\n"); 3123 wifi_destroy(wifi); 3124 audio_destroy(audio); 3125 fb_destroy(screen); 3126 if (display) drm_destroy(display); 3127 return 1; 3128 } 3129 rt->hdmi = hdmi; 3130 3131 // Read user config from EFI partition (/mnt/config.json) 3132 { 3133 FILE *cfg = fopen("/mnt/config.json", "r"); 3134 if (cfg) { 3135 char buf[32768] = {0}; 3136 size_t n = fread(buf, 1, sizeof(buf) - 1, cfg); 3137 buf[n] = '\0'; 3138 fclose(cfg); 3139 3140 // Skip identity block marker line if present 3141 char *json = buf; 3142 if (strncmp(buf, "AC_IDENTITY_BLOCK_V1", 20) == 0) { 3143 char *nl = strchr(buf, '\n'); 3144 if (nl) json = nl + 1; 3145 } 3146 3147 parse_config_string(json, "\"handle\"", rt->handle, sizeof(rt->handle)); 3148 parse_config_string(json, "\"piece\"", rt->piece, sizeof(rt->piece)); 3149 ac_log("[ac-native] Config: handle=%s piece=%s\n", 3150 rt->handle[0] ? rt->handle : "(none)", 3151 rt->piece[0] ? rt->piece : "(none)"); 3152 3153 // Check voice config (voice: "off" disables keystroke TTS) 3154 char voice_cfg[16] = {0}; 3155 if (parse_config_string(json, "\"voice\"", voice_cfg, sizeof(voice_cfg))) { 3156 voice_off = (strcmp(voice_cfg, "off") == 0); 3157 ac_log("[config] voice: %s\n", voice_cfg); 3158 } 3159 3160 // (Tokens are baked early in load_boot_visual_config() so the 3161 // boot-fade badges can render — see notes there.) 3162 } 3163 } 3164 3165 // Check for previous crash (written by signal handler) 3166 { 3167 FILE *cf = fopen("/mnt/crash.json", "r"); 3168 if (cf) { 3169 char cbuf[512] = {0}; 3170 fread(cbuf, 1, sizeof(cbuf) - 1, cf); 3171 fclose(cf); 3172 char sig[32] = {0}; 3173 parse_config_string(cbuf, "\"signal\"", sig, sizeof(sig)); 3174 if (sig[0]) { 3175 rt->crash_active = 1; 3176 rt->crash_frame = 0; 3177 snprintf(rt->crash_msg, sizeof(rt->crash_msg), 3178 "previous crash: %s", sig); 3179 ac_log("[ac-native] Previous crash detected: %s\n", sig); 3180 } 3181 // Remove crash file so we don't re-show it 3182 remove("/mnt/crash.json"); 3183 } 3184 } 3185 3186 // Override piece_path from config.json boot piece (pieces bundled at /pieces/) 3187 // Resolve aliases: "claude" and "cc" → terminal (with param "claude") 3188 if (strcmp(rt->piece, "claude") == 0 || strcmp(rt->piece, "cc") == 0) { 3189 strcpy(rt->piece, "terminal"); 3190 // Set initial jump params so terminal.mjs gets "claude" as param 3191 strcpy(rt->jump_params[0], "claude"); 3192 rt->jump_param_count = 1; 3193 } 3194 char piece_path_buf[256]; 3195 if (rt->piece[0]) { 3196 snprintf(piece_path_buf, sizeof(piece_path_buf), "/pieces/%s.mjs", rt->piece); 3197 if (access(piece_path_buf, R_OK) == 0) { 3198 piece_path = piece_path_buf; 3199 ac_log("[ac-native] Boot piece from config: %s\n", piece_path); 3200 } else { 3201 ac_log("[ac-native] Config piece '%s' not found, falling back to %s\n", 3202 rt->piece, piece_path); 3203 } 3204 } 3205 3206 // Load piece 3207 if (js_load_piece(rt, piece_path) < 0) { 3208 fprintf(stderr, "[ac-native] FATAL: Cannot load %s\n", piece_path); 3209 if (!headless) { 3210 graph_wipe(&graph, (ACColor){200, 0, 0, 255}); 3211 graph_ink(&graph, (ACColor){255, 255, 255, 255}); 3212 char msg[256]; 3213 snprintf(msg, sizeof(msg), "Cannot load: %s", piece_path); 3214 font_draw(&graph, msg, 20, 20, 2); 3215 ac_display_present(display, screen, pixel_scale); 3216 sleep(10); 3217 } 3218 js_destroy(rt); 3219 audio_destroy(audio); 3220 fb_destroy(screen); 3221 if (display) drm_destroy(display); 3222 if (logfile) { fclose(logfile); logfile = NULL; } 3223 sync(); 3224 ac_poweroff(); 3225 return 1; 3226 } 3227 3228 struct timespec boot_end; 3229 clock_gettime(CLOCK_MONOTONIC, &boot_end); 3230 double boot_ms = (boot_end.tv_sec - boot_start.tv_sec) * 1000.0 + 3231 (boot_end.tv_nsec - boot_start.tv_nsec) / 1000000.0; 3232 ac_log("[ac-native] Booted in %.1fms\n", boot_ms); 3233 3234 // Log audio and backlight status 3235 ac_log("[ac-native] Audio: %s\n", audio && audio->pcm ? "OK" : "NO PCM"); 3236 ac_log("[ac-native] Backlight: %s (max=%d)\n", bl_path[0] ? bl_path : "none", bl_max); 3237 ac_log("[ac-native] Input devices: %d\n", input ? input->count : 0); 3238 // Log each input device name for debugging keyboard issues 3239 if (input) { 3240 for (int i = 0; i < input->count; i++) { 3241 char dname[256] = "?"; 3242 ioctl(input->fds[i], EVIOCGNAME(sizeof(dname)), dname); 3243 ac_log("[input] dev%d: %s\n", i, dname); 3244 } 3245 } 3246 ac_log("[ac-native] NuPhy: has_analog=%d hidraw=%d\n", 3247 input ? input->has_analog : -1, input ? input->hidraw_count : -1); 3248 ac_log("[ac-native] JS: boot=%d paint=%d act=%d sim=%d\n", 3249 JS_IsFunction(rt->ctx, rt->boot_fn), 3250 JS_IsFunction(rt->ctx, rt->paint_fn), 3251 JS_IsFunction(rt->ctx, rt->act_fn), 3252 JS_IsFunction(rt->ctx, rt->sim_fn)); 3253 if (logfile) { fflush(logfile); } 3254 3255 // Prewarm audio engine so first keypress has zero latency 3256 audio_prewarm(audio); 3257 3258 // Drain any queued input events from boot animation (prevents first-key stick) 3259 input_poll(input); 3260 input->event_count = 0; 3261 3262 // Call boot() immediately — melody plays over the already-running piece 3263 js_call_boot(rt); 3264 3265 // Precache TTS in background — user won't type for several seconds 3266 pthread_t tts_precache_thread; 3267 int tts_precache_spawned = 0; 3268 if (tts) { 3269 tts_precache_spawned = (pthread_create(&tts_precache_thread, NULL, 3270 (void *(*)(void *))tts_precache, tts) == 0); 3271 } 3272 3273 // Let TTS finish and play ready melody (non-blocking to piece) 3274 if (tts) { 3275 tts_wait(tts); 3276 usleep(300000); // Let TTS ring buffer drain 3277 } 3278 audio_ready_melody(audio); 3279 3280 // Join TTS precache thread before entering main loop 3281 if (tts_precache_spawned) 3282 pthread_join(tts_precache_thread, NULL); 3283 3284 // Init performance logger 3285 perf_init(); 3286 3287 // ── Graceful DRM → cage transition ── 3288 // After boot completes in DRM mode, try to launch cage compositor 3289 // and re-exec ac-native under it. This gives us a Wayland session 3290 // for browser popups (Firefox, OAuth) while keeping fast DRM boot. 3291#ifdef USE_WAYLAND 3292 if (!is_wayland && !headless && getpid() == 1 && 3293 access("/bin/cage", X_OK) == 0 && access("/dev/dri/card0", F_OK) == 0) { 3294 ac_log("[cage-transition] Starting cage compositor...\n"); 3295 3296 // Close audio so cage child can open ALSA 3297 audio_destroy(audio); 3298 audio = NULL; 3299 3300 // Release DRM master so cage can take it 3301 drm_release_master(display); 3302 ac_log("[cage-transition] Released DRM master\n"); 3303 3304 pid_t cage_pid = fork(); 3305 if (cage_pid == 0) { 3306 // === CHILD: start seatd + cage + ac-native === 3307 setenv("HOME", "/tmp", 1); 3308 setenv("XDG_RUNTIME_DIR", "/tmp/xdg", 1); 3309 setenv("WLR_RENDERER", "pixman", 1); 3310 setenv("WLR_BACKENDS", "drm", 1); 3311 setenv("LIBGL_ALWAYS_SOFTWARE", "1", 1); 3312 setenv("WLR_LIBINPUT_NO_DEVICES", "1", 1); 3313 mkdir("/tmp/xdg", 0700); 3314 3315 // Log /dev/input state for debugging (write to USB) 3316 { 3317 int dfd = open("/mnt/cage-diag.log", O_WRONLY | O_CREAT | O_TRUNC, 0644); 3318 if (dfd >= 0) { 3319 char dbuf[1024]; 3320 int dlen = 0; 3321 dlen += snprintf(dbuf + dlen, sizeof(dbuf) - dlen, "pid=%d\n", getpid()); 3322 DIR *d = opendir("/dev/input"); 3323 if (d) { 3324 struct dirent *e; 3325 dlen += snprintf(dbuf + dlen, sizeof(dbuf) - dlen, "dev-input:"); 3326 while ((e = readdir(d))) { 3327 if (e->d_name[0] != '.') 3328 dlen += snprintf(dbuf + dlen, sizeof(dbuf) - dlen, " %s", e->d_name); 3329 } 3330 dlen += snprintf(dbuf + dlen, sizeof(dbuf) - dlen, "\n"); 3331 closedir(d); 3332 } else { 3333 dlen += snprintf(dbuf + dlen, sizeof(dbuf) - dlen, 3334 "dev-input: MISSING errno=%d\n", errno); 3335 } 3336 // Also list /dev/dri 3337 d = opendir("/dev/dri"); 3338 if (d) { 3339 struct dirent *e; 3340 dlen += snprintf(dbuf + dlen, sizeof(dbuf) - dlen, "dev-dri:"); 3341 while ((e = readdir(d))) { 3342 if (e->d_name[0] != '.') 3343 dlen += snprintf(dbuf + dlen, sizeof(dbuf) - dlen, " %s", e->d_name); 3344 } 3345 dlen += snprintf(dbuf + dlen, sizeof(dbuf) - dlen, "\n"); 3346 closedir(d); 3347 } 3348 write(dfd, dbuf, dlen); 3349 fsync(dfd); 3350 close(dfd); 3351 } 3352 } 3353 3354 // Ensure /run exists (may be missing if mount_minimal_fs re-mounted rootfs) 3355 mkdir("/run", 0755); 3356 mount("tmpfs", "/run", "tmpfs", 0, NULL); 3357 3358 // Start seatd 3359 system("seatd -g root > /tmp/seatd.log 2>&1 &"); 3360 for (int si = 0; si < 30; si++) { 3361 usleep(100000); 3362 if (access("/run/seatd.sock", F_OK) == 0) break; 3363 } 3364 3365 if (access("/run/seatd.sock", F_OK) != 0) { 3366 FILE *ef = fopen("/tmp/cage-stderr.log", "w"); 3367 if (ef) { 3368 fprintf(ef, "seatd failed to start after 3s\n"); 3369 fprintf(ef, "run-dir-exists=%d run-writable=%d\n", 3370 access("/run", F_OK) == 0, access("/run", W_OK) == 0); 3371 // Dump seatd log 3372 FILE *sl = fopen("/tmp/seatd.log", "r"); 3373 if (sl) { 3374 char sbuf[256]; 3375 fprintf(ef, "--- seatd.log ---\n"); 3376 while (fgets(sbuf, sizeof(sbuf), sl)) fputs(sbuf, ef); 3377 fclose(sl); 3378 } 3379 fclose(ef); 3380 } 3381 _exit(1); 3382 } 3383 3384 // Redirect stderr to file so parent can read cage errors 3385 FILE *cage_log = fopen("/tmp/cage-stderr.log", "w"); 3386 if (cage_log) { 3387 dup2(fileno(cage_log), STDERR_FILENO); 3388 fclose(cage_log); 3389 } 3390 fprintf(stderr, "[cage-transition] seatd ok, launching cage...\n"); 3391 3392 execlp("cage", "cage", "-s", "--", "/ac-native", "/piece.mjs", NULL); 3393 fprintf(stderr, "[cage-transition] exec cage failed: %s\n", strerror(errno)); 3394 _exit(127); 3395 } 3396 3397 if (cage_pid > 0) { 3398 // === PARENT: wait for cage session to end === 3399 ac_log("[cage-transition] cage pid=%d, waiting...\n", cage_pid); 3400 int status = 0; 3401 waitpid(cage_pid, &status, 0); 3402 int rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1; 3403 ac_log("[cage-transition] cage exited: %d\n", rc); 3404 3405 // Copy cage stderr to USB log 3406 FILE *cage_err = fopen("/tmp/cage-stderr.log", "r"); 3407 if (cage_err) { 3408 char buf[256]; 3409 while (fgets(buf, sizeof(buf), cage_err)) 3410 ac_log("[cage-err] %s", buf); 3411 fclose(cage_err); 3412 } 3413 // Copy child ac-native log to USB log 3414 FILE *cage_child = fopen("/tmp/ac-native-cage.log", "r"); 3415 if (cage_child) { 3416 char buf[256]; 3417 while (fgets(buf, sizeof(buf), cage_child)) 3418 ac_log("[cage-child] %s", buf); 3419 fclose(cage_child); 3420 } 3421 3422 // Cleanup seatd 3423 system("killall seatd 2>/dev/null"); 3424 } else { 3425 ac_log("[cage-transition] fork failed: %s\n", strerror(errno)); 3426 } 3427 3428 // Check if cage child requested reboot/poweroff 3429 if (reboot_requested) { 3430 ac_log("[cage-transition] Reboot requested by cage child\n"); 3431 ac_log_flush(); 3432 ac_reboot(); 3433 } 3434 if (poweroff_requested) { 3435 ac_log("[cage-transition] Poweroff requested by cage child\n"); 3436 ac_log_flush(); 3437 ac_poweroff(); 3438 } 3439 3440 // Reclaim DRM and continue in DRM mode (fallback) 3441 drm_acquire_master(display); 3442 audio = audio_init(); 3443 ac_log("[cage-transition] Reclaimed DRM, continuing in DRM mode\n"); 3444 } 3445#endif 3446 3447 // Video recorder (F9 to toggle) — declared before headless branch so cleanup is in scope 3448 ACRecorder *recorder = NULL; 3449 3450 if (headless) { 3451 for (int i = 0; i < 10 && running; i++) { 3452 js_call_sim(rt); 3453 js_call_paint(rt); 3454 } 3455 } else { 3456#ifdef HAVE_AVCODEC 3457 if (screen && audio) { 3458 recorder = recorder_create(screen->width, screen->height, 60, 3459 audio->actual_rate ? audio->actual_rate : 192000); 3460 if (recorder) { 3461 ac_log("[ac-native] recorder ready (%dx%d)\n", screen->width, screen->height); 3462 // Expose to JS via sound.tape.* bindings 3463 if (rt) rt->recorder = recorder; 3464 } 3465 } 3466#endif 3467 3468 // Main loop 3469 struct timespec frame_time; 3470 clock_gettime(CLOCK_MONOTONIC, &frame_time); 3471 3472 int main_frame = 0; 3473 while (running) { 3474#ifdef USE_WAYLAND 3475 // Under Wayland: input_poll handles all Wayland event dispatch 3476 // (reading from socket + firing listeners in correct order) 3477 if (is_wayland) { 3478 // nothing — input_poll does full dispatch 3479 } else 3480#endif 3481 { 3482 // DRM handoff: xdg-open sends SIGUSR1 to release, SIGUSR2 to reclaim 3483 if (drm_handoff_release && display) { 3484 drm_handoff_release = 0; 3485 3486 char browser_url[2048] = ""; 3487 FILE *uf = fopen("/tmp/.browser-url", "r"); 3488 if (uf) { 3489 if (fgets(browser_url, sizeof(browser_url), uf)) 3490 browser_url[strcspn(browser_url, "\n")] = 0; 3491 fclose(uf); 3492 unlink("/tmp/.browser-url"); 3493 } 3494 3495 if (browser_url[0]) { 3496 ac_log("[browser] URL: %s", browser_url); 3497 drm_release_master(display); 3498 ac_log("[browser] Released DRM master"); 3499 3500 // Fork a child for the entire browser session. 3501 // Child sets env, starts seatd+cage+firefox, then exits. 3502 // Parent waits, then reclaims DRM. If child crashes, parent survives. 3503 pid_t bpid = fork(); 3504 if (bpid == 0) { 3505 // === CHILD PROCESS === 3506 mkdir("/tmp/xdg-browser", 0700); 3507 mkdir("/tmp/.mozilla", 0755); 3508 mkdir("/run", 0755); 3509 3510 setenv("HOME", "/tmp", 1); 3511 setenv("XDG_RUNTIME_DIR", "/tmp/xdg-browser", 1); 3512 setenv("WLR_BACKENDS", "drm", 1); 3513 setenv("WLR_RENDERER", "pixman", 1); 3514 setenv("LD_LIBRARY_PATH", "/lib64:/opt/firefox", 1); 3515 setenv("LIBGL_ALWAYS_SOFTWARE", "1", 1); 3516 setenv("MOZ_ENABLE_WAYLAND", "1", 1); 3517 setenv("GDK_BACKEND", "wayland", 1); 3518 setenv("MOZ_APP_LAUNCHER", "/opt/firefox/firefox", 1); 3519 setenv("GRE_HOME", "/opt/firefox", 1); 3520 setenv("DBUS_SESSION_BUS_ADDRESS", "disabled:", 1); 3521 setenv("MOZ_DBUS_REMOTE", "0", 1); 3522 setenv("MOZ_DISABLE_CONTENT_SANDBOX", "1", 1); 3523 3524 // Ensure /etc/group exists for seatd 3525 { 3526 FILE *grp = fopen("/etc/group", "a"); 3527 if (grp) { 3528 fseek(grp, 0, SEEK_END); 3529 if (ftell(grp) == 0) fprintf(grp, "root:x:0:\n"); 3530 fclose(grp); 3531 } 3532 } 3533 3534 // Start seatd, wait for socket 3535 system("seatd -g root > /tmp/seatd.log 2>&1 &"); 3536 for (int si = 0; si < 15; si++) { 3537 usleep(200000); 3538 if (access("/run/seatd.sock", F_OK) == 0) break; 3539 } 3540 3541 // Run cage+firefox (blocks until browser exits) 3542 char cmd[4096]; 3543 snprintf(cmd, sizeof(cmd), 3544 "cd /opt/firefox && cage -s -- ./firefox --kiosk --no-remote " 3545 "--new-instance '%s' > /tmp/cage-out.log 2>&1", browser_url); 3546 int rc = system(cmd); 3547 3548 // Cleanup 3549 system("killall seatd 2>/dev/null"); 3550 // Copy logs to USB 3551 FILE *lf = fopen("/tmp/cage-out.log", "r"); 3552 if (lf) { 3553 FILE *mf = fopen("/mnt/cage.log", "w"); 3554 if (mf) { 3555 char buf[512]; 3556 while (fgets(buf, sizeof(buf), lf)) fputs(buf, mf); 3557 fclose(mf); 3558 sync(); 3559 } 3560 fclose(lf); 3561 } 3562 _exit(rc); 3563 } 3564 3565 // === PARENT PROCESS === 3566 if (bpid > 0) { 3567 int bstatus = 0; 3568 ac_log("[browser] child pid=%d, waiting...", bpid); 3569 waitpid(bpid, &bstatus, 0); 3570 if (WIFEXITED(bstatus)) 3571 ac_log("[browser] child exited: %d", WEXITSTATUS(bstatus)); 3572 else if (WIFSIGNALED(bstatus)) 3573 ac_log("[browser] child killed by signal %d", WTERMSIG(bstatus)); 3574 } else { 3575 ac_log("[browser] fork failed: %s", strerror(errno)); 3576 } 3577 3578 // Always reclaim DRM, even if child crashed 3579 drm_acquire_master(display); 3580 ac_log("[browser] Reclaimed DRM master"); 3581 } else { 3582 ac_log("[browser] SIGUSR1 but no URL in /tmp/.browser-url"); 3583 } 3584 } 3585 } 3586 3587 input_poll(input); 3588 main_frame++; 3589 // Check for device token API response (from wifi-connect fetch) 3590 if (main_frame % 300 == 0) { 3591 FILE *rf = fopen("/tmp/claude-api-resp.json", "r"); 3592 if (rf) { 3593 char rbuf[2048] = {0}; 3594 fread(rbuf, 1, sizeof(rbuf) - 1, rf); 3595 fclose(rf); 3596 unlink("/tmp/claude-api-resp.json"); 3597 // Extract Claude token: {"token":"sk-ant-...","githubPat":"ghp_..."} 3598 const char *tk = strstr(rbuf, "\"token\""); 3599 if (tk) { 3600 const char *tv = strchr(tk + 7, ':'); 3601 if (tv) { 3602 tv++; while (*tv == ' ' || *tv == '"') tv++; 3603 const char *te = strchr(tv, '"'); 3604 if (te && te > tv && (te - tv) > 10) { 3605 FILE *tf = fopen("/claude-token", "w"); 3606 if (tf) { fwrite(tv, 1, te - tv, tf); fclose(tf); } 3607 ac_log("[tokens] claude token from API (%d bytes)\n", (int)(te - tv)); 3608 } 3609 } 3610 } 3611 // Extract GitHub PAT 3612 const char *gk = strstr(rbuf, "\"githubPat\""); 3613 if (gk) { 3614 const char *gv = strchr(gk + 11, ':'); 3615 if (gv) { 3616 gv++; while (*gv == ' ' || *gv == '"') gv++; 3617 const char *ge = strchr(gv, '"'); 3618 if (ge && ge > gv && (ge - gv) > 5) { 3619 FILE *gf = fopen("/github-pat", "w"); 3620 if (gf) { fwrite(gv, 1, ge - gv, gf); fclose(gf); } 3621 ac_log("[tokens] github pat from API (%d bytes)\n", (int)(ge - gv)); 3622 } 3623 } 3624 } 3625 } 3626 } 3627 // Copy tmpfs debug logs to USB periodically (every 5 sec) 3628 if (logfile && main_frame % 300 == 0) { 3629 FILE *xf = fopen("/tmp/xdg-open.log", "r"); 3630 if (xf) { 3631 char xbuf[512]; 3632 while (fgets(xbuf, sizeof(xbuf), xf)) 3633 ac_log("[xdg] %s", xbuf); 3634 fclose(xf); 3635 unlink("/tmp/xdg-open.log"); // only report once 3636 } 3637 FILE *ff = fopen("/tmp/firefox-debug.log", "r"); 3638 if (ff) { 3639 char fbuf[512]; 3640 while (fgets(fbuf, sizeof(fbuf), ff)) 3641 ac_log("[firefox] %s", fbuf); 3642 fclose(ff); 3643 unlink("/tmp/firefox-debug.log"); 3644 } 3645 } 3646 // Log input state periodically (every 5 sec) 3647 if (logfile && main_frame % 300 == 1) { 3648 int analog_active = 0; 3649 for (int k = 0; k < MAX_ANALOG_KEYS; k++) 3650 if (input->analog_keys[k].active) analog_active++; 3651 ac_log("[input] frame=%d events=%d has_analog=%d hidraw=%d analog_active=%d evdev=%d\n", 3652 main_frame, input->event_count, input->has_analog, 3653 input->hidraw_count, analog_active, input->count); 3654 } 3655 // Log key events to USB + instant TTS for key presses 3656 for (int i = 0; i < input->event_count; i++) { 3657 if (input->events[i].type == AC_EVENT_KEYBOARD_DOWN) { 3658 if (logfile) { 3659 ac_log("[key] DOWN code=%d name=%s pressure=%.3f\n", 3660 input->events[i].key_code, 3661 input->events[i].key_name, 3662 input->events[i].pressure); 3663 } 3664 // Instant TTS: play cached sound immediately (bypasses JS frame delay) 3665 // Only when running prompt.mjs (other pieces should not voice keystrokes) 3666 if (tts && !voice_off && strcmp(rt->piece, "prompt") == 0) { 3667 const char *kn = input->events[i].key_name; 3668 if (kn && kn[0] && kn[1] == 0) { 3669 // Single printable character 3670 tts_speak_cached(tts, kn); 3671 } else if (kn && strcmp(kn, "space") == 0) { 3672 tts_speak_cached(tts, "space"); 3673 } else if (kn && strcmp(kn, "backspace") == 0) { 3674 tts_speak_cached(tts, "back"); 3675 } else if (kn && (strcmp(kn, "enter") == 0 || strcmp(kn, "return") == 0)) { 3676 tts_speak_cached(tts, "enter"); 3677 } else if (kn && strcmp(kn, "escape") == 0) { 3678 tts_speak_cached(tts, "clear"); 3679 } else if (kn && strcmp(kn, "tab") == 0) { 3680 tts_speak_cached(tts, "tab"); 3681 } 3682 } 3683 } else if (input->events[i].type == AC_EVENT_KEYBOARD_UP && logfile) { 3684 ac_log("[key] UP code=%d name=%s pressure=%.3f\n", 3685 input->events[i].key_code, 3686 input->events[i].key_name, 3687 input->events[i].pressure); 3688 } 3689 } 3690 // Hardware keys 3691 static int ctrl_held = 0; 3692 int power_pressed = 0; 3693 int scale_change = 0; // -1 = decrease density (bigger pixels), +1 = increase 3694 // Global escape → prompt fallback. If the current piece doesn't 3695 // jump() or otherwise consume an escape key-down this frame, we 3696 // transparently route the user back to the prompt after act() 3697 // returns. Notepat has its own triple-escape-to-exit behavior and 3698 // the prompt itself uses escape to clear input, so both are 3699 // exempt from the fallback. 3700 int escape_pressed_this_frame = 0; 3701 for (int i = 0; i < input->event_count; i++) { 3702 if (input->events[i].type == AC_EVENT_KEYBOARD_DOWN && 3703 input->events[i].key_code == KEY_ESC) { 3704 if (strcmp(rt->piece, "notepat") != 0 && 3705 strcmp(rt->piece, "prompt") != 0) { 3706 escape_pressed_this_frame = 1; 3707 } 3708 break; 3709 } 3710 } 3711 for (int i = 0; i < input->event_count; i++) { 3712 // Track ctrl modifier 3713 if (input->events[i].key_code == KEY_LEFTCTRL || input->events[i].key_code == KEY_RIGHTCTRL) { 3714 ctrl_held = (input->events[i].type == AC_EVENT_KEYBOARD_DOWN) ? 1 : 0; 3715 } 3716 if (input->events[i].type == AC_EVENT_KEYBOARD_DOWN) { 3717 // Ctrl+= (or Ctrl++) → bigger pixels (increase scale number) 3718 // Ctrl+- → smaller pixels (decrease scale number) 3719 if (ctrl_held && (input->events[i].key_code == KEY_EQUAL || 3720 input->events[i].key_code == KEY_KPPLUS)) { 3721 scale_change = -1; // bigger pixels (lower res) 3722 input->events[i].type = 0; // suppress from JS 3723 } else if (ctrl_held && (input->events[i].key_code == KEY_MINUS || 3724 input->events[i].key_code == KEY_KPMINUS)) { 3725 scale_change = 1; // smaller pixels (higher res) 3726 input->events[i].type = 0; // suppress from JS 3727 } else if (ctrl_held && input->events[i].key_code == KEY_0) { 3728 scale_change = 99; // reset to default (3) 3729 input->events[i].type = 0; // suppress from JS 3730 } 3731 // Volume: KEY_VOLUMEUP/DOWN/MUTE or F1/F2/F3 as fallback 3732 else if (strcmp(input->events[i].key_name, "audiovolumeup") == 0 || 3733 strcmp(input->events[i].key_name, "f3") == 0) { 3734 audio_volume_adjust(audio, 1); 3735 audio_synth(audio, WAVE_SINE, 1200.0, 0.04, 0.15, 0.001, 0.03, 0.0); 3736 } else if (strcmp(input->events[i].key_name, "audiovolumedown") == 0 || 3737 strcmp(input->events[i].key_name, "f2") == 0) { 3738 audio_volume_adjust(audio, -1); 3739 audio_synth(audio, WAVE_SINE, 800.0, 0.04, 0.15, 0.001, 0.03, 0.0); 3740 } else if (strcmp(input->events[i].key_name, "audiomute") == 0 || 3741 strcmp(input->events[i].key_name, "f1") == 0) { 3742 audio_volume_adjust(audio, 0); 3743 audio_synth(audio, WAVE_SINE, 440.0, 0.08, 0.15, 0.001, 0.07, 0.0); 3744 // Brightness: KEY_BRIGHTNESS or F5/F6 as fallback 3745 } else if ((strcmp(input->events[i].key_name, "brightnessup") == 0 || 3746 strcmp(input->events[i].key_name, "brightnessdown") == 0 || 3747 strcmp(input->events[i].key_name, "f6") == 0 || 3748 strcmp(input->events[i].key_name, "f5") == 0) && bl_path[0]) { 3749 int up = (strcmp(input->events[i].key_name, "brightnessup") == 0 || 3750 strcmp(input->events[i].key_name, "f6") == 0); 3751 char tmp[160]; 3752 snprintf(tmp, sizeof(tmp), "%s/brightness", bl_path); 3753 FILE *f = fopen(tmp, "r"); 3754 int cur = bl_max / 2; 3755 if (f) { fscanf(f, "%d", &cur); fclose(f); } 3756 int step = bl_max / 20; // 5% steps 3757 if (step < 1) step = 1; 3758 cur += up ? step : -step; 3759 if (cur < 1) cur = 1; // never fully off 3760 if (cur > bl_max) cur = bl_max; 3761 f = fopen(tmp, "w"); 3762 if (f) { fprintf(f, "%d", cur); fclose(f); } 3763 // Click sound: higher = brighter, lower = dimmer 3764 audio_synth(audio, WAVE_SINE, up ? 1000.0 : 600.0, 3765 0.04, 0.12, 0.001, 0.03, 0.0); 3766 } 3767 // Tape recording: PrintScreen (or Insert/Pause as fallbacks 3768 // on laptops where PrtSc doesn't fire standalone) toggles 3769 // MP4 tape recording. Saves to /mnt/tapes/<slug>.mp4 where 3770 // slug follows the same YYYY.MM.DD.HH.MM.SS.mmm format that 3771 // web AC uses for tapes. On stop, auto-uploads to the 3772 // cloud via a background curl shell-out. 3773 else if ((strcmp(input->events[i].key_name, "printscreen") == 0 || 3774 strcmp(input->events[i].key_name, "insert") == 0 || 3775 strcmp(input->events[i].key_name, "pause") == 0) && recorder) { 3776 if (recorder_is_recording(recorder)) { 3777 // Stop: remove audio tap, finalize file 3778 if (audio) { 3779 audio->rec_callback = NULL; 3780 audio->rec_userdata = NULL; 3781 } 3782 // Snapshot the path before stop (recorder_stop may clear it) 3783 char saved_tape_path[256] = {0}; 3784 strncpy(saved_tape_path, g_tape_current_path, sizeof(saved_tape_path) - 1); 3785 recorder_stop(recorder); 3786 // Clear the live overlay state 3787 g_tape_recording = 0; 3788 g_tape_current_path[0] = '\0'; 3789 // TTS announce + audible cue (descending) 3790 if (tts) tts_speak(tts, "tape stopped"); 3791 audio_synth(audio, WAVE_SINE, 880.0, 0.12, 0.2, 0.001, 0.10, 0.0); 3792 audio_synth(audio, WAVE_SINE, 660.0, 0.12, 0.15, 0.03, 0.09, 0.0); 3793 // Kick off background upload if we captured a path 3794 if (saved_tape_path[0]) { 3795 tape_upload_async(saved_tape_path); 3796 } 3797 } else { 3798 // Start: generate timestamped slug, wire up audio tap 3799 mkdir("/mnt/tapes", 0755); 3800 time_t now = time(NULL); 3801 struct tm *tm = gmtime(&now); 3802 // Milliseconds for slug uniqueness 3803 struct timespec ts; 3804 clock_gettime(CLOCK_REALTIME, &ts); 3805 int ms = (int)(ts.tv_nsec / 1000000); 3806 char rec_path[256]; 3807 snprintf(rec_path, sizeof(rec_path), 3808 "/mnt/tapes/%04d.%02d.%02d.%02d.%02d.%02d.%03d.mp4", 3809 tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, 3810 tm->tm_hour, tm->tm_min, tm->tm_sec, ms); 3811 if (recorder_start(recorder, rec_path) == 0) { 3812 // Wire up audio tap 3813 if (audio) { 3814 audio->rec_userdata = recorder; 3815 audio->rec_callback = rec_audio_tap; 3816 } 3817 // Latch the path + flag for the on-screen overlay 3818 strncpy(g_tape_current_path, rec_path, sizeof(g_tape_current_path) - 1); 3819 g_tape_current_path[sizeof(g_tape_current_path) - 1] = '\0'; 3820 g_tape_recording = 1; 3821 g_tape_start_sec = time(NULL); 3822 // TTS announce + audible cue (ascending) 3823 if (tts) tts_speak(tts, "tape rolling"); 3824 audio_synth(audio, WAVE_SINE, 660.0, 0.12, 0.2, 0.001, 0.10, 0.0); 3825 audio_synth(audio, WAVE_SINE, 880.0, 0.12, 0.15, 0.03, 0.09, 0.0); 3826 } 3827 } 3828 } 3829 else if (strcmp(input->events[i].key_name, "power") == 0 || 3830 input->events[i].key_code == KEY_POWER) 3831 power_pressed = 1; 3832 } 3833 } 3834 3835 // Pixel density scale change 3836 if (scale_change && display) { 3837 int new_scale = pixel_scale; 3838 if (scale_change == 99) { 3839 new_scale = 3; // reset to default 3840 } else if (scale_change == 1) { 3841 // Increase density (smaller pixels): 12→10→8→6→4→3→2→1 3842 if (pixel_scale > 6) new_scale = pixel_scale - 2; 3843 else if (pixel_scale > 3) new_scale = pixel_scale - 2; 3844 else if (pixel_scale > 1) new_scale = pixel_scale - 1; 3845 } else if (scale_change == -1) { 3846 // Decrease density (bigger pixels): 1→2→3→4→6→8→10→12 3847 if (pixel_scale < 3) new_scale = pixel_scale + 1; 3848 else if (pixel_scale < 6) new_scale = pixel_scale + 2; 3849 else new_scale = pixel_scale + 2; 3850 } 3851 if (new_scale != pixel_scale && new_scale >= 1 && new_scale <= 12) { 3852 pixel_scale = new_scale; 3853 // Recreate framebuffer at new resolution 3854 ACFramebuffer *new_screen = fb_create(display->width / pixel_scale, 3855 display->height / pixel_scale); 3856 if (new_screen) { 3857 fb_destroy(screen); 3858 screen = new_screen; 3859 graph_init(&graph, screen); 3860 if (display) graph_init_gpu(&graph, display); 3861 input->scale = pixel_scale; 3862 // Update JS runtime's graph reference (holds framebuffer) 3863 if (rt) { 3864 rt->graph = &graph; 3865 } 3866 // Recreate cursor overlay at new resolution 3867 if (cursor_fb) fb_destroy(cursor_fb); 3868 cursor_fb = fb_create(screen->width, screen->height); 3869 ac_log("[scale] pixel_scale=%d resolution=%dx%d", 3870 pixel_scale, screen->width, screen->height); 3871 audio_synth(audio, WAVE_SINE, 3872 440.0 + (6 - pixel_scale) * 150.0, 3873 0.06, 0.15, 0.002, 0.05, 0.0); 3874 } 3875 } 3876 } 3877 3878 if (power_pressed || poweroff_requested) { 3879 poweroff_requested = 0; // consume the flag 3880 // Say bye IMMEDIATELY (TTS + shutdown chime start before animation) 3881 char bye_title[80]; 3882 { 3883 const char *at = strchr(boot_title, '@'); 3884 if (at) 3885 snprintf(bye_title, sizeof(bye_title), "bye @%s", at + 1); 3886 else 3887 snprintf(bye_title, sizeof(bye_title), "bye"); 3888 char bye_speech[96]; 3889 if (at) 3890 snprintf(bye_speech, sizeof(bye_speech), "bye %s", at + 1); 3891 else 3892 snprintf(bye_speech, sizeof(bye_speech), "bye"); 3893 if (tts) tts_speak(tts, bye_speech); 3894 audio_shutdown_sound(audio); 3895 } 3896 3897 // Shutdown animation — chaotic red/white strobe with bye title 3898 struct timespec anim_time; 3899 clock_gettime(CLOCK_MONOTONIC, &anim_time); 3900 3901 for (int f = 0; f < 90; f++) { // 90 frames @ 60fps = 1.5s 3902 double t = (double)f / 90.0; 3903 3904 // Chaotic strobe: alternate red/white/black with randomish pattern 3905 int phase = (f * 7 + f / 3) % 6; // pseudo-random cycle 3906 uint8_t br, bg_g, bb; 3907 if (phase < 2) { br = 220; bg_g = 20; bb = 20; } // red 3908 else if (phase < 3) { br = 255; bg_g = 255; bb = 255; } // white 3909 else if (phase < 5) { br = 180; bg_g = 0; bb = 0; } // dark red 3910 else { br = 10; bg_g = 10; bb = 10; } // near black 3911 3912 // Fade intensity toward end 3913 double fade = 1.0 - t * t; 3914 br = (uint8_t)(br * fade); 3915 bg_g = (uint8_t)(bg_g * fade); 3916 bb = (uint8_t)(bb * fade); 3917 graph_wipe(&graph, (ACColor){br, bg_g, bb, 255}); 3918 3919 // Title text — jitter position, flicker between red and white 3920 if (t < 0.85) { 3921 int alpha = (int)(255.0 * (1.0 - t / 0.85)); 3922 int jx = (f * 13 % 7) - 3; // -3 to +3 pixel jitter 3923 int jy = (f * 17 % 5) - 2; // -2 to +2 3924 uint8_t tr = (f % 3 == 0) ? 255 : 200; 3925 uint8_t tg = (f % 3 == 0) ? 255 : 40; 3926 uint8_t tb = (f % 3 == 0) ? 255 : 40; 3927 graph_ink(&graph, (ACColor){tr, tg, tb, (uint8_t)alpha}); 3928 int tw = font_measure_matrix(bye_title, 3); 3929 font_draw_matrix(&graph, bye_title, 3930 (screen->width - tw) / 2 + jx, 3931 screen->height / 2 - 20 + jy, 3); 3932 graph_ink(&graph, (ACColor){(uint8_t)(120 * fade), 40, 40, (uint8_t)(alpha / 2)}); 3933 int sw = font_measure_matrix("aesthetic.computer", 1); 3934 font_draw_matrix(&graph, "aesthetic.computer", 3935 (screen->width - sw) / 2 + jx / 2, 3936 screen->height / 2 + 10 + jy / 2, 1); 3937 } 3938 3939 ac_display_present(display, screen, pixel_scale); 3940 frame_sync_60fps(&anim_time); 3941 } 3942 3943 // Final black frame + hide console text 3944 graph_wipe(&graph, (ACColor){0, 0, 0, 255}); 3945 ac_display_present(display, screen, pixel_scale); 3946 // Suppress kernel console output during shutdown 3947 { 3948 // Set kernel loglevel to 0 (suppress all printk) 3949 FILE *pl = fopen("/proc/sys/kernel/printk", "w"); 3950 if (pl) { fputs("0 0 0 0", pl); fclose(pl); } 3951 // Hide VT cursor 3952 FILE *vc = fopen("/dev/tty0", "w"); 3953 if (vc) { fputs("\033[?25l\033[2J", vc); fclose(vc); } 3954 } 3955 3956 running = 0; 3957 break; 3958 } 3959 3960 struct timespec _pf_start, _pf_act0, _pf_act1; 3961 clock_gettime(CLOCK_MONOTONIC, &_pf_start); 3962 clock_gettime(CLOCK_MONOTONIC, &_pf_act0); 3963 js_call_act(rt); 3964 clock_gettime(CLOCK_MONOTONIC, &_pf_act1); 3965 3966 // Global escape fallback: if the piece didn't consume the escape 3967 // key-down (i.e. didn't call system.jump() or otherwise stop the 3968 // event), send the user back to the prompt automatically. 3969 if (escape_pressed_this_frame && !rt->jump_requested) { 3970 ac_log("[ac-native] escape fallback: %s → prompt\n", rt->piece); 3971 strncpy(rt->jump_target, "prompt", sizeof(rt->jump_target) - 1); 3972 rt->jump_target[sizeof(rt->jump_target) - 1] = 0; 3973 rt->jump_requested = 1; 3974 } 3975 3976 // Handle piece jump requests from system.jump() 3977 if (rt->jump_requested) { 3978 rt->jump_requested = 0; 3979 3980 // Parse colon-separated params: "chat:clock" → name="chat", params=["clock"] 3981 rt->jump_param_count = 0; 3982 { 3983 char *colon = strchr(rt->jump_target, ':'); 3984 if (colon) { 3985 *colon = 0; // terminate piece name at first colon 3986 char *rest = colon + 1; 3987 while (rest && *rest && rt->jump_param_count < 8) { 3988 char *next = strchr(rest, ':'); 3989 if (next) *next = 0; 3990 strncpy(rt->jump_params[rt->jump_param_count], rest, 63); 3991 rt->jump_params[rt->jump_param_count][63] = 0; 3992 rt->jump_param_count++; 3993 rest = next ? next + 1 : NULL; 3994 } 3995 } 3996 } 3997 3998 ac_log("[ac-native] Jumping to piece: %s (%d params)\n", 3999 rt->jump_target, rt->jump_param_count); 4000 4001 // Call leave() on current piece 4002 js_call_leave(rt); 4003 4004 // Free old lifecycle functions 4005 JS_FreeValue(rt->ctx, rt->boot_fn); rt->boot_fn = JS_UNDEFINED; 4006 JS_FreeValue(rt->ctx, rt->paint_fn); rt->paint_fn = JS_UNDEFINED; 4007 JS_FreeValue(rt->ctx, rt->act_fn); rt->act_fn = JS_UNDEFINED; 4008 JS_FreeValue(rt->ctx, rt->sim_fn); rt->sim_fn = JS_UNDEFINED; 4009 JS_FreeValue(rt->ctx, rt->leave_fn); rt->leave_fn = JS_UNDEFINED; 4010 JS_FreeValue(rt->ctx, rt->beat_fn); rt->beat_fn = JS_UNDEFINED; 4011 4012 // Clear globalThis lifecycle refs so new piece starts clean 4013 JSValue global = JS_GetGlobalObject(rt->ctx); 4014 const char *lc_names[] = {"boot","paint","act","sim","leave","beat","configureAutopat",NULL}; 4015 for (int i = 0; lc_names[i]; i++) { 4016 JSAtom a = JS_NewAtom(rt->ctx, lc_names[i]); 4017 JS_DeleteProperty(rt->ctx, global, a, 0); 4018 JS_FreeAtom(rt->ctx, a); 4019 } 4020 JS_FreeValue(rt->ctx, global); 4021 4022 // Reset counters and crash state 4023 rt->paint_count = 0; 4024 rt->sim_count = 0; 4025 rt->crash_active = 0; 4026 rt->crash_count = 0; 4027 rt->crash_frame = 0; 4028 rt->crash_msg[0] = 0; 4029 4030 // NOTE: "claude" and "cc" aliases removed — claude.mjs handles auth curtain 4031 // before jumping to terminal:claude itself. 4032 4033 // Check for .lisp piece — hand off to SBCL 4034 { 4035 char lisp_path[256]; 4036 // Strip .lisp suffix if present 4037 char lisp_name[128]; 4038 strncpy(lisp_name, rt->jump_target, sizeof(lisp_name) - 1); 4039 lisp_name[sizeof(lisp_name) - 1] = 0; 4040 char *dot = strstr(lisp_name, ".lisp"); 4041 if (dot) *dot = 0; 4042 snprintf(lisp_path, sizeof(lisp_path), "/pieces/%s.lisp", lisp_name); 4043 if (access(lisp_path, F_OK) == 0 && access("/ac-swank", X_OK) == 0) { 4044 ac_log("[ac-native] Launching CL piece: %s\n", lisp_name); 4045 // Clean up before exec 4046 if (logfile) { fflush(logfile); fsync(fileno(logfile)); } 4047 sync(); 4048 execl("/ac-swank", "ac-swank", "--piece", lisp_name, NULL); 4049 // execl failed — fall through to JS path 4050 ac_log("[ac-native] execl /ac-swank failed: %s\n", strerror(errno)); 4051 } 4052 } 4053 4054 // Construct piece path: /pieces/<name>.mjs 4055 char jump_path[256]; 4056 snprintf(jump_path, sizeof(jump_path), "/pieces/%s.mjs", rt->jump_target); 4057 4058 // Load new piece 4059 if (js_load_piece(rt, jump_path) < 0) { 4060 ac_log("[ac-native] Failed to load %s, falling back to /piece.mjs\n", jump_path); 4061 // Fall back to default piece 4062 if (js_load_piece(rt, "/piece.mjs") < 0) { 4063 ac_log("[ac-native] FATAL: Cannot reload default piece\n"); 4064 running = 0; 4065 break; 4066 } 4067 } 4068 4069 // Flush log on piece transition so USB pull captures it 4070 if (logfile) { fflush(logfile); fsync(fileno(logfile)); } 4071 4072 // Clear screen and call boot() on new piece 4073 graph_wipe(&graph, (ACColor){0, 0, 0, 255}); 4074 js_call_boot(rt); 4075 } 4076 4077 struct timespec _pf_sim0, _pf_sim1, _pf_paint0, _pf_paint1, _pf_pres0, _pf_pres1; 4078 if (audio_beat_check(audio)) js_call_beat(rt); 4079 clock_gettime(CLOCK_MONOTONIC, &_pf_sim0); 4080 js_call_sim(rt); 4081 clock_gettime(CLOCK_MONOTONIC, &_pf_sim1); 4082 clock_gettime(CLOCK_MONOTONIC, &_pf_paint0); 4083 js_call_paint(rt); 4084 4085 clock_gettime(CLOCK_MONOTONIC, &_pf_paint1); 4086 4087 // Tape recording overlay — red REC dot + elapsed timer in 4088 // the top-left corner. Blinks slowly so it reads as "live" 4089 // without being visually distracting. 4090 if (g_tape_recording) { 4091 long elapsed = (long)(time(NULL) - g_tape_start_sec); 4092 int blink = ((main_frame / 30) & 1); // toggle ~every 0.5s 4093 // Red dot 4094 if (blink) { 4095 graph_ink(&graph, (ACColor){230, 40, 40, 240}); 4096 graph_box(&graph, 6, 6, 8, 8, 1); 4097 } 4098 // "TAPE 0:23" label 4099 char rec_label[32]; 4100 snprintf(rec_label, sizeof(rec_label), "TAPE %ld:%02ld", 4101 elapsed / 60, elapsed % 60); 4102 graph_ink(&graph, (ACColor){0, 0, 0, 180}); 4103 graph_box(&graph, 16, 4, (int)strlen(rec_label) * 6 + 4, 12, 1); 4104 graph_ink(&graph, (ACColor){255, 220, 220, 255}); 4105 font_draw_matrix(&graph, rec_label, 18, 6, 1); 4106 } 4107 4108 // Crash overlay — red bar with error message when JS throws 4109 if (rt->crash_active) { 4110 rt->crash_frame++; 4111 int bar_h = 24; 4112 int flash = (rt->crash_frame < 30) ? (rt->crash_frame % 6 < 3 ? 255 : 180) : 200; 4113 graph_ink(&graph, (ACColor){flash, 0, 0, 240}); 4114 graph_box(&graph, 0, 0, screen->width, bar_h, 1); 4115 graph_ink(&graph, (ACColor){255, 255, 255, 255}); 4116 font_draw_matrix(&graph, "CRASH", 4, 4, 2); 4117 // Truncate message to fit screen 4118 char crash_display[128]; 4119 snprintf(crash_display, sizeof(crash_display), "%s", rt->crash_msg); 4120 graph_ink(&graph, (ACColor){255, 200, 200, 255}); 4121 font_draw_matrix(&graph, crash_display, 60, 8, 1); 4122 // Auto-dismiss after 5 seconds (300 frames at 60fps) 4123 if (rt->crash_frame > 300) { 4124 rt->crash_active = 0; 4125 } 4126 } 4127 4128 // Software cursor on its own overlay buffer (unaffected by KidLisp effects) 4129 if (cursor_fb && input && (input->pointer_x || input->pointer_y)) { 4130 fb_clear(cursor_fb, 0x00000000); // transparent 4131 graph_page(&graph, cursor_fb); 4132 int cx = input->pointer_x / pixel_scale, cy = input->pointer_y / pixel_scale; 4133 // Shadow (black, offset +1,+1) 4134 graph_ink(&graph, (ACColor){0, 0, 0, 180}); 4135 graph_line(&graph, cx+1, cy-9, cx+1, cy-4); 4136 graph_line(&graph, cx+1, cy+6, cx+1, cy+11); 4137 graph_line(&graph, cx-9, cy+1, cx-4, cy+1); 4138 graph_line(&graph, cx+6, cy+1, cx+11, cy+1); 4139 graph_plot(&graph, cx+1, cy+1); 4140 // Crosshair color reflects WiFi state 4141 { 4142 ACColor cross_color = {128, 128, 128, 255}; // gray = no wifi 4143 if (wifi) { 4144 switch (wifi->state) { 4145 case WIFI_STATE_CONNECTED: 4146 cross_color = (ACColor){0, 255, 255, 255}; // cyan 4147 break; 4148 case WIFI_STATE_SCANNING: 4149 case WIFI_STATE_CONNECTING: 4150 cross_color = (ACColor){255, 255, 0, 255}; // yellow 4151 break; 4152 case WIFI_STATE_FAILED: 4153 cross_color = (ACColor){255, 60, 60, 255}; // red 4154 break; 4155 default: 4156 cross_color = (ACColor){128, 128, 128, 255}; // gray 4157 break; 4158 } 4159 } 4160 graph_ink(&graph, cross_color); 4161 } 4162 graph_line(&graph, cx, cy-10, cx, cy-5); 4163 graph_line(&graph, cx, cy+5, cx, cy+10); 4164 graph_line(&graph, cx-10, cy, cx-5, cy); 4165 graph_line(&graph, cx+5, cy, cx+10, cy); 4166 // White center dot 4167 graph_ink(&graph, (ACColor){255, 255, 255, 255}); 4168 graph_plot(&graph, cx, cy); 4169 // Composite cursor region onto screen (only the area around cursor) 4170 graph_page(&graph, screen); 4171 int bx = cx - 12, by = cy - 12, bw = 25, bh = 25; 4172 if (bx < 0) bx = 0; 4173 if (by < 0) by = 0; 4174 if (bx + bw > cursor_fb->width) bw = cursor_fb->width - bx; 4175 if (by + bh > cursor_fb->height) bh = cursor_fb->height - by; 4176 for (int py = by; py < by + bh; py++) { 4177 for (int px = bx; px < bx + bw; px++) { 4178 uint32_t pixel = cursor_fb->pixels[py * cursor_fb->stride + px]; 4179 if (pixel >> 24) // only blit non-transparent 4180 fb_blend_pixel(screen, px, py, pixel); 4181 } 4182 } 4183 } 4184 4185 // WiFi "online" TTS announcement 4186 { 4187 static int was_connected = 0; 4188 int is_connected = (wifi && wifi->state == WIFI_STATE_CONNECTED); 4189 if (is_connected && !was_connected) { 4190 if (tts) { 4191 char wifi_msg[128]; 4192 if (wifi->connected_ssid[0]) 4193 snprintf(wifi_msg, sizeof(wifi_msg), "connected to %s", wifi->connected_ssid); 4194 else 4195 snprintf(wifi_msg, sizeof(wifi_msg), "online"); 4196 tts_speak(tts, wifi_msg); 4197 } 4198 ac_log("[wifi-tts] connected to %s", wifi->connected_ssid[0] ? wifi->connected_ssid : "(unknown)"); 4199 4200 // Fetch device tokens (Claude + GitHub) from API (authenticated) 4201 { 4202 FILE *cf = fopen("/mnt/config.json", "r"); 4203 if (cf) { 4204 char cbuf[4096] = {0}; 4205 fread(cbuf, 1, sizeof(cbuf) - 1, cf); 4206 fclose(cf); 4207 char handle[64] = {0}, actoken[1024] = {0}; 4208 parse_config_string(cbuf, "\"handle\"", handle, sizeof(handle)); 4209 parse_config_string(cbuf, "\"token\"", actoken, sizeof(actoken)); 4210 if (handle[0] && actoken[0]) { 4211 char cmd[2048]; 4212 snprintf(cmd, sizeof(cmd), 4213 "curl -fsSL -H 'Authorization: Bearer %s' " 4214 "'https://aesthetic.computer/.netlify/functions/claude-token' " 4215 "-o /tmp/claude-api-resp.json 2>/dev/null &", actoken); 4216 system(cmd); 4217 ac_log("[tokens] fetching for @%s\n", handle); 4218 } 4219 } 4220 } 4221 // Clone (or pull) the aesthetic-computer repo to 4222 // /mnt/ac-repo so Claude Code has a real project to 4223 // work in. Fetch stays on the GitHub HTTPS mirror for 4224 // simple read access; if a Tangled SSH key was baked, 4225 // the repo also gets origin pushurls for knot + GitHub 4226 // and a dedicated `tangled` remote. Runs in the 4227 // background so boot isn't delayed. We use a shallow 4228 // clone (--depth=50) to keep the size manageable 4229 // (~200 MB) while still giving Claude enough history 4230 // for basic blame/log work. 4231 // 4232 // Idempotent: if /mnt/ac-repo/.git already exists, 4233 // `git pull` runs instead (bounded by 30s timeout). 4234 // Logs go to /tmp/ac-repo-clone.log. 4235 { 4236 char clone_cmd[3072]; 4237 snprintf(clone_cmd, sizeof(clone_cmd), 4238 "( REPO=/mnt/ac-repo; " 4239 " FETCH_URL='https://github.com/whistlegraph/aesthetic-computer.git'; " 4240 " TANGLED_URL='git@knot.aesthetic.computer:aesthetic.computer/core'; " 4241 " if [ -d \"$REPO/.git\" ]; then " 4242 " echo '[ac-repo] pulling latest' && " 4243 " cd \"$REPO\" && timeout 30 git pull --ff-only 2>&1; " 4244 " else " 4245 " echo '[ac-repo] cloning (shallow)' && " 4246 " timeout 300 git clone --depth=50 --branch=main \"$FETCH_URL\" \"$REPO\" 2>&1; " 4247 " fi; " 4248 " if [ -d \"$REPO/.git\" ]; then " 4249 " cd \"$REPO\"; " 4250 " git remote set-url origin \"$FETCH_URL\"; " 4251 " git config --unset-all remote.origin.pushurl 2>/dev/null || true; " 4252 " if [ -f /tmp/.ssh/tangled ]; then " 4253 " git config --add remote.origin.pushurl \"$TANGLED_URL\"; " 4254 " git config --add remote.origin.pushurl \"$FETCH_URL\"; " 4255 " if git remote | grep -qx tangled; then " 4256 " git remote set-url tangled \"$TANGLED_URL\"; " 4257 " else " 4258 " git remote add tangled \"$TANGLED_URL\"; " 4259 " fi; " 4260 " echo '[ac-repo] origin pushurl: tangled + github'; " 4261 " else " 4262 " git config --add remote.origin.pushurl \"$FETCH_URL\"; " 4263 " git remote remove tangled 2>/dev/null || true; " 4264 " echo '[ac-repo] origin pushurl: github only (no tangled key)'; " 4265 " fi; " 4266 " fi " 4267 ") >> /tmp/ac-repo-clone.log 2>&1 &"); 4268 system(clone_cmd); 4269 ac_log("[ac-repo] background clone/pull started\n"); 4270 } 4271 ac_log_flush(); 4272 } 4273 was_connected = is_connected; 4274 } 4275 4276 // Machines monitoring daemon (connects, heartbeats, handles commands) 4277 { 4278 static int fps_counter = 0, fps_display = 0; 4279 static struct timespec fps_last = {0}; 4280 fps_counter++; 4281 struct timespec fps_now; 4282 clock_gettime(CLOCK_MONOTONIC, &fps_now); 4283 if (fps_now.tv_sec > fps_last.tv_sec) { 4284 fps_display = fps_counter; 4285 fps_counter = 0; 4286 fps_last = fps_now; 4287 } 4288 machines_tick(&g_machines, wifi, main_frame, fps_display, 4289 rt->jump_target[0] ? rt->jump_target : rt->piece); 4290 4291 // Handle commands forwarded from machines daemon 4292 if (g_machines.cmd_pending) { 4293 g_machines.cmd_pending = 0; 4294 if (strcmp(g_machines.cmd_type, "jump") == 0 && g_machines.cmd_target[0]) { 4295 strncpy(rt->jump_target, g_machines.cmd_target, sizeof(rt->jump_target) - 1); 4296 rt->jump_requested = 1; 4297 ac_log("[machines] jump → %s\n", g_machines.cmd_target); 4298 } else if (strcmp(g_machines.cmd_type, "update") == 0) { 4299 // Jump to notepat which handles OTA updates 4300 strncpy(rt->jump_target, "notepat", sizeof(rt->jump_target) - 1); 4301 rt->jump_requested = 1; 4302 ac_log("[machines] update requested, jumping to notepat\n"); 4303 } 4304 } 4305 } 4306 4307 // Submit frame to recorder (after cursor overlay, before display) 4308 if (recorder_is_recording(recorder)) 4309 recorder_submit_video(recorder, screen->pixels, screen->stride); 4310 4311 // Draw recording indicator (red dot + duration) 4312 if (recorder_is_recording(recorder)) { 4313 graph_page(&graph, screen); 4314 graph_ink(&graph, (ACColor){255, 40, 40, 255}); 4315 // Red dot at top-right (4px circle) 4316 int rx = screen->width - 8, ry = 4; 4317 graph_circle(&graph, rx, ry, 3, 1); 4318 } 4319 4320 clock_gettime(CLOCK_MONOTONIC, &_pf_pres0); 4321 ac_display_present(display, screen, pixel_scale); 4322 clock_gettime(CLOCK_MONOTONIC, &_pf_pres1); 4323 4324 // HDMI: render waveform at ~7.5Hz (every 8 frames) — 4K dumb-buf is slow 4325 if (hdmi && audio && main_frame % 8 == 0) { 4326 drm_secondary_present_waveform(hdmi, &graph, 4327 audio->waveform_left, AUDIO_WAVEFORM_SIZE, audio->waveform_pos); 4328 } 4329 4330 // HDMI hotplug detection every ~180 frames (~3s) 4331 if (main_frame % 180 == 0 && display && !display->is_fbdev) { 4332 int hdmi_connected = drm_secondary_is_connected(display); 4333 if (hdmi_connected && !hdmi) { 4334 hdmi = drm_init_secondary(display); 4335 rt->hdmi = hdmi; 4336 if (hdmi) { 4337 ac_log("[ac-native] HDMI plugged in: %dx%d\n", hdmi->width, hdmi->height); 4338 // HDMI on: rising chord 4339 if (audio) { audio_synth(audio, WAVE_SINE, 523.25, 0.15, 0.2, 0.005, 0.12, 0.0); 4340 audio_synth(audio, WAVE_SINE, 783.99, 0.15, 0.15, 0.02, 0.11, 0.0); } 4341 } 4342 } else if (!hdmi_connected && hdmi) { 4343 ac_log("[ac-native] HDMI unplugged\n"); 4344 // HDMI off: descending two-tone 4345 if (audio) { audio_synth(audio, WAVE_SINE, 523.25, 0.12, 0.18, 0.005, 0.10, 0.0); 4346 audio_synth(audio, WAVE_SINE, 392.00, 0.12, 0.14, 0.02, 0.09, 0.0); } 4347 drm_secondary_destroy(hdmi); 4348 hdmi = NULL; 4349 rt->hdmi = NULL; 4350 } 4351 } 4352 4353 // Sync the USB log in batches instead of on every event. 4354 if (logfile && log_dirty && main_frame % 300 == 0) { 4355 ac_log_flush(); 4356 } 4357 4358 // ── Record frame perf ── 4359 { 4360 struct timespec _pf_end; 4361 clock_gettime(CLOCK_MONOTONIC, &_pf_end); 4362 #define TS_US(a, b) (uint16_t)({ \ 4363 long _d = ((b).tv_sec - (a).tv_sec) * 1000000L + \ 4364 ((b).tv_nsec - (a).tv_nsec) / 1000L; \ 4365 _d < 0 ? 0 : (_d > 65535 ? 65535 : _d); }) 4366 int _voices = 0; 4367 if (audio) { 4368 for (int _v = 0; _v < AUDIO_MAX_VOICES; _v++) 4369 if (audio->voices[_v].state != VOICE_INACTIVE) _voices++; 4370 } 4371 PerfRecord pr = { 4372 .frame = (uint32_t)main_frame, 4373 .total_us = TS_US(_pf_start, _pf_end), 4374 .act_us = TS_US(_pf_act0, _pf_act1), 4375 .sim_us = TS_US(_pf_sim0, _pf_sim1), 4376 .paint_us = TS_US(_pf_paint0, _pf_paint1), 4377 .present_us = TS_US(_pf_pres0, _pf_pres1), 4378 .voices = (uint8_t)(_voices > 255 ? 255 : _voices), 4379 .events = (uint8_t)(input->event_count > 255 ? 255 : input->event_count), 4380 .js_heap_mb = 0, 4381 .flags = (uint8_t)((input->pointer_x || input->pointer_y ? 2 : 0)), 4382 }; 4383 perf_record(&pr); 4384 4385 // Log perf summary every 5 seconds (300 frames @ 60fps) 4386 { 4387 static uint32_t perf_log_max = 0, perf_log_sum = 0; 4388 static uint16_t perf_log_paint_max = 0, perf_log_pres_max = 0; 4389 static int perf_log_slow = 0; // frames > 20ms 4390 if (pr.total_us > perf_log_max) perf_log_max = pr.total_us; 4391 if (pr.paint_us > perf_log_paint_max) perf_log_paint_max = pr.paint_us; 4392 if (pr.present_us > perf_log_pres_max) perf_log_pres_max = pr.present_us; 4393 perf_log_sum += pr.total_us; 4394 if (pr.total_us > 20000) perf_log_slow++; 4395 if (main_frame % 300 == 299) { 4396 ac_log("[perf] f=%d avg=%.1fms max=%.1fms paint_max=%.1fms pres_max=%.1fms slow=%d voices=%d\n", 4397 (int)main_frame, 4398 perf_log_sum / 300.0f / 1000.0f, 4399 perf_log_max / 1000.0f, 4400 perf_log_paint_max / 1000.0f, 4401 perf_log_pres_max / 1000.0f, 4402 perf_log_slow, pr.voices); 4403 perf_log_max = 0; perf_log_sum = 0; 4404 perf_log_paint_max = 0; perf_log_pres_max = 0; 4405 perf_log_slow = 0; 4406 } 4407 } 4408 4409 // Write frame marker to ftrace ring buffer for BPF correlation. 4410 // Only active when /tmp/.trace-active exists (zero cost otherwise). 4411 { 4412 static int trace_marker_fd = -2; // -2 = unchecked 4413 if (trace_marker_fd == -2) { 4414 struct stat _tst; 4415 if (stat("/tmp/.trace-active", &_tst) == 0) 4416 trace_marker_fd = open("/sys/kernel/tracing/trace_marker", 4417 O_WRONLY | O_CLOEXEC); 4418 else 4419 trace_marker_fd = -1; 4420 } 4421 if (trace_marker_fd >= 0) { 4422 char _tbuf[64]; 4423 int _tlen = snprintf(_tbuf, sizeof(_tbuf), 4424 "frame:%u total:%u\n", 4425 (uint32_t)main_frame, pr.total_us); 4426 (void)write(trace_marker_fd, _tbuf, _tlen); 4427 } 4428 } 4429 4430 // Flush chunk to disk every 30 seconds (fsync'd, crash-safe) 4431 if (main_frame - perf_flush_frame >= PERF_CHUNK_FRAMES) { 4432 perf_flush(); 4433 perf_flush_frame = main_frame; 4434 } 4435 #undef TS_US 4436 } 4437 4438 // DRM page flip already syncs to vblank (~16ms). Only use 4439 // frame_sync for fbdev/non-vsync paths to avoid double-wait. 4440 if (display && display->is_fbdev) 4441 frame_sync_60fps(&frame_time); 4442 } 4443 } 4444 4445 // Stop recording if active 4446 if (recorder) { 4447 if (audio) { audio->rec_callback = NULL; audio->rec_userdata = NULL; } 4448 recorder_destroy(recorder); 4449 recorder = NULL; 4450 } 4451 4452 // Flush final perf data 4453 perf_destroy(); 4454 4455 // Cleanup (TTS bye + shutdown chime already fired at power-press time) 4456 ac_log("[ac-native] Shutting down\n"); 4457 // Upload the complete boot-to-shutdown log before tearing down 4458 machines_flush_logs(&g_machines); 4459 machines_destroy(&g_machines); 4460 4461 if (logfile) { fclose(logfile); logfile = NULL; } 4462 sync(); 4463 // Unmount USB log 4464 umount("/mnt"); 4465 4466 js_call_leave(rt); 4467 js_destroy(rt); 4468 wifi_destroy(wifi); 4469 if (tts) { 4470 tts_wait(tts); 4471 usleep(300000); // Let TTS ring buffer drain 4472 } 4473 usleep(600000); // Let shutdown chime ring out 4474 tts_destroy(tts); 4475 audio_destroy(audio); 4476 if (input) input_destroy(input); 4477 if (hdmi) drm_secondary_destroy(hdmi); 4478 fb_destroy(screen); 4479 if (display) drm_destroy(display); 4480 4481 ac_poweroff(); 4482 4483 return 0; 4484}