diff --git a/docs/admin/settings/settings_general.rst b/docs/admin/settings/settings_general.rst index 75acb4f6d..cabdedbcf 100644 --- a/docs/admin/settings/settings_general.rst +++ b/docs/admin/settings/settings_general.rst @@ -16,8 +16,14 @@ open_metrics: '' ``debug`` : ``$SEARXNG_DEBUG`` - Allow a more detailed log if you run SearXNG directly. Display *detailed* error - messages in the browser too, so this must be deactivated in production. + In debug mode, the server provides an interactive debugger, will reload when + code is changed and activates a verbose logging. + + .. attention:: + + The debug setting is intended for local development server. Don't + activate debug (don't use a development server) when deploying to + production. ``donation_url`` : Set value to ``true`` to use your own donation page written in the diff --git a/searx/__init__.py b/searx/__init__.py index 933c49c20..ee77a523e 100644 --- a/searx/__init__.py +++ b/searx/__init__.py @@ -22,18 +22,18 @@ searx_dir = abspath(dirname(__file__)) searx_parent_dir = abspath(dirname(dirname(__file__))) settings = {} -searx_debug = False +sxng_debug = False logger = logging.getLogger('searx') _unset = object() def init_settings(): - """Initialize global ``settings`` and ``searx_debug`` variables and + """Initialize global ``settings`` and ``sxng_debug`` variables and ``logger`` from ``SEARXNG_SETTINGS_PATH``. """ - global settings, searx_debug # pylint: disable=global-variable-not-assigned + global settings, sxng_debug # pylint: disable=global-variable-not-assigned cfg, msg = searx.settings_loader.load_settings(load_user_settings=True) cfg = cfg or {} @@ -42,8 +42,8 @@ def init_settings(): settings.clear() settings.update(cfg) - searx_debug = settings['general']['debug'] - if searx_debug: + sxng_debug = get_setting("general.debug") + if sxng_debug: _logging_config_debug() else: logging.basicConfig(level=LOG_LEVEL_PROD, format=LOG_FORMAT_PROD) diff --git a/searx/network/network.py b/searx/network/network.py index 84aaebe34..178ebcbf2 100644 --- a/searx/network/network.py +++ b/searx/network/network.py @@ -12,7 +12,7 @@ from typing import Dict import httpx -from searx import logger, searx_debug +from searx import logger, sxng_debug from searx.extended_types import SXNG_Response from .client import new_client, get_loop, AsyncHTTPTransportNoHttp from .raise_for_httperror import raise_for_httperror @@ -186,7 +186,7 @@ class Network: local_address = next(self._local_addresses_cycle) proxies = next(self._proxies_cycle) # is a tuple so it can be part of the key key = (verify, max_redirects, local_address, proxies) - hook_log_response = self.log_response if searx_debug else None + hook_log_response = self.log_response if sxng_debug else None if key not in self._clients or self._clients[key].is_closed: client = new_client( self.enable_http, diff --git a/searx/search/checker/background.py b/searx/search/checker/background.py index 5e9d23e00..7333e6ad0 100644 --- a/searx/search/checker/background.py +++ b/searx/search/checker/background.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union import redis.exceptions -from searx import logger, settings, searx_debug +from searx import logger, settings, sxng_debug from searx.redisdb import client as get_redis_client from searx.exceptions import SearxSettingsException from searx.search.processors import PROCESSORS @@ -139,7 +139,7 @@ def initialize(): signal.signal(signal.SIGUSR1, _signal_handler) # special case when debug is activate - if searx_debug and settings['checker']['off_when_debug']: + if sxng_debug and settings['checker']['off_when_debug']: logger.info('debug mode: checker is disabled') return diff --git a/searx/webapp.py b/searx/webapp.py index 23c5d70cc..4ecaab7d2 100755 --- a/searx/webapp.py +++ b/searx/webapp.py @@ -6,6 +6,7 @@ # pylint: disable=use-dict-literal from __future__ import annotations +import inspect import hashlib import hmac import json @@ -29,6 +30,8 @@ from pygments import highlight from pygments.lexers import get_lexer_by_name from pygments.formatters import HtmlFormatter # pylint: disable=no-name-in-module +from werkzeug.serving import is_running_from_reloader + import flask from flask import ( @@ -48,12 +51,12 @@ from flask_babel import ( format_decimal, ) +import searx from searx.extended_types import sxng_request from searx import ( logger, get_setting, settings, - searx_debug, ) from searx import infopage @@ -81,7 +84,6 @@ from searx.webutils import ( exception_classname_to_text, new_hmac, is_hmac_of, - is_flask_run_cmdline, group_engines_in_tab, ) from searx.webadapter import ( @@ -128,11 +130,6 @@ logger = logger.getChild('webapp') warnings.simplefilter("always") -# check secret_key -if not searx_debug and settings['server']['secret_key'] == 'ultrasecretkey': - logger.error('server.secret_key is not changed. Please use something else instead of ultrasecretkey.') - sys.exit(1) - # about static logger.debug('static directory is %s', settings['ui']['static_path']) static_files = get_static_files(settings['ui']['static_path']) @@ -1329,45 +1326,108 @@ def page_not_found(_e): return render('404.html'), 404 -# see https://flask.palletsprojects.com/en/1.1.x/cli/ -# True if "FLASK_APP=searx/webapp.py FLASK_ENV=development flask run" -flask_run_development = ( - os.environ.get("FLASK_APP") is not None and os.environ.get("FLASK_ENV") == 'development' and is_flask_run_cmdline() -) +def run(): + """Runs the application on a local development server. -# True if reload feature is activated of werkzeug, False otherwise (including uwsgi, etc..) -# __name__ != "__main__" if searx.webapp is imported (make test, make docs, uwsgi...) -# see run() at the end of this file : searx_debug activates the reload feature. -werkzeug_reloader = flask_run_development or (searx_debug and __name__ == "__main__") + This run method is only called when SearXNG is started via ``__main__``:: + + python -m searx.webapp + + Do not use :ref:`run() ` in a production setting. It is + not intended to meet security and performance requirements for a production + server. + + It is not recommended to use this function for development with automatic + reloading as this is badly supported. Instead you should be using the flask + command line script’s run support:: + + flask --app searx.webapp run --debug --reload --host 127.0.0.1 --port 8888 + + .. _Flask.run: https://flask.palletsprojects.com/en/stable/api/#flask.Flask.run + """ + + host: str = get_setting("server.bind_address") # type: ignore + port: int = get_setting("server.port") # type: ignore + + if searx.sxng_debug: + logger.debug("run local development server (DEBUG) on %s:%s", host, port) + app.run( + debug=True, + port=port, + host=host, + threaded=True, + extra_files=[DEFAULT_SETTINGS_FILE], + ) + else: + logger.debug("run local development server on %s:%s", host, port) + app.run(port=port, host=host, threaded=True) + + +def is_werkzeug_reload_active() -> bool: + """Returns ``True`` if server is is launched by :ref:`werkzeug.serving` and + the ``use_reload`` argument was set to ``True``. If this is the case, it + should be avoided that the server is initialized twice (:py:obj:`init`, + :py:obj:`run`). + + .. _werkzeug.serving: + https://werkzeug.palletsprojects.com/en/stable/serving/#werkzeug.serving.run_simple + """ + + # https://github.com/searxng/searxng/pull/1656#issuecomment-1214198941 + # https://github.com/searxng/searxng/pull/1616#issuecomment-1206137468 + + frames = inspect.stack() + + if len(frames) > 1 and frames[-2].filename.endswith('flask/cli.py'): + # server was launched by "flask run", is argument "--reload" set? + if "--reload" in sys.argv or "--debug" in sys.argv: + return True + + elif frames[0].filename.endswith('searx/webapp.py'): + # server was launched by "python -m searx.webapp" / see run() + if searx.sxng_debug: + return True + + return False + + +def init(): + + if searx.sxng_debug or app.debug: + app.debug = True + searx.sxng_debug = True + + # check secret_key in production + + if not app.debug and get_setting("server.secret_key") == 'ultrasecretkey': + logger.error("server.secret_key is not changed. Please use something else instead of ultrasecretkey.") + sys.exit(1) + + # When automatic reloading is activated stop Flask from initialising twice. + # - https://github.com/pallets/flask/issues/5307#issuecomment-1774646119 + # - https://stackoverflow.com/a/25504196 + + reloader_active = is_werkzeug_reload_active() + werkzeug_run_main = is_running_from_reloader() + + if reloader_active and not werkzeug_run_main: + logger.info("in reloading mode and not in main loop, cancel the initialization") + return -# initialize the engines except on the first run of the werkzeug server. -if not werkzeug_reloader or (werkzeug_reloader and os.environ.get("WERKZEUG_RUN_MAIN") == "true"): locales_initialize() redis_initialize() searx.plugins.initialize(app) - searx.search.initialize( - enable_checker=True, - check_network=True, - enable_metrics=get_setting("general.enable_metrics"), - ) + + metrics: bool = get_setting("general.enable_metrics") # type: ignore + searx.search.initialize(enable_checker=True, check_network=True, enable_metrics=metrics) + limiter.initialize(app, settings) favicons.init() -def run(): - logger.debug('starting webserver on %s:%s', settings['server']['bind_address'], settings['server']['port']) - app.run( - debug=searx_debug, - use_debugger=searx_debug, - port=settings['server']['port'], - host=settings['server']['bind_address'], - threaded=True, - extra_files=[DEFAULT_SETTINGS_FILE], - ) - - application = app patch_application(app) +init() if __name__ == "__main__": run() diff --git a/searx/webutils.py b/searx/webutils.py index 6e49e3830..2a2da3797 100644 --- a/searx/webutils.py +++ b/searx/webutils.py @@ -9,7 +9,6 @@ import csv import hashlib import hmac import re -import inspect import itertools import json from datetime import datetime, timedelta @@ -316,21 +315,6 @@ def searxng_l10n_timespan(dt: datetime) -> str: # pylint: disable=invalid-name return format_date(dt) -def is_flask_run_cmdline(): - """Check if the application was started using "flask run" command line - - Inspect the callstack. - See https://github.com/pallets/flask/blob/master/src/flask/__main__.py - - Returns: - bool: True if the application was started using "flask run". - """ - frames = inspect.stack() - if len(frames) < 2: - return False - return frames[-2].filename.endswith('flask/cli.py') - - NO_SUBGROUPING = 'without further subgrouping' diff --git a/tests/__init__.py b/tests/__init__.py index e68d72feb..55a002196 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,13 +5,8 @@ import pathlib import os import aiounittest -# Before import from the searx package, we need to set up the (debug) -# environment. The import of the searx package initialize the searx.settings -# and this in turn takes the defaults from the environment! os.environ.pop('SEARXNG_SETTINGS_PATH', None) -os.environ['SEARXNG_DEBUG'] = '1' -os.environ['SEARXNG_DEBUG_LOG_LEVEL'] = 'WARNING' os.environ['SEARXNG_DISABLE_ETC_SETTINGS'] = '1' diff --git a/tests/robot/__main__.py b/tests/robot/__main__.py index 0b438745e..a18dd9e26 100644 --- a/tests/robot/__main__.py +++ b/tests/robot/__main__.py @@ -27,15 +27,6 @@ class SearxRobotLayer: webapp = str(tests_path.parent / 'searx' / 'webapp.py') exe = 'python' - # The Flask app is started by Flask.run(...), don't enable Flask's debug - # mode, the debugger from Flask will cause wired process model, where - # the server never dies. Further read: - # - # - debug mode: https://flask.palletsprojects.com/quickstart/#debug-mode - # - Flask.run(..): https://flask.palletsprojects.com/api/#flask.Flask.run - - os.environ['SEARXNG_DEBUG'] = '0' - # set robot settings path os.environ['SEARXNG_SETTINGS_PATH'] = str(tests_path / 'robot' / 'settings_robot.yml')