blob: 1606a8ddf62063e9ab351fe319cb9505339b162b [file] [log] [blame]
Daniel Dunbar2b0662c2008-09-19 23:32:11 +00001import BaseHTTPServer
2import SimpleHTTPServer
3import os
4import sys
5import urllib, urlparse
6import posixpath
7import StringIO
8import re
9import shutil
10import threading
11import time
12import socket
13
Daniel Dunbar61717b32008-09-20 01:43:16 +000014import Reporter
Daniel Dunbar3c697dd2008-09-22 02:27:45 +000015import ConfigParser
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000016
17# Keys replaced by server.
18
Daniel Dunbar14cad832008-09-21 20:34:58 +000019kReportColRE = re.compile('<!-- REPORTBUGCOL -->')
20kReportColRepl = '<td></td>'
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000021kReportBugRE = re.compile('<!-- REPORTBUG id="report-(.*)\\.html" -->')
Daniel Dunbar55c5aa42008-09-22 01:21:30 +000022kReportBugRepl = '<td class="View"><a href="report/\\1">Report Bug</a></td>'
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000023kBugKeyValueRE = re.compile('<!-- BUG([^ ]*) (.*) -->')
24
Daniel Dunbar14cad832008-09-21 20:34:58 +000025kReportReplacements = [(kReportColRE, kReportColRepl),
26 (kReportBugRE, kReportBugRepl)]
27
Daniel Dunbar2b353482008-09-21 23:02:25 +000028# Other simple parameters
29
30kResources = posixpath.join(posixpath.dirname(__file__), 'Resources')
Daniel Dunbar3c697dd2008-09-22 02:27:45 +000031kConfigPath = os.path.expanduser('~/.scanview.cfg')
Daniel Dunbar2b353482008-09-21 23:02:25 +000032
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000033###
34
35__version__ = "0.1"
36
37__all__ = ["create_server"]
38
39class ReporterThread(threading.Thread):
40 def __init__(self, report, reporter, parameters, server):
41 threading.Thread.__init__(self)
42 self.report = report
43 self.server = server
44 self.reporter = reporter
45 self.parameters = parameters
Daniel Dunbar55c5aa42008-09-22 01:21:30 +000046 self.success = False
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000047 self.status = None
48
49 def run(self):
50 result = None
51 try:
52 if self.server.options.debug:
53 print >>sys.stderr, "%s: SERVER: submitting bug."%(sys.argv[0],)
Daniel Dunbar55c5aa42008-09-22 01:21:30 +000054 self.status = self.reporter.fileReport(self.report, self.parameters)
55 self.success = True
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000056 time.sleep(3)
57 if self.server.options.debug:
58 print >>sys.stderr, "%s: SERVER: submission complete."%(sys.argv[0],)
Daniel Dunbar61717b32008-09-20 01:43:16 +000059 except Reporter.ReportFailure,e:
Daniel Dunbar55c5aa42008-09-22 01:21:30 +000060 self.status = e.value
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000061 except Exception,e:
62 s = StringIO.StringIO()
63 import traceback
Daniel Dunbar55c5aa42008-09-22 01:21:30 +000064 print >>s,'<b>Unhandled Exception</b><br><pre>'
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000065 traceback.print_exc(e,file=s)
66 print >>s,'</pre>'
67 self.status = s.getvalue()
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000068
69class ScanViewServer(BaseHTTPServer.HTTPServer):
70 def __init__(self, address, handler, root, reporters, options):
71 BaseHTTPServer.HTTPServer.__init__(self, address, handler)
72 self.root = root
73 self.reporters = reporters
74 self.options = options
75 self.halted = False
Daniel Dunbar3c697dd2008-09-22 02:27:45 +000076 self.config = None
77 self.load_config()
Daniel Dunbar2b0662c2008-09-19 23:32:11 +000078
Daniel Dunbar3c697dd2008-09-22 02:27:45 +000079 def load_config(self):
80 self.config = ConfigParser.RawConfigParser()
81
82 # Add defaults
83 self.config.add_section('ScanView')
Daniel Dunbar3266baa2008-09-22 02:53:12 +000084 for r in self.reporters:
Daniel Dunbar3c697dd2008-09-22 02:27:45 +000085 self.config.add_section(r.getName())
86 for p in r.getParameterNames():
87 self.config.set(r.getName(), p, '')
88
89 # Ignore parse errors
90 try:
91 self.config.read([kConfigPath])
92 except:
93 pass
94
95 # Save on exit
96 import atexit
97 atexit.register(lambda: self.save_config())
98
99 def save_config(self):
100 # Ignore errors (only called on exit).
101 try:
102 f = open(kConfigPath,'w')
103 self.config.write(f)
104 f.close()
105 except:
106 pass
107
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000108 def halt(self):
109 self.halted = True
110 if self.options.debug:
111 print >>sys.stderr, "%s: SERVER: halting." % (sys.argv[0],)
112
113 def serve_forever(self):
114 while not self.halted:
115 if self.options.debug > 1:
116 print >>sys.stderr, "%s: SERVER: waiting..." % (sys.argv[0],)
117 try:
118 self.handle_request()
119 except OSError,e:
120 print 'OSError',e.errno
121
Daniel Dunbar14cad832008-09-21 20:34:58 +0000122 def finish_request(self, request, client_address):
123 if self.options.autoReload:
124 import ScanView
125 self.RequestHandlerClass = reload(ScanView).ScanViewRequestHandler
126 BaseHTTPServer.HTTPServer.finish_request(self, request, client_address)
127
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000128 def handle_error(self, request, client_address):
129 # Ignore socket errors
130 info = sys.exc_info()
131 if info and isinstance(info[1], socket.error):
132 if self.options.debug > 1:
133 print >>sys.stderr, "%s: SERVER: ignored socket error." % (sys.argv[0],)
134 return
Daniel Dunbar14cad832008-09-21 20:34:58 +0000135 BaseHTTPServer.HTTPServer.handle_error(self, request, client_address)
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000136
137# Borrowed from Quixote, with simplifications.
138def parse_query(qs, fields=None):
139 if fields is None:
140 fields = {}
141 for chunk in filter(None, qs.split('&')):
142 if '=' not in chunk:
143 name = chunk
144 value = ''
145 else:
146 name, value = chunk.split('=', 1)
147 name = urllib.unquote(name.replace('+', ' '))
148 value = urllib.unquote(value.replace('+', ' '))
149 fields[name] = value
150 return fields
151
152class ScanViewRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
153 server_version = "ScanViewServer/" + __version__
Daniel Dunbar14cad832008-09-21 20:34:58 +0000154 dynamic_mtime = time.time()
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000155
156 def do_HEAD(self):
157 try:
158 SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(self)
159 except Exception,e:
160 self.handle_exception(e)
161
162 def do_GET(self):
163 try:
164 SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
165 except Exception,e:
166 self.handle_exception(e)
167
168 def do_POST(self):
169 """Serve a POST request."""
170 try:
171 length = self.headers.getheader('content-length') or "0"
172 try:
173 length = int(length)
174 except:
175 length = 0
176 content = self.rfile.read(length)
177 fields = parse_query(content)
178 f = self.send_head(fields)
179 if f:
180 self.copyfile(f, self.wfile)
181 f.close()
182 except Exception,e:
183 self.handle_exception(e)
184
185 def log_message(self, format, *args):
186 if self.server.options.debug:
187 sys.stderr.write("%s: SERVER: %s - - [%s] %s\n" %
188 (sys.argv[0],
189 self.address_string(),
190 self.log_date_time_string(),
191 format%args))
192
193 def load_report(self, report):
194 path = os.path.join(self.server.root, 'report-%s.html'%report)
195 data = open(path).read()
196 keys = {}
197 for item in kBugKeyValueRE.finditer(data):
198 k,v = item.groups()
199 keys[k] = v
200 return keys
201
202 def handle_exception(self, exc):
203 import traceback
204 s = StringIO.StringIO()
205 print >>s, "INTERNAL ERROR\n"
206 traceback.print_exc(exc, s)
207 f = self.send_string(s.getvalue(), 'text/plain')
208 if f:
209 self.copyfile(f, self.wfile)
210 f.close()
211
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000212 def submit_bug(self):
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000213 title = self.fields.get('title')
214 description = self.fields.get('description')
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000215 report = self.fields.get('report')
Daniel Dunbar3c697dd2008-09-22 02:27:45 +0000216 reporterIndex = self.fields.get('reporter')
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000217
218 # Type check form parameters.
219 reportPath = posixpath.join(self.server.root,
220 'report-%s.html' % report)
221 if not posixpath.exists(reportPath):
222 return (False, "Invalid report ID.")
223 if not title:
224 return (False, "Missing title.")
225 if not description:
226 return (False, "Missing description.")
227 try:
Daniel Dunbar3c697dd2008-09-22 02:27:45 +0000228 reporterIndex = int(reporterIndex)
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000229 except:
230 return (False, "Invalid report method.")
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000231
232 # Get the reporter and parameters.
Daniel Dunbar3c697dd2008-09-22 02:27:45 +0000233 reporter = self.server.reporters[reporterIndex]
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000234 parameters = {}
235 for o in reporter.getParameterNames():
236 name = '%s_%s'%(reporter.getName(),o)
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000237 if name not in self.fields:
238 return (False,
239 'Missing field "%s" for %s report method.'%(name,
240 reporter.getName()))
241 parameters[o] = self.fields[name]
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000242
Daniel Dunbar3c697dd2008-09-22 02:27:45 +0000243 # Update config defaults.
244 self.server.config.set('ScanView', 'reporter', reporterIndex)
245 for o in reporter.getParameterNames():
246 self.server.config.set(reporter.getName(), o, parameters[o])
247
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000248 # Create the report.
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000249 bug = Reporter.BugReport(title, description, [reportPath])
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000250
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000251 # Kick off a reporting thread.
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000252 t = ReporterThread(bug, reporter, parameters, self.server)
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000253 t.start()
254
255 # Wait for thread to die...
256 while t.isAlive():
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000257 time.sleep(.25)
258 submitStatus = t.status
259
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000260 return (t.success, t.status)
261
262 def send_report_submit(self):
Daniel Dunbarb76387e2008-09-22 03:08:32 +0000263 report = self.fields.get('report')
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000264 title = self.fields.get('title')
265 description = self.fields.get('description')
266
267 res,message = self.submit_bug()
268
269 if res:
270 statusClass = 'SubmitOk'
271 statusName = 'Succeeded'
272 else:
273 statusClass = 'SubmitFail'
274 statusName = 'Failed'
275
276 result = """
277<head>
Daniel Dunbarb76387e2008-09-22 03:08:32 +0000278 <title>Bug Submission</title>
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000279 <link rel="stylesheet" type="text/css" href="/scanview.css" />
280</head>
281<body>
Daniel Dunbarb76387e2008-09-22 03:08:32 +0000282<h3>
283<a href="/">Summary</a> >
284<a href="/report-%(report)s.html">Report %(report)s</a> >
285<a href="/report/%(report)s">File Bug</a> >
286Submit</h3>
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000287<form name="form" action="">
288<table class="form">
289<tr><td>
290<table class="form_group">
291<tr>
292 <td class="form_clabel">Title:</td>
293 <td class="form_value">
294 <input type="text" name="title" size="50" value="%(title)s" disabled>
295 </td>
296</tr>
297<tr>
298 <td class="form_label">Description:</td>
299 <td class="form_value">
300<textarea rows="10" cols="80" name="description" disabled>
301%(description)s
302</textarea>
303 </td>
304</table>
305</td></tr>
306</table>
307</form>
308<h1 class="%(statusClass)s">Submission %(statusName)s</h1>
309%(message)s
310<p>
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000311<hr>
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000312<a href="/">Return to Summary</a>
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000313</body>
Daniel Dunbar55c5aa42008-09-22 01:21:30 +0000314</html>"""%locals()
315 return self.send_string(result)
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000316
317 def send_report(self, report):
318 try:
319 keys = self.load_report(report)
320 except IOError:
Daniel Dunbarfe421482008-09-22 01:40:14 +0000321 return self.send_error(400, 'Invalid report.')
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000322
323 initialTitle = keys.get('DESC','')
Daniel Dunbar8b24edb2008-09-21 19:08:54 +0000324 initialDescription = """\
325Bug generated by the clang static analyzer.
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000326
Daniel Dunbar8b24edb2008-09-21 19:08:54 +0000327Description: %s
328File: %s
329Line: %s
330"""%(initialTitle,
331 keys.get('FILE','<unknown>'),
332 keys.get('LINE','<unknown>'))
333
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000334 reporterSelections = []
335 reporterOptions = []
336
Daniel Dunbar3c697dd2008-09-22 02:27:45 +0000337 try:
338 active = self.server.config.getint('ScanView','reporter')
339 except:
340 active = 0
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000341 for i,r in enumerate(self.server.reporters):
Daniel Dunbar3c697dd2008-09-22 02:27:45 +0000342 selected = (i == active)
343 if selected:
344 selectedStr = ' selected'
345 else:
346 selectedStr = ''
347 reporterSelections.append('<option value="%d"%s>%s</option>'%(i,selectedStr,r.getName()))
Daniel Dunbar0174b282008-09-22 00:11:51 +0000348 options = '\n'.join(["""\
349<tr>
350 <td class="form_clabel">%s:</td>
Daniel Dunbar3c697dd2008-09-22 02:27:45 +0000351 <td class="form_value"><input type="text" name="%s_%s" value="%s"></td>
352</tr>"""%(o,r.getName(),o,self.server.config.get(r.getName(), o)) for o in r.getParameterNames()])
353 display = ('none','')[selected]
Daniel Dunbar0174b282008-09-22 00:11:51 +0000354 reporterOptions.append("""\
355<tr id="%sReporterOptions" style="display:%s">
356 <td class="form_label">%s Options</td>
357 <td class="form_value">
358 <table class="form_inner_group">
359%s
360 </table>
361 </td>
362</tr>
363"""%(r.getName(),display,r.getName(),options))
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000364 reporterSelections = '\n'.join(reporterSelections)
365 reporterOptionsDivs = '\n'.join(reporterOptions)
366 reportersArray = '[%s]'%(','.join([`r.getName()` for r in self.server.reporters]))
367
368 result = """<html>
369<head>
Daniel Dunbarb76387e2008-09-22 03:08:32 +0000370 <title>File Bug</title>
Daniel Dunbar0174b282008-09-22 00:11:51 +0000371 <link rel="stylesheet" type="text/css" href="/scanview.css" />
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000372</head>
373<script language="javascript" type="text/javascript">
374var reporters = %(reportersArray)s;
375function updateReporterOptions() {
376 index = document.getElementById('reporter').selectedIndex;
377 for (var i=0; i < reporters.length; ++i) {
378 o = document.getElementById(reporters[i] + "ReporterOptions");
379 if (i == index) {
Daniel Dunbar0174b282008-09-22 00:11:51 +0000380 o.style.display = "";
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000381 } else {
382 o.style.display = "none";
383 }
384 }
385}
386</script>
Daniel Dunbarfe421482008-09-22 01:40:14 +0000387<body onLoad="updateReporterOptions()">
Daniel Dunbarb76387e2008-09-22 03:08:32 +0000388<h3>
389<a href="/">Summary</a> >
390<a href="/report-%(report)s.html">Report %(report)s</a> >
391File Bug</h3>
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000392<form name="form" action="/report_submit" method="post">
Daniel Dunbar0174b282008-09-22 00:11:51 +0000393<input type="hidden" name="report" value="%(report)s">
394
395<table class="form">
396<tr><td>
397<table class="form_group">
398<tr>
399 <td class="form_clabel">Title:</td>
400 <td class="form_value">
401 <input type="text" name="title" size="50" value="%(initialTitle)s">
402 </td>
403</tr>
404<tr>
405 <td class="form_label">Description:</td>
406 <td class="form_value">
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000407<textarea rows="10" cols="80" name="description">
408%(initialDescription)s
Daniel Dunbar0174b282008-09-22 00:11:51 +0000409</textarea>
410 </td>
411</table>
412<br>
413<table class="form_group">
414<tr>
415 <td class="form_clabel">Method:</td>
416 <td class="form_value">
417 <select id="reporter" name="reporter" onChange="updateReporterOptions()">
418 %(reporterSelections)s
419 </select>
420 </td>
421</tr>
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000422%(reporterOptionsDivs)s
Daniel Dunbar0174b282008-09-22 00:11:51 +0000423</table>
424<br>
425</td></tr>
426<tr><td class="form_submit">
427 <input align="right" type="submit" name="Submit" value="Submit">
428</td></tr>
429</table>
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000430</form>
Daniel Dunbar8b24edb2008-09-21 19:08:54 +0000431
Daniel Dunbarb76387e2008-09-22 03:08:32 +0000432<iframe src="/report-%(report)s.html" width="100%%" height="40%%"
Daniel Dunbar8b24edb2008-09-21 19:08:54 +0000433 scrolling="auto" frameborder="1">
Daniel Dunbar3c697dd2008-09-22 02:27:45 +0000434 <a href="/report-%(report)s.html">View Bug Report</a>
Daniel Dunbar8b24edb2008-09-21 19:08:54 +0000435</iframe>
436
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000437</body>
438</html>"""%locals()
439 return self.send_string(result)
440
441 def send_head(self, fields=None):
442 if fields is None:
443 fields = {}
444 self.fields = fields
445
446 o = urlparse.urlparse(self.path)
447 self.fields = parse_query(o.query, fields)
448 path = posixpath.normpath(urllib.unquote(o.path))
449
450 # Split the components and strip the root prefix.
451 components = path.split('/')[1:]
452
453 # Special case some top-level entries.
454 if components:
455 name = components[0]
Daniel Dunbar0174b282008-09-22 00:11:51 +0000456 if len(components)==2:
457 if name=='report':
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000458 return self.send_report(components[1])
Daniel Dunbar0174b282008-09-22 00:11:51 +0000459 elif len(components)==1:
460 if name=='quit':
461 self.server.halt()
462 return self.send_string('Goodbye.', 'text/plain')
463 elif name=='report_submit':
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000464 return self.send_report_submit()
Daniel Dunbar0174b282008-09-22 00:11:51 +0000465 elif name=='favicon.ico':
Daniel Dunbar2b353482008-09-21 23:02:25 +0000466 return self.send_path(posixpath.join(kResources,'bugcatcher.ico'))
Daniel Dunbar0174b282008-09-22 00:11:51 +0000467 elif name=='scanview.css':
468 return self.send_path(posixpath.join(kResources,'scanview.css'))
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000469
470 # Match directory entries.
471 if components[-1] == '':
472 components[-1] = 'index.html'
Daniel Dunbar2b353482008-09-21 23:02:25 +0000473
474 suffix = '/'.join(components)
475
476 # The summary may reference source files on disk using rooted
477 # paths. Make sure these resolve correctly for now.
478 # FIXME: This isn't a very good idea... we should probably
479 # mark rooted paths somehow.
480 if os.path.exists(posixpath.join('/', suffix)):
481 path = posixpath.join('/', suffix)
482 else:
483 path = posixpath.join(self.server.root, suffix)
484
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000485 if self.server.options.debug > 1:
486 print >>sys.stderr, '%s: SERVER: sending path "%s"'%(sys.argv[0],
487 path)
488 return self.send_path(path)
489
490 def send_404(self):
491 self.send_error(404, "File not found")
492 return None
493
494 def send_path(self, path):
495 ctype = self.guess_type(path)
496 if ctype.startswith('text/'):
497 # Patch file instead
498 return self.send_patched_file(path, ctype)
499 else:
500 mode = 'rb'
501 try:
502 f = open(path, mode)
503 except IOError:
504 return self.send_404()
505 return self.send_file(f, ctype)
506
507 def send_file(self, f, ctype):
508 # Patch files to add links, but skip binary files.
509 self.send_response(200)
510 self.send_header("Content-type", ctype)
511 fs = os.fstat(f.fileno())
512 self.send_header("Content-Length", str(fs[6]))
513 self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
514 self.end_headers()
515 return f
516
517 def send_string(self, s, ctype='text/html', headers=True, mtime=None):
518 if headers:
519 self.send_response(200)
520 self.send_header("Content-type", ctype)
521 self.send_header("Content-Length", str(len(s)))
Daniel Dunbar14cad832008-09-21 20:34:58 +0000522 if mtime is None:
523 mtime = self.dynamic_mtime
524 self.send_header("Last-Modified", self.date_time_string(mtime))
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000525 self.end_headers()
526 return StringIO.StringIO(s)
527
528 def send_patched_file(self, path, ctype):
Daniel Dunbarfe421482008-09-22 01:40:14 +0000529 try:
530 f = open(path,'r')
531 except IOError:
532 return self.send_404()
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000533 fs = os.fstat(f.fileno())
534 data = f.read()
Daniel Dunbar14cad832008-09-21 20:34:58 +0000535 for a,b in kReportReplacements:
536 data = a.sub(b, data)
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000537 return self.send_string(data, ctype, mtime=fs.st_mtime)
538
539
Daniel Dunbar3266baa2008-09-22 02:53:12 +0000540def create_server(address, options, root):
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000541 import Reporter
542
543 reporters = Reporter.getReporters()
544
Daniel Dunbar3266baa2008-09-22 02:53:12 +0000545 return ScanViewServer(address, ScanViewRequestHandler,
Daniel Dunbar2b0662c2008-09-19 23:32:11 +0000546 root,
547 reporters,
548 options)