background.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # lint: pylint
  3. # pylint: disable=missing-module-docstring
  4. # pyright: strict
  5. import json
  6. import random
  7. import time
  8. import threading
  9. import os
  10. import signal
  11. from typing import Dict, Union, List, Any, Tuple
  12. from typing_extensions import TypedDict, Literal
  13. from searx import logger, settings, searx_debug
  14. from searx.exceptions import SearxSettingsException
  15. from searx.search.processors import PROCESSORS
  16. from searx.search.checker import Checker
  17. from searx.shared import schedule, storage # pyright: ignore
  18. CHECKER_RESULT = 'CHECKER_RESULT'
  19. running = threading.Lock()
  20. CheckerResult = Union['CheckerOk', 'CheckerErr', 'CheckerOther']
  21. class CheckerOk(TypedDict):
  22. """Checking the engines succeeded"""
  23. status: Literal['ok']
  24. engines: Dict[str, 'EngineResult']
  25. timestamp: int
  26. class CheckerErr(TypedDict):
  27. """Checking the engines failed"""
  28. status: Literal['error']
  29. timestamp: int
  30. class CheckerOther(TypedDict):
  31. """The status is unknown or disabled"""
  32. status: Literal['unknown', 'disabled']
  33. EngineResult = Union['EngineOk', 'EngineErr']
  34. class EngineOk(TypedDict):
  35. """Checking the engine succeeded"""
  36. success: Literal[True]
  37. class EngineErr(TypedDict):
  38. """Checking the engine failed"""
  39. success: Literal[False]
  40. errors: Dict[str, List[str]]
  41. def _get_interval(every: Any, error_msg: str) -> Tuple[int, int]:
  42. if isinstance(every, int):
  43. return (every, every)
  44. if (
  45. not isinstance(every, (tuple, list))
  46. or len(every) != 2 # type: ignore
  47. or not isinstance(every[0], int)
  48. or not isinstance(every[1], int)
  49. ):
  50. raise SearxSettingsException(error_msg, None)
  51. return (every[0], every[1])
  52. def _get_every():
  53. every = settings.get('checker', {}).get('scheduling', {}).get('every', (300, 1800))
  54. return _get_interval(every, 'checker.scheduling.every is not a int or list')
  55. def get_result() -> CheckerResult:
  56. serialized_result = storage.get_str(CHECKER_RESULT)
  57. if serialized_result is not None:
  58. return json.loads(serialized_result)
  59. return {'status': 'unknown'}
  60. def _set_result(result: CheckerResult):
  61. storage.set_str(CHECKER_RESULT, json.dumps(result))
  62. def _timestamp():
  63. return int(time.time() / 3600) * 3600
  64. def run():
  65. if not running.acquire(blocking=False): # pylint: disable=consider-using-with
  66. return
  67. try:
  68. logger.info('Starting checker')
  69. result: CheckerOk = {'status': 'ok', 'engines': {}, 'timestamp': _timestamp()}
  70. for name, processor in PROCESSORS.items():
  71. logger.debug('Checking %s engine', name)
  72. checker = Checker(processor)
  73. checker.run()
  74. if checker.test_results.successful:
  75. result['engines'][name] = {'success': True}
  76. else:
  77. result['engines'][name] = {'success': False, 'errors': checker.test_results.errors}
  78. _set_result(result)
  79. logger.info('Check done')
  80. except Exception: # pylint: disable=broad-except
  81. _set_result({'status': 'error', 'timestamp': _timestamp()})
  82. logger.exception('Error while running the checker')
  83. finally:
  84. running.release()
  85. def _run_with_delay():
  86. every = _get_every()
  87. delay = random.randint(0, every[1] - every[0])
  88. logger.debug('Start checker in %i seconds', delay)
  89. time.sleep(delay)
  90. run()
  91. def _start_scheduling():
  92. every = _get_every()
  93. if schedule(every[0], _run_with_delay):
  94. run()
  95. def _signal_handler(_signum: int, _frame: Any):
  96. t = threading.Thread(target=run)
  97. t.daemon = True
  98. t.start()
  99. def initialize():
  100. if hasattr(signal, 'SIGUSR1'):
  101. # Windows doesn't support SIGUSR1
  102. logger.info('Send SIGUSR1 signal to pid %i to start the checker', os.getpid())
  103. signal.signal(signal.SIGUSR1, _signal_handler)
  104. # disabled by default
  105. _set_result({'status': 'disabled'})
  106. # special case when debug is activate
  107. if searx_debug and settings.get('checker', {}).get('off_when_debug', True):
  108. logger.info('debug mode: checker is disabled')
  109. return
  110. # check value of checker.scheduling.every now
  111. scheduling = settings.get('checker', {}).get('scheduling', None)
  112. if scheduling is None or not scheduling:
  113. logger.info('Checker scheduler is disabled')
  114. return
  115. #
  116. _set_result({'status': 'unknown'})
  117. start_after = scheduling.get('start_after', (300, 1800))
  118. start_after = _get_interval(start_after, 'checker.scheduling.start_after is not a int or list')
  119. delay = random.randint(start_after[0], start_after[1])
  120. logger.info('Start checker in %i seconds', delay)
  121. t = threading.Timer(delay, _start_scheduling)
  122. t.daemon = True
  123. t.start()