moth/tools/devel-server.py

293 lines
9.7 KiB
Python
Raw Normal View History

2017-10-30 11:25:58 -06:00
#!/usr/bin/python3
# To pick up any changes to this file without restarting anything:
# while true; do ./tools/devel-server.py --once; done
# It's kludgy, but it gets the job done.
# Feel free to make it suck less, for example using the `tcpserver` program.
2016-10-15 21:47:50 -06:00
import glob
2016-10-22 10:35:55 -06:00
import html
import http.server
2016-10-22 10:35:55 -06:00
import io
import mistune
import moth
2016-10-16 19:52:09 -06:00
import os
import pathlib
2016-10-22 10:35:55 -06:00
import shutil
import socketserver
2016-10-18 09:34:06 -06:00
import sys
import traceback
try:
from http.server import HTTPStatus
except ImportError:
2016-10-17 15:37:11 -06:00
class HTTPStatus:
OK = 200
NOT_FOUND = 404
INTERNAL_SERVER_ERROR = 500
sys.dont_write_bytecode = True
# XXX: This will eventually cause a problem. Do something more clever here.
seed = 1
def page(title, body, baseurl, scripts=[]):
return """<!DOCTYPE html>
<html>
<head>
2016-10-24 21:09:04 -06:00
<title>{title}</title>
<link rel="stylesheet" href="{baseurl}/files/src/www/res/style.css">
2017-10-23 10:01:11 -06:00
{scripts}
</head>
<body>
2016-10-24 21:09:04 -06:00
<h1>{title}</h1>
<div id="preview" class="terminal">
2016-10-24 21:09:04 -06:00
{body}
</div>
</body>
2017-10-23 10:01:11 -06:00
</html>""".format(
title=title,
body=body,
baseurl=baseurl,
2017-10-23 10:01:11 -06:00
scripts="\n".join('<script src="{}"></script>'.format(s) for s in scripts),
)
2017-02-02 11:17:03 -07:00
# XXX: What horrors did we unleash with our chdir shenanigans that
# makes this serve 404 and 500 when we mix in ThreadingMixIn?
class ThreadingServer(socketserver.ForkingMixIn, http.server.HTTPServer):
pass
2016-10-18 09:34:06 -06:00
class MothHandler(http.server.SimpleHTTPRequestHandler):
puzzles_dir = "puzzles"
base_url = ""
def mdpage(self, body, scripts=[]):
try:
title, _ = body.split('\n', 1)
except ValueError:
title = "Result"
title = title.lstrip("#")
title = title.strip()
return page(title, mistune.markdown(body, escape=False), self.base_url, scripts=scripts)
2016-10-18 09:34:06 -06:00
def handle_one_request(self):
try:
super().handle_one_request()
except:
tbtype, value, tb = sys.exc_info()
tblist = traceback.format_tb(tb, None) + traceback.format_exception_only(tbtype, value)
2016-10-22 10:35:55 -06:00
payload = ("Traceback (most recent call last)\n" +
"".join(tblist[:-1]) +
tblist[-1]).encode('utf-8')
self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", payload)
self.end_headers()
self.wfile.write(payload)
2016-10-18 09:34:06 -06:00
def do_GET(self):
if self.path == "/":
self.serve_front()
elif self.path.startswith("/puzzles/"):
self.serve_puzzles(self.path)
elif self.path.startswith("/files/"):
2016-10-22 10:35:55 -06:00
self.serve_file(self.translate_path(self.path))
else:
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
def translate_path(self, path):
if path.startswith('/files'):
path = path[7:]
return super().translate_path(path)
def serve_front(self):
2016-10-22 10:35:55 -06:00
body = """
MOTH Development Server Front Page
====================
Yo, it's the front page.
There's stuff you can do here:
* [Available puzzles](puzzles/)
* [Raw filesystem view](files/)
* [Documentation](files/docs/)
* [Instructions](files/docs/devel-server.md) for using this server
If you use this development server to run a contest,
you are a fool.
"""
payload = self.mdpage(body).encode('utf-8')
2016-10-22 10:35:55 -06:00
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", len(payload))
self.end_headers()
self.wfile.write(payload)
def serve_puzzles(self, path):
2016-10-22 10:35:55 -06:00
body = io.StringIO()
path = path.rstrip('/')
parts = path.split("/")
2017-10-23 10:01:11 -06:00
scripts = []
2016-10-22 10:35:55 -06:00
title = None
fpath = None
points = None
2016-10-22 10:35:55 -06:00
cat = None
puzzle = None
2016-10-22 10:35:55 -06:00
try:
fpath = os.path.join(self.puzzles_dir, parts[2])
points = int(parts[3])
2016-10-22 10:35:55 -06:00
except:
pass
if fpath:
cat = moth.Category(fpath, seed)
if points:
2016-11-17 14:24:01 -07:00
puzzle = cat.puzzle(points)
2016-10-22 10:35:55 -06:00
if not cat:
title = "Puzzle Categories"
body.write("<ul>")
for i in sorted(glob.glob(os.path.join(self.puzzles_dir, "*", ""))):
bn = os.path.basename(i.strip('/\\'))
body.write('<li><a href="{}/">puzzles/{}/</a></li>'.format(bn, bn))
2016-10-22 10:35:55 -06:00
body.write("</ul>")
elif not puzzle:
2016-10-16 19:52:09 -06:00
# List all point values in a category
2016-10-22 10:35:55 -06:00
title = "Puzzles in category `{}`".format(parts[2])
body.write("<ul>")
2017-02-02 11:34:57 -07:00
for points in cat.pointvals():
body.write('<li><a href="{points}/">puzzles/{cat}/{points}/</a></li>'.format(cat=parts[2], points=points))
2016-10-22 10:35:55 -06:00
body.write("</ul>")
elif len(parts) == 4:
2016-10-22 10:35:55 -06:00
# Serve up a puzzle
2017-10-23 10:01:11 -06:00
scripts = puzzle.scripts
2016-10-22 10:35:55 -06:00
title = "{} puzzle {}".format(parts[2], parts[3])
body.write("<h2>Body</h2>")
2017-10-30 12:16:23 -06:00
body.write("<div id='body' style='border: solid 1px silver;'>")
2016-10-22 10:35:55 -06:00
body.write(puzzle.html_body())
2017-10-30 12:16:23 -06:00
body.write("</div>")
body.write("<h2>Files</h2>")
body.write("<ul>")
for name,puzzlefile in sorted(puzzle.files.items()):
if puzzlefile.visible:
visibility = ''
else:
visibility = '(unlisted)'
body.write('<li><a href="{filename}">{filename}</a> {visibility}</li>'
.format(cat=parts[2],
points=puzzle.points,
filename=name,
visibility=visibility))
body.write("</ul>")
2016-10-22 10:35:55 -06:00
body.write("<h2>Answers</h2>")
2017-10-30 12:16:23 -06:00
body.write("<p>Input box (for scripts): <input id='answer' name='a'>")
2016-10-22 10:35:55 -06:00
body.write("<ul>")
2016-10-24 21:09:04 -06:00
assert puzzle.answers, 'No answers defined'
2016-10-22 10:35:55 -06:00
for a in puzzle.answers:
body.write("<li><code>{}</code></li>".format(html.escape(a)))
body.write("</ul>")
2017-01-30 12:13:02 -07:00
body.write("<h2>Authors</h2><p>{}</p>".format(', '.join(puzzle.get_authors())))
body.write("<h2>Summary</h2><p>{}</p>".format(puzzle.summary))
if puzzle.logs:
body.write("<h2>Debug Log</h2>")
body.write('<ul class="log">')
for l in puzzle.logs:
body.write("<li>{}</li>".format(html.escape(l)))
body.write("</ul>")
elif len(parts) == 5:
2016-10-22 10:35:55 -06:00
# Serve up a puzzle file
try:
pfile = puzzle.files[parts[4]]
except KeyError:
self.send_error(HTTPStatus.NOT_FOUND, "File not found. Did you add it to the Files: header or puzzle.add_stream?")
2016-10-22 10:35:55 -06:00
return
ctype = self.guess_type(pfile.name)
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", ctype)
self.end_headers()
shutil.copyfileobj(pfile.stream, self.wfile)
return
payload = page(title, body.getvalue(), self.base_url, scripts=scripts).encode('utf-8')
self.send_response(HTTPStatus.OK)
2016-10-22 10:35:55 -06:00
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", len(payload))
self.end_headers()
2016-10-22 10:35:55 -06:00
self.wfile.write(payload)
2016-10-22 10:35:55 -06:00
def serve_file(self, path):
lastmod = None
fspath = pathlib.Path(path)
2016-10-22 10:35:55 -06:00
if fspath.is_dir():
ctype = "text/html; charset=utf-8"
payload = self.list_directory(path)
# it sends headers but not body
shutil.copyfileobj(payload, self.wfile)
else:
2016-10-22 10:35:55 -06:00
ctype = self.guess_type(path)
try:
2016-10-22 10:35:55 -06:00
payload = fspath.read_bytes()
except OSError:
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
2016-10-22 10:35:55 -06:00
return
if path.endswith(".md"):
2016-10-22 10:35:55 -06:00
ctype = "text/html; charset=utf-8"
content = self.mdpage(payload.decode('utf-8'))
2016-10-22 10:35:55 -06:00
payload = content.encode('utf-8')
try:
fs = fspath.stat()
lastmod = self.date_time_string(fs.st_mtime)
except:
pass
2016-10-17 15:37:11 -06:00
self.send_response(HTTPStatus.OK)
2016-10-22 10:35:55 -06:00
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", len(payload))
if lastmod:
self.send_header("Last-Modified", lastmod)
self.end_headers()
2016-10-22 10:35:55 -06:00
self.wfile.write(payload)
def run(address=('127.0.0.1', 8080), once=False):
httpd = ThreadingServer(address, MothHandler)
2016-10-17 23:20:48 -06:00
print("=== Listening on http://{}:{}/".format(address[0], address[1]))
if once:
httpd.handle_request()
else:
httpd.serve_forever()
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description="MOTH puzzle development server")
parser.add_argument(
'--puzzles', default='puzzles',
help="Directory containing your puzzles"
)
parser.add_argument(
'--once', default=False, action='store_true',
help="Serve one page, then exit. For debugging the server."
)
parser.add_argument(
2018-05-05 16:00:03 -06:00
'--bind', default="0.0.0.0:8080",
help="Bind to ip:port"
)
parser.add_argument(
'--base', default="",
help="Base URL to this server, for reverse proxy setup"
)
args = parser.parse_args()
addr, port = args.bind.split(":")
port = int(port)
MothHandler.puzzles_dir = args.puzzles
MothHandler.base_url = args.base
run(address=(addr, port), once=args.once)