diff --git a/server.py b/server.py index 173f2ec..2d10092 100644 --- a/server.py +++ b/server.py @@ -6,24 +6,34 @@ Поддерживает: - GET /data.json - получение данных - POST /save - сохранение данных в файл -- POST /reset - сброс к дефолтным данным (загружает из data.default.json) +- 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 shutil +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' -# Дефолтные данные вынесены в отдельный файл data.default.json -# Если нужно создать файл с дефолтом - раскомментировать функцию ниже - class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): def end_headers(self): @@ -39,7 +49,6 @@ class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): parsed_path = urlparse(self.path) - # Отдаём data.json if parsed_path.path == '/data.json': self.send_response(200) self.send_header('Content-Type', 'application/json; charset=utf-8') @@ -51,11 +60,9 @@ class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): self.send_error(404, 'data.json not found') return - # Отдаём index.html 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') @@ -72,14 +79,12 @@ class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): 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) @@ -103,10 +108,10 @@ class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): self.wfile.write(response.encode('utf-8')) return - # Сброс к дефолту (если есть файл data.default.json) 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') @@ -131,17 +136,166 @@ class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): self.end_headers() def log_message(self, format, *args): - print(f"[{self.address_string()}] {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": [], @@ -150,11 +304,17 @@ def ensure_data_file(): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(empty_data, f, ensure_ascii=False, indent=2) print(f"📄 Создан пустой файл данных: {DATA_FILE}") - print(f"⚠️ Для работы с данными создайте файл {DEFAULT_DATA_FILE} с дефолтными данными") -def run_server(): - os.chdir(os.path.dirname(os.path.abspath(__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: @@ -163,7 +323,6 @@ def run_server(): print(f"📁 Директория: {os.getcwd()}") print(f"🌐 Локальный доступ: http://localhost:{PORT}") print(f"📄 Файл данных: {DATA_FILE}") - print(f"📄 Дефолтный файл (если есть): {DEFAULT_DATA_FILE}") print(f"⏹️ Для остановки сервера нажмите Ctrl+C") print("=" * 60) @@ -171,8 +330,52 @@ def run_server(): httpd.serve_forever() except KeyboardInterrupt: print("\n🛑 Сервер остановлен.") - httpd.shutdown() + 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__": - run_server() \ No newline at end of file + main() \ No newline at end of file