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 changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
import os import os
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
import eventlet
eventlet.monkey_patch()
import getopt import getopt
import platform import platform
import signal import signal
import socket
import sys
from werkzeug.serving import run_simple
import sys
from changedetectionio import store from changedetectionio import store
from changedetectionio.flask_app import changedetection_app from changedetectionio.flask_app import changedetection_app
from loguru import logger from loguru import logger
@ -52,9 +54,9 @@ def main():
datastore_path = None datastore_path = None
do_cleanup = False do_cleanup = False
host = '' host = "0.0.0.0"
ipv6_enabled = False ipv6_enabled = False
port = os.environ.get('PORT') or 5000 port = int(os.environ.get('PORT', 5000))
ssl_mode = False ssl_mode = False
# On Windows, create and use a default path. # On Windows, create and use a default path.
@ -150,6 +152,11 @@ def main():
app = changedetection_app(app_config, datastore) 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.SIGTERM, sigshutdown_handler)
signal.signal(signal.SIGINT, sigshutdown_handler) signal.signal(signal.SIGINT, sigshutdown_handler)
@ -201,87 +208,12 @@ def main():
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_prefix=1, x_host=1) 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 # SocketIO instance is already initialized in flask_app.py
from changedetectionio.flask_app import socketio_server
if socketio_server and datastore.data['settings']['application']['ui'].get('open_diff_in_new_tab'): # Launch using eventlet SocketIO run method for proper integration
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: if ssl_mode:
socketio_server.run( socketio.run(app, host=host, port=int(port), debug=False,
app, certfile='cert.pem', keyfile='privkey.pem')
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: else:
socketio_server.run( socketio.run(app, host=host, port=int(port), debug=False)
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
)
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
)

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> <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>
<div class="pure-control-group"> <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> </div>
<div class="tab-pane-inner" id="proxies"> <div class="tab-pane-inner" id="proxies">
<div id="recommended-proxy"> <div id="recommended-proxy">

Wyświetl plik

@ -733,6 +733,7 @@ class globalSettingsRequestForm(Form):
class globalSettingsApplicationUIForm(Form): class globalSettingsApplicationUIForm(Form):
open_diff_in_new_tab = BooleanField('Open diff page in a new tab', default=True, validators=[validators.Optional()]) 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'].. # datastore.data['settings']['application']..
class globalSettingsApplicationForm(commonSettingsForm): class globalSettingsApplicationForm(commonSettingsForm):

Wyświetl plik

@ -62,6 +62,7 @@ class model(dict):
'timezone': None, # Default IANA timezone name 'timezone': None, # Default IANA timezone name
'ui': { 'ui': {
'open_diff_in_new_tab': True, '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") logger.info("SignalHandler: Connected to queue_length signal")
# Create and start the queue update thread using gevent # Create and start the queue update thread using eventlet
import gevent import eventlet
logger.info("Using gevent for polling thread") logger.info("Using eventlet for polling thread")
self.polling_emitter_thread = gevent.spawn(self.polling_emit_running_or_queued_watches) self.polling_emitter_thread = eventlet.spawn(self.polling_emit_running_or_queued_watches)
# Store the thread reference in socketio for clean shutdown # Store the thread reference in socketio for clean shutdown
self.socketio_instance.polling_emitter_thread = self.polling_emitter_thread 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 """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 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 # 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 from changedetectionio.flask_app import app, running_update_threads
watch_check_update = signal('watch_check_update') watch_check_update = signal('watch_check_update')
# Use gevent sleep for non-blocking operation # Use eventlet sleep for non-blocking operation
from gevent import sleep as gevent_sleep from eventlet import sleep as eventlet_sleep
# Get the stop event from the socketio instance # Get the stop event from the socketio instance
stop_event = self.socketio_instance.stop_event if hasattr(self.socketio_instance, 'stop_event') else None stop_event = self.socketio_instance.stop_event if hasattr(self.socketio_instance, 'stop_event') else None
# Run until explicitly stopped # 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: try:
# For each item in the queue, send a signal, so we update the UI # For each item in the queue, send a signal, so we update the UI
for t in running_update_threads: for t in running_update_threads:
@ -98,22 +98,22 @@ class SignalHandler:
# Send with app_context to ensure proper URL generation # Send with app_context to ensure proper URL generation
with app.app_context(): with app.app_context():
watch_check_update.send(app_context=app, watch_uuid=t.current_uuid) watch_check_update.send(app_context=app, watch_uuid=t.current_uuid)
# Yield control back to gevent after each send to prevent blocking # Yield control back to eventlet after each send to prevent blocking
gevent_sleep(0.1) # Small sleep to yield control eventlet_sleep(0.1) # Small sleep to yield control
# Check if we need to stop in the middle of processing # 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 break
# Sleep between polling/update cycles # Sleep between polling/update cycles
gevent_sleep(2) eventlet_sleep(2)
except Exception as e: except Exception as e:
logger.error(f"Error in queue update greenlet: {str(e)}") logger.error(f"Error in queue update greenlet: {str(e)}")
# Sleep a bit to avoid flooding logs in case of persistent error # 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): def handle_watch_update(socketio, **kwargs):
@ -185,10 +185,9 @@ def handle_watch_update(socketio, **kwargs):
def init_socketio(app, datastore): def init_socketio(app, datastore):
"""Initialize SocketIO with the main Flask app""" """Initialize SocketIO with the main Flask app"""
# Use the threading async_mode instead of eventlet # Use eventlet async_mode to match the eventlet server
# This avoids the need for monkey patching eventlet, # This is required since the main app uses eventlet.wsgi.server
# Which leads to problems with async playwright etc async_mode = 'eventlet'
async_mode = 'gevent'
logger.info(f"Using {async_mode} mode for Socket.IO") logger.info(f"Using {async_mode} mode for Socket.IO")
# Restrict SocketIO CORS to same origin by default, can be overridden with env var # 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'))) engineio_logger=strtobool(os.getenv('SOCKETIO_LOGGING', 'False')))
# Set up event handlers # Set up event handlers
logger.info("Socket.IO: Registering connect event handler")
@socketio.on('connect') @socketio.on('connect')
def handle_connect(): def handle_connect():
"""Handle client connection""" """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 import request
from flask_login import current_user from flask_login import current_user
from changedetectionio.flask_app import update_q from changedetectionio.flask_app import update_q
# Access datastore from socketio # Access datastore from socketio
datastore = socketio.datastore 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 # 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) 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: if has_password_enabled and not current_user.is_authenticated:
logger.warning("Socket.IO: Rejecting unauthenticated connection") logger.warning("Socket.IO: Rejecting unauthenticated connection")
return False # Reject the 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: Client connected")
logger.info("Socket.IO: Registering disconnect event handler")
@socketio.on('disconnect') @socketio.on('disconnect')
def handle_disconnect(): def handle_disconnect():
"""Handle client disconnection""" """Handle client disconnection"""
@ -242,9 +245,9 @@ def init_socketio(app, datastore):
# Store the datastore reference on the socketio object for later use # Store the datastore reference on the socketio object for later use
socketio.datastore = datastore socketio.datastore = datastore
# Create a stop event for our queue update thread using gevent Event # Create a stop event for our queue update thread using eventlet Event
import gevent.event import eventlet.event
stop_event = gevent.event.Event() stop_event = eventlet.event.Event()
socketio.stop_event = stop_event socketio.stop_event = stop_event
@ -256,18 +259,20 @@ def init_socketio(app, datastore):
# Signal the queue update thread to stop # Signal the queue update thread to stop
if hasattr(socketio, 'stop_event'): if hasattr(socketio, 'stop_event'):
socketio.stop_event.set() socketio.stop_event.send()
logger.info("Socket.IO: Signaled queue update thread to stop") logger.info("Socket.IO: Signaled queue update thread to stop")
# Wait for the greenlet to exit (with timeout) # Wait for the greenlet to exit (with timeout)
if hasattr(socketio, 'polling_emitter_thread'): if hasattr(socketio, 'polling_emitter_thread'):
try: try:
# For gevent greenlets # For eventlet greenlets
socketio.polling_emitter_thread.join(timeout=5) eventlet.with_timeout(5, socketio.polling_emitter_thread.wait)
logger.info("Socket.IO: Queue update greenlet joined successfully") 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: except Exception as e:
logger.error(f"Error joining greenlet: {str(e)}") logger.error(f"Error joining eventlet greenlet: {str(e)}")
logger.info("Socket.IO: Queue update greenlet did not exit in time")
# Close any remaining client connections # Close any remaining client connections
#if hasattr(socketio, 'server'): #if hasattr(socketio, 'server'):
@ -280,4 +285,5 @@ def init_socketio(app, datastore):
socketio.shutdown = shutdown socketio.shutdown = shutdown
logger.info("Socket.IO initialized and attached to main Flask app") 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 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='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='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='realtime.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='timeago-init.js')}}" defer></script>
</head> </head>
<body class=""> <body class="">