Files
b_analysis/server.py
T
2026-06-09 13:31:20 +03:00

381 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Сервер для управления данными бизнес-объединений ДНР.
Поддерживает:
- GET /data.json - получение данных
- POST /save - сохранение данных в файл
- POST /reset - сброс к дефолтным данным
Запуск:
python server.py # обычный запуск
python server.py --daemon # фоновый режим (демон)
python server.py --stop # остановить фоновый процесс
python server.py --status # проверить статус фонового процесса
"""
import http.server
import socketserver
import json
import os
import sys
import signal
import atexit
import time
import socket
from urllib.parse import urlparse
# Конфигурация
PORT = 8092
DATA_FILE = 'data.json'
DEFAULT_DATA_FILE = 'data.default.json'
PID_FILE = 'server.pid'
LOG_FILE = 'server.log'
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
super().end_headers()
def do_OPTIONS(self):
self.send_response(200)
self.end_headers()
def do_GET(self):
parsed_path = urlparse(self.path)
if parsed_path.path == '/data.json':
self.send_response(200)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.end_headers()
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
self.wfile.write(f.read().encode('utf-8'))
except FileNotFoundError:
self.send_error(404, 'data.json not found')
return
if parsed_path.path == '/' or parsed_path.path == '/index.html':
self.path = '/index.html'
if self.path.endswith('.html'):
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.end_headers()
try:
with open(self.path[1:], 'rb') as f:
self.wfile.write(f.read())
except FileNotFoundError:
self.send_error(404, 'File not found')
return
super().do_GET()
def do_POST(self):
parsed_path = urlparse(self.path)
if parsed_path.path == '/save':
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)
try:
data = json.loads(post_data.decode('utf-8'))
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
response = json.dumps({'success': True, 'message': 'Данные сохранены'})
self.wfile.write(response.encode('utf-8'))
except json.JSONDecodeError as e:
self.send_response(400)
self.send_header('Content-Type', 'application/json')
self.end_headers()
response = json.dumps({'success': False, 'message': f'Ошибка JSON: {str(e)}'})
self.wfile.write(response.encode('utf-8'))
except Exception as e:
self.send_response(500)
self.send_header('Content-Type', 'application/json')
self.end_headers()
response = json.dumps({'success': False, 'message': f'Ошибка сервера: {str(e)}'})
self.wfile.write(response.encode('utf-8'))
return
if parsed_path.path == '/reset':
try:
if os.path.exists(DEFAULT_DATA_FILE):
import shutil
shutil.copy(DEFAULT_DATA_FILE, DATA_FILE)
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
response = json.dumps({'success': True, 'message': 'Данные сброшены к исходным'})
self.wfile.write(response.encode('utf-8'))
else:
self.send_response(404)
self.send_header('Content-Type', 'application/json')
self.end_headers()
response = json.dumps({'success': False, 'message': 'Файл с дефолтными данными не найден'})
self.wfile.write(response.encode('utf-8'))
except Exception as e:
self.send_response(500)
self.send_header('Content-Type', 'application/json')
self.end_headers()
response = json.dumps({'success': False, 'message': str(e)})
self.wfile.write(response.encode('utf-8'))
return
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
"""Логирование в файл или консоль в зависимости от режима"""
message = f"[{self.address_string()}] {format % args}"
if os.environ.get('DAEMON_MODE') == '1':
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {message}\n")
else:
print(message)
def is_port_in_use(port):
"""Проверка, занят ли порт"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(('', port))
return False
except OSError:
return True
def write_pid_file():
"""Запись PID в файл"""
with open(PID_FILE, 'w') as f:
f.write(str(os.getpid()))
def remove_pid_file():
"""Удаление PID файла"""
if os.path.exists(PID_FILE):
os.remove(PID_FILE)
def read_pid_file():
"""Чтение PID из файла"""
if os.path.exists(PID_FILE):
with open(PID_FILE, 'r') as f:
return int(f.read().strip())
return None
def is_process_running(pid):
"""Проверка, запущен ли процесс с указанным PID"""
try:
os.kill(pid, 0)
return True
except OSError:
return False
def stop_daemon():
"""Остановка фонового процесса"""
pid = read_pid_file()
if pid is None:
print("❌ PID файл не найден. Сервер не запущен в фоновом режиме.")
return False
if not is_process_running(pid):
print(f"⚠️ Процесс с PID {pid} не найден. Возможно, сервер уже остановлен.")
remove_pid_file()
return False
try:
os.kill(pid, signal.SIGTERM)
time.sleep(1)
if is_process_running(pid):
os.kill(pid, signal.SIGKILL)
remove_pid_file()
print(f"✅ Сервер (PID: {pid}) остановлен.")
return True
except OSError as e:
print(f"❌ Ошибка при остановке сервера: {e}")
return False
def check_status():
"""Проверка статуса фонового процесса"""
pid = read_pid_file()
if pid is None:
print("❌ Сервер не запущен в фоновом режиме.")
return
if is_process_running(pid):
print(f"✅ Сервер запущен (PID: {pid})")
print(f"🌐 http://localhost:{PORT}")
else:
print(f"⚠️ PID файл существует, но процесс {pid} не найден.")
remove_pid_file()
def run_daemon():
"""Запуск сервера в фоновом режиме"""
# Проверка, не запущен ли уже сервер
pid = read_pid_file()
if pid and is_process_running(pid):
print(f"❌ Сервер уже запущен (PID: {pid})")
print(f"🌐 http://localhost:{PORT}")
return
# Проверка порта
if is_port_in_use(PORT):
print(f"❌ Порт {PORT} уже занят. Сервер не может быть запущен.")
return
# Форк процесса для демонизации
try:
pid = os.fork()
if pid > 0:
# Родительский процесс завершается
print(f"🚀 Сервер запущен в фоновом режиме (PID: {pid})")
print(f"🌐 http://localhost:{PORT}")
print(f"📄 Лог-файл: {LOG_FILE}")
print(f"📄 PID-файл: {PID_FILE}")
print("\nУправление:")
print(f" python server.py --stop - остановить сервер")
print(f" python server.py --status - проверить статус")
sys.exit(0)
except OSError as e:
print(f"❌ Ошибка форка: {e}")
sys.exit(1)
# Демонизация: отсоединяемся от терминала
os.setsid()
os.umask(0)
# Перенаправление stdout/stderr в файл
sys.stdout.flush()
sys.stderr.flush()
with open(LOG_FILE, 'a') as log:
os.dup2(log.fileno(), sys.stdout.fileno())
os.dup2(log.fileno(), sys.stderr.fileno())
# Установка флага демона для логирования
os.environ['DAEMON_MODE'] = '1'
write_pid_file()
atexit.register(remove_pid_file)
# Обработка сигналов
def signal_handler(sig, frame):
sys.exit(0)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
ensure_data_file()
# Создание и запуск сервера
with socketserver.TCPServer(("", PORT), CustomHTTPRequestHandler) as httpd:
print(f"Сервер запущен на порту {PORT}")
httpd.serve_forever()
def ensure_data_file():
"""Проверяет наличие data.json, если нет - создаёт из дефолта или пустой"""
if not os.path.exists(DATA_FILE):
if os.path.exists(DEFAULT_DATA_FILE):
import shutil
shutil.copy(DEFAULT_DATA_FILE, DATA_FILE)
print(f"📄 Создан файл данных из {DEFAULT_DATA_FILE}")
else:
empty_data = {
"organizations": [],
"additionalOrganizations": [],
"metadata": {"lastUpdated": "2026-01-15", "totalOrganizations": 0}
}
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(empty_data, f, ensure_ascii=False, indent=2)
print(f"📄 Создан пустой файл данных: {DATA_FILE}")
def run_foreground():
"""Запуск сервера в обычном (не фоновом) режиме"""
if is_port_in_use(PORT):
print(f"❌ Порт {PORT} уже занят.")
print("Возможные решения:")
print(f" 1. Остановите другой сервер: python server.py --stop")
print(f" 2. Измените PORT в файле server.py")
return False
ensure_data_file()
with socketserver.TCPServer(("", PORT), CustomHTTPRequestHandler) as httpd:
print("=" * 60)
print(f"🚀 Сервер запущен!")
print(f"📁 Директория: {os.getcwd()}")
print(f"🌐 Локальный доступ: http://localhost:{PORT}")
print(f"📄 Файл данных: {DATA_FILE}")
print(f"⏹️ Для остановки сервера нажмите Ctrl+C")
print("=" * 60)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n🛑 Сервер остановлен.")
return True
def print_help():
"""Вывод справки"""
print("""
╔══════════════════════════════════════════════════════════════════╗
║ Сервер управления данными ДНР ║
╚══════════════════════════════════════════════════════════════════╝
Использование:
python server.py # Обычный запуск (в консоли)
python server.py --daemon # Фоновый запуск (демон)
python server.py --stop # Остановка фонового процесса
python server.py --status # Проверка статуса фонового процесса
python server.py --help # Показать эту справку
Настройки:
PORT = 8092 # Порт сервера
DATA_FILE = data.json # Файл с данными
LOG_FILE = server.log # Файл логов (для фонового режима)
PID_FILE = server.pid # Файл с PID процесса
После запуска:
Откройте в браузере: http://localhost:8000
""")
def main():
"""Главная функция"""
if len(sys.argv) > 1:
arg = sys.argv[1].lower()
if arg == '--daemon' or arg == '-d':
run_daemon()
elif arg == '--stop' or arg == '-s':
stop_daemon()
elif arg == '--status' or arg == '-st':
check_status()
elif arg == '--help' or arg == '-h':
print_help()
else:
print(f"❌ Неизвестный аргумент: {arg}")
print_help()
else:
run_foreground()
if __name__ == "__main__":
main()