aripbae commited on
Commit
b68ed24
·
verified ·
1 Parent(s): 5ff05a7

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +572 -0
app.py ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ from flask_cors import CORS
3
+ import os
4
+ import time
5
+ import traceback
6
+ from pathlib import Path
7
+ import threading
8
+ import atexit
9
+ import random
10
+ import math
11
+ from PIL import Image, ImageDraw
12
+ import requests
13
+ import uuid
14
+ from datetime import datetime, timedelta
15
+ from collections import deque
16
+ import string
17
+
18
+ app = Flask(__name__)
19
+ CORS(app)
20
+
21
+ BASE_DIR = Path(__file__).parent
22
+ PUBLIC_DIR = BASE_DIR / 'public'
23
+ PUBLIC_DIR.mkdir(exist_ok=True)
24
+
25
+ PORT = int(os.environ.get('PORT', 7860))
26
+
27
+ VIEWPORT_WIDTH = 1920
28
+ VIEWPORT_HEIGHT = 1080
29
+
30
+ VIEWPORT_CONFIG = {
31
+ 'width': VIEWPORT_WIDTH,
32
+ 'height': VIEWPORT_HEIGHT,
33
+ 'device_scale_factor': 1
34
+ }
35
+
36
+ MAX_ROOMS = 5
37
+ ROOM_TIMEOUT_MINUTES = 10
38
+ SCREENSHOT_EXPIRY_MINUTES = 10
39
+ JOB_EXPIRY_MINUTES = 30
40
+
41
+ SCREENSHOT_BASE_URL = "http://pnode1.danbot.host:1149"
42
+
43
+ class Job:
44
+ def __init__(self, job_id, code, lang, base_url):
45
+ self.job_id = job_id
46
+ self.code = code
47
+ self.lang = lang
48
+ self.base_url = base_url
49
+ self.status = 'queued'
50
+ self.result = None
51
+ self.error = None
52
+ self.created_at = datetime.now()
53
+ self.started_at = None
54
+ self.completed_at = None
55
+ self.room_id = None
56
+ self.screenshots = []
57
+
58
+ def to_dict(self):
59
+ return {
60
+ 'job_id': self.job_id,
61
+ 'status': self.status,
62
+ 'result': self.result,
63
+ 'error': self.error,
64
+ 'created_at': self.created_at.isoformat(),
65
+ 'started_at': self.started_at.isoformat() if self.started_at else None,
66
+ 'completed_at': self.completed_at.isoformat() if self.completed_at else None,
67
+ 'room_id': self.room_id,
68
+ 'screenshots': self.screenshots
69
+ }
70
+
71
+ def is_expired(self):
72
+ return (datetime.now() - self.created_at) > timedelta(minutes=JOB_EXPIRY_MINUTES)
73
+
74
+ class BrowserRoom:
75
+ def __init__(self, room_id):
76
+ self.room_id = room_id
77
+ self.browser = None
78
+ self.context = None
79
+ self.page = None
80
+ self.cookies = []
81
+ self.created_at = datetime.now()
82
+ self.last_activity = datetime.now()
83
+ self.is_busy = False
84
+ self.current_job_id = None
85
+ self.lock = threading.Lock()
86
+ self.screenshots_dir = PUBLIC_DIR / f'room_{room_id}'
87
+ self.screenshots_dir.mkdir(exist_ok=True)
88
+
89
+ def update_activity(self):
90
+ self.last_activity = datetime.now()
91
+
92
+ def is_expired(self):
93
+ return (datetime.now() - self.last_activity) > timedelta(minutes=ROOM_TIMEOUT_MINUTES)
94
+
95
+ def cleanup_screenshots(self):
96
+ try:
97
+ current_time = time.time()
98
+ for file in self.screenshots_dir.glob('*.png'):
99
+ file_age = current_time - file.stat().st_mtime
100
+ if file_age > (SCREENSHOT_EXPIRY_MINUTES * 60):
101
+ file.unlink()
102
+ print(f"[ROOM-{self.room_id}] Deleted expired screenshot: {file.name}")
103
+ except Exception as e:
104
+ print(f"[ROOM-{self.room_id}] Screenshot cleanup error: {e}")
105
+
106
+ def reset_browser(self):
107
+ try:
108
+ if self.page:
109
+ self.page.close()
110
+ self.page = None
111
+ if self.context:
112
+ try:
113
+ self.cookies = self.context.cookies()
114
+ print(f"[ROOM-{self.room_id}] Saved {len(self.cookies)} cookies")
115
+ except:
116
+ pass
117
+ self.context.close()
118
+ self.context = None
119
+ if self.browser:
120
+ self.browser.close()
121
+ self.browser = None
122
+ except Exception as e:
123
+ print(f"[ROOM-{self.room_id}] Browser reset error: {e}")
124
+
125
+ def cleanup(self):
126
+ try:
127
+ self.reset_browser()
128
+ self.cleanup_screenshots()
129
+ if self.screenshots_dir.exists():
130
+ try:
131
+ self.screenshots_dir.rmdir()
132
+ except:
133
+ pass
134
+ except Exception as e:
135
+ print(f"[ROOM-{self.room_id}] Cleanup error: {e}")
136
+
137
+ class JobManager:
138
+ def __init__(self):
139
+ self.jobs = {}
140
+ self.lock = threading.Lock()
141
+
142
+ def generate_job_id(self):
143
+ while True:
144
+ job_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=3))
145
+ if job_id not in self.jobs:
146
+ return job_id
147
+
148
+ def create_job(self, code, lang, base_url):
149
+ with self.lock:
150
+ job_id = self.generate_job_id()
151
+ job = Job(job_id, code, lang, base_url)
152
+ self.jobs[job_id] = job
153
+ return job
154
+
155
+ def get_job(self, job_id):
156
+ with self.lock:
157
+ return self.jobs.get(job_id)
158
+
159
+ def update_job(self, job_id, **kwargs):
160
+ with self.lock:
161
+ if job_id in self.jobs:
162
+ job = self.jobs[job_id]
163
+ for key, value in kwargs.items():
164
+ setattr(job, key, value)
165
+
166
+ def cleanup_expired_jobs(self):
167
+ with self.lock:
168
+ expired = [jid for jid, job in self.jobs.items() if job.is_expired()]
169
+ for jid in expired:
170
+ del self.jobs[jid]
171
+ print(f"[JOB-{jid}] Deleted expired job")
172
+
173
+ class RoomManager:
174
+ def __init__(self):
175
+ self.rooms = {}
176
+ self.available_rooms = deque(range(1, MAX_ROOMS + 1))
177
+ self.lock = threading.Lock()
178
+ self.cleanup_thread = None
179
+ self.stop_cleanup = threading.Event()
180
+
181
+ def acquire_room(self, job_id, timeout=300):
182
+ start_time = time.time()
183
+ while True:
184
+ with self.lock:
185
+ if self.available_rooms:
186
+ room_id = self.available_rooms.popleft()
187
+ if room_id not in self.rooms:
188
+ self.rooms[room_id] = BrowserRoom(room_id)
189
+ room = self.rooms[room_id]
190
+ room.is_busy = True
191
+ room.current_job_id = job_id
192
+ room.update_activity()
193
+ print(f"[ROOM-{room_id}] Acquired for job {job_id}. Available rooms: {len(self.available_rooms)}/{MAX_ROOMS}")
194
+ return room
195
+
196
+ if time.time() - start_time > timeout:
197
+ raise Exception(f"Timeout waiting for available room after {timeout}s. All {MAX_ROOMS} rooms are busy.")
198
+
199
+ print(f"[QUEUE] All {MAX_ROOMS} rooms busy. Waiting for available room...")
200
+ time.sleep(1)
201
+
202
+ def release_room(self, room):
203
+ with self.lock:
204
+ room.is_busy = False
205
+ room.current_job_id = None
206
+ room.update_activity()
207
+ if room.room_id not in self.available_rooms:
208
+ self.available_rooms.append(room.room_id)
209
+ print(f"[ROOM-{room.room_id}] Released. Available rooms: {len(self.available_rooms)}/{MAX_ROOMS}")
210
+
211
+ def cleanup_expired_rooms(self):
212
+ while not self.stop_cleanup.is_set():
213
+ try:
214
+ current_time = time.time()
215
+
216
+ with self.lock:
217
+ for room_id, room in list(self.rooms.items()):
218
+ if not room.is_busy:
219
+ try:
220
+ for file in room.screenshots_dir.glob('*.png'):
221
+ file_age = current_time - file.stat().st_mtime
222
+ if file_age > (SCREENSHOT_EXPIRY_MINUTES * 60):
223
+ file.unlink()
224
+ print(f"[ROOM-{room_id}] Auto-deleted expired screenshot: {file.name}")
225
+ except Exception as e:
226
+ print(f"[ROOM-{room_id}] Auto cleanup screenshot error: {e}")
227
+
228
+ if not room.is_busy and room.is_expired():
229
+ print(f"[ROOM-{room_id}] Cleaning up expired room")
230
+ self.rooms[room_id].cleanup()
231
+ del self.rooms[room_id]
232
+
233
+ except Exception as e:
234
+ print(f"[CLEANUP] Error: {e}")
235
+ self.stop_cleanup.wait(60)
236
+
237
+ def start_cleanup(self):
238
+ self.cleanup_thread = threading.Thread(target=self.cleanup_expired_rooms, daemon=True)
239
+ self.cleanup_thread.start()
240
+
241
+ def stop_cleanup_thread(self):
242
+ self.stop_cleanup.set()
243
+ if self.cleanup_thread:
244
+ self.cleanup_thread.join(timeout=5)
245
+
246
+ def shutdown_all(self):
247
+ with self.lock:
248
+ for room in self.rooms.values():
249
+ room.cleanup()
250
+ self.rooms.clear()
251
+ self.available_rooms.clear()
252
+
253
+ room_manager = RoomManager()
254
+ job_manager = JobManager()
255
+
256
+ def execute_in_room(code_snippet, room, job_id):
257
+ result = {'screenshots': [], 'data': None, 'error': None}
258
+
259
+ room.reset_browser()
260
+
261
+ browser_obj = None
262
+
263
+ try:
264
+ print(f"[ROOM-{room.room_id}] [JOB-{job_id}] Starting execution at {time.time()}")
265
+
266
+ from camoufox.sync_api import Camoufox
267
+
268
+ proxy_server = "http://pnode1.danbot.host:1271"
269
+
270
+ browser_obj = Camoufox(
271
+ headless=True,
272
+ humanize=True,
273
+ proxy={'server': proxy_server}
274
+ )
275
+
276
+ browser_obj = browser_obj.__enter__()
277
+
278
+ if room.cookies:
279
+ print(f"[ROOM-{room.room_id}] [JOB-{job_id}] Restoring {len(room.cookies)} cookies")
280
+
281
+ viewport_inject_code = """
282
+ import sys
283
+ _original_goto = None
284
+
285
+ def _patched_goto(self, url, **kwargs):
286
+ result = _original_goto(self, url, **kwargs)
287
+ try:
288
+ self.evaluate('''
289
+ let meta = document.querySelector('meta[name="viewport"]');
290
+ if (!meta) {
291
+ meta = document.createElement('meta');
292
+ meta.name = 'viewport';
293
+ document.head.appendChild(meta);
294
+ }
295
+ meta.content = 'width=1920, height=1080, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
296
+ document.documentElement.style.width = '1920px';
297
+ document.documentElement.style.height = '1080px';
298
+ document.body.style.width = '1920px';
299
+ document.body.style.height = '1080px';
300
+ ''')
301
+ except:
302
+ pass
303
+ return result
304
+
305
+ if 'browser' in dir() and browser is not None:
306
+ try:
307
+ from playwright.sync_api import Page
308
+ if not hasattr(Page, '_viewport_patched'):
309
+ _original_goto = Page.goto
310
+ Page.goto = _patched_goto
311
+ Page._viewport_patched = True
312
+ except:
313
+ pass
314
+ """
315
+
316
+ namespace = {
317
+ 'browser': browser_obj,
318
+ 'room_cookies': room.cookies,
319
+ 'public_dir': str(room.screenshots_dir),
320
+ 'room_id': room.room_id,
321
+ 'job_id': job_id,
322
+ 'time': time,
323
+ 'result': result,
324
+ 'Path': Path,
325
+ 'random': random,
326
+ 'math': math,
327
+ 'Image': Image,
328
+ 'ImageDraw': ImageDraw,
329
+ 'requests': requests,
330
+ 'print': print,
331
+ 'len': len,
332
+ 'int': int,
333
+ 'str': str,
334
+ 'dict': dict,
335
+ 'list': list,
336
+ 'VIEWPORT_WIDTH': VIEWPORT_WIDTH,
337
+ 'VIEWPORT_HEIGHT': VIEWPORT_HEIGHT,
338
+ }
339
+
340
+ exec(viewport_inject_code, namespace)
341
+
342
+ print(f"[ROOM-{room.room_id}] [JOB-{job_id}] Executing code...")
343
+ exec(code_snippet, namespace)
344
+
345
+ if 'return_value' in namespace:
346
+ result['data'] = namespace['return_value']
347
+
348
+ print(f"[ROOM-{room.room_id}] [JOB-{job_id}] Execution completed successfully")
349
+
350
+ except Exception as e:
351
+ error_msg = str(e) + "\n" + traceback.format_exc()
352
+ result['error'] = error_msg
353
+ print(f"[ROOM-{room.room_id}] [JOB-{job_id}] ERROR: {error_msg}")
354
+ finally:
355
+ if browser_obj:
356
+ try:
357
+ browser_obj.__exit__(None, None, None)
358
+ except:
359
+ pass
360
+
361
+ return result
362
+
363
+ def process_job(job):
364
+ room = None
365
+ try:
366
+ job_manager.update_job(job.job_id, status='running', started_at=datetime.now())
367
+
368
+ room = room_manager.acquire_room(job.job_id, timeout=900)
369
+ job_manager.update_job(job.job_id, room_id=room.room_id)
370
+
371
+ result = execute_in_room(job.code, room, job.job_id)
372
+
373
+ screenshot_files = []
374
+ for file in sorted(room.screenshots_dir.glob('*.png'), key=lambda x: x.stat().st_mtime):
375
+ screenshot_files.append({
376
+ 'name': file.name,
377
+ 'publicURL': f"{SCREENSHOT_BASE_URL}/files/room_{room.room_id}/{file.name}"
378
+ })
379
+
380
+ if result.get('error'):
381
+ job_manager.update_job(
382
+ job.job_id,
383
+ status='failed',
384
+ error=result['error'],
385
+ completed_at=datetime.now(),
386
+ screenshots=screenshot_files
387
+ )
388
+ else:
389
+ job_manager.update_job(
390
+ job.job_id,
391
+ status='completed',
392
+ result=result.get('data'),
393
+ completed_at=datetime.now(),
394
+ screenshots=screenshot_files
395
+ )
396
+
397
+ except Exception as e:
398
+ error_msg = str(e) + "\n" + traceback.format_exc()
399
+ job_manager.update_job(
400
+ job.job_id,
401
+ status='failed',
402
+ error=error_msg,
403
+ completed_at=datetime.now()
404
+ )
405
+ finally:
406
+ if room:
407
+ room.cleanup_screenshots()
408
+ room_manager.release_room(room)
409
+
410
+ @app.route('/api/s-playwright', methods=['POST'])
411
+ def execute_playwright():
412
+ try:
413
+ data = request.get_json()
414
+
415
+ if not data or 'code' not in data or 'lang' not in data:
416
+ return jsonify({
417
+ 'success': False,
418
+ 'error': 'Missing required fields: code and lang'
419
+ }), 400
420
+
421
+ code = data['code']
422
+ lang = data['lang'].lower()
423
+
424
+ if lang != 'python':
425
+ return jsonify({
426
+ 'success': False,
427
+ 'error': f'Only Python is supported, got: {lang}'
428
+ }), 400
429
+
430
+ base_url = request.url_root.rstrip('/')
431
+ job = job_manager.create_job(code, lang, base_url)
432
+
433
+ thread = threading.Thread(target=process_job, args=(job,))
434
+ thread.daemon = True
435
+ thread.start()
436
+
437
+ return jsonify({
438
+ 'success': True,
439
+ 'job_id': job.job_id,
440
+ 'status': 'queued',
441
+ 'check_url': f'/job/{job.job_id}'
442
+ })
443
+
444
+ except Exception as e:
445
+ print(f"[API ERROR] {str(e)}\n{traceback.format_exc()}")
446
+ return jsonify({
447
+ 'success': False,
448
+ 'error': str(e),
449
+ 'stack': traceback.format_exc()
450
+ }), 500
451
+
452
+ @app.route('/job/<string:job_id>', methods=['GET'])
453
+ def get_job_status(job_id):
454
+ job = job_manager.get_job(job_id.upper())
455
+
456
+ if not job:
457
+ return jsonify({
458
+ 'success': False,
459
+ 'error': 'Job not found'
460
+ }), 404
461
+
462
+ return jsonify({
463
+ 'success': True,
464
+ 'job': job.to_dict()
465
+ })
466
+
467
+ @app.route('/files/room_<int:room_id>/<path:filename>')
468
+ def serve_file(room_id, filename):
469
+ room_dir = PUBLIC_DIR / f'room_{room_id}'
470
+ return send_from_directory(room_dir, filename)
471
+
472
+ @app.route('/health', methods=['GET'])
473
+ def health():
474
+ with room_manager.lock:
475
+ busy_rooms = sum(1 for r in room_manager.rooms.values() if r.is_busy)
476
+ available = len(room_manager.available_rooms)
477
+
478
+ with job_manager.lock:
479
+ total_jobs = len(job_manager.jobs)
480
+ queued_jobs = sum(1 for j in job_manager.jobs.values() if j.status == 'queued')
481
+ running_jobs = sum(1 for j in job_manager.jobs.values() if j.status == 'running')
482
+
483
+ return jsonify({
484
+ 'status': 'healthy',
485
+ 'rooms': {
486
+ 'total': MAX_ROOMS,
487
+ 'available': available,
488
+ 'busy': busy_rooms
489
+ },
490
+ 'jobs': {
491
+ 'total': total_jobs,
492
+ 'queued': queued_jobs,
493
+ 'running': running_jobs
494
+ },
495
+ 'viewport': {
496
+ 'width': VIEWPORT_WIDTH,
497
+ 'height': VIEWPORT_HEIGHT,
498
+ 'locked': True
499
+ },
500
+ 'timestamp': int(time.time() * 1000)
501
+ })
502
+
503
+ @app.route('/', methods=['GET'])
504
+ def index():
505
+ with room_manager.lock:
506
+ busy_rooms = sum(1 for r in room_manager.rooms.values() if r.is_busy)
507
+ available = len(room_manager.available_rooms)
508
+
509
+ return jsonify({
510
+ 'message': 'Multi-Room Camoufox Anti-Detection API with Job System',
511
+ 'endpoints': {
512
+ 'POST /api/s-playwright': 'Execute camoufox code (returns job_id)',
513
+ 'GET /job/:id': 'Check job status (3-character job ID)',
514
+ 'GET /health': 'Check API health status'
515
+ },
516
+ 'features': [
517
+ f'{MAX_ROOMS} Isolated Browser Rooms',
518
+ 'Job Queue System with 3-char IDs',
519
+ 'Async Job Processing',
520
+ f'Viewport LOCKED at {VIEWPORT_WIDTH}x{VIEWPORT_HEIGHT}',
521
+ 'Camoufox Anti-Detection Browser',
522
+ 'Human-like Mouse Movement',
523
+ 'Advanced Cloudflare WAF Bypass',
524
+ 'Auto Room & Job Cleanup',
525
+ 'Concurrent Request Protection'
526
+ ],
527
+ 'configuration': {
528
+ 'viewport': {'width': VIEWPORT_WIDTH, 'height': VIEWPORT_HEIGHT, 'locked': True},
529
+ 'port': PORT,
530
+ 'rooms': {
531
+ 'total': MAX_ROOMS,
532
+ 'available': available,
533
+ 'busy': busy_rooms
534
+ },
535
+ 'timeout': '1200 seconds per execution',
536
+ 'room_timeout': f'{ROOM_TIMEOUT_MINUTES} minutes',
537
+ 'job_expiry': f'{JOB_EXPIRY_MINUTES} minutes',
538
+ 'screenshot_base_url': SCREENSHOT_BASE_URL
539
+ }
540
+ })
541
+
542
+ if __name__ == '__main__':
543
+ print(f"🚀 Multi-Room Camoufox API with Job System")
544
+ print(f"🌐 Port: {PORT}")
545
+ print(f"🏠 Browser Rooms: {MAX_ROOMS}")
546
+ print(f"📐 Viewport: LOCKED at {VIEWPORT_WIDTH}x{VIEWPORT_HEIGHT}")
547
+ print(f"📍 Endpoints: POST /api/s-playwright, GET /job/:id")
548
+ print(f"🎭 Features: Job queue, 3-char IDs, Async processing")
549
+ print(f"🖱️ Human mouse movement: ENABLED")
550
+ print(f"🌍 Proxy: http://pnode1.danbot.host:1271")
551
+ print(f"📸 Screenshot URL: {SCREENSHOT_BASE_URL}")
552
+ print(f"⏱️ Execution timeout: 1200 seconds")
553
+ print(f"🔒 Room timeout: {ROOM_TIMEOUT_MINUTES} minutes")
554
+ print(f"📦 Job expiry: {JOB_EXPIRY_MINUTES} minutes")
555
+
556
+ room_manager.start_cleanup()
557
+
558
+ def cleanup_jobs():
559
+ while True:
560
+ job_manager.cleanup_expired_jobs()
561
+ time.sleep(300)
562
+
563
+ job_cleanup_thread = threading.Thread(target=cleanup_jobs, daemon=True)
564
+ job_cleanup_thread.start()
565
+
566
+ def cleanup_on_exit():
567
+ room_manager.stop_cleanup_thread()
568
+ room_manager.shutdown_all()
569
+
570
+ atexit.register(cleanup_on_exit)
571
+
572
+ app.run(host='0.0.0.0', port=PORT, debug=False, threaded=True)