diff --git a/Tools/Manual-Tools.md b/Tools/Manual-Tools.md index de3f71a..cbd7ba1 100644 --- a/Tools/Manual-Tools.md +++ b/Tools/Manual-Tools.md @@ -20,6 +20,7 @@ making any changes. - [recreate_shares.sh](#recreate_sharessh) - [continuous_scripts_status.sh](#continuous_scripts_statussh) - [claude_startup.sh](#claude_startupsh) +- [ramdisk_stop.sh](#ramdisk_stopsh) - [Adding a New Tool](#adding-a-new-tool) --- @@ -631,6 +632,73 @@ claude_startup.sh --setup # set up symlinks only — no launch (for array_star --- +## ramdisk_stop.sh + +Safely stops the transcode ramdisk: redirects the transcode symlink to the SSD +fallback first (so Emby continues writing without interruption), then unmounts the +tmpfs and updates the state file. Primary use case is stopping the current ramdisk +before re-running `ramdisk_setup.sh` with new size or threshold values. + +### When to Use + +``` +Bumping RAMDISK_SIZE — setup script is idempotent, skips remount if already mounted + → stop first, then re-run ramdisk_setup.sh with new HOST*_RAMDISK_SIZE value + +Adjusting RAMDISK_WARN_GB / RAMDISK_LOW_GB thresholds + → no need to stop for threshold changes (transcode_manager reads vars live) + → only needed if you're also changing the size + +Temporarily freeing ramdisk RAM — reclaim tmpfs back to general memory pool + → stop, restart later with ramdisk_setup.sh +``` + +### Stop Sequence + +``` +1. Redirect symlink: TRANSCODE_LINK → TRANSCODE_SSD + Emby immediately writes to SSD — no broken-path window during unmount + +2. Check for active transcode files on ramdisk (warn, don't block) + Files in progress on the ramdisk are lost on unmount — expected for maintenance + +3. Unmount ramdisk + Regular umount first; if busy (directory handles only, no active writes) + falls back to lazy unmount automatically + +4. Update /tmp/transcode_state.db → current_target=TRANSCODE_SSD + transcode_manager.sh reads this on its next cycle +``` + +### transcode_manager Warning + +If `transcode_manager.sh` is running, it may flip the symlink back to the ramdisk +on its next cycle (once the ramdisk is unmounted, that flip will fail). Stop +`transcode_manager.sh` first if you need the SSD redirect to hold before remounting. + +### After Stopping + +```bash +# Update host*.conf with new size values: +# HOST1_RAMDISK_SIZE="10G" +# HOST1_RAMDISK_WARN_GB=8.5 +# HOST1_RAMDISK_LOW_GB=7 + +# Remount at new size: +bash Transcodes/ramdisk_setup.sh +``` + +### Usage + +```bash +ramdisk_stop.sh --status # show mount state, symlink, active files — always check first +ramdisk_stop.sh --dry-run # show what would happen without making changes +ramdisk_stop.sh # stop the ramdisk +ramdisk_stop.sh --log # verbose — show each step +``` + +--- + ## Adding a New Tool Write the tool when you solve a problem manually with bash commands. You'll face it again. diff --git a/Tools/README-Tools.md b/Tools/README-Tools.md index 850aa0c..23d7890 100644 --- a/Tools/README-Tools.md +++ b/Tools/README-Tools.md @@ -76,7 +76,7 @@ something new, write the tool. Store it here. Find it at 2am next time. `bulk_permissions_repair.sh`, `zfs_pool_scrub.sh` **Lifecycle Tools** — Backup, setup, and migration support -`container_data_export.sh`, `recreate_shares.sh`, `claude_startup.sh` +`container_data_export.sh`, `recreate_shares.sh`, `claude_startup.sh`, `ramdisk_stop.sh` **Library Sync Bootstrap** — Close the gap between Emby and arr libraries `emby_to_lidarr_sync.sh`, `emby_to_sonarr_sync.sh`, `emby_to_radarr_sync.sh` @@ -125,6 +125,7 @@ The relationship is one-way: Tools act on state that other scripts have written. | `recreate_shares.sh` | Share directories missing after fresh install or disk rebuild | After fresh unRAID install or disk replacement on HOST2 | | `continuous_scripts_status.sh` | Need a live view of watchdog and fallback state | Any time — manual dashboard, no schedule | | `claude_startup.sh` | Claude Code session setup after reboot — symlinks persistent storage | After each unRAID reboot, or called by array_started.sh | +| `ramdisk_stop.sh` | Safely stop the transcode ramdisk — redirect symlink to SSD, unmount, update state | Before re-running ramdisk_setup.sh with new size or thresholds | | `emby_to_lidarr_sync.sh` | Add all Emby album artists not yet tracked in Lidarr | After Lidarr setup, database wipe, or when you suspect gaps | | `emby_to_sonarr_sync.sh` | Add all Emby TV series not yet tracked in Sonarr | After Sonarr setup, database wipe, or when you suspect gaps | | `emby_to_radarr_sync.sh` | Add all Emby movies not yet tracked in Radarr | After Radarr setup, database wipe, or when you suspect gaps | @@ -151,6 +152,7 @@ Situation arises │ recreate_shares ◄── fresh HOST2 setup or disk rebuild │ │ continuous_scripts_status ◄── manual status check at any time │ │ claude_startup ◄── after each unRAID reboot │ +│ ramdisk_stop ◄── before ramdisk resize / remount │ │ │ │ emby_to_lidarr_sync ◄── Lidarr setup / database wipe / gap │ │ emby_to_sonarr_sync ◄── Sonarr setup / database wipe / gap │ diff --git a/Tools/ramdisk_stop.sh b/Tools/ramdisk_stop.sh new file mode 100755 index 0000000..5412a11 --- /dev/null +++ b/Tools/ramdisk_stop.sh @@ -0,0 +1,339 @@ +#!/bin/bash +# ============================================================================================== +# ================================= Ramdisk Stop =============================================== +# ============================================================================================== +# +# PURPOSE +# ───────────────────────────────────────────────────────────────────────────── +# Safely stops the transcode ramdisk: redirects the transcode symlink to the +# SSD fallback before unmounting so Emby continues writing without interruption, +# then unmounts the tmpfs and updates the state file. +# +# Primary use case: stopping the current ramdisk before re-running +# ramdisk_setup.sh with new size or threshold values (setup is idempotent — +# if the ramdisk is mounted, it skips the mount and reports status, so you +# must stop it first to change the size). +# +# ============================================================================================== +# OPERATIONAL MODEL +# ============================================================================================== +# +# Executes in safe order: +# 1. Validate — ramdisk mounted, SSD fallback exists +# 2. Redirect symlink → SSD (Emby immediately writes to SSD instead) +# 3. Warn if active transcode files still on ramdisk (informational — not a blocker) +# 4. Unmount ramdisk tmpfs +# 5. Update /tmp/transcode_state.db → current_target=TRANSCODE_SSD +# +# The symlink redirect happens before unmount so there is no window where Emby +# has nowhere to write. Existing in-progress transcode files on the ramdisk are +# lost on unmount — warn the user but proceed (this is expected for maintenance). +# +# ============================================================================================== +# OPERATIONAL SAFEGUARDS +# ============================================================================================== +# +# Root Required +# umount requires root. +# +# Single Instance Lock +# acquire_lock prevents concurrent stop attempts. +# +# Mounted Check +# Exits cleanly if ramdisk is not mounted — nothing to do. +# +# Symlink-First Order +# Symlink is redirected before unmount — Emby never sees a broken path. +# +# transcode_manager Warning +# Warns if transcode_manager.sh is running — it may flip the symlink back +# to ramdisk on its next cycle. Stop transcode_manager before running this +# if you need the SSD redirect to hold. +# +# ============================================================================================== +# CONFIGURATION +# ============================================================================================== +# +# host*.conf +# HOST*_TRANSCODE_SSD SSD fallback directory — redirect target during stop. +# Aliased by detect_hosts() → TRANSCODE_SSD. +# +# master.conf +# TRANSCODE_LINK Symlink Emby uses. Must match Emby's transcode path setting. +# RAMDISK_PATH tmpfs mount point. +# +# ============================================================================================== +# STATE FILES +# ============================================================================================== +# +# /tmp/transcode_state.db — updated to current_target=TRANSCODE_SSD after stop. +# transcode_manager.sh reads this on its next cycle. +# +# ============================================================================================== +# RUNTIME MODES +# ============================================================================================== +# +# ramdisk_stop.sh +# Stop the ramdisk: redirect symlink → SSD, unmount, update state. +# +# ramdisk_stop.sh --dry-run +# Show what would happen without making any changes. +# +# ramdisk_stop.sh --status +# Show current mount state, symlink target, active files on ramdisk. Exit. +# +# ramdisk_stop.sh --log +# Verbose output for each step. +# +# ============================================================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "$SCRIPT_DIR/../load_config.sh" + +parse_args "$@" + +# ============================================================================================== +# ━━━ Setup ━━━ +# ============================================================================================== +if [[ "$EUID" -ne 0 ]]; then + error "Must be run as root — umount requires root" + exit 1 +fi + +validate_unraid_cmd \ + "/usr/local/emhttp/plugins/dynamix/scripts/notify" \ + "" "" \ + "unRAID notify script" || warn "unRAID notify script not found — native notifications disabled" + +acquire_lock + +detect_hosts + +STATE_FILE="/tmp/transcode_state.db" + +log "Identity: $MY_ID ($LOCAL_SERVER_NAME)" +log "Ramdisk: $RAMDISK_PATH" +log "Fallback: $TRANSCODE_SSD" +log "Symlink: $TRANSCODE_LINK" + +[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made" + +# ============================================================================================== +# ━━━ Status ━━━ +# ============================================================================================== +if [[ "$SHOW_STATUS" == true ]]; then + echo "" + echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" + echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" + echo "$ICON_RAM Ramdisk path: $RAMDISK_PATH" + echo "$ICON_DISK SSD fallback: $TRANSCODE_SSD" + echo "$ICON_LINK Symlink: $TRANSCODE_LINK" + echo "" + + if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then + USAGE=$(df -BG "$RAMDISK_PATH" | awk 'NR==2 {print $3}') + AVAIL=$(df -BG "$RAMDISK_PATH" | awk 'NR==2 {print $4}') + echo " $ICON_RAM Ramdisk: mounted — $USAGE used / $AVAIL available ✅" + FILE_COUNT=$(find "$RAMDISK_PATH" -type f 2>/dev/null | wc -l) + echo " $ICON_RAM Active files on ramdisk: $FILE_COUNT" + else + echo " $ICON_RAM Ramdisk: NOT mounted" + fi + + if [[ -L "$TRANSCODE_LINK" ]]; then + TARGET=$(readlink "$TRANSCODE_LINK") + echo " $ICON_LINK Symlink: $TRANSCODE_LINK → $TARGET" + else + echo " $ICON_LINK Symlink: not set" + fi + + if [[ -f "$STATE_FILE" ]]; then + echo "" + echo " State file ($STATE_FILE):" + while IFS='=' read -r key value; do + [[ -z "$key" ]] && continue + echo " $key = $value" + done < "$STATE_FILE" + else + echo " $ICON_INFO State file: not found (ramdisk never started this boot)" + fi + + if pgrep -f "transcode_manager.sh" >/dev/null 2>&1; then + echo "" + warn "transcode_manager.sh is currently RUNNING" + fi + + echo "━━━━━━━━━━━━━━━━━━━━━━━" + exit 0 +fi + +# ============================================================================================== +# ━━━ Preflight ━━━ +# ============================================================================================== +echo "" +echo "━━━ $ICON_GEAR Preflight ━━━" + +# Bail if not mounted — nothing to do +if ! mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then + warn "Ramdisk is not mounted at $RAMDISK_PATH — nothing to stop" + exit 0 +fi + +log "Ramdisk is mounted ✅" + +# Warn if transcode_manager is running — it may flip symlink back on next cycle +if pgrep -f "transcode_manager.sh" >/dev/null 2>&1; then + warn "transcode_manager.sh is currently RUNNING" + warn "It may flip the symlink back to ramdisk on its next cycle" + warn "Stop transcode_manager.sh first if you need the SSD redirect to hold" + echo "" +fi + +# Confirm SSD fallback exists +if [[ ! -d "$TRANSCODE_SSD" ]]; then + if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — SSD fallback does not exist: $TRANSCODE_SSD" + warn "DRY RUN — would create it before redirecting symlink" + else + warn "SSD fallback does not exist — creating: $TRANSCODE_SSD" + mkdir -p "$TRANSCODE_SSD" || { + error "Failed to create SSD fallback: $TRANSCODE_SSD" + error "Cannot safely redirect symlink — aborting" + exit 1 + } + log "SSD fallback created ✅" + fi +else + log "SSD fallback exists: $TRANSCODE_SSD ✅" +fi + +START=$(date +%s) +STOP_SUCCESS=true + +# ============================================================================================== +# ━━━ Redirect Symlink → SSD ━━━ +# ============================================================================================== +# Redirect BEFORE unmount — Emby continues writing to SSD with no broken path window. +echo "" +echo "━━━ $ICON_LINK Redirect Symlink → SSD ━━━" + +if [[ -L "$TRANSCODE_LINK" ]]; then + CURRENT_TARGET=$(readlink "$TRANSCODE_LINK") + if [[ "$CURRENT_TARGET" == "$TRANSCODE_SSD" ]]; then + log "Symlink already points to SSD — no change needed" + else + if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — would redirect: $TRANSCODE_LINK → $TRANSCODE_SSD" + else + ln -sfn "$TRANSCODE_SSD" "$TRANSCODE_LINK" && \ + warn "Symlink redirected: $TRANSCODE_LINK → $TRANSCODE_SSD ✅" || { + error "Failed to redirect symlink" + STOP_SUCCESS=false + } + fi + fi +elif [[ -e "$TRANSCODE_LINK" ]]; then + warn "$TRANSCODE_LINK exists but is not a symlink — leaving as-is" +else + warn "Symlink $TRANSCODE_LINK does not exist — nothing to redirect" +fi + +# ============================================================================================== +# ━━━ Active Files Warning ━━━ +# ============================================================================================== +echo "" +echo "━━━ $ICON_RAM Active Files Check ━━━" + +FILE_COUNT=$(find "$RAMDISK_PATH" -type f 2>/dev/null | wc -l) +if [[ "$FILE_COUNT" -gt 0 ]]; then + warn "⚠️ $FILE_COUNT file(s) still on ramdisk — will be lost on unmount" + warn "Active transcode sessions should be stopped before unmounting" + warn "Proceeding regardless (this is expected for maintenance)" + if [[ "$LOG" == true ]]; then + find "$RAMDISK_PATH" -type f 2>/dev/null | while read -r f; do + log " $f" + done + fi +else + log "No active files on ramdisk ✅" +fi + +# ============================================================================================== +# ━━━ Unmount ━━━ +# ============================================================================================== +echo "" +echo "━━━ $ICON_RAM Unmount Ramdisk ━━━" + +if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — would unmount: $RAMDISK_PATH" +else + if umount "$RAMDISK_PATH" 2>/dev/null; then + warn "Ramdisk unmounted: $RAMDISK_PATH ✅" + else + # Regular unmount failed — check if only directory handles are open (no active writes) + OPEN_FILES=$(lsof +D "$RAMDISK_PATH" 2>/dev/null | awk 'NR>1 && $5 != "DIR"' | wc -l) + if [[ "$OPEN_FILES" -eq 0 ]]; then + warn "Busy — only directory handles open, no active writes — trying lazy unmount" + if umount -l "$RAMDISK_PATH"; then + warn "Ramdisk lazy-unmounted: $RAMDISK_PATH ✅" + warn "Handles will release when owning processes next check the directory" + else + error "Lazy unmount also failed — $RAMDISK_PATH" + STOP_SUCCESS=false + fi + else + error "Failed to unmount $RAMDISK_PATH — $OPEN_FILES file(s) still open for writing" + error "Stop active transcode sessions and retry" + STOP_SUCCESS=false + fi + fi +fi + +# ============================================================================================== +# ━━━ Update State File ━━━ +# ============================================================================================== +echo "" +echo "━━━ $ICON_GEAR State File ━━━" + +if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — would update $STATE_FILE: current_target=$TRANSCODE_SSD" +elif [[ "$STOP_SUCCESS" == true ]]; then + NOW=$(date +%s) + cat > "$STATE_FILE" </dev/null || echo "not set")" +echo "$ICON_TIME Duration: $(format_duration $(( END - START )))" +echo "" + +if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — no changes made" +elif [[ "$STOP_SUCCESS" == true ]]; then + echo "$ICON_DONE Status: done ✅" + echo "Run ramdisk_setup.sh to remount with new configuration" +else + echo "$ICON_ERROR Status: STOP HAD ERRORS" + notify "Ramdisk stop errors on $(hostname) ($MY_ID) — check output" \ + "Ramdisk Stop" "warning" + exit 1 +fi +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/common.sh b/common.sh index af6da45..cf58c62 100644 --- a/common.sh +++ b/common.sh @@ -592,7 +592,7 @@ detect_hosts() { _alias_assoc "WATCHDOG_APPDATA_SIZES" # ── Output ──────────────────────────────────────────────────────────────── - info "$ICON_HOST Host: $LOCAL_SERVER_NAME → $REMOTE_SERVER_NAME" + log "$ICON_HOST Host: $LOCAL_SERVER_NAME → $REMOTE_SERVER_NAME" } # ============================================================================================== diff --git a/host1.conf b/host1.conf index c7efac2..233de75 100644 --- a/host1.conf +++ b/host1.conf @@ -513,13 +513,13 @@ # ============================================================================================== # Ramdisk size ceiling — tmpfs only uses RAM actually needed, not the full size upfront. -# Real-world: 9 streams peaked at ~5.5GB — 8G gives comfortable headroom on 128GB RAM. - HOST1_RAMDISK_SIZE="8G" +# Real-world: 9 streams peaked at ~5.5GB — 10G gives generous headroom on 128GB RAM. + HOST1_RAMDISK_SIZE="10G" # Usage thresholds — coupled to HOST1_RAMDISK_SIZE, adjust all three together if size changes. -# Hysteresis gap (6.8 - 5.5 = 1.3GB) prevents flip-flop between ramdisk and SSD. - HOST1_RAMDISK_WARN_GB=6.8 # flip to SSD when ramdisk usage reaches this - HOST1_RAMDISK_LOW_GB=5.5 # flip back to ramdisk when usage drops to this +# Hysteresis gap (8.5 - 7 = 1.5GB) prevents flip-flop between ramdisk and SSD. + HOST1_RAMDISK_WARN_GB=8.5 # flip to SSD when ramdisk usage reaches this + HOST1_RAMDISK_LOW_GB=7 # flip back to ramdisk when usage drops to this # SSD fallback path — where transcodes land when ramdisk exceeds HOST1_RAMDISK_WARN_GB. # Must be on cache pool — array disks too slow for active transcode writes.