PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/gluon/newcron.py

https://github.com/goldenboy/skiheilw2p
Python | 313 lines | 290 code | 17 blank | 6 comment | 20 complexity | 2ad0e12cb8276de3f42a116e52f98052 MD5 | raw file
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. Created by Attila Csipa <web2py@csipa.in.rs>
  5. Modified by Massimo Di Pierro <mdipierro@cs.depaul.edu>
  6. """
  7. import sys
  8. import os
  9. import threading
  10. import logging
  11. import time
  12. import sched
  13. import re
  14. import datetime
  15. import platform
  16. import portalocker
  17. import fileutils
  18. import cPickle
  19. from settings import global_settings
  20. logger = logging.getLogger("web2py.cron")
  21. _cron_stopping = False
  22. def stopcron():
  23. "graceful shutdown of cron"
  24. global _cron_stopping
  25. _cron_stopping = True
  26. class extcron(threading.Thread):
  27. def __init__(self, applications_parent):
  28. threading.Thread.__init__(self)
  29. self.setDaemon(False)
  30. self.path = applications_parent
  31. crondance(self.path, 'external', startup=True)
  32. def run(self):
  33. if not _cron_stopping:
  34. logger.debug('external cron invocation')
  35. crondance(self.path, 'external', startup=False)
  36. class hardcron(threading.Thread):
  37. def __init__(self, applications_parent):
  38. threading.Thread.__init__(self)
  39. self.setDaemon(True)
  40. self.path = applications_parent
  41. crondance(self.path, 'hard', startup=True)
  42. def launch(self):
  43. if not _cron_stopping:
  44. logger.debug('hard cron invocation')
  45. crondance(self.path, 'hard', startup = False)
  46. def run(self):
  47. s = sched.scheduler(time.time, time.sleep)
  48. logger.info('Hard cron daemon started')
  49. while not _cron_stopping:
  50. now = time.time()
  51. s.enter(60 - now % 60, 1, self.launch, ())
  52. s.run()
  53. class softcron(threading.Thread):
  54. def __init__(self, applications_parent):
  55. threading.Thread.__init__(self)
  56. self.path = applications_parent
  57. crondance(self.path, 'soft', startup=True)
  58. def run(self):
  59. if not _cron_stopping:
  60. logger.debug('soft cron invocation')
  61. crondance(self.path, 'soft', startup=False)
  62. class Token(object):
  63. def __init__(self,path):
  64. self.path = os.path.join(path, 'cron.master')
  65. if not os.path.exists(self.path):
  66. fileutils.write_file(self.path, '', 'wb')
  67. self.master = None
  68. self.now = time.time()
  69. def acquire(self,startup=False):
  70. """
  71. returns the time when the lock is acquired or
  72. None if cron already running
  73. lock is implemented by writing a pickle (start, stop) in cron.master
  74. start is time when cron job starts and stop is time when cron completed
  75. stop == 0 if job started but did not yet complete
  76. if a cron job started within less than 60 seconds, acquire returns None
  77. if a cron job started before 60 seconds and did not stop,
  78. a warning is issue "Stale cron.master detected"
  79. """
  80. if portalocker.LOCK_EX == None:
  81. logger.warning('WEB2PY CRON: Disabled because no file locking')
  82. return None
  83. self.master = open(self.path,'rb+')
  84. try:
  85. ret = None
  86. portalocker.lock(self.master,portalocker.LOCK_EX)
  87. try:
  88. (start, stop) = cPickle.load(self.master)
  89. except:
  90. (start, stop) = (0, 1)
  91. if startup or self.now - start > 59.99:
  92. ret = self.now
  93. if not stop:
  94. # this happens if previous cron job longer than 1 minute
  95. logger.warning('WEB2PY CRON: Stale cron.master detected')
  96. logger.debug('WEB2PY CRON: Acquiring lock')
  97. self.master.seek(0)
  98. cPickle.dump((self.now,0),self.master)
  99. finally:
  100. portalocker.unlock(self.master)
  101. if not ret:
  102. # do this so no need to release
  103. self.master.close()
  104. return ret
  105. def release(self):
  106. """
  107. this function writes into cron.master the time when cron job
  108. was completed
  109. """
  110. if not self.master.closed:
  111. portalocker.lock(self.master,portalocker.LOCK_EX)
  112. logger.debug('WEB2PY CRON: Releasing cron lock')
  113. self.master.seek(0)
  114. (start, stop) = cPickle.load(self.master)
  115. if start == self.now: # if this is my lock
  116. self.master.seek(0)
  117. cPickle.dump((self.now,time.time()),self.master)
  118. portalocker.unlock(self.master)
  119. self.master.close()
  120. def rangetolist(s, period='min'):
  121. retval = []
  122. if s.startswith('*'):
  123. if period == 'min':
  124. s = s.replace('*', '0-59', 1)
  125. elif period == 'hr':
  126. s = s.replace('*', '0-23', 1)
  127. elif period == 'dom':
  128. s = s.replace('*', '1-31', 1)
  129. elif period == 'mon':
  130. s = s.replace('*', '1-12', 1)
  131. elif period == 'dow':
  132. s = s.replace('*', '0-6', 1)
  133. m = re.compile(r'(\d+)-(\d+)/(\d+)')
  134. match = m.match(s)
  135. if match:
  136. for i in range(int(match.group(1)), int(match.group(2)) + 1):
  137. if i % int(match.group(3)) == 0:
  138. retval.append(i)
  139. return retval
  140. def parsecronline(line):
  141. task = {}
  142. if line.startswith('@reboot'):
  143. line=line.replace('@reboot', '-1 * * * *')
  144. elif line.startswith('@yearly'):
  145. line=line.replace('@yearly', '0 0 1 1 *')
  146. elif line.startswith('@annually'):
  147. line=line.replace('@annually', '0 0 1 1 *')
  148. elif line.startswith('@monthly'):
  149. line=line.replace('@monthly', '0 0 1 * *')
  150. elif line.startswith('@weekly'):
  151. line=line.replace('@weekly', '0 0 * * 0')
  152. elif line.startswith('@daily'):
  153. line=line.replace('@daily', '0 0 * * *')
  154. elif line.startswith('@midnight'):
  155. line=line.replace('@midnight', '0 0 * * *')
  156. elif line.startswith('@hourly'):
  157. line=line.replace('@hourly', '0 * * * *')
  158. params = line.strip().split(None, 6)
  159. if len(params) < 7:
  160. return None
  161. daysofweek={'sun':0,'mon':1,'tue':2,'wed':3,'thu':4,'fri':5,'sat':6}
  162. for (s, id) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']):
  163. if not s in [None, '*']:
  164. task[id] = []
  165. vals = s.split(',')
  166. for val in vals:
  167. if val != '-1' and '-' in val and '/' not in val:
  168. val = '%s/1' % val
  169. if '/' in val:
  170. task[id] += rangetolist(val, id)
  171. elif val.isdigit() or val=='-1':
  172. task[id].append(int(val))
  173. elif id=='dow' and val[:3].lower() in daysofweek:
  174. task[id].append(daysofweek(val[:3].lower()))
  175. task['user'] = params[5]
  176. task['cmd'] = params[6]
  177. return task
  178. class cronlauncher(threading.Thread):
  179. def __init__(self, cmd, shell=True):
  180. threading.Thread.__init__(self)
  181. if platform.system() == 'Windows':
  182. shell = False
  183. elif isinstance(cmd,list):
  184. cmd = ' '.join(cmd)
  185. self.cmd = cmd
  186. self.shell = shell
  187. def run(self):
  188. import subprocess
  189. proc = subprocess.Popen(self.cmd,
  190. stdin=subprocess.PIPE,
  191. stdout=subprocess.PIPE,
  192. stderr=subprocess.PIPE,
  193. shell=self.shell)
  194. (stdoutdata,stderrdata) = proc.communicate()
  195. if proc.returncode != 0:
  196. logger.warning(
  197. 'WEB2PY CRON Call returned code %s:\n%s' % \
  198. (proc.returncode, stdoutdata+stderrdata))
  199. else:
  200. logger.debug('WEB2PY CRON Call returned success:\n%s' \
  201. % stdoutdata)
  202. def crondance(applications_parent, ctype='soft', startup=False):
  203. apppath = os.path.join(applications_parent,'applications')
  204. cron_path = os.path.join(apppath,'admin','cron')
  205. token = Token(cron_path)
  206. cronmaster = token.acquire(startup=startup)
  207. if not cronmaster:
  208. return
  209. now_s = time.localtime()
  210. checks=(('min',now_s.tm_min),
  211. ('hr',now_s.tm_hour),
  212. ('mon',now_s.tm_mon),
  213. ('dom',now_s.tm_mday),
  214. ('dow',(now_s.tm_wday+1)%7))
  215. apps = [x for x in os.listdir(apppath)
  216. if os.path.isdir(os.path.join(apppath, x))]
  217. for app in apps:
  218. if _cron_stopping:
  219. break;
  220. apath = os.path.join(apppath,app)
  221. cronpath = os.path.join(apath, 'cron')
  222. crontab = os.path.join(cronpath, 'crontab')
  223. if not os.path.exists(crontab):
  224. continue
  225. try:
  226. cronlines = fileutils.readlines_file(crontab, 'rt')
  227. lines = [x.strip() for x in cronlines if x.strip() and not x.strip().startswith('#')]
  228. tasks = [parsecronline(cline) for cline in lines]
  229. except Exception, e:
  230. logger.error('WEB2PY CRON: crontab read error %s' % e)
  231. continue
  232. for task in tasks:
  233. if _cron_stopping:
  234. break;
  235. commands = [sys.executable]
  236. w2p_path = fileutils.abspath('web2py.py', gluon=True)
  237. if os.path.exists(w2p_path):
  238. commands.append(w2p_path)
  239. if global_settings.applications_parent != global_settings.gluon_parent:
  240. commands.extend(('-f', global_settings.applications_parent))
  241. citems = [(k in task and not v in task[k]) for k,v in checks]
  242. task_min= task.get('min',[])
  243. if not task:
  244. continue
  245. elif not startup and task_min == [-1]:
  246. continue
  247. elif task_min != [-1] and reduce(lambda a,b: a or b, citems):
  248. continue
  249. logger.info('WEB2PY CRON (%s): %s executing %s in %s at %s' \
  250. % (ctype, app, task.get('cmd'),
  251. os.getcwd(), datetime.datetime.now()))
  252. action, command, models = False, task['cmd'], ''
  253. if command.startswith('**'):
  254. (action,models,command) = (True,'',command[2:])
  255. elif command.startswith('*'):
  256. (action,models,command) = (True,'-M',command[1:])
  257. else:
  258. action=False
  259. if action and command.endswith('.py'):
  260. commands.extend(('-J', # cron job
  261. models, # import models?
  262. '-S', app, # app name
  263. '-a', '"<recycle>"', # password
  264. '-R', command)) # command
  265. shell = True
  266. elif action:
  267. commands.extend(('-J', # cron job
  268. models, # import models?
  269. '-S', app+'/'+command, # app name
  270. '-a', '"<recycle>"')) # password
  271. shell = True
  272. else:
  273. commands = command
  274. shell = False
  275. try:
  276. cronlauncher(commands, shell=shell).start()
  277. except Exception, e:
  278. logger.warning(
  279. 'WEB2PY CRON: Execution error for %s: %s' \
  280. % (task.get('cmd'), e))
  281. token.release()