#!/bin/sh # AC Native OS init — DRM direct boot with crash recovery export PATH="/bin:/sbin:/usr/bin:/usr/sbin" # Capture kernel-handoff timestamp as early as possible so the boot # timing log has a meaningful zero. /proc/uptime is the first reliable # wall-clock source (shell `date` is usually epoch-zero at this point). # Falls back to 0 if /proc isn't mounted yet — which we fix in the next # block anyway. BOOT_INIT_START_SEC=$(awk '{print $1}' /proc/uptime 2>/dev/null || echo 0) mount -t proc proc /proc 2>/dev/null mount -t sysfs sysfs /sys 2>/dev/null mount -t devtmpfs devtmpfs /dev 2>/dev/null mkdir -p /dev/pts /dev/shm /tmp /run /etc mount -t devpts devpts /dev/pts -o ptmxmode=0666 2>/dev/null mount -t tmpfs tmpfs /dev/shm 2>/dev/null mount -t tmpfs tmpfs /tmp 2>/dev/null mount -t tmpfs tmpfs /run 2>/dev/null mount -t efivarfs efivarfs /sys/firmware/efi/efivars 2>/dev/null # Boot-phase timing helper. Appends "phase: +X.XXs @ T.TTs" to # /run/boot-timing so we can see where seconds are spent between # kernel handoff and ac-native launch. The full file is copied to # /mnt/boot-timing.log near the top of the pre-launch diagnostic # dump (when the USB config partition is available). BOOT_TIMING_LOG="/run/boot-timing" : > "$BOOT_TIMING_LOG" _boot_mark() { local now=$(awk '{print $1}' /proc/uptime 2>/dev/null || echo 0) local delta=$(awk "BEGIN{printf \"%.3f\", $now - $BOOT_INIT_START_SEC}") printf '%-32s Δ%6ss @%7ss\n' "$1" "$delta" "$now" >> "$BOOT_TIMING_LOG" BOOT_INIT_START_SEC=$now } _boot_mark "kernel-handoff" # debugfs — needed for ASoC DAPM state inspection in audio-diag. # CONFIG_DEBUG_FS must be y in the kernel; mount early so everything # downstream (audio-diag, gpio dump, etc) can read it. mkdir -p /sys/kernel/debug 2>/dev/null mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null # zram swap — skipped by default. notepat fits in ~200 MB, every ThinkPad # we target has ≥4 GB RAM, and the zram modprobe + mkswap chain adds # ~100-200 ms to boot for no observable benefit. Set AC_ZRAM=1 on the # kernel cmdline if you ever need it back (e.g. low-memory tablet boot). case " $(cat /proc/cmdline 2>/dev/null) " in *" AC_ZRAM=1 "*) modprobe zram 2>/dev/null || true if [ -e /sys/block/zram0/disksize ] && [ -b /dev/zram0 ]; then echo 1G > /sys/block/zram0/disksize && mkswap /dev/zram0 >/dev/null 2>&1 && swapon /dev/zram0 2>/dev/null fi ;; esac # Loopback ip link set lo up 2>/dev/null # Restore baked Claude credentials (tmpfs mount hid the originals). # Two bake paths produce creds at /: # /claude-creds.json legacy full JSON (linux ac-os, reads ~/.claude/.credentials.json) # /claude-token plain OAuth bearer (macOS flash-mac.sh, MongoDB year-long token) # Either presence triggers setup of /tmp/.claude/ so Claude Code boots without # onboarding/login prompts. pty.c separately reads /claude-token to set # CLAUDE_CODE_OAUTH_TOKEN in the child's env. if [ -f /claude-creds.json ] || [ -f /claude-token ]; then mkdir -p /tmp/.claude [ -f /claude-creds.json ] && cp /claude-creds.json /tmp/.claude/.credentials.json cp /claude-state.json /tmp/.claude.json 2>/dev/null # Pin Claude's default model to the "sonnet" alias — Anthropic's # CLI resolves "sonnet" to the latest Sonnet family member, which # is currently Sonnet 4.7. Users who want a specific pinned version # can override by writing a different model string into this file. # `theme: auto` matches the terminal's light/dark mode AND skips the # first-run theme picker, so Claude Code launches straight to a session. printf '{"permissions":{"allow":["Bash(*)","Read(*)","Write(*)","Edit(*)","Glob(*)","Grep(*)","WebFetch(*)","WebSearch(*)"]},"autoUpdates":false,"installMethod":"native","model":"sonnet","theme":"auto"}\n' > /tmp/.claude/settings.json fi if [ -f /tangled-key ] || [ -f /tangled-ssh-config ] || [ -f /tangled-known-hosts ]; then mkdir -p /tmp/.ssh [ -f /tangled-key ] && cp /tangled-key /tmp/.ssh/tangled 2>/dev/null && chmod 600 /tmp/.ssh/tangled 2>/dev/null [ -f /tangled-ssh-config ] && cp /tangled-ssh-config /tmp/.ssh/config 2>/dev/null && chmod 600 /tmp/.ssh/config 2>/dev/null [ -f /tangled-known-hosts ] && cp /tangled-known-hosts /tmp/.ssh/known_hosts 2>/dev/null && chmod 600 /tmp/.ssh/known_hosts 2>/dev/null fi mkdir -p /tmp/ac [ -f /device-claude.md ] && cp /device-claude.md /tmp/ac/CLAUDE.md 2>/dev/null [ -f /device-score.md ] && cp /device-score.md /tmp/ac/SCORE.md 2>/dev/null # /etc/group and /etc/passwd needed by seatd. Use a full 7-field passwd # entry: dropbear resolves root's home (/root, where authorized_keys lives) # and shell from here — a truncated entry left pw_dir empty and broke # key-based ssh. Interactive shells still use HOME=/tmp (set below + pty.c). echo "root:x:0:" > /etc/group echo "root:x:0:0:root:/root:/bin/sh" > /etc/passwd _boot_mark "tmpfs-mounts + creds" # Kick off USB mount in the background — independent of GPU probe, so # we overlap the two waits instead of running them serially. The main # thread then waits for GPU (below) and finally waits for this mount # result before continuing. Shaves up to ~2s on cold boot. modprobe vfat 2>/dev/null modprobe nls_cp437 2>/dev/null modprobe nls_ascii 2>/dev/null USB_MOUNTED=0 USB_PARTS="/dev/sda1 /dev/sda2 /dev/sda3 /dev/sdb1 /dev/sdb2 /dev/sdb3 /dev/sdc1 /dev/sdc2 /dev/sdc3 /dev/sdd1 /dev/sdd2 /dev/sdd3 /dev/nvme0n1p1 /dev/nvme0n1p2 /dev/nvme0n1p3" mount_usb_partition() { pass="$1" for p in $USB_PARTS; do if [ -b "$p" ]; then mkdir -p /mnt mount -t vfat "$p" /mnt 2>/dev/null || continue if [ "$pass" = "config" ] && [ -f /mnt/config.json ]; then return 0 fi if [ "$pass" = "boot" ] && { [ -f /mnt/EFI/BOOT/BOOTX64.EFI ] || [ -f /mnt/EFI/BOOT/KERNEL.EFI ]; }; then return 0 fi umount /mnt 2>/dev/null fi done return 1 } # Background USB mount: try up to 10 times with 1s delay, write result # to /run/usb-mounted so the foreground thread can check after GPU wait. ( for attempt in 1 2 3 4 5 6 7 8 9 10; do mount_usb_partition config && { echo 1 > /run/usb-mounted; exit 0; } mount_usb_partition boot && { echo 1 > /run/usb-mounted; exit 0; } sleep 1 done echo 0 > /run/usb-mounted ) & USB_MOUNT_PID=$! # Wait for GPU (up to 3 seconds) — runs in parallel with USB mount above. i=0 while [ ! -e /dev/dri/card0 ] && [ ! -e /dev/dri/card1 ] && [ ! -e /dev/fb0 ] && [ $i -lt 300 ]; do usleep 10000 2>/dev/null || sleep 1 i=$((i+1)) done # Converge: wait for the background USB mount to settle, pick up its result. wait $USB_MOUNT_PID 2>/dev/null [ -f /run/usb-mounted ] && USB_MOUNTED=$(cat /run/usb-mounted 2>/dev/null) && [ -z "$USB_MOUNTED" ] && USB_MOUNTED=0 _boot_mark "gpu-wait + usb-mount converge" # Performance governor (silently skip if cpufreq not available) if [ -d /sys/devices/system/cpu/cpu0/cpufreq ]; then for g in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo performance > "$g" 2>/dev/null done fi # Disable PCI runtime-PM on every audio controller so the SOF DSP + # SSP DAI + MAX98360A speaker amp don't auto-suspend after ~20s of # silence. Symptom was sdmode=1 at boot, sdmode=0 ~23s later, and # then the amp never came back on when notepat tried to play notes. # The kernel cmdline already has snd_hda_intel.power_save=0, but SOF # uses its own runtime PM policy; this is the portable backstop. for p in /sys/class/sound/card*/device/power/control \ /sys/bus/pci/drivers/sof-audio-pci-intel-icl/*/power/control \ /sys/bus/pci/drivers/snd_hda_intel/*/power/control; do [ -w "$p" ] && echo on > "$p" 2>/dev/null done export SSL_CERT_FILE="/etc/pki/tls/certs/ca-bundle.crt" export CURL_CA_BUNDLE="/etc/pki/tls/certs/ca-bundle.crt" export SSL_CERT_DIR="/etc/ssl/certs" export HOME="/tmp" # Claude Code CLI (/bin/claude) reads SHELL to know which interpreter to # spawn for its Bash tool. Without this it errors with "SHELL env var # isn't set". /bin/sh is busybox — good enough to run short commands. # TERM gives rich output; COLORTERM hints truecolor so the CLI uses colors. export SHELL="/bin/sh" export TERM="${TERM:-xterm-256color}" export COLORTERM="truecolor" export EDITOR="/bin/vi" # USB mount already settled above (kicked off in parallel with GPU wait). # USB_MOUNTED is populated from /run/usb-mounted at that wait() point. # Create samples directory on boot media + mount point for music USB if [ "$USB_MOUNTED" = "1" ]; then mkdir -p /mnt/samples fi mkdir -p /media if [ -x /scripts/usb-midi-gadget.sh ]; then if [ "$USB_MOUNTED" = "1" ] && [ -f /mnt/config.json ] && grep -Eq '"usbMidi"[[:space:]]*:[[:space:]]*true' /mnt/config.json 2>/dev/null; then /scripts/usb-midi-gadget.sh up >/tmp/usb-midi-gadget.log 2>&1 || true else /scripts/usb-midi-gadget.sh down >/tmp/usb-midi-gadget.log 2>&1 || true fi fi # Run ac-native in a loop — if it crashes, restart; if clean exit, shutdown export LD_LIBRARY_PATH="/lib64:/usr/lib64:${LD_LIBRARY_PATH:-}" # Pre-launch diagnostics — useful for audio probe / GPU / PCI debugging, # but the full dump writes ~60 KB to FAT32 (slow USB), does dozens of # shell forks over sysfs, and on older ThinkPads adds 1–2 s of wall time # to boot. The whole block now runs in a backgrounded subshell so # ac-native launches immediately; the dump completes concurrently while # the splash fade is drawing. Downstream tools (os-install-report, # post-mortem analysis) still get the same pre-launch.log at the same # path — just a second later than before. # # Set AC_NOLAUNCH_DIAG=1 on the kernel cmdline to skip the dump entirely # (useful for locked-down production kiosks that don't want the sysfs # scraping cost at all). _boot_mark "power-governor + sound PM" # Debug console writes to /dev/tty0 have been removed — they flashed # raw "[init] USB_MOUNTED=1" style text over the splash for ~1s on # every boot. Everything useful is still captured in the pre-launch # log + the new /mnt/boot-timing.log, visible post-hoc on the USB. if [ "$USB_MOUNTED" = "1" ]; then LOG=/mnt/pre-launch.log else LOG=/tmp/pre-launch.log fi _pre_launch_diag() { echo "=== PRE-LAUNCH ===" > $LOG ls /dev/dri/ >> $LOG 2>&1 echo "binary: $(ls -la /ac-native 2>&1)" >> $LOG echo "build: $(cat /etc/ac-build 2>&1)" >> $LOG echo "usb_mounted: $USB_MOUNTED" >> $LOG echo "gpu:" >> $LOG ls -la /dev/dri/ >> $LOG 2>&1 ls -la /dev/fb* >> $LOG 2>&1 echo "block devs:" >> $LOG ls /dev/sd* /dev/nvme* /dev/mmcblk* 2>/dev/null >> $LOG echo "net ifaces:" >> $LOG ls /sys/class/net/ 2>/dev/null >> $LOG echo "rfkill:" >> $LOG for rf in /sys/class/rfkill/rfkill*; do [ -d "$rf" ] || continue echo " $(basename $rf): type=$(cat $rf/type 2>/dev/null) soft=$(cat $rf/soft 2>/dev/null) hard=$(cat $rf/hard 2>/dev/null)" >> $LOG done echo "sound cards:" >> $LOG cat /proc/asound/cards 2>/dev/null >> $LOG echo "pci devices (storage/audio/net):" >> $LOG if [ -d /sys/bus/pci/devices ]; then for d in /sys/bus/pci/devices/*; do [ -d "$d" ] || continue class=$(cat $d/class 2>/dev/null) # 0x0c* = serial bus (eMMC), 0x0104* = RAID, 0x0106* = SATA, 0x0108* = NVMe # 0x0280* = network wireless, 0x0403* = audio case "$class" in 0x010*|0x028*|0x040*|0x080501|0x0805*) ven=$(cat $d/vendor 2>/dev/null) dev=$(cat $d/device 2>/dev/null) drv=$(basename $(readlink $d/driver 2>/dev/null) 2>/dev/null) echo " $(basename $d) class=$class ven=$ven dev=$dev drv=${drv:-NONE}" >> $LOG ;; esac done fi echo "=== DMESG (kernel ring) ===" >> $LOG # Kick off two /dev/kmsg readers in the background — do NOT block boot on # them. The foreground script used to sleep 15s here waiting for late SOF # machine-driver probes, which put a hard 15-second floor under every boot. # Instead: # - One reader appends to pre-launch.log for ~15s, then exits on its own. # - A persistent reader writes to /mnt/kmsg.log for the entire session so # runtime issues (audio probe failures, driver unbinds, OOMs) are still # captured. /init's wait-on-child keeps both alive long enough. if [ "$USB_MOUNTED" = "1" ]; then ( cat /dev/kmsg >> /mnt/kmsg.log 2>&1 ) & ( (cat /dev/kmsg & CAT_PID=$!; sleep 15; kill $CAT_PID 2>/dev/null) >> $LOG 2>&1 ) & fi # Snapshot devices + sound AFTER the ring-buffer dump so we see the state # that existed when drivers had their chance to probe. echo "=== POST-PROBE SNAPSHOT ===" >> $LOG echo "net ifaces:" >> $LOG ls /sys/class/net/ 2>/dev/null >> $LOG echo "sound cards:" >> $LOG cat /proc/asound/cards 2>/dev/null >> $LOG echo "pci drivers:" >> $LOG for d in /sys/bus/pci/devices/*; do [ -d "$d" ] || continue class=$(cat $d/class 2>/dev/null) case "$class" in 0x010*|0x028*|0x040*|0x0805*) drv=$(basename $(readlink $d/driver 2>/dev/null) 2>/dev/null) echo " $(basename $d) class=$class drv=${drv:-NONE}" >> $LOG ;; esac done # Audio codec ACPI probe — if SOF isn't creating a card on this machine, # the usual cause is that coreboot's DSDT doesn't declare the RT5682 / # MAX98360A / etc. ACPI devices with the HIDs SOF's JSL match table looks # for (10EC5682, MX98360A, DLGS7219…). Log what the kernel actually sees # on the ACPI bus so we can cross-reference against sof's allowlist. echo "ACPI audio codecs:" >> $LOG for d in /sys/bus/acpi/devices/*; do [ -d "$d" ] || continue hid=$(cat "$d/hid" 2>/dev/null) case "$hid" in 10EC*|RTL*|MX98*|DLGS*|ESSX*|1013*) path=$(basename "$d") status=$(cat "$d/status" 2>/dev/null) echo " hid=$hid acpi=$path status=$status" >> $LOG ;; esac done # All INT* HIDs — need to know what pinctrl device (INT34C8 for JSL, INT34C5 # for ICL, INT3450-INT3456 for other Intel platforms) is actually declared # on this firmware. Absence of any of these is the reason the Jasper Lake # pinctrl driver sits idle: the driver is linked and module_init ran, but # acpi_driver_match never fires without a matching device on the ACPI bus. echo "ACPI Intel HIDs (INT*):" >> $LOG for d in /sys/bus/acpi/devices/*; do [ -d "$d" ] || continue hid=$(cat "$d/hid" 2>/dev/null) case "$hid" in INT*) drv=$(basename "$(readlink "$d/physical_node/driver" 2>/dev/null)" 2>/dev/null) echo " $hid drv=${drv:-NONE}" >> $LOG ;; esac done # PNP + ACPI devices with the names coreboot uses for pinctrl on various # Intel platforms. Helps us find whether the board's DSDT simply uses a # different HID than upstream Linux expects. echo "ACPI pinctrl candidates:" >> $LOG for d in /sys/bus/acpi/devices/*; do [ -d "$d" ] || continue hid=$(cat "$d/hid" 2>/dev/null) name=$(basename "$d") case "$hid:$name" in INT34*|*GPI*|*GPIO*|PNP0C*) echo " $hid $name" >> $LOG ;; esac done echo "SOF platform devices:" >> $LOG ls /sys/bus/platform/devices/ 2>/dev/null | grep -iE "sof|jsl_rt5682|cs42l42|hda" >> $LOG echo "Platform drivers (audio-related):" >> $LOG ls /sys/bus/platform/drivers/ 2>/dev/null | grep -iE "sof_|snd_|jsl|rt5682|max98|hda" >> $LOG echo "jsl_rt5682_def binding:" >> $LOG if [ -d /sys/bus/platform/devices/jsl_rt5682_def ]; then drv=$(basename "$(readlink /sys/bus/platform/devices/jsl_rt5682_def/driver 2>/dev/null)" 2>/dev/null) echo " driver=${drv:-UNBOUND}" >> $LOG ls /sys/bus/platform/devices/jsl_rt5682_def/ >> $LOG 2>&1 fi echo "Sound class:" >> $LOG ls /sys/class/sound/ 2>/dev/null >> $LOG # Per-PCM names — SOF topologies name PCMs like "Headset (*)", "Speakers (*)", # "HDMI1 (*)" in /proc/asound/card0/pcmNp/info. If ac-native is opening the # wrong PCM (speaker vs headphone), this answers it in one glance. echo "Card 0 PCM names:" >> $LOG for p in /proc/asound/card0/pcm*p/info; do [ -r "$p" ] || continue dev=$(dirname "$p" | sed 's|.*/||') name=$(grep -E '^name:' "$p" 2>/dev/null | head -1) id=$(grep -E '^id:' "$p" 2>/dev/null | head -1) echo " $dev: $id / $name" >> $LOG done echo "ASoC debugfs:" >> $LOG mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null ls /sys/kernel/debug/asoc/ 2>/dev/null >> $LOG cat /sys/kernel/debug/asoc/components 2>/dev/null >> $LOG cat /sys/kernel/debug/asoc/platforms 2>/dev/null >> $LOG cat /sys/kernel/debug/asoc/dais 2>/dev/null | head -20 >> $LOG echo "Loaded audio modules/built-in symbols:" >> $LOG cat /proc/modules 2>/dev/null | grep -iE "sof|snd|rt5682|max98" >> $LOG # Even built-in drivers show as symbols in /proc/kallsyms — confirms which # drivers linked into this kernel image. Broadened to also detect whether # PINCTRL_JASPERLAKE actually took effect (its symbols show up with # jsl_pinctrl_/pinctrl_jsl_ / jasperlake_ prefixes). grep -E " sof_rt5682| snd_sof_| jsl_pinctrl| pinctrl_jsl| jasperlake_| max98357" /proc/kallsyms 2>/dev/null | head -20 >> $LOG # Running kernel .config (IKCONFIG_PROC=y) — grep for our critical audio # symbols so we can definitively see whether olddefconfig accepted them. if [ -r /proc/config.gz ]; then echo "Running kernel config (audio+gpio):" >> $LOG zcat /proc/config.gz 2>/dev/null | grep -E "^CONFIG_(PINCTRL_JASPERLAKE|PINCTRL_INTEL|PINCTRL_METEORLAKE|GPIOLIB|GPIO_ACPI|I2C_DESIGNWARE|SND_SOC_MAX98357A|SND_SOC_INTEL_SOF_RT5682)=" >> $LOG fi echo "GPIO chips:" >> $LOG for d in /sys/bus/gpio/devices/*; do [ -d "$d" ] || continue name=$(basename "$d") ngpio=$(cat "$d/dev" 2>/dev/null) label=$(cat "$d/label" 2>/dev/null) base=$(cat "$d/base" 2>/dev/null) # The device's parent is the platform_device — e.g. INT34C8:00 for JSL. parent=$(basename "$(readlink "$d/../../" 2>/dev/null)" 2>/dev/null) echo " $name label='$label' base=$base parent=$parent" >> $LOG done # MAX98357A's GPIO consumer state — if the amp driver successfully got a # gpiod handle for SD_MODE, this directory has an `enable` gpio-* file. echo "MAX98357A GPIO consumers:" >> $LOG for d in /sys/bus/platform/devices/MX98360A:00/ /sys/bus/platform/devices/MAX98357A:*/; do [ -d "$d" ] || continue ls -la "$d" 2>/dev/null | grep -iE 'gpio|sdmode|enable|of_node' | head -10 >> $LOG done # gpio descriptor consumers — shows which driver holds which pin. if [ -r /sys/kernel/debug/gpio ]; then echo "gpio descriptor consumers (debugfs):" >> $LOG head -60 /sys/kernel/debug/gpio >> $LOG 2>&1 fi # MAX98357A amp state — if the driver is bound but SD_MODE GPIO isn't # assigned, the amp stays in power-down mode and speakers are silent even # though the sound card registers cleanly. Dump its device dir so we see # what resources actually came through. echo "MAX98357A (speaker amp) state:" >> $LOG for d in /sys/bus/platform/devices/*; do name=$(basename "$d") case "$name" in MX98360A*|MAX98357A*|MAX98360A*|i2c-MX98*) echo " ${name}:" >> $LOG ls -la "$d" 2>/dev/null | head -20 >> $LOG [ -L "$d/driver" ] && echo " driver=$(basename $(readlink $d/driver))" >> $LOG ;; esac done echo "ALSA mixer (card 0):" >> $LOG if command -v amixer >/dev/null 2>&1; then amixer -c 0 contents 2>/dev/null | head -60 >> $LOG else echo " amixer not in initramfs (add to bundling if needed)" >> $LOG fi echo "DAPM widgets (speaker path):" >> $LOG for d in /sys/kernel/debug/asoc/sof-rt5682/dapm/Speaker /sys/kernel/debug/asoc/card0/dapm/Speaker; do [ -r "$d" ] && { echo " $d:"; cat "$d"; } >> $LOG 2>&1 done echo "=== CPUINFO ===" >> $LOG head -30 /proc/cpuinfo >> $LOG 2>&1 echo "=== CMDLINE ===" >> $LOG cat /proc/cmdline >> $LOG 2>&1 sync } # end _pre_launch_diag # Launch pre-launch diagnostics in the background unless cmdline disables. case " $(cat /proc/cmdline 2>/dev/null) " in *" AC_NOLAUNCH_DIAG=1 "*) : ;; *) _pre_launch_diag & ;; esac # GPU + USB state used to echo here as raw "[init] GPU: card0 USB=1" # over the splash. Dropped — both are captured in the pre-launch log # and the new boot-timing log for post-mortem review. # Start Swank server in background (if SBCL image exists) SWANK_PID=0 if [ -x /ac-swank ]; then LD_LIBRARY_PATH="/lib64:/usr/lib64" /ac-swank --swank-only >/dev/null 2>&1 & SWANK_PID=$! fi _boot_mark "pre-ac-native launch" # Copy the timing log to USB before ac-native takes over stdout/err # — so the user can post-mortem "how long did each phase take" right # off the stick without having to dig into kmsg. if [ "$USB_MOUNTED" = "1" ]; then cp /run/boot-timing /mnt/boot-timing.log 2>/dev/null fi # Main loop — ac-native runs as a child process (not PID 1). # First attempt: with SDL3/GPU env vars. # If it crashes (signal), retry without SDL (AC_NO_SDL=1). # If that also crashes, keep retrying without SDL with backoff. # # All launch / exit / crash messages are appended to /mnt/ac-crash.log # (persistent) and the stderr log. We no longer write them to /dev/tty0 # because that paints raw text on top of the splash during the boot # transition. CRASH_COUNT=0 SDL_ENABLED=1 while true; do if [ "$SDL_ENABLED" = "1" ]; then LD_LIBRARY_PATH="/lib64" LIBGL_DRIVERS_PATH="/lib64/dri" GBM_DRIVERS_PATH="/lib64/dri" MESA_LOADER_DRIVER_OVERRIDE=iris \ /ac-native /piece.mjs >>/mnt/ac-native-stdout.log 2>>/mnt/ac-native-stderr.log & else LD_LIBRARY_PATH="/lib64" AC_NO_SDL=1 \ /ac-native /piece.mjs >>/mnt/ac-native-stdout.log 2>>/mnt/ac-native-stderr.log & fi CHILD_PID=$! wait $CHILD_PID EXIT_CODE=$? # Exit info → log files only (not tty0; the splash stays clean). if [ "$USB_MOUNTED" = "1" ]; then echo "[init] ac-native pid=$CHILD_PID exit=$EXIT_CODE crash=$CRASH_COUNT sdl=$SDL_ENABLED $(date 2>/dev/null)" >> /mnt/ac-init.log fi # Save crash info to USB if mounted if [ "$USB_MOUNTED" = "1" ]; then echo "exit=$EXIT_CODE crash=$CRASH_COUNT $(date 2>/dev/null)" >> /mnt/ac-crash.log cp /tmp/ac-native-stderr.log /mnt/ac-native-stderr.log 2>/dev/null sync fi if [ "$EXIT_CODE" = "0" ]; then sync # Suppress all kernel output before shutdown echo 0 > /proc/sys/kernel/printk 2>/dev/null printf '\033[?25l\033[2J' > /dev/tty0 2>/dev/null poweroff -f 2>/dev/null echo o > /proc/sysrq-trigger 2>/dev/null sleep 30 break fi if [ "$EXIT_CODE" = "2" ]; then sync echo 0 > /proc/sys/kernel/printk 2>/dev/null printf '\033[?25l\033[2J' > /dev/tty0 2>/dev/null # Enable all sysrq if kernel supports it (belt-and-suspenders) echo 1 > /proc/sys/kernel/sysrq 2>/dev/null # Try every reboot path in order — different kernels accept different ones reboot -f 2>/dev/null echo b > /proc/sysrq-trigger 2>/dev/null # Last resort: direct reboot(2) syscall via busybox `reboot` busybox reboot -f 2>/dev/null halt -f 2>/dev/null sleep 5 # If we're still here, kernel panic is preferable to a silent freeze echo c > /proc/sysrq-trigger 2>/dev/null sleep 5 break fi CRASH_COUNT=$((CRASH_COUNT + 1)) # If crashed with SDL enabled, disable it and retry immediately if [ "$SDL_ENABLED" = "1" ] && [ "$EXIT_CODE" -gt 128 ]; then # Exit > 128 = killed by signal (128 + signal number) SIG=$((EXIT_CODE - 128)) SDL_ENABLED=0 if [ "$USB_MOUNTED" = "1" ]; then echo "sdl_crash: signal=$SIG, disabling SDL" >> /mnt/ac-crash.log sync fi sleep 1 continue fi # Flash screen red via framebuffer if [ -c /dev/fb0 ]; then dd if=/dev/zero bs=4096 count=512 2>/dev/null | tr '\0' '\377' > /dev/fb0 2>/dev/null fi sleep 2 done