~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Matt Nordhoff
  • Date: 2009-05-13 11:34:09 UTC
  • Revision ID: mnordhoff@mattnordhoff.com-20090513113409-jd0el9zmtdae0foe
Rename BranchWSGIApp.branch_url to work around bug #375948

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