core/main.py

275 lines
12 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-06-02 22:24:49 +00:00
from pathlib import Path
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-06-30 18:46:53 +00:00
from starlette.responses import FileResponse, 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"
2025-06-02 22:24:49 +00:00
DIST_DIR = Path(__file__).parent / "dist" # Директория для собранных файлов
INDEX_HTML = Path(__file__).parent / "index.html"
2024-01-25 19:41:27 +00:00
# Импортируем резолверы ПЕРЕД созданием схемы
2024-04-17 15:32:23 +00:00
import_module("resolvers")
2025-05-16 06:23:48 +00:00
# Создаем схему GraphQL
schema = make_executable_schema(load_schema_from_path("schema/"), list(resolvers))
2024-02-19 08:58:02 +00:00
2025-05-19 08:25:41 +00:00
# Создаем middleware с правильным порядком
middleware = [
2025-06-28 10:56:05 +00:00
# Начинаем с обработки ошибок
Middleware(ExceptionHandlerMiddleware),
2025-05-19 08:25:41 +00:00
# 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",
2025-06-28 10:56:05 +00:00
"http://localhost:3000",
2025-05-29 09:37:39 +00:00
"https://testing.discours.io",
2025-05-29 15:26:10 +00:00
"https://testing3.discours.io",
2025-06-28 10:56:05 +00:00
"https://v3.dscrs.site",
"https://session-daily.vercel.app",
2025-06-02 22:48:23 +00:00
"https://coretest.discours.io",
2025-05-20 22:34:02 +00:00
"https://new.discours.io",
2025-05-29 09:37:39 +00:00
],
allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS
2025-05-19 08:25:41 +00:00
allow_headers=["*"],
allow_credentials=True,
),
2025-06-28 10:56:05 +00:00
# Аутентификация должна быть после CORS
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-обработчик для лучшей обработки ошибок
2025-06-02 22:24:49 +00:00
async def graphql_handler(request: Request) -> Response:
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
return await auth_middleware.process_result(request, result)
2025-05-19 08:25:41 +00:00
except asyncio.CancelledError:
return JSONResponse({"error": "Request cancelled"}, status_code=499)
except Exception as e:
logger.error(f"GraphQL error: {e!s}")
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
2025-06-30 18:46:53 +00:00
async def spa_handler(request: Request) -> Response:
"""
Обработчик для SPA (Single Page Application) fallback.
Возвращает index.html для всех маршрутов, которые не найдены,
чтобы клиентский роутер (SolidJS) мог обработать маршрутинг.
Args:
request: Starlette Request объект
Returns:
FileResponse: ответ с содержимым index.html
"""
index_path = DIST_DIR / "index.html"
if index_path.exists():
return FileResponse(index_path, media_type="text/html")
return JSONResponse({"error": "Admin panel not built"}, status_code=404)
async def shutdown() -> None:
2025-05-22 01:34:30 +00:00
"""Остановка сервера и освобождение ресурсов"""
logger.info("Остановка сервера")
# Закрываем соединение с Redis
await redis.disconnect()
# Останавливаем поисковый сервис
2025-06-16 17:20:23 +00:00
await search_service.close()
2025-05-22 01:34:30 +00:00
# Удаляем PID-файл, если он существует
from settings import DEV_SERVER_PID_FILE_NAME
2025-05-29 09:37:39 +00:00
2025-06-02 22:24:49 +00:00
pid_file = Path(DEV_SERVER_PID_FILE_NAME)
if pid_file.exists():
pid_file.unlink()
2025-05-22 01:34:30 +00:00
async def dev_start() -> None:
2025-05-22 01:34:30 +00:00
"""
Инициализация сервера в 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:
2025-06-02 22:24:49 +00:00
pid_path = Path(DEV_SERVER_PID_FILE_NAME)
2025-05-22 01:34:30 +00:00
# Если PID-файл уже существует, проверяем, не запущен ли уже сервер с этим PID
2025-06-02 22:24:49 +00:00
if pid_path.exists():
2025-05-22 01:34:30 +00:00
try:
2025-06-02 22:24:49 +00:00
with pid_path.open(encoding="utf-8") as f:
2025-05-22 01:34:30 +00:00
old_pid = int(f.read().strip())
# Проверяем, существует ли процесс с таким PID
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("[warning] Invalid PID file found, recreating")
2025-05-29 09:37:39 +00:00
2025-05-22 01:34:30 +00:00
# Создаем или перезаписываем PID-файл
2025-06-02 22:24:49 +00:00
with pid_path.open("w", encoding="utf-8") as f:
2025-05-22 01:34:30 +00:00
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: {e!s}")
2025-05-22 01:34:30 +00:00
# Не прерываем запуск сервера из-за ошибки в этой функции
print(f"[warning] Error during DEV mode initialization: {e!s}")
2025-05-22 01:34:30 +00:00
2025-05-16 06:23:48 +00:00
2025-06-02 22:24:49 +00:00
# Глобальная переменная для background tasks
background_tasks = []
2022-09-03 10:50:14 +00:00
2025-06-02 22:24:49 +00:00
2025-06-16 17:20:23 +00:00
async def lifespan(app: Starlette):
2025-05-22 01:34:30 +00:00
"""
Функция жизненного цикла приложения.
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:
2025-06-16 17:20:23 +00:00
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
2025-06-02 22:24:49 +00:00
search_task = asyncio.create_task(initialize_search_index_background())
background_tasks.append(search_task)
# Не ждем завершения задачи, позволяем ей выполняться в фоне
2025-05-22 01:34:30 +00:00
yield
finally:
print("[lifespan] Shutting down application services")
2025-06-02 22:24:49 +00:00
# Отменяем все background tasks
for task in background_tasks:
if not task.done():
task.cancel()
2025-06-02 22:24:49 +00:00
# Ждем завершения отмены tasks
if background_tasks:
await asyncio.gather(*background_tasks, return_exceptions=True)
2024-10-14 09:13:18 +00:00
2025-05-22 01:34:30 +00:00
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-06-30 18:46:53 +00:00
# Статические файлы (CSS, JS, изображения)
Mount("/assets", app=StaticFiles(directory=str(DIST_DIR / "assets"))),
# Корневой маршрут для админ-панели
Route("/", spa_handler, methods=["GET"]),
# SPA fallback для всех остальных маршрутов
Route("/{path:path}", spa_handler, methods=["GET"]),
2025-05-22 01:34:30 +00:00
],
2025-06-28 10:56:05 +00:00
middleware=middleware, # Используем единый список middleware
2025-05-22 01:34:30 +00:00
lifespan=lifespan,
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,
2025-06-27 12:42:16 +00:00
allow_origins=["*"],
2025-05-22 01:34:30 +00:00
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)