~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Michael Hudson
  • Date: 2007-11-19 08:55:10 UTC
  • mto: This revision was merged to the branch mainline in revision 142.
  • Revision ID: michael.hudson@canonical.com-20071119085510-ossv2rsqdcg7ikzh
oops

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
#
24
22
import datetime
25
23
import logging
26
24
import re
 
25
import sha
27
26
import struct
 
27
import sys
28
28
import threading
29
29
import time
30
 
import sys
31
 
import os
32
 
import subprocess
33
 
 
34
 
try:
35
 
    from xml.etree import ElementTree as ET
36
 
except ImportError:
37
 
    from elementtree import ElementTree as ET
38
 
 
39
 
from bzrlib import urlutils
40
 
 
41
 
from simpletal.simpleTALUtils import HTMLStructureCleaner
 
30
import traceback
 
31
 
 
32
import turbogears
 
33
 
42
34
 
43
35
log = logging.getLogger("loggerhead.controllers")
44
36
 
45
37
 
 
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
 
46
73
def fix_year(year):
47
74
    if year < 70:
48
75
        year += 2000
50
77
        year += 1900
51
78
    return year
52
79
 
53
 
# Display of times.
54
 
 
55
 
# date_day -- just the day
56
 
# date_time -- full date with time
57
 
#
58
 
# displaydate -- for use in sentences
59
 
# approximatedate -- for use in tables
60
 
#
61
 
# displaydate and approximatedate return an elementtree <span> Element
62
 
# with the full date in a tooltip.
63
 
 
64
 
 
65
 
def date_day(value):
66
 
    return value.strftime('%Y-%m-%d')
67
 
 
68
 
 
69
 
def date_time(value):
70
 
    if value is not None:
71
 
        return value.strftime('%Y-%m-%d %H:%M:%S')
72
 
    else:
73
 
        return 'N/A'
74
 
 
75
 
 
76
 
def _displaydate(date):
77
 
    delta = abs(datetime.datetime.now() - date)
78
 
    if delta > datetime.timedelta(1, 0, 0):
79
 
        # far in the past or future, display the date
80
 
        return 'on ' + date_day(date)
81
 
    return _approximatedate(date)
82
 
 
83
 
 
84
 
def _approximatedate(date):
 
80
 
 
81
_g_format = '%Y-%m-%d @ %H:%M'
 
82
 
 
83
def format_date(date):
 
84
    if _g_format == 'fancy':
 
85
        return fancy_format_date(date)
 
86
    return date.strftime(_g_format)
 
87
 
 
88
def fancy_format_date(date):
85
89
    delta = datetime.datetime.now() - date
86
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
87
 
        # far in the past or future, display the date
88
 
        return date_day(date)
89
 
    future = delta < datetime.timedelta(0, 0, 0)
90
 
    delta = abs(delta)
91
 
    days = delta.days
92
 
    hours = delta.seconds / 3600
93
 
    minutes = (delta.seconds - (3600*hours)) / 60
94
 
    seconds = delta.seconds % 60
95
 
    result = ''
96
 
    if future:
97
 
        result += 'in '
98
 
    if days != 0:
99
 
        amount = days
100
 
        unit = 'day'
101
 
    elif hours != 0:
102
 
        amount = hours
103
 
        unit = 'hour'
104
 
    elif minutes != 0:
105
 
        amount = minutes
106
 
        unit = 'minute'
 
90
    if delta.days > 300:
 
91
        return date.strftime('%d %b %Y')
107
92
    else:
108
 
        amount = seconds
109
 
        unit = 'second'
110
 
    if amount != 1:
111
 
        unit += 's'
112
 
    result += '%s %s' % (amount, unit)
113
 
    if not future:
114
 
        result += ' ago'
115
 
        return result
116
 
 
117
 
 
118
 
def _wrap_with_date_time_title(date, formatted_date):
119
 
    elem = ET.Element("span")
120
 
    elem.text = formatted_date
121
 
    elem.set("title", date_time(date))
122
 
    return elem
123
 
 
124
 
 
125
 
def approximatedate(date):
126
 
    #FIXME: Returns an object instead of a string
127
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
128
 
 
129
 
 
130
 
def displaydate(date):
131
 
    return _wrap_with_date_time_title(date, _displaydate(date))
132
 
 
133
 
 
134
 
class Container(object):
 
93
        return date.strftime('%d %b %H:%M')
 
94
 
 
95
def set_date_format(format):
 
96
    global _g_format
 
97
    _g_format = format
 
98
 
 
99
 
 
100
class Container (object):
135
101
    """
136
102
    Convert a dict into an object with attributes.
137
103
    """
138
 
 
139
104
    def __init__(self, _dict=None, **kw):
140
 
        self._properties = {}
141
105
        if _dict is not None:
142
106
            for key, value in _dict.iteritems():
143
107
                setattr(self, key, value)
144
108
        for key, value in kw.iteritems():
145
109
            setattr(self, key, value)
146
 
 
 
110
    
147
111
    def __repr__(self):
148
112
        out = '{ '
149
113
        for key, value in self.__dict__.iteritems():
150
 
            if key.startswith('_') or (getattr(self.__dict__[key],
151
 
                                       '__call__', None) is not None):
 
114
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
152
115
                continue
153
116
            out += '%r => %r, ' % (key, value)
154
117
        out += '}'
155
118
        return out
156
119
 
157
 
    def __getattr__(self, attr):
158
 
        """Used for handling things that aren't already available."""
159
 
        if attr.startswith('_') or attr not in self._properties:
160
 
            raise AttributeError('No attribute: %s' % (attr,))
161
 
        val = self._properties[attr](self, attr)
162
 
        setattr(self, attr, val)
163
 
        return val
164
 
 
165
 
    def _set_property(self, attr, prop_func):
166
 
        """Set a function that will be called when an attribute is desired.
167
 
 
168
 
        We will cache the return value, so the function call should be
169
 
        idempotent. We will pass 'self' and the 'attr' name when triggered.
170
 
        """
171
 
        if attr.startswith('_'):
172
 
            raise ValueError("Cannot create properties that start with _")
173
 
        self._properties[attr] = prop_func
 
120
 
 
121
def clean_revid(revid):
 
122
    if revid == 'missing':
 
123
        return revid
 
124
    return sha.new(revid).hexdigest()
 
125
 
 
126
 
 
127
def obfuscate(text):
 
128
    return ''.join([ '&#%d;' % ord(c) for c in text ])
174
129
 
175
130
 
176
131
def trunc(text, limit=10):
179
134
    return text[:limit] + '...'
180
135
 
181
136
 
 
137
def to_utf8(s):
 
138
    if isinstance(s, unicode):
 
139
        return s.encode('utf-8')
 
140
    return s
 
141
 
 
142
 
182
143
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
183
144
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
184
145
 
185
 
 
186
146
def hide_email(email):
187
147
    """
188
148
    try to obsure any email address in a bazaar committer's name.
202
162
        return '%s at %s' % (username, domains[-2])
203
163
    return '%s at %s' % (username, domains[0])
204
164
 
205
 
def hide_emails(emails):
206
 
    """
207
 
    try to obscure any email address in a list of bazaar committers' names.
208
 
    """
209
 
    result = []
210
 
    for email in emails:
211
 
        result.append(hide_email(email))
212
 
    return result
 
165
    
 
166
def triple_factors(min_value=1):
 
167
    factors = (1, 3)
 
168
    index = 0
 
169
    n = 1
 
170
    while True:
 
171
        if n >= min_value:
 
172
            yield n * factors[index]
 
173
        index += 1
 
174
        if index >= len(factors):
 
175
            index = 0
 
176
            n *= 10
 
177
 
 
178
 
 
179
def scan_range(pos, max, pagesize=1):
 
180
    """
 
181
    given a position in a maximum range, return a list of negative and positive
 
182
    jump factors for an hgweb-style triple-factor geometric scan.
 
183
    
 
184
    for example, with pos=20 and max=500, the range would be:
 
185
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
186
    
 
187
    i admit this is a very strange way of jumping through revisions.  i didn't
 
188
    invent it. :)
 
189
    """
 
190
    out = []
 
191
    for n in triple_factors(pagesize + 1):
 
192
        if n > max:
 
193
            return out
 
194
        if pos + n < max:
 
195
            out.append(n)
 
196
        if pos - n >= 0:
 
197
            out.insert(0, -n)
 
198
 
213
199
 
214
200
# only do this if unicode turns out to be a problem
215
201
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
216
202
 
217
203
# FIXME: get rid of this method; use fixed_width() and avoid XML().
218
 
 
219
 
 
220
204
def html_clean(s):
221
205
    """
222
206
    clean up a string for html display.  expand any tabs, encode any html
228
212
    return s
229
213
 
230
214
 
 
215
 
231
216
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
232
217
 
233
 
 
234
 
def fill_div(s):
235
 
    """
236
 
    CSS is stupid. In some cases we need to replace an empty value with
237
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
238
 
 
239
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
240
 
    """
241
 
    if s is None:
242
 
        return '&nbsp;'
243
 
    elif isinstance(s, int):
244
 
        return s
245
 
    elif not s.strip():
246
 
        return '&nbsp;'
247
 
    else:
248
 
        try:
249
 
            s = s.decode('utf-8')
250
 
        except UnicodeDecodeError:
251
 
            s = s.decode('iso-8859-15')
252
 
        return s
253
 
 
254
 
HSC = HTMLStructureCleaner()
255
 
 
256
218
def fixed_width(s):
257
219
    """
258
220
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
268
230
            s = s.decode('utf-8')
269
231
        except UnicodeDecodeError:
270
232
            s = s.decode('iso-8859-15')
271
 
 
272
 
    s = cgi.escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
273
 
 
274
 
    return HSC.clean(s).replace('\n', '<br/>')
 
233
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
275
234
 
276
235
 
277
236
def fake_permissions(kind, executable):
283
242
    return '-rw-r--r--'
284
243
 
285
244
 
 
245
def if_present(format, value):
 
246
    """
 
247
    format a value using a format string, if the value exists and is not None.
 
248
    """
 
249
    if value is None:
 
250
        return ''
 
251
    return format % value
 
252
 
 
253
 
286
254
def b64(s):
287
255
    s = base64.encodestring(s).replace('\n', '')
288
256
    while (len(s) > 0) and (s[-1] == '='):
310
278
P95_MEG = int(0.9 * MEG)
311
279
P95_GIG = int(0.9 * GIG)
312
280
 
313
 
 
314
281
def human_size(size, min_divisor=0):
315
282
    size = int(size)
316
283
    if (size == 0) and (min_divisor == 0):
324
291
        divisor = MEG
325
292
    else:
326
293
        divisor = KILO
327
 
 
 
294
    
328
295
    dot = size % divisor
329
296
    base = size - dot
330
297
    dot = dot * 10 // divisor
332
299
    if dot >= 10:
333
300
        base += 1
334
301
        dot -= 10
335
 
 
 
302
    
336
303
    out = str(base)
337
304
    if (base < 100) and (dot != 0):
338
305
        out += '.%d' % (dot,)
343
310
    elif divisor == GIG:
344
311
        out += 'G'
345
312
    return out
346
 
 
347
 
 
348
 
def local_path_from_url(url):
349
 
    """Convert Bazaar URL to local path, ignoring readonly+ prefix"""
350
 
    readonly_prefix = 'readonly+'
351
 
    if url.startswith(readonly_prefix):
352
 
        url = url[len(readonly_prefix):]
353
 
    return urlutils.local_path_from_url(url)
354
 
 
 
313
    
355
314
 
356
315
def fill_in_navigation(navigation):
357
316
    """
364
323
        navigation.position = 0
365
324
    navigation.count = len(navigation.revid_list)
366
325
    navigation.page_position = navigation.position // navigation.pagesize + 1
367
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
368
 
 - 1)) // navigation.pagesize
369
 
 
 
326
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
327
    
370
328
    def get_offset(offset):
371
 
        if (navigation.position + offset < 0) or (
372
 
           navigation.position + offset > navigation.count - 1):
 
329
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
373
330
            return None
374
331
        return navigation.revid_list[navigation.position + offset]
375
 
 
376
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
 
332
    
377
333
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
378
334
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
379
 
    prev_page_revno = navigation.history.get_revno(
380
 
            navigation.prev_page_revid)
381
 
    next_page_revno = navigation.history.get_revno(
382
 
            navigation.next_page_revid)
383
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
384
 
 
385
 
    params = {'filter_file_id': navigation.filter_file_id}
 
335
    
 
336
    params = { 'file_id': navigation.file_id }
386
337
    if getattr(navigation, 'query', None) is not None:
387
338
        params['q'] = navigation.query
388
 
 
389
 
    if getattr(navigation, 'start_revid', None) is not None:
390
 
        params['start_revid'] = start_revno
391
 
 
 
339
    else:
 
340
        params['start_revid'] = navigation.start_revid
 
341
        
392
342
    if navigation.prev_page_revid:
393
 
        navigation.prev_page_url = navigation.branch.context_url(
394
 
            [navigation.scan_url, prev_page_revno], **params)
 
343
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
395
344
    if navigation.next_page_revid:
396
 
        navigation.next_page_url = navigation.branch.context_url(
397
 
            [navigation.scan_url, next_page_revno], **params)
398
 
 
399
 
 
400
 
def directory_breadcrumbs(path, is_root, view):
401
 
    """
402
 
    Generate breadcrumb information from the directory path given
403
 
 
404
 
    The path given should be a path up to any branch that is currently being
405
 
    served
406
 
 
407
 
    Arguments:
408
 
    path -- The path to convert into breadcrumbs
409
 
    is_root -- Whether or not loggerhead is serving a branch at its root
410
 
    view -- The type of view we are showing (files, changes etc)
411
 
    """
412
 
    # Is our root directory itself a branch?
413
 
    if is_root:
414
 
        breadcrumbs = [{
415
 
            'dir_name': path,
416
 
            'path': '',
417
 
            'suffix': view,
418
 
        }]
419
 
    else:
420
 
        # Create breadcrumb trail for the path leading up to the branch
421
 
        breadcrumbs = [{
422
 
            'dir_name': "(root)",
423
 
            'path': '',
424
 
            'suffix': '',
425
 
        }]
426
 
        if path != '/':
427
 
            dir_parts = path.strip('/').split('/')
428
 
            for index, dir_name in enumerate(dir_parts):
429
 
                breadcrumbs.append({
430
 
                    'dir_name': dir_name,
431
 
                    'path': '/'.join(dir_parts[:index + 1]),
432
 
                    'suffix': '',
433
 
                })
434
 
            # If we are not in the directory view, the last crumb is a branch,
435
 
            # so we need to specify a view
436
 
            if view != 'directory':
437
 
                breadcrumbs[-1]['suffix'] = '/' + view
438
 
    return breadcrumbs
439
 
 
440
 
 
441
 
def branch_breadcrumbs(path, inv, view):
442
 
    """
443
 
    Generate breadcrumb information from the branch path given
444
 
 
445
 
    The path given should be a path that exists within a branch
446
 
 
447
 
    Arguments:
448
 
    path -- The path to convert into breadcrumbs
449
 
    inv -- Inventory to get file information from
450
 
    view -- The type of view we are showing (files, changes etc)
451
 
    """
452
 
    dir_parts = path.strip('/').split('/')
453
 
    inner_breadcrumbs = []
454
 
    for index, dir_name in enumerate(dir_parts):
455
 
        inner_breadcrumbs.append({
456
 
            'dir_name': dir_name,
457
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
458
 
            'suffix': '/' + view,
459
 
        })
460
 
    return inner_breadcrumbs
 
345
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
 
346
 
 
347
 
 
348
def log_exception(log):
 
349
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
350
        log.debug(line)
461
351
 
462
352
 
463
353
def decorator(unbound):
464
 
 
465
354
    def new_decorator(f):
466
355
        g = unbound(f)
467
356
        g.__name__ = f.__name__
474
363
    return new_decorator
475
364
 
476
365
 
 
366
# common threading-lock decorator
 
367
def with_lock(lockname, debug_name=None):
 
368
    if debug_name is None:
 
369
        debug_name = lockname
 
370
    @decorator
 
371
    def _decorator(unbound):
 
372
        def locked(self, *args, **kw):
 
373
            getattr(self, lockname).acquire()
 
374
            try:
 
375
                return unbound(self, *args, **kw)
 
376
            finally:
 
377
                getattr(self, lockname).release()
 
378
        return locked
 
379
    return _decorator
 
380
 
 
381
 
 
382
@decorator
 
383
def strip_whitespace(f):
 
384
    def _f(*a, **kw):
 
385
        out = f(*a, **kw)
 
386
        orig_len = len(out)
 
387
        out = re.sub(r'\n\s+', '\n', out)
 
388
        out = re.sub(r'[ \t]+', ' ', out)
 
389
        out = re.sub(r'\s+\n', '\n', out)
 
390
        new_len = len(out)
 
391
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
 
392
                  human_size(orig_len - new_len),
 
393
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
 
394
        return out
 
395
    return _f
 
396
 
477
397
 
478
398
@decorator
479
399
def lsprof(f):
480
 
 
481
400
    def _f(*a, **kw):
482
401
        from loggerhead.lsprof import profile
483
402
        import cPickle
484
403
        z = time.time()
485
404
        ret, stats = profile(f, *a, **kw)
486
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
487
 
            int((time.time() - z) * 1000)))
 
405
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
488
406
        stats.sort()
489
407
        stats.freeze()
490
408
        now = time.time()
491
409
        msec = int(now * 1000) % 1000
492
 
        timestr = time.strftime('%Y%m%d%H%M%S',
493
 
                                time.localtime(now)) + ('%03d' % (msec,))
 
410
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
494
411
        filename = f.__name__ + '-' + timestr + '.lsprof'
495
412
        cPickle.dump(stats, open(filename, 'w'), 2)
496
413
        return ret
504
421
#         current location along the navigation path (while browsing)
505
422
#     - starting revid (start_revid)
506
423
#         the current beginning of navigation (navigation continues back to
507
 
#         the original revision) -- this defines an 'alternate mainline'
508
 
#         when the user navigates into a branch.
 
424
#         the original revision) -- this may not be along the primary revision
 
425
#         path since the user may have navigated into a branch
509
426
#     - file_id
510
 
#         the file being looked at
511
 
#     - filter_file_id
512
427
#         if navigating the revisions that touched a file
513
428
#     - q (query)
514
429
#         if navigating the revisions that matched a search query
525
440
#         for re-ordering an existing page by different sort
526
441
 
527
442
t_context = threading.local()
528
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
529
 
          'compare_revid', 'sort')
 
443
_valid = ('start_revid', 'file_id', 'q', 'remember', 'compare_revid', 'sort')
530
444
 
531
445
 
532
446
def set_context(map):
535
449
 
536
450
def get_context(**overrides):
537
451
    """
538
 
    Soon to be deprecated.
539
 
 
540
 
 
541
452
    return a context map that may be overriden by specific values passed in,
542
453
    but only contains keys from the list of valid context keys.
543
 
 
 
454
    
544
455
    if 'clear' is set, only the 'remember' context value will be added, and
545
456
    all other context will be omitted.
546
457
    """
552
463
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
553
464
    map.update(overrides)
554
465
    return map
555
 
 
556
 
 
557
 
class Reloader(object):
558
 
    """
559
 
    This class wraps all paste.reloader logic. All methods are @classmethod.
560
 
    """
561
 
 
562
 
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
563
 
 
564
 
    @classmethod
565
 
    def _turn_sigterm_into_systemexit(cls):
566
 
        """
567
 
        Attempts to turn a SIGTERM exception into a SystemExit exception.
568
 
        """
569
 
        try:
570
 
            import signal
571
 
        except ImportError:
572
 
            return
573
 
 
574
 
        def handle_term(signo, frame):
575
 
            raise SystemExit
576
 
        signal.signal(signal.SIGTERM, handle_term)
577
 
 
578
 
    @classmethod
579
 
    def is_installed(cls):
580
 
        return os.environ.get(cls._reloader_environ_key)
581
 
 
582
 
    @classmethod
583
 
    def install(cls):
584
 
        from paste import reloader
585
 
        reloader.install(int(1))
586
 
 
587
 
    @classmethod
588
 
    def restart_with_reloader(cls):
589
 
        """Based on restart_with_monitor from paste.script.serve."""
590
 
        print 'Starting subprocess with file monitor'
591
 
        while True:
592
 
            args = [sys.executable] + sys.argv
593
 
            new_environ = os.environ.copy()
594
 
            new_environ[cls._reloader_environ_key] = 'true'
595
 
            proc = None
596
 
            try:
597
 
                try:
598
 
                    cls._turn_sigterm_into_systemexit()
599
 
                    proc = subprocess.Popen(args, env=new_environ)
600
 
                    exit_code = proc.wait()
601
 
                    proc = None
602
 
                except KeyboardInterrupt:
603
 
                    print '^C caught in monitor process'
604
 
                    return 1
605
 
            finally:
606
 
                if (proc is not None
607
 
                    and getattr(os, 'kill', None) is not None):
608
 
                    import signal
609
 
                    try:
610
 
                        os.kill(proc.pid, signal.SIGTERM)
611
 
                    except (OSError, IOError):
612
 
                        pass
613
 
 
614
 
            # Reloader always exits with code 3; but if we are
615
 
            # a monitor, any exit code will restart
616
 
            if exit_code != 3:
617
 
                return exit_code
618
 
            print '-'*20, 'Restarting', '-'*20
619
 
 
620
 
 
621
 
def convert_file_errors(application):
622
 
    """WSGI wrapper to convert some file errors to Paste exceptions"""
623
 
    def new_application(environ, start_response):
624
 
        try:
625
 
            return application(environ, start_response)
626
 
        except (IOError, OSError), e:
627
 
            import errno
628
 
            from paste import httpexceptions
629
 
            if e.errno == errno.ENOENT:
630
 
                raise httpexceptions.HTTPNotFound()
631
 
            elif e.errno == errno.EACCES:
632
 
                raise httpexceptions.HTTPForbidden()
633
 
            else:
634
 
                raise
635
 
    return new_application