Hugdu commited on
Commit
6c7651a
·
verified ·
1 Parent(s): 947803d

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +318 -0
main.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Android Emulator - Hugging Face Spaces
4
+ Version ultra-legere sans noVNC
5
+ L'ecran est streame en screenshots
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import subprocess
11
+ import threading
12
+ import time
13
+ import json
14
+ import shutil
15
+ import http.server
16
+ import socketserver
17
+ import logging
18
+
19
+ PORT = 7860
20
+ WORK = "/app/android"
21
+
22
+ logging.basicConfig(level=logging.INFO)
23
+ log = logging.getLogger("emu")
24
+
25
+ LOG_BUFFER = []
26
+
27
+ class WL(logging.Handler):
28
+ def emit(self, record):
29
+ LOG_BUFFER.append(self.format(record))
30
+ if len(LOG_BUFFER) > 100:
31
+ LOG_BUFFER.pop(0)
32
+
33
+ wh = WL()
34
+ wh.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
35
+ log.addHandler(wh)
36
+
37
+ state = {"phase": "idle", "proc": None}
38
+
39
+ HTML = r"""<!DOCTYPE html>
40
+ <html>
41
+ <head>
42
+ <meta charset="UTF-8">
43
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
44
+ <title>Android Emulator</title>
45
+ <style>
46
+ *{margin:0;padding:0;box-sizing:border-box}
47
+ body{font-family:system-ui;background:#0a0a1a;color:#e0e0e0}
48
+ .c{max-width:900px;margin:0 auto;padding:15px}
49
+ h1{text-align:center;padding:20px;font-size:1.8em;
50
+ background:linear-gradient(135deg,#3ddc84,#4285f4);
51
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent}
52
+ .phase{text-align:center;font-size:1.2em;padding:10px;font-weight:bold}
53
+ .idle{color:#666}.downloading{color:#ffab40}
54
+ .booting{color:#ffab40}.running{color:#3ddc84}.error{color:#ff5252}
55
+ .btns{display:flex;gap:10px;justify-content:center;padding:15px}
56
+ .btn{padding:10px 25px;border:none;border-radius:6px;
57
+ font-weight:bold;cursor:pointer;font-size:1em}
58
+ .btn:disabled{opacity:.4;cursor:not-allowed}
59
+ .btn-go{background:#3ddc84;color:#000}
60
+ .btn-stop{background:#ff5252;color:#fff}
61
+ .card{background:#12122a;border:1px solid #2a2a4a;
62
+ border-radius:8px;padding:12px;margin:10px 0}
63
+ .card h3{color:#4285f4;margin-bottom:8px}
64
+ .row{display:flex;justify-content:space-between;padding:4px 0;
65
+ border-bottom:1px solid #1a1a3a;font-size:.9em}
66
+ .on{color:#3ddc84}.off{color:#ff5252}.warn{color:#ffab40}
67
+ .logs{background:#000;border-radius:6px;padding:10px;
68
+ font-family:monospace;font-size:.75em;color:#0f0;
69
+ max-height:300px;overflow-y:auto;white-space:pre-wrap}
70
+ .w{background:#2d1b00;border:1px solid #ff9800;
71
+ border-radius:8px;padding:12px;margin:10px 0;color:#ffcc80;font-size:.9em}
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div class="c">
76
+ <h1>Android Emulator</h1>
77
+
78
+ <div class="w">
79
+ <strong>Version proof-of-concept.</strong>
80
+ HF Free = 2GB RAM, pas de KVM.
81
+ Android sera tres lent a demarrer (10-30 min).
82
+ </div>
83
+
84
+ <div class="phase" id="phase">Chargement...</div>
85
+
86
+ <div class="btns">
87
+ <button class="btn btn-go" id="bS" onclick="api('start')">Demarrer</button>
88
+ <button class="btn btn-stop" id="bX" onclick="api('stop')" disabled>Arreter</button>
89
+ </div>
90
+
91
+ <div class="card">
92
+ <h3>Systeme</h3>
93
+ <div id="sys">-</div>
94
+ </div>
95
+
96
+ <div class="card">
97
+ <h3>Logs</h3>
98
+ <div class="logs" id="logs">En attente...</div>
99
+ </div>
100
+ </div>
101
+
102
+ <script>
103
+ const P={idle:"En attente",downloading:"Telechargement...",
104
+ booting:"Demarrage Android...",running:"Android tourne!",error:"Erreur"};
105
+
106
+ function api(a){fetch('/api/'+a).then(r=>r.json()).then(d=>{
107
+ setTimeout(refresh,1000)})}
108
+
109
+ function refresh(){fetch('/api/status').then(r=>r.json()).then(d=>{
110
+ const p=document.getElementById('phase');
111
+ p.textContent=P[d.phase]||d.phase;
112
+ p.className='phase '+d.phase;
113
+
114
+ const run=d.phase==='running'||d.phase==='booting';
115
+ const busy=d.phase==='downloading';
116
+ document.getElementById('bS').disabled=run||busy;
117
+ document.getElementById('bX').disabled=!run;
118
+
119
+ document.getElementById('sys').innerHTML=
120
+ '<div class="row"><span>KVM</span><span class="'+(d.kvm?'on':'off')+'">'+(d.kvm?'Oui':'Non')+'</span></div>'+
121
+ '<div class="row"><span>RAM</span><span>'+d.ram+'</span></div>'+
122
+ '<div class="row"><span>QEMU</span><span class="'+(d.qemu?'on':'off')+'">'+(d.qemu?'OK':'Absent')+'</span></div>'+
123
+ '<div class="row"><span>ISO</span><span class="'+(d.iso?'on':'warn')+'">'+(d.iso?'Prete':'Non')+'</span></div>'+
124
+ '<div class="row"><span>Disque</span><span class="'+(d.disk?'on':'warn')+'">'+(d.disk?'OK':'Non')+'</span></div>';
125
+
126
+ if(d.logs&&d.logs.length>0){
127
+ const l=document.getElementById('logs');
128
+ l.textContent=d.logs.join('\n');
129
+ l.scrollTop=l.scrollHeight}
130
+ }).catch(e=>{})}
131
+
132
+ setInterval(refresh,3000);
133
+ refresh();
134
+ </script>
135
+ </body>
136
+ </html>"""
137
+
138
+
139
+ def get_ram():
140
+ try:
141
+ with open("/proc/meminfo") as f:
142
+ lines = f.readlines()
143
+ total = int(lines[0].split()[1]) // 1024
144
+ avail = 0
145
+ for l in lines:
146
+ if l.startswith("MemAvailable:"):
147
+ avail = int(l.split()[1]) // 1024
148
+ return f"{total - avail}MB / {total}MB"
149
+ except:
150
+ return "?"
151
+
152
+
153
+ def download_iso():
154
+ iso = os.path.join(WORK, "android.iso")
155
+ if os.path.exists(iso) and os.path.getsize(iso) > 1000:
156
+ log.info(f"ISO deja la: {os.path.getsize(iso)//1048576}MB")
157
+ return True
158
+
159
+ os.makedirs(WORK, exist_ok=True)
160
+ log.info("Telechargement ISO Android-x86 9.0...")
161
+
162
+ url = "https://sourceforge.net/projects/android-x86/files/Release%209.0/android-x86_64-9.0-r2.iso/download"
163
+
164
+ r = subprocess.run(["wget","-q","-O",iso,url], capture_output=True, text=True)
165
+
166
+ if os.path.exists(iso) and os.path.getsize(iso) > 1000:
167
+ log.info(f"OK: {os.path.getsize(iso)//1048576}MB")
168
+ return True
169
+ else:
170
+ log.error("Echec telechargement")
171
+ log.error(r.stderr if r.stderr else "pas d'erreur")
172
+ return False
173
+
174
+
175
+ def create_disk():
176
+ disk = os.path.join(WORK, "disk.qcow2")
177
+ if os.path.exists(disk):
178
+ return True
179
+ log.info("Creation disque 4G...")
180
+ subprocess.run(["qemu-img","create","-f","qcow2",disk,"4G"], check=True)
181
+ return True
182
+
183
+
184
+ def start_qemu():
185
+ iso = os.path.join(WORK, "android.iso")
186
+ disk = os.path.join(WORK, "disk.qcow2")
187
+ kvm = os.path.exists("/dev/kvm")
188
+
189
+ cmd = ["qemu-system-x86_64"]
190
+
191
+ if kvm:
192
+ cmd += ["-enable-kvm", "-cpu", "host"]
193
+ else:
194
+ cmd += ["-cpu", "qemu64", "-accel", "tcg,thread=multi"]
195
+
196
+ cmd += [
197
+ "-m", "1024",
198
+ "-smp", "2",
199
+ "-drive", f"file={disk},format=qcow2,if=virtio",
200
+ "-cdrom", iso,
201
+ "-boot", "d",
202
+ "-display", "none",
203
+ "-net", "nic,model=virtio",
204
+ "-net", "user",
205
+ "-nographic",
206
+ ]
207
+
208
+ log.info(f"QEMU: {' '.join(cmd)}")
209
+
210
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
211
+
212
+ def reader():
213
+ for line in iter(proc.stdout.readline, b""):
214
+ t = line.decode("utf-8", errors="replace").strip()
215
+ if t:
216
+ log.info(f"[QEMU] {t}")
217
+
218
+ threading.Thread(target=reader, daemon=True).start()
219
+ return proc
220
+
221
+
222
+ def pipeline():
223
+ try:
224
+ state["phase"] = "downloading"
225
+ if not download_iso():
226
+ state["phase"] = "error"
227
+ return
228
+
229
+ create_disk()
230
+
231
+ state["phase"] = "booting"
232
+ state["proc"] = start_qemu()
233
+
234
+ state["phase"] = "running"
235
+ log.info("QEMU demarre!")
236
+ log.info("Sans KVM = tres lent, patience...")
237
+
238
+ except Exception as e:
239
+ log.error(f"Erreur: {e}")
240
+ state["phase"] = "error"
241
+
242
+
243
+ def stop():
244
+ if state["proc"]:
245
+ state["proc"].terminate()
246
+ state["proc"] = None
247
+ state["phase"] = "idle"
248
+
249
+
250
+ class H(http.server.BaseHTTPRequestHandler):
251
+ def do_GET(self):
252
+ p = self.path.split("?")[0]
253
+
254
+ if p == "/":
255
+ self.send_response(200)
256
+ self.send_header("Content-Type", "text/html")
257
+ self.end_headers()
258
+ self.wfile.write(HTML.encode())
259
+
260
+ elif p == "/api/status":
261
+ running = state["proc"] and state["proc"].poll() is None
262
+ d = {
263
+ "phase": state["phase"],
264
+ "kvm": os.path.exists("/dev/kvm"),
265
+ "ram": get_ram(),
266
+ "qemu": shutil.which("qemu-system-x86_64") is not None,
267
+ "iso": os.path.exists(os.path.join(WORK, "android.iso")),
268
+ "disk": os.path.exists(os.path.join(WORK, "disk.qcow2")),
269
+ "running": running,
270
+ "logs": LOG_BUFFER[-50:],
271
+ }
272
+ self.send_response(200)
273
+ self.send_header("Content-Type", "application/json")
274
+ self.end_headers()
275
+ self.wfile.write(json.dumps(d).encode())
276
+
277
+ elif p == "/api/start":
278
+ if state["phase"] in ("idle", "error"):
279
+ threading.Thread(target=pipeline, daemon=True).start()
280
+ m = {"ok": True, "message": "Demarrage..."}
281
+ else:
282
+ m = {"ok": False, "message": f"Phase: {state['phase']}"}
283
+ self.send_response(200)
284
+ self.send_header("Content-Type", "application/json")
285
+ self.end_headers()
286
+ self.wfile.write(json.dumps(m).encode())
287
+
288
+ elif p == "/api/stop":
289
+ stop()
290
+ m = {"ok": True, "message": "Arrete"}
291
+ self.send_response(200)
292
+ self.send_header("Content-Type", "application/json")
293
+ self.end_headers()
294
+ self.wfile.write(json.dumps(m).encode())
295
+
296
+ else:
297
+ self.send_error(404)
298
+
299
+ def log_message(self, *a):
300
+ pass
301
+
302
+
303
+ def main():
304
+ log.info("=" * 40)
305
+ log.info(" Android Emulator - HF Spaces")
306
+ log.info("=" * 40)
307
+ log.info(f"KVM: {os.path.exists('/dev/kvm')}")
308
+ log.info(f"QEMU: {shutil.which('qemu-system-x86_64')}")
309
+ log.info(f"RAM: {get_ram()}")
310
+
311
+ s = socketserver.ThreadingTCPServer(("0.0.0.0", PORT), H)
312
+ s.allow_reuse_address = True
313
+ log.info(f"http://0.0.0.0:{PORT}")
314
+ s.serve_forever()
315
+
316
+
317
+ if __name__ == "__main__":
318
+ main()