daemons.py 15KB

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