moved from Flask to http.server to support multiple clients
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import sys
|
||||
import threading
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from os.path import basename, dirname, abspath
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import markdown
|
||||
from os.path import basename, dirname
|
||||
import json
|
||||
|
||||
TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
|
||||
{css}
|
||||
<style>
|
||||
body {{
|
||||
font-family: sans-serif;
|
||||
@@ -30,10 +34,25 @@ TEMPLATE = """
|
||||
</head>
|
||||
<body>
|
||||
<script type=\"text/javascript\">
|
||||
let eventSource = new EventSource("/stream");
|
||||
eventSource.addEventListener('reload', function(e) {{
|
||||
window.location.reload(true);
|
||||
}});
|
||||
function req(first) {{
|
||||
var xmlhttp = new XMLHttpRequest();
|
||||
xmlhttp.onload = function() {{
|
||||
if (xmlhttp.status == 200) {{
|
||||
document.querySelector("div.container").innerHTML = xmlhttp.responseText;
|
||||
}} else if(xmlhttp.status == 304) {{
|
||||
}} else {{
|
||||
console.log(xmlhttp.status, xmlhttp.statusText);
|
||||
}}
|
||||
req(false);
|
||||
}};
|
||||
xmlhttp.onerror = function() {{
|
||||
console.log(xmlhttp.status, xmlhttp.statusText);
|
||||
setTimeout(req, 1000, false);
|
||||
}};
|
||||
xmlhttp.open("GET", first ? "/markdown" : "/reload", true);
|
||||
xmlhttp.send();
|
||||
}}
|
||||
req(true);
|
||||
</script>
|
||||
<div class="container">
|
||||
{content}
|
||||
@@ -42,20 +61,144 @@ TEMPLATE = """
|
||||
</html>
|
||||
"""
|
||||
|
||||
def create_css_tag(url_list):
|
||||
result = ''
|
||||
for url in url_list:
|
||||
result += '<link href="%s" rel="stylesheet">' % url
|
||||
return result
|
||||
|
||||
class ServerSentEvent(object):
|
||||
def compile_html(mdfile=None, extensions=None, raw=None, stylesheet=(), **kwargs):
|
||||
html = None
|
||||
with mdfile and open(mdfile, 'r') or sys.stdin as instream:
|
||||
html = markdown.markdown(instream.read(), extensions=extensions, output_format='html5')
|
||||
if raw:
|
||||
doc = html
|
||||
else:
|
||||
doc = TEMPLATE.format(content=html, css=create_css_tag(stylesheet))
|
||||
return doc
|
||||
|
||||
def __init__(self, id=None, event=None, data=None, retry=1000):
|
||||
self.id = id
|
||||
self.event = event
|
||||
self.data = json.dumps(data)
|
||||
self.retry = retry
|
||||
class MarkdownHTTPServer(ThreadingHTTPServer):
|
||||
|
||||
def encode(self):
|
||||
if not self.data:
|
||||
return ""
|
||||
lines = [f"{key}: {value}" for key, value in vars(self).items() if value]
|
||||
return "%s\n\n" % "\n".join(lines)
|
||||
def __init__(self, mdfile, extensions=(), stylesheet=(), handler=BaseHTTPRequestHandler, interface="127.0.0.1", port=8080):
|
||||
import inotify
|
||||
import inotify.adapters
|
||||
import signal
|
||||
|
||||
self.stop = False
|
||||
def sigint_handler(signum, frame):
|
||||
self.stop = True
|
||||
|
||||
handlers = (sigint_handler, signal.getsignal(signal.SIGINT))
|
||||
signal.signal(signal.SIGINT, lambda signum, frame: [handler(signum, frame) for handler in handlers])
|
||||
|
||||
self.mdfile = mdfile
|
||||
self.extensions = extensions
|
||||
self.stylesheet = stylesheet
|
||||
self.condition_variable = threading.Condition()
|
||||
self.hash = None
|
||||
self.etag = None
|
||||
def watch_file():
|
||||
watcher = inotify.adapters.Inotify()
|
||||
watcher.add_watch(dirname(abspath(self.mdfile)))
|
||||
target_file = basename(self.mdfile)
|
||||
while True:
|
||||
if self.stop:
|
||||
break
|
||||
for event in watcher.event_gen(yield_nones=True, timeout_s=1):
|
||||
if not event:
|
||||
continue
|
||||
(_, event_type, path, filename) = event
|
||||
if filename == target_file and len(set(event_type).intersection(
|
||||
{'IN_CLOSE_WRITE'})):
|
||||
self.condition_variable.acquire()
|
||||
if self.update_file_digest():
|
||||
self.condition_variable.notify_all()
|
||||
self.condition_variable.release()
|
||||
|
||||
file_watcher = threading.Thread(target=watch_file)
|
||||
file_watcher.start()
|
||||
super().__init__((interface, port), handler)
|
||||
|
||||
def update_file_digest(self):
|
||||
md5 = hashlib.md5()
|
||||
with open(self.mdfile, 'rb') as mdfile:
|
||||
md5.update(mdfile.read())
|
||||
digest = md5.digest()
|
||||
if not self.hash or self.hash != digest:
|
||||
self.hash = digest
|
||||
self.etag = md5.hexdigest()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class MarkdownRequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
status_map = {
|
||||
200: "OK",
|
||||
204: "No Content",
|
||||
304: "Not Modified",
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
404: "Not Found",
|
||||
499: "Service Error",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
503: "Service Unavailable"
|
||||
}
|
||||
|
||||
def answer(self, code, reply=None, content_type="text/plain",
|
||||
headers=()):
|
||||
output = self.wfile
|
||||
if not reply:
|
||||
reply = MarkdownRequestHandler.status_map[code]
|
||||
try:
|
||||
self.send_response(code, MarkdownRequestHandler.status_map[code])
|
||||
for header in headers:
|
||||
self.send_header(*header)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header('Content-Length', len(reply))
|
||||
self.end_headers()
|
||||
output.write(reply.encode("UTF-8"))
|
||||
output.flush()
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
def markdown_answer(self):
|
||||
if not self.server.etag:
|
||||
self.server.condition_variable.acquire()
|
||||
self.server.update_file_digest()
|
||||
self.server.condition_variable.release()
|
||||
self.answer(200, headers=(('Etag', self.server.etag),),
|
||||
reply=compile_html(mdfile=self.server.mdfile, extensions=self.server.extensions, raw=True),
|
||||
content_type='text/html')
|
||||
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path)
|
||||
if path.path == '/':
|
||||
self.answer(200, reply=TEMPLATE.format(content='', css=create_css_tag(self.server.stylesheet)),
|
||||
content_type='text/html')
|
||||
elif path.path == '/markdown':
|
||||
self.markdown_answer()
|
||||
elif path.path == '/reload':
|
||||
if 'If-None-Match' not in self.headers or self.headers['If-None-Match'] != self.server.etag:
|
||||
self.markdown_answer()
|
||||
else:
|
||||
self.server.condition_variable.acquire()
|
||||
self.server.condition_variable.wait(timeout=10)
|
||||
self.server.condition_variable.release()
|
||||
if self.server.stop:
|
||||
self.answer(503)
|
||||
elif self.headers['If-None-Match'] == self.server.etag:
|
||||
self.answer(304)
|
||||
else:
|
||||
self.answer(200, headers=(('Etag', self.server.etag),),
|
||||
reply=compile_html(mdfile=self.server.mdfile,
|
||||
extensions=self.server.extensions,
|
||||
raw=True),
|
||||
content_type='text/html')
|
||||
else:
|
||||
self.answer(404)
|
||||
|
||||
def parse_args(args=None):
|
||||
parser = argparse.ArgumentParser(description='Make a complete, styled HTML document from a Markdown file.')
|
||||
@@ -68,7 +211,6 @@ def parse_args(args=None):
|
||||
|
||||
try:
|
||||
import inotify
|
||||
import flask
|
||||
import gevent
|
||||
import signal
|
||||
parser.add_argument('-w', '--watch', action='store_true',
|
||||
@@ -77,77 +219,27 @@ def parse_args(args=None):
|
||||
help='Specify http server port (defaults to 5000)')
|
||||
parser.add_argument('-i', '--interface', default='',
|
||||
help='Specify http server listen interface (defaults to localhost)')
|
||||
parser.add_argument('-s', '--stylesheet', nargs='+',
|
||||
default=['http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css'],
|
||||
help='Specify a list of stylesheet URLs to add to the html page')
|
||||
except ImportError:
|
||||
pass
|
||||
return parser.parse_args(args)
|
||||
|
||||
def compile_html(mdfile=None, extensions=None, raw=None, **kwargs):
|
||||
html = None
|
||||
with mdfile and open(mdfile, 'r') or sys.stdin as instream:
|
||||
html = markdown.markdown(instream.read(), extensions=extensions, output_format='html5')
|
||||
if raw:
|
||||
doc = html
|
||||
else:
|
||||
doc = TEMPLATE.format(**dict(content=html))
|
||||
return doc
|
||||
|
||||
def write_html(out=None, **kwargs):
|
||||
doc = compile_html(**kwargs)
|
||||
with (out and open(out, 'w')) or sys.stdout as outstream:
|
||||
outstream.write(doc)
|
||||
|
||||
def main(args=None):
|
||||
import signal
|
||||
args = parse_args(args)
|
||||
exit = False
|
||||
def sigint_handler(signum, frame):
|
||||
nonlocal exit
|
||||
exit = True
|
||||
handlers = (sigint_handler, signal.getsignal(signal.SIGINT))
|
||||
signal.signal(signal.SIGINT, lambda signum, frame: [handler(signum, frame) for handler in handlers])
|
||||
if hasattr(args, 'watch') and args.watch:
|
||||
import threading
|
||||
from flask import Flask, Response
|
||||
from gevent.pywsgi import WSGIServer
|
||||
condition_variable = threading.Condition()
|
||||
def watch_file():
|
||||
import inotify.adapters
|
||||
nonlocal condition_variable, exit
|
||||
watcher = inotify.adapters.Inotify()
|
||||
watcher.add_watch(dirname(args.mdfile))
|
||||
target_file = basename(args.mdfile)
|
||||
while True:
|
||||
if exit:
|
||||
break
|
||||
for event in watcher.event_gen(yield_nones=True, timeout_s=1):
|
||||
if not event:
|
||||
continue
|
||||
(_, event_type, path, filename) = event
|
||||
if filename == target_file and len(set(event_type).intersection(
|
||||
{'IN_CREATE', 'IN_MODIFY', 'IN_CLOSE_WRITE'})):
|
||||
condition_variable.acquire()
|
||||
condition_variable.notify_all()
|
||||
condition_variable.release()
|
||||
|
||||
file_watcher = threading.Thread(target=watch_file)
|
||||
file_watcher.start()
|
||||
app = Flask(__name__)
|
||||
@app.route('/')
|
||||
def get():
|
||||
return Response(compile_html(**vars(args)), mimetype='text/html')
|
||||
|
||||
@app.route("/stream")
|
||||
def stream():
|
||||
nonlocal condition_variable
|
||||
def gen():
|
||||
while True:
|
||||
condition_variable.acquire()
|
||||
condition_variable.wait()
|
||||
sse = ServerSentEvent(event='reload')
|
||||
return sse.encode()
|
||||
return Response(gen(), mimetype="text/event-stream")
|
||||
|
||||
server = WSGIServer((args.interface, args.port), app, environ={'wsgi.multithread': True})
|
||||
server = MarkdownHTTPServer(args.mdfile,
|
||||
extensions=args.extensions,
|
||||
stylesheet=args.stylesheet,
|
||||
interface=args.interface,
|
||||
port=args.port,
|
||||
handler=MarkdownRequestHandler)
|
||||
server.serve_forever()
|
||||
else:
|
||||
write_html(**vars(args))
|
||||
|
Reference in New Issue
Block a user