~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robey Pointer
  • Date: 2006-12-19 09:52:38 UTC
  • Revision ID: robey@lag.net-20061219095238-eq9zix5ipyyymcaj
remove debugging line

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
2
 
# Copyright (C) 2008  Canonical Ltd.
3
 
#                     (Authored by Martin Albisetti <argentina@gmail.com)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
4
#
19
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
18
#
21
19
 
22
 
try:
23
 
    from xml.etree import ElementTree as ET
24
 
except ImportError:
25
 
    from elementtree import ElementTree as ET
26
 
 
27
 
from simpletal.simpleTALUtils import HTMLStructureCleaner
28
 
 
29
 
import base64
30
20
import cgi
31
21
import datetime
32
22
import logging
33
23
import re
34
 
import struct
 
24
import sha
 
25
import sys
35
26
import threading
36
 
import time
37
 
import sys
38
 
import os
39
 
import subprocess
 
27
import traceback
 
28
 
 
29
import turbogears
 
30
 
40
31
 
41
32
log = logging.getLogger("loggerhead.controllers")
42
33
 
43
34
 
 
35
def timespan(delta):
 
36
    if delta.days > 730:
 
37
        # good grief!
 
38
        return '%d years' % (int(delta.days // 365.25),)
 
39
    if delta.days >= 3:
 
40
        return '%d days' % delta.days
 
41
    seg = []
 
42
    if delta.days > 0:
 
43
        if delta.days == 1:
 
44
            seg.append('1 day')
 
45
        else:
 
46
            seg.append('%d days' % delta.days)
 
47
    hrs = delta.seconds // 3600
 
48
    mins = (delta.seconds % 3600) // 60
 
49
    if hrs > 0:
 
50
        if hrs == 1:
 
51
            seg.append('1 hour')
 
52
        else:
 
53
            seg.append('%d hours' % hrs)
 
54
    if delta.days == 0:
 
55
        if mins > 0:
 
56
            if mins == 1:
 
57
                seg.append('1 minute')
 
58
            else:
 
59
                seg.append('%d minutes' % mins)
 
60
        elif hrs == 0:
 
61
            seg.append('less than a minute')
 
62
    return ', '.join(seg)
 
63
 
 
64
 
 
65
def ago(timestamp):
 
66
    now = datetime.datetime.now()
 
67
    return timespan(now - timestamp) + ' ago'
 
68
 
 
69
 
44
70
def fix_year(year):
45
71
    if year < 70:
46
72
        year += 2000
48
74
        year += 1900
49
75
    return year
50
76
 
51
 
# Display of times.
52
 
 
53
 
# date_day -- just the day
54
 
# date_time -- full date with time
55
 
#
56
 
# displaydate -- for use in sentences
57
 
# approximatedate -- for use in tables
58
 
#
59
 
# displaydate and approximatedate return an elementtree <span> Element
60
 
# with the full date in a tooltip.
61
 
 
62
 
 
63
 
def date_day(value):
64
 
    return value.strftime('%Y-%m-%d')
65
 
 
66
 
 
67
 
def date_time(value):
68
 
    if value is not None:
69
 
        return value.strftime('%Y-%m-%d %T')
70
 
    else:
71
 
        return 'N/A'
72
 
 
73
 
 
74
 
def _displaydate(date):
75
 
    delta = abs(datetime.datetime.now() - date)
76
 
    if delta > datetime.timedelta(1, 0, 0):
77
 
        # far in the past or future, display the date
78
 
        return 'on ' + date_day(date)
79
 
    return _approximatedate(date)
80
 
 
81
 
 
82
 
def _approximatedate(date):
83
 
    delta = datetime.datetime.now() - date
84
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
85
 
        # far in the past or future, display the date
86
 
        return date_day(date)
87
 
    future = delta < datetime.timedelta(0, 0, 0)
88
 
    delta = abs(delta)
89
 
    days = delta.days
90
 
    hours = delta.seconds / 3600
91
 
    minutes = (delta.seconds - (3600*hours)) / 60
92
 
    seconds = delta.seconds % 60
93
 
    result = ''
94
 
    if future:
95
 
        result += 'in '
96
 
    if days != 0:
97
 
        amount = days
98
 
        unit = 'day'
99
 
    elif hours != 0:
100
 
        amount = hours
101
 
        unit = 'hour'
102
 
    elif minutes != 0:
103
 
        amount = minutes
104
 
        unit = 'minute'
105
 
    else:
106
 
        amount = seconds
107
 
        unit = 'second'
108
 
    if amount != 1:
109
 
        unit += 's'
110
 
    result += '%s %s' % (amount, unit)
111
 
    if not future:
112
 
        result += ' ago'
113
 
        return result
114
 
 
115
 
 
116
 
def _wrap_with_date_time_title(date, formatted_date):
117
 
    elem = ET.Element("span")
118
 
    elem.text = formatted_date
119
 
    elem.set("title", date_time(date))
120
 
    return elem
121
 
 
122
 
 
123
 
def approximatedate(date):
124
 
    #FIXME: Returns an object instead of a string
125
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
126
 
 
127
 
 
128
 
def displaydate(date):
129
 
    return _wrap_with_date_time_title(date, _displaydate(date))
130
 
 
131
77
 
132
78
class Container (object):
133
79
    """
134
80
    Convert a dict into an object with attributes.
135
81
    """
136
 
 
137
82
    def __init__(self, _dict=None, **kw):
138
83
        if _dict is not None:
139
84
            for key, value in _dict.iteritems():
140
85
                setattr(self, key, value)
141
86
        for key, value in kw.iteritems():
142
87
            setattr(self, key, value)
143
 
 
 
88
    
144
89
    def __repr__(self):
145
90
        out = '{ '
146
91
        for key, value in self.__dict__.iteritems():
147
 
            if key.startswith('_') or (getattr(self.__dict__[key],
148
 
                                       '__call__', None) is not None):
 
92
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
149
93
                continue
150
94
            out += '%r => %r, ' % (key, value)
151
95
        out += '}'
152
96
        return out
153
97
 
154
98
 
 
99
def clean_revid(revid):
 
100
    if revid == 'missing':
 
101
        return revid
 
102
    return sha.new(revid).hexdigest()
 
103
 
 
104
 
 
105
def obfuscate(text):
 
106
    return ''.join([ '&#%d;' % ord(c) for c in text ])
 
107
 
 
108
 
155
109
def trunc(text, limit=10):
156
110
    if len(text) <= limit:
157
111
        return text
158
112
    return text[:limit] + '...'
159
113
 
160
114
 
 
115
def to_utf8(s):
 
116
    if isinstance(s, unicode):
 
117
        return s.encode('utf-8')
 
118
    return s
 
119
 
 
120
 
161
121
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
162
122
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
163
123
 
164
 
 
165
124
def hide_email(email):
166
125
    """
167
126
    try to obsure any email address in a bazaar committer's name.
181
140
        return '%s at %s' % (username, domains[-2])
182
141
    return '%s at %s' % (username, domains[0])
183
142
 
 
143
    
 
144
def triple_factors(min_value=1):
 
145
    factors = (1, 3)
 
146
    index = 0
 
147
    n = 1
 
148
    while True:
 
149
        if n >= min_value:
 
150
            yield n * factors[index]
 
151
        index += 1
 
152
        if index >= len(factors):
 
153
            index = 0
 
154
            n *= 10
 
155
 
 
156
 
 
157
def scan_range(pos, max, pagesize=1):
 
158
    """
 
159
    given a position in a maximum range, return a list of negative and positive
 
160
    jump factors for an hgweb-style triple-factor geometric scan.
 
161
    
 
162
    for example, with pos=20 and max=500, the range would be:
 
163
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
164
    
 
165
    i admit this is a very strange way of jumping through revisions.  i didn't
 
166
    invent it. :)
 
167
    """
 
168
    out = []
 
169
    for n in triple_factors(pagesize + 1):
 
170
        if n > max:
 
171
            return out
 
172
        if pos + n < max:
 
173
            out.append(n)
 
174
        if pos - n >= 0:
 
175
            out.insert(0, -n)
 
176
 
184
177
 
185
178
# only do this if unicode turns out to be a problem
186
179
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
187
180
 
188
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
189
 
 
190
 
 
191
181
def html_clean(s):
192
182
    """
193
183
    clean up a string for html display.  expand any tabs, encode any html
195
185
    in displaying monospace text.
196
186
    """
197
187
    s = cgi.escape(s.expandtabs())
 
188
#    s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
198
189
    s = s.replace(' ', '&nbsp;')
199
190
    return s
200
191
 
201
192
 
202
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
203
 
 
204
 
 
205
 
def fill_div(s):
206
 
    """
207
 
    CSS is stupid. In some cases we need to replace an empty value with
208
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
209
 
 
210
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
211
 
    """
212
 
 
213
 
 
214
 
    if s is None:
215
 
        return '&nbsp;'
216
 
    elif isinstance(s, int):
217
 
        return s
218
 
    elif not s.strip():
219
 
        return '&nbsp;'
220
 
    else:
221
 
        try:
222
 
            s = s.decode('utf-8')
223
 
        except UnicodeDecodeError:
224
 
            s = s.decode('iso-8859-15')
225
 
        return s
226
 
 
227
 
HSC = HTMLStructureCleaner()
228
 
 
229
 
def fixed_width(s):
230
 
    """
231
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
232
 
    chop up the string.
233
 
    """
234
 
    if not isinstance(s, unicode):
235
 
        # this kinda sucks.  file contents are just binary data, and no
236
 
        # encoding metadata is stored, so we need to guess.  this is probably
237
 
        # okay for most code, but for people using things like KOI-8, this
238
 
        # will display gibberish.  we have no way of detecting the correct
239
 
        # encoding to use.
240
 
        try:
241
 
            s = s.decode('utf-8')
242
 
        except UnicodeDecodeError:
243
 
            s = s.decode('iso-8859-15')
244
 
 
245
 
    s = s.expandtabs().replace(' ', NONBREAKING_SPACE)
246
 
 
247
 
    return HSC.clean(s).replace('\n', '<br/>')
248
 
 
249
 
 
250
193
def fake_permissions(kind, executable):
251
194
    # fake up unix-style permissions given only a "kind" and executable bit
252
195
    if kind == 'directory':
256
199
    return '-rw-r--r--'
257
200
 
258
201
 
259
 
def b64(s):
260
 
    s = base64.encodestring(s).replace('\n', '')
261
 
    while (len(s) > 0) and (s[-1] == '='):
262
 
        s = s[:-1]
263
 
    return s
264
 
 
265
 
 
266
 
def uniq(uniqs, s):
267
 
    """
268
 
    turn a potentially long string into a unique smaller string.
269
 
    """
270
 
    if s in uniqs:
271
 
        return uniqs[s]
272
 
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
273
 
    x = struct.pack('>I', next)
274
 
    while (len(x) > 1) and (x[0] == '\x00'):
275
 
        x = x[1:]
276
 
    uniqs[s] = b64(x)
277
 
    return uniqs[s]
 
202
def if_present(format, value):
 
203
    """
 
204
    format a value using a format string, if the value exists and is not None.
 
205
    """
 
206
    if value is None:
 
207
        return ''
 
208
    return format % value
278
209
 
279
210
 
280
211
KILO = 1024
283
214
P95_MEG = int(0.9 * MEG)
284
215
P95_GIG = int(0.9 * GIG)
285
216
 
286
 
 
287
217
def human_size(size, min_divisor=0):
288
218
    size = int(size)
289
219
    if (size == 0) and (min_divisor == 0):
297
227
        divisor = MEG
298
228
    else:
299
229
        divisor = KILO
300
 
 
 
230
    
301
231
    dot = size % divisor
302
232
    base = size - dot
303
233
    dot = dot * 10 // divisor
305
235
    if dot >= 10:
306
236
        base += 1
307
237
        dot -= 10
308
 
 
 
238
    
309
239
    out = str(base)
310
240
    if (base < 100) and (dot != 0):
311
 
        out += '.%d' % (dot)
 
241
        out += '.%d' % (dot,)
312
242
    if divisor == KILO:
313
243
        out += 'K'
314
244
    elif divisor == MEG:
316
246
    elif divisor == GIG:
317
247
        out += 'G'
318
248
    return out
319
 
 
320
 
 
321
 
def fill_in_navigation(navigation):
 
249
    
 
250
 
 
251
def fill_in_navigation(history, navigation):
322
252
    """
323
253
    given a navigation block (used by the template for the page header), fill
324
254
    in useful calculated values.
325
255
    """
326
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
327
 
        navigation.position = navigation.revid_list.index(navigation.revid)
328
 
    else:
 
256
    navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
 
257
    if navigation.position is None:
329
258
        navigation.position = 0
330
259
    navigation.count = len(navigation.revid_list)
331
260
    navigation.page_position = navigation.position // navigation.pagesize + 1
332
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
333
 
 - 1)) // navigation.pagesize
334
 
 
 
261
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
262
    
335
263
    def get_offset(offset):
336
 
        if (navigation.position + offset < 0) or (
337
 
           navigation.position + offset > navigation.count - 1):
 
264
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
338
265
            return None
339
266
        return navigation.revid_list[navigation.position + offset]
340
 
 
341
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
 
267
    
342
268
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
343
269
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
344
 
    prev_page_revno = navigation.history.get_revno(
345
 
            navigation.prev_page_revid)
346
 
    next_page_revno = navigation.history.get_revno(
347
 
            navigation.next_page_revid)
348
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
349
 
 
350
 
    params = {'filter_file_id': navigation.filter_file_id}
 
270
    
 
271
    params = { 'file_id': navigation.file_id }
351
272
    if getattr(navigation, 'query', None) is not None:
352
273
        params['q'] = navigation.query
353
 
 
354
 
    if getattr(navigation, 'start_revid', None) is not None:
355
 
        params['start_revid'] = start_revno
356
 
 
 
274
    else:
 
275
        params['start_revid'] = navigation.start_revid
 
276
        
357
277
    if navigation.prev_page_revid:
358
 
        navigation.prev_page_url = navigation.branch.context_url(
359
 
            [navigation.scan_url, prev_page_revno], **params)
 
278
        navigation.prev_page_url = turbogears.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
360
279
    if navigation.next_page_revid:
361
 
        navigation.next_page_url = navigation.branch.context_url(
362
 
            [navigation.scan_url, next_page_revno], **params)
363
 
 
364
 
 
365
 
def directory_breadcrumbs(path, is_root, view):
366
 
    """
367
 
    Generate breadcrumb information from the directory path given
368
 
 
369
 
    The path given should be a path up to any branch that is currently being
370
 
    served
371
 
 
372
 
    Arguments:
373
 
    path -- The path to convert into breadcrumbs
374
 
    is_root -- Whether or not loggerhead is serving a branch at its root
375
 
    view -- The type of view we are showing (files, changes etc)
376
 
    """
377
 
    # Is our root directory itself a branch?
378
 
    if is_root:
379
 
        if view == 'directory':
380
 
            directory = 'files'
381
 
        breadcrumbs = [{
382
 
            'dir_name': path,
383
 
            'path': '',
384
 
            'suffix': view,
385
 
        }]
386
 
    else:
387
 
        # Create breadcrumb trail for the path leading up to the branch
388
 
        breadcrumbs = [{
389
 
            'dir_name': "(root)",
390
 
            'path': '',
391
 
            'suffix': '',
392
 
        }]
393
 
        if path != '/':
394
 
            dir_parts = path.strip('/').split('/')
395
 
            for index, dir_name in enumerate(dir_parts):
396
 
                breadcrumbs.append({
397
 
                    'dir_name': dir_name,
398
 
                    'path': '/'.join(dir_parts[:index + 1]),
399
 
                    'suffix': '',
400
 
                })
401
 
            # If we are not in the directory view, the last crumb is a branch,
402
 
            # so we need to specify a view
403
 
            if view != 'directory':
404
 
                breadcrumbs[-1]['suffix'] = '/' + view
405
 
    return breadcrumbs
406
 
 
407
 
 
408
 
def branch_breadcrumbs(path, inv, view):
409
 
    """
410
 
    Generate breadcrumb information from the branch path given
411
 
 
412
 
    The path given should be a path that exists within a branch
413
 
 
414
 
    Arguments:
415
 
    path -- The path to convert into breadcrumbs
416
 
    inv -- Inventory to get file information from
417
 
    view -- The type of view we are showing (files, changes etc)
418
 
    """
419
 
    dir_parts = path.strip('/').split('/')
420
 
    inner_breadcrumbs = []
421
 
    for index, dir_name in enumerate(dir_parts):
422
 
        inner_breadcrumbs.append({
423
 
            'dir_name': dir_name,
424
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
425
 
            'suffix': '/' + view,
426
 
        })
427
 
    return inner_breadcrumbs
 
280
        navigation.next_page_url = turbogears.url([ navigation.scan_url, navigation.next_page_revid ], **params)
 
281
 
 
282
 
 
283
def log_exception(log):
 
284
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
285
        log.debug(line)
428
286
 
429
287
 
430
288
def decorator(unbound):
431
 
 
432
289
    def new_decorator(f):
433
290
        g = unbound(f)
434
291
        g.__name__ = f.__name__
441
298
    return new_decorator
442
299
 
443
300
 
444
 
# common threading-lock decorator
445
 
 
446
 
 
447
 
def with_lock(lockname, debug_name=None):
448
 
    if debug_name is None:
449
 
        debug_name = lockname
450
 
 
451
 
    @decorator
452
 
    def _decorator(unbound):
453
 
 
454
 
        def locked(self, *args, **kw):
455
 
            getattr(self, lockname).acquire()
456
 
            try:
457
 
                return unbound(self, *args, **kw)
458
 
            finally:
459
 
                getattr(self, lockname).release()
460
 
        return locked
461
 
    return _decorator
462
 
 
463
 
 
464
 
@decorator
465
 
def lsprof(f):
466
 
 
467
 
    def _f(*a, **kw):
468
 
        from loggerhead.lsprof import profile
469
 
        import cPickle
470
 
        z = time.time()
471
 
        ret, stats = profile(f, *a, **kw)
472
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
473
 
            int((time.time() - z) * 1000)))
474
 
        stats.sort()
475
 
        stats.freeze()
476
 
        now = time.time()
477
 
        msec = int(now * 1000) % 1000
478
 
        timestr = time.strftime('%Y%m%d%H%M%S',
479
 
                                time.localtime(now)) + ('%03d' % msec)
480
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
481
 
        cPickle.dump(stats, open(filename, 'w'), 2)
482
 
        return ret
483
 
    return _f
484
 
 
485
 
 
486
 
# just thinking out loud here...
487
 
#
488
 
# so, when browsing around, there are 5 pieces of context, most optional:
489
 
#     - current revid
490
 
#         current location along the navigation path (while browsing)
491
 
#     - starting revid (start_revid)
492
 
#         the current beginning of navigation (navigation continues back to
493
 
#         the original revision) -- this defines an 'alternate mainline'
494
 
#         when the user navigates into a branch.
495
 
#     - file_id
496
 
#         the file being looked at
497
 
#     - filter_file_id
498
 
#         if navigating the revisions that touched a file
499
 
#     - q (query)
500
 
#         if navigating the revisions that matched a search query
501
 
#     - remember
502
 
#         a previous revision to remember for future comparisons
503
 
#
504
 
# current revid is given on the url path.  the rest are optional components
505
 
# in the url params.
506
 
#
507
 
# other transient things can be set:
508
 
#     - compare_revid
509
 
#         to compare one revision to another, on /revision only
510
 
#     - sort
511
 
#         for re-ordering an existing page by different sort
512
 
 
513
 
t_context = threading.local()
514
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
515
 
          'compare_revid', 'sort')
516
 
 
517
 
 
518
 
def set_context(map):
519
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
520
 
 
521
 
 
522
 
def get_context(**overrides):
523
 
    """
524
 
    Soon to be deprecated.
525
 
 
526
 
 
527
 
    return a context map that may be overriden by specific values passed in,
528
 
    but only contains keys from the list of valid context keys.
529
 
 
530
 
    if 'clear' is set, only the 'remember' context value will be added, and
531
 
    all other context will be omitted.
532
 
    """
533
 
    map = dict()
534
 
    if overrides.get('clear', False):
535
 
        map['remember'] = t_context.map.get('remember', None)
536
 
    else:
537
 
        map.update(t_context.map)
538
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
539
 
    map.update(overrides)
540
 
    return map
541
 
 
542
 
 
543
 
class Reloader(object):
544
 
    """
545
 
    This class wraps all paste.reloader logic. All methods are @classmethod.
546
 
    """
547
 
 
548
 
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
549
 
 
550
 
    @classmethod
551
 
    def _turn_sigterm_into_systemexit(self):
552
 
        """
553
 
        Attempts to turn a SIGTERM exception into a SystemExit exception.
554
 
        """
555
 
        try:
556
 
            import signal
557
 
        except ImportError:
558
 
            return
559
 
 
560
 
        def handle_term(signo, frame):
561
 
            raise SystemExit
562
 
        signal.signal(signal.SIGTERM, handle_term)
563
 
 
564
 
    @classmethod
565
 
    def is_installed(self):
566
 
        return os.environ.get(self._reloader_environ_key)
567
 
 
568
 
    @classmethod
569
 
    def install(self):
570
 
        from paste import reloader
571
 
        reloader.install(int(1))
572
 
 
573
 
    @classmethod
574
 
    def restart_with_reloader(self):
575
 
        """Based on restart_with_monitor from paste.script.serve."""
576
 
        print 'Starting subprocess with file monitor'
577
 
        while 1:
578
 
            args = [sys.executable] + sys.argv
579
 
            new_environ = os.environ.copy()
580
 
            new_environ[self._reloader_environ_key] = 'true'
581
 
            proc = None
582
 
            try:
583
 
                try:
584
 
                    self._turn_sigterm_into_systemexit()
585
 
                    proc = subprocess.Popen(args, env=new_environ)
586
 
                    exit_code = proc.wait()
587
 
                    proc = None
588
 
                except KeyboardInterrupt:
589
 
                    print '^C caught in monitor process'
590
 
                    return 1
591
 
            finally:
592
 
                if (proc is not None
593
 
                    and hasattr(os, 'kill')):
594
 
                    import signal
595
 
                    try:
596
 
                        os.kill(proc.pid, signal.SIGTERM)
597
 
                    except (OSError, IOError):
598
 
                        pass
599
 
 
600
 
            # Reloader always exits with code 3; but if we are
601
 
            # a monitor, any exit code will restart
602
 
            if exit_code != 3:
603
 
                return exit_code
604
 
            print '-'*20, 'Restarting', '-'*20
 
301
# global branch history & cache
 
302
 
 
303
_history = None
 
304
_history_lock = threading.RLock()
 
305
_index = None
 
306
 
 
307
def get_history():
 
308
    global _history, _index
 
309
    from loggerhead.history import History
 
310
    
 
311
    config = get_config()
 
312
    
 
313
    _history_lock.acquire()
 
314
    try:
 
315
        if (_history is None) or _history.out_of_date():
 
316
            log.debug('Reload branch history...')
 
317
            if _history is not None:
 
318
                _history.dont_use_cache()
 
319
            _history = History.from_folder(config.get('folder'))
 
320
            _history.use_cache(config.get('cachepath'))
 
321
        return _history
 
322
    finally:
 
323
        _history_lock.release()
 
324
 
 
325
def get_index():
 
326
    global _index
 
327
    from loggerhead.textindex import TextIndex
 
328
 
 
329
    config = get_config()
 
330
    cachepath = config.get('cachepath', None)
 
331
    if cachepath is None:
 
332
        return None
 
333
    _history_lock.acquire()
 
334
    try:
 
335
        if _index is None:
 
336
            _index = TextIndex(get_history(), config.get('cachepath'))
 
337
        return _index
 
338
    finally:
 
339
        _history_lock.release()
 
340
 
 
341
 
 
342
_config = None
 
343
 
 
344
def set_config(config):
 
345
    global _config
 
346
    _config = config
 
347
 
 
348
def get_config():
 
349
    return _config
 
350