core/main.py

236 lines
9.9 KiB
Python
Raw Normal View History

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
from starlette.applications import Starlette
2025-05-16 06:23:48 +00:00
from starlette.middleware import Middleware
2025-05-29 09:37:39 +00:00
from starlette.middleware.cors import CORSMiddleware
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-29 09:37:39 +00:00
from starlette.routing import Mount, Route
2025-05-16 06:23:48 +00:00
from starlette.staticfiles import StaticFiles
2023-11-28 19:07:53 +00:00
2025-05-29 09:37:39 +00:00
from auth.handler import EnhancedGraphQLHTTPHandler
from auth.middleware import AuthMiddleware, auth_middleware
2025-05-30 11:08:29 +00:00
from auth.oauth import oauth_callback, oauth_login
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
from settings import DEV_SERVER_PID_FILE_NAME
2025-05-29 09:37:39 +00:00
from utils.logger import root_logger as logger
2025-05-22 01:34:30 +00:00
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=[
2025-05-29 09:37:39 +00:00
"https://localhost:3000",
"https://testing.discours.io",
2025-05-29 15:26:10 +00:00
"https://testing3.discours.io",
2025-05-29 09:37:39 +00:00
"https://discours.io",
2025-05-20 22:34:02 +00:00
"https://new.discours.io",
"https://discours.ru",
2025-05-29 09:37:39 +00:00
"https://new.discours.ru",
],
allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS
2025-05-19 08:25:41 +00:00
allow_headers=["*"],
allow_credentials=True,
),
2025-05-30 11:05:50 +00:00
# извлечение токена + аутентификация + cookies
2025-05-19 08:25:41 +00:00
Middleware(AuthMiddleware),
]
2025-05-19 21:00:24 +00:00
# Создаем экземпляр GraphQL с улучшенным обработчиком
2025-05-29 09:37:39 +00:00
graphql_app = GraphQL(schema, debug=DEVMODE, 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 и обработкой ошибок.
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
Выполняет:
1. Проверку метода запроса (GET, POST, OPTIONS)
2. Обработку GraphQL запроса через ariadne
3. Применение middleware для корректной обработки cookie и авторизации
4. Обработку исключений и формирование ответа
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
Args:
request: Starlette Request объект
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
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-29 09:37:39 +00:00
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-29 09:37:39 +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
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
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
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
if exists(DEV_SERVER_PID_FILE_NAME):
os.unlink(DEV_SERVER_PID_FILE_NAME)
async def dev_start():
"""
Инициализация сервера в DEV режиме.
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
Функция:
1. Проверяет наличие DEV режима
2. Создает PID-файл для отслеживания процесса
3. Логирует информацию о старте сервера
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
Используется только при запуске сервера с флагом "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
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
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")
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
# Создаем или перезаписываем 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):
"""
Функция жизненного цикла приложения.
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
Обеспечивает:
1. Инициализацию всех необходимых сервисов и компонентов
2. Предзагрузку кеша данных
3. Подключение к Redis и поисковому сервису
4. Корректное завершение работы при остановке сервера
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
Args:
_app: экземпляр Starlette приложения
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
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")
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
# Обновляем маршрут в 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"]),
2025-05-30 11:05:50 +00:00
# OAuth маршруты
Route("/oauth/{provider}", oauth_login, methods=["GET"]),
Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]),
2025-05-29 09:37:39 +00:00
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True)),
2025-05-22 01:34:30 +00:00
],
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=["*"],
)