Upload 14 files
Browse files- .dockerignore +46 -0
- .gitignore +39 -0
- Dockerfile +134 -0
- IMPROVED_PROMPT.md +186 -0
- README.md +212 -9
- app.py +504 -0
- healthcheck.py +61 -0
- nginx.conf +169 -0
- requirements.txt +3 -0
- setup_storage.sh +39 -0
- setup_ubuntu.sh +95 -0
- setup_vnc.sh +71 -0
- start.sh +54 -0
- supervisord.conf +132 -0
.dockerignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ββ .dockerignore βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
# Exclude files that should never end up in the Docker image
|
| 3 |
+
|
| 4 |
+
# Git metadata
|
| 5 |
+
.git
|
| 6 |
+
.gitignore
|
| 7 |
+
|
| 8 |
+
# Docs (not needed at runtime)
|
| 9 |
+
README.md
|
| 10 |
+
*.md
|
| 11 |
+
docs/
|
| 12 |
+
|
| 13 |
+
# Python cache
|
| 14 |
+
__pycache__/
|
| 15 |
+
*.py[cod]
|
| 16 |
+
*.pyo
|
| 17 |
+
.pytest_cache/
|
| 18 |
+
.mypy_cache/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
dist/
|
| 21 |
+
build/
|
| 22 |
+
.eggs/
|
| 23 |
+
|
| 24 |
+
# Virtual envs
|
| 25 |
+
.venv/
|
| 26 |
+
venv/
|
| 27 |
+
env/
|
| 28 |
+
|
| 29 |
+
# IDE / editor
|
| 30 |
+
.vscode/
|
| 31 |
+
.idea/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
.DS_Store
|
| 35 |
+
|
| 36 |
+
# Local dev overrides
|
| 37 |
+
.env
|
| 38 |
+
.env.*
|
| 39 |
+
docker-compose*.yml
|
| 40 |
+
*.local
|
| 41 |
+
|
| 42 |
+
# Test artefacts
|
| 43 |
+
tests/
|
| 44 |
+
test_*
|
| 45 |
+
coverage.xml
|
| 46 |
+
.coverage
|
.gitignore
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ββ .gitignore ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
|
| 3 |
+
# Python
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.py[cod]
|
| 6 |
+
*.pyo
|
| 7 |
+
.pytest_cache/
|
| 8 |
+
*.egg-info/
|
| 9 |
+
dist/
|
| 10 |
+
build/
|
| 11 |
+
.eggs/
|
| 12 |
+
.venv/
|
| 13 |
+
venv/
|
| 14 |
+
env/
|
| 15 |
+
|
| 16 |
+
# Environment / secrets
|
| 17 |
+
.env
|
| 18 |
+
.env.*
|
| 19 |
+
*.secret
|
| 20 |
+
|
| 21 |
+
# IDE
|
| 22 |
+
.vscode/
|
| 23 |
+
.idea/
|
| 24 |
+
*.swp
|
| 25 |
+
*.swo
|
| 26 |
+
.DS_Store
|
| 27 |
+
Thumbs.db
|
| 28 |
+
|
| 29 |
+
# Docker local artifacts
|
| 30 |
+
docker-compose*.yml
|
| 31 |
+
*.local
|
| 32 |
+
|
| 33 |
+
# Runtime data (stays in /data, not in repo)
|
| 34 |
+
data/
|
| 35 |
+
logs/
|
| 36 |
+
*.log
|
| 37 |
+
|
| 38 |
+
# OS
|
| 39 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM ubuntu:24.04
|
| 2 |
+
|
| 3 |
+
# ββ Metadata ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
LABEL maintainer="HuggingFace Space"
|
| 5 |
+
LABEL description="Ubuntu 24.04 XFCE Desktop via noVNC β PRoot-Distro Style"
|
| 6 |
+
LABEL version="1.0.0"
|
| 7 |
+
|
| 8 |
+
# ββ Environment βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
+
ENV DEBIAN_FRONTEND=noninteractive \
|
| 10 |
+
TZ=UTC \
|
| 11 |
+
LANG=en_US.UTF-8 \
|
| 12 |
+
LANGUAGE=en_US:en \
|
| 13 |
+
LC_ALL=en_US.UTF-8 \
|
| 14 |
+
DISPLAY=:1 \
|
| 15 |
+
VNC_PORT=5901 \
|
| 16 |
+
NOVNC_PORT=6080 \
|
| 17 |
+
DASH_PORT=5000 \
|
| 18 |
+
NGINX_PORT=7860 \
|
| 19 |
+
RESOLUTION=1280x720 \
|
| 20 |
+
COLOR_DEPTH=16 \
|
| 21 |
+
VNC_PASSWORD="" \
|
| 22 |
+
HOME=/data/home \
|
| 23 |
+
USER=ubuntu \
|
| 24 |
+
XDG_RUNTIME_DIR=/tmp/runtime-ubuntu
|
| 25 |
+
|
| 26 |
+
# ββ Stage 1: base tools + locale ββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
+
# Install curl/gnupg/ca-certificates first so we can add the Mozilla apt repo.
|
| 28 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 29 |
+
locales \
|
| 30 |
+
ca-certificates \
|
| 31 |
+
curl \
|
| 32 |
+
wget \
|
| 33 |
+
gnupg \
|
| 34 |
+
&& locale-gen en_US.UTF-8 \
|
| 35 |
+
&& update-locale LANG=en_US.UTF-8 \
|
| 36 |
+
&& apt-get clean \
|
| 37 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 38 |
+
|
| 39 |
+
# ββ Stage 2: Mozilla apt repo βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
+
# firefox-esr is a Debian package; Ubuntu 24.04 ships Firefox as a snap which
|
| 41 |
+
# does NOT work inside Docker containers. Mozilla's official deb repo is the
|
| 42 |
+
# correct fix for Ubuntu 24.04 Docker images.
|
| 43 |
+
RUN install -d -m 0755 /etc/apt/keyrings \
|
| 44 |
+
&& curl -fsSL https://packages.mozilla.org/apt/repo-signing-key.gpg \
|
| 45 |
+
-o /etc/apt/keyrings/packages.mozilla.org.asc \
|
| 46 |
+
&& echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] \
|
| 47 |
+
https://packages.mozilla.org/apt mozilla main" \
|
| 48 |
+
> /etc/apt/sources.list.d/mozilla.list \
|
| 49 |
+
&& printf 'Package: *\nPin: origin packages.mozilla.org\nPin-Priority: 1000\n' \
|
| 50 |
+
> /etc/apt/preferences.d/mozilla
|
| 51 |
+
|
| 52 |
+
# ββ Stage 3: full package install βββββββββββββββββββββββββββββββββββββββββββ
|
| 53 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 54 |
+
# Desktop
|
| 55 |
+
xfce4 \
|
| 56 |
+
xfce4-goodies \
|
| 57 |
+
xfce4-terminal \
|
| 58 |
+
xfce4-taskmanager \
|
| 59 |
+
# VNC (tigervnc provides Xvnc binary; more reliable on 24.04 than tightvnc)
|
| 60 |
+
tigervnc-standalone-server \
|
| 61 |
+
tigervnc-tools \
|
| 62 |
+
# noVNC + bridge
|
| 63 |
+
novnc \
|
| 64 |
+
websockify \
|
| 65 |
+
# X11 helpers
|
| 66 |
+
dbus-x11 \
|
| 67 |
+
x11-xserver-utils \
|
| 68 |
+
x11-utils \
|
| 69 |
+
xdg-utils \
|
| 70 |
+
# Browser β resolved from Mozilla apt repo added in Stage 2
|
| 71 |
+
firefox-esr \
|
| 72 |
+
# Web server
|
| 73 |
+
nginx \
|
| 74 |
+
# Process manager
|
| 75 |
+
supervisor \
|
| 76 |
+
# Python runtime
|
| 77 |
+
python3 \
|
| 78 |
+
python3-pip \
|
| 79 |
+
python3-venv \
|
| 80 |
+
# Shell tools
|
| 81 |
+
nano \
|
| 82 |
+
htop \
|
| 83 |
+
git \
|
| 84 |
+
unzip \
|
| 85 |
+
zip \
|
| 86 |
+
net-tools \
|
| 87 |
+
procps \
|
| 88 |
+
sudo \
|
| 89 |
+
&& apt-get autoremove -y \
|
| 90 |
+
&& apt-get clean \
|
| 91 |
+
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
| 92 |
+
|
| 93 |
+
# ββ Create ubuntu user βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
RUN useradd -m -s /bin/bash -u 1000 ubuntu \
|
| 95 |
+
&& echo "ubuntu:ubuntu" | chpasswd \
|
| 96 |
+
&& echo "ubuntu ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \
|
| 97 |
+
&& mkdir -p /tmp/runtime-ubuntu \
|
| 98 |
+
&& chown ubuntu:ubuntu /tmp/runtime-ubuntu \
|
| 99 |
+
&& chmod 700 /tmp/runtime-ubuntu
|
| 100 |
+
|
| 101 |
+
# ββ Python dependencies ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 102 |
+
COPY requirements.txt /tmp/requirements.txt
|
| 103 |
+
RUN pip3 install --break-system-packages --no-cache-dir -r /tmp/requirements.txt
|
| 104 |
+
|
| 105 |
+
# ββ Application files ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
RUN mkdir -p /app
|
| 107 |
+
COPY app.py /app/app.py
|
| 108 |
+
COPY healthcheck.py /app/healthcheck.py
|
| 109 |
+
|
| 110 |
+
# ββ Config files βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 111 |
+
COPY supervisord.conf /etc/supervisor/conf.d/desktop.conf
|
| 112 |
+
COPY nginx.conf /etc/nginx/sites-available/default
|
| 113 |
+
|
| 114 |
+
# ββ Shell scripts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 115 |
+
COPY start.sh /start.sh
|
| 116 |
+
COPY setup_storage.sh /setup_storage.sh
|
| 117 |
+
COPY setup_ubuntu.sh /setup_ubuntu.sh
|
| 118 |
+
COPY setup_vnc.sh /setup_vnc.sh
|
| 119 |
+
|
| 120 |
+
RUN chmod +x /start.sh /setup_storage.sh /setup_ubuntu.sh /setup_vnc.sh
|
| 121 |
+
|
| 122 |
+
# ββ Remove default nginx site if it conflicts ββββββββββββββββββββββββββββββββ
|
| 123 |
+
RUN rm -f /etc/nginx/sites-enabled/default \
|
| 124 |
+
&& ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
|
| 125 |
+
|
| 126 |
+
# ββ Expose HF Spaces port ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 127 |
+
EXPOSE 7860
|
| 128 |
+
|
| 129 |
+
# ββ Health check βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 130 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
|
| 131 |
+
CMD python3 /app/healthcheck.py
|
| 132 |
+
|
| 133 |
+
# ββ Entry point ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
+
CMD ["/start.sh"]
|
IMPROVED_PROMPT.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Improved Prompt: Ubuntu Desktop on Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
## What Changed & Why
|
| 6 |
+
|
| 7 |
+
The original prompt was good but had several gaps that would cause an LLM to generate
|
| 8 |
+
broken or incomplete code. Below is the improved version, followed by a diff-style
|
| 9 |
+
notes section.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## β
Improved Prompt
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
### Goal
|
| 18 |
+
|
| 19 |
+
Build a fully working **Docker-based Hugging Face Space** that serves an Ubuntu 24.04
|
| 20 |
+
XFCE desktop in the browser via noVNC β no manual setup, directly deployable.
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
### Runtime Stack (be explicit)
|
| 25 |
+
|
| 26 |
+
| Layer | Chosen tool | Why |
|
| 27 |
+
|---|---|---|
|
| 28 |
+
| Base image | `ubuntu:24.04` | LTS, matches target |
|
| 29 |
+
| VNC server | **tigervnc-standalone-server** (`Xvnc` binary) | tightvncserver is a Perl wrapper that forks and daemonises itself; tigervnc runs in foreground β required for supervisor to manage it cleanly |
|
| 30 |
+
| noVNC static files | `novnc` apt package | `/usr/share/novnc/` |
|
| 31 |
+
| WebSocket bridge | `websockify` | Bridges noVNC WS to VNC TCP |
|
| 32 |
+
| Web server | `nginx` on **port 7860** | HF Spaces only exposes 7860 externally |
|
| 33 |
+
| Desktop | XFCE4 | Low RAM; works headless |
|
| 34 |
+
| Dashboard | Flask + psutil | Simple stats UI |
|
| 35 |
+
| Process manager | `supervisord` | Manages all 5 processes; restarts on crash |
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
### Critical Hugging Face Spaces constraints
|
| 40 |
+
|
| 41 |
+
These must be respected β a naive implementation breaks in all of them:
|
| 42 |
+
|
| 43 |
+
1. **Port**: The container MUST listen on **port 7860**. HF does not expose any other port.
|
| 44 |
+
2. **WebSocket path**: The noVNC launcher page must derive `host`, `port`, and `path`
|
| 45 |
+
**dynamically from `window.location`** in JavaScript. Hard-coded URLs break because
|
| 46 |
+
every Space has a unique subdomain (`username-spacename.hf.space`) and all traffic
|
| 47 |
+
comes in on port 443 externally (mapped to 7860 internally via Cloudflare).
|
| 48 |
+
3. **Persistent storage**: HF mounts `/data` only if **Persistent Storage is enabled**
|
| 49 |
+
in Space settings. The code must handle the case where `/data` does not exist
|
| 50 |
+
(create an in-container fallback) and log a warning.
|
| 51 |
+
4. **Space sleeping**: Free CPU Spaces sleep after ~15 min of no browser traffic.
|
| 52 |
+
This is expected and acceptable. Do NOT try to fight it with background polling
|
| 53 |
+
(violates HF ToS). The `HEALTHCHECK` in the Dockerfile keeps the container
|
| 54 |
+
internally healthy; sleeping is controlled by HF's infrastructure.
|
| 55 |
+
5. **Supervisor + Xvnc**: Xvnc must run in **foreground** (supervisord manages it).
|
| 56 |
+
Never use `tightvncserver` or `vncserver` wrapper scripts β they fork and exit,
|
| 57 |
+
making supervisor think the process died.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
### Files to generate (13 total)
|
| 62 |
+
|
| 63 |
+
Generate **complete, production-ready** source for every file. No placeholders,
|
| 64 |
+
no `# TODO` comments, no `...` ellipsis. Every file must be runnable as-is.
|
| 65 |
+
|
| 66 |
+
| File | Purpose |
|
| 67 |
+
|---|---|
|
| 68 |
+
| `Dockerfile` | Multi-stage aware; installs all packages; sets up user; runs `start.sh` |
|
| 69 |
+
| `start.sh` | Entrypoint: calls setup scripts in order, then `exec supervisord` |
|
| 70 |
+
| `setup_storage.sh` | Creates `/data` directory tree; logs warning if `/data` is missing |
|
| 71 |
+
| `setup_ubuntu.sh` | Configures ubuntu user env; symlinks home dirs to `/data` |
|
| 72 |
+
| `setup_vnc.sh` | Writes `/data/vnc/xstartup` for XFCE; optional VNC password from env |
|
| 73 |
+
| `supervisord.conf` | Manages: `xvnc`, `xfce4`, `websockify`, `dashboard`, `nginx` |
|
| 74 |
+
| `nginx.conf` | Port 7860 β routes `/websockify` (WS), `/vnc/` (static), `/dashboard`, `/api/`, `/` (launcher HTML) |
|
| 75 |
+
| `app.py` | Flask: `/dashboard` (HTML), `/api/stats` (JSON), `/api/logs/<name>`, `/api/restart/<name>`, `/health` |
|
| 76 |
+
| `healthcheck.py` | TCP-checks ports 7860, 6080, 5901, 5000; used by Docker HEALTHCHECK |
|
| 77 |
+
| `requirements.txt` | `flask>=3.0.0` and `psutil>=5.9.0` only |
|
| 78 |
+
| `README.md` | Deployment steps, architecture diagram, troubleshooting, persistent storage guide |
|
| 79 |
+
| `.dockerignore` | Excludes `.git`, docs, Python cache, IDE files |
|
| 80 |
+
| `.gitignore` | Standard Python + editor ignores |
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
### `/data` directory layout
|
| 85 |
+
|
| 86 |
+
```
|
| 87 |
+
/data/
|
| 88 |
+
βββ home/ β ubuntu user $HOME (symlinked from /home/ubuntu)
|
| 89 |
+
β βββ .config/xfce4/ β XFCE session config (persists themes, panels)
|
| 90 |
+
β βββ Desktop/ β desktop shortcuts
|
| 91 |
+
β βββ Downloads/ β symlinked to /data/downloads
|
| 92 |
+
βββ workspace/ β user project files
|
| 93 |
+
βββ downloads/ β browser + wget downloads
|
| 94 |
+
βββ firefox/ β Firefox profile (symlinked from ~/.mozilla/firefox)
|
| 95 |
+
βββ vnc/ β VNC state: xstartup, .Xauthority, optional passwd
|
| 96 |
+
βββ logs/ β all supervisor + service logs
|
| 97 |
+
βββ cache/ β XDG_CACHE_HOME
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
### Service startup order (supervisor priority)
|
| 103 |
+
|
| 104 |
+
```
|
| 105 |
+
100 xvnc β must start first (creates :1 display)
|
| 106 |
+
200 xfce4 β sleep 2s then dbus-launch startxfce4 on DISPLAY=:1
|
| 107 |
+
300 websockify β bridges localhost:5901 β WS :6080
|
| 108 |
+
400 dashboard β Flask on localhost:5000
|
| 109 |
+
500 nginx β public-facing on :7860
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
Each must have `autorestart=true`, `startretries=10`, and per-service log files
|
| 113 |
+
under `/data/logs/`.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
### noVNC launcher page (critical detail)
|
| 118 |
+
|
| 119 |
+
The root URL (`/`) must return an HTML page with this JavaScript logic:
|
| 120 |
+
|
| 121 |
+
```javascript
|
| 122 |
+
const host = window.location.hostname;
|
| 123 |
+
const port = window.location.port || (location.protocol === 'https:' ? '443' : '80');
|
| 124 |
+
const params = new URLSearchParams({
|
| 125 |
+
host, port,
|
| 126 |
+
path: 'websockify',
|
| 127 |
+
autoconnect: 'true',
|
| 128 |
+
reconnect: 'true',
|
| 129 |
+
reconnect_delay: '3000',
|
| 130 |
+
resize: 'scale',
|
| 131 |
+
quality: '6',
|
| 132 |
+
compression: '2',
|
| 133 |
+
});
|
| 134 |
+
window.location.href = `/vnc/vnc.html?${params}`;
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
This ensures the correct `wss://` WebSocket URL is constructed regardless of
|
| 138 |
+
the Space's subdomain.
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
### Dashboard requirements (app.py)
|
| 143 |
+
|
| 144 |
+
- Dark-themed single-page HTML served inline (no separate template files)
|
| 145 |
+
- Stats cards: CPU %, RAM (used/total GB + %), Disk `/data` (used/total GB + %), session uptime
|
| 146 |
+
- Services table: name, status badge (Running/Stopped/other), PID, Restart button
|
| 147 |
+
- Log viewer: dropdown to pick service log, last 100 lines, auto-scroll toggle, 5s live refresh toggle
|
| 148 |
+
- All stats polled via `/api/stats` every 5 seconds with `fetch()`
|
| 149 |
+
- Restart POSTs to `/api/restart/<name>` which calls `supervisorctl restart <name>`
|
| 150 |
+
|
| 151 |
+
---
|
| 152 |
+
|
| 153 |
+
### Performance targets (free CPU tier)
|
| 154 |
+
|
| 155 |
+
| Metric | Target |
|
| 156 |
+
|---|---|
|
| 157 |
+
| Idle RAM | < 450 MB |
|
| 158 |
+
| Startup time | < 90 seconds |
|
| 159 |
+
| VNC resolution | 1280Γ720 |
|
| 160 |
+
| Colour depth | 16-bit |
|
| 161 |
+
| nginx workers | 1 |
|
| 162 |
+
| worker_connections | 256 |
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
### Output format
|
| 167 |
+
|
| 168 |
+
- Output each file in a separate fenced code block labelled with the filename
|
| 169 |
+
- Include a brief (1β2 sentence) comment block at the top of each file explaining its role
|
| 170 |
+
- No extra prose between files once generation starts
|
| 171 |
+
|
| 172 |
+
---
|
| 173 |
+
|
| 174 |
+
## π What the Original Prompt Was Missing
|
| 175 |
+
|
| 176 |
+
| Gap | Impact | Fix in improved prompt |
|
| 177 |
+
|---|---|---|
|
| 178 |
+
| Used "TightVNCServer" | tightvncserver daemonises itself; supervisor thinks it crashed and restart-loops | Specified tigervnc `Xvnc` binary in foreground |
|
| 179 |
+
| No mention of port 7860 constraint | LLM might expose port 80 or 443 which don't work on HF | Explicitly stated under "Critical constraints" |
|
| 180 |
+
| Hard-coded noVNC URL | Breaks on any Space URL (every Space has a unique subdomain) | Specified dynamic `window.location`-based URL construction |
|
| 181 |
+
| `/data` missing fallback | If user forgets to enable Persistent Storage, container panics | Specified graceful fallback with warning |
|
| 182 |
+
| "Prevent instant Space pausing" with no method | LLM might generate polling scripts that violate HF ToS | Clarified sleep is expected; HEALTHCHECK keeps container healthy |
|
| 183 |
+
| No supervisor priority/ordering | Xvnc might not be ready when XFCE4 starts β black screen | Explicit priority table with sleep 2s in xfce4 command |
|
| 184 |
+
| No WebSocket nginx config details | LLM often forgets `Upgrade`/`Connection` headers β WS 502 | Explicit nginx WS proxy headers required |
|
| 185 |
+
| Dashboard only described as "Flask" | LLM generates skeleton with no real content | Full functional spec: routes, polling interval, log allowlist |
|
| 186 |
+
| "Use low RAM" vague | No concrete targets | Explicit RAM/startup/nginx worker targets |
|
README.md
CHANGED
|
@@ -1,13 +1,216 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version: 6.14.0
|
| 8 |
-
python_version: '3.13'
|
| 9 |
-
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Ubuntu Desktop
|
| 3 |
+
emoji: π§
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# π§ Ubuntu Desktop on Hugging Face Spaces
|
| 11 |
+
|
| 12 |
+
A browser-accessible **Ubuntu 24.04 XFCE Desktop** running on a free Hugging Face
|
| 13 |
+
Docker Space β inspired by Android Termux + PRoot-Distro workflows, adapted for
|
| 14 |
+
cloud deployment with zero manual setup.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## β¨ What You Get
|
| 19 |
+
|
| 20 |
+
| Feature | Detail |
|
| 21 |
+
|---|---|
|
| 22 |
+
| **Desktop** | Ubuntu 24.04 + XFCE4, accessible from any browser |
|
| 23 |
+
| **VNC** | TigerVNC (Xvnc) at 1280Γ720, 16-bit colour, 24 FPS |
|
| 24 |
+
| **Browser Access** | noVNC via WebSocket β no VNC client needed |
|
| 25 |
+
| **Persistent Storage** | All data stored in `/data` β survives Space restarts |
|
| 26 |
+
| **Dashboard** | Flask app with live CPU / RAM / disk stats + log viewer |
|
| 27 |
+
| **Browser** | Firefox ESR pre-installed |
|
| 28 |
+
| **Free CPU Tier** | Optimised for low RAM + CPU (β 400 MB idle) |
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
## π Deployment to Hugging Face Spaces
|
| 33 |
+
|
| 34 |
+
### Prerequisites
|
| 35 |
+
|
| 36 |
+
- A [Hugging Face](https://huggingface.co) account
|
| 37 |
+
- Git installed locally (or use the HF web UI)
|
| 38 |
+
|
| 39 |
+
### Step 1 β Create a new Space
|
| 40 |
+
|
| 41 |
+
1. Go to **huggingface.co/new-space**
|
| 42 |
+
2. Choose **Docker** as the Space SDK
|
| 43 |
+
3. Set hardware to **CPU Basic** (free tier)
|
| 44 |
+
4. Enable **Persistent Storage** in the Space settings (required for `/data`)
|
| 45 |
+
|
| 46 |
+
### Step 2 β Upload the files
|
| 47 |
+
|
| 48 |
+
Either use the HF web editor to paste each file, or clone the Space repo and push:
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
git clone https://huggingface.co/spaces/<YOUR_USERNAME>/<YOUR_SPACE_NAME>
|
| 52 |
+
cd <YOUR_SPACE_NAME>
|
| 53 |
+
|
| 54 |
+
# Copy all project files here
|
| 55 |
+
cp -r /path/to/hf-ubuntu-space/* .
|
| 56 |
+
|
| 57 |
+
git add .
|
| 58 |
+
git commit -m "Initial Ubuntu Desktop Space"
|
| 59 |
+
git push
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
### Step 3 β Wait for the build
|
| 63 |
+
|
| 64 |
+
The first build takes **5β8 minutes** (downloading Ubuntu + XFCE packages).
|
| 65 |
+
Watch the **Build Logs** tab in the Space UI.
|
| 66 |
+
|
| 67 |
+
### Step 4 β Access your desktop
|
| 68 |
+
|
| 69 |
+
Once the Space shows **Running**, click the **App** tab, or open:
|
| 70 |
+
|
| 71 |
+
```
|
| 72 |
+
https://<username>-<spacename>.hf.space
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Click **π₯ Launch Desktop** to open the noVNC browser desktop.
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
## π₯ Browser Desktop Access
|
| 80 |
+
|
| 81 |
+
| URL | Purpose |
|
| 82 |
+
|---|---|
|
| 83 |
+
| `https://β¦hf.space/` | Launcher page (recommended) |
|
| 84 |
+
| `https://β¦hf.space/vnc/vnc.html` | noVNC direct (raw interface) |
|
| 85 |
+
| `https://β¦hf.space/dashboard` | System stats dashboard |
|
| 86 |
+
|
| 87 |
+
The launcher page auto-detects your Space's hostname and builds the correct
|
| 88 |
+
WebSocket URL for noVNC β this is important because HF Spaces use Cloudflare
|
| 89 |
+
proxying where the port is always 443 externally.
|
| 90 |
+
|
| 91 |
+
### Mobile browsers
|
| 92 |
+
|
| 93 |
+
noVNC works on mobile. Use the scale/resize button (β€’) in the noVNC toolbar
|
| 94 |
+
to fit the 1280Γ720 desktop to your screen.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## πΎ Persistent Storage
|
| 99 |
+
|
| 100 |
+
All state lives under `/data` (Hugging Face's persistent volume):
|
| 101 |
+
|
| 102 |
+
```
|
| 103 |
+
/data/
|
| 104 |
+
βββ home/ β ubuntu user home (~/.bashrc, Desktop, etc.)
|
| 105 |
+
βββ workspace/ β your project files (persists forever)
|
| 106 |
+
βββ downloads/ β browser and wget downloads
|
| 107 |
+
βββ firefox/ β Firefox profile (bookmarks, passwords, etc.)
|
| 108 |
+
βββ xfce/ β XFCE session config, wallpaper, themes
|
| 109 |
+
βββ vnc/ β VNC server state + xstartup
|
| 110 |
+
βββ logs/ β all service logs
|
| 111 |
+
βββ cache/ β app-level cache
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
> **Note:** You must enable **Persistent Storage** in your Space settings.
|
| 115 |
+
> Without it `/data` is ephemeral and resets on every restart.
|
| 116 |
+
|
| 117 |
+
---
|
| 118 |
+
|
| 119 |
+
## βοΈ PRoot-Distro Similarities
|
| 120 |
+
|
| 121 |
+
This Space simulates the Termux + PRoot-Distro workflow:
|
| 122 |
+
|
| 123 |
+
| Termux / PRoot | This Space |
|
| 124 |
+
|---|---|
|
| 125 |
+
| `pkg install proot-distro` | Base Docker layer |
|
| 126 |
+
| `proot-distro install ubuntu` | `FROM ubuntu:24.04` in Dockerfile |
|
| 127 |
+
| `proot-distro login ubuntu` | Container starts automatically |
|
| 128 |
+
| `~` (Termux home) | `/data/home` (persisted) |
|
| 129 |
+
| Internal storage | `/data/workspace` |
|
| 130 |
+
|
| 131 |
+
The key difference is that this runs Docker natively (not PRoot), giving full
|
| 132 |
+
kernel-level isolation without the overhead of a userspace chroot.
|
| 133 |
+
|
| 134 |
+
---
|
| 135 |
+
|
| 136 |
+
## π§ Configuration
|
| 137 |
+
|
| 138 |
+
### Environment variables (set in Space settings)
|
| 139 |
+
|
| 140 |
+
| Variable | Default | Description |
|
| 141 |
+
|---|---|---|
|
| 142 |
+
| `VNC_PASSWORD` | _(empty)_ | Set a VNC password (optional; localhost-only anyway) |
|
| 143 |
+
| `RESOLUTION` | `1280x720` | Desktop resolution |
|
| 144 |
+
| `COLOR_DEPTH` | `16` | VNC colour depth (16 or 24) |
|
| 145 |
+
|
| 146 |
+
### Changing resolution
|
| 147 |
+
|
| 148 |
+
Edit `supervisord.conf`, update the Xvnc command:
|
| 149 |
+
|
| 150 |
+
```ini
|
| 151 |
+
command=Xvnc :1 -geometry 1920x1080 -depth 24 ...
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## π Troubleshooting
|
| 157 |
+
|
| 158 |
+
### Desktop doesn't load / black screen
|
| 159 |
+
|
| 160 |
+
1. Open the **Dashboard** β check all services show **Running**
|
| 161 |
+
2. Check **Xvnc** and **XFCE4** logs for errors
|
| 162 |
+
3. Try restarting XFCE4 from the dashboard restart button
|
| 163 |
+
|
| 164 |
+
### "Failed to connect to server" in noVNC
|
| 165 |
+
|
| 166 |
+
- Make sure you're using the **launcher page** (`/`) not a hard-coded URL
|
| 167 |
+
- The WebSocket path must be `/websockify` β check the URL bar in noVNC shows the correct host
|
| 168 |
+
|
| 169 |
+
### Space keeps sleeping
|
| 170 |
+
|
| 171 |
+
HF free CPU Spaces sleep after ~15 min of no browser traffic. This is expected.
|
| 172 |
+
Wake it by opening the Space URL. Your `/data` files are always preserved.
|
| 173 |
+
|
| 174 |
+
### High memory usage
|
| 175 |
+
|
| 176 |
+
XFCE4 uses ~300β400 MB at idle. With Firefox open, expect 800 MB+. The free
|
| 177 |
+
CPU tier has 16 GB RAM, so this is fine. If you see OOM kills, close unused
|
| 178 |
+
browser tabs in the desktop.
|
| 179 |
+
|
| 180 |
+
### Logs
|
| 181 |
+
|
| 182 |
+
All logs are in `/data/logs/` and viewable from the Dashboard log viewer:
|
| 183 |
+
|
| 184 |
+
```
|
| 185 |
+
/data/logs/supervisord.log
|
| 186 |
+
/data/logs/xvnc.log / xvnc_err.log
|
| 187 |
+
/data/logs/xfce4.log / xfce4_err.log
|
| 188 |
+
/data/logs/websockify.log
|
| 189 |
+
/data/logs/dashboard.log
|
| 190 |
+
/data/logs/nginx.log / nginx_error.log
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## π Architecture
|
| 196 |
+
|
| 197 |
+
```
|
| 198 |
+
Browser (HTTPS :443)
|
| 199 |
+
β
|
| 200 |
+
βΌ
|
| 201 |
+
nginx :7860 (HF Spaces default port)
|
| 202 |
+
ββ / ββββββββββββββββββββββββ Launcher HTML (auto-builds WSS URL)
|
| 203 |
+
ββ /vnc/ βββββββββββββββββββ noVNC static assets
|
| 204 |
+
ββ /websockify βββββββββββββ WebSocket proxy βββΊ websockify :6080
|
| 205 |
+
β β
|
| 206 |
+
β Xvnc :5901
|
| 207 |
+
β XFCE4 session
|
| 208 |
+
ββ /dashboard ββββββββββββββ Flask app :5000
|
| 209 |
+
ββ /api/ βββββββββββββββββββ Flask REST API :5000
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## π Licence
|
| 215 |
+
|
| 216 |
+
MIT β use freely, no warranties.
|
app.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py β Flask dashboard for Ubuntu Desktop on Hugging Face Spaces
|
| 3 |
+
Serves: stats API, log viewer, service restart, and embedded desktop
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import subprocess
|
| 8 |
+
import time
|
| 9 |
+
import datetime
|
| 10 |
+
import glob
|
| 11 |
+
import json
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import psutil
|
| 15 |
+
from flask import Flask, jsonify, render_template_string, request
|
| 16 |
+
|
| 17 |
+
app = Flask(__name__)
|
| 18 |
+
START_TIME = time.time()
|
| 19 |
+
|
| 20 |
+
# ββ HTML Template βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
DASHBOARD_HTML = """<!DOCTYPE html>
|
| 22 |
+
<html lang="en">
|
| 23 |
+
<head>
|
| 24 |
+
<meta charset="UTF-8">
|
| 25 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 26 |
+
<title>Ubuntu Space Dashboard</title>
|
| 27 |
+
<style>
|
| 28 |
+
:root {
|
| 29 |
+
--bg: #0d0d1a;
|
| 30 |
+
--surface: #141426;
|
| 31 |
+
--border: #1e1e3c;
|
| 32 |
+
--accent: #6366f1;
|
| 33 |
+
--accent2: #22d3ee;
|
| 34 |
+
--text: #e2e2f0;
|
| 35 |
+
--muted: #6b6b90;
|
| 36 |
+
--green: #34d399;
|
| 37 |
+
--red: #f87171;
|
| 38 |
+
--yellow: #fbbf24;
|
| 39 |
+
--radius: 10px;
|
| 40 |
+
}
|
| 41 |
+
* { margin:0; padding:0; box-sizing:border-box; }
|
| 42 |
+
body {
|
| 43 |
+
background: var(--bg);
|
| 44 |
+
color: var(--text);
|
| 45 |
+
font-family: 'Segoe UI', system-ui, sans-serif;
|
| 46 |
+
font-size: 14px;
|
| 47 |
+
min-height: 100vh;
|
| 48 |
+
}
|
| 49 |
+
a { color: var(--accent); text-decoration: none; }
|
| 50 |
+
a:hover { text-decoration: underline; }
|
| 51 |
+
|
| 52 |
+
/* ββ Layout ββ */
|
| 53 |
+
.layout { display: grid; grid-template-columns: 220px 1fr; min-height: 100vh; }
|
| 54 |
+
.sidebar {
|
| 55 |
+
background: var(--surface);
|
| 56 |
+
border-right: 1px solid var(--border);
|
| 57 |
+
padding: 1.5rem 1rem;
|
| 58 |
+
display: flex; flex-direction: column; gap: 0.25rem;
|
| 59 |
+
}
|
| 60 |
+
.sidebar h2 { font-size: .75rem; color: var(--muted); text-transform: uppercase;
|
| 61 |
+
letter-spacing: .1em; margin: 1rem 0 .4rem .5rem; }
|
| 62 |
+
.sidebar a {
|
| 63 |
+
display: flex; align-items: center; gap: .6rem;
|
| 64 |
+
padding: .55rem .75rem; border-radius: 7px; color: var(--text);
|
| 65 |
+
transition: .15s;
|
| 66 |
+
}
|
| 67 |
+
.sidebar a:hover, .sidebar a.active { background: var(--border); text-decoration: none; }
|
| 68 |
+
.brand { font-size: 1.1rem; font-weight: 700; color: var(--accent);
|
| 69 |
+
margin-bottom: .5rem; padding: .5rem; }
|
| 70 |
+
.brand span { color: var(--text); }
|
| 71 |
+
.main { padding: 1.5rem 2rem; overflow-y: auto; }
|
| 72 |
+
|
| 73 |
+
/* ββ Header ββ */
|
| 74 |
+
.page-header { display: flex; align-items: center; justify-content: space-between;
|
| 75 |
+
margin-bottom: 1.5rem; }
|
| 76 |
+
.page-header h1 { font-size: 1.25rem; font-weight: 600; }
|
| 77 |
+
.uptime { font-size: .8rem; color: var(--muted); }
|
| 78 |
+
|
| 79 |
+
/* ββ Cards grid ββ */
|
| 80 |
+
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
| 81 |
+
gap: 1rem; margin-bottom: 1.5rem; }
|
| 82 |
+
.card {
|
| 83 |
+
background: var(--surface);
|
| 84 |
+
border: 1px solid var(--border);
|
| 85 |
+
border-radius: var(--radius);
|
| 86 |
+
padding: 1.1rem 1.3rem;
|
| 87 |
+
}
|
| 88 |
+
.card-label { font-size: .7rem; text-transform: uppercase; letter-spacing: .08em;
|
| 89 |
+
color: var(--muted); margin-bottom: .35rem; }
|
| 90 |
+
.card-value { font-size: 1.6rem; font-weight: 700; line-height: 1; }
|
| 91 |
+
.card-sub { font-size: .75rem; color: var(--muted); margin-top: .3rem; }
|
| 92 |
+
.bar-wrap { margin-top: .6rem; background: var(--border); border-radius: 4px; height: 6px; }
|
| 93 |
+
.bar-fill { height: 6px; border-radius: 4px; transition: width .4s; }
|
| 94 |
+
|
| 95 |
+
/* ββ Services table ββ */
|
| 96 |
+
.section { background: var(--surface); border: 1px solid var(--border);
|
| 97 |
+
border-radius: var(--radius); margin-bottom: 1.5rem; }
|
| 98 |
+
.section-header { padding: .9rem 1.3rem; border-bottom: 1px solid var(--border);
|
| 99 |
+
font-weight: 600; display: flex; align-items: center; gap: .5rem; }
|
| 100 |
+
table { width: 100%; border-collapse: collapse; }
|
| 101 |
+
th, td { text-align: left; padding: .75rem 1.3rem; border-bottom: 1px solid var(--border); }
|
| 102 |
+
th { font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
|
| 103 |
+
tr:last-child td { border-bottom: none; }
|
| 104 |
+
tr:hover td { background: rgba(99,102,241,.04); }
|
| 105 |
+
|
| 106 |
+
/* ββ Badges ββ */
|
| 107 |
+
.badge {
|
| 108 |
+
display: inline-flex; align-items: center; gap: .35rem;
|
| 109 |
+
padding: .2rem .65rem; border-radius: 20px; font-size: .75rem; font-weight: 600;
|
| 110 |
+
}
|
| 111 |
+
.badge-green { background: rgba(52,211,153,.12); color: var(--green); }
|
| 112 |
+
.badge-red { background: rgba(248,113,113,.12); color: var(--red); }
|
| 113 |
+
.badge-yellow { background: rgba(251,191,36,.12); color: var(--yellow); }
|
| 114 |
+
.dot { width:7px; height:7px; border-radius:50%; background:currentColor; }
|
| 115 |
+
|
| 116 |
+
/* ββ Buttons ββ */
|
| 117 |
+
.btn {
|
| 118 |
+
padding: .4rem .9rem; border-radius: 6px; border: none; cursor: pointer;
|
| 119 |
+
font-size: .8rem; font-weight: 600; transition: .15s;
|
| 120 |
+
}
|
| 121 |
+
.btn-primary { background: var(--accent); color: #fff; }
|
| 122 |
+
.btn-primary:hover { background: #5254d4; }
|
| 123 |
+
.btn-ghost { background: var(--border); color: var(--text); }
|
| 124 |
+
.btn-ghost:hover { background: #2a2a4e; }
|
| 125 |
+
.btn-danger { background: rgba(248,113,113,.15); color: var(--red); }
|
| 126 |
+
.btn-danger:hover { background: rgba(248,113,113,.25); }
|
| 127 |
+
|
| 128 |
+
/* ββ Log viewer ββ */
|
| 129 |
+
.log-box {
|
| 130 |
+
background: #060610; border-radius: 0 0 var(--radius) var(--radius);
|
| 131 |
+
padding: 1rem 1.3rem; font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 132 |
+
font-size: .78rem; color: #9090c0; height: 280px;
|
| 133 |
+
overflow-y: auto; white-space: pre-wrap; word-break: break-all;
|
| 134 |
+
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
| 135 |
+
}
|
| 136 |
+
.log-controls { padding: .75rem 1.3rem; border-bottom: 1px solid var(--border);
|
| 137 |
+
display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
|
| 138 |
+
select { background: var(--border); color: var(--text); border: none;
|
| 139 |
+
border-radius: 5px; padding: .35rem .6rem; font-size: .8rem; }
|
| 140 |
+
|
| 141 |
+
/* ββ Launch bar ββ */
|
| 142 |
+
.launch-bar {
|
| 143 |
+
background: linear-gradient(135deg, rgba(99,102,241,.15), rgba(34,211,238,.08));
|
| 144 |
+
border: 1px solid rgba(99,102,241,.3);
|
| 145 |
+
border-radius: var(--radius); padding: 1.3rem 1.5rem;
|
| 146 |
+
display: flex; align-items: center; justify-content: space-between;
|
| 147 |
+
margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;
|
| 148 |
+
}
|
| 149 |
+
.launch-bar h2 { font-size: 1rem; font-weight: 600; }
|
| 150 |
+
.launch-bar p { font-size: .8rem; color: var(--muted); margin-top: .2rem; }
|
| 151 |
+
|
| 152 |
+
/* Responsive */
|
| 153 |
+
@media (max-width: 640px) {
|
| 154 |
+
.layout { grid-template-columns: 1fr; }
|
| 155 |
+
.sidebar { display: none; }
|
| 156 |
+
.main { padding: 1rem; }
|
| 157 |
+
}
|
| 158 |
+
</style>
|
| 159 |
+
</head>
|
| 160 |
+
<body>
|
| 161 |
+
<div class="layout">
|
| 162 |
+
|
| 163 |
+
<!-- Sidebar -->
|
| 164 |
+
<aside class="sidebar">
|
| 165 |
+
<div class="brand">π§ <span>Ubuntu Space</span></div>
|
| 166 |
+
<h2>Navigation</h2>
|
| 167 |
+
<a href="/dashboard" class="active">π Dashboard</a>
|
| 168 |
+
<a href="/" >π₯ Desktop</a>
|
| 169 |
+
<a href="/vnc/vnc.html" target="_blank">π noVNC Direct</a>
|
| 170 |
+
<a href="#logs">π Logs</a>
|
| 171 |
+
<h2>Info</h2>
|
| 172 |
+
<a href="https://huggingface.co/docs/hub/spaces" target="_blank">π HF Spaces Docs</a>
|
| 173 |
+
</aside>
|
| 174 |
+
|
| 175 |
+
<!-- Main -->
|
| 176 |
+
<main class="main">
|
| 177 |
+
<div class="page-header">
|
| 178 |
+
<h1>System Dashboard</h1>
|
| 179 |
+
<span class="uptime" id="uptime-label">Loadingβ¦</span>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<!-- Launch bar -->
|
| 183 |
+
<div class="launch-bar">
|
| 184 |
+
<div>
|
| 185 |
+
<h2>Ubuntu 24.04 XFCE Desktop</h2>
|
| 186 |
+
<p>PRoot-Distro style isolated environment on Hugging Face Spaces</p>
|
| 187 |
+
</div>
|
| 188 |
+
<div style="display:flex;gap:.75rem;flex-wrap:wrap">
|
| 189 |
+
<a href="/" class="btn btn-primary">π₯ Open Desktop</a>
|
| 190 |
+
<button class="btn btn-ghost" onclick="refreshAll()">π Refresh</button>
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
<!-- Stats cards -->
|
| 195 |
+
<div class="cards" id="stats-cards">
|
| 196 |
+
<div class="card">
|
| 197 |
+
<div class="card-label">CPU Usage</div>
|
| 198 |
+
<div class="card-value" id="cpu-val">β</div>
|
| 199 |
+
<div class="bar-wrap"><div class="bar-fill" id="cpu-bar" style="background:var(--accent)"></div></div>
|
| 200 |
+
<div class="card-sub" id="cpu-cores">β</div>
|
| 201 |
+
</div>
|
| 202 |
+
<div class="card">
|
| 203 |
+
<div class="card-label">Memory</div>
|
| 204 |
+
<div class="card-value" id="ram-val">β</div>
|
| 205 |
+
<div class="bar-wrap"><div class="bar-fill" id="ram-bar" style="background:var(--accent2)"></div></div>
|
| 206 |
+
<div class="card-sub" id="ram-sub">β</div>
|
| 207 |
+
</div>
|
| 208 |
+
<div class="card">
|
| 209 |
+
<div class="card-label">Storage (/data)</div>
|
| 210 |
+
<div class="card-value" id="disk-val">β</div>
|
| 211 |
+
<div class="bar-wrap"><div class="bar-fill" id="disk-bar" style="background:var(--green)"></div></div>
|
| 212 |
+
<div class="card-sub" id="disk-sub">β</div>
|
| 213 |
+
</div>
|
| 214 |
+
<div class="card">
|
| 215 |
+
<div class="card-label">Session Uptime</div>
|
| 216 |
+
<div class="card-value" id="uptime-val" style="font-size:1.2rem">β</div>
|
| 217 |
+
<div class="card-sub">Space started</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
<!-- Services -->
|
| 222 |
+
<div class="section">
|
| 223 |
+
<div class="section-header">βοΈ Services</div>
|
| 224 |
+
<table>
|
| 225 |
+
<thead>
|
| 226 |
+
<tr><th>Service</th><th>Status</th><th>PID</th><th>Action</th></tr>
|
| 227 |
+
</thead>
|
| 228 |
+
<tbody id="services-tbody">
|
| 229 |
+
<tr><td colspan="4" style="color:var(--muted);text-align:center;padding:1.5rem">Loadingβ¦</td></tr>
|
| 230 |
+
</tbody>
|
| 231 |
+
</table>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<!-- Log viewer -->
|
| 235 |
+
<div class="section" id="logs">
|
| 236 |
+
<div class="section-header">π Logs</div>
|
| 237 |
+
<div class="log-controls">
|
| 238 |
+
<select id="log-select">
|
| 239 |
+
<option value="supervisord">supervisord</option>
|
| 240 |
+
<option value="xvnc">Xvnc</option>
|
| 241 |
+
<option value="xfce4">XFCE4</option>
|
| 242 |
+
<option value="websockify">websockify</option>
|
| 243 |
+
<option value="dashboard">dashboard</option>
|
| 244 |
+
<option value="nginx">nginx</option>
|
| 245 |
+
</select>
|
| 246 |
+
<button class="btn btn-ghost" onclick="loadLog()">π Reload</button>
|
| 247 |
+
<label style="display:flex;align-items:center;gap:.4rem;font-size:.8rem;color:var(--muted)">
|
| 248 |
+
<input type="checkbox" id="auto-scroll" checked> Auto-scroll
|
| 249 |
+
</label>
|
| 250 |
+
<label style="display:flex;align-items:center;gap:.4rem;font-size:.8rem;color:var(--muted)">
|
| 251 |
+
<input type="checkbox" id="auto-refresh-log"> Live (5s)
|
| 252 |
+
</label>
|
| 253 |
+
</div>
|
| 254 |
+
<div class="log-box" id="log-box">Select a log file aboveβ¦</div>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
</main>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<script>
|
| 261 |
+
let logRefreshTimer = null;
|
| 262 |
+
|
| 263 |
+
// ββ Fetch stats ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 264 |
+
async function fetchStats() {
|
| 265 |
+
try {
|
| 266 |
+
const r = await fetch('/api/stats');
|
| 267 |
+
const d = await r.json();
|
| 268 |
+
|
| 269 |
+
// CPU
|
| 270 |
+
document.getElementById('cpu-val').textContent = d.cpu.percent + '%';
|
| 271 |
+
document.getElementById('cpu-bar').style.width = d.cpu.percent + '%';
|
| 272 |
+
document.getElementById('cpu-cores').textContent = `${d.cpu.count} cores`;
|
| 273 |
+
|
| 274 |
+
// RAM
|
| 275 |
+
const ramPct = d.memory.percent;
|
| 276 |
+
document.getElementById('ram-val').textContent = ramPct + '%';
|
| 277 |
+
document.getElementById('ram-bar').style.width = ramPct + '%';
|
| 278 |
+
document.getElementById('ram-sub').textContent =
|
| 279 |
+
`${d.memory.used_gb} / ${d.memory.total_gb} GB`;
|
| 280 |
+
|
| 281 |
+
// Disk
|
| 282 |
+
const diskPct = d.disk.percent;
|
| 283 |
+
document.getElementById('disk-val').textContent = diskPct + '%';
|
| 284 |
+
document.getElementById('disk-bar').style.width = diskPct + '%';
|
| 285 |
+
document.getElementById('disk-sub').textContent =
|
| 286 |
+
`${d.disk.used_gb} / ${d.disk.total_gb} GB`;
|
| 287 |
+
|
| 288 |
+
// Uptime
|
| 289 |
+
document.getElementById('uptime-val').textContent = d.uptime.human;
|
| 290 |
+
document.getElementById('uptime-label').textContent = 'Uptime: ' + d.uptime.human;
|
| 291 |
+
|
| 292 |
+
// Services
|
| 293 |
+
renderServices(d.services);
|
| 294 |
+
} catch(e) {
|
| 295 |
+
console.error('Stats fetch failed:', e);
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
function badgeFor(state) {
|
| 300 |
+
if (state === 'RUNNING') return '<span class="badge badge-green"><span class="dot"></span>Running</span>';
|
| 301 |
+
if (state === 'STOPPED') return '<span class="badge badge-red"><span class="dot"></span>Stopped</span>';
|
| 302 |
+
return `<span class="badge badge-yellow"><span class="dot"></span>${state}</span>`;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
function renderServices(services) {
|
| 306 |
+
const tbody = document.getElementById('services-tbody');
|
| 307 |
+
tbody.innerHTML = services.map(s => `
|
| 308 |
+
<tr>
|
| 309 |
+
<td><strong>${s.name}</strong><br><small style="color:var(--muted)">${s.description}</small></td>
|
| 310 |
+
<td>${badgeFor(s.state)}</td>
|
| 311 |
+
<td style="font-family:monospace;color:var(--muted)">${s.pid || 'β'}</td>
|
| 312 |
+
<td>
|
| 313 |
+
<button class="btn btn-ghost" onclick="restartService('${s.name}')">βΊ Restart</button>
|
| 314 |
+
</td>
|
| 315 |
+
</tr>
|
| 316 |
+
`).join('');
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
async function restartService(name) {
|
| 320 |
+
if (!confirm(`Restart ${name}?`)) return;
|
| 321 |
+
try {
|
| 322 |
+
const r = await fetch(`/api/restart/${name}`, { method: 'POST' });
|
| 323 |
+
const d = await r.json();
|
| 324 |
+
alert(d.message || d.error);
|
| 325 |
+
setTimeout(fetchStats, 1500);
|
| 326 |
+
} catch(e) {
|
| 327 |
+
alert('Restart failed: ' + e);
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// ββ Fetch logs βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 332 |
+
async function loadLog() {
|
| 333 |
+
const name = document.getElementById('log-select').value;
|
| 334 |
+
try {
|
| 335 |
+
const r = await fetch(`/api/logs/${name}`);
|
| 336 |
+
const d = await r.json();
|
| 337 |
+
const box = document.getElementById('log-box');
|
| 338 |
+
box.textContent = d.content || '(empty)';
|
| 339 |
+
if (document.getElementById('auto-scroll').checked)
|
| 340 |
+
box.scrollTop = box.scrollHeight;
|
| 341 |
+
} catch(e) {
|
| 342 |
+
document.getElementById('log-box').textContent = 'Error loading log: ' + e;
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
document.getElementById('log-select').addEventListener('change', loadLog);
|
| 347 |
+
document.getElementById('auto-refresh-log').addEventListener('change', function() {
|
| 348 |
+
clearInterval(logRefreshTimer);
|
| 349 |
+
if (this.checked) logRefreshTimer = setInterval(loadLog, 5000);
|
| 350 |
+
});
|
| 351 |
+
|
| 352 |
+
function refreshAll() { fetchStats(); loadLog(); }
|
| 353 |
+
|
| 354 |
+
// ββ Poll stats every 5 s ββββββββββββββββββββββββββββββββββββββ
|
| 355 |
+
fetchStats();
|
| 356 |
+
loadLog();
|
| 357 |
+
setInterval(fetchStats, 5000);
|
| 358 |
+
</script>
|
| 359 |
+
</body>
|
| 360 |
+
</html>"""
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
# ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 364 |
+
|
| 365 |
+
def _supervisor_ctl(*args) -> str:
|
| 366 |
+
try:
|
| 367 |
+
result = subprocess.run(
|
| 368 |
+
["supervisorctl"] + list(args),
|
| 369 |
+
capture_output=True, text=True, timeout=10
|
| 370 |
+
)
|
| 371 |
+
return result.stdout + result.stderr
|
| 372 |
+
except Exception as e:
|
| 373 |
+
return str(e)
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def _supervisor_status() -> list[dict]:
|
| 377 |
+
"""Parse `supervisorctl status` into structured list."""
|
| 378 |
+
output = _supervisor_ctl("status")
|
| 379 |
+
services = []
|
| 380 |
+
descriptions = {
|
| 381 |
+
"xvnc": "Virtual framebuffer + VNC server",
|
| 382 |
+
"xfce4": "XFCE4 desktop session",
|
| 383 |
+
"websockify": "noVNC β VNC WebSocket bridge",
|
| 384 |
+
"dashboard": "Flask stats dashboard",
|
| 385 |
+
"nginx": "Reverse proxy (port 7860)",
|
| 386 |
+
}
|
| 387 |
+
for line in output.strip().splitlines():
|
| 388 |
+
if not line.strip():
|
| 389 |
+
continue
|
| 390 |
+
parts = line.split()
|
| 391 |
+
if len(parts) < 2:
|
| 392 |
+
continue
|
| 393 |
+
name = parts[0]
|
| 394 |
+
state = parts[1]
|
| 395 |
+
pid = None
|
| 396 |
+
for p in parts:
|
| 397 |
+
if p.startswith("pid"):
|
| 398 |
+
try:
|
| 399 |
+
pid = parts[parts.index(p) + 1].rstrip(",")
|
| 400 |
+
except Exception:
|
| 401 |
+
pass
|
| 402 |
+
services.append({
|
| 403 |
+
"name": name,
|
| 404 |
+
"state": state,
|
| 405 |
+
"pid": pid,
|
| 406 |
+
"description": descriptions.get(name, ""),
|
| 407 |
+
})
|
| 408 |
+
return services
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def _human_uptime(seconds: float) -> str:
|
| 412 |
+
d = int(seconds // 86400)
|
| 413 |
+
h = int((seconds % 86400) // 3600)
|
| 414 |
+
m = int((seconds % 3600) // 60)
|
| 415 |
+
s = int(seconds % 60)
|
| 416 |
+
if d:
|
| 417 |
+
return f"{d}d {h}h {m}m"
|
| 418 |
+
if h:
|
| 419 |
+
return f"{h}h {m}m {s}s"
|
| 420 |
+
return f"{m}m {s}s"
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
# ββ Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 424 |
+
|
| 425 |
+
@app.route("/dashboard")
|
| 426 |
+
def dashboard():
|
| 427 |
+
return render_template_string(DASHBOARD_HTML)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
@app.route("/health")
|
| 431 |
+
def health():
|
| 432 |
+
return jsonify({"status": "ok", "ts": datetime.datetime.utcnow().isoformat()})
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
@app.route("/api/stats")
|
| 436 |
+
def api_stats():
|
| 437 |
+
cpu = psutil.cpu_percent(interval=0.3)
|
| 438 |
+
mem = psutil.virtual_memory()
|
| 439 |
+
try:
|
| 440 |
+
disk = psutil.disk_usage("/data")
|
| 441 |
+
disk_data = {
|
| 442 |
+
"percent": disk.percent,
|
| 443 |
+
"used_gb": round(disk.used / 1e9, 2),
|
| 444 |
+
"total_gb": round(disk.total / 1e9, 2),
|
| 445 |
+
"free_gb": round(disk.free / 1e9, 2),
|
| 446 |
+
}
|
| 447 |
+
except Exception:
|
| 448 |
+
disk_data = {"percent": 0, "used_gb": 0, "total_gb": 0, "free_gb": 0}
|
| 449 |
+
|
| 450 |
+
uptime_secs = time.time() - START_TIME
|
| 451 |
+
|
| 452 |
+
return jsonify({
|
| 453 |
+
"cpu": {
|
| 454 |
+
"percent": round(cpu, 1),
|
| 455 |
+
"count": psutil.cpu_count(),
|
| 456 |
+
},
|
| 457 |
+
"memory": {
|
| 458 |
+
"percent": mem.percent,
|
| 459 |
+
"used_gb": round(mem.used / 1e9, 2),
|
| 460 |
+
"total_gb": round(mem.total / 1e9, 2),
|
| 461 |
+
"free_gb": round(mem.available / 1e9, 2),
|
| 462 |
+
},
|
| 463 |
+
"disk": disk_data,
|
| 464 |
+
"uptime": {
|
| 465 |
+
"seconds": round(uptime_secs),
|
| 466 |
+
"human": _human_uptime(uptime_secs),
|
| 467 |
+
},
|
| 468 |
+
"services": _supervisor_status(),
|
| 469 |
+
"ts": datetime.datetime.utcnow().isoformat(),
|
| 470 |
+
})
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
@app.route("/api/logs/<name>")
|
| 474 |
+
def api_logs(name: str):
|
| 475 |
+
# Allowlist to prevent path traversal
|
| 476 |
+
allowed = {"supervisord", "xvnc", "xfce4", "websockify", "dashboard", "nginx",
|
| 477 |
+
"xvnc_err", "xfce4_err", "websockify_err", "dashboard_err", "nginx_err"}
|
| 478 |
+
if name not in allowed:
|
| 479 |
+
return jsonify({"error": "Unknown log name"}), 400
|
| 480 |
+
|
| 481 |
+
log_path = Path(f"/data/logs/{name}.log")
|
| 482 |
+
if not log_path.exists():
|
| 483 |
+
return jsonify({"content": f"Log file not found: {log_path}"})
|
| 484 |
+
|
| 485 |
+
try:
|
| 486 |
+
# Return last 100 lines
|
| 487 |
+
lines = log_path.read_text(errors="replace").splitlines()
|
| 488 |
+
return jsonify({"content": "\n".join(lines[-100:])})
|
| 489 |
+
except Exception as e:
|
| 490 |
+
return jsonify({"error": str(e)}), 500
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
@app.route("/api/restart/<name>", methods=["POST"])
|
| 494 |
+
def api_restart(name: str):
|
| 495 |
+
allowed = {"xvnc", "xfce4", "websockify", "dashboard", "nginx"}
|
| 496 |
+
if name not in allowed:
|
| 497 |
+
return jsonify({"error": "Unknown service"}), 400
|
| 498 |
+
out = _supervisor_ctl("restart", name)
|
| 499 |
+
return jsonify({"message": out.strip()})
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
# ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 503 |
+
if __name__ == "__main__":
|
| 504 |
+
app.run(host="127.0.0.1", port=5000, debug=False, threaded=True)
|
healthcheck.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
healthcheck.py β Docker HEALTHCHECK script
|
| 4 |
+
Verifies that all critical services are alive.
|
| 5 |
+
Exit 0 = healthy, Exit 1 = unhealthy.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import sys
|
| 9 |
+
import socket
|
| 10 |
+
import subprocess
|
| 11 |
+
|
| 12 |
+
CHECKS = [
|
| 13 |
+
("nginx", "tcp", "127.0.0.1", 7860),
|
| 14 |
+
("websockify", "tcp", "127.0.0.1", 6080),
|
| 15 |
+
("dashboard", "tcp", "127.0.0.1", 5000),
|
| 16 |
+
("Xvnc", "tcp", "127.0.0.1", 5901),
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def tcp_alive(host: str, port: int, timeout: float = 3.0) -> bool:
|
| 21 |
+
try:
|
| 22 |
+
with socket.create_connection((host, port), timeout=timeout):
|
| 23 |
+
return True
|
| 24 |
+
except OSError:
|
| 25 |
+
return False
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def supervisord_alive() -> bool:
|
| 29 |
+
try:
|
| 30 |
+
result = subprocess.run(
|
| 31 |
+
["supervisorctl", "status"],
|
| 32 |
+
capture_output=True, text=True, timeout=5
|
| 33 |
+
)
|
| 34 |
+
return result.returncode == 0
|
| 35 |
+
except Exception:
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def main() -> int:
|
| 40 |
+
failures = []
|
| 41 |
+
|
| 42 |
+
# Check supervisor itself
|
| 43 |
+
if not supervisord_alive():
|
| 44 |
+
failures.append("supervisord not responding")
|
| 45 |
+
|
| 46 |
+
# Check TCP ports
|
| 47 |
+
for name, kind, host, port in CHECKS:
|
| 48 |
+
if not tcp_alive(host, port):
|
| 49 |
+
failures.append(f"{name} not listening on port {port}")
|
| 50 |
+
|
| 51 |
+
if failures:
|
| 52 |
+
for f in failures:
|
| 53 |
+
print(f"UNHEALTHY: {f}", file=sys.stderr)
|
| 54 |
+
return 1
|
| 55 |
+
|
| 56 |
+
print("HEALTHY: all services up")
|
| 57 |
+
return 0
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
sys.exit(main())
|
nginx.conf
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================
|
| 2 |
+
# nginx.conf β Reverse proxy for Ubuntu Desktop Space
|
| 3 |
+
# Port 7860 (HF Spaces default) routes:
|
| 4 |
+
# / β noVNC launcher (auto-connect to desktop)
|
| 5 |
+
# /vnc/ β noVNC static assets
|
| 6 |
+
# /websockify β WebSocket bridge to VNC (via websockify:6080)
|
| 7 |
+
# /dashboard β Flask stats dashboard (port 5000)
|
| 8 |
+
# /api/ β Flask REST API (port 5000)
|
| 9 |
+
# ============================================================
|
| 10 |
+
|
| 11 |
+
# Reduce worker count for free CPU tier
|
| 12 |
+
worker_processes 1;
|
| 13 |
+
worker_rlimit_nofile 1024;
|
| 14 |
+
|
| 15 |
+
events {
|
| 16 |
+
worker_connections 256;
|
| 17 |
+
use epoll;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
http {
|
| 21 |
+
include /etc/nginx/mime.types;
|
| 22 |
+
default_type application/octet-stream;
|
| 23 |
+
|
| 24 |
+
# Logging β /data/logs
|
| 25 |
+
access_log /data/logs/nginx_access.log combined buffer=32k flush=5s;
|
| 26 |
+
error_log /data/logs/nginx_error.log warn;
|
| 27 |
+
|
| 28 |
+
# Performance
|
| 29 |
+
sendfile on;
|
| 30 |
+
tcp_nopush on;
|
| 31 |
+
tcp_nodelay on;
|
| 32 |
+
keepalive_timeout 65;
|
| 33 |
+
gzip on;
|
| 34 |
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
| 35 |
+
|
| 36 |
+
# Upstreams
|
| 37 |
+
upstream novnc_ws {
|
| 38 |
+
server 127.0.0.1:6080;
|
| 39 |
+
keepalive 4;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
upstream dashboard {
|
| 43 |
+
server 127.0.0.1:5000;
|
| 44 |
+
keepalive 4;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
server {
|
| 48 |
+
listen 7860 default_server;
|
| 49 |
+
server_name _;
|
| 50 |
+
|
| 51 |
+
# Large proxy timeouts for persistent VNC sessions
|
| 52 |
+
proxy_read_timeout 3600s;
|
| 53 |
+
proxy_send_timeout 3600s;
|
| 54 |
+
proxy_connect_timeout 10s;
|
| 55 |
+
proxy_buffering off;
|
| 56 |
+
|
| 57 |
+
# ββ noVNC static assets ββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
location /vnc/ {
|
| 59 |
+
alias /usr/share/novnc/;
|
| 60 |
+
expires 1h;
|
| 61 |
+
add_header Cache-Control "public, immutable";
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# ββ WebSocket bridge (noVNC β VNC) ββββββββββββββββββββββββββββββ
|
| 65 |
+
# noVNC connects to wss://host/websockify
|
| 66 |
+
location /websockify {
|
| 67 |
+
proxy_pass http://novnc_ws/;
|
| 68 |
+
proxy_http_version 1.1;
|
| 69 |
+
proxy_set_header Upgrade $http_upgrade;
|
| 70 |
+
proxy_set_header Connection "upgrade";
|
| 71 |
+
proxy_set_header Host $host;
|
| 72 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 73 |
+
proxy_read_timeout 3600s;
|
| 74 |
+
proxy_send_timeout 3600s;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# ββ Flask dashboard ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 78 |
+
location /dashboard {
|
| 79 |
+
proxy_pass http://dashboard/dashboard;
|
| 80 |
+
proxy_set_header Host $host;
|
| 81 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 82 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 83 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# ββ Flask REST API βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 87 |
+
location /api/ {
|
| 88 |
+
proxy_pass http://dashboard/api/;
|
| 89 |
+
proxy_set_header Host $host;
|
| 90 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
# ββ Root: serve launcher page ββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
# A small HTML page that auto-connects noVNC using the *current*
|
| 95 |
+
# browser host, so it works on any HF Space URL.
|
| 96 |
+
location = / {
|
| 97 |
+
return 200 '<!DOCTYPE html>
|
| 98 |
+
<html lang="en">
|
| 99 |
+
<head>
|
| 100 |
+
<meta charset="UTF-8">
|
| 101 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 102 |
+
<title>Ubuntu Desktop β Hugging Face Space</title>
|
| 103 |
+
<style>
|
| 104 |
+
* { margin:0; padding:0; box-sizing:border-box; }
|
| 105 |
+
body { background:#0f0f1a; color:#e0e0ff; font-family: system-ui, sans-serif;
|
| 106 |
+
display:flex; flex-direction:column; align-items:center;
|
| 107 |
+
justify-content:center; height:100vh; gap:1.5rem; }
|
| 108 |
+
h1 { font-size:1.6rem; font-weight:600; }
|
| 109 |
+
p { color:#888; font-size:.9rem; }
|
| 110 |
+
.btn { padding:.75rem 2rem; border-radius:8px; font-size:1rem;
|
| 111 |
+
font-weight:600; cursor:pointer; text-decoration:none;
|
| 112 |
+
border:none; transition:.2s; }
|
| 113 |
+
.primary { background:#5b5bf0; color:#fff; }
|
| 114 |
+
.primary:hover { background:#4a4adc; }
|
| 115 |
+
.secondary{ background:#1e1e3a; color:#9090ff; border:1px solid #5b5bf0; }
|
| 116 |
+
.secondary:hover{ background:#2a2a50; }
|
| 117 |
+
.btns { display:flex; gap:1rem; flex-wrap:wrap; justify-content:center; }
|
| 118 |
+
.tag { background:#1e1e3a; border-radius:20px; padding:.3rem .9rem;
|
| 119 |
+
font-size:.8rem; color:#7070ff; border:1px solid #2e2e5e; }
|
| 120 |
+
.tags { display:flex; gap:.5rem; flex-wrap:wrap; justify-content:center; }
|
| 121 |
+
</style>
|
| 122 |
+
</head>
|
| 123 |
+
<body>
|
| 124 |
+
<h1>π§ Ubuntu 24.04 Desktop</h1>
|
| 125 |
+
<div class="tags">
|
| 126 |
+
<span class="tag">XFCE4</span>
|
| 127 |
+
<span class="tag">noVNC</span>
|
| 128 |
+
<span class="tag">Firefox ESR</span>
|
| 129 |
+
<span class="tag">PRoot-Style</span>
|
| 130 |
+
<span class="tag">Persistent /data</span>
|
| 131 |
+
</div>
|
| 132 |
+
<p>Browser-accessible Ubuntu desktop running on Hugging Face Spaces.</p>
|
| 133 |
+
<div class="btns">
|
| 134 |
+
<a id="vnc-btn" class="btn primary" href="#">π₯ Launch Desktop</a>
|
| 135 |
+
<a href="/dashboard" class="btn secondary">π Dashboard</a>
|
| 136 |
+
</div>
|
| 137 |
+
<p id="status" style="color:#666;font-size:.8rem">Connectingβ¦</p>
|
| 138 |
+
<script>
|
| 139 |
+
// Build noVNC URL using current window host so it works on any HF Space URL
|
| 140 |
+
const host = window.location.hostname;
|
| 141 |
+
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
|
| 142 |
+
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
| 143 |
+
const params = new URLSearchParams({
|
| 144 |
+
host, port,
|
| 145 |
+
path: "websockify",
|
| 146 |
+
autoconnect: "true",
|
| 147 |
+
reconnect: "true",
|
| 148 |
+
reconnect_delay: "3000",
|
| 149 |
+
resize: "scale",
|
| 150 |
+
quality: "6",
|
| 151 |
+
compression: "2",
|
| 152 |
+
show_dot: "false"
|
| 153 |
+
});
|
| 154 |
+
const vncUrl = `/vnc/vnc.html?${params}`;
|
| 155 |
+
document.getElementById("vnc-btn").href = vncUrl;
|
| 156 |
+
document.getElementById("status").textContent = `WebSocket target: ${proto}://${host}:${port}/websockify`;
|
| 157 |
+
</script>
|
| 158 |
+
</body>
|
| 159 |
+
</html>';
|
| 160 |
+
add_header Content-Type "text/html; charset=utf-8";
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
# ββ Health probe (used by HF and Docker HEALTHCHECK) βββββββββββββ
|
| 164 |
+
location = /health {
|
| 165 |
+
proxy_pass http://dashboard/health;
|
| 166 |
+
access_log off;
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ββ Flask dashboard dependencies ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
flask>=3.0.0
|
| 3 |
+
psutil>=5.9.0
|
setup_storage.sh
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# setup_storage.sh β Create /data directory structure
|
| 4 |
+
# All state lives here so it survives HF Space restarts
|
| 5 |
+
# ============================================================
|
| 6 |
+
set -euo pipefail
|
| 7 |
+
|
| 8 |
+
log() { echo "[STORAGE] $*"; }
|
| 9 |
+
|
| 10 |
+
# ββ Directory layout βββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
+
declare -a DIRS=(
|
| 12 |
+
/data/home # ubuntu user home (replaces /home/ubuntu)
|
| 13 |
+
/data/home/.config # XDG config
|
| 14 |
+
/data/home/.local/share
|
| 15 |
+
/data/home/Desktop
|
| 16 |
+
/data/home/Downloads
|
| 17 |
+
/data/home/.mozilla/firefox
|
| 18 |
+
/data/workspace # user project files
|
| 19 |
+
/data/vnc # VNC server state + xstartup
|
| 20 |
+
/data/firefox # persistent Firefox profile
|
| 21 |
+
/data/xfce # XFCE session config
|
| 22 |
+
/data/downloads # download folder
|
| 23 |
+
/data/logs # all service logs
|
| 24 |
+
/data/cache # app cache
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
for dir in "${DIRS[@]}"; do
|
| 28 |
+
if [ ! -d "$dir" ]; then
|
| 29 |
+
mkdir -p "$dir"
|
| 30 |
+
log "Created $dir"
|
| 31 |
+
fi
|
| 32 |
+
done
|
| 33 |
+
|
| 34 |
+
# ββ Permissions ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
+
chown -R ubuntu:ubuntu /data
|
| 36 |
+
chmod 755 /data
|
| 37 |
+
chmod 700 /data/vnc # VNC passwd must be protected
|
| 38 |
+
|
| 39 |
+
log "Persistent storage ready."
|
setup_ubuntu.sh
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# setup_ubuntu.sh β Configure ubuntu user environment
|
| 4 |
+
# Symlinks home dirs to /data so they persist across restarts
|
| 5 |
+
# ============================================================
|
| 6 |
+
set -euo pipefail
|
| 7 |
+
|
| 8 |
+
log() { echo "[UBUNTU] $*"; }
|
| 9 |
+
|
| 10 |
+
export HOME=/data/home
|
| 11 |
+
export USER=ubuntu
|
| 12 |
+
|
| 13 |
+
# ββ Symlink Downloads β /data/downloads βββββββββββββββββββββ
|
| 14 |
+
DOWNLOADS_LINK=/data/home/Downloads
|
| 15 |
+
if [ -d "$DOWNLOADS_LINK" ] && [ ! -L "$DOWNLOADS_LINK" ]; then
|
| 16 |
+
# First run: real dir exists β migrate contents and replace with symlink
|
| 17 |
+
cp -rn "$DOWNLOADS_LINK/." /data/downloads/ 2>/dev/null || true
|
| 18 |
+
rm -rf "$DOWNLOADS_LINK"
|
| 19 |
+
fi
|
| 20 |
+
if [ ! -L "$DOWNLOADS_LINK" ]; then
|
| 21 |
+
ln -sf /data/downloads "$DOWNLOADS_LINK"
|
| 22 |
+
log "Symlinked Downloads β /data/downloads"
|
| 23 |
+
fi
|
| 24 |
+
|
| 25 |
+
# ββ Symlink Firefox profile β /data/firefox ββββββββββββββββββ
|
| 26 |
+
FIREFOX_DIR=/data/home/.mozilla/firefox
|
| 27 |
+
if [ -d "$FIREFOX_DIR" ] && [ ! -L "$FIREFOX_DIR" ]; then
|
| 28 |
+
cp -rn "$FIREFOX_DIR/." /data/firefox/ 2>/dev/null || true
|
| 29 |
+
rm -rf "$FIREFOX_DIR"
|
| 30 |
+
fi
|
| 31 |
+
if [ ! -L "$FIREFOX_DIR" ]; then
|
| 32 |
+
ln -sf /data/firefox "$FIREFOX_DIR"
|
| 33 |
+
log "Symlinked Firefox profile β /data/firefox"
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
# ββ XFCE config βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
mkdir -p /data/home/.config/xfce4
|
| 38 |
+
if [ ! -L /data/home/.config/xfce4 ]; then
|
| 39 |
+
log "XFCE config dir ready at /data/home/.config/xfce4"
|
| 40 |
+
fi
|
| 41 |
+
|
| 42 |
+
# ββ XDG runtime dir βββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
mkdir -p /tmp/runtime-ubuntu
|
| 44 |
+
chown ubuntu:ubuntu /tmp/runtime-ubuntu
|
| 45 |
+
chmod 700 /tmp/runtime-ubuntu
|
| 46 |
+
|
| 47 |
+
# ββ .bashrc / .profile βββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
if [ ! -f /data/home/.bashrc ]; then
|
| 49 |
+
cat > /data/home/.bashrc << 'EOF'
|
| 50 |
+
# Ubuntu PRoot-style environment on Hugging Face Space
|
| 51 |
+
export HOME=/data/home
|
| 52 |
+
export WORKSPACE=/data/workspace
|
| 53 |
+
export PS1='\[\033[01;32m\]ubuntu@hf-space\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
|
| 54 |
+
alias ll='ls -alF'
|
| 55 |
+
alias la='ls -A'
|
| 56 |
+
alias workspace='cd /data/workspace'
|
| 57 |
+
echo "Welcome to your Ubuntu Desktop on Hugging Face Spaces!"
|
| 58 |
+
echo "Workspace: /data/workspace | Home: /data/home"
|
| 59 |
+
EOF
|
| 60 |
+
log "Created .bashrc"
|
| 61 |
+
fi
|
| 62 |
+
|
| 63 |
+
# ββ Desktop shortcut: Workspace folder ββββββββββββββββββββββββ
|
| 64 |
+
DESKTOP_DIR=/data/home/Desktop
|
| 65 |
+
cat > "$DESKTOP_DIR/workspace.desktop" << 'EOF'
|
| 66 |
+
[Desktop Entry]
|
| 67 |
+
Version=1.0
|
| 68 |
+
Type=Application
|
| 69 |
+
Name=Workspace
|
| 70 |
+
Comment=Open persistent workspace folder
|
| 71 |
+
Exec=thunar /data/workspace
|
| 72 |
+
Icon=folder-documents
|
| 73 |
+
Terminal=false
|
| 74 |
+
Categories=Utility;
|
| 75 |
+
EOF
|
| 76 |
+
chmod +x "$DESKTOP_DIR/workspace.desktop"
|
| 77 |
+
|
| 78 |
+
# ββ Desktop shortcut: Terminal ββββββββββββββββββββββββββββββββ
|
| 79 |
+
cat > "$DESKTOP_DIR/terminal.desktop" << 'EOF'
|
| 80 |
+
[Desktop Entry]
|
| 81 |
+
Version=1.0
|
| 82 |
+
Type=Application
|
| 83 |
+
Name=Terminal
|
| 84 |
+
Comment=Open XFCE Terminal
|
| 85 |
+
Exec=xfce4-terminal
|
| 86 |
+
Icon=utilities-terminal
|
| 87 |
+
Terminal=false
|
| 88 |
+
Categories=System;TerminalEmulator;
|
| 89 |
+
EOF
|
| 90 |
+
chmod +x "$DESKTOP_DIR/terminal.desktop"
|
| 91 |
+
|
| 92 |
+
# ββ Final ownership βββββββββββββββββββββββββββββββββββββββββββ
|
| 93 |
+
chown -R ubuntu:ubuntu /data/home
|
| 94 |
+
|
| 95 |
+
log "Ubuntu user environment ready."
|
setup_vnc.sh
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# setup_vnc.sh β Configure Xvnc and XFCE xstartup
|
| 4 |
+
# VNC state is stored in /data/vnc for persistence
|
| 5 |
+
# ============================================================
|
| 6 |
+
set -euo pipefail
|
| 7 |
+
|
| 8 |
+
log() { echo "[VNC] $*"; }
|
| 9 |
+
|
| 10 |
+
mkdir -p /data/vnc
|
| 11 |
+
mkdir -p /data/home/.vnc
|
| 12 |
+
|
| 13 |
+
# ββ Symlink ~/.vnc β /data/vnc ββββββββββββββββββββββββββββββββ
|
| 14 |
+
VNC_LINK=/data/home/.vnc
|
| 15 |
+
if [ -d "$VNC_LINK" ] && [ ! -L "$VNC_LINK" ]; then
|
| 16 |
+
cp -rn "$VNC_LINK/." /data/vnc/ 2>/dev/null || true
|
| 17 |
+
rm -rf "$VNC_LINK"
|
| 18 |
+
fi
|
| 19 |
+
if [ ! -L "$VNC_LINK" ]; then
|
| 20 |
+
ln -sf /data/vnc "$VNC_LINK"
|
| 21 |
+
log "Symlinked ~/.vnc β /data/vnc"
|
| 22 |
+
fi
|
| 23 |
+
|
| 24 |
+
# ββ VNC password ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
if [ -n "${VNC_PASSWORD:-}" ]; then
|
| 26 |
+
echo "$VNC_PASSWORD" | vncpasswd -f > /data/vnc/passwd
|
| 27 |
+
chmod 600 /data/vnc/passwd
|
| 28 |
+
log "VNC password set"
|
| 29 |
+
else
|
| 30 |
+
# No-auth mode (safe because Xvnc binds localhost only)
|
| 31 |
+
rm -f /data/vnc/passwd
|
| 32 |
+
log "VNC running in no-password mode (localhost-only, proxied via nginx)"
|
| 33 |
+
fi
|
| 34 |
+
|
| 35 |
+
# ββ xstartup β launches XFCE session βββββββββββββββββββββββββ
|
| 36 |
+
cat > /data/vnc/xstartup << 'XSTARTUP'
|
| 37 |
+
#!/bin/bash
|
| 38 |
+
# VNC xstartup β launched by Xvnc when a client connects
|
| 39 |
+
|
| 40 |
+
unset SESSION_MANAGER
|
| 41 |
+
unset DBUS_SESSION_BUS_ADDRESS
|
| 42 |
+
|
| 43 |
+
export DISPLAY=:1
|
| 44 |
+
export HOME=/data/home
|
| 45 |
+
export USER=ubuntu
|
| 46 |
+
export XDG_CONFIG_HOME=/data/home/.config
|
| 47 |
+
export XDG_DATA_HOME=/data/home/.local/share
|
| 48 |
+
export XDG_CACHE_HOME=/data/cache
|
| 49 |
+
export XDG_RUNTIME_DIR=/tmp/runtime-ubuntu
|
| 50 |
+
export LANG=en_US.UTF-8
|
| 51 |
+
|
| 52 |
+
# Ensure runtime dir exists
|
| 53 |
+
mkdir -p /tmp/runtime-ubuntu
|
| 54 |
+
chmod 700 /tmp/runtime-ubuntu
|
| 55 |
+
|
| 56 |
+
# Wallpaper / theme baseline (non-fatal)
|
| 57 |
+
xsetroot -solid "#1a1a2e" 2>/dev/null || true
|
| 58 |
+
|
| 59 |
+
# Start XFCE desktop session
|
| 60 |
+
exec dbus-launch --exit-with-session startxfce4
|
| 61 |
+
XSTARTUP
|
| 62 |
+
chmod +x /data/vnc/xstartup
|
| 63 |
+
|
| 64 |
+
# ββ Xvnc log file βββββββββββββββββββββββββββββββββββββββββββββ
|
| 65 |
+
touch /data/logs/xvnc.log
|
| 66 |
+
touch /data/logs/xfce4.log
|
| 67 |
+
|
| 68 |
+
# ββ Ownership βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
+
chown -R ubuntu:ubuntu /data/vnc /data/home/.vnc 2>/dev/null || true
|
| 70 |
+
|
| 71 |
+
log "VNC configuration ready."
|
start.sh
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# ============================================================
|
| 3 |
+
# start.sh β Main container entrypoint
|
| 4 |
+
# Initialises storage, user env, VNC, then hands off to supervisord
|
| 5 |
+
# ============================================================
|
| 6 |
+
set -euo pipefail
|
| 7 |
+
|
| 8 |
+
# ββ Colour helpers βββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
| 10 |
+
BLUE='\033[0;34m'; NC='\033[0m'
|
| 11 |
+
|
| 12 |
+
log() { echo -e "${GREEN}[START]${NC} $*"; }
|
| 13 |
+
warn() { echo -e "${YELLOW}[WARN ]${NC} $*"; }
|
| 14 |
+
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
| 15 |
+
|
| 16 |
+
# ββ Banner βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
+
echo ""
|
| 18 |
+
echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
|
| 19 |
+
echo -e "${BLUE}β Ubuntu 24.04 XFCE Desktop β HuggingFace Space β${NC}"
|
| 20 |
+
echo -e "${BLUE}β PRoot-Distro Style β noVNC Browser Access β${NC}"
|
| 21 |
+
echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
|
| 22 |
+
echo " Started: $(date)"
|
| 23 |
+
echo ""
|
| 24 |
+
|
| 25 |
+
# ββ Ensure /data exists (HF mounts it; fallback for local dev) ββ
|
| 26 |
+
if [ ! -d /data ]; then
|
| 27 |
+
warn "/data not found β creating in-container fallback (data will NOT persist)"
|
| 28 |
+
mkdir -p /data
|
| 29 |
+
fi
|
| 30 |
+
|
| 31 |
+
# ββ Run setup phases βββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
log "Phase 1/3 β Persistent storage"
|
| 33 |
+
bash /setup_storage.sh
|
| 34 |
+
|
| 35 |
+
log "Phase 2/3 β Ubuntu user environment"
|
| 36 |
+
bash /setup_ubuntu.sh
|
| 37 |
+
|
| 38 |
+
log "Phase 3/3 β VNC configuration"
|
| 39 |
+
bash /setup_vnc.sh
|
| 40 |
+
|
| 41 |
+
# ββ Ensure supervisor log dir ββββββββββββββββββββββββββββββββ
|
| 42 |
+
mkdir -p /data/logs
|
| 43 |
+
mkdir -p /var/log/supervisor
|
| 44 |
+
|
| 45 |
+
# ββ Fix nginx tmp dirs βββββββββββββββββββββββββββββββββββββββ
|
| 46 |
+
mkdir -p /var/lib/nginx/body /var/lib/nginx/proxy \
|
| 47 |
+
/var/lib/nginx/fastcgi /var/lib/nginx/uwsgi \
|
| 48 |
+
/var/lib/nginx/scgi
|
| 49 |
+
chown -R www-data:www-data /var/lib/nginx
|
| 50 |
+
|
| 51 |
+
# ββ Hand off to supervisord ββββββββββββββββββββββββββββββββββ
|
| 52 |
+
log "Launching supervisord (manages all services)β¦"
|
| 53 |
+
echo ""
|
| 54 |
+
exec /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf
|
supervisord.conf
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[supervisord]
|
| 2 |
+
nodaemon=true
|
| 3 |
+
logfile=/data/logs/supervisord.log
|
| 4 |
+
logfile_maxbytes=10MB
|
| 5 |
+
logfile_backups=3
|
| 6 |
+
loglevel=info
|
| 7 |
+
pidfile=/var/run/supervisord.pid
|
| 8 |
+
user=root
|
| 9 |
+
childlogdir=/data/logs
|
| 10 |
+
|
| 11 |
+
[unix_http_server]
|
| 12 |
+
file=/var/run/supervisor.sock
|
| 13 |
+
chmod=0700
|
| 14 |
+
|
| 15 |
+
[rpcinterface:supervisor]
|
| 16 |
+
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
| 17 |
+
|
| 18 |
+
[supervisorctl]
|
| 19 |
+
serverurl=unix:///var/run/supervisor.sock
|
| 20 |
+
|
| 21 |
+
; ββ 1. Xvnc (virtual framebuffer + VNC server) ββββββββββββββββββββββββββββββ
|
| 22 |
+
; Starts first (priority=100). Binds to localhost:5901 only.
|
| 23 |
+
; SecurityTypes=None is safe because nginx/websockify front the connection.
|
| 24 |
+
[program:xvnc]
|
| 25 |
+
command=Xvnc :1
|
| 26 |
+
-geometry 1280x720
|
| 27 |
+
-depth 16
|
| 28 |
+
-rfbport 5901
|
| 29 |
+
-SecurityTypes None
|
| 30 |
+
-localhost
|
| 31 |
+
-nolisten tcp
|
| 32 |
+
-fp /usr/share/fonts/X11/misc/,built-ins
|
| 33 |
+
priority=100
|
| 34 |
+
user=ubuntu
|
| 35 |
+
environment=HOME="/data/home",USER="ubuntu",XAUTHORITY="/data/vnc/.Xauthority",XDG_RUNTIME_DIR="/tmp/runtime-ubuntu"
|
| 36 |
+
autostart=true
|
| 37 |
+
autorestart=true
|
| 38 |
+
startsecs=3
|
| 39 |
+
startretries=10
|
| 40 |
+
stopwaitsecs=10
|
| 41 |
+
stdout_logfile=/data/logs/xvnc.log
|
| 42 |
+
stderr_logfile=/data/logs/xvnc_err.log
|
| 43 |
+
stdout_logfile_maxbytes=5MB
|
| 44 |
+
stdout_logfile_backups=2
|
| 45 |
+
|
| 46 |
+
; ββ 2. XFCE4 desktop session ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
; Starts after Xvnc (priority=200). Wraps xstartup to handle restarts cleanly.
|
| 48 |
+
[program:xfce4]
|
| 49 |
+
command=/bin/bash -c "
|
| 50 |
+
sleep 2 &&
|
| 51 |
+
export DISPLAY=:1 &&
|
| 52 |
+
export HOME=/data/home &&
|
| 53 |
+
export USER=ubuntu &&
|
| 54 |
+
export XDG_CONFIG_HOME=/data/home/.config &&
|
| 55 |
+
export XDG_DATA_HOME=/data/home/.local/share &&
|
| 56 |
+
export XDG_CACHE_HOME=/data/cache &&
|
| 57 |
+
export XDG_RUNTIME_DIR=/tmp/runtime-ubuntu &&
|
| 58 |
+
exec dbus-launch --exit-with-session startxfce4
|
| 59 |
+
"
|
| 60 |
+
priority=200
|
| 61 |
+
user=ubuntu
|
| 62 |
+
environment=DISPLAY=":1",HOME="/data/home",USER="ubuntu",XDG_CONFIG_HOME="/data/home/.config",XDG_DATA_HOME="/data/home/.local/share",XDG_CACHE_HOME="/data/cache",XDG_RUNTIME_DIR="/tmp/runtime-ubuntu",LANG="en_US.UTF-8"
|
| 63 |
+
autostart=true
|
| 64 |
+
autorestart=true
|
| 65 |
+
startsecs=5
|
| 66 |
+
startretries=10
|
| 67 |
+
stopwaitsecs=15
|
| 68 |
+
stdout_logfile=/data/logs/xfce4.log
|
| 69 |
+
stderr_logfile=/data/logs/xfce4_err.log
|
| 70 |
+
stdout_logfile_maxbytes=5MB
|
| 71 |
+
stdout_logfile_backups=2
|
| 72 |
+
|
| 73 |
+
; ββ 3. websockify (noVNC β VNC bridge) ββββββββββββββββββββββββββββββββββββββ
|
| 74 |
+
; Bridges WebSocket connections (from nginx/noVNC) to VNC on localhost:5901.
|
| 75 |
+
[program:websockify]
|
| 76 |
+
command=websockify
|
| 77 |
+
--web=/usr/share/novnc
|
| 78 |
+
--heartbeat=30
|
| 79 |
+
6080
|
| 80 |
+
localhost:5901
|
| 81 |
+
priority=300
|
| 82 |
+
user=ubuntu
|
| 83 |
+
environment=HOME="/data/home"
|
| 84 |
+
autostart=true
|
| 85 |
+
autorestart=true
|
| 86 |
+
startsecs=5
|
| 87 |
+
startretries=10
|
| 88 |
+
stopwaitsecs=10
|
| 89 |
+
stdout_logfile=/data/logs/websockify.log
|
| 90 |
+
stderr_logfile=/data/logs/websockify_err.log
|
| 91 |
+
stdout_logfile_maxbytes=5MB
|
| 92 |
+
stdout_logfile_backups=2
|
| 93 |
+
|
| 94 |
+
; ββ 4. Flask dashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 95 |
+
[program:dashboard]
|
| 96 |
+
command=python3 -u /app/app.py
|
| 97 |
+
priority=400
|
| 98 |
+
user=ubuntu
|
| 99 |
+
environment=HOME="/data/home",USER="ubuntu",PYTHONUNBUFFERED="1"
|
| 100 |
+
autostart=true
|
| 101 |
+
autorestart=true
|
| 102 |
+
startsecs=3
|
| 103 |
+
startretries=5
|
| 104 |
+
stopwaitsecs=10
|
| 105 |
+
stdout_logfile=/data/logs/dashboard.log
|
| 106 |
+
stderr_logfile=/data/logs/dashboard_err.log
|
| 107 |
+
stdout_logfile_maxbytes=5MB
|
| 108 |
+
stdout_logfile_backups=2
|
| 109 |
+
|
| 110 |
+
; ββ 5. nginx (public-facing reverse proxy on port 7860) βββββββββββββββββββββ
|
| 111 |
+
[program:nginx]
|
| 112 |
+
command=nginx -g "daemon off;"
|
| 113 |
+
priority=500
|
| 114 |
+
user=root
|
| 115 |
+
autostart=true
|
| 116 |
+
autorestart=true
|
| 117 |
+
startsecs=3
|
| 118 |
+
startretries=5
|
| 119 |
+
stopwaitsecs=10
|
| 120 |
+
stdout_logfile=/data/logs/nginx.log
|
| 121 |
+
stderr_logfile=/data/logs/nginx_err.log
|
| 122 |
+
stdout_logfile_maxbytes=5MB
|
| 123 |
+
stdout_logfile_backups=2
|
| 124 |
+
|
| 125 |
+
; ββ Event listener: restart crashed services ββββββββββββββββββββββββββββββββ
|
| 126 |
+
[eventlistener:crashmail]
|
| 127 |
+
command=/bin/bash -c "
|
| 128 |
+
while read line; do
|
| 129 |
+
echo \"[CRASH] \$(date): \$line\" >> /data/logs/crashes.log
|
| 130 |
+
done
|
| 131 |
+
"
|
| 132 |
+
events=PROCESS_STATE_FATAL
|