A micro Crontab & Task Queue for Python Web.
30 |
120 |
123 |
124 | % if not data:
125 | 暂无日志
126 | % end
127 | % for i, item in enumerate(data, 1):
128 | {{ item.strip() }}
129 | % end
130 |
131 | % if count > 10 * page:
132 | 下一页 |
133 | % end
134 | % if page > 1:
135 | 上一页 |
136 | % end
137 | {{ sort['title'] }} |
138 | {{ other['title'] }}
139 |
140 |
141 |
142 | '''.replace('#nav#', nav).replace('#style#', style).replace('#github#', github)
143 |
--------------------------------------------------------------------------------
/ucron/utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*
3 | from __future__ import absolute_import
4 |
5 | import sys
6 | from numbers import Number
7 | try:
8 | from ujson import dumps, loads
9 | except ImportError:
10 | from json import dumps, loads
11 |
12 | py3k = sys.version_info.major > 2
13 |
14 | # Lots of stdlib and builtin differences.
15 | if py3k:
16 | from urllib import request
17 | from urllib.parse import urlencode
18 | from urllib.parse import parse_qsl
19 | from urllib.error import URLError
20 | from queue import Queue
21 | unicode = str
22 | else: # 2.x
23 | import urllib2 as request
24 | from urllib import urlencode
25 | from urlparse import parse_qsl
26 | from urllib2 import URLError
27 | from Queue import Queue
28 | unicode = unicode
29 |
30 |
31 | # Some helpers for string/bytes handling
32 | def to_bytes(s, encoding='utf8'):
33 | if isinstance(s, unicode):
34 | return s.encode(encoding)
35 | if isinstance(s, (bool, Number)):
36 | return str(s).encode(encoding)
37 | return bytes('' if s is None else s)
38 |
39 |
40 | def to_unicode(s, encoding='utf8', errors='strict'):
41 | if isinstance(s, bytes):
42 | return s.decode(encoding, errors)
43 | return unicode('' if s is None else s)
44 |
45 |
46 | to_string = to_unicode if py3k else to_bytes
47 |
48 |
49 | def dict_encode(d, encoding='utf8'):
50 | return dict({(to_bytes(k, encoding), to_bytes(v, encoding))
51 | for k, v in d.items()})
52 |
53 |
54 | # Copy from https://github.com/webpy/webpy/blob/master/web/utils.py
55 | class IterBetter:
56 | """
57 | Returns an object that can be used as an iterator
58 | but can also be used via __getitem__ (although it
59 | cannot go backwards -- that is, you cannot request
60 | `iterbetter[0]` after requesting `iterbetter[1]`).
61 |
62 | >>> import itertools
63 | >>> c = iterbetter(itertools.count())
64 | >>> c[1]
65 | 1
66 | >>> c[5]
67 | 5
68 | >>> c[3]
69 | Traceback (most recent call last):
70 | ...
71 | IndexError: already passed 3
72 | It is also possible to get the first value of the iterator or None.
73 | >>> c = iterbetter(iter([3, 4, 5]))
74 | >>> print(c.first())
75 | 3
76 | >>> c = iterbetter(iter([]))
77 | >>> print(c.first())
78 | None
79 | For boolean test, IterBetter peeps at first value in the itertor without effecting the iteration.
80 | >>> c = iterbetter(iter(range(5)))
81 | >>> bool(c)
82 | True
83 | >>> list(c)
84 | [0, 1, 2, 3, 4]
85 | >>> c = iterbetter(iter([]))
86 | >>> bool(c)
87 | False
88 | >>> list(c)
89 | []
90 | """
91 | def __init__(self, iterator):
92 | self.i, self.c = iterator, 0
93 |
94 | def first(self, default=None):
95 | """Returns the first element of the iterator or None when there are no
96 | elements.
97 | If the optional argument default is specified, that is returned instead
98 | of None when there are no elements.
99 | """
100 | try:
101 | return next(iter(self))
102 | except StopIteration:
103 | return default
104 |
105 | def list(self):
106 | return list(self)
107 |
108 | def __iter__(self):
109 | if hasattr(self, "_head"):
110 | yield self._head
111 |
112 | while 1:
113 | try:
114 | yield next(self.i)
115 | except StopIteration:
116 | return
117 | self.c += 1
118 |
119 | def __getitem__(self, i):
120 | # todo: slices
121 | if i < self.c:
122 | raise IndexError("already passed " + str(i))
123 | try:
124 | while i > self.c:
125 | next(self.i)
126 | self.c += 1
127 | # now self.c == i
128 | self.c += 1
129 | return next(self.i)
130 | except StopIteration:
131 | raise IndexError(str(i))
132 |
133 | def __nonzero__(self):
134 | if hasattr(self, "__len__"):
135 | return self.__len__() != 0
136 | elif hasattr(self, "_head"):
137 | return True
138 | else:
139 | try:
140 | self._head = next(self.i)
141 | except StopIteration:
142 | return False
143 | else:
144 | return True
145 |
146 | __bool__ = __nonzero__
147 |
148 |
149 | iterbetter = IterBetter
150 |
151 |
152 | __all__ = ['dumps', 'loads', 'py3k', 'request', 'urlencode', 'parse_qsl', 'URLError', 'Queue',
153 | 'unicode', 'to_bytes', 'to_unicode', 'to_string', 'dict_encode', 'iterbetter']
154 |
--------------------------------------------------------------------------------
/ucron/worker.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*
3 | from __future__ import absolute_import
4 |
5 | import time
6 | import uuid
7 | import threading
8 | from datetime import tzinfo, timedelta, datetime
9 |
10 | from ucron import conf, db, __version__
11 | from ucron.utils import request, urlencode, parse_qsl, Queue, to_string
12 |
13 | stdout_q = Queue()
14 | lock = threading.Lock()
15 |
16 |
17 | class UTC(tzinfo):
18 | def __init__(self, offset=0):
19 | self._offset = offset
20 |
21 | def utcoffset(self, dt):
22 | return timedelta(hours=self._offset)
23 |
24 | def tzname(self, dt):
25 | return 'UTC %+d:00' % self._offset
26 |
27 | def dst(self, dt):
28 | return timedelta(hours=self._offset)
29 |
30 |
31 | class Job:
32 | def __init__(self, args):
33 | self.path = args[0]
34 | if len(args) > 2:
35 | self.args = urlencode(parse_qsl(args[1]))
36 | self.method = args[2]
37 | elif len(args) == 2:
38 | if args[1].upper() in ['GET', 'POST']:
39 | self.args = ''
40 | self.method = args[1]
41 | else:
42 | self.args = urlencode(parse_qsl(args[1]))
43 | self.method = 'GET'
44 | else:
45 | self.args = ''
46 | self.method = 'GET'
47 |
48 |
49 | def parse_schedule(node, attr, scope, schedule):
50 | if node == '*':
51 | schedule[attr] = list(range(*scope))
52 | elif '/' in node:
53 | node, step = node.split('/')
54 | if '-' in node:
55 | scope = list(map(int, node.split('-')))
56 | scope = [scope[0], scope[1] + 1]
57 | scope.append(int(step))
58 | schedule[attr] = list(range(*scope))
59 | elif '-' in node:
60 | scope = list(map(int, node.split('-')))
61 | scope = [scope[0], scope[1] + 1]
62 | schedule[attr] = list(range(*scope))
63 | elif ',' in node:
64 | schedule[attr] = list(map(int, node.split(',')))
65 | else:
66 | schedule[attr] = [int(node)]
67 |
68 |
69 | def parse_crontab(line):
70 | args = line.split(' ')
71 | job_id = uuid.uuid5(uuid.NAMESPACE_DNS, line).hex
72 | cron, job = args[:5], Job(args[5:])
73 | db.status.push(job_id, ' '.join(cron))
74 | schedule = {}
75 | parse_schedule(cron[0], 'minute', [0, 60], schedule)
76 | parse_schedule(cron[1], 'hour', [0, 24], schedule)
77 | parse_schedule(cron[2], 'day', [1, 32], schedule)
78 | parse_schedule(cron[3], 'month', [1, 13], schedule)
79 | parse_schedule(cron[4], 'weekday', [0, 7], schedule)
80 | db.cron.push(job_id, job.path, job.args, job.method, schedule)
81 |
82 |
83 | def urlopen(path, args, method, json=False):
84 | now = datetime.now(UTC(conf.utc)).strftime('%d/%b/%Y %H:%M:%S')
85 | try:
86 | headers = {'User-Agent': 'uCron v%s' % __version__}
87 | path += '/' if path.count('/') < 3 else ''
88 | if method.upper() == 'POST' or json:
89 | method = 'POST'
90 | data = args.encode('utf8') if args else b''
91 | if json:
92 | headers['Content-Type'] = 'application/json'
93 | else:
94 | data = None
95 | path += '?' + args if args else ''
96 | resp = request.urlopen(request.Request(path, headers=headers), data)
97 | return '[%s] %s %s - %s' % (now, path, method, resp.code)
98 | except Exception as common_ex:
99 | return '[%s] %s %s - %s' % (now, path, method, common_ex)
100 |
101 |
102 | def now():
103 | now = datetime.now(UTC(conf.utc))
104 | now = {
105 | 'minute': now.minute,
106 | 'hour': now.hour,
107 | 'day': now.day,
108 | 'month': now.month,
109 | 'weekday': (now.weekday() + 1) % 7 # Monday == 1 ... Saturday == 6, Sunday == 0
110 | }
111 | return now
112 |
113 |
114 | def sub_in(now, job):
115 | for key in now.keys():
116 | if now[key] not in job[key]:
117 | return False
118 | return True
119 |
120 |
121 | def daemon_cron():
122 | while True:
123 | for job in db.cron.fetchall():
124 | if sub_in(now(), job):
125 | resp = urlopen(job['path'], job['args'], job['method'])
126 | db.status.update(job['id'], resp)
127 | stdout_q.put('Cron %s' % resp)
128 | time.sleep(60.1 - datetime.now().second)
129 |
130 |
131 | def run_task(task):
132 | resp = urlopen(*task[:-1])
133 | stdout_q.put('Task %s' % resp)
134 |
135 |
136 | def select_task(name, mode):
137 | if mode == 'seq':
138 | while True:
139 | task = db.task.pop(name)
140 | if not task:
141 | break
142 | run_task(task)
143 | else:
144 | threads = []
145 | for task in db.task.fetchall(name):
146 | threads.append(threading.Thread(target=run_task, args=(task,)))
147 | for t in threads:
148 | t.start()
149 |
150 |
151 | def daemon_task():
152 | while True:
153 | threads = []
154 | for name, mode in db.taskq.fetchall():
155 | threads.append(threading.Thread(target=select_task, args=(name, mode)))
156 | for t in threads:
157 | t.start()
158 | time.sleep(0.01)
159 |
160 |
161 | def _stdout():
162 | prefix = '%s' if conf.quiet else '* %s'
163 | while True:
164 | line = stdout_q.get()
165 | with open(conf.log, 'ab') as f:
166 | f.write(('%s\n' % line).encode('utf8'))
167 | print(prefix % line)
168 |
169 |
170 | def clean_log():
171 | lock.acquire()
172 | with open(conf.log, 'rb') as f:
173 | lines = f.readlines()
174 | with open(conf.log, 'wb') as f:
175 | f.writelines(lines[-conf.max:])
176 | lock.release()
177 |
178 |
179 | def load_crontab():
180 | db.cron.empty() # empty old cron
181 | parse_crontab('%s %s/clean' % (conf.tab, conf.local)) # add clean task
182 |
183 | if not conf.cron: # nothing to do
184 | return
185 |
186 | if conf.reload:
187 | parse_crontab('* * * * * %s/reload' % conf.local) # add reload task
188 |
189 | with open(conf.cron, 'rb') as f:
190 | for line in f.readlines():
191 | line = to_string(line).strip()
192 | if not line:
193 | break
194 | if line.startswith('#'):
195 | continue
196 | parse_crontab(line)
197 |
198 |
199 | def start():
200 | print('uCron v%s server starting up ...' % __version__)
201 |
202 | load_crontab()
203 | db.taskq.push('default_seq', 'seq') # add default sequence queue
204 | db.taskq.push('default_con', 'con') # add default concurrence queue
205 |
206 | threads = []
207 | for func in (daemon_cron, daemon_task, _stdout):
208 | threads.append(threading.Thread(target=func))
209 | for t in threads:
210 | t.daemon = True
211 | t.start()
212 |
--------------------------------------------------------------------------------