tao-shen commited on
Commit
a3bfafd
·
0 Parent(s):

HuggingRun: Run anything on Hugging Face — generic Docker + persistence

Browse files
Files changed (10) hide show
  1. .env.example +20 -0
  2. .gitignore +10 -0
  3. DESIGN.md +39 -0
  4. Dockerfile +35 -0
  5. README.md +76 -0
  6. app/demo_app.py +57 -0
  7. app/fastapi_sqlite.py +51 -0
  8. requirements.txt +1 -0
  9. scripts/entrypoint.sh +4 -0
  10. scripts/sync_hf.py +228 -0
.env.example ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingRun — set as Repository secrets in HF Space (or .env for local Docker)
2
+
3
+ # Required for persistence (optional for demo-only)
4
+ HF_TOKEN=
5
+ HF_DATASET_REPO=
6
+
7
+ # Optional: auto-create dataset repo on first run
8
+ AUTO_CREATE_DATASET=false
9
+
10
+ # Persistence path (default /data). Your app should read/write here.
11
+ PERSIST_PATH=/data
12
+
13
+ # Your app command. Empty = run default demo (HTTP server on 7860).
14
+ RUN_CMD=
15
+
16
+ # Sync interval in seconds
17
+ SYNC_INTERVAL=60
18
+
19
+ # Port for default demo (HF Spaces expect 7860)
20
+ PORT=7860
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .venv
3
+ __pycache__
4
+ *.pyc
5
+ .pytest_cache
6
+ .coverage
7
+ htmlcov/
8
+ *.egg-info/
9
+ dist/
10
+ build/
DESIGN.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingRun 设计
2
+
3
+ **Slogan:** Run anything on Hugging Face.
4
+
5
+ ## 目标
6
+
7
+ - 把 Hugging Face Spaces 当成**通用容器**:只要应用能 Docker 化,就能在 HF 上跑。
8
+ - 参考 HuggingClaw 的持久化与入口模式,做成**可配置**的通用运行时。
9
+ - 通过至少一个「难例」部署验证(如 FastAPI+SQLite 或带状态的 Web 应用)。
10
+
11
+ ## 架构
12
+
13
+ 1. **单一 Docker 镜像**
14
+ - 使用 `ARG BASE_IMAGE` 选择基础镜像(默认 `python:3.11-slim`)。
15
+ - 在基础镜像上安装 Python3 + `huggingface_hub`(若基础镜像无),并加入通用脚本。
16
+ - 通过 `ENV RUN_CMD` 指定要执行的命令(应用由用户通过 CMD/环境变量提供)。
17
+
18
+ 2. **入口脚本 `entrypoint.sh`**
19
+ - 调用 `sync_hf.py` 做启动时恢复与周期性上传(可选)。
20
+ - 最后 `exec` 执行 `RUN_CMD`(或 Dockerfile 的 CMD),使主进程为 PID 1。
21
+
22
+ 3. **持久化 `sync_hf.py`**
23
+ - 可配置持久化目录 `PERSIST_PATH`(默认 `/data`)。
24
+ - 可配置 HF Dataset repo:`HF_DATASET_REPO` 或由 `SPACE_ID` 推导 `{SPACE_ID}-data`。
25
+ - 启动时:若存在 `HF_TOKEN` 与 repo,则 `snapshot_download` 到 `PERSIST_PATH`。
26
+ - 后台线程:按 `SYNC_INTERVAL` 将 `PERSIST_PATH` 上传到 Dataset。
27
+ - 退出时:最后一次上传。
28
+
29
+ 4. **HF Spaces 约定**
30
+ - `sdk: docker`,`app_port: 7860`。
31
+ - 用户需在 Space 中设置 `RUN_CMD`(以及可选 `HF_TOKEN`、`HF_DATASET_REPO`、`PERSIST_PATH`)。
32
+ - 若使用默认 demo,`RUN_CMD` 启动监听 7860 的简单服务并读写 `PERSIST_PATH`,用于验证持久化。
33
+
34
+ ## 完成标准(迭代开发)
35
+
36
+ - [ ] 本地 `docker build` 与 `docker run` 成功,默认 demo 在 7860 响应。
37
+ - [ ] 持久化:重启容器后,写入 `PERSIST_PATH` 的内容仍在(通过 HF Dataset 恢复)。
38
+ - [ ] 至少一个难例(如 FastAPI+SQLite 或 Gradio+状态)在 README 中说明并在 HF 上可复现。
39
+ - [ ] 代码推送到 HF Space,且在该 Space 中成功运行并通过验证。
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingRun — Run anything on Hugging Face
2
+ # Optional build arg: BASE_IMAGE (default python:3.11-slim)
3
+ ARG BASE_IMAGE=python:3.11-slim
4
+ FROM ${BASE_IMAGE}
5
+
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ curl \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Ensure Python and pip for sync script (base may not have them if BASE_IMAGE is e.g. node)
11
+ RUN (command -v python3 >/dev/null 2>&1 || (apt-get update && apt-get install -y --no-install-recommends python3 python3-pip && rm -rf /var/lib/apt/lists/*)) \
12
+ && pip3 install --no-cache-dir --break-system-packages huggingface_hub 2>/dev/null || pip3 install --no-cache-dir huggingface_hub \
13
+ && pip3 install --no-cache-dir --break-system-packages fastapi uvicorn 2>/dev/null || pip3 install --no-cache-dir fastapi uvicorn
14
+
15
+ # HF Spaces run as user 1000
16
+ RUN useradd -m -u 1000 user
17
+ ENV HOME=/home/user
18
+ WORKDIR /home/user
19
+
20
+ COPY --chown=user:user scripts /scripts
21
+ COPY --chown=user:user app /app
22
+ RUN chmod +x /scripts/entrypoint.sh
23
+
24
+ # Default: run demo app. Override with RUN_CMD in Space secrets.
25
+ ENV PERSIST_PATH=/data
26
+ ENV RUN_CMD=""
27
+ ENV PORT=7860
28
+ ENV PYTHONPATH=/app
29
+
30
+ # Persist path must be writable by user 1000
31
+ RUN mkdir -p /data && chown user:user /data
32
+
33
+ USER user
34
+ EXPOSE 7860
35
+ ENTRYPOINT ["/scripts/entrypoint.sh"]
README.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: HuggingRun
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ app_port: 7860
10
+ tags:
11
+ - docker
12
+ - huggingface-spaces
13
+ - persistence
14
+ - deployment
15
+ - run-anything
16
+ ---
17
+
18
+ # HuggingRun
19
+
20
+ **Run anything on Hugging Face.**
21
+
22
+ Turn Hugging Face Spaces into a generic container: run any Docker-friendly app with optional persistence via HF Datasets. No Mac Mini, no paid cloud — 2 vCPU, 16GB RAM, 50GB storage, free tier.
23
+
24
+ ## Why HuggingRun?
25
+
26
+ - **Generic**: Not tied to one app. Set `RUN_CMD` to run your server, API, or bot.
27
+ - **Persistent**: Sync a directory (e.g. `/data`) to a private HF Dataset so state survives restarts.
28
+ - **One Dockerfile**: Parameterized with `BASE_IMAGE` and `RUN_CMD`; duplicate the Space and configure.
29
+
30
+ ## Quick Start
31
+
32
+ ### 1. Duplicate this Space
33
+
34
+ Click **Duplicate this Space** on the HuggingRun Space page. After duplicating, set your Space **Repository secrets**:
35
+
36
+ | Secret | Required | Description |
37
+ |--------|----------|-------------|
38
+ | `HF_TOKEN` | For persistence | HF token with write access ([create](https://huggingface.co/settings/tokens)) |
39
+ | `HF_DATASET_REPO` | Optional | Dataset repo for backup (e.g. `your-name/HuggingRun-data`). Default: `{SPACE_ID}-data` |
40
+ | `RUN_CMD` | Optional | Command to run (e.g. `python3 my_app.py`). Empty = default demo on port 7860 |
41
+
42
+ ### 2. Persistence (optional)
43
+
44
+ - **Manual**: Create a private Dataset repo (e.g. `your-name/HuggingRun-data`), set `HF_DATASET_REPO` and `HF_TOKEN`. Data under `PERSIST_PATH` (default `/data`) syncs every `SYNC_INTERVAL` seconds (default 60).
45
+ - **Auto**: Set `AUTO_CREATE_DATASET=true` and `HF_TOKEN`; a private dataset is created on first run.
46
+
47
+ ### 3. Open your Space
48
+
49
+ Visit your Space URL. You’ll see the default demo (visit counter with persistence). To run your own app, set `RUN_CMD` in secrets and rebuild.
50
+
51
+ ## Environment variables
52
+
53
+ | Variable | Default | Description |
54
+ |----------|---------|-------------|
55
+ | `RUN_CMD` | (default demo) | Command to run (e.g. `uvicorn app:app --host 0.0.0.0 --port 7860`) |
56
+ | `PERSIST_PATH` | `/data` | Directory to sync to the HF Dataset |
57
+ | `HF_TOKEN` | — | HF token for persistence |
58
+ | `HF_DATASET_REPO` | `{SPACE_ID}-data` | Dataset repo ID |
59
+ | `AUTO_CREATE_DATASET` | `false` | Create dataset repo if missing |
60
+ | `SYNC_INTERVAL` | `60` | Seconds between uploads |
61
+ | `PORT` | `7860` | Port for default demo (HF expects 7860) |
62
+
63
+ ## Hard example: FastAPI + SQLite
64
+
65
+ This repo includes a **hard example** (FastAPI + SQLite) in the image. To run it:
66
+
67
+ 1. In your Space **Repository secrets**, set:
68
+ - `RUN_CMD=uvicorn app.fastapi_sqlite:app --host 0.0.0.0 --port 7860`
69
+ 2. (Optional) Set `HF_TOKEN` and `AUTO_CREATE_DATASET=true` for persistence so the SQLite DB in `/data` survives restarts.
70
+ 3. Rebuild the Space. Open the app: you’ll see a visit counter and `/api` returning JSON. Restart the Space and refresh — the count persists.
71
+
72
+ This proves a stateful, “hard” app (FastAPI + SQLite + persistence) runs on HuggingRun.
73
+
74
+ ## License
75
+
76
+ MIT
app/demo_app.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ HuggingRun default demo: minimal HTTP app on port 7860 with persistence.
4
+ - Serves a simple page showing visit counter and PERSIST_PATH status.
5
+ - Counter is stored in PERSIST_PATH/counter.txt so it survives restarts (via HF Dataset sync).
6
+ """
7
+ import os
8
+ import http.server
9
+ import socketserver
10
+ from pathlib import Path
11
+
12
+ PORT = int(os.environ.get("PORT", "7860"))
13
+ PERSIST_PATH = Path(os.environ.get("PERSIST_PATH", "/data"))
14
+ COUNTER_FILE = PERSIST_PATH / "counter.txt"
15
+
16
+
17
+ def get_counter():
18
+ PERSIST_PATH.mkdir(parents=True, exist_ok=True)
19
+ try:
20
+ return int(COUNTER_FILE.read_text().strip())
21
+ except (FileNotFoundError, ValueError):
22
+ return 0
23
+
24
+
25
+ def inc_counter():
26
+ c = get_counter() + 1
27
+ PERSIST_PATH.mkdir(parents=True, exist_ok=True)
28
+ COUNTER_FILE.write_text(str(c))
29
+ return c
30
+
31
+
32
+ class DemoHandler(http.server.BaseHTTPRequestHandler):
33
+ def do_GET(self):
34
+ count = inc_counter()
35
+ body = f"""<!DOCTYPE html>
36
+ <html><head><meta charset="utf-8"><title>HuggingRun</title></head>
37
+ <body style="font-family:sans-serif;max-width:600px;margin:2em auto;padding:1em;">
38
+ <h1>Run anything on Hugging Face.</h1>
39
+ <p>This is the default HuggingRun demo. Persistence path: <code>{PERSIST_PATH}</code></p>
40
+ <p><strong>Visit count (persisted):</strong> {count}</p>
41
+ <p>Set <code>RUN_CMD</code> in your Space secrets to run your own app.</p>
42
+ </body></html>"""
43
+ self.send_response(200)
44
+ self.send_header("Content-type", "text/html; charset=utf-8")
45
+ self.send_header("Content-Length", str(len(body.encode("utf-8"))))
46
+ self.end_headers()
47
+ self.wfile.write(body.encode("utf-8"))
48
+
49
+ def log_message(self, format, *args):
50
+ print(f"[demo] {args[0]}")
51
+
52
+
53
+ if __name__ == "__main__":
54
+ PERSIST_PATH.mkdir(parents=True, exist_ok=True)
55
+ with socketserver.TCPServer(("0.0.0.0", PORT), DemoHandler) as httpd:
56
+ print(f"[HuggingRun demo] Listening on 0.0.0.0:{PORT}")
57
+ httpd.serve_forever()
app/fastapi_sqlite.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ HuggingRun hard example: FastAPI + SQLite with persistence in PERSIST_PATH.
4
+ Run with: RUN_CMD=uvicorn app.fastapi_sqlite:app --host 0.0.0.0 --port 7860
5
+ """
6
+ import os
7
+ from pathlib import Path
8
+ from fastapi import FastAPI
9
+ from fastapi.responses import HTMLResponse
10
+ import sqlite3
11
+
12
+ app = FastAPI(title="HuggingRun FastAPI+SQLite")
13
+ DB_PATH = Path(os.environ.get("PERSIST_PATH", "/data")) / "huggingrun.db"
14
+
15
+
16
+ def get_visits():
17
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
18
+ conn = sqlite3.connect(str(DB_PATH))
19
+ conn.execute("CREATE TABLE IF NOT EXISTS visits (id INTEGER PRIMARY KEY, count INTEGER)")
20
+ conn.commit()
21
+ n = conn.execute("SELECT COALESCE(SUM(count), 0) FROM visits").fetchone()[0]
22
+ conn.close()
23
+ return n
24
+
25
+
26
+ def inc_visits():
27
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
28
+ conn = sqlite3.connect(str(DB_PATH))
29
+ conn.execute("CREATE TABLE IF NOT EXISTS visits (id INTEGER PRIMARY KEY, count INTEGER)")
30
+ conn.execute("INSERT INTO visits (count) VALUES (1)")
31
+ conn.commit()
32
+ n = conn.execute("SELECT COALESCE(SUM(count), 0) FROM visits").fetchone()[0]
33
+ conn.close()
34
+ return n
35
+
36
+
37
+ @app.get("/", response_class=HTMLResponse)
38
+ def root():
39
+ n = inc_visits()
40
+ return f"""<!DOCTYPE html><html><head><meta charset="utf-8"><title>HuggingRun + FastAPI + SQLite</title></head>
41
+ <body style="font-family:sans-serif;max-width:600px;margin:2em auto;">
42
+ <h1>Run anything on Hugging Face.</h1>
43
+ <p><strong>Hard example:</strong> FastAPI + SQLite. DB path: <code>{DB_PATH}</code></p>
44
+ <p><strong>Total visits (persisted):</strong> {n}</p>
45
+ </body></html>"""
46
+
47
+
48
+ @app.get("/api")
49
+ def api():
50
+ n = get_visits()
51
+ return {"message": "HuggingRun FastAPI+SQLite", "total_visits": n}
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ huggingface_hub>=0.20.0
scripts/entrypoint.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -e
3
+ echo "[HuggingRun] Entrypoint — persistence + RUN_CMD"
4
+ exec python3 -u /scripts/sync_hf.py
scripts/sync_hf.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ HuggingRun — Generic persistence for any app on Hugging Face Spaces
4
+ ====================================================================
5
+
6
+ - Startup: snapshot_download from HF Dataset → PERSIST_PATH
7
+ - Periodic: upload_folder PERSIST_PATH → dataset
8
+ - Shutdown: final upload
9
+ - Then: exec user's RUN_CMD (or default demo app)
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import time
15
+ import threading
16
+ import subprocess
17
+ import signal
18
+ import shutil
19
+ import tempfile
20
+ import traceback
21
+ from pathlib import Path
22
+ from datetime import datetime
23
+
24
+ os.environ.setdefault("HF_HUB_DOWNLOAD_TIMEOUT", "300")
25
+ os.environ.setdefault("HF_HUB_UPLOAD_TIMEOUT", "600")
26
+
27
+ from huggingface_hub import HfApi, snapshot_download
28
+
29
+ # ── Configuration ───────────────────────────────────────────────────────────
30
+
31
+ HF_TOKEN = os.environ.get("HF_TOKEN")
32
+ PERSIST_PATH = Path(os.environ.get("PERSIST_PATH", "/data"))
33
+ RUN_CMD = os.environ.get("RUN_CMD", "")
34
+ SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "60"))
35
+ AUTO_CREATE_DATASET = os.environ.get("AUTO_CREATE_DATASET", "false").lower() in ("true", "1", "yes")
36
+ # In dataset we store under this path_in_repo subfolder
37
+ DATASET_SUBFOLDER = "data"
38
+
39
+ SPACE_ID = os.environ.get("SPACE_ID", "")
40
+ HF_REPO_ID = os.environ.get("HF_DATASET_REPO", "")
41
+ if not HF_REPO_ID and SPACE_ID:
42
+ HF_REPO_ID = f"{SPACE_ID}-data"
43
+ if not HF_REPO_ID and HF_TOKEN:
44
+ try:
45
+ api = HfApi(token=HF_TOKEN)
46
+ uname = api.whoami()["name"]
47
+ HF_REPO_ID = f"{uname}/HuggingRun-data"
48
+ del api, uname
49
+ except Exception:
50
+ HF_REPO_ID = ""
51
+
52
+
53
+ class GenericSync:
54
+ """Upload/download PERSIST_PATH to/from HF Dataset."""
55
+
56
+ def __init__(self):
57
+ self.enabled = False
58
+ self.dataset_exists = False
59
+ self.api = None
60
+
61
+ if not HF_TOKEN:
62
+ print("[HuggingRun] HF_TOKEN not set. Persistence disabled.")
63
+ return
64
+ if not HF_REPO_ID:
65
+ print("[HuggingRun] HF_DATASET_REPO/SPACE_ID not set. Persistence disabled.")
66
+ return
67
+
68
+ self.enabled = True
69
+ self.api = HfApi(token=HF_TOKEN)
70
+ self.dataset_exists = self._ensure_repo_exists()
71
+
72
+ def _ensure_repo_exists(self):
73
+ try:
74
+ self.api.repo_info(repo_id=HF_REPO_ID, repo_type="dataset")
75
+ print(f"[HuggingRun] Dataset found: {HF_REPO_ID}")
76
+ return True
77
+ except Exception:
78
+ if not AUTO_CREATE_DATASET:
79
+ print(f"[HuggingRun] Dataset not found: {HF_REPO_ID}. Set AUTO_CREATE_DATASET=true to create.")
80
+ return False
81
+ try:
82
+ self.api.create_repo(
83
+ repo_id=HF_REPO_ID,
84
+ repo_type="dataset",
85
+ private=True,
86
+ )
87
+ print(f"[HuggingRun] Created dataset: {HF_REPO_ID}")
88
+ return True
89
+ except Exception as e:
90
+ print(f"[HuggingRun] Failed to create dataset: {e}")
91
+ return False
92
+
93
+ def load_from_repo(self):
94
+ """Download dataset → PERSIST_PATH."""
95
+ if not self.enabled or not self.dataset_exists:
96
+ PERSIST_PATH.mkdir(parents=True, exist_ok=True)
97
+ return
98
+
99
+ try:
100
+ files = self.api.list_repo_files(repo_id=HF_REPO_ID, repo_type="dataset")
101
+ prefix = f"{DATASET_SUBFOLDER}/"
102
+ data_files = [f for f in files if f.startswith(prefix)]
103
+ if not data_files:
104
+ print(f"[HuggingRun] No {prefix} in dataset. Starting fresh.")
105
+ PERSIST_PATH.mkdir(parents=True, exist_ok=True)
106
+ return
107
+
108
+ print(f"[HuggingRun] Restoring {PERSIST_PATH} from {HF_REPO_ID} ...")
109
+ PERSIST_PATH.mkdir(parents=True, exist_ok=True)
110
+ with tempfile.TemporaryDirectory() as tmpdir:
111
+ snapshot_download(
112
+ repo_id=HF_REPO_ID,
113
+ repo_type="dataset",
114
+ allow_patterns=f"{prefix}**",
115
+ local_dir=tmpdir,
116
+ token=HF_TOKEN,
117
+ )
118
+ src = Path(tmpdir) / DATASET_SUBFOLDER
119
+ if src.exists():
120
+ for item in src.rglob("*"):
121
+ if item.is_file():
122
+ rel = item.relative_to(src)
123
+ dest = PERSIST_PATH / rel
124
+ dest.parent.mkdir(parents=True, exist_ok=True)
125
+ shutil.copy2(str(item), str(dest))
126
+ print("[HuggingRun] Restore completed.")
127
+ except Exception as e:
128
+ print(f"[HuggingRun] Restore failed: {e}")
129
+ traceback.print_exc()
130
+ PERSIST_PATH.mkdir(parents=True, exist_ok=True)
131
+
132
+ def save_to_repo(self):
133
+ """Upload PERSIST_PATH → dataset."""
134
+ if not self.enabled or not self.dataset_exists:
135
+ return
136
+ if not PERSIST_PATH.exists():
137
+ return
138
+
139
+ try:
140
+ if not self._ensure_repo_exists():
141
+ return
142
+ self.api.upload_folder(
143
+ folder_path=str(PERSIST_PATH),
144
+ path_in_repo=DATASET_SUBFOLDER,
145
+ repo_id=HF_REPO_ID,
146
+ repo_type="dataset",
147
+ token=HF_TOKEN,
148
+ commit_message=f"HuggingRun sync — {datetime.now().isoformat()}",
149
+ ignore_patterns=["__pycache__", "*.pyc", ".git"],
150
+ )
151
+ print(f"[HuggingRun] Upload completed.")
152
+ except Exception as e:
153
+ print(f"[HuggingRun] Upload failed: {e}")
154
+ traceback.print_exc()
155
+
156
+ def background_sync_loop(self, stop_event):
157
+ while not stop_event.is_set():
158
+ if stop_event.wait(timeout=SYNC_INTERVAL):
159
+ break
160
+ self.save_to_repo()
161
+
162
+ def run_user_cmd(self):
163
+ """Run RUN_CMD (or default demo). Returns Popen process or None."""
164
+ cmd = (RUN_CMD or "python3 /app/demo_app.py").strip()
165
+ if not cmd:
166
+ print("[HuggingRun] RUN_CMD empty and no default demo. Exiting.")
167
+ return None
168
+ print(f"[HuggingRun] Running: {cmd}")
169
+ try:
170
+ process = subprocess.Popen(
171
+ cmd,
172
+ shell=True,
173
+ env=os.environ.copy(),
174
+ stdout=sys.stdout,
175
+ stderr=sys.stderr,
176
+ )
177
+ print(f"[HuggingRun] Process started PID={process.pid}")
178
+ return process
179
+ except Exception as e:
180
+ print(f"[HuggingRun] Failed to start: {e}")
181
+ traceback.print_exc()
182
+ return None
183
+
184
+
185
+ def main():
186
+ try:
187
+ sync = GenericSync()
188
+ sync.load_from_repo()
189
+
190
+ stop_event = threading.Event()
191
+ t = threading.Thread(target=sync.background_sync_loop, args=(stop_event,), daemon=True)
192
+ t.start()
193
+
194
+ process = sync.run_user_cmd()
195
+ if process is None:
196
+ stop_event.set()
197
+ t.join(timeout=5)
198
+ sys.exit(1)
199
+
200
+ def on_signal(sig, frame):
201
+ print(f"\n[HuggingRun] Signal {sig}. Shutting down...")
202
+ stop_event.set()
203
+ t.join(timeout=10)
204
+ if process.poll() is None:
205
+ process.terminate()
206
+ try:
207
+ process.wait(timeout=5)
208
+ except subprocess.TimeoutExpired:
209
+ process.kill()
210
+ sync.save_to_repo()
211
+ sys.exit(0)
212
+
213
+ signal.signal(signal.SIGINT, on_signal)
214
+ signal.signal(signal.SIGTERM, on_signal)
215
+
216
+ code = process.wait()
217
+ stop_event.set()
218
+ t.join(timeout=10)
219
+ sync.save_to_repo()
220
+ sys.exit(code)
221
+ except Exception as e:
222
+ print(f"[HuggingRun] FATAL: {e}")
223
+ traceback.print_exc()
224
+ sys.exit(1)
225
+
226
+
227
+ if __name__ == "__main__":
228
+ main()