HomeAssistant VM Watchdog 指令手冊

建立日期:2026-04-11
分類:commands
環境:macOS 15 · VirtualBox 7.1.8 · launchd


部署指令(一次性設定)

Step 1:建立 Watchdog Script

# 建立目錄
mkdir -p ~/.local/bin

# 建立 watchdog 腳本
cat > ~/.local/bin/homeassistant-vm-watchdog.sh << 'EOF'
#!/bin/bash
# ─────────────────────────────────────────────────────────────────
# HomeAssistant VM Watchdog + Cloudflare Tunnel
# • 開機自動啟動 VirtualBox VM(headless 模式)
# • VM crash 後自動重啟
# • 啟動並持續監控 Cloudflare Tunnel(ha.mansionlai.com)
# • Tunnel 異常退出後自動重啟
# • Log rotation:超過 1MB 自動輪替,保留最近 7 天
# ─────────────────────────────────────────────────────────────────

VM_NAME="HomeAssistant"
VBOX_HEADLESS="/usr/local/bin/VBoxHeadless"
VBOX_MANAGE="/usr/local/bin/VBoxManage"
CLOUDFLARED="/opt/homebrew/bin/cloudflared"
CF_CONFIG="${HOME}/.cloudflared/config.yml"
LOG_FILE="${HOME}/Library/Logs/homeassistant-vm.log"
LOG_MAX_BYTES=$((1024 * 1024))   # 1 MB
LOG_MAX_DAYS=7                   # 保留天數
POLL_INTERVAL=60                 # 秒:偵測狀態的輪詢間隔

CF_PID=""

# ── Log 工具 ──────────────────────────────────────────────────────
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') [homeassistant-watchdog] $*" >> "$LOG_FILE"
}

rotate_log() {
    [ ! -f "$LOG_FILE" ] && return

    local size
    size=$(stat -f%z "$LOG_FILE" 2>/dev/null || echo 0)
    if [ "$size" -gt "$LOG_MAX_BYTES" ]; then
        local ts
        ts=$(date '+%Y%m%d-%H%M%S')
        mv "$LOG_FILE" "${LOG_FILE}.${ts}"
        log "Log rotated (was ${size} bytes → archived as .${ts})"
    fi

    find "$(dirname "$LOG_FILE")" \
        -name "$(basename "$LOG_FILE").*" \
        -mtime +${LOG_MAX_DAYS} \
        -delete 2>/dev/null
}

# ── Cloudflare Tunnel 管理 ─────────────────────────────────────────
start_cloudflared() {
    log "Starting Cloudflare Tunnel (ha.mansionlai.com)..."
    "$CLOUDFLARED" tunnel --config "$CF_CONFIG" --no-autoupdate run >> "$LOG_FILE" 2>&1 &
    CF_PID=$!
    log "Cloudflare Tunnel started (PID=$CF_PID)"
}

is_cloudflared_running() {
    [ -n "$CF_PID" ] && kill -0 "$CF_PID" 2>/dev/null
}

# ── VM 狀態查詢 ───────────────────────────────────────────────────
get_vm_state() {
    "$VBOX_MANAGE" showvminfo "$VM_NAME" --machinereadable 2>/dev/null \
        | grep '^VMState=' | cut -d'"' -f2
}

# ── 啟動 ──────────────────────────────────────────────────────────
log "Watchdog started (VM=$VM_NAME, interval=${POLL_INTERVAL}s, maxLog=${LOG_MAX_BYTES}B, retention=${LOG_MAX_DAYS}d)"

# 先啟動 Cloudflare Tunnel
start_cloudflared

# ── 主迴圈 ────────────────────────────────────────────────────────
while true; do
    rotate_log

    # ── 監控 Cloudflare Tunnel ────────────────────────────────────
    if ! is_cloudflared_running; then
        log "Cloudflare Tunnel (PID=$CF_PID) has stopped, restarting..."
        start_cloudflared
    fi

    # ── 監控 VirtualBox VM ────────────────────────────────────────
    STATE=$(get_vm_state)

    case "$STATE" in
        running|starting|restoring|saving)
            log "VM is '$STATE', monitoring..."
            sleep "$POLL_INTERVAL"
            ;;
        "")
            log "Could not query VM state, retrying in ${POLL_INTERVAL}s..."
            sleep "$POLL_INTERVAL"
            ;;
        *)
            log "VM state is '$STATE', launching headless..."
            "$VBOX_HEADLESS" --startvm "$VM_NAME" >> "$LOG_FILE" 2>&1
            EXIT_CODE=$?
            log "VBoxHeadless exited with code $EXIT_CODE (state: $(get_vm_state))"
            sleep 5
            ;;
    esac
done
EOF

# 賦予執行權限
chmod +x ~/.local/bin/homeassistant-vm-watchdog.sh

Step 2:建立 launchd Plist

# 建立 plist 設定檔
cat > ~/Library/LaunchAgents/com.user.homeassistant-vm.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.homeassistant-vm</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/mansionlai/.local/bin/homeassistant-vm-watchdog.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>ThrottleInterval</key>
    <integer>30</integer>
    <key>StandardOutPath</key>
    <string>/dev/null</string>
    <key>StandardErrorPath</key>
    <string>/Users/mansionlai/Library/Logs/homeassistant-vm-error.log</string>
</dict>
</plist>
EOF

Step 3:載入並啟用

# 載入 launchd agent(立即生效,重開機也自動載入)
launchctl load ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

# 確認已載入(應顯示 PID 和 0 exit code)
launchctl list | grep homeassistant
# 輸出範例: 21006    0    com.user.homeassistant-vm

日常管理指令

查看狀態與 Log

# 查看 watchdog 是否在運行(第一欄為 PID)
launchctl list | grep homeassistant

# 即時查看 log(Ctrl+C 停止)
tail -f ~/Library/Logs/homeassistant-vm.log

# 查看最後 20 行 log
tail -20 ~/Library/Logs/homeassistant-vm.log

# 查看錯誤 log
tail -f ~/Library/Logs/homeassistant-vm-error.log

# 查看 VM 目前狀態
VBoxManage showvminfo HomeAssistant --machinereadable | grep VMState

停止 / 重啟 VM

# ── 優雅關機(ACPI 電源按鈕,HA 會自行 shutdown)──────
VBoxManage controlvm HomeAssistant acpipowerbutton
# ⚠️ Watchdog 偵測到 VM stopped 後會自動重啟!
# 若要永久停止,先 unload 再關機:

# ── 永久停止(停用 watchdog + 關閉 VM)────────────────
launchctl unload ~/Library/LaunchAgents/com.user.homeassistant-vm.plist
VBoxManage controlvm HomeAssistant acpipowerbutton

# ── 強制關閉 VM(緊急用)────────────────────────────
launchctl unload ~/Library/LaunchAgents/com.user.homeassistant-vm.plist
VBoxManage controlvm HomeAssistant poweroff

啟用 / 停用 Watchdog

# 停用 watchdog(下次開機不再自動啟動)
launchctl unload ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

# 重新啟用 watchdog
launchctl load ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

# 重啟 watchdog(修改 plist 後需執行)
launchctl unload ~/Library/LaunchAgents/com.user.homeassistant-vm.plist
launchctl load ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

測試 Crash Recovery

# Step 1:確認 VM 正在運行
VBoxManage showvminfo HomeAssistant --machinereadable | grep VMState
# 預期: VMState="running"

# Step 2:模擬 crash(強制斷電)
VBoxManage controlvm HomeAssistant poweroff

# Step 3:等待 ~20 秒後確認 VM 已自動重啟
sleep 20 && VBoxManage showvminfo HomeAssistant --machinereadable | grep VMState
# 預期: VMState="running"

# Step 4:查看 log 確認 watchdog 偵測到 crash 並重啟
tail -10 ~/Library/Logs/homeassistant-vm.log
# 預期輸出:
# 2026-04-11 XX:XX:XX [homeassistant-watchdog] VM state is 'aborted', launching headless...
# 2026-04-11 XX:XX:XX [homeassistant-watchdog] VBoxHeadless exited with code 0 ...

疑難排解

# Watchdog 未啟動:檢查 plist 語法
plutil -lint ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

# 查看 launchd 載入錯誤
launchctl error $(launchctl list | grep homeassistant | awk '{print $3}') 2>/dev/null

# 手動測試腳本(不透過 launchd)
bash ~/.local/bin/homeassistant-vm-watchdog.sh

# 清除 log 並重新追蹤
> ~/Library/Logs/homeassistant-vm.log
tail -f ~/Library/Logs/homeassistant-vm.log

# VBoxManage 找不到 VM:確認 VM 名稱
VBoxManage list vms

檔案位置速查

檔案 路徑
Watchdog 腳本 ~/.local/bin/homeassistant-vm-watchdog.sh
launchd plist ~/Library/LaunchAgents/com.user.homeassistant-vm.plist
正常 log ~/Library/Logs/homeassistant-vm.log
錯誤 log ~/Library/Logs/homeassistant-vm-error.log

Cloudflare Tunnel 管理指令

設定日期:2026-07-26

查看狀態

# 確認 cloudflared 是否正在執行(第一欄為 PID)
launchctl list | grep cloudflare

# 查看即時 tunnel log
tail -f ~/.cloudflared/tunnel.log

# 確認 Tunnel 對外連線正常
curl -si https://ha.mansionlai.com/ | head -5

停止 / 重啟 Tunnel

# 停止 Tunnel(會同時停止監控 VM 的 Watchdog)
launchctl unload ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

# 重新啟動 Tunnel 和 Watchdog
launchctl load ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

# 查詢現有 Tunnel 清單
cloudflared tunnel list

完整移除 Cloudflare Tunnel

# 停止 LaunchAgent
launchctl unload ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

# 從 watchdog 腳本中移除 cloudflared 相關程式碼,再重新載入
# (修改 ~/.local/bin/homeassistant-vm-watchdog.sh)
launchctl load ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

# 移除 Cloudflare 設定檔
rm -rf ~/.cloudflared

# (選用)在 Cloudflare Dashboard 刪除 Tunnel 和 DNS 紀錄

檔案位置速查

檔案 路徑
Tunnel 設定 ~/.cloudflared/config.yml
Tunnel 金鑰 ~/.cloudflared/a430f2b4-fd44-4967-abee-b9a37938cd1a.json
Cloudflare 憑證 ~/.cloudflared/cert.pem
LaunchAgent plist ~/Library/LaunchAgents/com.user.homeassistant-vm.plist

參考資料