Skip to content

Instantly share code, notes, and snippets.

@j027
Created August 3, 2026 05:11
Show Gist options
  • Select an option

  • Save j027/7faa9df1035ac03783cfe6f565d89e1b to your computer and use it in GitHub Desktop.

Select an option

Save j027/7faa9df1035ac03783cfe6f565d89e1b to your computer and use it in GitHub Desktop.
Enphase IQ Gateway SD Card sync and tmpfs bind mount
#!/bin/bash
# setup_sd_longevity_v2.sh — Enphase Envoy eMMC Longevity Setup (tmpfs + async sync)
# Run as root on the Envoy after inserting the SD card (via USB adapter)
#
# Usage: bash setup_sd_longevity_v2.sh # Install / re-install
# bash setup_sd_longevity_v2.sh --uninstall # Remove everything
#
# Architecture:
# - All high-write paths are tmpfs bind mounts (in RAM)
# - Services write to tmpfs — fast, always available, never fails
# - A sync daemon periodically copies tmpfs → USB/SD (if present)
# - On boot, data is seeded: eMMC first, then USB overlay (if present)
# - Critical DBs backed to eMMC daily/weekly via cron
# - USB can vanish/reconnect without affecting services
#
# IMPORTANT: Review the detected device before confirming format!
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# ─── Configuration ──────────────────────────────────────────────
# tmpfs size limits (actual RAM usage = actual data stored, not the limit)
# Based on measured data: ~10MB total, these limits give ~10x headroom
TMPFS_DB_SIZE="64M" # All database dirs combined
TMPFS_LOG_SIZE="32M" # All log dirs combined
TMPFS_CORE_SIZE="16M" # Core dumps
TMPFS_TMP_SIZE="64M" # /tmp
# Sync interval in seconds
SYNC_INTERVAL=300
# eMMC backup location (for fallback seeding)
EMMC_BACKUP="/var/opt/emu/.emmc_backup"
# ─── Uninstall mode ─────────────────────────────────────────────
if [ "$1" = "--uninstall" ]; then
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}ERROR: Must run as root${NC}"
exit 1
fi
echo -e "${YELLOW}=== Uninstalling SD Longevity Setup (v2 — tmpfs) ===${NC}"
echo ""
# Stop sync daemon first
if [ -f /tmp/sd_sync.pid ]; then
echo "Stopping sync daemon..."
kill "$(cat /tmp/sd_sync.pid)" 2>/dev/null || true
rm -f /tmp/sd_sync.pid
fi
for pid in $(pgrep -f sd_sync_daemon.sh 2>/dev/null); do
kill "$pid" 2>/dev/null || true
done
# Stop services for clean DB copy
echo "Stopping services for clean data copy-back..."
/etc/init.d/envoy_monit stop 2>/dev/null || true
sleep 2
# ─── Write back tmpfs data to eMMC ────────────────────────────
echo -e "${YELLOW}Writing tmpfs data back to eMMC...${NC}"
# Databases
for dir in binned event pdm archive lifetime ens_device_commission; do
SRC="/var/opt/emu/$dir"
if [ -d "$SRC" ] && mountpoint -q "$SRC" 2>/dev/null; then
echo " Writing back $dir/..."
# Create a temp copy, unmount tmpfs, then move data to eMMC
TMP="/var/opt/emu/.uninstall_tmp_${dir}"
mkdir -p "$TMP"
cp -a "$SRC/." "$TMP/" 2>/dev/null || true
umount -l "$SRC" 2>/dev/null || true
mkdir -p "$SRC"
cp -a "$TMP/." "$SRC/" 2>/dev/null && echo " ✓ $dir/" || echo " ✗ $dir/ (failed)"
rm -rf "$TMP"
fi
done
# Root-level DBs
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
DB_PATH="/var/opt/emu/$db"
if [ -f "$DB_PATH" ]; then
TMP="/var/opt/emu/.uninstall_tmp_${db}"
cp -a "$DB_PATH" "$TMP" 2>/dev/null || true
umount -l "$DB_PATH" 2>/dev/null || true
if [ -f "$TMP" ]; then
cp -a "$TMP" "$DB_PATH" 2>/dev/null && echo " ✓ $db" || echo " ✗ $db (failed)"
rm -f "$TMP"
fi
fi
done
# Logs
for dir in emu nginx agf chrony tinyproxy; do
SRC="/var/log/$dir"
if [ -d "$SRC" ] && mountpoint -q "$SRC" 2>/dev/null; then
TMP="/var/log/.uninstall_tmp_${dir}"
mkdir -p "$TMP"
cp -a "$SRC/." "$TMP/" 2>/dev/null || true
umount -l "$SRC" 2>/dev/null || true
mkdir -p "$SRC"
cp -a "$TMP/." "$SRC/" 2>/dev/null && echo " ✓ log/$dir/" || echo " ✗ log/$dir/ (failed)"
rm -rf "$TMP"
fi
done
for logfile in messages dmesg; do
TARGET="/var/log/$logfile"
if [ -f "$TARGET" ] && [ ! -L "$TARGET" ]; then
TMP="/var/log/.uninstall_tmp_${logfile}"
cp -a "$TARGET" "$TMP" 2>/dev/null || true
umount -l "$TARGET" 2>/dev/null || true
if [ -f "$TMP" ]; then
cp -a "$TMP" "$TARGET" 2>/dev/null && echo " ✓ log/$logfile" || echo " ✗ log/$logfile (failed)"
rm -f "$TMP"
fi
fi
done
# Web logs
if mountpoint -q /var/www/logs 2>/dev/null; then
TMP="/var/www/.uninstall_tmp_logs"
mkdir -p "$TMP"
cp -a /var/www/logs/. "$TMP/" 2>/dev/null || true
umount -l /var/www/logs 2>/dev/null || true
mkdir -p /var/www/logs
cp -a "$TMP/." /var/www/logs/ 2>/dev/null && echo " ✓ www/logs/" || echo " ✗ www/logs/ (failed)"
rm -rf "$TMP"
fi
# Corefiles
if mountpoint -q /corefiles 2>/dev/null; then
umount -l /corefiles 2>/dev/null || true
fi
# Misc high-write paths (cfg, mal, agf meter logs, wifi)
for pair in "cfg:/var/opt/cfg" "mal:/var/opt/mal" "agf_mlgr:/var/opt/agf/mlgr" "wifi:/etc/wifi"; do
TDIR="${pair%%:*}"
TARGET="${pair#*:}"
if mountpoint -q "$TARGET" 2>/dev/null; then
TMP="/tmp/.uninstall_tmp_${TDIR}"
mkdir -p "$TMP"
cp -a "$TARGET/." "$TMP/" 2>/dev/null || true
umount -l "$TARGET" 2>/dev/null || true
mkdir -p "$TARGET"
cp -a "$TMP/." "$TARGET/" 2>/dev/null && echo " ✓ $TDIR/" || echo " ✗ $TDIR/ (failed)"
rm -rf "$TMP"
fi
done
# cache (regeneratable API XML) — raw.db is handled by Enphase's tmpfs_link
if mountpoint -q /var/opt/emu/cache 2>/dev/null; then
TMP="/tmp/.uninstall_tmp_cache"
mkdir -p "$TMP"
cp -a /var/opt/emu/cache/. "$TMP/" 2>/dev/null || true
umount -l /var/opt/emu/cache 2>/dev/null || true
mkdir -p /var/opt/emu/cache
cp -a "$TMP/." /var/opt/emu/cache/ 2>/dev/null && echo " ✓ cache/" || echo " ✗ cache/ (failed)"
rm -rf "$TMP"
fi
# Unmount /var/tmp tmpfs
umount -l /var/tmp 2>/dev/null || true
# Unmount utmp bind mount
umount -l /var/run/utmp 2>/dev/null || true
echo "Data write-back complete."
echo ""
# Also do a final sync to USB if it's available
if mountpoint -q /mnt/sd 2>/dev/null; then
echo "Final sync to USB drive..."
/usr/local/bin/sd_sync_daemon.sh --once 2>/dev/null || true
umount -l /mnt/sd 2>/dev/null || true
fi
# Restart services so they use eMMC copies
/etc/init.d/envoy_monit start 2>/dev/null || true
# Remove init script and boot/shutdown links
echo "Removing init scripts and boot links..."
rm -f /etc/rc3.d/S80sd_bind_mounts
rm -f /etc/rc0.d/K97sd_bind_mounts
rm -f /etc/rc6.d/K97sd_bind_mounts
rm -f /etc/rcS.d/S03sd_bind_mounts /etc/rcS.d/S10sd_bind_mounts
rm -f /etc/rcS.d/S11sd_bind_mounts /etc/rcS.d/S96sd_bind_mounts
rm -f /etc/init.d/sd_bind_mounts
# Remove sync daemon, old watchdog, cron jobs, tools
echo "Removing tools and cron jobs..."
rm -f /usr/local/bin/sd_sync_daemon.sh
rm -f /usr/local/bin/sd_watchdog.sh
rm -f /tmp/sd_sync.pid
rm -f /tmp/sd_watchdog.pid
rm -f /etc/cron.weekly/emmc_health
rm -f /etc/cron.weekly/sd_backup_critical
rm -f /etc/cron.daily/sd_emmc_backup
rm -f /etc/cron.daily/write_stats
rm -f /usr/local/bin/check_writes
# Clean up eMMC backup dir
rm -rf "$EMMC_BACKUP"
# Revert sysctl.conf
if grep -q "# eMMC longevity" /etc/sysctl.conf 2>/dev/null; then
echo "Reverting sysctl.conf..."
sed -i '/# eMMC longevity/d' /etc/sysctl.conf
sed -i '/^vm.dirty_writeback_centisecs = 1500$/d' /etc/sysctl.conf
sed -i '/^vm.dirty_expire_centisecs = 3000$/d' /etc/sysctl.conf
sed -i 's/^#\(vm.dirty_writeback_centisecs.*\) # disabled for eMMC longevity/\1/' /etc/sysctl.conf
sed -i 's/^#\(vm.dirty_expire_centisecs.*\) # disabled for eMMC longevity/\1/' /etc/sysctl.conf
fi
# Revert fstab
echo "Reverting fstab..."
sed -i '/^tmpfs \/tmp tmpfs.*eMMC\|^tmpfs \/tmp tmpfs defaults,noatime,nosuid,nodev/d' /etc/fstab 2>/dev/null || true
sed -i 's/^#\(\/dev\/mmcblk0p6.*\) # disabled for eMMC longevity/\1/' /etc/fstab 2>/dev/null || true
sed -i 's/,commit=60//g' /etc/fstab 2>/dev/null || true
# Restore original sysctl values
sysctl -w vm.dirty_writeback_centisecs=100 > /dev/null 2>&1 || true
sysctl -w vm.dirty_expire_centisecs=100 > /dev/null 2>&1 || true
echo ""
echo -e "${GREEN}=== Uninstall Complete ===${NC}"
echo ""
echo "What was removed:"
echo " ✓ tmpfs data written back to eMMC"
echo " ✓ Final sync to USB (if present)"
echo " ✓ All tmpfs bind mounts removed"
echo " ✓ Sync daemon stopped and removed"
echo " ✓ Init script and boot links removed"
echo " ✓ Cron jobs removed"
echo " ✓ sysctl.conf and fstab reverted"
echo ""
echo -e "${YELLOW}Reboot recommended to fully restore original behavior.${NC}"
exit 0
fi
# ═══════════════════════════════════════════════════════════════════
# INSTALL MODE
# ═══════════════════════════════════════════════════════════════════
echo -e "${GREEN}=== Envoy eMMC Longevity Setup (v2 — tmpfs + async sync) ===${NC}"
echo ""
# ─── Step 0: Check prerequisites ────────────────────────────────
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}ERROR: Must run as root${NC}"
exit 1
fi
echo -e "${YELLOW}Current eMMC health:${NC}"
ruby /opt/emu/scripts/mmc_utils.rb 2>/dev/null || echo " (mmc_utils not available)"
echo ""
echo -e "${YELLOW}Current disk writes (ext4 lifetime):${NC}"
EMMC_TOTAL_MB=0
for part in /dev/mmcblk0p*; do
if [ -b "$part" ]; then
LW=$(tune2fs -l "$part" 2>/dev/null | grep "Lifetime writes:" | awk '{print $3, $4}')
if [ -n "$LW" ]; then
echo " $part: $LW"
VAL=$(echo "$LW" | awk '{print $1}')
UNIT=$(echo "$LW" | awk '{print $2}')
case "$UNIT" in
GB) ADD=$(awk -v v="$VAL" 'BEGIN {printf "%.0f", v * 1024}') ;;
MB) ADD=$(awk -v v="$VAL" 'BEGIN {printf "%.0f", v}') ;;
kB) ADD=$(awk -v v="$VAL" 'BEGIN {printf "%.0f", v / 1024}') ;;
*) ADD=0 ;;
esac
EMMC_TOTAL_MB=$((EMMC_TOTAL_MB + ADD))
fi
fi
done
if [ "$EMMC_TOTAL_MB" -ge 1024 ]; then
echo " Total eMMC lifetime writes: $(awk -v v="$EMMC_TOTAL_MB" 'BEGIN {printf "%.1f", v / 1024}') GB"
else
echo " Total eMMC lifetime writes: ${EMMC_TOTAL_MB} MB"
fi
echo ""
echo -e "${YELLOW}Current RAM:${NC}"
free
echo ""
# ─── Step 1: Detect SD card ─────────────────────────────────────
echo -e "${YELLOW}Detecting SD card / USB storage...${NC}"
DEVS=()
for dev in /dev/sd[a-z] /dev/mmcblk[1-9]; do
if [ -b "$dev" ]; then
SIZE=0
devname=$(basename "$dev")
if [ -f "/sys/block/$devname/size" ]; then
SECTORS=$(cat "/sys/block/$devname/size" 2>/dev/null || echo 0)
SIZE=$((SECTORS * 512))
fi
if [ "$SIZE" -eq 0 ]; then
SIZE=$(blockdev --getsize64 "$dev" 2>/dev/null || echo 0)
fi
if [ "$SIZE" -eq 0 ]; then
if dd if="$dev" bs=512 count=1 of=/dev/null 2>/dev/null; then
SIZE=1
fi
fi
if [ "$SIZE" -gt 0 ] 2>/dev/null; then
SIZE_GB=$(echo "$SIZE / 1073741824" | bc 2>/dev/null || echo "$((SIZE / 1073741824))")
DEVS+=("$dev")
echo " [${#DEVS[@]}] $dev (${SIZE_GB} GB)"
else
echo " SKIP: $dev (empty slot or unreadable)"
fi
fi
done
if [ ${#DEVS[@]} -eq 0 ]; then
echo -e "${RED}No SD card / USB storage detected!${NC}"
echo " Make sure the USB adapter with SD card is plugged in."
echo " Check 'dmesg | tail -20' for USB detection messages."
exit 1
fi
if [ ${#DEVS[@]} -eq 1 ]; then
SD_DEV="${DEVS[0]}"
echo ""
echo -e "${YELLOW}Selected device: ${SD_DEV}${NC}"
else
echo ""
echo -e "${YELLOW}Multiple devices found. Which is your microSD card?${NC}"
read -p "Enter number [1-${#DEVS[@]}]: " CHOICE
if [ -z "$CHOICE" ] || [ "$CHOICE" -lt 1 ] || [ "$CHOICE" -gt ${#DEVS[@]} ] 2>/dev/null; then
echo -e "${RED}Invalid selection.${NC}"
exit 1
fi
SD_DEV="${DEVS[$((CHOICE-1))]}"
echo -e "${YELLOW}Selected device: ${SD_DEV}${NC}"
fi
if echo "$SD_DEV" | grep -q mmcblk; then
SD_PART="${SD_DEV}p1"
else
SD_PART="${SD_DEV}1"
fi
# ─── Step 2: Confirm and format ─────────────────────────────────
echo ""
EXISTING_LABEL=$(blkid "$SD_PART" 2>/dev/null | grep -o 'LABEL="[^"]*"' | cut -d'"' -f2 || true)
SKIP_FORMAT=0
if [ "$EXISTING_LABEL" = "envoy-sd" ]; then
echo -e "${GREEN}SD card already has 'envoy-sd' label — previously set up.${NC}"
echo ""
echo " [1] Skip format, just reinstall scripts (preserves SD data)"
echo " [2] Full format (ERASES ALL DATA on SD)"
echo ""
read -p "Choose [1/2]: " FORMAT_CHOICE
if [ "$FORMAT_CHOICE" = "1" ]; then
SKIP_FORMAT=1
echo "Skipping format."
fi
fi
if [ "$SKIP_FORMAT" -eq 0 ]; then
echo -e "${RED}WARNING: This will ERASE ALL DATA on ${SD_DEV}${NC}"
echo "Contents:"
fdisk -l "$SD_DEV" 2>/dev/null | head -10
echo ""
read -p "Type YES to format ${SD_DEV}: " CONFIRM
if [ "$CONFIRM" != "YES" ]; then
echo "Aborted."
exit 1
fi
echo ""
echo "Partitioning ${SD_DEV}..."
(echo o; echo n; echo p; echo 1; echo; echo; echo w) | fdisk "$SD_DEV" 2>/dev/null || true
sleep 1
partprobe "$SD_DEV" 2>/dev/null || blockdev --rereadpt "$SD_DEV" 2>/dev/null || true
sleep 1
echo "Formatting ${SD_PART} as ext4 (no journal)..."
mkfs.ext4 -L envoy-sd -m 1 "$SD_PART"
tune2fs -c 0 -i 0 "$SD_PART"
echo -e "${GREEN}Format complete.${NC}"
fi
# ─── Step 3: Mount SD card ──────────────────────────────────────
echo ""
echo "Mounting SD card..."
mkdir -p /mnt/sd
if mountpoint -q /mnt/sd 2>/dev/null; then
echo "SD card already mounted at /mnt/sd, remounting..."
mount -o remount,rw,noatime,nodiratime /mnt/sd
else
mount -o rw,noatime,nodiratime "$SD_PART" /mnt/sd
fi
echo "SD card mounted at /mnt/sd"
df -h /mnt/sd
# ─── Step 4: Create directory structure on SD ────────────────────
echo ""
echo "Creating directory structure on SD card..."
mkdir -p /mnt/sd/var/log/emu
mkdir -p /mnt/sd/var/log/nginx
mkdir -p /mnt/sd/var/log/agf
mkdir -p /mnt/sd/var/log/chrony
mkdir -p /mnt/sd/var/log/tinyproxy
mkdir -p /mnt/sd/var/www/logs
mkdir -p /mnt/sd/var/log/misc
mkdir -p /mnt/sd/var/opt/emu/binned
mkdir -p /mnt/sd/var/opt/emu/event
mkdir -p /mnt/sd/var/opt/emu/pdm
mkdir -p /mnt/sd/var/opt/emu/archive
mkdir -p /mnt/sd/var/opt/emu/lifetime
mkdir -p /mnt/sd/var/opt/emu/ens_device_commission
mkdir -p /mnt/sd/var/opt/emu/root_dbs
mkdir -p /mnt/sd/corefiles
mkdir -p /mnt/sd/var/opt/cfg
mkdir -p /mnt/sd/var/opt/mal
mkdir -p /mnt/sd/var/opt/agf/mlgr
mkdir -p /mnt/sd/etc/wifi
echo " Done."
# ─── Step 5: Copy existing data to SD ───────────────────────────
echo ""
echo "Copying existing data to SD card..."
for db_dir in binned event pdm archive lifetime ens_device_commission; do
SRC="/var/opt/emu/$db_dir"
DST="/mnt/sd/var/opt/emu/$db_dir"
if [ -d "$SRC" ]; then
echo " Copying $SRC → $DST"
cp -a "$SRC/"* "$DST/" 2>/dev/null || true
fi
done
echo "Copying root-level databases..."
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
if [ -f "/var/opt/emu/$db" ]; then
echo " Copying /var/opt/emu/$db"
cp -a "/var/opt/emu/$db" "/mnt/sd/var/opt/emu/root_dbs/$db" 2>/dev/null || true
fi
done
echo "Copying logs..."
for log_dir in emu nginx agf chrony tinyproxy; do
SRC="/var/log/$log_dir"
DST="/mnt/sd/var/log/$log_dir"
if [ -d "$SRC" ]; then
echo " Copying $SRC → $DST"
cp -a "$SRC/"* "$DST/" 2>/dev/null || true
fi
done
cp /var/log/messages /mnt/sd/var/log/misc/messages 2>/dev/null || true
cp /var/log/dmesg /mnt/sd/var/log/misc/dmesg 2>/dev/null || true
echo -e "${GREEN}Data copy complete.${NC}"
# ─── Step 5b: Create eMMC backup (for boot fallback seeding) ────
echo ""
echo "Creating eMMC backup for boot fallback..."
mkdir -p "$EMMC_BACKUP/root_dbs"
for dir in binned event pdm archive lifetime ens_device_commission; do
mkdir -p "$EMMC_BACKUP/$dir"
if [ -d "/var/opt/emu/$dir" ]; then
cp -a "/var/opt/emu/$dir/"* "$EMMC_BACKUP/$dir/" 2>/dev/null || true
fi
done
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
if [ -f "/var/opt/emu/$db" ]; then
cp -a "/var/opt/emu/$db" "$EMMC_BACKUP/root_dbs/$db" 2>/dev/null || true
fi
done
echo -e " ${GREEN}eMMC backup created at $EMMC_BACKUP${NC}"
# eMMC backup for misc high-write paths
mkdir -p "$EMMC_BACKUP/cfg" "$EMMC_BACKUP/mal" "$EMMC_BACKUP/agf_mlgr" "$EMMC_BACKUP/wifi"
cp -a /var/opt/cfg/* "$EMMC_BACKUP/cfg/" 2>/dev/null || true
cp -a /var/opt/mal/* "$EMMC_BACKUP/mal/" 2>/dev/null || true
cp -a /var/opt/agf/mlgr/* "$EMMC_BACKUP/agf_mlgr/" 2>/dev/null || true
cp -a /etc/wifi/* "$EMMC_BACKUP/wifi/" 2>/dev/null || true
# ─── Step 6: Create init script (tmpfs + seed) ──────────────────
echo ""
echo "Installing tmpfs bind mount init script..."
cat > /etc/init.d/sd_bind_mounts << 'INITSCRIPT'
#!/bin/sh
### BEGIN INIT INFO
# Provides: sd_bind_mounts
# Required-Start: $local_fs
# Required-Stop: $local_fs
# Default-Start: 3
# Default-Stop: 0 6
# Short-Description: tmpfs bind mounts + async USB sync for eMMC longevity
### END INIT INFO
SD_MOUNT="/mnt/sd"
EMMC_BACKUP="/var/opt/emu/.emmc_backup"
SYNC_PID="/tmp/sd_sync.pid"
# ─── Logging: echo + dmesg via /dev/kmsg ────────────────────────
log_msg() {
echo "$1"
echo "<6>sd_bind_mounts: $1" > /dev/kmsg 2>/dev/null || true
}
# ─── Helper: find SD/USB device by label ────────────────────────
find_sd_partition() {
rm -f /etc/blkid.tab /run/blkid/blkid.tab 2>/dev/null
for dev in /dev/sd[a-z]1 /dev/mmcblk[1-9]p1; do
if [ -b "$dev" ]; then
LABEL=$(blkid "$dev" 2>/dev/null | grep -o 'LABEL="[^"]*"' | cut -d'"' -f2)
if [ "$LABEL" = "envoy-sd" ]; then
echo "$dev"
return 0
fi
fi
done
return 1
}
# ─── Helper: mount tmpfs and bind over target ───────────────────
# Usage: tmpfs_bind <backing_dir> <target_path> <size>
# Creates a tmpfs at a staging point, copies seed data in,
# then bind mounts it over the target.
tmpfs_bind_dir() {
BACKING="$1" # staging/backing dir under /var/run/sd_tmpfs/
TARGET="$2" # real path services write to
SIZE="$3" # tmpfs size limit
mkdir -p "$BACKING"
# Mount tmpfs at the backing location (if not already)
if ! mountpoint -q "$BACKING" 2>/dev/null; then
mount -t tmpfs -o size="$SIZE",noatime,nosuid,nodev tmpfs "$BACKING"
fi
# Peel any existing bind mount on target
while mountpoint -q "$TARGET" 2>/dev/null; do
umount -l "$TARGET" 2>/dev/null || break
done
mkdir -p "$TARGET"
mount --bind "$BACKING" "$TARGET"
}
# Same but for individual files (emu.db, etc.)
tmpfs_bind_file() {
BACKING="$1" # backing file under /var/run/sd_tmpfs/
TARGET="$2" # real file path
# Peel any existing bind mount
while mountpoint -q "$TARGET" 2>/dev/null; do
umount -l "$TARGET" 2>/dev/null || break
done
touch "$BACKING"
mount --bind "$BACKING" "$TARGET"
}
case "$1" in
start)
log_msg "=== tmpfs bind mounts: starting ==="
# ── Phase 1: Create tmpfs backing store ──────────────────────
# One tmpfs for all DB data, one for all logs
TMPFS_DB="/var/run/sd_tmpfs/db"
TMPFS_LOG="/var/run/sd_tmpfs/log"
TMPFS_CORE="/var/run/sd_tmpfs/core"
mkdir -p "$TMPFS_DB" "$TMPFS_LOG" "$TMPFS_CORE"
if ! mountpoint -q "$TMPFS_DB" 2>/dev/null; then
mount -t tmpfs -o size=64M,noatime,nosuid tmpfs "$TMPFS_DB"
fi
if ! mountpoint -q "$TMPFS_LOG" 2>/dev/null; then
mount -t tmpfs -o size=32M,noatime,nosuid tmpfs "$TMPFS_LOG"
fi
if ! mountpoint -q "$TMPFS_CORE" 2>/dev/null; then
mount -t tmpfs -o size=16M,noatime,nosuid tmpfs "$TMPFS_CORE"
fi
# Create subdirectory structure in tmpfs
mkdir -p "$TMPFS_DB/binned" "$TMPFS_DB/event" "$TMPFS_DB/pdm" \
"$TMPFS_DB/archive" "$TMPFS_DB/lifetime" \
"$TMPFS_DB/ens_device_commission" "$TMPFS_DB/root_dbs"
mkdir -p "$TMPFS_LOG/emu" "$TMPFS_LOG/nginx" "$TMPFS_LOG/agf" \
"$TMPFS_LOG/chrony" "$TMPFS_LOG/tinyproxy" \
"$TMPFS_LOG/www_logs" "$TMPFS_LOG/misc" "$TMPFS_LOG/stats"
mkdir -p "$TMPFS_CORE/corefiles"
# Additional tmpfs for high-write paths discovered via inotifywait
TMPFS_MISC="/var/run/sd_tmpfs/misc"
mkdir -p "$TMPFS_MISC"
if ! mountpoint -q "$TMPFS_MISC" 2>/dev/null; then
mount -t tmpfs -o size=32M,noatime,nosuid tmpfs "$TMPFS_MISC"
fi
mkdir -p "$TMPFS_MISC/cfg" "$TMPFS_MISC/mal" "$TMPFS_MISC/agf_mlgr" "$TMPFS_MISC/wifi"
mkdir -p "$TMPFS_MISC/cache" # API XML cache — volatile, no sync needed
# Mount /var/tmp as tmpfs (SQLite etilqs temp files)
if ! mountpoint -q /var/tmp 2>/dev/null; then
mount -t tmpfs -o size=32M,noatime,nosuid,nodev tmpfs /var/tmp
log_msg " /var/tmp mounted as tmpfs"
fi
log_msg "tmpfs backing stores mounted"
# ── Phase 2: Seed from eMMC (guaranteed available) ───────────
log_msg "Seeding tmpfs from eMMC backup..."
if [ -d "$EMMC_BACKUP" ]; then
for dir in binned event pdm archive lifetime ens_device_commission; do
if [ -d "$EMMC_BACKUP/$dir" ]; then
cp -a "$EMMC_BACKUP/$dir/"* "$TMPFS_DB/$dir/" 2>/dev/null || true
fi
done
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
if [ -f "$EMMC_BACKUP/root_dbs/$db" ]; then
cp -a "$EMMC_BACKUP/root_dbs/$db" "$TMPFS_DB/root_dbs/$db" 2>/dev/null || true
fi
done
log_msg " eMMC seed complete"
# Seed misc paths from eMMC backup
TMPFS_MISC="/var/run/sd_tmpfs/misc"
for pair in "cfg:cfg" "mal:mal" "agf_mlgr:agf_mlgr" "wifi:wifi"; do
TDIR="${pair%%:*}"
BDIR="${pair#*:}"
if [ -d "$EMMC_BACKUP/$BDIR" ]; then
cp -a "$EMMC_BACKUP/$BDIR/"* "$TMPFS_MISC/$TDIR/" 2>/dev/null || true
fi
done
else
# No backup dir — seed from live eMMC paths
log_msg " No eMMC backup dir, seeding from live paths..."
for dir in binned event pdm archive lifetime ens_device_commission; do
if [ -d "/var/opt/emu/$dir" ]; then
cp -a "/var/opt/emu/$dir/"* "$TMPFS_DB/$dir/" 2>/dev/null || true
fi
done
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
if [ -f "/var/opt/emu/$db" ]; then
cp -a "/var/opt/emu/$db" "$TMPFS_DB/root_dbs/$db" 2>/dev/null || true
fi
done
fi
# Seed logs from eMMC (whatever is there currently)
for dir in emu nginx agf chrony tinyproxy; do
if [ -d "/var/log/$dir" ]; then
cp -a "/var/log/$dir/"* "$TMPFS_LOG/$dir/" 2>/dev/null || true
fi
done
for logfile in messages dmesg; do
if [ -f "/var/log/$logfile" ] && [ ! -L "/var/log/$logfile" ]; then
cp -a "/var/log/$logfile" "$TMPFS_LOG/misc/$logfile" 2>/dev/null || true
fi
done
if [ -d /var/www/logs ]; then
cp -a /var/www/logs/* "$TMPFS_LOG/www_logs/" 2>/dev/null || true
fi
# Seed misc high-write paths from eMMC
TMPFS_MISC="/var/run/sd_tmpfs/misc"
if [ -d /var/opt/cfg ]; then
cp -a /var/opt/cfg/* "$TMPFS_MISC/cfg/" 2>/dev/null || true
fi
if [ -d /var/opt/mal ]; then
cp -a /var/opt/mal/* "$TMPFS_MISC/mal/" 2>/dev/null || true
fi
if [ -d /var/opt/agf/mlgr ]; then
cp -a /var/opt/agf/mlgr/* "$TMPFS_MISC/agf_mlgr/" 2>/dev/null || true
fi
if [ -d /etc/wifi ]; then
cp -a /etc/wifi/* "$TMPFS_MISC/wifi/" 2>/dev/null || true
fi
# Note: /var/opt/emu/raw is handled by Enphase's tmpfs_link — don't override
if [ -d /var/opt/emu/cache ]; then
cp -a /var/opt/emu/cache/* "$TMPFS_MISC/cache/" 2>/dev/null || true
fi
# ── Phase 3: Try USB overlay (newer data) ────────────────────
# Attempt to mount USB and overwrite tmpfs with fresher data.
# This is best-effort — if USB isn't there, eMMC seed is fine.
if ! mountpoint -q "$SD_MOUNT" 2>/dev/null; then
SD_PART=$(find_sd_partition)
if [ -n "$SD_PART" ]; then
log_msg "Found USB drive at $SD_PART — overlaying with newer data..."
mkdir -p "$SD_MOUNT"
mount -o ro,noatime,nodiratime "$SD_PART" "$SD_MOUNT" 2>/dev/null
if [ $? -eq 0 ]; then
# Overlay DB data from USB (newer than eMMC backup)
for dir in binned event pdm archive lifetime ens_device_commission; do
if [ -d "$SD_MOUNT/var/opt/emu/$dir" ]; then
cp -a "$SD_MOUNT/var/opt/emu/$dir/"* "$TMPFS_DB/$dir/" 2>/dev/null || true
fi
done
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
if [ -f "$SD_MOUNT/var/opt/emu/root_dbs/$db" ]; then
cp -a "$SD_MOUNT/var/opt/emu/root_dbs/$db" "$TMPFS_DB/root_dbs/$db" 2>/dev/null || true
fi
done
# Overlay logs from USB
for dir in emu nginx agf chrony tinyproxy; do
if [ -d "$SD_MOUNT/var/log/$dir" ]; then
cp -a "$SD_MOUNT/var/log/$dir/"* "$TMPFS_LOG/$dir/" 2>/dev/null || true
fi
done
for logfile in messages dmesg; do
if [ -f "$SD_MOUNT/var/log/misc/$logfile" ]; then
cp -a "$SD_MOUNT/var/log/misc/$logfile" "$TMPFS_LOG/misc/$logfile" 2>/dev/null || true
fi
done
if [ -d "$SD_MOUNT/var/www/logs" ]; then
cp -a "$SD_MOUNT/var/www/logs/"* "$TMPFS_LOG/www_logs/" 2>/dev/null || true
fi
# Overlay misc paths from USB
for mdir in cfg mal; do
if [ -d "$SD_MOUNT/var/opt/$mdir" ]; then
cp -a "$SD_MOUNT/var/opt/$mdir/"* "$TMPFS_MISC/$mdir/" 2>/dev/null || true
fi
done
if [ -d "$SD_MOUNT/var/opt/agf/mlgr" ]; then
cp -a "$SD_MOUNT/var/opt/agf/mlgr/"* "$TMPFS_MISC/agf_mlgr/" 2>/dev/null || true
fi
if [ -d "$SD_MOUNT/etc/wifi" ]; then
cp -a "$SD_MOUNT/etc/wifi/"* "$TMPFS_MISC/wifi/" 2>/dev/null || true
fi
log_msg " USB overlay complete"
# Remount read-write for sync daemon later
umount "$SD_MOUNT" 2>/dev/null
mount -o rw,noatime,nodiratime "$SD_PART" "$SD_MOUNT" 2>/dev/null || true
else
log_msg " USB mount failed — using eMMC seed only"
fi
else
log_msg "No USB drive found — using eMMC seed only (this is fine)"
fi
else
log_msg "USB already mounted at $SD_MOUNT — overlaying..."
for dir in binned event pdm archive lifetime ens_device_commission; do
if [ -d "$SD_MOUNT/var/opt/emu/$dir" ]; then
cp -a "$SD_MOUNT/var/opt/emu/$dir/"* "$TMPFS_DB/$dir/" 2>/dev/null || true
fi
done
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
if [ -f "$SD_MOUNT/var/opt/emu/root_dbs/$db" ]; then
cp -a "$SD_MOUNT/var/opt/emu/root_dbs/$db" "$TMPFS_DB/root_dbs/$db" 2>/dev/null || true
fi
done
log_msg " USB overlay complete"
fi
# ── Phase 4: Bind mount tmpfs over service paths ─────────────
log_msg "Activating bind mounts..."
# Database directories
for dir in binned event pdm archive lifetime ens_device_commission; do
TARGET="/var/opt/emu/$dir"
while mountpoint -q "$TARGET" 2>/dev/null; do
umount -l "$TARGET" 2>/dev/null || break
done
mkdir -p "$TARGET"
mount --bind "$TMPFS_DB/$dir" "$TARGET"
done
# Root-level DB files
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
EMU_DB="/var/opt/emu/$db"
TMPFS_FILE="$TMPFS_DB/root_dbs/$db"
if [ -f "$EMU_DB" ] || [ -f "$TMPFS_FILE" ]; then
# Ensure both source and target exist
touch "$TMPFS_FILE" "$EMU_DB" 2>/dev/null
while mountpoint -q "$EMU_DB" 2>/dev/null; do
umount -l "$EMU_DB" 2>/dev/null || break
done
mount --bind "$TMPFS_FILE" "$EMU_DB"
fi
done
# Log directories
for dir in emu nginx agf chrony tinyproxy; do
TARGET="/var/log/$dir"
while mountpoint -q "$TARGET" 2>/dev/null; do
umount -l "$TARGET" 2>/dev/null || break
done
mkdir -p "$TARGET"
mount --bind "$TMPFS_LOG/$dir" "$TARGET" 2>/dev/null || true
# nginx runs as nobody, needs write access to its log dir
[ "$dir" = "nginx" ] && chmod 1777 "$TARGET"
done
# Web logs (mongrel2 runs as nobody, needs write access)
while mountpoint -q /var/www/logs 2>/dev/null; do
umount -l /var/www/logs 2>/dev/null || break
done
mkdir -p /var/www/logs
mount --bind "$TMPFS_LOG/www_logs" /var/www/logs 2>/dev/null || true
chmod 1777 /var/www/logs
# System logs (only real files, not symlinks to /dev/null)
for logfile in messages dmesg; do
TARGET="/var/log/$logfile"
if [ -f "$TARGET" ] && [ ! -L "$TARGET" ]; then
while mountpoint -q "$TARGET" 2>/dev/null; do
umount -l "$TARGET" 2>/dev/null || break
done
mount --bind "$TMPFS_LOG/misc/$logfile" "$TARGET"
else
log_msg "SKIP: /var/log/$logfile (symlink or missing)"
fi
done
# Core dumps
while mountpoint -q /corefiles 2>/dev/null; do
umount -l /corefiles 2>/dev/null || break
done
mkdir -p /corefiles
mount --bind "$TMPFS_CORE/corefiles" /corefiles 2>/dev/null || true
# Misc high-write paths (cfg, mal, agf meter logs, wifi config)
TMPFS_MISC="/var/run/sd_tmpfs/misc"
for pair in "cfg:/var/opt/cfg" "mal:/var/opt/mal" "agf_mlgr:/var/opt/agf/mlgr" "wifi:/etc/wifi" "cache:/var/opt/emu/cache"; do
TDIR="${pair%%:*}"
TARGET="${pair#*:}"
while mountpoint -q "$TARGET" 2>/dev/null; do
umount -l "$TARGET" 2>/dev/null || break
done
mkdir -p "$TARGET"
mount --bind "$TMPFS_MISC/$TDIR" "$TARGET"
done
# WiFi dir needs to be writable by services
chmod 755 /etc/wifi 2>/dev/null || true
# utmp — login tracking, written frequently, causes eMMC journal commits
# /var/run is on eMMC on this Yocto system, so bind-mount utmp from /tmp (tmpfs)
if [ -f /var/run/utmp ]; then
cp -a /var/run/utmp /tmp/utmp 2>/dev/null || true
mount --bind /tmp/utmp /var/run/utmp 2>/dev/null || true
log_msg "utmp bind-mounted from /tmp (tmpfs)"
fi
log_msg "All tmpfs bind mounts active"
# ── Phase 5: Start sync daemon ──────────────────────────────
# Kill any existing sync daemon
if [ -f "$SYNC_PID" ]; then
kill "$(cat "$SYNC_PID")" 2>/dev/null || true
rm -f "$SYNC_PID"
fi
if [ -x /usr/local/bin/sd_sync_daemon.sh ]; then
/usr/local/bin/sd_sync_daemon.sh &
log_msg "Sync daemon started (PID $!)"
fi
log_msg "=== tmpfs bind mounts: ready ==="
;;
stop)
echo "=== tmpfs bind mounts: stopping ==="
# Stop sync daemon
if [ -f "$SYNC_PID" ]; then
kill "$(cat "$SYNC_PID")" 2>/dev/null || true
rm -f "$SYNC_PID"
fi
for pid in $(pgrep -f sd_sync_daemon.sh 2>/dev/null); do
[ "$pid" != "$$" ] && [ "$pid" != "$PPID" ] && kill "$pid" 2>/dev/null || true
done
# Final sync to USB before shutdown
if [ -x /usr/local/bin/sd_sync_daemon.sh ]; then
/usr/local/bin/sd_sync_daemon.sh --once 2>/dev/null || true
fi
# Final sync critical DBs to eMMC backup AND live eMMC paths
# This ensures: (a) re-seeding works after SD reformat, (b) --uninstall
# run after a reboot (bind mounts gone) still sees correct data.
TMPFS_DB="/var/run/sd_tmpfs/db"
TMPFS_MISC="/var/run/sd_tmpfs/misc"
echo "Writing critical data back to eMMC..."
# --- DB directories → EMMC_BACKUP AND live eMMC paths ---
for dir in binned event pdm archive lifetime ens_device_commission; do
TARGET="/var/opt/emu/$dir"
if mountpoint -q "$TARGET" 2>/dev/null && [ -d "$TMPFS_DB/$dir" ]; then
TMP="/tmp/stop_wb_${dir}"
mkdir -p "$TMP"
cp -a "$TMPFS_DB/$dir/." "$TMP/" 2>/dev/null || true
umount -l "$TARGET" 2>/dev/null || true
mkdir -p "$TARGET"
cp -a "$TMP/." "$TARGET/" 2>/dev/null || true
mkdir -p "$EMMC_BACKUP/$dir" 2>/dev/null || true
cp -a "$TMP/." "$EMMC_BACKUP/$dir/" 2>/dev/null || true
rm -rf "$TMP"
elif [ -d "$TMPFS_DB/$dir" ]; then
mkdir -p "$EMMC_BACKUP/$dir" 2>/dev/null || true
cp -a "$TMPFS_DB/$dir/." "$EMMC_BACKUP/$dir/" 2>/dev/null || true
fi
done
# --- emu.db root-level files → EMMC_BACKUP AND live eMMC paths ---
if [ -d "$TMPFS_DB/root_dbs" ]; then
mkdir -p "$EMMC_BACKUP/root_dbs" 2>/dev/null || true
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
DB_LIVE="/var/opt/emu/$db"
DB_TMPFS="$TMPFS_DB/root_dbs/$db"
if [ -f "$DB_TMPFS" ]; then
cp -a "$DB_TMPFS" "$EMMC_BACKUP/root_dbs/$db" 2>/dev/null || true
if grep -q " $DB_LIVE " /proc/mounts 2>/dev/null; then
# File-level bind mount active — unmount to expose eMMC, write back
TMP="/tmp/stop_wb_${db}"
cp -a "$DB_TMPFS" "$TMP" 2>/dev/null || true
umount -l "$DB_LIVE" 2>/dev/null || true
cp -a "$TMP" "$DB_LIVE" 2>/dev/null || true
rm -f "$TMP"
fi
fi
done
fi
# --- mal/ and cfg/ → EMMC_BACKUP AND live eMMC paths ---
# These were never updated in EMMC_BACKUP after initial install, causing
# revert to stale meter config when SD is reformatted or unavailable.
for pair in "mal:/var/opt/mal" "cfg:/var/opt/cfg" "agf_mlgr:/var/opt/agf/mlgr"; do
TDIR="${pair%%:*}"
TARGET="${pair#*:}"
if mountpoint -q "$TARGET" 2>/dev/null; then
# Under the active bind mount, $TARGET reads from tmpfs.
# Copy to a temp, unmount to expose bare eMMC, copy from temp.
TMP="/tmp/stop_wb_${TDIR}"
mkdir -p "$TMP"
cp -a "$TARGET/." "$TMP/" 2>/dev/null || true
umount -l "$TARGET" 2>/dev/null || true # exposes underlying eMMC
mkdir -p "$TARGET"
cp -a "$TMP/." "$TARGET/" 2>/dev/null || true # write to live eMMC
mkdir -p "$EMMC_BACKUP/$TDIR" 2>/dev/null || true
cp -a "$TMP/." "$EMMC_BACKUP/$TDIR/" 2>/dev/null || true # update backup
rm -rf "$TMP"
elif [ -d "$TMPFS_MISC/$TDIR" ]; then
# Bind mount already gone (e.g. called standalone) — copy from tmpfs directly
mkdir -p "$TARGET"
cp -a "$TMPFS_MISC/$TDIR/." "$TARGET/" 2>/dev/null || true
mkdir -p "$EMMC_BACKUP/$TDIR" 2>/dev/null || true
cp -a "$TMPFS_MISC/$TDIR/." "$EMMC_BACKUP/$TDIR/" 2>/dev/null || true
fi
done
sync 2>/dev/null || true
# Unmount bind mounts
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
n=0; while [ $n -lt 5 ] && grep -q " /var/opt/emu/$db " /proc/mounts 2>/dev/null; do
umount -l "/var/opt/emu/$db" 2>/dev/null || break
n=$((n + 1))
done
done
for d in /var/log/emu /var/log/nginx /var/log/agf /var/log/chrono \
/var/log/tinyproxy /var/www/logs \
/var/opt/emu/binned /var/opt/emu/event /var/opt/emu/pdm \
/var/opt/emu/archive /var/opt/emu/lifetime \
/var/opt/emu/ens_device_commission /corefiles \
/var/opt/cfg /var/opt/mal /var/opt/agf/mlgr /etc/wifi \
/var/opt/emu/cache; do
n=0; while [ $n -lt 5 ] && grep -q " ${d} " /proc/mounts 2>/dev/null; do
umount -l "$d" 2>/dev/null || break
n=$((n + 1))
done
done
for logfile in /var/log/messages /var/log/dmesg; do
if [ -f "$logfile" ] && [ ! -L "$logfile" ]; then
n=0; while [ $n -lt 5 ] && grep -q " ${logfile} " /proc/mounts 2>/dev/null; do
umount -l "$logfile" 2>/dev/null || break
n=$((n + 1))
done
fi
done
# Unmount utmp bind mount
umount -l /var/run/utmp 2>/dev/null || true
# Unmount tmpfs backing stores
for d in /var/run/sd_tmpfs/db /var/run/sd_tmpfs/log /var/run/sd_tmpfs/core /var/run/sd_tmpfs/misc; do
umount -l "$d" 2>/dev/null || true
done
# Unmount /var/tmp tmpfs
umount -l /var/tmp 2>/dev/null || true
# Unmount USB
umount -l "$SD_MOUNT" 2>/dev/null || true
echo "=== tmpfs bind mounts: stopped ==="
;;
status)
echo "=== tmpfs bind mount status ==="
echo ""
echo "tmpfs backing stores:"
for d in /var/run/sd_tmpfs/db /var/run/sd_tmpfs/log /var/run/sd_tmpfs/core /var/run/sd_tmpfs/misc; do
if mountpoint -q "$d" 2>/dev/null; then
USED=$(du -sh "$d" 2>/dev/null | awk '{print $1}')
echo " ✓ $d ($USED used)"
else
echo " ✗ $d (not mounted)"
fi
done
echo ""
echo "USB drive:"
if mountpoint -q "$SD_MOUNT" 2>/dev/null; then
echo " ✓ Mounted at $SD_MOUNT"
df -h "$SD_MOUNT"
else
echo " ✗ Not mounted (sync daemon will retry)"
fi
echo ""
echo "Sync daemon:"
if [ -f /tmp/sd_sync.pid ] && kill -0 "$(cat /tmp/sd_sync.pid)" 2>/dev/null; then
echo " ✓ Running (PID $(cat /tmp/sd_sync.pid))"
if [ -f /tmp/sd_sync_last ]; then
echo " Last sync: $(cat /tmp/sd_sync_last)"
fi
else
echo " ✗ Not running"
fi
echo ""
echo "Active bind mounts:"
grep "sd_tmpfs" /proc/mounts 2>/dev/null | awk '{print " " $2}' || echo " (none)"
echo ""
echo "RAM usage:"
free
;;
*)
echo "Usage: $0 {start|stop|status}"
exit 1
;;
esac
INITSCRIPT
chmod +x /etc/init.d/sd_bind_mounts
# Link into boot sequence (rc3.d S80 — after USB init, before app startup)
ln -sf ../init.d/sd_bind_mounts /etc/rc3.d/S80sd_bind_mounts
# Remove old rcS.d links if present
rm -f /etc/rcS.d/S03sd_bind_mounts /etc/rcS.d/S10sd_bind_mounts /etc/rcS.d/S11sd_bind_mounts /etc/rcS.d/S96sd_bind_mounts
# Shutdown links
ln -sf ../init.d/sd_bind_mounts /etc/rc0.d/K97sd_bind_mounts
ln -sf ../init.d/sd_bind_mounts /etc/rc6.d/K97sd_bind_mounts
echo -e "${GREEN}Init script installed (S80sd_bind_mounts).${NC}"
# ─── Step 6b: Install sync daemon ────────────────────────────────
echo ""
echo "Installing sync daemon..."
cat > /usr/local/bin/sd_sync_daemon.sh << 'SYNCDAEMON'
#!/bin/sh
# sd_sync_daemon.sh — Periodically sync tmpfs data to USB drive
# Lightweight, retry-friendly. If USB is gone, just skip and try next cycle.
#
# Usage:
# sd_sync_daemon.sh # Run as daemon (background loop)
# sd_sync_daemon.sh --once # Single sync pass then exit
SD_MOUNT="/mnt/sd"
TMPFS_DB="/var/run/sd_tmpfs/db"
TMPFS_LOG="/var/run/sd_tmpfs/log"
TMPFS_MISC="/var/run/sd_tmpfs/misc"
SYNC_INTERVAL=300
PIDFILE="/tmp/sd_sync.pid"
LAST_SYNC_FILE="/tmp/sd_sync_last"
ONE_SHOT=0
if [ "$1" = "--once" ]; then
ONE_SHOT=1
fi
# ─── Find and mount USB if not already mounted ──────────────────
try_mount_usb() {
if mountpoint -q "$SD_MOUNT" 2>/dev/null; then
# Check if the underlying block device still exists BEFORE any I/O.
# USB readers can disconnect and re-enumerate under a different node (e.g.
# sda→sdb). Writing through a stale mount to a gone device causes an ext4
# journal abort — so we detect and clear the stale mount first.
CURRENT_DEV=$(awk "\$2 == \"$SD_MOUNT\" {print \$1}" /proc/mounts 2>/dev/null)
if [ -n "$CURRENT_DEV" ] && [ ! -b "$CURRENT_DEV" ]; then
# Block device is gone — lazy-unmount before any write attempt
umount -l "$SD_MOUNT" 2>/dev/null || true
elif touch "$SD_MOUNT/.mount_test" 2>/dev/null; then
rm -f "$SD_MOUNT/.mount_test"
return 0
else
# Write test failed (I/O error or read-only) — force remount
umount -l "$SD_MOUNT" 2>/dev/null || true
fi
fi
# Try to find our labeled device (handles sda→sdb re-enumeration)
rm -f /etc/blkid.tab /run/blkid/blkid.tab 2>/dev/null
for dev in /dev/sd[a-z]1 /dev/mmcblk[1-9]p1; do
if [ -b "$dev" ]; then
LABEL=$(blkid "$dev" 2>/dev/null | grep -o 'LABEL="[^"]*"' | cut -d'"' -f2)
if [ "$LABEL" = "envoy-sd" ]; then
mkdir -p "$SD_MOUNT"
mount -o rw,noatime,nodiratime,errors=continue "$dev" "$SD_MOUNT" 2>/dev/null
return $?
fi
fi
done
return 1
}
# ─── Sync tmpfs → USB ───────────────────────────────────────────
do_sync() {
# Check USB is mounted and writable
if ! mountpoint -q "$SD_MOUNT" 2>/dev/null; then
return 1
fi
if ! touch "$SD_MOUNT/.sync_test" 2>/dev/null; then
return 1
fi
rm -f "$SD_MOUNT/.sync_test"
# Sync database directories
for dir in binned event pdm archive lifetime ens_device_commission; do
SRC="$TMPFS_DB/$dir"
DST="$SD_MOUNT/var/opt/emu/$dir"
if [ -d "$SRC" ]; then
mkdir -p "$DST"
cp -a "$SRC/"* "$DST/" 2>/dev/null || true
fi
done
# Sync root-level DBs
mkdir -p "$SD_MOUNT/var/opt/emu/root_dbs"
for db in emu.db emu.db-wal emu.db-shm agf_perf.db agf_perf.db-wal agf_perf.db-shm; do
if [ -f "$TMPFS_DB/root_dbs/$db" ]; then
cp -a "$TMPFS_DB/root_dbs/$db" "$SD_MOUNT/var/opt/emu/root_dbs/$db" 2>/dev/null || true
fi
done
# Sync logs
for dir in emu nginx agf chrony tinyproxy; do
SRC="$TMPFS_LOG/$dir"
DST="$SD_MOUNT/var/log/$dir"
if [ -d "$SRC" ]; then
mkdir -p "$DST"
cp -a "$SRC/"* "$DST/" 2>/dev/null || true
fi
done
mkdir -p "$SD_MOUNT/var/log/misc"
for logfile in messages dmesg; do
if [ -f "$TMPFS_LOG/misc/$logfile" ]; then
cp -a "$TMPFS_LOG/misc/$logfile" "$SD_MOUNT/var/log/misc/$logfile" 2>/dev/null || true
fi
done
mkdir -p "$SD_MOUNT/var/www/logs"
if [ -d "$TMPFS_LOG/www_logs" ]; then
cp -a "$TMPFS_LOG/www_logs/"* "$SD_MOUNT/var/www/logs/" 2>/dev/null || true
fi
# Sync stats logs (write_stats.log, emmc_health.log)
if [ -d "$TMPFS_LOG/stats" ]; then
mkdir -p "$SD_MOUNT/stats"
cp -a "$TMPFS_LOG/stats/"* "$SD_MOUNT/stats/" 2>/dev/null || true
fi
# Sync misc high-write paths (cfg, mal, agf meter logs, wifi)
for pair in "cfg:var/opt/cfg" "mal:var/opt/mal" "agf_mlgr:var/opt/agf/mlgr" "wifi:etc/wifi"; do
TDIR="${pair%%:*}"
SDPATH="${pair#*:}"
if [ -d "$TMPFS_MISC/$TDIR" ]; then
mkdir -p "$SD_MOUNT/$SDPATH"
cp -a "$TMPFS_MISC/$TDIR/"* "$SD_MOUNT/$SDPATH/" 2>/dev/null || true
fi
done
# Note: /var/opt/emu/cache is NOT synced — it's regeneratable by the app
# Record sync time
date '+%Y-%m-%d %H:%M:%S' > "$LAST_SYNC_FILE"
return 0
}
# ─── One-shot mode ──────────────────────────────────────────────
if [ "$ONE_SHOT" -eq 1 ]; then
try_mount_usb
if do_sync; then
sync 2>/dev/null || true
fi
exit 0
fi
# ─── Daemon mode ────────────────────────────────────────────────
echo $$ > "$PIDFILE"
trap 'rm -f "$PIDFILE"; exit 0' INT TERM
logger -t sd_sync "Sync daemon started (PID $$, interval=${SYNC_INTERVAL}s)"
while true; do
sleep "$SYNC_INTERVAL"
# Try to mount USB if not already
try_mount_usb
if do_sync; then
# Successful sync — no need to log every time (too noisy)
:
else
# USB not available — log occasionally (not every cycle)
FAIL_COUNT=$((${FAIL_COUNT:-0} + 1))
if [ "$FAIL_COUNT" -eq 1 ] || [ $((FAIL_COUNT % 60)) -eq 0 ]; then
logger -t sd_sync "USB not available (attempt #${FAIL_COUNT}) — data safe in tmpfs"
fi
fi
done
SYNCDAEMON
chmod +x /usr/local/bin/sd_sync_daemon.sh
echo -e "${GREEN}Sync daemon installed.${NC}"
# ─── Step 6c: Remove old watchdog if present ─────────────────────
if [ -f /usr/local/bin/sd_watchdog.sh ]; then
echo "Removing old SD watchdog (replaced by sync daemon)..."
if [ -f /tmp/sd_watchdog.pid ]; then
kill "$(cat /tmp/sd_watchdog.pid)" 2>/dev/null || true
rm -f /tmp/sd_watchdog.pid
fi
rm -f /usr/local/bin/sd_watchdog.sh
fi
# ─── Step 7: Fix sysctl writeback timers ─────────────────────────
echo ""
echo "Fixing kernel writeback timers..."
sysctl -w vm.dirty_writeback_centisecs=1500 > /dev/null
sysctl -w vm.dirty_expire_centisecs=3000 > /dev/null
if grep -q "# eMMC longevity" /etc/sysctl.conf 2>/dev/null; then
echo " sysctl.conf already patched."
else
sed -i 's/^vm.dirty_writeback_centisecs.*/#& # disabled for eMMC longevity/' /etc/sysctl.conf
sed -i 's/^vm.dirty_expire_centisecs.*/#& # disabled for eMMC longevity/' /etc/sysctl.conf
cat >> /etc/sysctl.conf << 'EOF'
# eMMC longevity — reduce flush frequency (was 100/100 = 1 second!)
vm.dirty_writeback_centisecs = 1500
vm.dirty_expire_centisecs = 3000
EOF
echo -e " ${GREEN}sysctl.conf patched: 1s → 15s/30s writeback${NC}"
fi
# ─── Step 8: Mount /tmp as tmpfs ─────────────────────────────────
echo ""
echo "Setting up /tmp as tmpfs..."
if mount | grep -q "tmpfs on /tmp"; then
echo " /tmp is already tmpfs."
else
umount /tmp 2>/dev/null || true
mount -t tmpfs -o defaults,noatime,nosuid,nodev,size=64M tmpfs /tmp 2>/dev/null && \
echo -e " ${GREEN}/tmp mounted as tmpfs (64MB)${NC}" || \
echo -e " ${YELLOW}/tmp busy — will be tmpfs after reboot${NC}"
fi
if grep -q "^tmpfs.*/tmp" /etc/fstab; then
echo " fstab already has tmpfs /tmp."
else
sed -i 's|^/dev/mmcblk0p6.*|#& # disabled for eMMC longevity\ntmpfs /tmp tmpfs defaults,noatime,nosuid,nodev,size=64M 0 0|' /etc/fstab
echo -e " ${GREEN}fstab updated: /tmp → tmpfs${NC}"
fi
# ─── Step 9: Increase ext4 commit intervals ─────────────────────
echo ""
echo "Increasing ext4 journal commit intervals..."
mount -o remount,commit=60 / 2>/dev/null && echo " / remounted with commit=60" || true
mount -o remount,commit=60 /var 2>/dev/null && echo " /var remounted with commit=60" || true
mount -o remount,commit=60 /home 2>/dev/null && echo " /home remounted with commit=60" || true
sed -i 's/\(,commit=60\)\{2,\}/,commit=60/g' /etc/fstab 2>/dev/null || true
for part in mmcblk0p3 mmcblk0p5 mmcblk0p7; do
if grep -q "/dev/$part" /etc/fstab 2>/dev/null && ! grep "/dev/$part" /etc/fstab | grep -q "commit=60"; then
sed -i "s|\(/dev/$part.*ext4.*\)rw,noatime|\1rw,noatime,commit=60|" /etc/fstab
echo " fstab: $part → commit=60"
fi
done
# ─── Step 10: Set SQLite WAL mode ───────────────────────────────
echo ""
echo "Setting SQLite WAL mode on databases..."
for db in /mnt/sd/var/opt/emu/binned/binned.db \
/mnt/sd/var/opt/emu/event/event.db \
/mnt/sd/var/opt/emu/pdm/pdm.db \
/mnt/sd/var/opt/emu/root_dbs/emu.db \
/mnt/sd/var/opt/emu/lifetime/lifetime.db; do
if [ -f "$db" ]; then
sqlite3 "$db" "PRAGMA journal_mode=WAL;" 2>/dev/null && \
echo " WAL: $db" || echo " SKIP: $db (in use?)"
fi
done
# ─── Step 11: Activate tmpfs mounts + sync NOW ───────────────────
echo ""
echo "Stopping services before bind mount activation..."
# Services must be stopped so their open file descriptors are closed.
# Without this, processes holding open fds to eMMC files bypass the bind
# mount and continue writing directly to eMMC instead of tmpfs.
/etc/init.d/envoy_monit stop 2>/dev/null || true
sleep 2
echo "Activating tmpfs bind mounts and sync daemon..."
/etc/init.d/sd_bind_mounts start
echo "Restarting services against tmpfs paths..."
# Services now open files through the bind-mounted (tmpfs) paths.
/etc/init.d/envoy_monit start 2>/dev/null || true
# ─── Step 12: Install daily eMMC backup cron ─────────────────────
echo ""
echo "Installing daily eMMC backup of critical DBs..."
cat > /etc/cron.daily/sd_emmc_backup << 'CRONSCRIPT'
#!/bin/sh
# Daily backup: critical DBs from tmpfs → eMMC
# Ensures eMMC has a recent copy if USB and RAM are both lost (power failure)
TMPFS_DB="/var/run/sd_tmpfs/db"
EMMC_BACKUP="/var/opt/emu/.emmc_backup"
if ! mountpoint -q "$TMPFS_DB" 2>/dev/null; then
logger -t sd_emmc_backup "tmpfs not mounted, skipping"
exit 0
fi
mkdir -p "$EMMC_BACKUP/root_dbs" "$EMMC_BACKUP/lifetime"
# Critical: emu.db (device config, inverter pairing)
for db in emu.db emu.db-wal emu.db-shm; do
if [ -f "$TMPFS_DB/root_dbs/$db" ]; then
cp -a "$TMPFS_DB/root_dbs/$db" "$EMMC_BACKUP/root_dbs/$db" 2>/dev/null
fi
done
# Critical: lifetime.db (production totals)
if [ -d "$TMPFS_DB/lifetime" ]; then
cp -a "$TMPFS_DB/lifetime/"* "$EMMC_BACKUP/lifetime/" 2>/dev/null
fi
sync
logger -t sd_emmc_backup "Backed up emu.db + lifetime to eMMC"
CRONSCRIPT
chmod +x /etc/cron.daily/sd_emmc_backup
echo -e " ${GREEN}Daily eMMC backup installed.${NC}"
# ─── Step 12b: Install weekly eMMC health monitor ────────────────
echo ""
echo "Installing weekly eMMC health monitor..."
cat > /etc/cron.weekly/emmc_health << 'CRONSCRIPT'
#!/bin/sh
LIFE=$(cat /sys/class/mmc_host/mmc0/mmc0:0001/life_time 2>/dev/null || echo "N/A")
EOL=$(cat /sys/class/mmc_host/mmc0/mmc0:0001/pre_eol_info 2>/dev/null || echo "N/A")
SECTORS=$(awk '/mmcblk0 / {print $10}' /proc/diskstats)
PE=$(ruby /opt/emu/scripts/mmc_utils.rb 2>/dev/null | tr -d '\n' || echo "N/A")
# Write to tmpfs — sync daemon carries it to USB
LOG_DIR="/var/run/sd_tmpfs/log/stats"
mkdir -p "$LOG_DIR"
echo "$(date '+%Y-%m-%d %H:%M'): life=$LIFE eol=$EOL sectors=$SECTORS pe=$PE" >> "$LOG_DIR/emmc_health.log"
CRONSCRIPT
chmod +x /etc/cron.weekly/emmc_health
# ─── Step 12c: Install daily write stats logger ──────────────────
echo ""
echo "Installing daily write stats logger..."
cat > /etc/cron.daily/write_stats << 'CRONSCRIPT'
#!/bin/sh
DATE=$(date '+%Y-%m-%d %H:%M')
LIFE=$(cat /sys/class/mmc_host/mmc0/mmc0:0001/life_time 2>/dev/null || echo "N/A")
EOL=$(cat /sys/class/mmc_host/mmc0/mmc0:0001/pre_eol_info 2>/dev/null || echo "N/A")
EMMC_TOTAL=$(awk '/mmcblk0 / {print $10}' /proc/diskstats)
EXT4_STATS=""
for dev in /sys/fs/ext4/*/lifetime_write_kbytes; do
if [ -f "$dev" ]; then
DEVNAME=$(echo "$dev" | sed 's|/sys/fs/ext4/||;s|/lifetime_write_kbytes||')
KB=$(cat "$dev" 2>/dev/null || echo 0)
MB=$((KB / 1024))
EXT4_STATS="${EXT4_STATS} ${DEVNAME}=${MB}MB"
fi
done
SD_SECTORS="N/A"
SD_DEV=$(mount | awk '/\/mnt\/sd / {print $1}')
if [ -n "$SD_DEV" ]; then
SD_DISK=$(echo "$SD_DEV" | sed 's/[0-9]*$//' | sed 's|/dev/||')
SD_SECTORS=$(awk -v d="$SD_DISK " '$0 ~ d {print $10}' /proc/diskstats)
[ -z "$SD_SECTORS" ] && SD_SECTORS="N/A"
fi
# tmpfs usage
TMPFS_USED="N/A"
if mountpoint -q /var/run/sd_tmpfs/db 2>/dev/null; then
TMPFS_USED=$(du -sh /var/run/sd_tmpfs/ 2>/dev/null | tail -1 | awk '{print $1}')
fi
# Write to tmpfs — sync daemon carries it to USB
LOG_DIR="/var/run/sd_tmpfs/log/stats"
mkdir -p "$LOG_DIR"
echo "${DATE} | emmc_life=${LIFE} eol=${EOL} | emmc_total=${EMMC_TOTAL}s | sd_total=${SD_SECTORS}s | tmpfs_used=${TMPFS_USED} | ext4:${EXT4_STATS}" >> "$LOG_DIR/write_stats.log"
CRONSCRIPT
chmod +x /etc/cron.daily/write_stats
echo -e " ${GREEN}Daily write stats logger installed.${NC}"
# ─── Step 13: Install write rate monitor ─────────────────────────
cat > /usr/local/bin/check_writes << 'MONITOR'
#!/bin/sh
# Quick check of eMMC write rate + tmpfs usage
WAIT=${1:-60}
S1=$(awk '/mmcblk0 / {print $10}' /proc/diskstats)
echo "Measuring eMMC writes for ${WAIT} seconds..."
sleep "$WAIT"
S2=$(awk '/mmcblk0 / {print $10}' /proc/diskstats)
DELTA=$((S2 - S1))
KB=$((DELTA * 512 / 1024))
RATE=$((KB * 86400 / WAIT / 1024))
echo "Sectors written: $DELTA"
echo "KB written in ${WAIT}s: $KB"
echo "Estimated daily rate: ${RATE} MB/day"
echo ""
echo "tmpfs usage:"
for d in /var/run/sd_tmpfs/db /var/run/sd_tmpfs/log /var/run/sd_tmpfs/core; do
if mountpoint -q "$d" 2>/dev/null; then
echo " $d: $(du -sh "$d" 2>/dev/null | awk '{print $1}')"
fi
done
echo ""
echo "eMMC wear level:"
ruby /opt/emu/scripts/mmc_utils.rb 2>/dev/null || echo " N/A"
MONITOR
chmod +x /usr/local/bin/check_writes
# Remove old weekly backup cron (replaced by daily)
rm -f /etc/cron.weekly/sd_backup_critical
# ─── Summary ─────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}════════════════════════════════════════════${NC}"
echo -e "${GREEN} Setup Complete! (v2 — tmpfs + async sync)${NC}"
echo -e "${GREEN}════════════════════════════════════════════${NC}"
echo ""
echo "Architecture:"
echo " Services write to → tmpfs (RAM) → sync daemon → USB drive"
echo " On boot: eMMC seed → USB overlay → tmpfs ready"
echo " On shutdown: tmpfs → USB + critical DBs → eMMC"
echo ""
echo "What was done:"
echo " ✓ tmpfs bind mounts for all high-write paths (in RAM)"
echo " ✓ Data seeded from eMMC + USB overlay"
echo " ✓ Sync daemon: tmpfs → USB every ${SYNC_INTERVAL}s"
echo " ✓ Daily eMMC backup of critical DBs (emu.db, lifetime.db)"
echo " ✓ Kernel writeback: 1s → 15s/30s"
echo " ✓ /tmp → tmpfs (64MB RAM)"
echo " ✓ ext4 commit interval → 60s"
echo " ✓ SQLite WAL mode on databases"
echo " ✓ Weekly eMMC health monitor"
echo " ✓ Daily write stats logger"
echo " ✓ Old watchdog removed (not needed anymore)"
echo ""
echo "If USB vanishes:"
echo " - Services continue uninterrupted (tmpfs always available)"
echo " - Sync daemon logs a warning and retries each cycle"
echo " - When USB returns, sync resumes automatically"
echo " - eMMC backup ensures data survives even total loss"
echo ""
echo "Commands:"
echo " check_writes 60 — measure eMMC write rate"
echo " /etc/init.d/sd_bind_mounts status — check tmpfs + sync status"
echo " sd_sync_daemon.sh --once — force sync to USB now"
echo " ruby /opt/emu/scripts/mmc_utils.rb — check eMMC wear"
echo ""
echo -e "${YELLOW}Reboot recommended to fully activate all changes.${NC}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment