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