#!/usr/bin/env python
# coding: utf-8
import time
from errno import EAGAIN, EMFILE, ENOTCONN, EPIPE, EWOULDBLOCK # noqa: F401 # 保持模块符号面
from functools import lru_cache, partial # noqa: F401 # 保持模块符号面
from io import DEFAULT_BUFFER_SIZE, BufferedRWPair, RawIOBase # noqa: F401 # 保持模块符号面
from traceback import print_exc # noqa: F401 # 保持模块符号面
from urllib.parse import unquote_plus # noqa: F401 # 保持模块符号面
from email.message import Message # noqa: F401 # 保持模块符号面
from typing import Dict, List, Optional, Any, Tuple # noqa: F401 # 保持模块符号面
try:
from greenlet import GreenletExit, getcurrent, greenlet # noqa: F401 # 保持模块符号面
HAS_GREENLET = True
except ImportError:
HAS_GREENLET = False
greenlet = None
getcurrent = None
GreenletExit = None
try:
from select import EPOLLERR, EPOLLET, EPOLLHUP, EPOLLIN, EPOLLOUT # noqa: F401
from select import epoll as select_epoll # noqa: F401
HAS_EPOLL = True
except (ImportError, AttributeError):
HAS_EPOLL = False
EPOLLIN = EPOLLOUT = EPOLLHUP = EPOLLERR = EPOLLET = 0
select_epoll = None
from .._json import dump, load
from ..exceptions import HttpError # noqa: F401 # 对外再导出
from ..handlers import RequestHandler, parse_form, server_software # noqa: F401 # 对外再导出
import array # noqa: F401 # 保持模块符号面
import traceback # noqa: F401 # 保持模块符号面
import logging
import socket
import os
import signal
import select
import tempfile
from .greenlet_server_base import Epoll # noqa: F401 # 内部拆分模块再导出
from .greenlet_server_base import SocketIO # noqa: F401 # 内部拆分模块再导出
from .greenlet_server_base import TCPServer # noqa: F401 # 内部拆分模块再导出
from .greenlet_server_base import mainloop # noqa: F401 # 内部拆分模块再导出
from .greenlet_server_base import make_environ # noqa: F401 # 内部拆分模块再导出
from .greenlet_server_base import make_headers # noqa: F401 # 内部拆分模块再导出
from .greenlet_server_base import parse_header # noqa: F401 # 内部拆分模块再导出
from .greenlet_server_base import should_retry_error # noqa: F401 # 内部拆分模块再导出
from .greenlet_server_base import epoll # noqa: F401 # 内部拆分模块再导出
from . import greenlet_server_base as _greenlet_server_base # 内部拆分模块句柄
[文档]
class HTTPServer(TCPServer):
allow_reuse_address = 1
max_request_size = 10485760
[文档]
def server_bind(self):
TCPServer.server_bind(self)
host, port = self.socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
[文档]
class ProcessHTTPServer(HTTPServer):
"""多进程 HTTP 服务器(改进版)
改进点:
1. 使用 PID 文件管理进程,避免残留
2. Master-Worker 架构,Master 负责管理 Worker
3. 优雅关闭,先停止接收新连接,等待现有请求完成
4. 信号管道通信,确保信号可靠传递
"""
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, processes=4):
super().__init__(server_address, RequestHandlerClass, False)
self.processes = processes
self.workers = []
self._shutdown_requested = False
self._master_pid = os.getpid()
self._worker_pids = []
self._signal_pipe_r = None
self._signal_pipe_w = None
self._listen_sock = None
def _get_pid_file(self):
"""获取 PID 文件路径"""
host, port = self.server_address
pid_dir = os.path.join(tempfile.gettempdir(), 'litefs')
os.makedirs(pid_dir, exist_ok=True)
return os.path.join(pid_dir, f'litefs_{host}_{port}.pid')
def _write_pid_file(self):
"""写入 PID 文件"""
pid_file = self._get_pid_file()
pid_data = {
'master_pid': os.getpid(),
'worker_pids': self._worker_pids,
'port': self.server_address[1],
'host': self.server_address[0],
'starttime': time.time()
}
try:
with open(pid_file, 'wb') as f:
dump(pid_data, f)
except Exception as e:
logging.warning(f"无法写入 PID 文件: {e}")
def _read_pid_file(self):
"""读取 PID 文件"""
pid_file = self._get_pid_file()
try:
if os.path.exists(pid_file):
with open(pid_file, 'rb') as f:
return load(f)
except Exception:
pass
return None
def _remove_pid_file(self):
"""删除 PID 文件"""
pid_file = self._get_pid_file()
try:
if os.path.exists(pid_file):
os.remove(pid_file)
except Exception:
pass
def _is_process_alive(self, pid):
"""检查进程是否存活"""
try:
os.kill(pid, 0)
return True
except OSError:
return False
def _kill_stale_processes(self):
"""清理残留进程"""
pid_data = self._read_pid_file()
if not pid_data:
return
master_pid = pid_data.get('master_pid')
worker_pids = pid_data.get('worker_pids', [])
all_pids = [master_pid] + worker_pids
all_pids = [p for p in all_pids if p and p != os.getpid()]
for pid in all_pids:
if self._is_process_alive(pid):
logging.info(f"清理残留进程: PID {pid}")
try:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
if self._is_process_alive(pid):
os.kill(pid, signal.SIGKILL)
except OSError:
pass
self._remove_pid_file()
time.sleep(0.5)
def _setup_signal_handlers(self):
"""设置信号处理器"""
def handle_signal(signum, frame):
if self._signal_pipe_w is not None:
try:
os.write(self._signal_pipe_w, b'1')
except OSError:
pass
signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
[文档]
def server_forever(self, poll_interval=0.1):
"""启动多进程服务器"""
self._kill_stale_processes()
self._signal_pipe_r, self._signal_pipe_w = os.pipe()
self.server_bind()
self.server_activate()
self._listen_sock = self.socket
self._setup_signal_handlers()
for i in range(self.processes):
worker_pid = os.fork()
if worker_pid == 0:
self._run_worker(i, poll_interval)
os._exit(0)
else:
self._worker_pids.append(worker_pid)
logging.info(f"启动工作进程 {i}, PID: {worker_pid}")
self._write_pid_file()
try:
self._master_loop(poll_interval)
except Exception as e:
logging.error(f"Master 进程错误: {e}")
finally:
self._shutdown_workers()
self._remove_pid_file()
def _master_loop(self, poll_interval):
"""Master 进程主循环"""
while not self._shutdown_requested:
try:
rlist, _, _ = select.select([self._signal_pipe_r], [], [], poll_interval)
if rlist:
self._shutdown_requested = True
break
self._check_workers()
except KeyboardInterrupt:
self._shutdown_requested = True
break
except Exception as e:
logging.error(f"Master 循环错误: {e}")
def _check_workers(self):
"""检查工作进程状态,自动重启意外退出的进程"""
for i, pid in enumerate(self._worker_pids):
if pid and not self._is_process_alive(pid):
logging.warning(f"工作进程 {i} (PID: {pid}) 意外退出,正在重启...")
new_pid = os.fork()
if new_pid == 0:
self._run_worker(i, 0.1)
os._exit(0)
else:
self._worker_pids[i] = new_pid
self._write_pid_file()
logging.info(f"重启工作进程 {i}, 新 PID: {new_pid}")
def _run_worker(self, worker_id, poll_interval):
"""运行工作进程"""
signal.signal(signal.SIGTERM, signal.SIG_DFL)
signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
logging.info(f"工作进程 {worker_id} 启动,PID: {os.getpid()}")
global epoll
epoll = Epoll() if HAS_EPOLL else None
# 拆分后 epoll 单例由 greenlet_server_base 持有:worker 重建实例后必须同步,
# 确保 TCPServer/SocketIO/mainloop 与本方法操作同一 epoll 实例(与拆分前行为一致)
_greenlet_server_base.epoll = epoll
if HAS_EPOLL:
epoll.register(self)
self._started = True
try:
while not self._shutdown_requested:
epoll.poll(poll_interval=poll_interval)
except KeyboardInterrupt:
pass
except Exception as e:
logging.error(f"工作进程 {worker_id} 错误: {e}")
finally:
try:
epoll.unregister(self)
except Exception:
pass
else:
try:
while not self._shutdown_requested:
self.handle_request()
except KeyboardInterrupt:
pass
def _shutdown_workers(self):
"""关闭所有工作进程"""
if not self._worker_pids:
return
logging.info("正在关闭工作进程...")
for pid in self._worker_pids:
if pid and self._is_process_alive(pid):
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
timeout = 5
start_time = time.time()
while time.time() - start_time < timeout:
all_dead = True
for pid in self._worker_pids:
if pid and self._is_process_alive(pid):
all_dead = False
break
if all_dead:
break
time.sleep(0.1)
for pid in self._worker_pids:
if pid and self._is_process_alive(pid):
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
self._worker_pids = []
logging.info("所有工作进程已关闭")
[文档]
def shutdown(self):
"""关闭服务器"""
self._shutdown_requested = True
if os.getpid() == self._master_pid:
self._shutdown_workers()
self._remove_pid_file()
try:
self.server_close()
except Exception:
pass
if self._signal_pipe_r is not None:
try:
os.close(self._signal_pipe_r)
except OSError:
pass
if self._signal_pipe_w is not None:
try:
os.close(self._signal_pipe_w)
except OSError:
pass
[文档]
class WSGIServer(HTTPServer):
application = None
[文档]
def server_bind(self):
HTTPServer.server_bind(self)
self.setup_environ()
[文档]
def setup_environ(self):
env = {}
env["SERVER_NAME"] = self.server_name
env["SERVER_PORT"] = str(self.server_port)
env["REMOTE_HOST"] = ""
env["CONTENT_LENGTH"] = -1
env["SCRIPT_NAME"] = ""
self.base_environ = env
[文档]
def get_app(self):
return self.application
[文档]
def set_app(self, application):
self.application = application
server_forever = _greenlet_server_base.server_forever