def serve(self): sock = get_systemd_socket() if sock is not None: log('Using inherited socket %r' % (sock.getsockname(), )) else: sock = self.get_listening_socket() self.serve_on_socket(sock)
def extract_session_id(self, conn): """Find a session indentifier for the current request. Return the ID and the SCGI environment dictionary. """ # we have to be careful here, we don't want to block the server # processing look while waiting on sockets. In practice this # implementation seems to work well (tested with nginx and Apache). env = {} # select is necessary since even with MSG_PEEK the recv() can block # if there is no data available r, w, e = select.select([conn], [], [], 0.2) if r: headers = conn.recv(4096, socket.MSG_PEEK) headers = ns_reads(io.BytesIO(headers)) env = parse_env(headers) cookies = env.get('HTTP_COOKIE') if cookies: m = re.search(self.SESSION_ID_PATTERN, cookies) if m: return m.group('id'), env else: log('gave up waiting to peek at session id') ip = env.get('HTTP_X_FORWARDED_FOR') or env.get('REMOTE_ADDR') if ip: return ip, env try: return conn.getpeername()[0], env except socket.error: return self.DEFAULT_SESSION_ID, env
def delegate_request(self, conn): """Pass a request fd to a child process to handle. This method blocks if all the children are busy and we have reached the max_children limit.""" # There lots of subtleties here. First, to determine the readiness # of a child we can't use the writable status of the Unix domain # socket connected to the child, since select will return true # if the buffer is not filled. Instead, each child writes one # byte of data when it is ready for a request. The normal case # is that a child is ready for a request. We want that case to # be fast. Also, we want to pass requests to the same child if # possible. Finally, we need to gracefully handle children # dying at any time. # If no children are ready and we haven't reached max_children # then we want another child to be started without delay. timeout = 0 # Number of times to retry delegating request. If no child can # be found after those tries, give up. In that case 'conn' will # be closed without handling it. retry_count = 30 for i in range(retry_count): fds = [child.fd for child in self.children if not child.closed] r, w, e = select.select(fds, [], [], timeout) if r: # One or more children look like they are ready. Sort # the file descriptions so that we keep preferring the # same child. child = None for child in self.children: if not child.closed and child.fd in r: break if child is None: continue # no child found, should not get here if child.process(conn): return # passed fd to child, we are done # didn't find any child, check if any died self.reap_children() # start more children if we haven't met max_children limit if len(self.children) < self.max_children: self.spawn_child(conn) # Start blocking inside select. We might have reached # max_children limit and they are all busy. timeout = 2 log('failed to delegate request %s' % conn)
def prune_children(self): n = len(self.children) if n == 0: return now = time.time() if now - self.last_prune < 20: return self.last_prune = now for child in list(self.children.values()): if self._is_old(child): log('closed old child (pid=%s nchild=%s)' % (child.pid, len(self.children) - 1)) child.close() del self.children[child.session_id] self.reap_children()
def delegate_request(self, conn): """Pass a request fd to a child process to handle. """ try: session_id, env = self.extract_session_id(conn) except Exception: log('error extracting session id, traceback follows') traceback.print_exc(file=sys.stderr) session_id = self.DEFAULT_SESSION_ID env = {} child = self.children.get(session_id) if child is None or child.closed: child = self.spawn_child(session_id, conn) log('process pid=%s %s %s %s' % (child.pid, env.get('REMOTE_ADDR'), env.get('REQUEST_METHOD'), env.get('REQUEST_URI'))) child.queue_request(conn)
def spawn_child(self, conn=None): parent_fd, child_fd = passfd.socketpair(socket.AF_UNIX, socket.SOCK_STREAM) # make child fd non-blocking flags = fcntl.fcntl(child_fd, fcntl.F_GETFL, 0) fcntl.fcntl(child_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) pid = os.fork() if pid == 0: if conn is not None: conn.close() # in the midst of handling a request, close # the connection in the child os.close(child_fd) self.socket.close() for child in self.children: child.close() self.handler_class(parent_fd).serve() sys.exit(0) else: os.close(parent_fd) self.children.append(Child(pid, child_fd)) log('started child (pid=%s nchild=%s)' % (pid, len(self.children)))
def log(self, msg): log('%s (pid=%s)' % (msg, self.pid))
def do_restart(self): log('restarting child processes') self.do_stop() self.restart = 0
def hup_signal(self, signum, frame): log('got HUP signal, scheduling restart') self.restart = 1