Switch to eventlet as handler, UI option to enable/disable

socketio-tweaks
dgtlmoon 2025-05-28 11:13:47 +02:00
rodzic 46f78f0164
commit b74eaca83f
6 zmienionych plików z 56 dodań i 117 usunięć

Wyświetl plik

@ -7,13 +7,15 @@ __version__ = '0.49.18'
from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError
import os
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
import eventlet
eventlet.monkey_patch()
import getopt
import platform
import signal
import socket
import sys
from werkzeug.serving import run_simple
import sys
from changedetectionio import store
from changedetectionio.flask_app import changedetection_app
from loguru import logger
@ -52,9 +54,9 @@ def main():
datastore_path = None
do_cleanup = False
host = ''
host = "0.0.0.0"
ipv6_enabled = False
port = os.environ.get('PORT') or 5000
port = int(os.environ.get('PORT', 5000))
ssl_mode = False
# On Windows, create and use a default path.
@ -150,6 +152,11 @@ def main():
app = changedetection_app(app_config, datastore)
# Get the SocketIO instance from the Flask app (created in flask_app.py)
from changedetectionio.flask_app import socketio_server
global socketio
socketio = socketio_server
signal.signal(signal.SIGTERM, sigshutdown_handler)
signal.signal(signal.SIGINT, sigshutdown_handler)
@ -201,87 +208,12 @@ def main():
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_prefix=1, x_host=1)
s_type = socket.AF_INET6 if ipv6_enabled else socket.AF_INET
# Get socketio_server from flask_app
from changedetectionio.flask_app import socketio_server
# SocketIO instance is already initialized in flask_app.py
if socketio_server and datastore.data['settings']['application']['ui'].get('open_diff_in_new_tab'):
logger.info("Starting server with Socket.IO support (using threading)...")
# Use Flask-SocketIO's run method with error handling for Werkzeug warning
# This is the cleanest approach that works with all Flask-SocketIO versions
# Use '0.0.0.0' as the default host if none is specified
# This will listen on all available interfaces
listen_host = '0.0.0.0' if host == '' else host
logger.info(f"Using host: {listen_host} and port: {port}")
try:
# First try with the allow_unsafe_werkzeug parameter (newer versions)
if ssl_mode:
socketio_server.run(
app,
host=listen_host,
port=int(port),
certfile='cert.pem',
keyfile='privkey.pem',
debug=False,
use_reloader=False,
allow_unsafe_werkzeug=True # Only in newer versions
)
else:
socketio_server.run(
app,
host=listen_host,
port=int(port),
debug=False,
use_reloader=False,
allow_unsafe_werkzeug=True # Only in newer versions
)
except TypeError:
# If allow_unsafe_werkzeug is not a valid parameter, try without it
logger.info("Falling back to basic run method without allow_unsafe_werkzeug")
# Override the werkzeug safety check by setting an environment variable
os.environ['WERKZEUG_RUN_MAIN'] = 'true'
if ssl_mode:
socketio_server.run(
app,
host=listen_host,
port=int(port),
certfile='cert.pem',
keyfile='privkey.pem',
debug=False,
use_reloader=False
)
else:
socketio_server.run(
app,
host=listen_host,
port=int(port),
debug=False,
use_reloader=False
)
# Launch using eventlet SocketIO run method for proper integration
if ssl_mode:
socketio.run(app, host=host, port=int(port), debug=False,
certfile='cert.pem', keyfile='privkey.pem')
else:
logger.warning("Socket.IO server not initialized, falling back to standard WSGI server")
# Fallback to standard WSGI server if socketio_server is not available
listen_host = '0.0.0.0' if host == '' else host
if ssl_mode:
# Use Werkzeug's run_simple with SSL support
run_simple(
hostname=listen_host,
port=int(port),
application=app,
use_reloader=False,
use_debugger=False,
ssl_context=('cert.pem', 'privkey.pem')
)
else:
# Use Werkzeug's run_simple for standard HTTP
run_simple(
hostname=listen_host,
port=int(port),
application=app,
use_reloader=False,
use_debugger=False
)
socketio.run(app, host=host, port=int(port), debug=False)

Wyświetl plik

@ -247,9 +247,9 @@ nav
<span class="pure-form-message-inline">Enable this setting to open the diff page in a new tab. If disabled, the diff page will open in the current tab.</span>
</div>
<div class="pure-control-group">
<span class="pure-form-message-inline">Enable realtime updates in the UI</span>
{{ render_checkbox_field(form.application.form.ui.form.socket_io_enabled, class="socket_io_enabled") }}
<span class="pure-form-message-inline">Realtime UI Updates Enabled - (Restart required if this is changed)</span>
</div>
</div>
<div class="tab-pane-inner" id="proxies">
<div id="recommended-proxy">

Wyświetl plik

@ -733,6 +733,7 @@ class globalSettingsRequestForm(Form):
class globalSettingsApplicationUIForm(Form):
open_diff_in_new_tab = BooleanField('Open diff page in a new tab', default=True, validators=[validators.Optional()])
socket_io_enabled = BooleanField('Realtime UI Updates Enabled', default=True, validators=[validators.Optional()])
# datastore.data['settings']['application']..
class globalSettingsApplicationForm(commonSettingsForm):

Wyświetl plik

@ -62,6 +62,7 @@ class model(dict):
'timezone': None, # Default IANA timezone name
'ui': {
'open_diff_in_new_tab': True,
'socket_io_enabled': True
},
}
}

Wyświetl plik

@ -25,10 +25,10 @@ class SignalHandler:
logger.info("SignalHandler: Connected to queue_length signal")
# Create and start the queue update thread using gevent
import gevent
logger.info("Using gevent for polling thread")
self.polling_emitter_thread = gevent.spawn(self.polling_emit_running_or_queued_watches)
# Create and start the queue update thread using eventlet
import eventlet
logger.info("Using eventlet for polling thread")
self.polling_emitter_thread = eventlet.spawn(self.polling_emit_running_or_queued_watches)
# Store the thread reference in socketio for clean shutdown
self.socketio_instance.polling_emitter_thread = self.polling_emitter_thread
@ -76,20 +76,20 @@ class SignalHandler:
"""Greenlet that periodically updates the browser/frontend with current state of who is being checked or queued
This is because sometimes the browser page could reload (like on clicking on a link) but the data is old
"""
logger.info("Queue update greenlet started")
logger.info("Queue update eventlet greenlet started")
# Import the watch_check_update signal, update_q, and running_update_threads here to avoid circular imports
from changedetectionio.flask_app import app, running_update_threads
watch_check_update = signal('watch_check_update')
# Use gevent sleep for non-blocking operation
from gevent import sleep as gevent_sleep
# Use eventlet sleep for non-blocking operation
from eventlet import sleep as eventlet_sleep
# Get the stop event from the socketio instance
stop_event = self.socketio_instance.stop_event if hasattr(self.socketio_instance, 'stop_event') else None
# Run until explicitly stopped
while stop_event is None or not stop_event.is_set():
while stop_event is None or not stop_event.ready():
try:
# For each item in the queue, send a signal, so we update the UI
for t in running_update_threads:
@ -98,22 +98,22 @@ class SignalHandler:
# Send with app_context to ensure proper URL generation
with app.app_context():
watch_check_update.send(app_context=app, watch_uuid=t.current_uuid)
# Yield control back to gevent after each send to prevent blocking
gevent_sleep(0.1) # Small sleep to yield control
# Yield control back to eventlet after each send to prevent blocking
eventlet_sleep(0.1) # Small sleep to yield control
# Check if we need to stop in the middle of processing
if stop_event is not None and stop_event.is_set():
if stop_event is not None and stop_event.ready():
break
# Sleep between polling/update cycles
gevent_sleep(2)
eventlet_sleep(2)
except Exception as e:
logger.error(f"Error in queue update greenlet: {str(e)}")
# Sleep a bit to avoid flooding logs in case of persistent error
gevent_sleep(0.5)
eventlet_sleep(0.5)
logger.info("Queue update greenlet stopped")
logger.info("Queue update eventlet greenlet stopped")
def handle_watch_update(socketio, **kwargs):
@ -185,10 +185,9 @@ def handle_watch_update(socketio, **kwargs):
def init_socketio(app, datastore):
"""Initialize SocketIO with the main Flask app"""
# Use the threading async_mode instead of eventlet
# This avoids the need for monkey patching eventlet,
# Which leads to problems with async playwright etc
async_mode = 'gevent'
# Use eventlet async_mode to match the eventlet server
# This is required since the main app uses eventlet.wsgi.server
async_mode = 'eventlet'
logger.info(f"Using {async_mode} mode for Socket.IO")
# Restrict SocketIO CORS to same origin by default, can be overridden with env var
@ -201,19 +200,22 @@ def init_socketio(app, datastore):
engineio_logger=strtobool(os.getenv('SOCKETIO_LOGGING', 'False')))
# Set up event handlers
logger.info("Socket.IO: Registering connect event handler")
@socketio.on('connect')
def handle_connect():
"""Handle client connection"""
from changedetectionio.auth_decorator import login_optionally_required
logger.info("Socket.IO: CONNECT HANDLER CALLED - Starting connection process")
from flask import request
from flask_login import current_user
from changedetectionio.flask_app import update_q
# Access datastore from socketio
datastore = socketio.datastore
logger.info(f"Socket.IO: Current user authenticated: {current_user.is_authenticated if hasattr(current_user, 'is_authenticated') else 'No current_user'}")
# Check if authentication is required and user is not authenticated
has_password_enabled = datastore.data['settings']['application'].get('password') or os.getenv("SALTED_PASS", False)
logger.info(f"Socket.IO: Password enabled: {has_password_enabled}")
if has_password_enabled and not current_user.is_authenticated:
logger.warning("Socket.IO: Rejecting unauthenticated connection")
return False # Reject the connection
@ -231,6 +233,7 @@ def init_socketio(app, datastore):
logger.info("Socket.IO: Client connected")
logger.info("Socket.IO: Registering disconnect event handler")
@socketio.on('disconnect')
def handle_disconnect():
"""Handle client disconnection"""
@ -242,9 +245,9 @@ def init_socketio(app, datastore):
# Store the datastore reference on the socketio object for later use
socketio.datastore = datastore
# Create a stop event for our queue update thread using gevent Event
import gevent.event
stop_event = gevent.event.Event()
# Create a stop event for our queue update thread using eventlet Event
import eventlet.event
stop_event = eventlet.event.Event()
socketio.stop_event = stop_event
@ -256,18 +259,20 @@ def init_socketio(app, datastore):
# Signal the queue update thread to stop
if hasattr(socketio, 'stop_event'):
socketio.stop_event.set()
socketio.stop_event.send()
logger.info("Socket.IO: Signaled queue update thread to stop")
# Wait for the greenlet to exit (with timeout)
if hasattr(socketio, 'polling_emitter_thread'):
try:
# For gevent greenlets
socketio.polling_emitter_thread.join(timeout=5)
logger.info("Socket.IO: Queue update greenlet joined successfully")
# For eventlet greenlets
eventlet.with_timeout(5, socketio.polling_emitter_thread.wait)
logger.info("Socket.IO: Queue update eventlet greenlet joined successfully")
except eventlet.Timeout:
logger.info("Socket.IO: Queue update eventlet greenlet did not exit in time")
socketio.polling_emitter_thread.kill()
except Exception as e:
logger.error(f"Error joining greenlet: {str(e)}")
logger.info("Socket.IO: Queue update greenlet did not exit in time")
logger.error(f"Error joining eventlet greenlet: {str(e)}")
# Close any remaining client connections
#if hasattr(socketio, 'server'):
@ -280,4 +285,5 @@ def init_socketio(app, datastore):
socketio.shutdown = shutdown
logger.info("Socket.IO initialized and attached to main Flask app")
logger.info(f"Socket.IO: Registered event handlers: {socketio.handlers if hasattr(socketio, 'handlers') else 'No handlers found'}")
return socketio

Wyświetl plik

@ -35,7 +35,6 @@
<script src="{{url_for('static_content', group='js', filename='csrf.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='socket.io.min.js')}}"></script>
<script src="{{url_for('static_content', group='js', filename='realtime.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='timeago-init.js')}}" defer></script>
</head>
<body class="">