class TCPServerThread(PyThread): def __init__(self, port, ip='', max_clients=None, queue_capacity=None, stagger=None, enabled=True): PyThread.__init__(self) self.Sock = None self.Clients = TaskQueue(max_clients, capacity=queue_capacity, stagger=stagger) self.Port = port self.IP = ip self.Enabled = enabled self.Shutdown = False self.LastIdle = 0.0 def run(self): self.Sock = socket(AF_INET, SOCK_STREAM) self.Sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.Sock.bind((self.IP, self.Port)) if self.Enabled: self.enable() while not self.Shutdown: fd = self.Sock.fileno() #print("TCPServerThread: select...") r, w, e = select.select([fd], [fd], [fd], 1.0) #print("TCPServerThread: r,w,e:", r, w, e) if fd in r or fd in e or fd in w: csock, caddr = self.Sock.accept() task = self.createClientInterface(csock, caddr) #print ("TCPServerThread: client task created:", task) if task is not None: self.Clients.addTask(task) else: t = time.time() self.idle(t, self.LastIdle) self.LastIdle = t self.Sock.close() @synchronized def enable(self, backlog=5): if self.Sock is not None: self.Sock.listen(backlog) self.Clients.release() self.Enabled = True @synchronized def disable(self): self.Sock.listen(0) self.Clients.hold() self.Enabled = False def shutdown(self): self.Shutdown = True def waitForClients(self): self.Clients.waitUntilEmpty() # overridables def idle(self, now, last_idle): pass def createClientInterface(self, sock, addr): return None pass # virtual
class Service(Primitive, Logged): def __init__(self, config, logger=None): name = config["name"] #print("Service(): config:", config) self.ServiceName = name Primitive.__init__(self, name=f"[service {name}]") Logged.__init__(self, f"[app {name}]", logger, debug=True) self.Config = None self.Initialized = self.initialize(config) @synchronized def initialize(self, config=None): config = config or self.Config self.Config = config reload_files = config.get("touch_reload", []) if isinstance(reload_files, str): reload_files = [reload_files] self.ReloadFileTimestamps = { path: self.mtime(path) for path in reload_files } self.Prefix = config.get("prefix", "/") self.ReplacePrefix = config.get("replace_prefix") self.Timeout = config.get("timeout", 10) saved_path = sys.path[:] saved_modules = set(sys.modules.keys()) saved_environ = os.environ.copy() try: args = None if "file" in config: print( '*** Use of "file" parameter is deprecated. Use "module" instead' ) self.ScriptFileName = fname = config.get("module", config.get("file")) g = {} extra_path = config.get("python_path") if extra_path is not None: if isinstance(extra_path, str): extra_path = [extra_path] sys.path = extra_path + sys.path if "env" in config: os.environ.update(config["env"]) try: exec(open(fname, "r").read(), g) except: tb = traceback.format_exc() self.log_error(f"Error importing module {fname}:\n{tb}") return False if "create" in config: # deprecated print( '*** Use of "create" parameter is deprecated. Use "application: function()" instead' ) application = config["create"] + "()" else: application = config.get("application", "application") if application.endswith("()"): args = config.get("args") fcn_name = application[:-2] fcn = g.get(fcn_name) if fcn is None: self.log_error( f"Application creation function {fcn_name} not found in module {fname}" ) return False try: if isinstance(args, dict): app = fcn(**args) elif isinstance(args, (list, tuple)): app = fcn(*args) elif args is None: app = fcn() else: app = fcn(args) except: tb = traceback.format_exc() self.log_error( f"Error calling the application initialization function:\n{tb}" ) return False if app is None: self.log_error( f'Application creation function {fcn_name} returned None' ) return False else: app = g.get(application) if app is None: self.log_error( f'Application object "{application}" not found in {fname}' ) return False self.AppArgs = args self.WSGIApp = app max_workers = config.get("max_workers", 5) queue_capacity = config.get("queue_capacity", 10) self.RequestQueue = TaskQueue(max_workers, capacity=queue_capacity, delegate=self) self.log("initiaized") except: tb = traceback.format_exc() self.log_error(f"Error initializing application:\n{tb}") return False finally: sys.path = saved_path extra_modules = set(sys.modules.keys()) - set(saved_modules) #print("loadApp: removing modules:", sorted(list(extra_modules))) for m in extra_modules: del sys.modules[m] for n in set(os.environ.keys()) - set(saved_environ.keys()): del os.environ[n] os.environ.update(saved_environ) return True def taskFailed(self, queue, task, exc_type, exc_value, tb): self.log_error( "request failed:", "".join(traceback.format_exception(exc_type, exc_value, tb))) try: task.Request.close() except: pass def accept(self, request): #print(f"Service {self}: accept()") if not self.Initialized: return False header = request.HTTPHeader uri = header.URI self.debug("accept: uri:", uri, " prefix:", self.Prefix) #print("Sevice", self," accept: uri:", uri, " prefix:", self.Prefix) if uri.startswith(self.Prefix): uri = uri[len(self.Prefix):] if not uri.startswith("/"): uri = "/" + uri if self.ReplacePrefix: uri = self.ReplacePrefix + uri header.replaceURI(uri) request.AppName = self.ServiceName script_path = self.Prefix while script_path and script_path.endswith("/"): script_path = script_path[:-1] request.Environ["SCRIPT_NAME"] = script_path request.Environ["SCRIPT_FILENAME"] = self.ScriptFileName self.RequestQueue.addTask( RequestTask(self.WSGIApp, request, self.Logger)) #print("Service", self, " accepted") return True else: #print("Service", self, " rejected") return False def close(self): self.RequestQueue.hold() def join(self): self.RequestQueue.join() def mtime(self, path): try: return os.path.getmtime(path) except: return None def reloadIfNeeded(self): for path, old_timestamp in self.ReloadFileTimestamps.items(): mt = self.mtime(path) if mt is not None and mt != old_timestamp: ct = time.ctime(mt) self.log(f"file {path} was modified at {ct}") break else: return False self.Initialized = self.initialize()
class HTTPServer(PyThread, Logged): def __init__(self, port, app=None, services=[], sock=None, logger=None, max_connections=100, timeout=20.0, enabled=True, max_queued=100, logging=False, log_file="-", debug=None, certfile=None, keyfile=None, verify="none", ca_file=None, password=None): PyThread.__init__(self) self.Port = port self.Sock = sock assert self.Port is not None, "Port must be specified" if logger is None and logging: logger = Logger(log_file) #print("logs sent to:", f) Logged.__init__(self, f"[server {self.Port}]", logger, debug=True) self.Logger = logger self.Timeout = timeout max_connections = max_connections queue_capacity = max_queued self.RequestReaderQueue = TaskQueue(max_connections, capacity=queue_capacity, delegate=self) self.SocketWrapper = SSLSocketWrapper( certfile, keyfile, verify, ca_file, password) if keyfile else None if app is not None: services = [Service(app, logger)] self.Services = services self.Stop = False def close(self): self.Stop = True self.RequestReaderQueue.hold() def join(self): self.RequestReaderQueue.join() @staticmethod def from_config(config, services, logger=None, logging=False, log_file=None, debug=None): port = config["port"] timeout = config.get("timeout", 20.0) max_connections = config.get("max_connections", 100) queue_capacity = config.get("queue_capacity", 100) # TLS certfile = config.get("cert") keyfile = config.get("key") verify = config.get("verify", "none") ca_file = config.get("ca_file") password = config.get("password") #print("HTTPServer.from_config: services:", services) return HTTPServer(port, services=services, logger=logger, max_connections=max_connections, timeout=timeout, max_queued=queue_capacity, logging=logging, log_file=log_file, debug=debug, certfile=certfile, keyfile=keyfile, verify=verify, ca_file=ca_file, password=password) def setServices(self, services): self.Services = services def connectionCount(self): return len(self.Connections) def run(self): if self.Sock is None: # therwise use the socket supplied to the constructior self.Sock = socket(AF_INET, SOCK_STREAM) self.Sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.Sock.bind(('', self.Port)) self.Sock.listen(10) while not self.Stop: self.debug("--- accept loop port=%d start" % (self.Port, )) csock = None caddr = ('-', '-') try: csock, caddr = self.Sock.accept() self.connection_accepted(csock, caddr) except Exception as exc: #print(exc) if not self.Stop: self.debug("connection processing error: %s" % (traceback.format_exc(), )) self.log_error(caddr, "Error processing connection: %s" % (exc, )) if csock is not None: try: csock.close() except: pass self.debug("--- accept loop port=%d end" % (self.Port, )) if self.Stop: self.debug("stopped") try: self.Sock.close() except: pass self.Sock = None def connection_accepted(self, csock, caddr): # called externally by multiserver request = Request(self.Port, csock, caddr) self.debug("connection %s accepted from %s:%s" % (request.Id, caddr[0], caddr[1])) reader = RequestReader(self, request, self.SocketWrapper, self.Timeout, self.Logger) self.RequestReaderQueue << reader @synchronized def stop(self): self.Stop = True try: self.Sock.close() except: pass @synchronized def dispatch(self, request): for service in self.Services: if service.accept(request): return service else: return None def taskFailed(self, queue, task, exc_type, exc, tb): traceback.print_exception(exc_type, exc, tb)