2024-10-14 08:11:13 +00:00
|
|
|
|
import asyncio
|
2023-01-17 21:07:44 +00:00
|
|
|
|
import os
|
2022-09-03 10:50:14 +00:00
|
|
|
|
from importlib import import_module
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from os.path import exists, join
|
2023-12-17 20:30:20 +00:00
|
|
|
|
|
2024-02-19 08:58:02 +00:00
|
|
|
|
from ariadne import load_schema_from_path, make_executable_schema
|
2022-09-03 10:50:14 +00:00
|
|
|
|
from ariadne.asgi import GraphQL
|
2025-05-22 01:34:30 +00:00
|
|
|
|
|
|
|
|
|
from auth.handler import EnhancedGraphQLHTTPHandler
|
|
|
|
|
from auth.internal import InternalAuthentication
|
|
|
|
|
from auth.middleware import auth_middleware, AuthMiddleware
|
2022-09-03 10:50:14 +00:00
|
|
|
|
from starlette.applications import Starlette
|
2024-12-11 22:04:11 +00:00
|
|
|
|
from starlette.middleware.cors import CORSMiddleware
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
|
|
|
from starlette.middleware import Middleware
|
2024-10-14 09:31:55 +00:00
|
|
|
|
from starlette.requests import Request
|
2025-05-22 01:34:30 +00:00
|
|
|
|
from starlette.responses import JSONResponse, Response
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from starlette.routing import Route, Mount
|
|
|
|
|
from starlette.staticfiles import StaticFiles
|
2023-11-28 19:07:53 +00:00
|
|
|
|
|
2024-08-09 06:37:06 +00:00
|
|
|
|
from cache.precache import precache_data
|
|
|
|
|
from cache.revalidator import revalidation_manager
|
2024-06-04 06:07:46 +00:00
|
|
|
|
from services.exception import ExceptionHandlerMiddleware
|
2024-08-09 06:37:06 +00:00
|
|
|
|
from services.redis import redis
|
2025-02-10 15:04:08 +00:00
|
|
|
|
from services.schema import create_all_tables, resolvers
|
2025-05-22 01:34:30 +00:00
|
|
|
|
from services.search import check_search_service, initialize_search_index_background, search_service
|
|
|
|
|
from services.viewed import ViewedStorage
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from utils.logger import root_logger as logger
|
2025-05-22 01:34:30 +00:00
|
|
|
|
from settings import DEV_SERVER_PID_FILE_NAME
|
|
|
|
|
|
|
|
|
|
DEVMODE = os.getenv("DOKKU_APP_TYPE", "false").lower() == "false"
|
|
|
|
|
DIST_DIR = join(os.path.dirname(__file__), "dist") # Директория для собранных файлов
|
|
|
|
|
INDEX_HTML = join(os.path.dirname(__file__), "index.html")
|
2024-01-25 19:41:27 +00:00
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
# Импортируем резолверы
|
2024-04-17 15:32:23 +00:00
|
|
|
|
import_module("resolvers")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
# Создаем схему GraphQL
|
2024-04-17 15:32:23 +00:00
|
|
|
|
schema = make_executable_schema(load_schema_from_path("schema/"), resolvers)
|
2024-02-19 08:58:02 +00:00
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
# Создаем middleware с правильным порядком
|
|
|
|
|
middleware = [
|
|
|
|
|
# Начинаем с обработки ошибок
|
|
|
|
|
Middleware(ExceptionHandlerMiddleware),
|
|
|
|
|
# CORS должен быть перед другими middleware для корректной обработки preflight-запросов
|
|
|
|
|
Middleware(
|
|
|
|
|
CORSMiddleware,
|
2025-05-20 22:34:02 +00:00
|
|
|
|
allow_origins=[
|
|
|
|
|
"https://localhost:3000",
|
|
|
|
|
"https://testing.discours.io",
|
|
|
|
|
"https://discours.io",
|
|
|
|
|
"https://new.discours.io",
|
|
|
|
|
"https://discours.ru",
|
|
|
|
|
"https://new.discours.ru"
|
|
|
|
|
],
|
2025-05-19 08:25:41 +00:00
|
|
|
|
allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
),
|
2025-05-22 01:34:30 +00:00
|
|
|
|
# Сначала AuthMiddleware (для обработки токенов)
|
2025-05-19 08:25:41 +00:00
|
|
|
|
Middleware(AuthMiddleware),
|
2025-05-22 01:34:30 +00:00
|
|
|
|
# Затем AuthenticationMiddleware (для создания request.user на основе токена)
|
2025-05-19 08:25:41 +00:00
|
|
|
|
Middleware(AuthenticationMiddleware, backend=InternalAuthentication()),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2025-05-19 21:00:24 +00:00
|
|
|
|
# Создаем экземпляр GraphQL с улучшенным обработчиком
|
|
|
|
|
graphql_app = GraphQL(
|
|
|
|
|
schema,
|
2025-05-22 01:34:30 +00:00
|
|
|
|
debug=DEVMODE,
|
2025-05-19 21:00:24 +00:00
|
|
|
|
http_handler=EnhancedGraphQLHTTPHandler()
|
|
|
|
|
)
|
2025-05-19 08:25:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
|
|
|
|
|
async def graphql_handler(request: Request):
|
2025-05-22 01:34:30 +00:00
|
|
|
|
"""
|
|
|
|
|
Обработчик GraphQL запросов с поддержкой middleware и обработкой ошибок.
|
|
|
|
|
|
|
|
|
|
Выполняет:
|
|
|
|
|
1. Проверку метода запроса (GET, POST, OPTIONS)
|
|
|
|
|
2. Обработку GraphQL запроса через ariadne
|
|
|
|
|
3. Применение middleware для корректной обработки cookie и авторизации
|
|
|
|
|
4. Обработку исключений и формирование ответа
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request: Starlette Request объект
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Response: объект ответа (обычно JSONResponse)
|
|
|
|
|
"""
|
2025-05-19 08:25:41 +00:00
|
|
|
|
if request.method not in ["GET", "POST", "OPTIONS"]:
|
|
|
|
|
return JSONResponse({"error": "Method Not Allowed by main.py"}, status_code=405)
|
2025-05-22 01:34:30 +00:00
|
|
|
|
|
|
|
|
|
# Проверяем, что все необходимые middleware корректно отработали
|
|
|
|
|
if not hasattr(request, "scope") or "auth" not in request.scope:
|
|
|
|
|
logger.warning("[graphql] AuthMiddleware не обработал запрос перед GraphQL обработчиком")
|
2025-05-19 08:25:41 +00:00
|
|
|
|
|
|
|
|
|
try:
|
2025-05-22 01:34:30 +00:00
|
|
|
|
# Обрабатываем запрос через GraphQL приложение
|
2025-05-19 08:25:41 +00:00
|
|
|
|
result = await graphql_app.handle_request(request)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
2025-05-22 01:34:30 +00:00
|
|
|
|
# Применяем middleware для установки cookie
|
|
|
|
|
# Используем метод process_result из auth_middleware для корректной обработки
|
|
|
|
|
# cookie на основе результатов операций login/logout
|
|
|
|
|
response = await auth_middleware.process_result(request, result)
|
|
|
|
|
return response
|
2025-05-19 08:25:41 +00:00
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
return JSONResponse({"error": "Request cancelled"}, status_code=499)
|
|
|
|
|
except Exception as e:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
logger.error(f"GraphQL error: {str(e)}")
|
2025-05-22 01:34:30 +00:00
|
|
|
|
# Логируем более подробную информацию для отладки
|
|
|
|
|
import traceback
|
|
|
|
|
logger.debug(f"GraphQL error traceback: {traceback.format_exc()}")
|
2025-05-19 08:25:41 +00:00
|
|
|
|
return JSONResponse({"error": str(e)}, status_code=500)
|
2025-05-22 01:34:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def shutdown():
|
|
|
|
|
"""Остановка сервера и освобождение ресурсов"""
|
|
|
|
|
logger.info("Остановка сервера")
|
|
|
|
|
|
|
|
|
|
# Закрываем соединение с Redis
|
|
|
|
|
await redis.disconnect()
|
|
|
|
|
|
|
|
|
|
# Останавливаем поисковый сервис
|
|
|
|
|
search_service.close()
|
|
|
|
|
|
|
|
|
|
# Удаляем PID-файл, если он существует
|
|
|
|
|
from settings import DEV_SERVER_PID_FILE_NAME
|
|
|
|
|
if exists(DEV_SERVER_PID_FILE_NAME):
|
|
|
|
|
os.unlink(DEV_SERVER_PID_FILE_NAME)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def dev_start():
|
|
|
|
|
"""
|
|
|
|
|
Инициализация сервера в DEV режиме.
|
2025-05-16 07:30:02 +00:00
|
|
|
|
|
2025-05-22 01:34:30 +00:00
|
|
|
|
Функция:
|
|
|
|
|
1. Проверяет наличие DEV режима
|
|
|
|
|
2. Создает PID-файл для отслеживания процесса
|
|
|
|
|
3. Логирует информацию о старте сервера
|
|
|
|
|
|
|
|
|
|
Используется только при запуске сервера с флагом "dev".
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
pid_path = DEV_SERVER_PID_FILE_NAME
|
|
|
|
|
# Если PID-файл уже существует, проверяем, не запущен ли уже сервер с этим PID
|
|
|
|
|
if exists(pid_path):
|
|
|
|
|
try:
|
|
|
|
|
with open(pid_path, "r", encoding="utf-8") as f:
|
|
|
|
|
old_pid = int(f.read().strip())
|
|
|
|
|
# Проверяем, существует ли процесс с таким PID
|
|
|
|
|
import signal
|
|
|
|
|
try:
|
|
|
|
|
os.kill(old_pid, 0) # Сигнал 0 только проверяет существование процесса
|
|
|
|
|
print(f"[warning] DEV server already running with PID {old_pid}")
|
|
|
|
|
except OSError:
|
|
|
|
|
print(f"[info] Stale PID file found, previous process {old_pid} not running")
|
|
|
|
|
except (ValueError, FileNotFoundError):
|
|
|
|
|
print(f"[warning] Invalid PID file found, recreating")
|
|
|
|
|
|
|
|
|
|
# Создаем или перезаписываем PID-файл
|
|
|
|
|
with open(pid_path, "w", encoding="utf-8") as f:
|
|
|
|
|
f.write(str(os.getpid()))
|
|
|
|
|
print(f"[main] process started in DEV mode with PID {os.getpid()}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"[main] Error during server startup: {str(e)}")
|
|
|
|
|
# Не прерываем запуск сервера из-за ошибки в этой функции
|
|
|
|
|
print(f"[warning] Error during DEV mode initialization: {str(e)}")
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-05-22 01:34:30 +00:00
|
|
|
|
async def lifespan(_app):
|
|
|
|
|
"""
|
|
|
|
|
Функция жизненного цикла приложения.
|
|
|
|
|
|
|
|
|
|
Обеспечивает:
|
|
|
|
|
1. Инициализацию всех необходимых сервисов и компонентов
|
|
|
|
|
2. Предзагрузку кеша данных
|
|
|
|
|
3. Подключение к Redis и поисковому сервису
|
|
|
|
|
4. Корректное завершение работы при остановке сервера
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
_app: экземпляр Starlette приложения
|
|
|
|
|
|
|
|
|
|
Yields:
|
|
|
|
|
None: генератор для управления жизненным циклом
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
print("[lifespan] Starting application initialization")
|
|
|
|
|
create_all_tables()
|
|
|
|
|
await asyncio.gather(
|
|
|
|
|
redis.connect(),
|
|
|
|
|
precache_data(),
|
|
|
|
|
ViewedStorage.init(),
|
|
|
|
|
check_search_service(),
|
|
|
|
|
revalidation_manager.start(),
|
|
|
|
|
)
|
|
|
|
|
if DEVMODE:
|
|
|
|
|
await dev_start()
|
|
|
|
|
print("[lifespan] Basic initialization complete")
|
|
|
|
|
|
|
|
|
|
# Add a delay before starting the intensive search indexing
|
|
|
|
|
print("[lifespan] Waiting for system stabilization before search indexing...")
|
|
|
|
|
await asyncio.sleep(10) # 10-second delay to let the system stabilize
|
|
|
|
|
|
|
|
|
|
# Start search indexing as a background task with lower priority
|
|
|
|
|
asyncio.create_task(initialize_search_index_background())
|
|
|
|
|
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
print("[lifespan] Shutting down application services")
|
|
|
|
|
tasks = [redis.disconnect(), ViewedStorage.stop(), revalidation_manager.stop()]
|
|
|
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
|
print("[lifespan] Shutdown complete")
|
|
|
|
|
|
|
|
|
|
# Обновляем маршрут в Starlette
|
2024-02-16 09:40:41 +00:00
|
|
|
|
app = Starlette(
|
2025-05-22 01:34:30 +00:00
|
|
|
|
routes=[
|
|
|
|
|
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
|
|
|
|
|
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True))
|
|
|
|
|
],
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
middleware=middleware, # Явно указываем список middleware
|
|
|
|
|
debug=True,
|
2024-04-08 06:17:05 +00:00
|
|
|
|
)
|
2025-05-22 01:34:30 +00:00
|
|
|
|
|
|
|
|
|
if DEVMODE:
|
|
|
|
|
# Для DEV режима регистрируем дополнительный CORS middleware только для localhost
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=["https://localhost:3000"],
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|