Fix: Use reference logic with user_lock pattern, remove eventlet dependency
Browse files
app.py
CHANGED
|
@@ -4,10 +4,6 @@ Pong backend (GPU, eager) for Hugging Face Spaces.
|
|
| 4 |
Broadcasts readiness via Socket.IO so the frontend can auto-hide a loading overlay once the model is ready.
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
# Eventlet must be imported first and monkey-patched before other imports
|
| 8 |
-
import eventlet
|
| 9 |
-
eventlet.monkey_patch()
|
| 10 |
-
|
| 11 |
import sys
|
| 12 |
import os
|
| 13 |
import time
|
|
@@ -42,11 +38,11 @@ from src.config import Config
|
|
| 42 |
# --------------------------
|
| 43 |
app = Flask(__name__, static_folder='static')
|
| 44 |
CORS(app)
|
| 45 |
-
# Configure SocketIO - use
|
| 46 |
socketio = SocketIO(
|
| 47 |
app,
|
| 48 |
cors_allowed_origins="*",
|
| 49 |
-
async_mode='
|
| 50 |
logger=False,
|
| 51 |
engineio_logger=False,
|
| 52 |
ping_timeout=60,
|
|
@@ -63,10 +59,13 @@ device = None
|
|
| 63 |
|
| 64 |
server_ready = False # <--- readiness flag
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
stream_lock = threading.Lock()
|
| 67 |
stream_thread = None
|
| 68 |
stream_running = False
|
| 69 |
-
current_player_sid = None # Track which session is currently playing
|
| 70 |
latest_action = 1 # 0=init, 1=nothing, 2=up, 3=down
|
| 71 |
target_fps = 30
|
| 72 |
frame_index = 0
|
|
@@ -148,7 +147,7 @@ def _reset_cache_fresh():
|
|
| 148 |
|
| 149 |
def _broadcast_ready():
|
| 150 |
"""Tell all clients whether the server is ready."""
|
| 151 |
-
socketio.emit('server_status', {'ready': server_ready
|
| 152 |
|
| 153 |
# --------------------------
|
| 154 |
# Model init (pure eager) & warmup
|
|
@@ -369,16 +368,13 @@ def generate_frames():
|
|
| 369 |
# --------------------------
|
| 370 |
# Socket events & helpers
|
| 371 |
# --------------------------
|
| 372 |
-
def start_stream(n_steps=8, cfg=0.0, fps=30, clamp=True
|
| 373 |
-
global stream_thread, stream_running, frame_index, target_fps, latest_action
|
| 374 |
if not server_ready:
|
| 375 |
_broadcast_ready()
|
| 376 |
raise RuntimeError("Server not ready")
|
| 377 |
with stream_lock:
|
| 378 |
-
if stream_running and current_player_sid is not None:
|
| 379 |
-
raise RuntimeError(f"Another player is already using the stream (session: {current_player_sid[:8]}...)")
|
| 380 |
stop_stream()
|
| 381 |
-
current_player_sid = player_sid
|
| 382 |
target_fps = int(fps)
|
| 383 |
frame_index = 0
|
| 384 |
_reset_cache_fresh()
|
|
@@ -388,16 +384,12 @@ def start_stream(n_steps=8, cfg=0.0, fps=30, clamp=True, player_sid=None):
|
|
| 388 |
stream_thread.start()
|
| 389 |
|
| 390 |
def stop_stream():
|
| 391 |
-
global stream_thread, stream_running
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
stream_running = False
|
| 398 |
-
current_player_sid = None
|
| 399 |
-
# Notify all clients that the stream is available
|
| 400 |
-
socketio.emit('stream_available', {'status': 'available'})
|
| 401 |
|
| 402 |
@socketio.on_error_default
|
| 403 |
def default_error_handler(e):
|
|
@@ -411,15 +403,20 @@ def handle_connect():
|
|
| 411 |
sid = request.sid
|
| 412 |
print(f'Client connected: {sid}')
|
| 413 |
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
| 415 |
emit('server_status', {
|
| 416 |
'ready': server_ready,
|
| 417 |
-
'busy':
|
|
|
|
| 418 |
})
|
| 419 |
emit('connected', {
|
| 420 |
'status': 'connected',
|
| 421 |
'model_loaded': model is not None,
|
| 422 |
-
'ready': server_ready
|
|
|
|
| 423 |
})
|
| 424 |
except Exception as e:
|
| 425 |
print(f"Error in handle_connect: {e}")
|
|
@@ -428,50 +425,73 @@ def handle_connect():
|
|
| 428 |
|
| 429 |
@socketio.on('disconnect')
|
| 430 |
def handle_disconnect(*args):
|
|
|
|
| 431 |
sid = request.sid
|
| 432 |
print(f'Client disconnected: {sid}')
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
@socketio.on('start_stream')
|
| 440 |
def handle_start_stream(data):
|
|
|
|
| 441 |
try:
|
| 442 |
sid = request.sid
|
|
|
|
| 443 |
if not server_ready:
|
| 444 |
# Tell client to keep showing spinner
|
| 445 |
-
emit('server_status', {'ready': server_ready
|
| 446 |
return
|
| 447 |
|
| 448 |
-
# Check if
|
| 449 |
-
with
|
| 450 |
-
if
|
| 451 |
-
emit('error', {'message': '
|
| 452 |
-
emit('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
n_steps = int(data.get('n_steps', 8))
|
| 456 |
cfg = float(data.get('cfg', 0))
|
| 457 |
fps = int(data.get('fps', 30))
|
| 458 |
clamp = bool(data.get('clamp', True))
|
| 459 |
-
print(f"Starting stream @ {fps} FPS (n_steps={n_steps}, cfg={cfg}, clamp={clamp})
|
| 460 |
try:
|
| 461 |
-
start_stream(n_steps=n_steps, cfg=cfg, fps=fps, clamp=clamp
|
| 462 |
emit('stream_started', {'status': 'ok'})
|
| 463 |
-
# Notify other clients that stream is now busy
|
| 464 |
-
socketio.emit('stream_busy', {'current_player': sid[:8]}, include_self=False)
|
| 465 |
-
except RuntimeError as e:
|
| 466 |
-
if "already using" in str(e):
|
| 467 |
-
emit('error', {'message': str(e)})
|
| 468 |
-
emit('stream_busy', {'current_player': current_player_sid[:8] if current_player_sid else 'unknown'})
|
| 469 |
-
else:
|
| 470 |
-
raise
|
| 471 |
except Exception as e:
|
| 472 |
print(f"Error starting stream: {e}")
|
| 473 |
import traceback
|
| 474 |
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
emit('error', {'message': str(e)})
|
| 476 |
except Exception as e:
|
| 477 |
print(f"Error in handle_start_stream: {e}")
|
|
@@ -481,34 +501,43 @@ def handle_start_stream(data):
|
|
| 481 |
|
| 482 |
@socketio.on('action')
|
| 483 |
def handle_action(data):
|
| 484 |
-
global latest_action
|
| 485 |
sid = request.sid
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
with stream_lock:
|
| 488 |
-
if current_player_sid != sid:
|
| 489 |
-
emit('error', {'message': 'You are not the current player. Please wait your turn.'})
|
| 490 |
-
return
|
| 491 |
-
action = int(data.get('action', 1))
|
| 492 |
latest_action = action
|
| 493 |
emit('action_ack', {'received': action, 'will_apply_to_frame_index': frame_index})
|
| 494 |
|
| 495 |
@socketio.on('stop_stream')
|
| 496 |
def handle_stop_stream():
|
|
|
|
| 497 |
sid = request.sid
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
# --------------------------
|
| 514 |
# Entrypoint
|
|
|
|
| 4 |
Broadcasts readiness via Socket.IO so the frontend can auto-hide a loading overlay once the model is ready.
|
| 5 |
"""
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import sys
|
| 8 |
import os
|
| 9 |
import time
|
|
|
|
| 38 |
# --------------------------
|
| 39 |
app = Flask(__name__, static_folder='static')
|
| 40 |
CORS(app)
|
| 41 |
+
# Configure SocketIO - use threading mode (no eventlet needed)
|
| 42 |
socketio = SocketIO(
|
| 43 |
app,
|
| 44 |
cors_allowed_origins="*",
|
| 45 |
+
async_mode='threading',
|
| 46 |
logger=False,
|
| 47 |
engineio_logger=False,
|
| 48 |
ping_timeout=60,
|
|
|
|
| 59 |
|
| 60 |
server_ready = False # <--- readiness flag
|
| 61 |
|
| 62 |
+
# Single-user limitation
|
| 63 |
+
active_user_sid = None # Session ID of the active user
|
| 64 |
+
user_lock = threading.Lock() # Protects active_user_sid
|
| 65 |
+
|
| 66 |
stream_lock = threading.Lock()
|
| 67 |
stream_thread = None
|
| 68 |
stream_running = False
|
|
|
|
| 69 |
latest_action = 1 # 0=init, 1=nothing, 2=up, 3=down
|
| 70 |
target_fps = 30
|
| 71 |
frame_index = 0
|
|
|
|
| 147 |
|
| 148 |
def _broadcast_ready():
|
| 149 |
"""Tell all clients whether the server is ready."""
|
| 150 |
+
socketio.emit('server_status', {'ready': server_ready})
|
| 151 |
|
| 152 |
# --------------------------
|
| 153 |
# Model init (pure eager) & warmup
|
|
|
|
| 368 |
# --------------------------
|
| 369 |
# Socket events & helpers
|
| 370 |
# --------------------------
|
| 371 |
+
def start_stream(n_steps=8, cfg=0.0, fps=30, clamp=True):
|
| 372 |
+
global stream_thread, stream_running, frame_index, target_fps, latest_action
|
| 373 |
if not server_ready:
|
| 374 |
_broadcast_ready()
|
| 375 |
raise RuntimeError("Server not ready")
|
| 376 |
with stream_lock:
|
|
|
|
|
|
|
| 377 |
stop_stream()
|
|
|
|
| 378 |
target_fps = int(fps)
|
| 379 |
frame_index = 0
|
| 380 |
_reset_cache_fresh()
|
|
|
|
| 384 |
stream_thread.start()
|
| 385 |
|
| 386 |
def stop_stream():
|
| 387 |
+
global stream_thread, stream_running
|
| 388 |
+
if stream_thread is not None:
|
| 389 |
+
stream_thread.stop()
|
| 390 |
+
stream_thread.join(timeout=1.0)
|
| 391 |
+
stream_thread = None
|
| 392 |
+
stream_running = False
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
@socketio.on_error_default
|
| 395 |
def default_error_handler(e):
|
|
|
|
| 403 |
sid = request.sid
|
| 404 |
print(f'Client connected: {sid}')
|
| 405 |
|
| 406 |
+
with user_lock:
|
| 407 |
+
is_busy = active_user_sid is not None and active_user_sid != sid
|
| 408 |
+
|
| 409 |
+
# Immediately tell the new client current readiness and availability
|
| 410 |
emit('server_status', {
|
| 411 |
'ready': server_ready,
|
| 412 |
+
'busy': is_busy,
|
| 413 |
+
'is_active_user': not is_busy
|
| 414 |
})
|
| 415 |
emit('connected', {
|
| 416 |
'status': 'connected',
|
| 417 |
'model_loaded': model is not None,
|
| 418 |
+
'ready': server_ready,
|
| 419 |
+
'busy': is_busy
|
| 420 |
})
|
| 421 |
except Exception as e:
|
| 422 |
print(f"Error in handle_connect: {e}")
|
|
|
|
| 425 |
|
| 426 |
@socketio.on('disconnect')
|
| 427 |
def handle_disconnect(*args):
|
| 428 |
+
global active_user_sid
|
| 429 |
sid = request.sid
|
| 430 |
print(f'Client disconnected: {sid}')
|
| 431 |
+
|
| 432 |
+
# Release the active user slot if this was the active user
|
| 433 |
+
with user_lock:
|
| 434 |
+
if active_user_sid == sid:
|
| 435 |
+
print(f'Active user {sid} disconnected, freeing slot')
|
| 436 |
+
active_user_sid = None
|
| 437 |
+
# Notify all other clients that server is now available
|
| 438 |
+
socketio.emit('server_status', {
|
| 439 |
+
'ready': server_ready,
|
| 440 |
+
'busy': False,
|
| 441 |
+
'is_active_user': False
|
| 442 |
+
})
|
| 443 |
+
|
| 444 |
+
stop_stream()
|
| 445 |
|
| 446 |
@socketio.on('start_stream')
|
| 447 |
def handle_start_stream(data):
|
| 448 |
+
global active_user_sid
|
| 449 |
try:
|
| 450 |
sid = request.sid
|
| 451 |
+
|
| 452 |
if not server_ready:
|
| 453 |
# Tell client to keep showing spinner
|
| 454 |
+
emit('server_status', {'ready': server_ready})
|
| 455 |
return
|
| 456 |
|
| 457 |
+
# Check if server is busy with another user
|
| 458 |
+
with user_lock:
|
| 459 |
+
if active_user_sid is not None and active_user_sid != sid:
|
| 460 |
+
emit('error', {'message': 'Server is currently being used by another user. Please wait.'})
|
| 461 |
+
emit('server_status', {
|
| 462 |
+
'ready': server_ready,
|
| 463 |
+
'busy': True,
|
| 464 |
+
'is_active_user': False
|
| 465 |
+
})
|
| 466 |
+
emit('stream_busy', {'current_player': active_user_sid[:8] if active_user_sid else 'unknown'})
|
| 467 |
return
|
| 468 |
+
# Claim the active user slot
|
| 469 |
+
active_user_sid = sid
|
| 470 |
+
print(f'User {sid} claimed active slot')
|
| 471 |
+
|
| 472 |
+
# Notify all clients about the new busy state
|
| 473 |
+
socketio.emit('server_status', {
|
| 474 |
+
'ready': server_ready,
|
| 475 |
+
'busy': True,
|
| 476 |
+
'is_active_user': False
|
| 477 |
+
}, include_self=False)
|
| 478 |
|
| 479 |
n_steps = int(data.get('n_steps', 8))
|
| 480 |
cfg = float(data.get('cfg', 0))
|
| 481 |
fps = int(data.get('fps', 30))
|
| 482 |
clamp = bool(data.get('clamp', True))
|
| 483 |
+
print(f"Starting stream @ {fps} FPS (n_steps={n_steps}, cfg={cfg}, clamp={clamp})")
|
| 484 |
try:
|
| 485 |
+
start_stream(n_steps=n_steps, cfg=cfg, fps=fps, clamp=clamp)
|
| 486 |
emit('stream_started', {'status': 'ok'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
except Exception as e:
|
| 488 |
print(f"Error starting stream: {e}")
|
| 489 |
import traceback
|
| 490 |
traceback.print_exc()
|
| 491 |
+
# Release the slot on error
|
| 492 |
+
with user_lock:
|
| 493 |
+
if active_user_sid == sid:
|
| 494 |
+
active_user_sid = None
|
| 495 |
emit('error', {'message': str(e)})
|
| 496 |
except Exception as e:
|
| 497 |
print(f"Error in handle_start_stream: {e}")
|
|
|
|
| 501 |
|
| 502 |
@socketio.on('action')
|
| 503 |
def handle_action(data):
|
| 504 |
+
global latest_action
|
| 505 |
sid = request.sid
|
| 506 |
+
|
| 507 |
+
# Only accept actions from the active user
|
| 508 |
+
with user_lock:
|
| 509 |
+
if active_user_sid != sid:
|
| 510 |
+
return # Silently ignore actions from non-active users
|
| 511 |
+
|
| 512 |
+
action = int(data.get('action', 1))
|
| 513 |
with stream_lock:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
latest_action = action
|
| 515 |
emit('action_ack', {'received': action, 'will_apply_to_frame_index': frame_index})
|
| 516 |
|
| 517 |
@socketio.on('stop_stream')
|
| 518 |
def handle_stop_stream():
|
| 519 |
+
global active_user_sid
|
| 520 |
sid = request.sid
|
| 521 |
+
|
| 522 |
+
# Only the active user can stop the stream
|
| 523 |
+
with user_lock:
|
| 524 |
+
if active_user_sid != sid:
|
| 525 |
+
return # Silently ignore stop requests from non-active users
|
| 526 |
+
# Release the active user slot
|
| 527 |
+
print(f'User {sid} stopped stream and released slot')
|
| 528 |
+
active_user_sid = None
|
| 529 |
+
|
| 530 |
+
# Notify all clients that server is now available
|
| 531 |
+
socketio.emit('server_status', {
|
| 532 |
+
'ready': server_ready,
|
| 533 |
+
'busy': False,
|
| 534 |
+
'is_active_user': False
|
| 535 |
+
})
|
| 536 |
+
socketio.emit('stream_available', {'status': 'available'})
|
| 537 |
+
|
| 538 |
+
print('Stopping stream')
|
| 539 |
+
stop_stream()
|
| 540 |
+
emit('stream_stopped', {'status': 'ok'})
|
| 541 |
|
| 542 |
# --------------------------
|
| 543 |
# Entrypoint
|