~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:
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
20
import base64
30
21
import cgi
31
22
import datetime
32
23
import logging
33
24
import re
 
25
import sha
34
26
import struct
 
27
import sys
35
28
import threading
36
 
import time
37
 
import sys
38
 
import os
39
 
import subprocess
 
29
import traceback
 
30
 
 
31
import turbogears
 
32
 
40
33
 
41
34
log = logging.getLogger("loggerhead.controllers")
42
35
 
43
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
 
44
72
def fix_year(year):
45
73
    if year < 70:
46
74
        year += 2000
48
76
        year += 1900
49
77
    return year
50
78
 
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
79
 
132
80
class Container (object):
133
81
    """
134
82
    Convert a dict into an object with attributes.
135
83
    """
136
 
 
137
84
    def __init__(self, _dict=None, **kw):
138
85
        if _dict is not None:
139
86
            for key, value in _dict.iteritems():
140
87
                setattr(self, key, value)
141
88
        for key, value in kw.iteritems():
142
89
            setattr(self, key, value)
143
 
 
 
90
    
144
91
    def __repr__(self):
145
92
        out = '{ '
146
93
        for key, value in self.__dict__.iteritems():
147
 
            if key.startswith('_') or (getattr(self.__dict__[key],
148
 
                                       '__call__', None) is not None):
 
94
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
149
95
                continue
150
96
            out += '%r => %r, ' % (key, value)
151
97
        out += '}'
152
98
        return out
153
99
 
154
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
 
155
111
def trunc(text, limit=10):
156
112
    if len(text) <= limit:
157
113
        return text
158
114
    return text[:limit] + '...'
159
115
 
160
116
 
 
117
def to_utf8(s):
 
118
    if isinstance(s, unicode):
 
119
        return s.encode('utf-8')
 
120
    return s
 
121
 
 
122
 
161
123
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
162
124
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
163
125
 
164
 
 
165
126
def hide_email(email):
166
127
    """
167
128
    try to obsure any email address in a bazaar committer's name.
181
142
        return '%s at %s' % (username, domains[-2])
182
143
    return '%s at %s' % (username, domains[0])
183
144
 
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
 
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
 
192
179
 
193
180
# only do this if unicode turns out to be a problem
194
181
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
195
182
 
196
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
197
 
 
198
 
 
199
183
def html_clean(s):
200
184
    """
201
185
    clean up a string for html display.  expand any tabs, encode any html
203
187
    in displaying monospace text.
204
188
    """
205
189
    s = cgi.escape(s.expandtabs())
 
190
#    s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
206
191
    s = s.replace(' ', '&nbsp;')
207
192
    return s
208
193
 
209
194
 
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
 
 
258
195
def fake_permissions(kind, executable):
259
196
    # fake up unix-style permissions given only a "kind" and executable bit
260
197
    if kind == 'directory':
264
201
    return '-rw-r--r--'
265
202
 
266
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
 
267
213
def b64(s):
268
214
    s = base64.encodestring(s).replace('\n', '')
269
215
    while (len(s) > 0) and (s[-1] == '='):
291
237
P95_MEG = int(0.9 * MEG)
292
238
P95_GIG = int(0.9 * GIG)
293
239
 
294
 
 
295
240
def human_size(size, min_divisor=0):
296
241
    size = int(size)
297
242
    if (size == 0) and (min_divisor == 0):
305
250
        divisor = MEG
306
251
    else:
307
252
        divisor = KILO
308
 
 
 
253
    
309
254
    dot = size % divisor
310
255
    base = size - dot
311
256
    dot = dot * 10 // divisor
313
258
    if dot >= 10:
314
259
        base += 1
315
260
        dot -= 10
316
 
 
 
261
    
317
262
    out = str(base)
318
263
    if (base < 100) and (dot != 0):
319
 
        out += '.%d' % (dot)
 
264
        out += '.%d' % (dot,)
320
265
    if divisor == KILO:
321
266
        out += 'K'
322
267
    elif divisor == MEG:
324
269
    elif divisor == GIG:
325
270
        out += 'G'
326
271
    return out
327
 
 
328
 
 
329
 
def fill_in_navigation(navigation):
 
272
    
 
273
 
 
274
def fill_in_navigation(history, navigation):
330
275
    """
331
276
    given a navigation block (used by the template for the page header), fill
332
277
    in useful calculated values.
333
278
    """
334
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
335
 
        navigation.position = navigation.revid_list.index(navigation.revid)
336
 
    else:
 
279
    navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
 
280
    if navigation.position is None:
337
281
        navigation.position = 0
338
282
    navigation.count = len(navigation.revid_list)
339
283
    navigation.page_position = navigation.position // navigation.pagesize + 1
340
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
341
 
 - 1)) // navigation.pagesize
342
 
 
 
284
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
285
    
343
286
    def get_offset(offset):
344
 
        if (navigation.position + offset < 0) or (
345
 
           navigation.position + offset > navigation.count - 1):
 
287
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
346
288
            return None
347
289
        return navigation.revid_list[navigation.position + offset]
348
 
 
349
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
 
290
    
350
291
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
351
292
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
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}
 
293
    
 
294
    params = { 'file_id': navigation.file_id }
359
295
    if getattr(navigation, 'query', None) is not None:
360
296
        params['q'] = navigation.query
361
 
 
362
 
    if getattr(navigation, 'start_revid', None) is not None:
363
 
        params['start_revid'] = start_revno
364
 
 
 
297
    else:
 
298
        params['start_revid'] = navigation.start_revid
 
299
        
365
300
    if navigation.prev_page_revid:
366
 
        navigation.prev_page_url = navigation.branch.context_url(
367
 
            [navigation.scan_url, prev_page_revno], **params)
 
301
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
368
302
    if navigation.next_page_revid:
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
 
        breadcrumbs = [{
388
 
            'dir_name': path,
389
 
            'path': '',
390
 
            'suffix': view,
391
 
        }]
392
 
    else:
393
 
        # Create breadcrumb trail for the path leading up to the branch
394
 
        breadcrumbs = [{
395
 
            'dir_name': "(root)",
396
 
            'path': '',
397
 
            'suffix': '',
398
 
        }]
399
 
        if path != '/':
400
 
            dir_parts = path.strip('/').split('/')
401
 
            for index, dir_name in enumerate(dir_parts):
402
 
                breadcrumbs.append({
403
 
                    'dir_name': dir_name,
404
 
                    'path': '/'.join(dir_parts[:index + 1]),
405
 
                    'suffix': '',
406
 
                })
407
 
            # If we are not in the directory view, the last crumb is a branch,
408
 
            # so we need to specify a view
409
 
            if view != 'directory':
410
 
                breadcrumbs[-1]['suffix'] = '/' + view
411
 
    return breadcrumbs
412
 
 
413
 
 
414
 
def branch_breadcrumbs(path, inv, view):
415
 
    """
416
 
    Generate breadcrumb information from the branch path given
417
 
 
418
 
    The path given should be a path that exists within a branch
419
 
 
420
 
    Arguments:
421
 
    path -- The path to convert into breadcrumbs
422
 
    inv -- Inventory to get file information from
423
 
    view -- The type of view we are showing (files, changes etc)
424
 
    """
425
 
    dir_parts = path.strip('/').split('/')
426
 
    inner_breadcrumbs = []
427
 
    for index, dir_name in enumerate(dir_parts):
428
 
        inner_breadcrumbs.append({
429
 
            'dir_name': dir_name,
430
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
431
 
            'suffix': '/' + view,
432
 
        })
433
 
    return inner_breadcrumbs
 
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)
434
309
 
435
310
 
436
311
def decorator(unbound):
437
 
 
438
312
    def new_decorator(f):
439
313
        g = unbound(f)
440
314
        g.__name__ = f.__name__
447
321
    return new_decorator
448
322
 
449
323
 
450
 
 
451
 
@decorator
452
 
def lsprof(f):
453
 
 
454
 
    def _f(*a, **kw):
455
 
        from loggerhead.lsprof import profile
456
 
        import cPickle
457
 
        z = time.time()
458
 
        ret, stats = profile(f, *a, **kw)
459
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
460
 
            int((time.time() - z) * 1000)))
461
 
        stats.sort()
462
 
        stats.freeze()
463
 
        now = time.time()
464
 
        msec = int(now * 1000) % 1000
465
 
        timestr = time.strftime('%Y%m%d%H%M%S',
466
 
                                time.localtime(now)) + ('%03d' % msec)
467
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
468
 
        cPickle.dump(stats, open(filename, 'w'), 2)
469
 
        return ret
470
 
    return _f
471
 
 
472
 
 
473
 
# just thinking out loud here...
474
 
#
475
 
# so, when browsing around, there are 5 pieces of context, most optional:
476
 
#     - current revid
477
 
#         current location along the navigation path (while browsing)
478
 
#     - starting revid (start_revid)
479
 
#         the current beginning of navigation (navigation continues back to
480
 
#         the original revision) -- this defines an 'alternate mainline'
481
 
#         when the user navigates into a branch.
482
 
#     - file_id
483
 
#         the file being looked at
484
 
#     - filter_file_id
485
 
#         if navigating the revisions that touched a file
486
 
#     - q (query)
487
 
#         if navigating the revisions that matched a search query
488
 
#     - remember
489
 
#         a previous revision to remember for future comparisons
490
 
#
491
 
# current revid is given on the url path.  the rest are optional components
492
 
# in the url params.
493
 
#
494
 
# other transient things can be set:
495
 
#     - compare_revid
496
 
#         to compare one revision to another, on /revision only
497
 
#     - sort
498
 
#         for re-ordering an existing page by different sort
499
 
 
500
 
t_context = threading.local()
501
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
502
 
          'compare_revid', 'sort')
503
 
 
504
 
 
505
 
def set_context(map):
506
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
507
 
 
508
 
 
509
 
def get_context(**overrides):
510
 
    """
511
 
    Soon to be deprecated.
512
 
 
513
 
 
514
 
    return a context map that may be overriden by specific values passed in,
515
 
    but only contains keys from the list of valid context keys.
516
 
 
517
 
    if 'clear' is set, only the 'remember' context value will be added, and
518
 
    all other context will be omitted.
519
 
    """
520
 
    map = dict()
521
 
    if overrides.get('clear', False):
522
 
        map['remember'] = t_context.map.get('remember', None)
523
 
    else:
524
 
        map.update(t_context.map)
525
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
526
 
    map.update(overrides)
527
 
    return map
528
 
 
529
 
 
530
 
class Reloader(object):
531
 
    """
532
 
    This class wraps all paste.reloader logic. All methods are @classmethod.
533
 
    """
534
 
 
535
 
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
536
 
 
537
 
    @classmethod
538
 
    def _turn_sigterm_into_systemexit(self):
539
 
        """
540
 
        Attempts to turn a SIGTERM exception into a SystemExit exception.
541
 
        """
542
 
        try:
543
 
            import signal
544
 
        except ImportError:
545
 
            return
546
 
 
547
 
        def handle_term(signo, frame):
548
 
            raise SystemExit
549
 
        signal.signal(signal.SIGTERM, handle_term)
550
 
 
551
 
    @classmethod
552
 
    def is_installed(self):
553
 
        return os.environ.get(self._reloader_environ_key)
554
 
 
555
 
    @classmethod
556
 
    def install(self):
557
 
        from paste import reloader
558
 
        reloader.install(int(1))
559
 
 
560
 
    @classmethod
561
 
    def restart_with_reloader(self):
562
 
        """Based on restart_with_monitor from paste.script.serve."""
563
 
        print 'Starting subprocess with file monitor'
564
 
        while 1:
565
 
            args = [sys.executable] + sys.argv
566
 
            new_environ = os.environ.copy()
567
 
            new_environ[self._reloader_environ_key] = 'true'
568
 
            proc = None
 
324
# common threading-lock decorator
 
325
def with_lock(lockname, debug_name=None):
 
326
    if debug_name is None:
 
327
        debug_name = lockname
 
328
    @decorator
 
329
    def _decorator(unbound):
 
330
        def locked(self, *args, **kw):
 
331
            #self.log.debug('-> %r lock %r', id(threading.currentThread()), debug_name)
 
332
            getattr(self, lockname).acquire()
569
333
            try:
570
 
                try:
571
 
                    self._turn_sigterm_into_systemexit()
572
 
                    proc = subprocess.Popen(args, env=new_environ)
573
 
                    exit_code = proc.wait()
574
 
                    proc = None
575
 
                except KeyboardInterrupt:
576
 
                    print '^C caught in monitor process'
577
 
                    return 1
 
334
                return unbound(self, *args, **kw)
578
335
            finally:
579
 
                if (proc is not None
580
 
                    and hasattr(os, 'kill')):
581
 
                    import signal
582
 
                    try:
583
 
                        os.kill(proc.pid, signal.SIGTERM)
584
 
                    except (OSError, IOError):
585
 
                        pass
 
336
                getattr(self, lockname).release()
 
337
                #self.log.debug('<- %r unlock %r', id(threading.currentThread()), debug_name)
 
338
        return locked
 
339
    return _decorator
586
340
 
587
 
            # Reloader always exits with code 3; but if we are
588
 
            # a monitor, any exit code will restart
589
 
            if exit_code != 3:
590
 
                return exit_code
591
 
            print '-'*20, 'Restarting', '-'*20