#!/usr/bin/env python3 """ Android Emulator on Hugging Face Spaces Zero cost. Zero card. """ import os import sys import subprocess import threading import time import json import shutil import http.server import socketserver import urllib.request import logging # ============================================================ # CONFIG # ============================================================ PORT = 7860 # HF Spaces expose ce port WORK = "/app/android" VNC_PORT = 5900 NOVNC_PORT = 6080 logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") log = logging.getLogger("emu") LOG_BUFFER = [] class WebLog(logging.Handler): def emit(self, record): LOG_BUFFER.append(self.format(record)) if len(LOG_BUFFER) > 200: LOG_BUFFER.pop(0) wh = WebLog() wh.setFormatter(logging.Formatter("%(asctime)s %(message)s")) log.addHandler(wh) # ============================================================ # STATE # ============================================================ state = { "phase": "idle", "qemu_proc": None, "novnc_proc": None, } # ============================================================ # ANDROID IMAGE # ============================================================ def download_android(): iso = os.path.join(WORK, "android.iso") disk = os.path.join(WORK, "disk.qcow2") os.makedirs(WORK, exist_ok=True) if not os.path.exists(iso): log.info("Telechargement Android-x86...") state["phase"] = "downloading" # Android-x86 9.0 depuis SourceForge url = "https://sourceforge.net/projects/android-x86/files/Release%209.0/android-x86_64-9.0-r2.iso/download" subprocess.run([ "wget", "-q", "--show-progress", "-O", iso, url ], check=False) if os.path.exists(iso) and os.path.getsize(iso) > 1000: log.info(f"ISO telechargee: {os.path.getsize(iso) // 1048576}MB") else: log.error("Echec telechargement ISO") if os.path.exists(iso): os.remove(iso) return False if not os.path.exists(disk): log.info("Creation disque virtuel...") subprocess.run([ "qemu-img", "create", "-f", "qcow2", disk, "8G" ], check=True) return True # ============================================================ # QEMU # ============================================================ def start_qemu(): iso = os.path.join(WORK, "android.iso") disk = os.path.join(WORK, "disk.qcow2") kvm = os.path.exists("/dev/kvm") cmd = ["qemu-system-x86_64"] if kvm: cmd += ["-enable-kvm", "-cpu", "host"] log.info("KVM disponible") else: cmd += [ "-cpu", "qemu64", "-accel", "tcg,thread=multi", ] log.warning("Pas de KVM = LENT") cmd += [ "-m", "1024", "-smp", "2", "-drive", f"file={disk},format=qcow2,if=virtio", "-cdrom", iso, "-boot", "d", "-vnc", ":0", "-vga", "std", "-display", "none", "-usb", "-device", "usb-tablet", "-net", "nic,model=virtio", "-net", "user", "-audiodev", "none,id=noaudio", ] log.info("Demarrage QEMU...") log.info(f"CMD: {' '.join(cmd)}") proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) def reader(): for line in iter(proc.stdout.readline, b""): txt = line.decode("utf-8", errors="replace").strip() if txt: log.info(f"[QEMU] {txt}") threading.Thread(target=reader, daemon=True).start() return proc # ============================================================ # NOVNC # ============================================================ def start_novnc(): novnc_dir = "/opt/noVNC" cmd = [ sys.executable, "-m", "websockify", "--web", novnc_dir, str(NOVNC_PORT), f"localhost:{VNC_PORT}", ] log.info("Demarrage noVNC...") proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) return proc # ============================================================ # PIPELINE # ============================================================ def start_all(): try: state["phase"] = "downloading" ok = download_android() if not ok: state["phase"] = "error" return state["phase"] = "booting" state["qemu_proc"] = start_qemu() time.sleep(3) state["novnc_proc"] = start_novnc() state["phase"] = "running" log.info("EMULATEUR PRET") log.info(f"noVNC sur le port {NOVNC_PORT}") except Exception as e: log.error(f"Erreur: {e}") state["phase"] = "error" def stop_all(): if state["qemu_proc"]: state["qemu_proc"].terminate() state["qemu_proc"] = None if state["novnc_proc"]: state["novnc_proc"].terminate() state["novnc_proc"] = None state["phase"] = "idle" # ============================================================ # WEB # ============================================================ HTML = """
Cliquez Demarrer