2024-10-14 08:11:13 +00:00
|
|
|
|
import asyncio
|
2023-01-17 21:07:44 +00:00
|
|
|
|
import os
|
2025-06-02 22:25:24 +00:00
|
|
|
|
from collections.abc import AsyncGenerator
|
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
|
2025-06-02 22:25:24 +00:00
|
|
|
|
from typing import Any
|
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
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
2025-06-01 23:56:11 +00:00
|
|
|
|
# Импортируем резолверы ПЕРЕД созданием схемы
|
2024-04-17 15:32:23 +00:00
|
|
|
|
import_module("resolvers")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
# Создаем схему GraphQL
|
2025-06-01 23:56:11 +00:00
|
|
|
|
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 = [
|
|
|
|
|
# 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-06-02 22:53:19 +00:00
|
|
|
|
"https://testing.dscrs.site",
|
2025-05-29 15:26:10 +00:00
|
|
|
|
"https://testing3.discours.io",
|
2025-06-02 22:48:23 +00:00
|
|
|
|
"https://coretest.discours.io",
|
2025-06-02 22:53:19 +00:00
|
|
|
|
"https://core.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",
|
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-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-обработчик для лучшей обработки ошибок
|
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
|
2025-06-01 23:56:11 +00:00
|
|
|
|
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:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
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-01 23:56:11 +00:00
|
|
|
|
async def shutdown() -> None:
|
2025-05-22 01:34:30 +00:00
|
|
|
|
"""Остановка сервера и освобождение ресурсов"""
|
|
|
|
|
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-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
|
|
|
|
|
|
|
|
|
|
2025-06-01 23:56:11 +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):
|
2025-06-01 23:56:11 +00:00
|
|
|
|
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:
|
2025-06-01 23:56:11 +00:00
|
|
|
|
logger.error(f"[main] Error during server startup: {e!s}")
|
2025-05-22 01:34:30 +00:00
|
|
|
|
# Не прерываем запуск сервера из-за ошибки в этой функции
|
2025-06-01 23:56:11 +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
|
|
|
|
|
|
|
|
|
async def lifespan(_app: Any) -> AsyncGenerator[None, None]:
|
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:
|
|
|
|
|
_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-03-25 00:42:51 +00:00
|
|
|
|
|
2025-06-02 22:24:49 +00:00
|
|
|
|
# Отменяем все background tasks
|
|
|
|
|
for task in background_tasks:
|
|
|
|
|
if not task.done():
|
|
|
|
|
task.cancel()
|
2025-03-25 00:42:51 +00:00
|
|
|
|
|
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-02 22:24:49 +00:00
|
|
|
|
Mount("/", app=StaticFiles(directory=str(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=["*"],
|
|
|
|
|
)
|