CPU温度を監視しクロックを制限する「CPU Temperature Monitor」

これから暑くなっていくので、CPU温度を監視して、指定温度になったらクロックを抑える監視モニターを作ってみました。

出来るだけホスト環境を汚さないようにLXDコンテナを作成して行うようにしています。

スクリプトが自動で行うこと

  • コンテナ名の確認 — 作成するLXDコンテナ名を対話的に入力
  • 前提条件チェック — LXD、lm-sensors、curl が無ければインストール
  • ホスト側ファイル生成 — throttle.shweb_server.py、systemd service ファイルを /opt/script/cpu/ に配置
  • LXDコンテナ作成 — Ubuntu 26.04(非特権)+ /sys 読み取り専用マウント + 共有ディレクトリ(ホストへのポート転送は無し)
  • コンテナ内セットアップ — Python3 + Webサーバ + Tailscale インストール・有効化 + systemd サービス設定
  • ホスト側systemd有効化 — 再起動時に自動起動
  • 動作確認 — コンテナ状態、コンテナ内Webサーバ応答、Tailscale状態、サービス状態を表示
  • コンテナ内シェルに入る — 最後に lxc exec <コンテナ名> -- bash を実行した状態で終了。ここで手動でTailscaleの認証を行う

設定値

変数デフォルト説明
CONTAINER_NAMEcpuLXDコンテナ名(実行時に対話入力でも変更可)
WEB_PORT3336Webダッシュボードのポート(コンテナ内)
THRESHOLD80温度閾値の初期値(℃)※後からWebで変更可能
THROTTLE_FREQ1800000制限時のクロック(kHz)

構成

ホスト
├── /opt/script/cpu/throttle.sh          ← 温度監視・クロック制限(実際の書き換えはここ)
├── /opt/script/cpu/web_server.py        ← Webダッシュボード(コンテナへコピーされる)
├── /opt/script/cpu/threshold.conf       ← 温度閾値(Webから変更可能)
├── /opt/script/cpu/throttle.log         ← ログ
└── /opt/script/cpu/cpu-throttle.service ← systemd設定(ホスト側)

LXDコンテナ(非特権, Ubuntu 26.04)
├── /host/sys/          ← ホストの/sys(読み取り専用マウント)
├── /opt/script/cpu/    ← 共有ディレクトリ(ホストと同一パス)
├── Tailscale           ← MagicDNS経由のアクセス用(手動認証)
└── Webサーバ (port 3336, ホストへのポート転送なし)

インストール

実行するとまずコンテナ名の入力を求められます(Enterでデフォルトの cpu を使用)。

sudo mkdir -p /opt/script
sudo chown $USER:$USER /opt/script
chmod u+rwx /opt/script
cd /opt/script
nano install-cpucheck1.sh
sudo bash install-cpucheck1.sh
#!/usr/bin/env bash
# ============================================================================
# CPU温度監視環境 セットアップスクリプト
# LXDコンテナ (Ubuntu 26.04) + Webダッシュボード + CPUスロットル制御
# Tailscale経由でアクセス(ホスト側ポート転送なし)
# ============================================================================
set -euo pipefail

# --- 設定(デフォルト値) ---
CONTAINER_NAME="cpu"
WEB_PORT=3336
THRESHOLD=80
THROTTLE_FREQ=1800000   # 制限時クロック (kHz)
INSTALL_DIR="/opt/script/cpu"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# --- カラー出力 ---
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${CYAN}[INFO]${NC} $*"; }
ok()    { echo -e "${GREEN}[OK]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
err()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }

# --- コンテナ名の確認 ---
prompt_container_name() {
    echo ""
    read -p "作成するLXDコンテナ名を入力してください [デフォルト: ${CONTAINER_NAME}]: " input_name
    if [[ -n "$input_name" ]]; then
        CONTAINER_NAME="$input_name"
    fi
    ok "コンテナ名: ${CONTAINER_NAME}"
}

# --- 前提チェック ---
check_prereqs() {
    info "前提条件をチェック中..."

    if [[ $EUID -ne 0 ]]; then
        err "sudo で実行してください: sudo $0"
        exit 1
    fi

    # LXD
    if ! command -v lxd &>/dev/null && ! command -v lxc &>/dev/null; then
        warn "LXD がインストールされていません。インストールします..."
        if command -v snap &>/dev/null; then
            snap install lxd
            lxd init --auto
        else
            err "snap が見つかりません。手動でLXDをインストールしてください。"
            exit 1
        fi
    fi
    ok "LXD: $(lxd --version 2>/dev/null || snap list lxd 2>/dev/null | awk 'NR==2{print $2}')"

    # lm-sensors
    if ! command -v sensors &>/dev/null; then
        warn "lm-sensors がインストールされていません。インストールします..."
        apt-get install -y lm-sensors
        sensors-detect --auto 2>/dev/null || true
    fi
    ok "lm-sensors: $(sensors --version 2>/dev/null | head -1)"

    # curl
    if ! command -v curl &>/dev/null; then
        apt-get install -y curl
    fi
    ok "curl: $(curl --version | head -1)"

    echo ""
}

# --- ファイル生成 ---
generate_throttle_script() {
    mkdir -p "$INSTALL_DIR"
    cat > "$INSTALL_DIR/throttle.sh" << 'THROTTLE_EOF'
#!/usr/bin/env bash
set -euo pipefail

DEFAULT_THRESHOLD=__THRESHOLD__
THROTTLE_FREQ=__THROTTLE_FREQ__
INTERVAL=2
LOG_FILE="__INSTALL_DIR__/throttle.log"
CTRL_FILE="__INSTALL_DIR__/throttle_ctrl"
THRESHOLD_FILE="__INSTALL_DIR__/threshold.conf"

log() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
    echo "$msg"
    echo "$msg" >> "$LOG_FILE"
}

get_threshold() {
    if [[ -f "$THRESHOLD_FILE" ]]; then
        local v
        v=$(cat "$THRESHOLD_FILE" 2>/dev/null | tr -d '[:space:]')
        if [[ "$v" =~ ^[0-9]+$ ]]; then
            echo "$v"
            return
        fi
    fi
    echo "$DEFAULT_THRESHOLD"
}

get_temp() {
    sensors 2>/dev/null | awk '/Package id 0/ {gsub(/[^0-9.]/, "", $3); printf "%d", $3}'
}

get_hw_max_freq() {
    cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
}

get_cur_max_freq() {
    cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
}

get_cpu_count() {
    ls -d /sys/devices/system/cpu/cpu[0-9]* | wc -l
}

set_all_max_freq() {
    local freq=$1
    local count
    count=$(get_cpu_count)
    for ((i = 0; i < count; i++)); do
        echo "$freq" > "/sys/devices/system/cpu/cpu${i}/cpufreq/scaling_max_freq"
    done
}

do_throttle() {
    local cur hw
    cur=$(get_cur_max_freq)
    hw=$(get_hw_max_freq)
    if [[ "$cur" -eq "$hw" ]]; then
        log "手動制限: ${THROTTLE_FREQ} kHz に設定"
        set_all_max_freq "$THROTTLE_FREQ"
    fi
}

do_restore() {
    local hw
    hw=$(get_hw_max_freq)
    log "手動復帰: ${hw} kHz に設定"
    set_all_max_freq "$hw"
}

check_ctrl() {
    if [[ -f "$CTRL_FILE" ]]; then
        local cmd
        cmd=$(cat "$CTRL_FILE" 2>/dev/null || true)
        rm -f "$CTRL_FILE"
        case "$cmd" in
            throttle) do_throttle ;;
            restore)  do_restore ;;
        esac
    fi
}

if [[ $EUID -ne 0 ]]; then
    echo "エラー: sudo で実行してください" >&2
    exit 1
fi

# 初回起動時に閾値ファイルが無ければデフォルト値で作成
if [[ ! -f "$THRESHOLD_FILE" ]]; then
    echo "$DEFAULT_THRESHOLD" > "$THRESHOLD_FILE"
fi

HW_MAX=$(get_hw_max_freq)
CPU_COUNT=$(get_cpu_count)
THROTTLED=false
LAST_THRESHOLD=$(get_threshold)

log "=== CPU温度監視スクリプト開始 ==="
log "CPU数: ${CPU_COUNT}, 最大クロック: ${HW_MAX} kHz"
log "制限クロック: ${THROTTLE_FREQ} kHz, 初期閾値: ${LAST_THRESHOLD}℃"

trap 'log "スクリプト停止"; exit 0' INT TERM

while true; do
    check_ctrl
    THRESHOLD=$(get_threshold)
    if [[ "$THRESHOLD" != "$LAST_THRESHOLD" ]]; then
        log "閾値変更を検知: ${LAST_THRESHOLD}℃ → ${THRESHOLD}℃"
        LAST_THRESHOLD="$THRESHOLD"
    fi
    TEMP=$(get_temp)
    if [[ -z "$TEMP" ]]; then
        log "警告: 温度の取得に失敗"
        sleep "$INTERVAL"
        continue
    fi
    if [[ "$THROTTLED" == false && "$TEMP" -ge "$THRESHOLD" ]]; then
        log "温度 ${TEMP}℃ >= ${THRESHOLD}℃ → 制限開始 (${THROTTLE_FREQ} kHz)"
        set_all_max_freq "$THROTTLE_FREQ"
        THROTTLED=true
    elif [[ "$THROTTLED" == true && "$TEMP" -lt "$THRESHOLD" ]]; then
        log "温度 ${TEMP}℃ < ${THRESHOLD}℃ → 復帰 (${HW_MAX} kHz)"
        set_all_max_freq "$HW_MAX"
        THROTTLED=false
    fi
    sleep "$INTERVAL"
done
THROTTLE_EOF

    # プレースホルダー置換
    sed -i "s|__THRESHOLD__|${THRESHOLD}|g" "$INSTALL_DIR/throttle.sh"
    sed -i "s|__THROTTLE_FREQ__|${THROTTLE_FREQ}|g" "$INSTALL_DIR/throttle.sh"
    sed -i "s|__INSTALL_DIR__|${INSTALL_DIR}|g" "$INSTALL_DIR/throttle.sh"
    chmod +x "$INSTALL_DIR/throttle.sh"
    ok "throttle.sh を生成: $INSTALL_DIR/throttle.sh"
}

generate_web_server() {
    cat > "$INSTALL_DIR/web_server.py" << 'WEB_EOF'
#!/usr/bin/env python3
"""CPU温度監視 Webダッシュボード"""
import http.server, time
from pathlib import Path
from urllib.parse import urlparse, parse_qs

PORT = __WEB_PORT__
SYS_ROOT = Path("/host/sys")
SHARED_DIR = Path("__INSTALL_DIR__")
CTRL_FILE = SHARED_DIR / "throttle_ctrl"
THRESHOLD_FILE = SHARED_DIR / "threshold.conf"
DEFAULT_THRESHOLD = __THRESHOLD__

def get_threshold():
    try:
        v = THRESHOLD_FILE.read_text().strip()
        return int(v)
    except Exception:
        return DEFAULT_THRESHOLD

def set_threshold(v):
    THRESHOLD_FILE.write_text(f"{v}\n")

def get_cpu_freq():
    result = {}
    cpu_base = SYS_ROOT / "devices" / "system" / "cpu"
    if not cpu_base.exists(): return result
    for d in sorted(cpu_base.iterdir()):
        if not d.name.startswith("cpu"): continue
        cp = d / "cpufreq"
        if not cp.exists(): continue
        try:
            result[d.name] = {
                "cur": int((cp/"scaling_cur_freq").read_text().strip())//1000,
                "max": int((cp/"scaling_max_freq").read_text().strip())//1000,
                "hw":  int((cp/"cpuinfo_max_freq").read_text().strip())//1000,
            }
        except: pass
    return result

def get_temps():
    temps = {}
    hb = SYS_ROOT / "class" / "hwmon"
    if not hb.exists(): return temps
    for hw in sorted(hb.iterdir()):
        nf = hw / "name"
        if not nf.exists(): continue
        nm = nf.read_text().strip()
        if nm not in ("coretemp","acpitz"): continue
        for f in sorted(hw.iterdir()):
            if not (f.name.startswith("temp") and f.name.endswith("_input")): continue
            try:
                v = int(f.read_text().strip())/1000
                lf = hw / f.name.replace("_input","_label")
                lb = lf.read_text().strip() if lf.exists() else f.name
                temps[lb] = round(v,1)
            except: pass
    return temps

def get_state():
    lf = SHARED_DIR / "throttle.log"
    if not lf.exists(): return False
    try:
        for l in reversed(lf.read_text().strip().split("\n")):
            if "制限開始" in l: return True
            if "復帰" in l: return False
    except: pass
    return False

def get_logs(n=8):
    lf = SHARED_DIR / "throttle.log"
    if not lf.exists(): return "---"
    try: return "\n".join(lf.read_text().strip().split("\n")[-n:])
    except: return "---"

def build():
    ci = get_cpu_freq(); tp = get_temps(); th = get_state(); lg = get_logs()
    cur_threshold = get_threshold()
    cr = ""
    for nm,inf in ci.items():
        p = int(inf["cur"]/inf["hw"]*100) if inf["hw"]>0 else 0
        c = "#4caf50" if p<60 else "#ff9800" if p<85 else "#f44336"
        cr += f'<tr><td>{nm}</td><td>{inf["cur"]} MHz</td><td>{inf["max"]} MHz</td><td><div class="bb"><div class="b" style="width:{p}%;background:{c}"></div></div></td></tr>'
    tr = ""
    for lb,v in tp.items():
        c = "#4caf50" if v<60 else "#ff9800" if v<80 else "#f44336"
        tr += f'<tr><td>{lb}</td><td style="color:{c};font-weight:bold">{v}°C</td></tr>'
    st = "THROTTLED" if th else "NORMAL"
    sc = "#f44336" if th else "#4caf50"
    bl = "Restore Full Speed" if th else "Throttle Now"
    ba = "restore" if th else "throttle"
    bc = "#4caf50" if th else "#f44336"
    return f'''<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CPU Monitor</title><meta http-equiv="refresh" content="10"><style>
*{{margin:0;padding:0;box-sizing:border-box}}body{{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0f0f1a;color:#ccc;padding:20px}}
h1{{text-align:center;margin-bottom:16px;color:#00d4ff;font-size:1.4em}}
.st{{text-align:center;font-size:1.1em;margin-bottom:16px;padding:8px;border-radius:6px;background:{sc}22;color:{sc}}}
.bt{{display:block;margin:0 auto 16px;padding:8px 24px;border:none;border-radius:6px;background:{bc};color:#fff;font-size:1em;cursor:pointer}}
.bt:hover{{opacity:.85}}.g{{display:grid;grid-template-columns:1fr 1fr;gap:16px;max-width:860px;margin:0 auto}}
.c{{background:#161625;border-radius:10px;padding:16px;border:1px solid #252540}}
.c h2{{font-size:.9em;margin-bottom:10px;color:#00d4ff;border-bottom:1px solid #252540;padding-bottom:6px}}
table{{width:100%;border-collapse:collapse}}th,td{{padding:6px 10px;text-align:left;font-size:.85em}}th{{color:#666;font-weight:normal}}
.bb{{background:#252540;border-radius:3px;height:18px;width:100%}}.b{{height:100%;border-radius:3px;transition:width .5s}}
.lb{{background:#0a0a14;border-radius:8px;padding:12px;font-family:monospace;font-size:.75em;white-space:pre-wrap;max-height:180px;overflow-y:auto;margin-top:16px;max-width:860px;margin-left:auto;margin-right:auto;border:1px solid #252540}}
.lt{{text-align:center;color:#666;margin-top:16px;margin-bottom:4px;font-size:.85em}}
.th{{max-width:860px;margin:0 auto 16px;text-align:center}}
.th input{{width:70px;padding:6px;border-radius:4px;border:1px solid #252540;background:#0a0a14;color:#ccc;text-align:center;font-size:1em}}
.th button{{padding:6px 16px;border:none;border-radius:4px;background:#00d4ff;color:#0f0f1a;font-weight:bold;cursor:pointer;margin-left:6px}}
@media(max-width:600px){{.g{{grid-template-columns:1fr}}}}</style></head><body>
<h1>CPU Temperature Monitor</h1>
<div class="st">{st}</div>
<button class="bt" onclick="fetch('/api/{ba}').then(()=>location.reload())">{bl}</button>
<div class="th">閾値: <input type="number" id="th_input" value="{cur_threshold}" min="30" max="100">℃
<button onclick="setThreshold()">適用</button></div>
<div class="g"><div class="c"><h2>Temperature</h2><table>{tr}</table></div>
<div class="c"><h2>CPU Frequency</h2><table><tr><th>Core</th><th>Current</th><th>Limit</th><th></th></tr>{cr}</table></div></div>
<div class="lt">Throttle Log</div><div class="lb">{lg}</div>
<script>
function setThreshold() {{
  const v = document.getElementById('th_input').value;
  fetch('/api/set_threshold?value=' + encodeURIComponent(v)).then(()=>location.reload());
}}
</script>
</body></html>'''

class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urlparse(self.path)
        p = parsed.path
        if p=="/api/throttle":
            CTRL_FILE.write_text("throttle\n")
            self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers()
            self.wfile.write(b'{"ok":true}')
        elif p=="/api/restore":
            CTRL_FILE.write_text("restore\n")
            self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers()
            self.wfile.write(b'{"ok":true}')
        elif p=="/api/set_threshold":
            qs = parse_qs(parsed.query)
            try:
                v = int(qs.get("value", [""])[0])
                if 0 < v < 150:
                    set_threshold(v)
                    ok = True
                else:
                    ok = False
            except Exception:
                ok = False
            self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers()
            self.wfile.write(f'{{"ok":{str(ok).lower()}}}'.encode())
        else:
            self.send_response(200); self.send_header("Content-Type","text/html;charset=utf-8"); self.end_headers()
            self.wfile.write(build().encode())
    def log_message(self,*a): pass

if __name__=="__main__":
    s=http.server.HTTPServer(("0.0.0.0",PORT),H)
    print(f"CPU Monitor on http://0.0.0.0:{PORT}"); s.serve_forever()
WEB_EOF
    sed -i "s|__WEB_PORT__|${WEB_PORT}|g" "$INSTALL_DIR/web_server.py"
    sed -i "s|__INSTALL_DIR__|${INSTALL_DIR}|g" "$INSTALL_DIR/web_server.py"
    sed -i "s|__THRESHOLD__|${THRESHOLD}|g" "$INSTALL_DIR/web_server.py"
    ok "web_server.py を生成: $INSTALL_DIR/web_server.py"
}

generate_service_file() {
    cat > "$INSTALL_DIR/cpu-throttle.service" << SVC_EOF
[Unit]
Description=CPU Temperature Throttle Service
After=multi-user.target

[Service]
Type=simple
ExecStart=${INSTALL_DIR}/throttle.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
SVC_EOF
    ok "cpu-throttle.service を生成: $INSTALL_DIR/cpu-throttle.service"
}

# --- LXDコンテナ setup ---
setup_container() {
    info "LXDコンテナ '${CONTAINER_NAME}' をセットアップ中..."

    # 既存コンテナがあれば確認
    if lxc info "$CONTAINER_NAME" &>/dev/null; then
        warn "コンテナ '${CONTAINER_NAME}' は既に存在します"
        read -p "削除して再作成しますか? (y/N): " confirm
        if [[ "$confirm" =~ ^[yY]$ ]]; then
            lxc stop "$CONTAINER_NAME" 2>/dev/null || true
            lxc delete "$CONTAINER_NAME" --force
            ok "既存コンテナを削除しました"
        else
            info "既存コンテナを使用します"
            return 0
        fi
    fi

    # コンテナ作成(Ubuntu 26.04)
    info "Ubuntu 26.04 コンテナを作成中..."
    lxc init ubuntu:26.04 "$CONTAINER_NAME"

    # 共有ディレクトリ・sysfs読み取り専用マウントのみ設定
    # 実際のクロック書き換えはホスト側のcpu-throttle.serviceが行うため、
    # コンテナに特権(security.privileged)は不要。
    mkdir -p "$INSTALL_DIR"
    chmod 777 "$INSTALL_DIR"
    lxc config device add "$CONTAINER_NAME" sysfs disk source=/sys path=/host/sys readonly=true
    lxc config device add "$CONTAINER_NAME" shared_dir disk source="$INSTALL_DIR" path="$INSTALL_DIR"

    ok "コンテナ設定完了(非特権, /sys読み取り専用マウント, ホスト側ポート転送なし)"

    # 起動
    info "コンテナを起動中..."
    lxc start "$CONTAINER_NAME"
    sleep 8

    # ネットワーク設定(DHCPが効かない場合のフォールバック)
    lxc exec "$CONTAINER_NAME" -- bash -c "
ip addr add 10.65.26.100/24 dev eth0 2>/dev/null || true
ip link set eth0 up
ip route add default via 10.65.26.1 2>/dev/null || true
echo 'nameserver 8.8.8.8' > /etc/resolv.conf
echo 'nameserver 8.8.4.4' >> /etc/resolv.conf
" 2>/dev/null || true

    # Python + Tailscaleセットアップ
    info "コンテナ内でパッケージをインストール中..."
    lxc exec "$CONTAINER_NAME" -- bash -c "
apt-get update -qq 2>/dev/null
apt-get install -y -qq python3 curl 2>/dev/null
" 2>/dev/null || true

    info "コンテナ内にTailscaleをインストール中..."
    lxc exec "$CONTAINER_NAME" -- bash -c "
curl -fsSL https://tailscale.com/install.sh | sh
systemctl enable --now tailscaled
" 2>/dev/null || warn "Tailscaleのインストールに失敗した可能性があります。手動で確認してください。"

    ok "コンテナセットアップ完了"
}

# --- コンテナ内サービス setup ---
setup_container_service() {
    info "コンテナ内systemdサービスを設定中..."

    lxc exec "$CONTAINER_NAME" -- bash -c "
cat > /etc/systemd/system/cpu-monitor.service << 'EOF'
[Unit]
Description=CPU Monitor Web Server
After=network.target

[Service]
Type=simple
WorkingDirectory=${INSTALL_DIR}
ExecStart=/usr/bin/python3 ${INSTALL_DIR}/web_server.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable cpu-monitor.service
" 2>/dev/null

    # Webサーバ起動
    lxc exec "$CONTAINER_NAME" -- systemctl start cpu-monitor.service 2>/dev/null || \
    lxc exec "$CONTAINER_NAME" -- bash -c "cd ${INSTALL_DIR} && nohup python3 web_server.py > /tmp/web.log 2>&1 &" 2>/dev/null

    ok "コンテナ内Webサーバ起動完了"
}

# --- ホスト側systemd setup ---
setup_host_service() {
    info "ホスト側systemdサービスを設定中..."

    cp "$INSTALL_DIR/cpu-throttle.service" /etc/systemd/system/cpu-throttle.service
    systemctl daemon-reload
    systemctl enable cpu-throttle.service

    ok "ホスト側throttleサービス有効化完了"
    warn "サービスを開始するには: sudo systemctl start cpu-throttle"
}

# --- 動作確認 ---
verify() {
    info "動作確認中..."

    echo ""
    echo -e "${CYAN}=== コンテナ状態 ===${NC}"
    lxc list "$CONTAINER_NAME" --format csv 2>/dev/null | head -1

    echo ""
    echo -e "${CYAN}=== Webサーバ(コンテナ内) ===${NC}"
    if lxc exec "$CONTAINER_NAME" -- curl -s --connect-timeout 3 "http://localhost:${WEB_PORT}" >/dev/null 2>&1; then
        ok "コンテナ内 http://localhost:${WEB_PORT} でアクセス可能"
    else
        warn "Webサーバにまだアクセスできません(起動待ちの可能性があります)"
    fi

    echo ""
    echo -e "${CYAN}=== Tailscale状態 ===${NC}"
    lxc exec "$CONTAINER_NAME" -- tailscale status 2>/dev/null || warn "Tailscale未認証(手動で 'sudo tailscale up' を実行してください)"

    echo ""
    echo -e "${CYAN}=== サービス状態 ===${NC}"
    systemctl is-enabled cpu-throttle.service 2>/dev/null && ok "cpu-throttle.service (host): enabled" || warn "cpu-throttle.service (host): 未設定"
    lxc exec "$CONTAINER_NAME" -- systemctl is-enabled cpu-monitor.service 2>/dev/null && ok "cpu-monitor.service (container): enabled" || warn "cpu-monitor.service (container): 未設定"

    echo ""
}

# --- メイン ---
main() {
    echo ""
    echo -e "${CYAN}╔══════════════════════════════════════════╗${NC}"
    echo -e "${CYAN}║  CPU温度監視環境 セットアップ           ║${NC}"
    echo -e "${CYAN}╚══════════════════════════════════════════╝${NC}"
    echo ""

    prompt_container_name
    check_prereqs
    generate_throttle_script
    generate_web_server
    generate_service_file
    setup_container
    setup_container_service
    setup_host_service
    verify

    echo ""
    echo -e "${GREEN}╔══════════════════════════════════════════╗${NC}"
    echo -e "${GREEN}║  セットアップ完了!                     ║${NC}"
    echo -e "${GREEN}╚══════════════════════════════════════════╝${NC}"
    echo ""
    echo "  ダッシュボード(コンテナ内ポート ${WEB_PORT})は"
    echo "  Tailscale MagicDNS経由でアクセスしてください。"
    echo ""
    echo "  1. これからコンテナ '${CONTAINER_NAME}' 内のシェルに入ります"
    echo "  2. コンテナ内で以下を実行してTailscaleを認証してください:"
    echo "       sudo tailscale up"
    echo "  3. 認証後、他のTailscaleデバイスから以下でアクセス可能です:"
    echo "       http://<コンテナのMagicDNS名>:${WEB_PORT}"
    echo ""
    echo "  ホスト側の温度監視サービス開始:"
    echo "    sudo systemctl start cpu-throttle"
    echo ""

    info "コンテナ '${CONTAINER_NAME}' のシェルに入ります..."
    lxc exec "$CONTAINER_NAME" -- bash
}

main "$@"

初回セットアップ後の手動操作(Tailscale認証)

セットアップ完了後、自動でコンテナ内シェルに入るので、そこで以下を実行。

tailscale up

表示されるURLでブラウザ認証を行うと、コンテナがTailscaleネットワークに参加します。認証後、コンテナのシェルからは exit で抜けられます。

また下記にもまとめていますが、ホスト側で監視サービスを起動します。

sudo systemctl start cpu-throttle

手動操作

bash
# 温度監視サービス開始(ホスト側)
sudo systemctl start cpu-throttle

# 温度監視サービス停止(ホスト側)
sudo systemctl stop cpu-throttle

# ダッシュボードアクセス(TailscaleのMagicDNS経由)
http://<コンテナのMagicDNS名>:3336

# コンテナに再度入る
lxc exec <コンテナ名> -- bash

閾値の変更

ダッシュボード上の入力欄に値を入力し「適用」ボタンを押すと、/opt/script/cpu/threshold.conf が更新され、次のホスト側監視ループ(最大2秒後)から新しい閾値が反映されます。

アンインストール

やることは以下の通りです。

  1. コンテナ名を入力(削除対象を明示的に確認)
  2. 削除内容を表示して最終確認(y/N)
  3. ホスト側 cpu-throttle.service を停止・無効化・ユニットファイル削除
  4. 念のため、制限がかかったままなら全コアのクロックをcpuinfo_max_freq(ハード上限)に復帰
  5. LXDコンテナを stop → delete –force(Tailscaleの認証情報ごと消えます)
  6. 共有ディレクトリ /opt/script/cpu(ログ・閾値設定含む)を削除

コンテナを消すとTailscaleのノード登録もローカル側からは消えますが、Tailscale管理コンソール側にデバイスが残ることがあるので、気になる場合は https://login.tailscale.com/admin/machines から手動で該当マシンを削除してください。

cd /opt/script
nano uninstall-cpucheck1.sh
sudo bash uninstall-cpucheck1.sh
#!/usr/bin/env bash
# ============================================================================
# CPU温度監視環境 アンインストールスクリプト
# ============================================================================
set -euo pipefail

INSTALL_DIR="/opt/script/cpu"

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${CYAN}[INFO]${NC} $*"; }
ok()    { echo -e "${GREEN}[OK]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
err()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }

if [[ $EUID -ne 0 ]]; then
    err "sudo で実行してください: sudo $0"
    exit 1
fi

echo ""
echo -e "${CYAN}╔══════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║  CPU温度監視環境 アンインストール       ║${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════╝${NC}"
echo ""

# --- コンテナ名の確認 ---
read -p "削除するLXDコンテナ名を入力してください: " CONTAINER_NAME
if [[ -z "$CONTAINER_NAME" ]]; then
    err "コンテナ名が入力されていません。中止します。"
    exit 1
fi

echo ""
warn "以下を削除します:"
echo "    - ホスト側サービス: cpu-throttle.service"
echo "    - LXDコンテナ: ${CONTAINER_NAME}"
echo "    - 共有ディレクトリ: ${INSTALL_DIR}(ログ・閾値設定含む)"
echo ""
read -p "本当に続行しますか? (y/N): " confirm
if [[ ! "$confirm" =~ ^[yY]$ ]]; then
    info "中止しました。"
    exit 0
fi
echo ""

# --- ホスト側 throttle サービス停止・削除 ---
info "ホスト側サービスを停止・削除中..."
if systemctl is-active --quiet cpu-throttle.service 2>/dev/null; then
    systemctl stop cpu-throttle.service
fi
if systemctl is-enabled --quiet cpu-throttle.service 2>/dev/null; then
    systemctl disable cpu-throttle.service
fi
if [[ -f /etc/systemd/system/cpu-throttle.service ]]; then
    rm -f /etc/systemd/system/cpu-throttle.service
fi
systemctl daemon-reload
ok "cpu-throttle.service を削除しました"

# 念のため、制限がかかったままの場合は最大クロックに復帰させる
if [[ -f /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq ]]; then
    HW_MAX=$(cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq)
    COUNT=$(ls -d /sys/devices/system/cpu/cpu[0-9]* | wc -l)
    for ((i = 0; i < COUNT; i++)); do
        echo "$HW_MAX" > "/sys/devices/system/cpu/cpu${i}/cpufreq/scaling_max_freq" 2>/dev/null || true
    done
    ok "CPUクロック制限を解除しました(全コア ${HW_MAX} kHz に復帰)"
fi

# --- LXDコンテナ削除 ---
info "LXDコンテナを削除中..."
if lxc info "$CONTAINER_NAME" &>/dev/null; then
    lxc stop "$CONTAINER_NAME" 2>/dev/null || true
    lxc delete "$CONTAINER_NAME" --force
    ok "コンテナ '${CONTAINER_NAME}' を削除しました"
else
    warn "コンテナ '${CONTAINER_NAME}' は見つかりませんでした(既に削除済みの可能性)"
fi

# --- 共有ディレクトリ削除 ---
info "共有ディレクトリを削除中..."
if [[ -d "$INSTALL_DIR" ]]; then
    rm -rf "$INSTALL_DIR"
    ok "${INSTALL_DIR} を削除しました"
else
    warn "${INSTALL_DIR} は見つかりませんでした"
fi

echo ""
echo -e "${GREEN}╔══════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║  アンインストール完了                   ║${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════╝${NC}"
echo ""
タイトルとURLをコピーしました