#!/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.
import glob
import html
import http.server
import io
import mistune
import moth
import os
import pathlib
import shutil
import socketserver
import sys
import traceback
try:
from http.server import HTTPStatus
except ImportError:
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, scripts=[]):
return """
{title}
{scripts}
{title}
{body}
""".format(
title=title,
body=body,
scripts="\n".join(''.format(s) for s in scripts),
)
def mdpage(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), scripts=scripts)
# 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
class MothHandler(http.server.SimpleHTTPRequestHandler):
puzzles_dir = "puzzles"
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)
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)
def do_GET(self):
if self.path == "/":
self.serve_front()
elif self.path.startswith("/puzzles"):
self.serve_puzzles()
elif self.path.startswith("/files"):
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):
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 = mdpage(body).encode('utf-8')
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):
body = io.StringIO()
path = self.path.rstrip('/')
parts = path.split("/")
scripts = []
title = None
fpath = None
points = None
cat = None
puzzle = None
try:
fpath = os.path.join(self.puzzles_dir, parts[2])
points = int(parts[3])
except:
pass
if fpath:
cat = moth.Category(fpath, seed)
if points:
puzzle = cat.puzzle(points)
if not cat:
title = "Puzzle Categories"
body.write("
")
for i in sorted(glob.glob(os.path.join(self.puzzles_dir, "*", ""))):
bn = os.path.basename(i.strip('/\\'))
body.write('