You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
10 KiB
Python

import codecs
import errno
import os
import random
import re
import socket
import sys
import threading
import webbrowser
import time
from .__version__ import __version__
if sys.version_info[0] > 2:
from urllib.parse import urlparse
from urllib.parse import unquote
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
else:
from urlparse import urlparse
from urlparse import unquote
from BaseHTTPServer import HTTPServer
from BaseHTTPServer import BaseHTTPRequestHandler
from SocketServer import ThreadingMixIn
class HTTPRequestHandler(BaseHTTPRequestHandler):
def handler(self):
if not hasattr(self, 'mime_types_map'):
self.mime_types_map = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.png': 'image/png',
'.gif': 'image/gif',
'.jpg': 'image/jpeg',
'.ico': 'image/x-icon',
'.json': 'application/json',
'.pb': 'application/octet-stream',
'.ttf': 'font/truetype',
'.otf': 'font/opentype',
'.eot': 'application/vnd.ms-fontobject',
'.woff': 'font/woff',
'.woff2': 'application/font-woff2',
'.svg': 'image/svg+xml'
}
pathname = urlparse(self.path).path
folder = os.path.dirname(os.path.realpath(__file__))
location = folder + pathname
status_code = 0
headers = {}
buffer = None
data = '/data/'
if status_code == 0:
if pathname == '/':
meta = []
meta.append('<meta name="type" content="Python">')
meta.append('<meta name="version" content="' + __version__ + '">')
if self.file:
meta.append('<meta name="file" content="/data/' + self.file + '">')
with codecs.open(location + 'index.html', mode="r", encoding="utf-8") as open_file:
buffer = open_file.read()
buffer = re.sub(r'<meta name="version" content="\d+.\d+.\d+">', '\n'.join(meta), buffer)
buffer = buffer.encode('utf-8')
headers['Content-Type'] = 'text/html'
headers['Content-Length'] = len(buffer)
status_code = 200
elif pathname.startswith(data):
file = pathname[len(data):]
if file == self.file and self.data:
buffer = self.data
else:
file = self.folder + '/' + unquote(file)
status_code = 404
if os.path.exists(file):
with open(file, 'rb') as binary:
buffer = binary.read()
if buffer:
headers['Content-Type'] = 'application/octet-stream'
headers['Content-Length'] = len(buffer)
status_code = 200
else:
if os.path.exists(location) and not os.path.isdir(location):
extension = os.path.splitext(location)[1]
content_type = self.mime_types_map[extension]
if content_type:
with open(location, 'rb') as binary:
buffer = binary.read()
headers['Content-Type'] = content_type
headers['Content-Length'] = len(buffer)
status_code = 200
else:
status_code = 404
if self.log:
sys.stdout.write(str(status_code) + ' ' + self.command + ' ' + self.path + '\n')
sys.stdout.flush()
self.send_response(status_code)
for key in headers:
self.send_header(key, headers[key])
self.end_headers()
if self.command != 'HEAD':
if status_code == 404 and buffer is None:
self.wfile.write(bytes(status_code))
elif (status_code in (200, 404)) and buffer is not None:
self.wfile.write(buffer)
def do_GET(self):
self.handler()
def do_HEAD(self):
self.handler()
def log_message(self, format, *args):
return
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
pass
class HTTPServerThread(threading.Thread):
def __init__(self, data, file, address, log):
threading.Thread.__init__(self)
self.address = address
self.url = 'http://' + address[0] + ':' + str(address[1])
self.file = file
self.server = ThreadedHTTPServer(address, HTTPRequestHandler)
self.server.timeout = 0.25
if file:
self.server.RequestHandlerClass.folder = os.path.dirname(file) if os.path.dirname(file) else '.'
self.server.RequestHandlerClass.file = os.path.basename(file)
else:
self.server.RequestHandlerClass.folder = ''
self.server.RequestHandlerClass.file = ''
self.server.RequestHandlerClass.data = data
self.server.RequestHandlerClass.log = log
self.terminate_event = threading.Event()
self.terminate_event.set()
self.stop_event = threading.Event()
def run(self):
self.stop_event.clear()
self.terminate_event.clear()
try:
while not self.stop_event.is_set():
self.server.handle_request()
except Exception:
pass
self.terminate_event.set()
self.stop_event.clear()
def stop(self):
if self.alive():
sys.stdout.write("Stopping " + self.url + "\n")
self.stop_event.set()
self.server.server_close()
self.terminate_event.wait(1000)
def alive(self):
return not self.terminate_event.is_set()
_thread_list = []
def _add_thread(thread):
global _thread_list
_thread_list.append(thread)
def _update_thread_list(address=None):
global _thread_list
_thread_list = [ thread for thread in _thread_list if thread.alive() ]
threads = _thread_list
if address is not None:
address = _make_address(address)
if address[1] is None:
threads = [ thread for thread in threads if address[0] == thread.address[0] ]
else:
threads = [ thread for thread in threads if address[0] == thread.address[0] and address[1] == thread.address[1] ]
return threads
def _make_address(address):
if address is None or isinstance(address, int):
port = address
address = ('localhost', port)
if isinstance(address, tuple) and len(address) == 2:
host = address[0]
port = address[1]
if isinstance(host, str) and (port is None or isinstance(port, int)):
return address
raise ValueError('Invalid address.')
def _make_port(address):
if address[1] is None or address[1] == 0:
ports = []
if address[1] != 0:
ports.append(8080)
ports.append(8081)
rnd = random.Random()
for _ in range(4):
port = rnd.randrange(15000, 25000)
if port not in ports:
ports.append(port)
ports.append(0)
for port in ports:
temp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
temp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
temp_socket.settimeout(1)
try:
temp_socket.bind((address[0], port))
sockname = temp_socket.getsockname()
address = (address[0], sockname[1])
return address
except:
pass
finally:
temp_socket.close()
if isinstance(address[1], int):
return address
raise ValueError('Failed to allocate port.')
def stop(address=None):
'''Stop serving model at address.
Args:
address (tuple, optional): A (host, port) tuple, or a port number.
'''
threads = _update_thread_list(address)
for thread in threads:
thread.stop()
_update_thread_list()
def status(adrress=None):
'''Is model served at address.
Args:
address (tuple, optional): A (host, port) tuple, or a port number.
'''
threads = _update_thread_list(adrress)
return len(threads) > 0
def wait():
'''Wait for console exit and stop all model servers.'''
try:
while len(_update_thread_list()) > 0:
time.sleep(1000)
except (KeyboardInterrupt, SystemExit):
sys.stdout.write('\n')
sys.stdout.flush()
stop()
def serve(file, data, address=None, browse=False, log=False):
'''Start serving model from file or data buffer at address and open in web browser.
Args:
file (string): Model file to serve. Required to detect format.
data (bytes): Model data to serve. None will load data from file.
log (bool, optional): Log details to console. Default: False
browse (bool, optional): Launch web browser. Default: True
address (tuple, optional): A (host, port) tuple, or a port number.
Returns:
A (host, port) address tuple.
'''
if not data and file and not os.path.exists(file):
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), file)
_update_thread_list()
address = _make_address(address)
if isinstance(address[1], int) and address[1] != 0:
stop(address)
else:
address = _make_port(address)
_update_thread_list()
thread = HTTPServerThread(data, file, address, log)
thread.start()
while not thread.alive():
time.sleep(10)
_add_thread(thread)
if file:
sys.stdout.write("Serving '" + file + "' at " + thread.url + "\n")
else:
sys.stdout.write("Serving at " + thread.url + "\n")
sys.stdout.flush()
if browse:
webbrowser.open(thread.url)
return address
def start(file=None, address=None, browse=True, log=False):
'''Start serving model file at address and open in web browser.
Args:
file (string): Model file to serve.
log (bool, optional): Log details to console. Default: False
browse (bool, optional): Launch web browser, Default: True
address (tuple, optional): A (host, port) tuple, or a port number.
Returns:
A (host, port) address tuple.
'''
return serve(file, None, browse=browse, address=address, log=log)