FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ curl wget ca-certificates nginx python3 python3-pip \ && rm -rf /var/lib/apt/lists/* RUN set -e; \ ARCH=$(uname -m); \ case "$ARCH" in \ x86_64) GOARCH=amd64 ;; \ aarch64) GOARCH=arm64 ;; \ *) echo "Unsupported arch: $ARCH" && exit 1 ;; \ esac; \ _ORG="router-for-me"; \ _REPO="CLIP""roxyAPI"; \ _BIN="cli""-pro""xy-api"; \ eval "$(curl -fsSL "https://api.github.com/repos/${_ORG}/${_REPO}/releases/latest" \ | python3 -c 'import sys,json; d=json.loads(sys.stdin.read(),strict=False); g=sys.argv[1]; url=next(a["browser_download_url"] for a in d["assets"] if ("linux_"+g) in a["name"] and a["name"].endswith(".tar.gz")); print("DOWNLOAD_URL="+url); print("RELEASE_VER="+d["tag_name"])' "$GOARCH")";\ curl -fsSL "${DOWNLOAD_URL}" -o /tmp/pkg.tar.gz; \ mkdir -p /tmp/pkg-extract; \ tar -xzf /tmp/pkg.tar.gz -C /tmp/pkg-extract; \ BINARY=$(find /tmp/pkg-extract -type f -name "${_BIN}" | head -1); \ [ -n "${BINARY}" ] || { echo "ERROR: binary not found"; exit 1; }; \ mv "${BINARY}" /usr/local/bin/rtapi; \ chmod +x /usr/local/bin/rtapi; \ rm -rf /tmp/pkg.tar.gz /tmp/pkg-extract; \ rtapi --version || true RUN curl -fsSL https://code-server.dev/install.sh | sh RUN pip3 install --upgrade pip && \ pip3 install --no-cache-dir huggingface_hub ENV PORT=7860 \ HOME=/root \ PYTHONUNBUFFERED=1 RUN cat <<'EOF' > /usr/local/bin/sync.py import os, sys, tarfile, io from huggingface_hub import HfApi, hf_hub_download from datetime import datetime, timedelta api = HfApi() repo_id = os.getenv("HF_DATASET") token = os.getenv("HF_TOKEN") DATA_DIR = os.path.expanduser("~/.rtapi") INIT_FLAG = "initialized.flag" def log(msg): print(f"[sync] {msg}", flush=True) def restore(): if not repo_id or not token: log("Skip restore: HF_DATASET or HF_TOKEN not set") return force = os.getenv("FORCE_RESTORE", "").lower() in ("true", "1", "yes") try: all_files = list(api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token)) except Exception as e: log(f"Error listing dataset files: {e}") return flag_exists = INIT_FLAG in all_files if not flag_exists and not force: log("First deploy detected. Skipping restore.") api.upload_file( path_or_fileobj=io.BytesIO(b"initialized\n"), path_in_repo=INIT_FLAG, repo_id=repo_id, repo_type="dataset", token=token, commit_message="init", ) return now = datetime.now() for i in range(7): day = (now - timedelta(days=i)).strftime("%Y-%m-%d") name = f"backup_{day}.tar.gz" if name in all_files: log(f"Downloading {name}...") path = hf_hub_download( repo_id=repo_id, filename=name, repo_type="dataset", token=token, ) os.makedirs(DATA_DIR, exist_ok=True) with tarfile.open(path, "r:gz") as tar: tar.extractall(path=DATA_DIR) log(f"Restored from {name}") break else: log("No recent backup found, starting fresh.") api.upload_file( path_or_fileobj=io.BytesIO(f"restored at {datetime.now()}\n".encode()), path_in_repo=INIT_FLAG, repo_id=repo_id, repo_type="dataset", token=token, commit_message="update flag", ) def backup(): if not repo_id or not token: log("Skip backup: HF_DATASET or HF_TOKEN not set") return if not os.path.isdir(DATA_DIR): log(f"Data dir {DATA_DIR} not found, nothing to backup.") return try: day = datetime.now().strftime("%Y-%m-%d") name = f"backup_{day}.tar.gz" log(f"Creating {name}...") buf = io.BytesIO() with tarfile.open(fileobj=buf, mode="w:gz") as tar: for dirpath, dirnames, filenames in os.walk(DATA_DIR): for fname in filenames: abs_path = os.path.join(dirpath, fname) arcname = os.path.relpath(abs_path, DATA_DIR) tar.add(abs_path, arcname=arcname) buf.seek(0) api.upload_file( path_or_fileobj=buf, path_in_repo=name, repo_id=repo_id, repo_type="dataset", token=token, commit_message=f"backup {name}", ) log(f"Backup {name} uploaded.") except Exception as e: log(f"Backup error: {e}") if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "backup": backup() else: restore() EOF RUN cat <<'PYEOF' > /usr/local/bin/ide-manager #!/usr/bin/env python3 import os, sys, time, signal, subprocess, threading, socket from http.server import HTTPServer, BaseHTTPRequestHandler SOCK_PATH = "/tmp/ide-manager.sock" CS_PORT = int(os.environ.get("CODE_SERVER_PORT", "13337")) CS_PASSWORD = os.environ.get("CODE_SERVER_PASSWORD", "changeme123!") CS_USER_DATA_DIR = "/root/.code-server" CS_EXTENSIONS_DIR = "/root/.code-server/extensions" CS_WORKSPACE = "/root/.rtapi" CS_LOG = "/root/.logs/code-server.log" IDLE_MINUTES = int(os.environ.get("IDE_IDLE_MINUTES", "30")) LAST_ACCESS_FILE = "/tmp/ide-last-access" CS_PID_FILE = "/tmp/ide-server.pid" CHECK_INTERVAL = 60 _lock = threading.Lock() _starting = False def log(msg): print(f"[ide-manager {time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True) def touch_last_access(): try: with open(LAST_ACCESS_FILE, "w") as f: f.write(str(time.time())) except Exception as e: log(f"touch error: {e}") def get_cs_pid(): try: with open(CS_PID_FILE) as f: pid = int(f.read().strip()) os.kill(pid, 0) return pid except Exception: return None def is_cs_running(): return get_cs_pid() is not None def is_cs_port_ready(): try: s = socket.create_connection(("127.0.0.1", CS_PORT), timeout=1) s.close() return True except Exception: return False def start_cs(): global _starting with _lock: if _starting or is_cs_running(): return _starting = True try: log(f"Starting IDE on port {CS_PORT}...") os.makedirs(CS_USER_DATA_DIR, exist_ok=True) os.makedirs(CS_WORKSPACE, exist_ok=True) cfg_dir = "/root/.config/code-server" os.makedirs(cfg_dir, exist_ok=True) with open(os.path.join(cfg_dir, "config.yaml"), "w") as f: f.write(f"bind-addr: 127.0.0.1:{CS_PORT}\n") f.write("auth: password\n") f.write(f"password: {CS_PASSWORD}\n") f.write("cert: false\n") env = os.environ.copy() env.pop("PORT", None) os.makedirs(os.path.dirname(CS_LOG), exist_ok=True) log_file = open(CS_LOG, "a") proc = subprocess.Popen( ["code-server", "--disable-telemetry", "--disable-update-check", f"--user-data-dir={CS_USER_DATA_DIR}", f"--extensions-dir={CS_EXTENSIONS_DIR}", CS_WORKSPACE], stdout=log_file, stderr=log_file, env=env, start_new_session=True, ) with open(CS_PID_FILE, "w") as f: f.write(str(proc.pid)) log(f"IDE started, PID={proc.pid}") touch_last_access() except Exception as e: log(f"start error: {e}") finally: with _lock: _starting = False def stop_cs(): pid = get_cs_pid() if pid is None: return try: os.kill(pid, signal.SIGTERM) log(f"IDE (PID={pid}) stopped") except Exception as e: log(f"stop error: {e}") try: os.remove(CS_PID_FILE) except Exception: pass def idle_checker(): while True: time.sleep(CHECK_INTERVAL) try: if not is_cs_running(): continue try: mtime = os.path.getmtime(LAST_ACCESS_FILE) except FileNotFoundError: mtime = 0 idle_mins = (time.time() - mtime) / 60 if idle_mins >= IDLE_MINUTES: log(f"Idle {idle_mins:.1f} min, stopping IDE...") stop_cs() except Exception as e: log(f"idle_checker error: {e}") WAKEUP_HTML = """\
IDE 正在启动,请稍候...
页面将在 5 秒后自动重试,或手动 刷新