Spaces:
Runtime error
Runtime error
| export DISPLAY=:99 | |
| export ANDROID_SDK_ROOT=/opt/android-sdk | |
| export ANDROID_HOME=/opt/android-sdk | |
| export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 | |
| export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:/opt/android-studio/bin | |
| # Clean locks | |
| rm -f /tmp/.X99-lock /tmp/.X11-unix/X99 | |
| echo "Starting Xvfb..." | |
| Xvfb :99 -screen 0 1280x800x16 -ac & | |
| sleep 3 | |
| echo "Starting Openbox..." | |
| # Create a minimal menu file to avoid the warning | |
| mkdir -p /var/lib/openbox | |
| cat > /var/lib/openbox/debian-menu.xml << 'EOF' | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <openbox_menu> | |
| <menu id="root-menu" label="Openbox"> | |
| <item label="Android Studio"> | |
| <action name="Execute"> | |
| <command>/opt/android-studio/bin/studio.sh</command> | |
| </action> | |
| </item> | |
| <item label="Terminal"> | |
| <action name="Execute"> | |
| <command>xterm</command> | |
| </action> | |
| </item> | |
| </menu> | |
| </openbox_menu> | |
| EOF | |
| openbox --replace & | |
| sleep 2 | |
| echo "Starting Android Studio..." | |
| /opt/android-studio/bin/studio.sh & | |
| sleep 5 | |
| echo "Starting VNC..." | |
| x11vnc -display :99 -nopw -forever -shared -rfbport 5900 -defer 10 -wait 10 -q & | |
| sleep 3 | |
| # Create a simple landing page that redirects to vnc.html | |
| cat > /usr/share/novnc/index.html << 'EOF' | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta http-equiv="refresh" content="0; url=/vnc.html"> | |
| <title>Redirecting to Android Studio VNC</title> | |
| </head> | |
| <body> | |
| Redirecting to <a href="/vnc.html">Android Studio VNC</a>... | |
| </body> | |
| </html> | |
| EOF | |
| # Create a custom vnc.html that works with HF Spaces | |
| cat > /usr/share/novnc/vnc.html << 'EOF' | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Android Studio on Hugging Face</title> | |
| <meta charset="utf-8"> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: #1e1e1e; | |
| color: #fff; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 10px 20px; | |
| background: #2d2d2d; | |
| border-bottom: 2px solid #3ddc84; | |
| } | |
| .title h1 { | |
| margin: 0; | |
| font-size: 1.3rem; | |
| color: #3ddc84; | |
| } | |
| .badge { | |
| background: #3ddc84; | |
| color: #000; | |
| padding: 2px 8px; | |
| border-radius: 12px; | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| margin-left: 10px; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| button { | |
| background: #3ddc84; | |
| color: #000; | |
| border: none; | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| font-size: 0.8rem; | |
| } | |
| button.secondary { | |
| background: transparent; | |
| color: #3ddc84; | |
| border: 1px solid #3ddc84; | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .vnc-container { | |
| flex: 1; | |
| background: #000; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #vnc { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .status { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| margin-right: 15px; | |
| font-size: 0.8rem; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| } | |
| .status-dot.connected { | |
| background: #3ddc84; | |
| animation: pulse 2s infinite; | |
| } | |
| .status-dot.connecting { | |
| background: #ffaa00; | |
| } | |
| .status-dot.disconnected { | |
| background: #ff4444; | |
| } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 0 0 rgba(61, 220, 132, 0.7); } | |
| 70% { box-shadow: 0 0 0 6px rgba(61, 220, 132, 0); } | |
| } | |
| .loading { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| text-align: center; | |
| z-index: 10; | |
| background: rgba(30,30,30,0.9); | |
| padding: 20px; | |
| border-radius: 8px; | |
| border: 1px solid #3ddc84; | |
| } | |
| .spinner { | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #3ddc84; | |
| border-radius: 50%; | |
| width: 30px; | |
| height: 30px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 10px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .footer { | |
| text-align: center; | |
| padding: 5px; | |
| background: #2d2d2d; | |
| font-size: 0.7rem; | |
| color: #888; | |
| } | |
| .footer a { | |
| color: #3ddc84; | |
| text-decoration: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <div class="title"> | |
| <span class="badge">HF Space</span> | |
| <h1>🤖 Android Studio</h1> | |
| </div> | |
| <div class="controls"> | |
| <div class="status"> | |
| <span class="status-dot disconnected" id="status-dot"></span> | |
| <span id="status-text">Disconnected</span> | |
| </div> | |
| <button onclick="toggleViewOnly()" class="secondary" id="viewOnlyBtn" disabled>View Only: OFF</button> | |
| <button onclick="sendCtrlAltDel()" class="secondary" id="ctrlAltDelBtn" disabled>Ctrl+Alt+Del</button> | |
| <button onclick="reconnect()" id="reconnectBtn">Reconnect</button> | |
| </div> | |
| </div> | |
| <div class="vnc-container"> | |
| <div id="vnc"></div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <div id="loading-text">Connecting to Android Studio...</div> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| ⚡ Running on Hugging Face Spaces | <a href="#" onclick="toggleFullscreen()">Fullscreen</a> | |
| </div> | |
| <script> | |
| let rfb; | |
| let retryCount = 0; | |
| const maxRetries = 10; | |
| const loading = document.getElementById('loading'); | |
| const loadingText = document.getElementById('loading-text'); | |
| const statusDot = document.getElementById('status-dot'); | |
| const statusText = document.getElementById('status-text'); | |
| const viewOnlyBtn = document.getElementById('viewOnlyBtn'); | |
| const ctrlAltDelBtn = document.getElementById('ctrlAltDelBtn'); | |
| function setStatus(state, message) { | |
| statusDot.className = 'status-dot ' + state; | |
| statusText.textContent = message; | |
| } | |
| function toggleViewOnly() { | |
| if (rfb) { | |
| rfb.viewOnly = !rfb.viewOnly; | |
| viewOnlyBtn.textContent = 'View Only: ' + (rfb.viewOnly ? 'ON' : 'OFF'); | |
| } | |
| } | |
| function sendCtrlAltDel() { | |
| if (rfb) rfb.sendCtrlAltDel(); | |
| } | |
| function toggleFullscreen() { | |
| const container = document.querySelector('.vnc-container'); | |
| if (document.fullscreenElement) { | |
| document.exitFullscreen(); | |
| } else { | |
| container.requestFullscreen(); | |
| } | |
| } | |
| function reconnect() { | |
| if (rfb) rfb.disconnect(); | |
| setTimeout(connect, 1000); | |
| } | |
| function connect() { | |
| loading.style.display = 'block'; | |
| loadingText.textContent = 'Connecting to Android Studio...'; | |
| setStatus('connecting', 'Connecting...'); | |
| viewOnlyBtn.disabled = true; | |
| ctrlAltDelBtn.disabled = true; | |
| // Hugging Face Spaces WebSocket endpoint | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const host = window.location.host; | |
| const wsUrl = `${protocol}//${host}/proxy/7860/websockify`; | |
| console.log('Connecting to:', wsUrl); | |
| try { | |
| rfb = new RFB(document.getElementById('vnc'), wsUrl, { | |
| credentials: { password: '' }, | |
| shared: true, | |
| viewOnly: false, | |
| resizeSession: true | |
| }); | |
| rfb.addEventListener('connect', () => { | |
| loading.style.display = 'none'; | |
| setStatus('connected', 'Connected'); | |
| viewOnlyBtn.disabled = false; | |
| ctrlAltDelBtn.disabled = false; | |
| retryCount = 0; | |
| }); | |
| rfb.addEventListener('disconnect', () => { | |
| loading.style.display = 'block'; | |
| setStatus('disconnected', 'Disconnected'); | |
| viewOnlyBtn.disabled = true; | |
| ctrlAltDelBtn.disabled = true; | |
| if (retryCount < maxRetries) { | |
| retryCount++; | |
| const delay = Math.min(1000 * retryCount, 10000); | |
| loadingText.textContent = `Reconnecting in ${delay/1000}s... (${retryCount}/${maxRetries})`; | |
| setTimeout(connect, delay); | |
| } else { | |
| loadingText.textContent = 'Failed to connect. Click Reconnect.'; | |
| } | |
| }); | |
| rfb.addEventListener('securityfailure', (e) => { | |
| console.error('Security failure:', e.detail); | |
| loadingText.textContent = 'Security error'; | |
| }); | |
| } catch (error) { | |
| console.error('Connection error:', error); | |
| loadingText.textContent = 'Connection error'; | |
| } | |
| } | |
| window.onload = connect; | |
| window.addEventListener('resize', () => { | |
| if (rfb) setTimeout(() => rfb.resizeSession(), 100); | |
| }); | |
| </script> | |
| <script src="vnc.js"></script> | |
| </body> | |
| </html> | |
| EOF | |
| # Create diagnostic page | |
| cat > /usr/share/novnc/diagnostic.html << 'EOF' | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>HF Space Diagnostic</title> | |
| <style> | |
| body { background: #1e1e1e; color: #fff; font-family: monospace; padding: 20px; } | |
| pre { background: #2d2d2d; padding: 15px; border-radius: 5px; } | |
| .success { color: #3ddc84; } | |
| .error { color: #ff4444; } | |
| button { background: #3ddc84; color: #000; border: none; padding: 10px; border-radius: 4px; cursor: pointer; margin: 5px; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>🔧 Diagnostic Tool</h1> | |
| <button onclick="testWebSocket()">Test WebSocket</button> | |
| <button onclick="window.location.href='/vnc.html'">Go to VNC</button> | |
| <pre id="output"></pre> | |
| <script> | |
| const output = document.getElementById('output'); | |
| async function testWebSocket() { | |
| output.textContent = 'Testing WebSocket connections...\n'; | |
| const endpoints = [ | |
| `/proxy/7860/websockify`, | |
| `/websockify` | |
| ]; | |
| for (const endpoint of endpoints) { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const url = `${protocol}//${window.location.host}${endpoint}`; | |
| output.textContent += `\nTesting: ${url}\n`; | |
| try { | |
| const ws = new WebSocket(url); | |
| await new Promise((resolve, reject) => { | |
| ws.onopen = () => { | |
| output.textContent += '✅ SUCCESS\n'; | |
| ws.close(); | |
| resolve(); | |
| }; | |
| ws.onerror = () => { | |
| output.textContent += '❌ FAILED\n'; | |
| reject(); | |
| }; | |
| setTimeout(() => { | |
| output.textContent += '⏱️ TIMEOUT\n'; | |
| reject(); | |
| }, 3000); | |
| }); | |
| } catch (e) { | |
| output.textContent += `❌ ERROR: ${e.message}\n`; | |
| } | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| EOF | |
| echo "Starting noVNC with HF Spaces configuration..." | |
| cd /usr/share/novnc | |
| # Ensure vnc.js is available | |
| if [ ! -f "vnc.js" ]; then | |
| ln -sf /usr/share/novnc/vnc.js . 2>/dev/null || true | |
| fi | |
| # Start websockify | |
| websockify --web=/usr/share/novnc 0.0.0.0:7860 localhost:5900 |