daemons.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import logging
  2. import threading
  3. from configparser import DuplicateSectionError
  4. from wsgiref.simple_server import make_server
  5. import signal
  6. import collections
  7. from radicale import Application as RadicaleApplication
  8. from radicale import HTTPServer as BaseRadicaleHTTPServer
  9. from radicale import HTTPSServer as BaseRadicaleHTTPSServer
  10. from radicale import RequestHandler as RadicaleRequestHandler
  11. from radicale import config as radicale_config
  12. from rq import Connection as RQConnection
  13. from rq import Worker as BaseRQWorker
  14. from redis import Redis
  15. from tracim.lib.base import logger
  16. from tracim.lib.exceptions import AlreadyRunningDaemon
  17. from tracim.lib.utils import add_signal_handler
  18. class DaemonsManager(object):
  19. def __init__(self):
  20. self._running_daemons = {}
  21. add_signal_handler(signal.SIGTERM, self.stop_all)
  22. def run(self, name: str, daemon_class: object, **kwargs) -> None:
  23. """
  24. Start a daemon with given daemon class.
  25. :param name: Name of runned daemon. It's not possible to start two
  26. daemon with same name. In the opposite case, raise
  27. tracim.lib.exceptions.AlreadyRunningDaemon
  28. :param daemon_class: Daemon class to use for daemon instance.
  29. :param kwargs: Other kwargs will be given to daemon class
  30. instantiation.
  31. """
  32. if name in self._running_daemons:
  33. raise AlreadyRunningDaemon(
  34. 'Daemon with name "{0}" already running'.format(name)
  35. )
  36. logger.info(self, 'Starting daemon with name "{0}" and class "{1}" ...'
  37. .format(name, daemon_class))
  38. daemon = daemon_class(name=name, kwargs=kwargs, daemon=True)
  39. daemon.start()
  40. self._running_daemons[name] = daemon
  41. def stop(self, name: str) -> None:
  42. """
  43. Stop daemon with his name and wait for him.
  44. Where name is given name when daemon started
  45. with run method.
  46. :param name:
  47. """
  48. if name in self._running_daemons:
  49. logger.info(self, 'Stopping daemon with name "{0}" ...'
  50. .format(name))
  51. self._running_daemons[name].stop()
  52. self._running_daemons[name].join()
  53. del self._running_daemons[name]
  54. logger.info(self, 'Stopping daemon with name "{0}": OK'
  55. .format(name))
  56. def stop_all(self, *args, **kwargs) -> None:
  57. """
  58. Stop all started daemons and wait for them.
  59. """
  60. logger.info(self, 'Stopping all daemons')
  61. for name, daemon in self._running_daemons.items():
  62. logger.info(self, 'Stopping daemon "{0}" ...'.format(name))
  63. daemon.stop()
  64. for name, daemon in self._running_daemons.items():
  65. daemon.join()
  66. logger.info(self, 'Stopping daemon "{0}" OK'.format(name))
  67. self._running_daemons = {}
  68. def execute_in_thread(self, thread_name, callback):
  69. self._running_daemons[thread_name].append_thread_callback(callback)
  70. class TracimSocketServerMixin(object):
  71. """
  72. Mixin to use with socketserver.BaseServer who add _after_serve_actions
  73. method executed after end of server execution.
  74. """
  75. def __init__(self, *args, **kwargs):
  76. super().__init__(*args, **kwargs)
  77. self._daemon_execute_callbacks = []
  78. def append_thread_callback(self, callback: collections.Callable) -> None:
  79. """
  80. Add callback to self._daemon_execute_callbacks. See service_actions
  81. function to their usages.
  82. :param callback: callback to execute in daemon
  83. """
  84. self._daemon_execute_callbacks.append(callback)
  85. def serve_forever(self, *args, **kwargs):
  86. super().serve_forever(*args, **kwargs)
  87. # After serving (in case of stop) do following:
  88. self._after_serve_actions()
  89. def _after_serve_actions(self):
  90. """
  91. Override (and call super if needed) to execute actions when server
  92. finish it's job.
  93. """
  94. pass
  95. def service_actions(self):
  96. if len(self._daemon_execute_callbacks):
  97. try:
  98. while True:
  99. self._daemon_execute_callbacks.pop()()
  100. except IndexError:
  101. pass # Finished to iter
  102. class Daemon(threading.Thread):
  103. """
  104. Thread who contains daemon. You must implement start and stop methods to
  105. manage daemon life correctly.
  106. """
  107. def run(self) -> None:
  108. """
  109. Place here code who have to be executed in Daemon.
  110. """
  111. raise NotImplementedError()
  112. def stop(self) -> None:
  113. """
  114. Place here code who stop your daemon
  115. """
  116. raise NotImplementedError()
  117. def append_thread_callback(self, callback: collections.Callable) -> None:
  118. """
  119. Place here the logic who permit to execute a callback in your daemon.
  120. To get an exemple of that, take a look at
  121. socketserver.BaseServer#service_actions and how we use it in
  122. tracim.lib.daemons.TracimSocketServerMixin#service_actions .
  123. :param callback: callback to execute in your thread.
  124. """
  125. raise NotImplementedError()
  126. class MailSenderDaemon(Daemon):
  127. # NOTE: use *args and **kwargs because parent __init__ use strange
  128. # * parameter
  129. def __init__(self, *args, **kwargs):
  130. super().__init__(*args, **kwargs)
  131. self.worker = None # type: RQWorker
  132. def append_thread_callback(self, callback: collections.Callable) -> None:
  133. logger.warning('MailSenderDaemon not implement append_thread_callback')
  134. pass
  135. def stop(self) -> None:
  136. self.worker.request_stop('TRACIM STOP', None)
  137. def run(self) -> None:
  138. from tracim.config.app_cfg import CFG
  139. cfg = CFG.get_instance()
  140. with RQConnection(Redis(
  141. host=cfg.EMAIL_SENDER_REDIS_HOST,
  142. port=cfg.EMAIL_SENDER_REDIS_PORT,
  143. db=cfg.EMAIL_SENDER_REDIS_DB,
  144. )):
  145. self.worker = RQWorker(['mail_sender'])
  146. self.worker.work()
  147. class RQWorker(BaseRQWorker):
  148. def _install_signal_handlers(self):
  149. # TODO BS 20170126: RQ WWorker is designed to work in main thread
  150. # So we have to disable these signals (we implement server stop in
  151. # MailSenderDaemon.stop method). When bug
  152. # https://github.com/tracim/tracim/issues/166 will be fixed, ensure
  153. # This worker terminate correctly.
  154. pass
  155. class RadicaleHTTPSServer(TracimSocketServerMixin, BaseRadicaleHTTPSServer):
  156. pass
  157. class RadicaleHTTPServer(TracimSocketServerMixin, BaseRadicaleHTTPServer):
  158. pass
  159. class RadicaleDaemon(Daemon):
  160. def __init__(self, *args, **kwargs):
  161. super().__init__(*args, **kwargs)
  162. self._prepare_config()
  163. self._server = None
  164. def run(self):
  165. """
  166. To see origin radical server start method, refer to
  167. radicale.__main__.run
  168. """
  169. self._server = self._get_server()
  170. self._server.serve_forever()
  171. def stop(self):
  172. self._server.shutdown()
  173. def _prepare_config(self):
  174. from tracim.config.app_cfg import CFG
  175. cfg = CFG.get_instance()
  176. tracim_auth = 'tracim.lib.radicale.auth'
  177. tracim_rights = 'tracim.lib.radicale.rights'
  178. tracim_storage = 'tracim.lib.radicale.storage'
  179. fs_path = cfg.RADICALE_SERVER_FILE_SYSTEM_FOLDER
  180. allow_origin = cfg.RADICALE_SERVER_ALLOW_ORIGIN
  181. realm_message = cfg.RADICALE_SERVER_REALM_MESSAGE
  182. radicale_config.set('auth', 'type', 'custom')
  183. radicale_config.set('auth', 'custom_handler', tracim_auth)
  184. radicale_config.set('rights', 'type', 'custom')
  185. radicale_config.set('rights', 'custom_handler', tracim_rights)
  186. radicale_config.set('storage', 'type', 'custom')
  187. radicale_config.set('storage', 'custom_handler', tracim_storage)
  188. radicale_config.set('storage', 'filesystem_folder', fs_path)
  189. radicale_config.set('server', 'realm', realm_message)
  190. try:
  191. radicale_config.add_section('headers')
  192. except DuplicateSectionError:
  193. pass # It is not a problem, we just want it exist
  194. if allow_origin:
  195. radicale_config.set(
  196. 'headers',
  197. 'Access-Control-Allow-Origin',
  198. allow_origin,
  199. )
  200. # Radicale is not 100% CALDAV Compliant, we force some Allow-Methods
  201. radicale_config.set(
  202. 'headers',
  203. 'Access-Control-Allow-Methods',
  204. 'DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, OPTIONS, PROPFIND, '
  205. 'PROPPATCH, PUT, REPORT',
  206. )
  207. # Radicale is not 100% CALDAV Compliant, we force some Allow-Headers
  208. radicale_config.set(
  209. 'headers',
  210. 'Access-Control-Allow-Headers',
  211. 'X-Requested-With,X-Auth-Token,Content-Type,Content-Length,'
  212. 'X-Client,Authorization,depth,Prefer,If-None-Match,If-Match',
  213. )
  214. def _get_server(self):
  215. from tracim.config.app_cfg import CFG
  216. cfg = CFG.get_instance()
  217. return make_server(
  218. cfg.RADICALE_SERVER_HOST,
  219. cfg.RADICALE_SERVER_PORT,
  220. RadicaleApplication(),
  221. RadicaleHTTPSServer if cfg.RADICALE_SERVER_SSL else RadicaleHTTPServer,
  222. RadicaleRequestHandler
  223. )
  224. def append_thread_callback(self, callback: collections.Callable) -> None:
  225. """
  226. Give the callback to running server through
  227. tracim.lib.daemons.TracimSocketServerMixin#append_thread_callback
  228. :param callback: callback to execute in daemon
  229. """
  230. self._server.append_thread_callback(callback)
  231. # TODO : webdav deamon, make it clean !
  232. import sys, os
  233. from wsgidav.wsgidav_app import DEFAULT_CONFIG
  234. from wsgidav.xml_tools import useLxml
  235. from wsgidav.wsgidav_app import WsgiDAVApp
  236. from wsgidav._version import __version__
  237. from tracim.lib.webdav.sql_dav_provider import Provider
  238. from tracim.lib.webdav.sql_domain_controller import TracimDomainController
  239. from inspect import isfunction
  240. import traceback
  241. from wsgidav.server.cherrypy import wsgiserver
  242. from wsgidav.server.cherrypy.wsgiserver.wsgiserver3 import CherryPyWSGIServer
  243. DEFAULT_CONFIG_FILE = "wsgidav.conf"
  244. PYTHON_VERSION = "%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
  245. class WsgiDavDaemon(Daemon):
  246. def __init__(self, *args, **kwargs):
  247. super().__init__(*args, **kwargs)
  248. self.config = self._initConfig()
  249. self._server = None
  250. def _initConfig(self):
  251. """Setup configuration dictionary from default, command line and configuration file."""
  252. from tg import config as tg_config
  253. # Set config defaults
  254. config = DEFAULT_CONFIG.copy()
  255. temp_verbose = config["verbose"]
  256. # Configuration file overrides defaults
  257. default_config_file = os.path.abspath(DEFAULT_CONFIG_FILE)
  258. config_file = tg_config.get('wsgidav.config_path', default_config_file)
  259. fileConf = self._readConfigFile(config_file, temp_verbose)
  260. config.update(fileConf)
  261. if not useLxml and config["verbose"] >= 1:
  262. print(
  263. "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
  264. from wsgidav.dir_browser import WsgiDavDirBrowser
  265. from tracim.lib.webdav.tracim_http_authenticator import TracimHTTPAuthenticator
  266. from wsgidav.error_printer import ErrorPrinter
  267. from tracim.lib.webdav.utils import TracimWsgiDavDebugFilter
  268. config['middleware_stack'] = [
  269. WsgiDavDirBrowser,
  270. TracimHTTPAuthenticator,
  271. ErrorPrinter,
  272. TracimWsgiDavDebugFilter,
  273. ]
  274. config['provider_mapping'] = {
  275. config['root_path']: Provider(
  276. # TODO: Test to Re enabme archived and deleted
  277. show_archived=False, # config['show_archived'],
  278. show_deleted=False, # config['show_deleted'],
  279. show_history=False, # config['show_history'],
  280. manage_locks=config['manager_locks']
  281. )
  282. }
  283. config['domaincontroller'] = TracimDomainController(presetdomain=None, presetserver=None)
  284. return config
  285. def _readConfigFile(self, config_file, verbose):
  286. """Read configuration file options into a dictionary."""
  287. if not os.path.exists(config_file):
  288. raise RuntimeError("Couldn't open configuration file '%s'." % config_file)
  289. try:
  290. import imp
  291. conf = {}
  292. configmodule = imp.load_source("configuration_module", config_file)
  293. for k, v in vars(configmodule).items():
  294. if k.startswith("__"):
  295. continue
  296. elif isfunction(v):
  297. continue
  298. conf[k] = v
  299. except Exception as e:
  300. exceptioninfo = traceback.format_exception_only(sys.exc_type, sys.exc_value) # @UndefinedVariable
  301. exceptiontext = ""
  302. for einfo in exceptioninfo:
  303. exceptiontext += einfo + "\n"
  304. print("Failed to read configuration file: " + config_file + "\nDue to " + exceptiontext, file=sys.stderr)
  305. raise
  306. return conf
  307. def run(self):
  308. app = WsgiDAVApp(self.config)
  309. # Try running WsgiDAV inside the following external servers:
  310. self._runCherryPy(app, self.config)
  311. def _runCherryPy(self, app, config):
  312. version = "WsgiDAV/%s %s Python/%s" % (
  313. __version__,
  314. wsgiserver.CherryPyWSGIServer.version,
  315. PYTHON_VERSION
  316. )
  317. wsgiserver.CherryPyWSGIServer.version = version
  318. protocol = "http"
  319. if config["verbose"] >= 1:
  320. print("Running %s" % version)
  321. print("Listening on %s://%s:%s ..." % (protocol, config["host"], config["port"]))
  322. self._server = CherryPyWSGIServer(
  323. (config["host"], config["port"]),
  324. app,
  325. server_name=version,
  326. )
  327. self._server.start()
  328. def stop(self):
  329. self._server.stop()
  330. def append_thread_callback(self, callback: collections.Callable) -> None:
  331. """
  332. Place here the logic who permit to execute a callback in your daemon.
  333. To get an exemple of that, take a look at
  334. socketserver.BaseServer#service_actions and how we use it in
  335. tracim.lib.daemons.TracimSocketServerMixin#service_actions .
  336. :param callback: callback to execute in your thread.
  337. """
  338. raise NotImplementedError()