Spaces:
Sleeping
Sleeping
feat: git_sync_daemon — gitfs-like auto-sync without FUSE
Browse filesFUSE/gitfs not available in HF Spaces (no CAP_MKNOD).
Implements same behavior via pure git:
- git clone dataset → /data on startup
- rsync / → /data/rootfs/ periodically
- git add/commit/push automatically (like gitfs current/)
- Dataset mirrors filesystem at actual paths
- Configurable sync interval (default 120s)
Simplified start-server.sh: only starts services (sshd, ws-bridge,
ttyd, nginx). All persistence handled by git_sync_daemon.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Dockerfile +4 -3
- scripts/entrypoint.sh +39 -79
- ubuntu-server/git_sync_daemon.py +211 -0
- ubuntu-server/start-server.sh +9 -86
Dockerfile
CHANGED
|
@@ -5,10 +5,11 @@ FROM ubuntu:24.04
|
|
| 5 |
|
| 6 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 7 |
|
| 8 |
-
# System + Python (for HuggingRun
|
| 9 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
-
ca-certificates curl wget python3 python3-pip python3-venv git \
|
| 11 |
&& pip3 install --no-cache-dir --break-system-packages huggingface_hub websockets \
|
|
|
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
# Server: SSH + nginx + ttyd + tools
|
|
@@ -45,7 +46,7 @@ RUN mkdir -p /data
|
|
| 45 |
# nginx + bridge + startup scripts
|
| 46 |
COPY ubuntu-server/nginx.conf /etc/nginx/nginx.conf
|
| 47 |
COPY ubuntu-server/ws-ssh-bridge.py /opt/ws-ssh-bridge.py
|
| 48 |
-
COPY ubuntu-server/
|
| 49 |
COPY ubuntu-server/start-server.sh /opt/start-server.sh
|
| 50 |
COPY scripts /scripts
|
| 51 |
RUN chmod +x /scripts/entrypoint.sh /opt/start-server.sh
|
|
|
|
| 5 |
|
| 6 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 7 |
|
| 8 |
+
# System + Python + git-lfs (for HuggingRun git-based persistence)
|
| 9 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
+
ca-certificates curl wget python3 python3-pip python3-venv git git-lfs \
|
| 11 |
&& pip3 install --no-cache-dir --break-system-packages huggingface_hub websockets \
|
| 12 |
+
&& git lfs install \
|
| 13 |
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
|
| 15 |
# Server: SSH + nginx + ttyd + tools
|
|
|
|
| 46 |
# nginx + bridge + startup scripts
|
| 47 |
COPY ubuntu-server/nginx.conf /etc/nginx/nginx.conf
|
| 48 |
COPY ubuntu-server/ws-ssh-bridge.py /opt/ws-ssh-bridge.py
|
| 49 |
+
COPY ubuntu-server/git_sync_daemon.py /opt/git_sync_daemon.py
|
| 50 |
COPY ubuntu-server/start-server.sh /opt/start-server.sh
|
| 51 |
COPY scripts /scripts
|
| 52 |
RUN chmod +x /scripts/entrypoint.sh /opt/start-server.sh
|
scripts/entrypoint.sh
CHANGED
|
@@ -1,95 +1,55 @@
|
|
| 1 |
#!/bin/bash
|
| 2 |
# ─────────────────────────────────────────────────────────────────────
|
| 3 |
# HuggingRun Entrypoint
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
# 3. Run user command (start-server.sh sources the env and handles upload)
|
| 7 |
# ─────────────────────────────────────────────────────────────────────
|
| 8 |
set -e
|
| 9 |
-
echo "[HuggingRun] Entrypoint — persistence + RUN_CMD"
|
| 10 |
-
|
| 11 |
-
PERSIST_PATH="${PERSIST_PATH:-/data}"
|
| 12 |
-
ENV_FILE="/etc/huggingrun.env"
|
| 13 |
-
|
| 14 |
-
# Step 1: Download dataset and write env file
|
| 15 |
-
python3 -u << 'PYEOF'
|
| 16 |
-
import os, sys, traceback
|
| 17 |
-
from pathlib import Path
|
| 18 |
-
|
| 19 |
-
PERSIST_PATH = Path(os.environ.get("PERSIST_PATH", "/data"))
|
| 20 |
-
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 21 |
-
SPACE_ID = os.environ.get("SPACE_ID", "")
|
| 22 |
-
HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "")
|
| 23 |
-
ENV_FILE = "/etc/huggingrun.env"
|
| 24 |
|
| 25 |
# Determine dataset repo
|
| 26 |
-
if
|
| 27 |
-
if SPACE_ID
|
| 28 |
-
|
| 29 |
-
elif HF_TOKEN
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
except:
|
| 34 |
-
pass
|
| 35 |
-
|
| 36 |
-
# Write env file for other processes
|
| 37 |
-
with open(ENV_FILE, "w") as f:
|
| 38 |
-
f.write(f'export HF_TOKEN="{HF_TOKEN}"\n')
|
| 39 |
-
f.write(f'export HF_DATASET_REPO="{HF_DATASET_REPO}"\n')
|
| 40 |
-
f.write(f'export PERSIST_PATH="{PERSIST_PATH}"\n')
|
| 41 |
-
print(f"[HuggingRun] Wrote env to {ENV_FILE}")
|
| 42 |
-
|
| 43 |
-
if not HF_TOKEN or not HF_DATASET_REPO:
|
| 44 |
-
print("[HuggingRun] No HF_TOKEN or dataset. Persistence disabled.")
|
| 45 |
-
PERSIST_PATH.mkdir(parents=True, exist_ok=True)
|
| 46 |
-
sys.exit(0)
|
| 47 |
-
|
| 48 |
-
print(f"[HuggingRun] Dataset repo: {HF_DATASET_REPO}")
|
| 49 |
-
|
| 50 |
-
from huggingface_hub import HfApi, snapshot_download
|
| 51 |
-
|
| 52 |
-
api = HfApi(token=HF_TOKEN)
|
| 53 |
-
|
| 54 |
-
# Ensure dataset exists
|
| 55 |
try:
|
| 56 |
-
|
| 57 |
-
print(f"[HuggingRun] Dataset found: {HF_DATASET_REPO}")
|
| 58 |
except:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
print(f"[HuggingRun] Cannot create dataset: {e}")
|
| 64 |
-
PERSIST_PATH.mkdir(parents=True, exist_ok=True)
|
| 65 |
-
sys.exit(0)
|
| 66 |
-
|
| 67 |
-
# Check if dataset has files
|
| 68 |
-
files = api.list_repo_files(repo_id=HF_DATASET_REPO, repo_type="dataset")
|
| 69 |
-
data_files = [f for f in files if not f.startswith(".") and f != "README.md"]
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
snapshot_download(
|
| 79 |
-
repo_id=HF_DATASET_REPO,
|
| 80 |
-
repo_type="dataset",
|
| 81 |
-
local_dir=str(PERSIST_PATH),
|
| 82 |
-
token=HF_TOKEN,
|
| 83 |
-
ignore_patterns=[".git*", "README.md"],
|
| 84 |
-
)
|
| 85 |
-
print("[HuggingRun] Download completed.")
|
| 86 |
-
PYEOF
|
| 87 |
|
| 88 |
-
#
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
| 92 |
|
|
|
|
| 93 |
CMD="${RUN_CMD:-python3 /app/demo_app.py}"
|
| 94 |
echo "[HuggingRun] Running: $CMD"
|
| 95 |
exec $CMD
|
|
|
|
| 1 |
#!/bin/bash
|
| 2 |
# ─────────────────────────────────────────────────────────────────────
|
| 3 |
# HuggingRun Entrypoint
|
| 4 |
+
# Uses git_sync_daemon.py (gitfs-like) for dataset ↔ filesystem sync
|
| 5 |
+
# Then runs the user command (start-server.sh)
|
|
|
|
| 6 |
# ─────────────────────────────────────────────────────────────────────
|
| 7 |
set -e
|
| 8 |
+
echo "[HuggingRun] Entrypoint — git-sync persistence + RUN_CMD"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# Determine dataset repo
|
| 11 |
+
if [ -z "$HF_DATASET_REPO" ]; then
|
| 12 |
+
if [ -n "$SPACE_ID" ]; then
|
| 13 |
+
export HF_DATASET_REPO="${SPACE_ID}-data"
|
| 14 |
+
elif [ -n "$HF_TOKEN" ]; then
|
| 15 |
+
export HF_DATASET_REPO=$(python3 -c "
|
| 16 |
+
from huggingface_hub import HfApi
|
| 17 |
+
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
try:
|
| 19 |
+
print(HfApi(token=os.environ['HF_TOKEN']).whoami()['name'] + '/HuggingRun-data')
|
|
|
|
| 20 |
except:
|
| 21 |
+
print('')
|
| 22 |
+
" 2>/dev/null)
|
| 23 |
+
fi
|
| 24 |
+
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
# Ensure dataset repo exists
|
| 27 |
+
if [ -n "$HF_TOKEN" ] && [ -n "$HF_DATASET_REPO" ]; then
|
| 28 |
+
python3 -c "
|
| 29 |
+
from huggingface_hub import HfApi
|
| 30 |
+
import os
|
| 31 |
+
api = HfApi(token=os.environ['HF_TOKEN'])
|
| 32 |
+
repo = os.environ['HF_DATASET_REPO']
|
| 33 |
+
try:
|
| 34 |
+
api.repo_info(repo_id=repo, repo_type='dataset')
|
| 35 |
+
print(f'[HuggingRun] Dataset: {repo}')
|
| 36 |
+
except:
|
| 37 |
+
api.create_repo(repo_id=repo, repo_type='dataset', private=True)
|
| 38 |
+
print(f'[HuggingRun] Created: {repo}')
|
| 39 |
+
" 2>/dev/null || echo "[HuggingRun] Could not verify dataset"
|
| 40 |
+
fi
|
| 41 |
|
| 42 |
+
# Run git-sync daemon (clones repo, restores system, starts background sync)
|
| 43 |
+
python3 -u /opt/git_sync_daemon.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
# Write env for other processes (sshd, cron, etc.)
|
| 46 |
+
cat > /etc/huggingrun.env << ENVEOF
|
| 47 |
+
export HF_TOKEN="${HF_TOKEN}"
|
| 48 |
+
export HF_DATASET_REPO="${HF_DATASET_REPO}"
|
| 49 |
+
export PERSIST_PATH="${PERSIST_PATH:-/data}"
|
| 50 |
+
ENVEOF
|
| 51 |
|
| 52 |
+
# Run user command
|
| 53 |
CMD="${RUN_CMD:-python3 /app/demo_app.py}"
|
| 54 |
echo "[HuggingRun] Running: $CMD"
|
| 55 |
exec $CMD
|
ubuntu-server/git_sync_daemon.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
git_sync_daemon.py — gitfs-like auto-sync without FUSE
|
| 4 |
+
|
| 5 |
+
Behavior (mirrors gitfs):
|
| 6 |
+
1. Clones HF dataset repo to /data
|
| 7 |
+
2. Periodically rsyncs filesystem → /data/rootfs/
|
| 8 |
+
3. Auto-commits and pushes changes via git
|
| 9 |
+
4. On restart: git pull → rsync /data/rootfs/ → /
|
| 10 |
+
|
| 11 |
+
The dataset contains the actual filesystem at rootfs/usr/local/bin/..., etc.
|
| 12 |
+
Every change is a git commit. Dataset = disk, always in sync.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import time
|
| 18 |
+
import subprocess
|
| 19 |
+
import signal
|
| 20 |
+
import threading
|
| 21 |
+
|
| 22 |
+
# ── Config ─────────────────────────────────────────────────────────
|
| 23 |
+
PERSIST_PATH = os.environ.get("PERSIST_PATH", "/data")
|
| 24 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 25 |
+
HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "")
|
| 26 |
+
ROOTFS = os.path.join(PERSIST_PATH, "rootfs")
|
| 27 |
+
|
| 28 |
+
SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "120")) # seconds between syncs
|
| 29 |
+
COMMIT_NAME = "HuggingRun"
|
| 30 |
+
COMMIT_EMAIL = "huggingrun@hf.space"
|
| 31 |
+
|
| 32 |
+
RSYNC_EXCLUDES = [
|
| 33 |
+
"/proc", "/sys", "/dev", "/data", "/tmp", "/run",
|
| 34 |
+
"/mnt", "/media", "/snap",
|
| 35 |
+
"/var/cache/apt", "/var/lib/apt/lists",
|
| 36 |
+
"__pycache__", "*.pyc", "*.lock", "*.pid", "*.sock",
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
stop_event = threading.Event()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def log(msg):
|
| 43 |
+
print(f"[git-sync] {msg}", file=sys.stderr, flush=True)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def run(cmd, cwd=None, check=False):
|
| 47 |
+
"""Run a shell command, return (returncode, stdout)."""
|
| 48 |
+
r = subprocess.run(
|
| 49 |
+
cmd, shell=True, cwd=cwd,
|
| 50 |
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
| 51 |
+
text=True,
|
| 52 |
+
)
|
| 53 |
+
if check and r.returncode != 0:
|
| 54 |
+
log(f"Command failed: {cmd}\n{r.stdout}")
|
| 55 |
+
return r.returncode, r.stdout.strip()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def git_clone_or_pull():
|
| 59 |
+
"""Clone the dataset repo to PERSIST_PATH, or pull if already cloned."""
|
| 60 |
+
repo_url = f"https://user:{HF_TOKEN}@huggingface.co/datasets/{HF_DATASET_REPO}"
|
| 61 |
+
|
| 62 |
+
if os.path.isdir(os.path.join(PERSIST_PATH, ".git")):
|
| 63 |
+
log("Git repo exists, pulling ...")
|
| 64 |
+
run(f"git -C {PERSIST_PATH} fetch origin main", cwd=PERSIST_PATH)
|
| 65 |
+
run(f"git -C {PERSIST_PATH} reset --hard origin/main", cwd=PERSIST_PATH)
|
| 66 |
+
log("Pull completed")
|
| 67 |
+
else:
|
| 68 |
+
log(f"Cloning {HF_DATASET_REPO} to {PERSIST_PATH} ...")
|
| 69 |
+
# Remove any existing non-git content
|
| 70 |
+
if os.path.exists(PERSIST_PATH):
|
| 71 |
+
run(f"rm -rf {PERSIST_PATH}")
|
| 72 |
+
rc, out = run(f"git clone --depth 1 {repo_url} {PERSIST_PATH}")
|
| 73 |
+
if rc != 0:
|
| 74 |
+
log(f"Clone failed: {out}")
|
| 75 |
+
os.makedirs(PERSIST_PATH, exist_ok=True)
|
| 76 |
+
# Init empty repo
|
| 77 |
+
run(f"git init {PERSIST_PATH}")
|
| 78 |
+
run(f"git -C {PERSIST_PATH} remote add origin {repo_url}")
|
| 79 |
+
run(f"git -C {PERSIST_PATH} checkout -b main")
|
| 80 |
+
log("Clone completed")
|
| 81 |
+
|
| 82 |
+
# Configure git
|
| 83 |
+
run(f'git config user.name "{COMMIT_NAME}"', cwd=PERSIST_PATH)
|
| 84 |
+
run(f'git config user.email "{COMMIT_EMAIL}"', cwd=PERSIST_PATH)
|
| 85 |
+
# Enable git-lfs
|
| 86 |
+
run("git lfs install --skip-smudge", cwd=PERSIST_PATH)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def restore_system():
|
| 90 |
+
"""Restore filesystem from /data/rootfs/ → /."""
|
| 91 |
+
if not os.path.isdir(ROOTFS) or not os.listdir(ROOTFS):
|
| 92 |
+
log("No rootfs found, starting fresh")
|
| 93 |
+
os.makedirs(ROOTFS, exist_ok=True)
|
| 94 |
+
return
|
| 95 |
+
|
| 96 |
+
log("Restoring filesystem from rootfs ...")
|
| 97 |
+
excludes = " ".join(f"--exclude='{e}'" for e in RSYNC_EXCLUDES)
|
| 98 |
+
run(f"rsync -aAX {excludes} --exclude='/scripts' {ROOTFS}/ /")
|
| 99 |
+
log("Filesystem restored")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def save_and_push():
|
| 103 |
+
"""Rsync filesystem → rootfs, git commit + push."""
|
| 104 |
+
log("Saving filesystem ...")
|
| 105 |
+
os.makedirs(ROOTFS, exist_ok=True)
|
| 106 |
+
|
| 107 |
+
# Rsync / → /data/rootfs/
|
| 108 |
+
excludes = " ".join(f"--exclude='{e}'" for e in RSYNC_EXCLUDES)
|
| 109 |
+
run(f"rsync -aAX --delete {excludes} / {ROOTFS}/")
|
| 110 |
+
|
| 111 |
+
# Fix permissions for git
|
| 112 |
+
run(f"chmod -R a+r {ROOTFS}/")
|
| 113 |
+
|
| 114 |
+
# Git add + commit + push
|
| 115 |
+
run("git add -A", cwd=PERSIST_PATH)
|
| 116 |
+
|
| 117 |
+
# Check if there are changes
|
| 118 |
+
rc, _ = run("git diff --cached --quiet", cwd=PERSIST_PATH)
|
| 119 |
+
if rc == 0:
|
| 120 |
+
log("No changes to commit")
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
ts = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
|
| 124 |
+
run(f'git commit -m "sync {ts}"', cwd=PERSIST_PATH)
|
| 125 |
+
|
| 126 |
+
log("Pushing to remote ...")
|
| 127 |
+
rc, out = run("git push origin main", cwd=PERSIST_PATH)
|
| 128 |
+
if rc != 0:
|
| 129 |
+
log(f"Push failed, trying force push: {out}")
|
| 130 |
+
rc, out = run("git push --force origin main", cwd=PERSIST_PATH)
|
| 131 |
+
if rc != 0:
|
| 132 |
+
log(f"Force push also failed: {out}")
|
| 133 |
+
else:
|
| 134 |
+
log("Force push succeeded")
|
| 135 |
+
else:
|
| 136 |
+
log("Push completed")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def ensure_passwords():
|
| 140 |
+
"""Re-ensure user/root passwords after restore."""
|
| 141 |
+
run("id user >/dev/null 2>&1 || useradd -m -s /bin/bash user")
|
| 142 |
+
run('usermod -p "$(openssl passwd -6 huggingrun)" user')
|
| 143 |
+
run('usermod -p "$(openssl passwd -6 huggingrun)" root')
|
| 144 |
+
run("ldconfig")
|
| 145 |
+
log("Passwords and ldconfig refreshed")
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def sync_loop():
|
| 149 |
+
"""Background loop: periodically save and push."""
|
| 150 |
+
log(f"Sync loop started (interval={SYNC_INTERVAL}s)")
|
| 151 |
+
# Initial delay to let services start
|
| 152 |
+
if stop_event.wait(timeout=60):
|
| 153 |
+
return
|
| 154 |
+
|
| 155 |
+
while not stop_event.is_set():
|
| 156 |
+
try:
|
| 157 |
+
save_and_push()
|
| 158 |
+
except Exception as e:
|
| 159 |
+
log(f"Sync error: {e}")
|
| 160 |
+
|
| 161 |
+
if stop_event.wait(timeout=SYNC_INTERVAL):
|
| 162 |
+
break
|
| 163 |
+
|
| 164 |
+
# Final save on exit
|
| 165 |
+
log("Final save before exit ...")
|
| 166 |
+
try:
|
| 167 |
+
save_and_push()
|
| 168 |
+
except Exception as e:
|
| 169 |
+
log(f"Final save error: {e}")
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def main():
|
| 173 |
+
if not HF_TOKEN or not HF_DATASET_REPO:
|
| 174 |
+
log("No HF_TOKEN or HF_DATASET_REPO. Sync disabled.")
|
| 175 |
+
os.makedirs(PERSIST_PATH, exist_ok=True)
|
| 176 |
+
return
|
| 177 |
+
|
| 178 |
+
log(f"Starting git-sync daemon")
|
| 179 |
+
log(f" Dataset: {HF_DATASET_REPO}")
|
| 180 |
+
log(f" Persist: {PERSIST_PATH}")
|
| 181 |
+
log(f" Interval: {SYNC_INTERVAL}s")
|
| 182 |
+
|
| 183 |
+
# Clone or pull
|
| 184 |
+
git_clone_or_pull()
|
| 185 |
+
|
| 186 |
+
# Restore system
|
| 187 |
+
restore_system()
|
| 188 |
+
|
| 189 |
+
# Re-set passwords
|
| 190 |
+
ensure_passwords()
|
| 191 |
+
|
| 192 |
+
# Start sync loop in background thread
|
| 193 |
+
sync_thread = threading.Thread(target=sync_loop, daemon=True)
|
| 194 |
+
sync_thread.start()
|
| 195 |
+
|
| 196 |
+
# Handle signals
|
| 197 |
+
def on_signal(sig, frame):
|
| 198 |
+
log(f"Signal {sig} received, stopping ...")
|
| 199 |
+
stop_event.set()
|
| 200 |
+
sync_thread.join(timeout=30)
|
| 201 |
+
sys.exit(0)
|
| 202 |
+
|
| 203 |
+
signal.signal(signal.SIGTERM, on_signal)
|
| 204 |
+
signal.signal(signal.SIGINT, on_signal)
|
| 205 |
+
|
| 206 |
+
# Keep main thread alive (will be used by start-server.sh exec)
|
| 207 |
+
log("Daemon ready. System restored.")
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
main()
|
ubuntu-server/start-server.sh
CHANGED
|
@@ -1,94 +1,17 @@
|
|
| 1 |
#!/bin/bash
|
| 2 |
# ─────────────────────────────────────────────────────────────────────
|
| 3 |
-
# HuggingRun Ubuntu Server:
|
| 4 |
-
# Port 7860 (nginx): web terminal + SSH
|
| 5 |
#
|
| 6 |
-
# Persistence
|
| 7 |
-
#
|
| 8 |
-
# upload_folder pushes /data/rootfs/ to HF dataset periodically
|
| 9 |
-
# Dataset shows actual filesystem paths: rootfs/usr/local/bin/...
|
| 10 |
# ─────────────────────────────────────────────────────────────────────
|
| 11 |
-
echo "[start-server] Starting ..." >&2
|
| 12 |
-
set -e
|
| 13 |
|
| 14 |
-
# Source HuggingRun env (HF_TOKEN, HF_DATASET_REPO, PERSIST_PATH)
|
| 15 |
-
[ -f /etc/huggingrun.env ] && source /etc/huggingrun.env
|
| 16 |
-
|
| 17 |
-
export PERSIST_PATH="${PERSIST_PATH:-/data}"
|
| 18 |
export SSH_PORT="${SSH_PORT:-2222}"
|
| 19 |
export TTYD_PORT="${TTYD_PORT:-7681}"
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
# Rsync exclude list
|
| 24 |
-
RSYNC_EXCLUDE=(
|
| 25 |
-
--exclude='/proc' --exclude='/sys' --exclude='/dev'
|
| 26 |
-
--exclude='/data' --exclude='/tmp' --exclude='/run'
|
| 27 |
-
--exclude='/mnt' --exclude='/media' --exclude='/snap'
|
| 28 |
-
--exclude='/var/cache/apt' --exclude='/var/lib/apt/lists'
|
| 29 |
-
--exclude='__pycache__' --exclude='*.pyc'
|
| 30 |
-
--exclude='.git'
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
# ── Phase 1: Restore system from dataset ──────────────────────────
|
| 34 |
-
echo "[start-server] Restoring persisted system state ..." >&2
|
| 35 |
-
if [ -d "$ROOTFS" ] && [ "$(ls -A "$ROOTFS" 2>/dev/null)" ]; then
|
| 36 |
-
echo "[persist] Restoring filesystem from $ROOTFS ..." >&2
|
| 37 |
-
rsync -aAX \
|
| 38 |
-
--exclude='/scripts' \
|
| 39 |
-
"${RSYNC_EXCLUDE[@]}" \
|
| 40 |
-
"$ROOTFS/" / 2>/dev/null || true
|
| 41 |
-
echo "[persist] Filesystem restored" >&2
|
| 42 |
-
else
|
| 43 |
-
echo "[persist] No rootfs found, starting fresh" >&2
|
| 44 |
-
mkdir -p "$ROOTFS"
|
| 45 |
-
fi
|
| 46 |
-
|
| 47 |
-
# Always re-ensure passwords and user
|
| 48 |
-
id user &>/dev/null || useradd -m -s /bin/bash user 2>/dev/null || true
|
| 49 |
-
usermod -p "$(openssl passwd -6 huggingrun)" user 2>/dev/null || true
|
| 50 |
-
usermod -p "$(openssl passwd -6 huggingrun)" root 2>/dev/null || true
|
| 51 |
-
ldconfig 2>/dev/null || true
|
| 52 |
-
echo "[persist] System ready" >&2
|
| 53 |
-
|
| 54 |
-
# ── Phase 2: Save system (rsync + upload_folder) ─────────────────
|
| 55 |
-
save_system() {
|
| 56 |
-
echo "[persist] Saving filesystem ..." >&2
|
| 57 |
-
mkdir -p "$ROOTFS"
|
| 58 |
-
|
| 59 |
-
# Rsync entire filesystem to /data/rootfs (exclude lock files etc.)
|
| 60 |
-
rsync -aAX --delete \
|
| 61 |
-
"${RSYNC_EXCLUDE[@]}" \
|
| 62 |
-
--exclude='*.lock' --exclude='*.pid' --exclude='*.sock' \
|
| 63 |
-
/ "$ROOTFS/" 2>/dev/null || true
|
| 64 |
-
|
| 65 |
-
# Fix permissions so upload can read all files
|
| 66 |
-
chmod -R a+r "$ROOTFS/" 2>/dev/null || true
|
| 67 |
-
|
| 68 |
-
# Source env for credentials
|
| 69 |
-
[ -f /etc/huggingrun.env ] && source /etc/huggingrun.env
|
| 70 |
-
|
| 71 |
-
# Upload via huggingface_hub
|
| 72 |
-
python3 -u /opt/upload_sync.py 2>&1 || true
|
| 73 |
-
|
| 74 |
-
echo "[persist] Save completed at $(date -u +%H:%M:%S)" >&2
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
# Background: save every 300 seconds
|
| 78 |
-
(
|
| 79 |
-
sleep 180 # initial delay (3 min, let system settle)
|
| 80 |
-
while true; do
|
| 81 |
-
save_system
|
| 82 |
-
sleep 300
|
| 83 |
-
done
|
| 84 |
-
) &
|
| 85 |
-
PERSIST_PID=$!
|
| 86 |
-
echo "[start-server] Persistence background PID=$PERSIST_PID" >&2
|
| 87 |
-
|
| 88 |
-
# Save on exit
|
| 89 |
-
trap 'echo "[start-server] Saving before exit..." >&2; save_system; exit 0' SIGTERM SIGINT
|
| 90 |
-
|
| 91 |
-
# ── Phase 3: Start sshd ──────────────────────────────────────────
|
| 92 |
mkdir -p /run/sshd
|
| 93 |
echo "[start-server] Starting sshd on 127.0.0.1:$SSH_PORT ..." >&2
|
| 94 |
/usr/sbin/sshd -o "Port=$SSH_PORT" \
|
|
@@ -102,20 +25,20 @@ SSHD_PID=$!
|
|
| 102 |
sleep 1
|
| 103 |
echo "[start-server] sshd PID=$SSHD_PID" >&2
|
| 104 |
|
| 105 |
-
# ──
|
| 106 |
echo "[start-server] Starting WS-SSH bridge on 127.0.0.1:7862 ..." >&2
|
| 107 |
python3 /opt/ws-ssh-bridge.py &
|
| 108 |
BRIDGE_PID=$!
|
| 109 |
sleep 1
|
| 110 |
echo "[start-server] WS-SSH bridge PID=$BRIDGE_PID" >&2
|
| 111 |
|
| 112 |
-
# ──
|
| 113 |
echo "[start-server] Starting ttyd on 127.0.0.1:$TTYD_PORT ..." >&2
|
| 114 |
ttyd --port "$TTYD_PORT" --writable --base-path / bash --login &
|
| 115 |
TTYD_PID=$!
|
| 116 |
sleep 1
|
| 117 |
echo "[start-server] ttyd PID=$TTYD_PID" >&2
|
| 118 |
|
| 119 |
-
# ──
|
| 120 |
echo "[start-server] Starting nginx on 0.0.0.0:7860 ..." >&2
|
| 121 |
exec nginx -g 'daemon off;'
|
|
|
|
| 1 |
#!/bin/bash
|
| 2 |
# ─────────────────────────────────────────────────────────────────────
|
| 3 |
+
# HuggingRun Ubuntu Server: ttyd + SSH-over-WebSocket
|
| 4 |
+
# Port 7860 (nginx): web terminal + SSH
|
| 5 |
#
|
| 6 |
+
# Persistence is handled by git_sync_daemon.py (started in entrypoint)
|
| 7 |
+
# This script just starts the services.
|
|
|
|
|
|
|
| 8 |
# ─────────────────────────────────────────────────────────────────────
|
| 9 |
+
echo "[start-server] Starting services ..." >&2
|
|
|
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
export SSH_PORT="${SSH_PORT:-2222}"
|
| 12 |
export TTYD_PORT="${TTYD_PORT:-7681}"
|
| 13 |
|
| 14 |
+
# ── sshd ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
mkdir -p /run/sshd
|
| 16 |
echo "[start-server] Starting sshd on 127.0.0.1:$SSH_PORT ..." >&2
|
| 17 |
/usr/sbin/sshd -o "Port=$SSH_PORT" \
|
|
|
|
| 25 |
sleep 1
|
| 26 |
echo "[start-server] sshd PID=$SSHD_PID" >&2
|
| 27 |
|
| 28 |
+
# ── WebSocket-to-SSH bridge ──────────────────────────────────────
|
| 29 |
echo "[start-server] Starting WS-SSH bridge on 127.0.0.1:7862 ..." >&2
|
| 30 |
python3 /opt/ws-ssh-bridge.py &
|
| 31 |
BRIDGE_PID=$!
|
| 32 |
sleep 1
|
| 33 |
echo "[start-server] WS-SSH bridge PID=$BRIDGE_PID" >&2
|
| 34 |
|
| 35 |
+
# ── ttyd (web terminal) ─────────────────────────────────────────
|
| 36 |
echo "[start-server] Starting ttyd on 127.0.0.1:$TTYD_PORT ..." >&2
|
| 37 |
ttyd --port "$TTYD_PORT" --writable --base-path / bash --login &
|
| 38 |
TTYD_PID=$!
|
| 39 |
sleep 1
|
| 40 |
echo "[start-server] ttyd PID=$TTYD_PID" >&2
|
| 41 |
|
| 42 |
+
# ── nginx (foreground, port 7860) ────────────────────────────────
|
| 43 |
echo "[start-server] Starting nginx on 0.0.0.0:7860 ..." >&2
|
| 44 |
exec nginx -g 'daemon off;'
|