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.append('') if self.file: meta.append('') with codecs.open(location + 'index.html', mode="r", encoding="utf-8") as open_file: buffer = open_file.read() buffer = re.sub(r'', '\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)