381 lines
14 KiB
Python
381 lines
14 KiB
Python
#!/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() |