~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robey Pointer
  • Date: 2007-01-14 05:40:40 UTC
  • Revision ID: robey@lag.net-20070114054040-7i9lbhq992e612rq
fix up dev.cfg so that nobody will ever have to edit it, by letting the
important params be overridable in loggerhead.conf.

make start-loggerhead actually daemonize, write a pid file, and write logs
to normal log files, instead of requiring 'nohup' stuff.  ie act like a real
server.  added stop-loggerhead to do a clean shutdown.  changed the README
to clarify how it should work now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
18
#
19
19
 
20
 
try:
21
 
    from xml.etree import ElementTree as ET
22
 
except ImportError:
23
 
    from elementtree import ElementTree as ET
24
 
 
25
20
import base64
26
21
import cgi
27
22
import datetime
28
23
import logging
29
24
import re
 
25
import sha
30
26
import struct
 
27
import sys
31
28
import threading
32
 
import time
 
29
import traceback
 
30
 
 
31
import turbogears
33
32
 
34
33
 
35
34
log = logging.getLogger("loggerhead.controllers")
36
35
 
 
36
 
 
37
def timespan(delta):
 
38
    if delta.days > 730:
 
39
        # good grief!
 
40
        return '%d years' % (int(delta.days // 365.25),)
 
41
    if delta.days >= 3:
 
42
        return '%d days' % delta.days
 
43
    seg = []
 
44
    if delta.days > 0:
 
45
        if delta.days == 1:
 
46
            seg.append('1 day')
 
47
        else:
 
48
            seg.append('%d days' % delta.days)
 
49
    hrs = delta.seconds // 3600
 
50
    mins = (delta.seconds % 3600) // 60
 
51
    if hrs > 0:
 
52
        if hrs == 1:
 
53
            seg.append('1 hour')
 
54
        else:
 
55
            seg.append('%d hours' % hrs)
 
56
    if delta.days == 0:
 
57
        if mins > 0:
 
58
            if mins == 1:
 
59
                seg.append('1 minute')
 
60
            else:
 
61
                seg.append('%d minutes' % mins)
 
62
        elif hrs == 0:
 
63
            seg.append('less than a minute')
 
64
    return ', '.join(seg)
 
65
 
 
66
 
 
67
def ago(timestamp):
 
68
    now = datetime.datetime.now()
 
69
    return timespan(now - timestamp) + ' ago'
 
70
 
 
71
 
37
72
def fix_year(year):
38
73
    if year < 70:
39
74
        year += 2000
41
76
        year += 1900
42
77
    return year
43
78
 
44
 
# Display of times.
45
 
 
46
 
# date_day -- just the day
47
 
# date_time -- full date with time
48
 
#
49
 
# displaydate -- for use in sentences
50
 
# approximatedate -- for use in tables
51
 
#
52
 
# displaydate and approximatedate return an elementtree <span> Element
53
 
# with the full date in a tooltip.
54
 
 
55
 
def date_day(value):
56
 
    return value.strftime('%Y-%m-%d')
57
 
 
58
 
 
59
 
def date_time(value):
60
 
    return value.strftime('%Y-%m-%d %T')
61
 
 
62
 
 
63
 
def _displaydate(date):
64
 
    delta = abs(datetime.datetime.now() - date)
65
 
    if delta > datetime.timedelta(1, 0, 0):
66
 
        # far in the past or future, display the date
67
 
        return 'on ' + date_day(date)
68
 
    return _approximatedate(date)
69
 
 
70
 
 
71
 
def _approximatedate(date):
72
 
    delta = datetime.datetime.now() - date
73
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
74
 
        # far in the past or future, display the date
75
 
        return date_day(date)
76
 
    future = delta < datetime.timedelta(0, 0, 0)
77
 
    delta = abs(delta)
78
 
    days = delta.days
79
 
    hours = delta.seconds / 3600
80
 
    minutes = (delta.seconds - (3600*hours)) / 60
81
 
    seconds = delta.seconds % 60
82
 
    result = ''
83
 
    if future:
84
 
        result += 'in '
85
 
    if days != 0:
86
 
        amount = days
87
 
        unit = 'day'
88
 
    elif hours != 0:
89
 
        amount = hours
90
 
        unit = 'hour'
91
 
    elif minutes != 0:
92
 
        amount = minutes
93
 
        unit = 'minute'
94
 
    else:
95
 
        amount = seconds
96
 
        unit = 'second'
97
 
    if amount != 1:
98
 
        unit += 's'
99
 
    result += '%s %s' % (amount, unit)
100
 
    if not future:
101
 
        result += ' ago'
102
 
        return result
103
 
 
104
 
 
105
 
def _wrap_with_date_time_title(date, formatted_date):
106
 
    elem = ET.Element("span")
107
 
    elem.text = formatted_date
108
 
    elem.set("title", date_time(date))
109
 
    return elem
110
 
 
111
 
 
112
 
def approximatedate(date):
113
 
    #FIXME: Returns an object instead of a string
114
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
115
 
 
116
 
 
117
 
def displaydate(date):
118
 
    return _wrap_with_date_time_title(date, _displaydate(date))
119
 
 
120
79
 
121
80
class Container (object):
122
81
    """
128
87
                setattr(self, key, value)
129
88
        for key, value in kw.iteritems():
130
89
            setattr(self, key, value)
131
 
 
 
90
    
132
91
    def __repr__(self):
133
92
        out = '{ '
134
93
        for key, value in self.__dict__.iteritems():
139
98
        return out
140
99
 
141
100
 
 
101
def clean_revid(revid):
 
102
    if revid == 'missing':
 
103
        return revid
 
104
    return sha.new(revid).hexdigest()
 
105
 
 
106
 
 
107
def obfuscate(text):
 
108
    return ''.join([ '&#%d;' % ord(c) for c in text ])
 
109
 
 
110
 
142
111
def trunc(text, limit=10):
143
112
    if len(text) <= limit:
144
113
        return text
145
114
    return text[:limit] + '...'
146
115
 
147
116
 
 
117
def to_utf8(s):
 
118
    if isinstance(s, unicode):
 
119
        return s.encode('utf-8')
 
120
    return s
 
121
 
 
122
 
148
123
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
149
124
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
150
125
 
167
142
        return '%s at %s' % (username, domains[-2])
168
143
    return '%s at %s' % (username, domains[0])
169
144
 
 
145
    
 
146
def triple_factors(min_value=1):
 
147
    factors = (1, 3)
 
148
    index = 0
 
149
    n = 1
 
150
    while True:
 
151
        if n >= min_value:
 
152
            yield n * factors[index]
 
153
        index += 1
 
154
        if index >= len(factors):
 
155
            index = 0
 
156
            n *= 10
 
157
 
 
158
 
 
159
def scan_range(pos, max, pagesize=1):
 
160
    """
 
161
    given a position in a maximum range, return a list of negative and positive
 
162
    jump factors for an hgweb-style triple-factor geometric scan.
 
163
    
 
164
    for example, with pos=20 and max=500, the range would be:
 
165
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
166
    
 
167
    i admit this is a very strange way of jumping through revisions.  i didn't
 
168
    invent it. :)
 
169
    """
 
170
    out = []
 
171
    for n in triple_factors(pagesize + 1):
 
172
        if n > max:
 
173
            return out
 
174
        if pos + n < max:
 
175
            out.append(n)
 
176
        if pos - n >= 0:
 
177
            out.insert(0, -n)
 
178
 
170
179
 
171
180
# only do this if unicode turns out to be a problem
172
181
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
173
182
 
174
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
175
183
def html_clean(s):
176
184
    """
177
185
    clean up a string for html display.  expand any tabs, encode any html
179
187
    in displaying monospace text.
180
188
    """
181
189
    s = cgi.escape(s.expandtabs())
 
190
#    s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
182
191
    s = s.replace(' ', '&nbsp;')
183
192
    return s
184
193
 
185
194
 
186
 
 
187
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
188
 
 
189
 
def fixed_width(s):
190
 
    """
191
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
192
 
    chop up the string.
193
 
    """
194
 
    if not isinstance(s, unicode):
195
 
        # this kinda sucks.  file contents are just binary data, and no
196
 
        # encoding metadata is stored, so we need to guess.  this is probably
197
 
        # okay for most code, but for people using things like KOI-8, this
198
 
        # will display gibberish.  we have no way of detecting the correct
199
 
        # encoding to use.
200
 
        try:
201
 
            s = s.decode('utf-8')
202
 
        except UnicodeDecodeError:
203
 
            s = s.decode('iso-8859-15')
204
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
205
 
 
206
 
 
207
195
def fake_permissions(kind, executable):
208
196
    # fake up unix-style permissions given only a "kind" and executable bit
209
197
    if kind == 'directory':
213
201
    return '-rw-r--r--'
214
202
 
215
203
 
 
204
def if_present(format, value):
 
205
    """
 
206
    format a value using a format string, if the value exists and is not None.
 
207
    """
 
208
    if value is None:
 
209
        return ''
 
210
    return format % value
 
211
 
 
212
 
216
213
def b64(s):
217
214
    s = base64.encodestring(s).replace('\n', '')
218
215
    while (len(s) > 0) and (s[-1] == '='):
253
250
        divisor = MEG
254
251
    else:
255
252
        divisor = KILO
256
 
 
 
253
    
257
254
    dot = size % divisor
258
255
    base = size - dot
259
256
    dot = dot * 10 // divisor
261
258
    if dot >= 10:
262
259
        base += 1
263
260
        dot -= 10
264
 
 
 
261
    
265
262
    out = str(base)
266
263
    if (base < 100) and (dot != 0):
267
264
        out += '.%d' % (dot,)
272
269
    elif divisor == GIG:
273
270
        out += 'G'
274
271
    return out
275
 
 
276
 
 
277
 
def fill_in_navigation(navigation):
 
272
    
 
273
 
 
274
def fill_in_navigation(history, navigation):
278
275
    """
279
276
    given a navigation block (used by the template for the page header), fill
280
277
    in useful calculated values.
281
278
    """
282
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
283
 
        navigation.position = navigation.revid_list.index(navigation.revid)
284
 
    else:
 
279
    navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
 
280
    if navigation.position is None:
285
281
        navigation.position = 0
286
282
    navigation.count = len(navigation.revid_list)
287
283
    navigation.page_position = navigation.position // navigation.pagesize + 1
288
284
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
289
 
 
 
285
    
290
286
    def get_offset(offset):
291
287
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
292
288
            return None
293
289
        return navigation.revid_list[navigation.position + offset]
294
 
 
 
290
    
295
291
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
296
292
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
297
 
    prev_page_revno = navigation.history.get_revno(
298
 
            navigation.prev_page_revid)
299
 
    next_page_revno = navigation.history.get_revno(
300
 
            navigation.next_page_revid)
301
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
302
 
 
303
 
    params = { 'filter_file_id': navigation.filter_file_id }
 
293
    
 
294
    params = { 'file_id': navigation.file_id }
304
295
    if getattr(navigation, 'query', None) is not None:
305
296
        params['q'] = navigation.query
306
 
 
307
 
    if getattr(navigation, 'start_revid', None) is not None:
308
 
        params['start_revid'] = start_revno
309
 
 
 
297
    else:
 
298
        params['start_revid'] = navigation.start_revid
 
299
        
310
300
    if navigation.prev_page_revid:
311
 
        navigation.prev_page_url = navigation.branch.context_url(
312
 
            [navigation.scan_url, prev_page_revno], **params)
 
301
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
313
302
    if navigation.next_page_revid:
314
 
        navigation.next_page_url = navigation.branch.context_url(
315
 
            [navigation.scan_url, next_page_revno], **params)
 
303
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **params)
 
304
 
 
305
 
 
306
def log_exception(log):
 
307
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
308
        log.debug(line)
316
309
 
317
310
 
318
311
def decorator(unbound):
335
328
    @decorator
336
329
    def _decorator(unbound):
337
330
        def locked(self, *args, **kw):
 
331
            #self.log.debug('-> %r lock %r', id(threading.currentThread()), debug_name)
338
332
            getattr(self, lockname).acquire()
339
333
            try:
340
334
                return unbound(self, *args, **kw)
341
335
            finally:
342
336
                getattr(self, lockname).release()
 
337
                #self.log.debug('<- %r unlock %r', id(threading.currentThread()), debug_name)
343
338
        return locked
344
339
    return _decorator
345
340
 
346
 
 
347
 
@decorator
348
 
def lsprof(f):
349
 
    def _f(*a, **kw):
350
 
        from loggerhead.lsprof import profile
351
 
        import cPickle
352
 
        z = time.time()
353
 
        ret, stats = profile(f, *a, **kw)
354
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
355
 
        stats.sort()
356
 
        stats.freeze()
357
 
        now = time.time()
358
 
        msec = int(now * 1000) % 1000
359
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
360
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
361
 
        cPickle.dump(stats, open(filename, 'w'), 2)
362
 
        return ret
363
 
    return _f
364
 
 
365
 
 
366
 
# just thinking out loud here...
367
 
#
368
 
# so, when browsing around, there are 5 pieces of context, most optional:
369
 
#     - current revid
370
 
#         current location along the navigation path (while browsing)
371
 
#     - starting revid (start_revid)
372
 
#         the current beginning of navigation (navigation continues back to
373
 
#         the original revision) -- this defines an 'alternate mainline'
374
 
#         when the user navigates into a branch.
375
 
#     - file_id
376
 
#         the file being looked at
377
 
#     - filter_file_id
378
 
#         if navigating the revisions that touched a file
379
 
#     - q (query)
380
 
#         if navigating the revisions that matched a search query
381
 
#     - remember
382
 
#         a previous revision to remember for future comparisons
383
 
#
384
 
# current revid is given on the url path.  the rest are optional components
385
 
# in the url params.
386
 
#
387
 
# other transient things can be set:
388
 
#     - compare_revid
389
 
#         to compare one revision to another, on /revision only
390
 
#     - sort
391
 
#         for re-ordering an existing page by different sort
392
 
 
393
 
t_context = threading.local()
394
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
395
 
          'compare_revid', 'sort')
396
 
 
397
 
 
398
 
def set_context(map):
399
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
400
 
 
401
 
 
402
 
def get_context(**overrides):
403
 
    """
404
 
    Soon to be deprecated.
405
 
 
406
 
 
407
 
    return a context map that may be overriden by specific values passed in,
408
 
    but only contains keys from the list of valid context keys.
409
 
 
410
 
    if 'clear' is set, only the 'remember' context value will be added, and
411
 
    all other context will be omitted.
412
 
    """
413
 
    map = dict()
414
 
    if overrides.get('clear', False):
415
 
        map['remember'] = t_context.map.get('remember', None)
416
 
    else:
417
 
        map.update(t_context.map)
418
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
419
 
    map.update(overrides)
420
 
    return map