2
0

server.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. ''' Python Server implementation '''
  2. import codecs
  3. import errno
  4. import http.server
  5. import importlib.util
  6. import os
  7. import random
  8. import re
  9. import socket
  10. import socketserver
  11. import sys
  12. import threading
  13. import time
  14. import webbrowser
  15. import urllib.parse
  16. __version__ = '0.0.0'
  17. class HTTPRequestHandler(http.server.BaseHTTPRequestHandler):
  18. ''' HTTP Request Handler '''
  19. file = ""
  20. data = bytearray()
  21. folder = ""
  22. verbosity = 1
  23. mime_types_map = {
  24. '.html': 'text/html',
  25. '.js': 'text/javascript',
  26. '.css': 'text/css',
  27. '.png': 'image/png',
  28. '.gif': 'image/gif',
  29. '.jpg': 'image/jpeg',
  30. '.ico': 'image/x-icon',
  31. '.json': 'application/json',
  32. '.pb': 'application/octet-stream',
  33. '.ttf': 'font/truetype',
  34. '.otf': 'font/opentype',
  35. '.eot': 'application/vnd.ms-fontobject',
  36. '.woff': 'font/woff',
  37. '.woff2': 'application/font-woff2',
  38. '.svg': 'image/svg+xml'
  39. }
  40. def do_GET(self): # pylint: disable=invalid-name
  41. ''' Serve a GET request '''
  42. path = urllib.parse.urlparse(self.path).path
  43. status_code = 0
  44. headers = {}
  45. buffer = None
  46. if path in ('/', '/index.html'):
  47. meta = []
  48. meta.append('<meta name="type" content="Python">')
  49. meta.append('<meta name="version" content="' + __version__ + '">')
  50. if self.file:
  51. meta.append('<meta name="file" content="/data/' + self.file + '">')
  52. basedir = os.path.dirname(os.path.realpath(__file__))
  53. with codecs.open(basedir + '/index.html', mode="r", encoding="utf-8") as open_file:
  54. buffer = open_file.read()
  55. meta = '\n'.join(meta)
  56. buffer = re.sub(r'<meta name="version" content="\d+.\d+.\d+">', meta, buffer)
  57. buffer = buffer.encode('utf-8')
  58. headers['Content-Type'] = 'text/html'
  59. headers['Content-Length'] = len(buffer)
  60. status_code = 200
  61. elif path.startswith('/data/'):
  62. status_code = 404
  63. path = urllib.parse.unquote(path[len('/data/'):])
  64. if path == self.file and self.data:
  65. buffer = self.data
  66. else:
  67. basedir = os.path.realpath(self.folder)
  68. path = os.path.normpath(os.path.realpath(basedir + '/' + path))
  69. if os.path.commonprefix([basedir, path]) == basedir:
  70. if os.path.exists(path) and not os.path.isdir(path):
  71. with open(path, 'rb') as file:
  72. buffer = file.read()
  73. if buffer:
  74. headers['Content-Type'] = 'application/octet-stream'
  75. headers['Content-Length'] = len(buffer)
  76. status_code = 200
  77. else:
  78. status_code = 404
  79. basedir = os.path.dirname(os.path.realpath(__file__))
  80. path = os.path.normpath(os.path.realpath(basedir + path))
  81. if os.path.commonprefix([basedir, path]) == basedir:
  82. if os.path.exists(path) and not os.path.isdir(path):
  83. extension = os.path.splitext(path)[1]
  84. content_type = self.mime_types_map[extension]
  85. if content_type:
  86. with open(path, 'rb') as file:
  87. buffer = file.read()
  88. headers['Content-Type'] = content_type
  89. headers['Content-Length'] = len(buffer)
  90. status_code = 200
  91. if self.verbosity > 1:
  92. sys.stdout.write(str(status_code) + ' ' + self.command + ' ' + self.path + '\n')
  93. sys.stdout.flush()
  94. self.send_response(status_code)
  95. for key, value in headers.items():
  96. self.send_header(key, value)
  97. self.end_headers()
  98. if self.command != 'HEAD':
  99. if status_code == 404 and buffer is None:
  100. self.wfile.write(bytes(status_code))
  101. elif (status_code in (200, 404)) and buffer is not None:
  102. self.wfile.write(buffer)
  103. def do_HEAD(self): # pylint: disable=invalid-name
  104. ''' Serve a HEAD request '''
  105. self.do_GET()
  106. def log_message(self, format, *args): # pylint: disable=redefined-builtin
  107. return
  108. class HTTPServerThread(threading.Thread):
  109. ''' HTTP Server Thread '''
  110. def __init__(self, data, file, address, verbosity):
  111. threading.Thread.__init__(self)
  112. class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
  113. ''' Threaded HTTP Server '''
  114. self.verbosity = verbosity
  115. self.address = address
  116. self.url = 'http://' + address[0] + ':' + str(address[1])
  117. self.file = file
  118. self.server = ThreadedHTTPServer(address, HTTPRequestHandler)
  119. self.server.timeout = 0.25
  120. if file:
  121. folder = os.path.dirname(file) if os.path.dirname(file) else '.'
  122. self.server.RequestHandlerClass.folder = folder
  123. self.server.RequestHandlerClass.file = os.path.basename(file)
  124. else:
  125. self.server.RequestHandlerClass.folder = ''
  126. self.server.RequestHandlerClass.file = ''
  127. self.server.RequestHandlerClass.data = data
  128. self.server.RequestHandlerClass.verbosity = verbosity
  129. self.terminate_event = threading.Event()
  130. self.terminate_event.set()
  131. self.stop_event = threading.Event()
  132. def run(self):
  133. self.stop_event.clear()
  134. self.terminate_event.clear()
  135. try:
  136. while not self.stop_event.is_set():
  137. self.server.handle_request()
  138. except: # pylint: disable=bare-except
  139. pass
  140. self.terminate_event.set()
  141. self.stop_event.clear()
  142. def stop(self):
  143. ''' Stop server '''
  144. if self.alive():
  145. if self.verbosity > 0:
  146. sys.stdout.write("Stopping " + self.url + "\n")
  147. sys.stdout.flush()
  148. self.stop_event.set()
  149. self.server.server_close()
  150. self.terminate_event.wait(1000)
  151. def alive(self):
  152. ''' Check server status '''
  153. return not self.terminate_event.is_set()
  154. _thread_list = []
  155. def _add_thread(thread):
  156. global _thread_list
  157. _thread_list.append(thread)
  158. def _update_thread_list(address=None):
  159. global _thread_list
  160. _thread_list = [ thread for thread in _thread_list if thread.alive() ]
  161. threads = _thread_list
  162. if address is not None:
  163. address = _make_address(address)
  164. threads = [ _ for _ in threads if address[0] == _.address[0] ]
  165. if address[1]:
  166. threads = [ _ for _ in threads if address[1] == _.address[1] ]
  167. return threads
  168. def _make_address(address):
  169. if address is None or isinstance(address, int):
  170. port = address
  171. address = ('localhost', port)
  172. if isinstance(address, tuple) and len(address) == 2:
  173. host = address[0]
  174. port = address[1]
  175. if isinstance(host, str) and (port is None or isinstance(port, int)):
  176. return address
  177. raise ValueError('Invalid address.')
  178. def _make_port(address):
  179. if address[1] is None or address[1] == 0:
  180. ports = []
  181. if address[1] != 0:
  182. ports.append(8080)
  183. ports.append(8081)
  184. rnd = random.Random()
  185. for _ in range(4):
  186. port = rnd.randrange(15000, 25000)
  187. if port not in ports:
  188. ports.append(port)
  189. ports.append(0)
  190. for port in ports:
  191. temp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  192. temp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  193. temp_socket.settimeout(1)
  194. try:
  195. temp_socket.bind((address[0], port))
  196. sockname = temp_socket.getsockname()
  197. address = (address[0], sockname[1])
  198. return address
  199. except: # pylint: disable=bare-except
  200. pass
  201. finally:
  202. temp_socket.close()
  203. if isinstance(address[1], int):
  204. return address
  205. raise ValueError('Failed to allocate port.')
  206. def stop(address=None):
  207. '''Stop serving model at address.
  208. Args:
  209. address (tuple, optional): A (host, port) tuple, or a port number.
  210. '''
  211. threads = _update_thread_list(address)
  212. for thread in threads:
  213. thread.stop()
  214. _update_thread_list()
  215. def status(adrress=None):
  216. '''Is model served at address.
  217. Args:
  218. address (tuple, optional): A (host, port) tuple, or a port number.
  219. '''
  220. threads = _update_thread_list(adrress)
  221. return len(threads) > 0
  222. def wait():
  223. '''Wait for console exit and stop all model servers.'''
  224. try:
  225. while len(_update_thread_list()) > 0:
  226. time.sleep(1000)
  227. except (KeyboardInterrupt, SystemExit):
  228. sys.stdout.write('\n')
  229. sys.stdout.flush()
  230. stop()
  231. def serve(file, data, address=None, browse=False, verbosity=1):
  232. '''Start serving model from file or data buffer at address and open in web browser.
  233. Args:
  234. file (string): Model file to serve. Required to detect format.
  235. data (bytes): Model data to serve. None will load data from file.
  236. address (tuple, optional): A (host, port) tuple, or a port number.
  237. browse (bool, optional): Launch web browser. Default: True
  238. log (bool, optional): Log details to console. Default: False
  239. Returns:
  240. A (host, port) address tuple.
  241. '''
  242. verbosity = { '0': 0, 'quiet': 0, '1': 1, 'default': 1, '2': 2, 'debug': 2 }[str(verbosity)]
  243. if not data and file and not os.path.exists(file):
  244. raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), file)
  245. if data and not isinstance(data, bytearray) and isinstance(data.__class__, type):
  246. registry = dict([
  247. ('onnx.onnx_ml_pb2.ModelProto', '.onnx'),
  248. ('torch.Graph', '.pytorch'),
  249. ('torch._C.Graph', '.pytorch'),
  250. ('torch.nn.modules.module.Module', '.pytorch')
  251. ])
  252. queue = [ data.__class__ ]
  253. while len(queue) > 0:
  254. current = queue.pop(0)
  255. if current.__module__ and current.__name__:
  256. name = current.__module__ + '.' + current.__name__
  257. if name in registry:
  258. module_name = registry[name]
  259. if module_name.startswith('.'):
  260. file = os.path.join(os.path.dirname(__file__), module_name[1:] + '.py')
  261. spec = importlib.util.spec_from_file_location(module_name, file)
  262. module = importlib.util.module_from_spec(spec)
  263. spec.loader.exec_module(module)
  264. else:
  265. module = __import__(module_name)
  266. model_factory = module.ModelFactory()
  267. if verbosity > 1:
  268. sys.stdout.write('Experimental\n')
  269. sys.stdout.flush()
  270. data = model_factory.serialize(data)
  271. file = 'test.json'
  272. break
  273. for base in current.__bases__:
  274. if isinstance(base, type):
  275. queue.append(base)
  276. _update_thread_list()
  277. address = _make_address(address)
  278. if isinstance(address[1], int) and address[1] != 0:
  279. stop(address)
  280. else:
  281. address = _make_port(address)
  282. _update_thread_list()
  283. thread = HTTPServerThread(data, file, address, verbosity)
  284. thread.start()
  285. while not thread.alive():
  286. time.sleep(10)
  287. _add_thread(thread)
  288. if file:
  289. if verbosity > 0:
  290. sys.stdout.write("Serving '" + file + "' at " + thread.url + "\n")
  291. sys.stdout.flush()
  292. else:
  293. if verbosity > 0:
  294. sys.stdout.write("Serving at " + thread.url + "\n")
  295. sys.stdout.flush()
  296. if browse:
  297. webbrowser.open(thread.url)
  298. return address
  299. def start(file=None, address=None, browse=True, verbosity=1):
  300. '''Start serving model file at address and open in web browser.
  301. Args:
  302. file (string): Model file to serve.
  303. log (bool, optional): Log details to console. Default: False
  304. browse (bool, optional): Launch web browser, Default: True
  305. address (tuple, optional): A (host, port) tuple, or a port number.
  306. Returns:
  307. A (host, port) address tuple.
  308. '''
  309. return serve(file, None, browse=browse, address=address, verbosity=verbosity)