mirror of
https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git
synced 2025-08-14 00:25:46 +02:00
Проверка 09.02.2025
This commit is contained in:
@@ -1,21 +1,16 @@
|
||||
from typing import TYPE_CHECKING, Type
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from uvicorn.supervisors.basereload import BaseReload
|
||||
from uvicorn.supervisors.multiprocess import Multiprocess
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ChangeReload: Type[BaseReload]
|
||||
ChangeReload: type[BaseReload]
|
||||
else:
|
||||
try:
|
||||
from uvicorn.supervisors.watchfilesreload import (
|
||||
WatchFilesReload as ChangeReload,
|
||||
)
|
||||
from uvicorn.supervisors.watchfilesreload import WatchFilesReload as ChangeReload
|
||||
except ImportError: # pragma: no cover
|
||||
try:
|
||||
from uvicorn.supervisors.watchgodreload import (
|
||||
WatchGodReload as ChangeReload,
|
||||
)
|
||||
except ImportError:
|
||||
from uvicorn.supervisors.statreload import StatReload as ChangeReload
|
||||
from uvicorn.supervisors.statreload import StatReload as ChangeReload
|
||||
|
||||
__all__ = ["Multiprocess", "ChangeReload"]
|
||||
|
@@ -5,10 +5,11 @@ import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
from types import FrameType
|
||||
from typing import Callable, Iterator
|
||||
from typing import Callable
|
||||
|
||||
import click
|
||||
|
||||
@@ -38,14 +39,14 @@ class BaseReload:
|
||||
self.is_restarting = False
|
||||
self.reloader_name: str | None = None
|
||||
|
||||
def signal_handler(self, sig: int, frame: FrameType | None) -> None:
|
||||
def signal_handler(self, sig: int, frame: FrameType | None) -> None: # pragma: full coverage
|
||||
"""
|
||||
A signal handler that is registered with the parent process.
|
||||
"""
|
||||
if sys.platform == "win32" and self.is_restarting:
|
||||
self.is_restarting = False # pragma: py-not-win32
|
||||
self.is_restarting = False
|
||||
else:
|
||||
self.should_exit.set() # pragma: py-win32
|
||||
self.should_exit.set()
|
||||
|
||||
def run(self) -> None:
|
||||
self.startup()
|
||||
@@ -81,9 +82,7 @@ class BaseReload:
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self.signal_handler)
|
||||
|
||||
self.process = get_subprocess(
|
||||
config=self.config, target=self.target, sockets=self.sockets
|
||||
)
|
||||
self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets)
|
||||
self.process.start()
|
||||
|
||||
def restart(self) -> None:
|
||||
@@ -95,9 +94,7 @@ class BaseReload:
|
||||
self.process.terminate()
|
||||
self.process.join()
|
||||
|
||||
self.process = get_subprocess(
|
||||
config=self.config, target=self.target, sockets=self.sockets
|
||||
)
|
||||
self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets)
|
||||
self.process.start()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
@@ -110,10 +107,8 @@ class BaseReload:
|
||||
for sock in self.sockets:
|
||||
sock.close()
|
||||
|
||||
message = "Stopping reloader process [{}]".format(str(self.pid))
|
||||
color_message = "Stopping reloader process [{}]".format(
|
||||
click.style(str(self.pid), fg="cyan", bold=True)
|
||||
)
|
||||
message = f"Stopping reloader process [{str(self.pid)}]"
|
||||
color_message = "Stopping reloader process [{}]".format(click.style(str(self.pid), fg="cyan", bold=True))
|
||||
logger.info(message, extra={"color_message": color_message})
|
||||
|
||||
def should_restart(self) -> list[Path] | None:
|
||||
|
@@ -4,24 +4,101 @@ import logging
|
||||
import os
|
||||
import signal
|
||||
import threading
|
||||
from multiprocessing.context import SpawnProcess
|
||||
from multiprocessing import Pipe
|
||||
from socket import socket
|
||||
from types import FrameType
|
||||
from typing import Callable
|
||||
from typing import Any, Callable
|
||||
|
||||
import click
|
||||
|
||||
from uvicorn._subprocess import get_subprocess
|
||||
from uvicorn.config import Config
|
||||
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||
)
|
||||
SIGNALS = {
|
||||
getattr(signal, f"SIG{x}"): x
|
||||
for x in "INT TERM BREAK HUP QUIT TTIN TTOU USR1 USR2 WINCH".split()
|
||||
if hasattr(signal, f"SIG{x}")
|
||||
}
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
|
||||
|
||||
class Process:
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
target: Callable[[list[socket] | None], None],
|
||||
sockets: list[socket],
|
||||
) -> None:
|
||||
self.real_target = target
|
||||
|
||||
self.parent_conn, self.child_conn = Pipe()
|
||||
self.process = get_subprocess(config, self.target, sockets)
|
||||
|
||||
def ping(self, timeout: float = 5) -> bool:
|
||||
self.parent_conn.send(b"ping")
|
||||
if self.parent_conn.poll(timeout):
|
||||
self.parent_conn.recv()
|
||||
return True
|
||||
return False
|
||||
|
||||
def pong(self) -> None:
|
||||
self.child_conn.recv()
|
||||
self.child_conn.send(b"pong")
|
||||
|
||||
def always_pong(self) -> None:
|
||||
while True:
|
||||
self.pong()
|
||||
|
||||
def target(self, sockets: list[socket] | None = None) -> Any: # pragma: no cover
|
||||
if os.name == "nt": # pragma: py-not-win32
|
||||
# Windows doesn't support SIGTERM, so we use SIGBREAK instead.
|
||||
# And then we raise SIGTERM when SIGBREAK is received.
|
||||
# https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/signal?view=msvc-170
|
||||
signal.signal(
|
||||
signal.SIGBREAK, # type: ignore[attr-defined]
|
||||
lambda sig, frame: signal.raise_signal(signal.SIGTERM),
|
||||
)
|
||||
|
||||
threading.Thread(target=self.always_pong, daemon=True).start()
|
||||
return self.real_target(sockets)
|
||||
|
||||
def is_alive(self, timeout: float = 5) -> bool:
|
||||
if not self.process.is_alive():
|
||||
return False # pragma: full coverage
|
||||
|
||||
return self.ping(timeout)
|
||||
|
||||
def start(self) -> None:
|
||||
self.process.start()
|
||||
|
||||
def terminate(self) -> None:
|
||||
if self.process.exitcode is None: # Process is still running
|
||||
assert self.process.pid is not None
|
||||
if os.name == "nt": # pragma: py-not-win32
|
||||
# Windows doesn't support SIGTERM.
|
||||
# So send SIGBREAK, and then in process raise SIGTERM.
|
||||
os.kill(self.process.pid, signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined]
|
||||
else:
|
||||
os.kill(self.process.pid, signal.SIGTERM)
|
||||
logger.info(f"Terminated child process [{self.process.pid}]")
|
||||
|
||||
self.parent_conn.close()
|
||||
self.child_conn.close()
|
||||
|
||||
def kill(self) -> None:
|
||||
# In Windows, the method will call `TerminateProcess` to kill the process.
|
||||
# In Unix, the method will send SIGKILL to the process.
|
||||
self.process.kill()
|
||||
|
||||
def join(self) -> None:
|
||||
logger.info(f"Waiting for child process [{self.process.pid}]")
|
||||
self.process.join()
|
||||
|
||||
@property
|
||||
def pid(self) -> int | None:
|
||||
return self.process.pid
|
||||
|
||||
|
||||
class Multiprocess:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -32,45 +109,114 @@ class Multiprocess:
|
||||
self.config = config
|
||||
self.target = target
|
||||
self.sockets = sockets
|
||||
self.processes: list[SpawnProcess] = []
|
||||
|
||||
self.processes_num = config.workers
|
||||
self.processes: list[Process] = []
|
||||
|
||||
self.should_exit = threading.Event()
|
||||
self.pid = os.getpid()
|
||||
|
||||
def signal_handler(self, sig: int, frame: FrameType | None) -> None:
|
||||
"""
|
||||
A signal handler that is registered with the parent process.
|
||||
"""
|
||||
self.should_exit.set()
|
||||
self.signal_queue: list[int] = []
|
||||
for sig in SIGNALS:
|
||||
signal.signal(sig, lambda sig, frame: self.signal_queue.append(sig))
|
||||
|
||||
def run(self) -> None:
|
||||
self.startup()
|
||||
self.should_exit.wait()
|
||||
self.shutdown()
|
||||
|
||||
def startup(self) -> None:
|
||||
message = "Started parent process [{}]".format(str(self.pid))
|
||||
color_message = "Started parent process [{}]".format(
|
||||
click.style(str(self.pid), fg="cyan", bold=True)
|
||||
)
|
||||
logger.info(message, extra={"color_message": color_message})
|
||||
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self.signal_handler)
|
||||
|
||||
for _idx in range(self.config.workers):
|
||||
process = get_subprocess(
|
||||
config=self.config, target=self.target, sockets=self.sockets
|
||||
)
|
||||
def init_processes(self) -> None:
|
||||
for _ in range(self.processes_num):
|
||||
process = Process(self.config, self.target, self.sockets)
|
||||
process.start()
|
||||
self.processes.append(process)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
def terminate_all(self) -> None:
|
||||
for process in self.processes:
|
||||
process.terminate()
|
||||
|
||||
def join_all(self) -> None:
|
||||
for process in self.processes:
|
||||
process.join()
|
||||
|
||||
message = "Stopping parent process [{}]".format(str(self.pid))
|
||||
color_message = "Stopping parent process [{}]".format(
|
||||
click.style(str(self.pid), fg="cyan", bold=True)
|
||||
)
|
||||
def restart_all(self) -> None:
|
||||
for idx, process in enumerate(self.processes):
|
||||
process.terminate()
|
||||
process.join()
|
||||
new_process = Process(self.config, self.target, self.sockets)
|
||||
new_process.start()
|
||||
self.processes[idx] = new_process
|
||||
|
||||
def run(self) -> None:
|
||||
message = f"Started parent process [{os.getpid()}]"
|
||||
color_message = "Started parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True))
|
||||
logger.info(message, extra={"color_message": color_message})
|
||||
|
||||
self.init_processes()
|
||||
|
||||
while not self.should_exit.wait(0.5):
|
||||
self.handle_signals()
|
||||
self.keep_subprocess_alive()
|
||||
|
||||
self.terminate_all()
|
||||
self.join_all()
|
||||
|
||||
message = f"Stopping parent process [{os.getpid()}]"
|
||||
color_message = "Stopping parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True))
|
||||
logger.info(message, extra={"color_message": color_message})
|
||||
|
||||
def keep_subprocess_alive(self) -> None:
|
||||
if self.should_exit.is_set():
|
||||
return # parent process is exiting, no need to keep subprocess alive
|
||||
|
||||
for idx, process in enumerate(self.processes):
|
||||
if process.is_alive():
|
||||
continue
|
||||
|
||||
process.kill() # process is hung, kill it
|
||||
process.join()
|
||||
|
||||
if self.should_exit.is_set():
|
||||
return # pragma: full coverage
|
||||
|
||||
logger.info(f"Child process [{process.pid}] died")
|
||||
process = Process(self.config, self.target, self.sockets)
|
||||
process.start()
|
||||
self.processes[idx] = process
|
||||
|
||||
def handle_signals(self) -> None:
|
||||
for sig in tuple(self.signal_queue):
|
||||
self.signal_queue.remove(sig)
|
||||
sig_name = SIGNALS[sig]
|
||||
sig_handler = getattr(self, f"handle_{sig_name.lower()}", None)
|
||||
if sig_handler is not None:
|
||||
sig_handler()
|
||||
else: # pragma: no cover
|
||||
logger.debug(f"Received signal {sig_name}, but no handler is defined for it.")
|
||||
|
||||
def handle_int(self) -> None:
|
||||
logger.info("Received SIGINT, exiting.")
|
||||
self.should_exit.set()
|
||||
|
||||
def handle_term(self) -> None:
|
||||
logger.info("Received SIGTERM, exiting.")
|
||||
self.should_exit.set()
|
||||
|
||||
def handle_break(self) -> None: # pragma: py-not-win32
|
||||
logger.info("Received SIGBREAK, exiting.")
|
||||
self.should_exit.set()
|
||||
|
||||
def handle_hup(self) -> None: # pragma: py-win32
|
||||
logger.info("Received SIGHUP, restarting processes.")
|
||||
self.restart_all()
|
||||
|
||||
def handle_ttin(self) -> None: # pragma: py-win32
|
||||
logger.info("Received SIGTTIN, increasing the number of processes.")
|
||||
self.processes_num += 1
|
||||
process = Process(self.config, self.target, self.sockets)
|
||||
process.start()
|
||||
self.processes.append(process)
|
||||
|
||||
def handle_ttou(self) -> None: # pragma: py-win32
|
||||
logger.info("Received SIGTTOU, decreasing number of processes.")
|
||||
if self.processes_num <= 1:
|
||||
logger.info("Already reached one process, cannot decrease the number of processes anymore.")
|
||||
return
|
||||
self.processes_num -= 1
|
||||
process = self.processes.pop()
|
||||
process.terminate()
|
||||
process.join()
|
||||
|
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
from typing import Callable, Iterator
|
||||
from typing import Callable
|
||||
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.supervisors.basereload import BaseReload
|
||||
@@ -23,10 +24,7 @@ class StatReload(BaseReload):
|
||||
self.mtimes: dict[Path, float] = {}
|
||||
|
||||
if config.reload_excludes or config.reload_includes:
|
||||
logger.warning(
|
||||
"--reload-include and --reload-exclude have no effect unless "
|
||||
"watchfiles is installed."
|
||||
)
|
||||
logger.warning("--reload-include and --reload-exclude have no effect unless " "watchfiles is installed.")
|
||||
|
||||
def should_restart(self) -> list[Path] | None:
|
||||
self.pause()
|
||||
|
@@ -13,20 +13,12 @@ from uvicorn.supervisors.basereload import BaseReload
|
||||
class FileFilter:
|
||||
def __init__(self, config: Config):
|
||||
default_includes = ["*.py"]
|
||||
self.includes = [
|
||||
default
|
||||
for default in default_includes
|
||||
if default not in config.reload_excludes
|
||||
]
|
||||
self.includes = [default for default in default_includes if default not in config.reload_excludes]
|
||||
self.includes.extend(config.reload_includes)
|
||||
self.includes = list(set(self.includes))
|
||||
|
||||
default_excludes = [".*", ".py[cod]", ".sw.*", "~*"]
|
||||
self.excludes = [
|
||||
default
|
||||
for default in default_excludes
|
||||
if default not in config.reload_includes
|
||||
]
|
||||
self.excludes = [default for default in default_excludes if default not in config.reload_includes]
|
||||
self.exclude_dirs = []
|
||||
for e in config.reload_excludes:
|
||||
p = Path(e)
|
||||
@@ -39,14 +31,14 @@ class FileFilter:
|
||||
if is_dir:
|
||||
self.exclude_dirs.append(p)
|
||||
else:
|
||||
self.excludes.append(e)
|
||||
self.excludes.append(e) # pragma: full coverage
|
||||
self.excludes = list(set(self.excludes))
|
||||
|
||||
def __call__(self, path: Path) -> bool:
|
||||
for include_pattern in self.includes:
|
||||
if path.match(include_pattern):
|
||||
if str(path).endswith(include_pattern):
|
||||
return True
|
||||
return True # pragma: full coverage
|
||||
|
||||
for exclude_dir in self.exclude_dirs:
|
||||
if exclude_dir in path.parents:
|
||||
@@ -54,7 +46,7 @@ class FileFilter:
|
||||
|
||||
for exclude_pattern in self.excludes:
|
||||
if path.match(exclude_pattern):
|
||||
return False
|
||||
return False # pragma: full coverage
|
||||
|
||||
return True
|
||||
return False
|
||||
|
@@ -1,163 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from watchgod import DefaultWatcher
|
||||
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.supervisors.basereload import BaseReload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import os
|
||||
|
||||
DirEntry = os.DirEntry[str]
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
|
||||
|
||||
class CustomWatcher(DefaultWatcher):
|
||||
def __init__(self, root_path: Path, config: Config):
|
||||
default_includes = ["*.py"]
|
||||
self.includes = [
|
||||
default
|
||||
for default in default_includes
|
||||
if default not in config.reload_excludes
|
||||
]
|
||||
self.includes.extend(config.reload_includes)
|
||||
self.includes = list(set(self.includes))
|
||||
|
||||
default_excludes = [".*", ".py[cod]", ".sw.*", "~*"]
|
||||
self.excludes = [
|
||||
default
|
||||
for default in default_excludes
|
||||
if default not in config.reload_includes
|
||||
]
|
||||
self.excludes.extend(config.reload_excludes)
|
||||
self.excludes = list(set(self.excludes))
|
||||
|
||||
self.watched_dirs: dict[str, bool] = {}
|
||||
self.watched_files: dict[str, bool] = {}
|
||||
self.dirs_includes = set(config.reload_dirs)
|
||||
self.dirs_excludes = set(config.reload_dirs_excludes)
|
||||
self.resolved_root = root_path
|
||||
super().__init__(str(root_path))
|
||||
|
||||
def should_watch_file(self, entry: "DirEntry") -> bool:
|
||||
cached_result = self.watched_files.get(entry.path)
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
|
||||
entry_path = Path(entry)
|
||||
|
||||
# cwd is not verified through should_watch_dir, so we need to verify here
|
||||
if entry_path.parent == Path.cwd() and Path.cwd() not in self.dirs_includes:
|
||||
self.watched_files[entry.path] = False
|
||||
return False
|
||||
for include_pattern in self.includes:
|
||||
if str(entry_path).endswith(include_pattern):
|
||||
self.watched_files[entry.path] = True
|
||||
return True
|
||||
if entry_path.match(include_pattern):
|
||||
for exclude_pattern in self.excludes:
|
||||
if entry_path.match(exclude_pattern):
|
||||
self.watched_files[entry.path] = False
|
||||
return False
|
||||
self.watched_files[entry.path] = True
|
||||
return True
|
||||
self.watched_files[entry.path] = False
|
||||
return False
|
||||
|
||||
def should_watch_dir(self, entry: "DirEntry") -> bool:
|
||||
cached_result = self.watched_dirs.get(entry.path)
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
|
||||
entry_path = Path(entry)
|
||||
|
||||
if entry_path in self.dirs_excludes:
|
||||
self.watched_dirs[entry.path] = False
|
||||
return False
|
||||
|
||||
for exclude_pattern in self.excludes:
|
||||
if entry_path.match(exclude_pattern):
|
||||
is_watched = False
|
||||
if entry_path in self.dirs_includes:
|
||||
is_watched = True
|
||||
|
||||
for directory in self.dirs_includes:
|
||||
if directory in entry_path.parents:
|
||||
is_watched = True
|
||||
|
||||
if is_watched:
|
||||
logger.debug(
|
||||
"WatchGodReload detected a new excluded dir '%s' in '%s'; "
|
||||
"Adding to exclude list.",
|
||||
entry_path.relative_to(self.resolved_root),
|
||||
str(self.resolved_root),
|
||||
)
|
||||
self.watched_dirs[entry.path] = False
|
||||
self.dirs_excludes.add(entry_path)
|
||||
return False
|
||||
|
||||
if entry_path in self.dirs_includes:
|
||||
self.watched_dirs[entry.path] = True
|
||||
return True
|
||||
|
||||
for directory in self.dirs_includes:
|
||||
if directory in entry_path.parents:
|
||||
self.watched_dirs[entry.path] = True
|
||||
return True
|
||||
|
||||
for include_pattern in self.includes:
|
||||
if entry_path.match(include_pattern):
|
||||
logger.info(
|
||||
"WatchGodReload detected a new reload dir '%s' in '%s'; "
|
||||
"Adding to watch list.",
|
||||
str(entry_path.relative_to(self.resolved_root)),
|
||||
str(self.resolved_root),
|
||||
)
|
||||
self.dirs_includes.add(entry_path)
|
||||
self.watched_dirs[entry.path] = True
|
||||
return True
|
||||
|
||||
self.watched_dirs[entry.path] = False
|
||||
return False
|
||||
|
||||
|
||||
class WatchGodReload(BaseReload):
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
target: Callable[[list[socket] | None], None],
|
||||
sockets: list[socket],
|
||||
) -> None:
|
||||
warnings.warn(
|
||||
'"watchgod" is deprecated, you should switch '
|
||||
"to watchfiles (`pip install watchfiles`).",
|
||||
DeprecationWarning,
|
||||
)
|
||||
super().__init__(config, target, sockets)
|
||||
self.reloader_name = "WatchGod"
|
||||
self.watchers = []
|
||||
reload_dirs = []
|
||||
for directory in config.reload_dirs:
|
||||
if Path.cwd() not in directory.parents:
|
||||
reload_dirs.append(directory)
|
||||
if Path.cwd() not in reload_dirs:
|
||||
reload_dirs.append(Path.cwd())
|
||||
for w in reload_dirs:
|
||||
self.watchers.append(CustomWatcher(w.resolve(), self.config))
|
||||
|
||||
def should_restart(self) -> list[Path] | None:
|
||||
self.pause()
|
||||
|
||||
for watcher in self.watchers:
|
||||
change = watcher.check()
|
||||
if change != set():
|
||||
return list({Path(c[1]) for c in change})
|
||||
|
||||
return None
|
Reference in New Issue
Block a user