pabla1322 commited on
Commit
e5de808
Β·
verified Β·
1 Parent(s): 08bb0ff

Upload 14 files

Browse files
Files changed (14) hide show
  1. .dockerignore +46 -0
  2. .gitignore +39 -0
  3. Dockerfile +134 -0
  4. IMPROVED_PROMPT.md +186 -0
  5. README.md +212 -9
  6. app.py +504 -0
  7. healthcheck.py +61 -0
  8. nginx.conf +169 -0
  9. requirements.txt +3 -0
  10. setup_storage.sh +39 -0
  11. setup_ubuntu.sh +95 -0
  12. setup_vnc.sh +71 -0
  13. start.sh +54 -0
  14. 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: D
3
- emoji: 😻
4
- colorFrom: gray
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
- app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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