kopia lustrzana https://github.com/dgtlmoon/changedetection.io
Switch to eventlet as handler, UI option to enable/disable
rodzic
46f78f0164
commit
b74eaca83f
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class model(dict):
|
|||
'timezone': None, # Default IANA timezone name
|
||||
'ui': {
|
||||
'open_diff_in_new_tab': True,
|
||||
'socket_io_enabled': True
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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="">
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue