#!/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()