| | import os |
| | import subprocess |
| | import time |
| | import socket |
| | import signal |
| | import sys |
| | from pyngrok import ngrok |
| | import gradio as gr |
| |
|
| | |
| | NGROK_AUTHTOKEN = os.getenv("NGROK_AUTHTOKEN") |
| |
|
| | |
| | processes = {} |
| |
|
| | def is_port_open(port, host='localhost', timeout=2): |
| | """Check if a port is open and listening""" |
| | try: |
| | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: |
| | sock.settimeout(timeout) |
| | result = sock.connect_ex((host, port)) |
| | return result == 0 |
| | except: |
| | return False |
| |
|
| | def cleanup_processes(): |
| | """Cleanup all processes on exit""" |
| | print("\nCleaning up processes...") |
| | for name, proc in processes.items(): |
| | try: |
| | if proc and proc.poll() is None: |
| | print(f"Terminating {name}...") |
| | proc.terminate() |
| | proc.wait(timeout=5) |
| | except: |
| | try: |
| | proc.kill() |
| | except: |
| | pass |
| |
|
| | def signal_handler(sig, frame): |
| | """Handle interrupt signals""" |
| | cleanup_processes() |
| | sys.exit(0) |
| |
|
| | signal.signal(signal.SIGINT, signal_handler) |
| | signal.signal(signal.SIGTERM, signal_handler) |
| |
|
| | def cleanup_previous_sessions(): |
| | """Clean up any previous sessions""" |
| | print("Cleaning up previous sessions...") |
| | |
| | commands = [ |
| | ["pkill", "-f", "x11vnc"], |
| | ["pkill", "-f", "websockify"], |
| | ["pkill", "-f", "Xvfb"], |
| | ["pkill", "-f", "fluxbox"], |
| | ["pkill", "-f", "organichits-exchanger"], |
| | ["fuser", "-k", "5901/tcp"], |
| | ["fuser", "-k", "8081/tcp"] |
| | ] |
| | |
| | for cmd in commands: |
| | try: |
| | subprocess.run(cmd, capture_output=True, timeout=5) |
| | except: |
| | pass |
| | |
| | time.sleep(2) |
| |
|
| | def setup_display(): |
| | """Setup virtual display with proper settings for Electron/Chromium""" |
| | print("Setting up virtual display for Electron app...") |
| | |
| | |
| | subprocess.run(["pkill", "-f", "Xvfb :99"], capture_output=True) |
| | time.sleep(1) |
| | |
| | |
| | xvfb_process = subprocess.Popen([ |
| | "Xvfb", ":99", |
| | "-screen", "0", "1280x720x24", |
| | "-ac", |
| | "-nolisten", "tcp", |
| | "-dpi", "96", |
| | "+extension", "RANDR" |
| | ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| | |
| | processes['xvfb'] = xvfb_process |
| | |
| | |
| | time.sleep(3) |
| | |
| | |
| | os.environ['DISPLAY'] = ':99' |
| | |
| | |
| | try: |
| | result = subprocess.run( |
| | ["xdpyinfo", "-display", ":99"], |
| | capture_output=True, |
| | timeout=5 |
| | ) |
| | if result.returncode == 0: |
| | print("β Virtual display :99 is ready") |
| | else: |
| | print("β Display may not be fully ready") |
| | except: |
| | print("β Could not verify display") |
| | |
| | |
| | print("Starting window manager...") |
| | fluxbox_process = subprocess.Popen( |
| | ["fluxbox"], |
| | env={**os.environ, 'DISPLAY': ':99'}, |
| | stdout=subprocess.PIPE, |
| | stderr=subprocess.PIPE |
| | ) |
| | |
| | processes['fluxbox'] = fluxbox_process |
| | time.sleep(2) |
| | |
| | return True |
| |
|
| | def start_vnc_server(): |
| | """Start VNC server connected to virtual display""" |
| | print("Starting VNC server...") |
| | |
| | |
| | vnc_process = subprocess.Popen([ |
| | "x11vnc", |
| | "-display", ":99", |
| | "-forever", |
| | "-shared", |
| | "-nopw", |
| | "-listen", "0.0.0.0", |
| | "-rfbport", "5901", |
| | "-xkb", |
| | "-noxrecord", |
| | "-noxfixes", |
| | "-noxdamage", |
| | "-wait", "10", |
| | "-defer", "10" |
| | ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| | |
| | processes['vnc'] = vnc_process |
| | |
| | |
| | for i in range(15): |
| | if is_port_open(5901): |
| | print("β VNC server started on port 5901") |
| | return True |
| | time.sleep(1) |
| | |
| | print("β VNC server failed to start") |
| | return False |
| |
|
| | def start_organichits_app(): |
| | """Start OrganicHits Exchanger with proper Electron/Chromium flags""" |
| | print("Starting OrganicHits Exchanger...") |
| | |
| | |
| | os.environ['DISPLAY'] = ':99' |
| | |
| | |
| | app_dir = "./OrganicHits" |
| | app_path = os.path.join(app_dir, "organichits-exchanger") |
| | |
| | |
| | os.chmod(app_path, 0o755) |
| | |
| | |
| | critical_files = ['icudtl.dat', 'resources.pak', 'v8_context_snapshot.bin'] |
| | for file in critical_files: |
| | file_path = os.path.join(app_dir, file) |
| | if not os.path.exists(file_path): |
| | print(f"β Warning: Missing critical file: {file}") |
| | |
| | |
| | app_process = subprocess.Popen([ |
| | app_path, |
| | "--no-sandbox", |
| | "--disable-setuid-sandbox", |
| | "--disable-dev-shm-usage", |
| | "--disable-gpu", |
| | "--disable-software-rasterizer", |
| | "--disable-gpu-compositing", |
| | "--enable-features=UseOzonePlatform", |
| | "--ozone-platform=x11", |
| | "--disable-features=VizDisplayCompositor", |
| | "--in-process-gpu", |
| | "--disable-accelerated-2d-canvas", |
| | "--disable-accelerated-video-decode", |
| | "--disable-accelerated-video-encode", |
| | "--no-first-run", |
| | "--no-default-browser-check", |
| | "--disable-background-timer-throttling", |
| | "--disable-backgrounding-occluded-windows", |
| | "--disable-renderer-backgrounding", |
| | "--disable-breakpad" |
| | ], |
| | env={ |
| | **os.environ, |
| | 'DISPLAY': ':99', |
| | 'ELECTRON_DISABLE_SANDBOX': '1', |
| | 'ELECTRON_ENABLE_LOGGING': '1', |
| | 'LD_LIBRARY_PATH': app_dir |
| | }, |
| | stdout=subprocess.PIPE, |
| | stderr=subprocess.PIPE, |
| | cwd=app_dir |
| | ) |
| | |
| | processes['app'] = app_process |
| | |
| | |
| | time.sleep(5) |
| | |
| | |
| | if app_process.poll() is None: |
| | print("β OrganicHits application started") |
| | return True |
| | else: |
| | stdout, stderr = app_process.communicate(timeout=1) |
| | print(f"β Application failed to start") |
| | print(f"stdout: {stdout.decode()[:500]}") |
| | print(f"stderr: {stderr.decode()[:500]}") |
| | return False |
| |
|
| | def start_websockify(): |
| | """Start websockify proxy for web access""" |
| | print("Starting websockify on port 8081...") |
| | |
| | websockify_process = subprocess.Popen([ |
| | "websockify", |
| | "8081", |
| | "localhost:5901", |
| | "--web=/usr/share/novnc/", |
| | "--verbose" |
| | ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| | |
| | processes['websockify'] = websockify_process |
| | |
| | |
| | for i in range(10): |
| | if is_port_open(8081): |
| | print("β Websockify started on port 8081") |
| | return True |
| | time.sleep(1) |
| | |
| | print("β Websockify failed to start") |
| | return False |
| |
|
| | def start_vnc_and_ngrok(): |
| | """Main function to start everything""" |
| | if not NGROK_AUTHTOKEN: |
| | return "β Error: NGROK_AUTHTOKEN not found in environment secrets!" |
| |
|
| | try: |
| | |
| | cleanup_previous_sessions() |
| | |
| | |
| | if not setup_display(): |
| | return "β Failed to setup virtual display" |
| | |
| | |
| | if not start_vnc_server(): |
| | return "β Failed to start VNC server" |
| | |
| | |
| | if not start_websockify(): |
| | return "β Failed to start websockify" |
| | |
| | |
| | app_started = start_organichits_app() |
| | app_status = "β Running" if app_started else "β May have failed (check logs)" |
| | |
| | |
| | print("Creating ngrok tunnel...") |
| | try: |
| | ngrok.set_auth_token(NGROK_AUTHTOKEN) |
| | public_url = ngrok.connect(8081, "http") |
| | novnc_url = f"{public_url}/vnc.html?autoconnect=true" |
| | |
| | return f"""β
VNC Session Started Successfully! |
| | |
| | π Access Your Application: |
| | {novnc_url} |
| | |
| | π Status: |
| | β’ Virtual Display: β Running on :99 |
| | β’ VNC Server: β Running on port 5901 |
| | β’ Websockify: β Running on port 8081 |
| | β’ OrganicHits App: {app_status} |
| | |
| | π± Click the URL above to access your OrganicHits Exchanger! |
| | |
| | β οΈ Notes: |
| | - First load may take 10-20 seconds |
| | - Click "Connect" if not auto-connected |
| | - No password required |
| | - Keep this tab open to maintain the session |
| | |
| | π§ Troubleshooting: |
| | - If black screen persists, wait 30 seconds and refresh |
| | - Check "Service Status" below |
| | - Application logs are being captured |
| | """ |
| | |
| | except Exception as e: |
| | return f"β Failed to create ngrok tunnel: {e}" |
| |
|
| | except Exception as e: |
| | return f"β Unexpected error: {str(e)}" |
| |
|
| | def check_services(): |
| | """Check status of all services""" |
| | services = { |
| | 'Virtual Display (Xvfb)': is_port_open(6099) or ('xvfb' in processes and processes['xvfb'].poll() is None), |
| | 'VNC Server': is_port_open(5901), |
| | 'Websockify': is_port_open(8081), |
| | 'OrganicHits App': 'app' in processes and processes['app'].poll() is None |
| | } |
| | |
| | status_text = "π Service Status:\n\n" |
| | for service, running in services.items(): |
| | emoji = "β
" if running else "β" |
| | status_text += f"{emoji} {service}\n" |
| | |
| | return status_text |
| |
|
| | def get_app_logs(): |
| | """Get application logs""" |
| | if 'app' not in processes or processes['app'].poll() is not None: |
| | return "Application not running or has stopped" |
| | |
| | try: |
| | |
| | return "Application is running. Check console for detailed logs." |
| | except: |
| | return "Could not retrieve logs" |
| |
|
| | def main(): |
| | with gr.Blocks(title="OrganicHits VNC Remote Desktop", theme=gr.themes.Soft()) as demo: |
| | gr.Markdown(""" |
| | # π₯οΈ OrganicHits Exchanger - Remote Desktop |
| | |
| | Run OrganicHits Exchanger in your browser via VNC |
| | """) |
| | |
| | with gr.Row(): |
| | with gr.Column(scale=2): |
| | output = gr.Textbox( |
| | label="π Access Information", |
| | lines=15, |
| | placeholder="Click 'Start VNC Session' to begin...", |
| | show_copy_button=True |
| | ) |
| | |
| | with gr.Row(): |
| | start_button = gr.Button( |
| | "π Start VNC Session", |
| | variant="primary", |
| | size="lg", |
| | scale=2 |
| | ) |
| | check_btn = gr.Button( |
| | "π Check Status", |
| | variant="secondary", |
| | scale=1 |
| | ) |
| | |
| | with gr.Column(scale=1): |
| | status = gr.Textbox( |
| | label="π Service Status", |
| | lines=8, |
| | value="Services not started yet..." |
| | ) |
| | |
| | gr.Markdown(""" |
| | ### β±οΈ Expected Timeline: |
| | - Display setup: 3-5s |
| | - VNC start: 2-3s |
| | - App launch: 5-10s |
| | - **Total: ~20-30 seconds** |
| | """) |
| | |
| | gr.Markdown(""" |
| | --- |
| | ### π How to Use: |
| | |
| | 1. **Click "Start VNC Session"** and wait ~30 seconds |
| | 2. **Click the URL** that appears above |
| | 3. **Wait for connection** - you'll see the desktop |
| | 4. **OrganicHits will auto-launch** in the window |
| | |
| | ### π― What You'll See: |
| | - A desktop environment (Fluxbox) |
| | - OrganicHits Exchanger window |
| | - You can interact with it normally! |
| | |
| | ### β οΈ Important Notes: |
| | - Keep this browser tab open |
| | - No authentication (temporary session) |
| | - Session ends if you close this page |
| | - First connection may show black screen for 10-20 seconds (normal!) |
| | |
| | ### π§ Troubleshooting: |
| | - **Black screen?** Wait 30 seconds and refresh the VNC page |
| | - **Can't connect?** Check if NGROK_AUTHTOKEN is set in Secrets |
| | - **App not showing?** Click "Check Status" to diagnose |
| | """) |
| | |
| | start_button.click( |
| | fn=start_vnc_and_ngrok, |
| | outputs=output |
| | ) |
| | |
| | check_btn.click( |
| | fn=check_services, |
| | outputs=status |
| | ) |
| |
|
| | |
| | demo.launch( |
| | server_name="0.0.0.0", |
| | server_port=7860, |
| | share=False, |
| | show_error=True |
| | ) |
| |
|
| | if __name__ == "__main__": |
| | main() |