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 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)...") |     if ssl_mode: | ||||||
| 
 |         socketio.run(app, host=host, port=int(port), debug=False,  | ||||||
|         # Use Flask-SocketIO's run method with error handling for Werkzeug warning |                     certfile='cert.pem', keyfile='privkey.pem') | ||||||
|         # 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 |  | ||||||
|                 ) |  | ||||||
|     else: |     else: | ||||||
|         logger.warning("Socket.IO server not initialized, falling back to standard WSGI server") |         socketio.run(app, host=host, port=int(port), debug=False) | ||||||
|         # 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 |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -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"> | ||||||
|  |  | ||||||
|  | @ -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): | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|                     }, |                     }, | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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=""> | ||||||
|  |  | ||||||
		Ładowanie…
	
		Reference in New Issue
	
	 dgtlmoon
						dgtlmoon