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 启动中...

IDE 正在启动,请稍候...

页面将在 5 秒后自动重试,或手动 刷新

""" class Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): pass def do_GET(self): if self.path.startswith("/wakeup"): self._handle_wakeup() elif self.path.startswith("/heartbeat"): self._handle_heartbeat() else: self.send_response(404) self.end_headers() def _handle_wakeup(self): if is_cs_port_ready(): touch_last_access() self.send_response(302) self.send_header("Location", "/ide/") self.end_headers() else: if not is_cs_running(): threading.Thread(target=start_cs, daemon=True).start() html = WAKEUP_HTML.encode() self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(html))) self.end_headers() self.wfile.write(html) def _handle_heartbeat(self): touch_last_access() self.send_response(204) self.end_headers() class UnixSocketHTTPServer(HTTPServer): address_family = socket.AF_UNIX def server_bind(self): try: os.unlink(self.server_address) except FileNotFoundError: pass super().server_bind() os.chmod(self.server_address, 0o666) if __name__ == "__main__": log(f"ide-manager starting (idle_timeout={IDLE_MINUTES} min, port={CS_PORT})") threading.Thread(target=idle_checker, daemon=True).start() server = UnixSocketHTTPServer(SOCK_PATH, Handler) try: server.serve_forever() except KeyboardInterrupt: server.server_close() PYEOF RUN chmod +x /usr/local/bin/ide-manager RUN cat <<'EOF' > /usr/local/bin/start-app #!/bin/bash set -e LISTEN_PORT="${PORT:-7860}" SVC_PORT=8317 CODE_SERVER_PORT="${CODE_SERVER_PORT:-13337}" IDE_IDLE_MINUTES="${IDE_IDLE_MINUTES:-30}" export CODE_SERVER_PORT IDE_IDLE_MINUTES export CODE_SERVER_PASSWORD="${CODE_SERVER_PASSWORD:-changeme123!}" DATA_DIR="/root/.rtapi" CONFIG_FILE="${DATA_DIR}/config.yaml" mkdir -p "${DATA_DIR}" python3 /usr/local/bin/sync.py restore if [ ! -f "${CONFIG_FILE}" ]; then cat > "${CONFIG_FILE}" <&1 | tee /root/.logs/svc.log & python3 /usr/local/bin/ide-manager 2>&1 | tee /root/.logs/ide.log & for i in $(seq 1 20); do [ -S /tmp/ide-manager.sock ] && break sleep 0.5 done (while true; do sleep 3600; python3 /usr/local/bin/sync.py backup; done) & for i in $(seq 1 30); do if curl -fsS http://127.0.0.1:${SVC_PORT}/ >/dev/null 2>&1; then break fi sleep 2 done rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf cat > /etc/nginx/conf.d/app.conf <