1""" 2Watches the key ``paste.httpserver.thread_pool`` to see how many 3threads there are and report on any wedged threads. 4""" 5import sys 6import cgi 7import time 8import traceback 9from cStringIO import StringIO 10from thread import get_ident 11from paste import httpexceptions 12from paste.request import construct_url, parse_formvars 13from paste.util.template import HTMLTemplate, bunch 14 15page_template = HTMLTemplate(''' 16<html> 17 <head> 18 <style type="text/css"> 19 body { 20 font-family: sans-serif; 21 } 22 table.environ tr td { 23 border-bottom: #bbb 1px solid; 24 } 25 table.environ tr td.bottom { 26 border-bottom: none; 27 } 28 table.thread { 29 border: 1px solid #000; 30 margin-bottom: 1em; 31 } 32 table.thread tr td { 33 border-bottom: #999 1px solid; 34 padding-right: 1em; 35 } 36 table.thread tr td.bottom { 37 border-bottom: none; 38 } 39 table.thread tr.this_thread td { 40 background-color: #006; 41 color: #fff; 42 } 43 a.button { 44 background-color: #ddd; 45 border: #aaa outset 2px; 46 text-decoration: none; 47 margin-top: 10px; 48 font-size: 80%; 49 color: #000; 50 } 51 a.button:hover { 52 background-color: #eee; 53 border: #bbb outset 2px; 54 } 55 a.button:active { 56 border: #bbb inset 2px; 57 } 58 </style> 59 <title>{{title}}</title> 60 </head> 61 <body> 62 <h1>{{title}}</h1> 63 {{if kill_thread_id}} 64 <div style="background-color: #060; color: #fff; 65 border: 2px solid #000;"> 66 Thread {{kill_thread_id}} killed 67 </div> 68 {{endif}} 69 <div>Pool size: {{nworkers}} 70 {{if actual_workers > nworkers}} 71 + {{actual_workers-nworkers}} extra 72 {{endif}} 73 ({{nworkers_used}} used including current request)<br> 74 idle: {{len(track_threads["idle"])}}, 75 busy: {{len(track_threads["busy"])}}, 76 hung: {{len(track_threads["hung"])}}, 77 dying: {{len(track_threads["dying"])}}, 78 zombie: {{len(track_threads["zombie"])}}</div> 79 80{{for thread in threads}} 81 82<table class="thread"> 83 <tr {{if thread.thread_id == this_thread_id}}class="this_thread"{{endif}}> 84 <td> 85 <b>Thread</b> 86 {{if thread.thread_id == this_thread_id}} 87 (<i>this</i> request) 88 {{endif}}</td> 89 <td> 90 <b>{{thread.thread_id}} 91 {{if allow_kill}} 92 <form action="{{script_name}}/kill" method="POST" 93 style="display: inline"> 94 <input type="hidden" name="thread_id" value="{{thread.thread_id}}"> 95 <input type="submit" value="kill"> 96 </form> 97 {{endif}} 98 </b> 99 </td> 100 </tr> 101 <tr> 102 <td>Time processing request</td> 103 <td>{{thread.time_html|html}}</td> 104 </tr> 105 <tr> 106 <td>URI</td> 107 <td>{{if thread.uri == 'unknown'}} 108 unknown 109 {{else}}<a href="{{thread.uri}}">{{thread.uri_short}}</a> 110 {{endif}} 111 </td> 112 <tr> 113 <td colspan="2" class="bottom"> 114 <a href="#" class="button" style="width: 9em; display: block" 115 onclick=" 116 var el = document.getElementById('environ-{{thread.thread_id}}'); 117 if (el.style.display) { 118 el.style.display = ''; 119 this.innerHTML = \'▾ Hide environ\'; 120 } else { 121 el.style.display = 'none'; 122 this.innerHTML = \'▸ Show environ\'; 123 } 124 return false 125 ">▸ Show environ</a> 126 127 <div id="environ-{{thread.thread_id}}" style="display: none"> 128 {{if thread.environ:}} 129 <table class="environ"> 130 {{for loop, item in looper(sorted(thread.environ.items()))}} 131 {{py:key, value=item}} 132 <tr> 133 <td {{if loop.last}}class="bottom"{{endif}}>{{key}}</td> 134 <td {{if loop.last}}class="bottom"{{endif}}>{{value}}</td> 135 </tr> 136 {{endfor}} 137 </table> 138 {{else}} 139 Thread is in process of starting 140 {{endif}} 141 </div> 142 143 {{if thread.traceback}} 144 <a href="#" class="button" style="width: 9em; display: block" 145 onclick=" 146 var el = document.getElementById('traceback-{{thread.thread_id}}'); 147 if (el.style.display) { 148 el.style.display = ''; 149 this.innerHTML = \'▾ Hide traceback\'; 150 } else { 151 el.style.display = 'none'; 152 this.innerHTML = \'▸ Show traceback\'; 153 } 154 return false 155 ">▸ Show traceback</a> 156 157 <div id="traceback-{{thread.thread_id}}" style="display: none"> 158 <pre class="traceback">{{thread.traceback}}</pre> 159 </div> 160 {{endif}} 161 162 </td> 163 </tr> 164</table> 165 166{{endfor}} 167 168 </body> 169</html> 170''', name='watchthreads.page_template') 171 172class WatchThreads(object): 173 174 """ 175 Application that watches the threads in ``paste.httpserver``, 176 showing the length each thread has been working on a request. 177 178 If allow_kill is true, then you can kill errant threads through 179 this application. 180 181 This application can expose private information (specifically in 182 the environment, like cookies), so it should be protected. 183 """ 184 185 def __init__(self, allow_kill=False): 186 self.allow_kill = allow_kill 187 188 def __call__(self, environ, start_response): 189 if 'paste.httpserver.thread_pool' not in environ: 190 start_response('403 Forbidden', [('Content-type', 'text/plain')]) 191 return ['You must use the threaded Paste HTTP server to use this application'] 192 if environ.get('PATH_INFO') == '/kill': 193 return self.kill(environ, start_response) 194 else: 195 return self.show(environ, start_response) 196 197 def show(self, environ, start_response): 198 start_response('200 OK', [('Content-type', 'text/html')]) 199 form = parse_formvars(environ) 200 if form.get('kill'): 201 kill_thread_id = form['kill'] 202 else: 203 kill_thread_id = None 204 thread_pool = environ['paste.httpserver.thread_pool'] 205 nworkers = thread_pool.nworkers 206 now = time.time() 207 208 209 workers = thread_pool.worker_tracker.items() 210 workers.sort(key=lambda v: v[1][0]) 211 threads = [] 212 for thread_id, (time_started, worker_environ) in workers: 213 thread = bunch() 214 threads.append(thread) 215 if worker_environ: 216 thread.uri = construct_url(worker_environ) 217 else: 218 thread.uri = 'unknown' 219 thread.thread_id = thread_id 220 thread.time_html = format_time(now-time_started) 221 thread.uri_short = shorten(thread.uri) 222 thread.environ = worker_environ 223 thread.traceback = traceback_thread(thread_id) 224 225 page = page_template.substitute( 226 title="Thread Pool Worker Tracker", 227 nworkers=nworkers, 228 actual_workers=len(thread_pool.workers), 229 nworkers_used=len(workers), 230 script_name=environ['SCRIPT_NAME'], 231 kill_thread_id=kill_thread_id, 232 allow_kill=self.allow_kill, 233 threads=threads, 234 this_thread_id=get_ident(), 235 track_threads=thread_pool.track_threads()) 236 237 return [page] 238 239 def kill(self, environ, start_response): 240 if not self.allow_kill: 241 exc = httpexceptions.HTTPForbidden( 242 'Killing threads has not been enabled. Shame on you ' 243 'for trying!') 244 return exc(environ, start_response) 245 vars = parse_formvars(environ) 246 thread_id = int(vars['thread_id']) 247 thread_pool = environ['paste.httpserver.thread_pool'] 248 if thread_id not in thread_pool.worker_tracker: 249 exc = httpexceptions.PreconditionFailed( 250 'You tried to kill thread %s, but it is not working on ' 251 'any requests' % thread_id) 252 return exc(environ, start_response) 253 thread_pool.kill_worker(thread_id) 254 script_name = environ['SCRIPT_NAME'] or '/' 255 exc = httpexceptions.HTTPFound( 256 headers=[('Location', script_name+'?kill=%s' % thread_id)]) 257 return exc(environ, start_response) 258 259def traceback_thread(thread_id): 260 """ 261 Returns a plain-text traceback of the given thread, or None if it 262 can't get a traceback. 263 """ 264 if not hasattr(sys, '_current_frames'): 265 # Only 2.5 has support for this, with this special function 266 return None 267 frames = sys._current_frames() 268 if not thread_id in frames: 269 return None 270 frame = frames[thread_id] 271 out = StringIO() 272 traceback.print_stack(frame, file=out) 273 return out.getvalue() 274 275hide_keys = ['paste.httpserver.thread_pool'] 276 277def format_environ(environ): 278 if environ is None: 279 return environ_template.substitute( 280 key='---', 281 value='No environment registered for this thread yet') 282 environ_rows = [] 283 for key, value in sorted(environ.items()): 284 if key in hide_keys: 285 continue 286 try: 287 if key.upper() != key: 288 value = repr(value) 289 environ_rows.append( 290 environ_template.substitute( 291 key=cgi.escape(str(key)), 292 value=cgi.escape(str(value)))) 293 except Exception as e: 294 environ_rows.append( 295 environ_template.substitute( 296 key=cgi.escape(str(key)), 297 value='Error in <code>repr()</code>: %s' % e)) 298 return ''.join(environ_rows) 299 300def format_time(time_length): 301 if time_length >= 60*60: 302 # More than an hour 303 time_string = '%i:%02i:%02i' % (int(time_length/60/60), 304 int(time_length/60) % 60, 305 time_length % 60) 306 elif time_length >= 120: 307 time_string = '%i:%02i' % (int(time_length/60), 308 time_length % 60) 309 elif time_length > 60: 310 time_string = '%i sec' % time_length 311 elif time_length > 1: 312 time_string = '%0.1f sec' % time_length 313 else: 314 time_string = '%0.2f sec' % time_length 315 if time_length < 5: 316 return time_string 317 elif time_length < 120: 318 return '<span style="color: #900">%s</span>' % time_string 319 else: 320 return '<span style="background-color: #600; color: #fff">%s</span>' % time_string 321 322def shorten(s): 323 if len(s) > 60: 324 return s[:40]+'...'+s[-10:] 325 else: 326 return s 327 328def make_watch_threads(global_conf, allow_kill=False): 329 from paste.deploy.converters import asbool 330 return WatchThreads(allow_kill=asbool(allow_kill)) 331make_watch_threads.__doc__ = WatchThreads.__doc__ 332 333def make_bad_app(global_conf, pause=0): 334 pause = int(pause) 335 def bad_app(environ, start_response): 336 import thread 337 if pause: 338 time.sleep(pause) 339 else: 340 count = 0 341 while 1: 342 print("I'm alive %s (%s)" % (count, thread.get_ident())) 343 time.sleep(10) 344 count += 1 345 start_response('200 OK', [('content-type', 'text/plain')]) 346 return ['OK, paused %s seconds' % pause] 347 return bad_app 348