|
|
|
|
@ -1,61 +1,169 @@
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import socket
|
|
|
|
|
import time
|
|
|
|
|
import subprocess
|
|
|
|
|
import configparser
|
|
|
|
|
import multiprocessing
|
|
|
|
|
|
|
|
|
|
CONFIG_FILE = "/etc/butler.ini"
|
|
|
|
|
|
|
|
|
|
CHECK_USERS_CMD = ["/root/mcrcon/mcrcon", "-H", "192.168.0.21", "-p", "password", "list"]
|
|
|
|
|
CHECK_INTERVAL = 60
|
|
|
|
|
WASTED_LIMIT = 20 * 60
|
|
|
|
|
# --- Функции, выполняющие действия ---
|
|
|
|
|
|
|
|
|
|
def are_users_there():
|
|
|
|
|
def are_users_there(check_cmd, service_name):
|
|
|
|
|
"""
|
|
|
|
|
Выполняет команду проверки через shell.
|
|
|
|
|
Возвращает True, если код возврата 0 (успех).
|
|
|
|
|
Возвращает False, если код возврата не 0 или произошла ошибка.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
output = subprocess.check_output(CHECK_USERS_CMD)
|
|
|
|
|
# Maybe, server isn't running?
|
|
|
|
|
except subprocess.CalledProcessError as err:
|
|
|
|
|
print(f"Cannot access mcrcon: {err}", flush=True)
|
|
|
|
|
# shell=True позволяет использовать пайпы (|) и другие возможности shell
|
|
|
|
|
# check=True бросит исключение CalledProcessError при ненулевом коде возврата
|
|
|
|
|
print(f"[{service_name}] run {check_cmd}.", flush=True)
|
|
|
|
|
subprocess.run(
|
|
|
|
|
check_cmd,
|
|
|
|
|
shell=True,
|
|
|
|
|
check=True,
|
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
stderr=subprocess.DEVNULL,
|
|
|
|
|
executable='/bin/bash'
|
|
|
|
|
)
|
|
|
|
|
return True
|
|
|
|
|
header = output.decode().split("\n")[0]
|
|
|
|
|
if header.startswith("There are 0 of"):
|
|
|
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
|
|
|
# CalledProcessError: команда вернула ненулевой код (считаем, что активности нет)
|
|
|
|
|
# FileNotFoundError: команда не найдена (например, опечатка в пути)
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def server_shutdown():
|
|
|
|
|
subprocess.run("service minecraft stop".split(" "))
|
|
|
|
|
def server_shutdown(stop_cmd, service_name):
|
|
|
|
|
print(f"[{service_name}] Stopping server...", flush=True)
|
|
|
|
|
subprocess.run(stop_cmd, shell=True)
|
|
|
|
|
print(f"[{service_name}] Stop command sent.", flush=True)
|
|
|
|
|
|
|
|
|
|
def server_startup():
|
|
|
|
|
subprocess.run("service minecraft start".split(" "))
|
|
|
|
|
def server_startup(start_cmd, service_name):
|
|
|
|
|
print(f"[{service_name}] Starting server...", flush=True)
|
|
|
|
|
subprocess.run(start_cmd, shell=True)
|
|
|
|
|
print(f"[{service_name}] Start command sent.", flush=True)
|
|
|
|
|
|
|
|
|
|
def wait_client(host, port, service_name):
|
|
|
|
|
"""
|
|
|
|
|
Слушает порт и ждет одного подключения.
|
|
|
|
|
Использует SO_REUSEADDR для немедленного освобождения порта.
|
|
|
|
|
"""
|
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
|
|
|
# Эта опция решает проблему "Address already in use"
|
|
|
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
|
|
|
|
|
|
sock.bind((host, port))
|
|
|
|
|
sock.listen(0)
|
|
|
|
|
print(f"[{service_name}] Listening on {host}:{port}...", flush=True)
|
|
|
|
|
connection, address = sock.accept()
|
|
|
|
|
print(f"[{service_name}] Connection from: {address}", flush=True)
|
|
|
|
|
connection.close()
|
|
|
|
|
print(f"[{service_name}] Socket closed, proceeding to start.", flush=True)
|
|
|
|
|
|
|
|
|
|
# --- Функции состояний для конечного автомата (FSM) ---
|
|
|
|
|
|
|
|
|
|
def state_check_users(ctx, config):
|
|
|
|
|
service_name = config['service_name']
|
|
|
|
|
if are_users_there(config['check_command'], service_name):
|
|
|
|
|
ctx["wasted"] = 0
|
|
|
|
|
print(f"[{service_name}] Activity detected, check is OK.", flush=True)
|
|
|
|
|
return "check_users", config['check_interval_sec']
|
|
|
|
|
else:
|
|
|
|
|
ctx["wasted"] += config['check_interval_sec']
|
|
|
|
|
print(f"[{service_name}] No activity detected ({ctx['wasted']}/{config['idle_limit_sec']} sec).", flush=True)
|
|
|
|
|
if ctx["wasted"] >= config['idle_limit_sec']:
|
|
|
|
|
print(f"[{service_name}] Idle limit reached.", flush=True)
|
|
|
|
|
return "shutdown", 0
|
|
|
|
|
else:
|
|
|
|
|
return "check_users", config['check_interval_sec']
|
|
|
|
|
|
|
|
|
|
def state_shutdown(ctx, config):
|
|
|
|
|
server_shutdown(config['stop_command'], config['service_name'])
|
|
|
|
|
return "wait_client", 0
|
|
|
|
|
|
|
|
|
|
def state_wait_client(ctx, config):
|
|
|
|
|
wait_client(config['listen_host'], config['listen_port'], config['service_name'])
|
|
|
|
|
return "startup", 0
|
|
|
|
|
|
|
|
|
|
def state_startup(ctx, config):
|
|
|
|
|
server_startup(config['start_command'], config['service_name'])
|
|
|
|
|
ctx["wasted"] = 0
|
|
|
|
|
# Даем сервису время на инициализацию перед первой проверкой
|
|
|
|
|
return "check_users", config['startup_delay_sec']
|
|
|
|
|
|
|
|
|
|
# Карта переходов
|
|
|
|
|
state_map = {
|
|
|
|
|
"check_users": state_check_users,
|
|
|
|
|
"shutdown": state_shutdown,
|
|
|
|
|
"wait_client": state_wait_client,
|
|
|
|
|
"startup": state_startup,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def run_service_butler(config):
|
|
|
|
|
"""
|
|
|
|
|
Основной цикл конечного автомата для одного сервиса.
|
|
|
|
|
"""
|
|
|
|
|
ctx = { "wasted": 0 }
|
|
|
|
|
state = "wait_client" # Всегда начинаем с ожидания клиента
|
|
|
|
|
|
|
|
|
|
print(f"[{config['service_name']}] Butler process started.", flush=True)
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
wasted_cnt_seconds = 0
|
|
|
|
|
while True:
|
|
|
|
|
if are_users_there():
|
|
|
|
|
wasted_cnt_seconds = 0
|
|
|
|
|
print("Users ok", flush=True)
|
|
|
|
|
time.sleep(CHECK_INTERVAL)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
wasted_cnt_seconds += CHECK_INTERVAL
|
|
|
|
|
print(f"No users ({wasted_cnt_seconds})", flush=True)
|
|
|
|
|
if wasted_cnt_seconds > WASTED_LIMIT:
|
|
|
|
|
print("Wasted limit", flush=True)
|
|
|
|
|
server_shutdown()
|
|
|
|
|
wait_client()
|
|
|
|
|
server_startup()
|
|
|
|
|
print("Server started", flush=True)
|
|
|
|
|
wasted_cnt_seconds = 0
|
|
|
|
|
|
|
|
|
|
time.sleep(CHECK_INTERVAL)
|
|
|
|
|
|
|
|
|
|
def wait_client():
|
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
|
sock.bind(("0.0.0.0", 25565))
|
|
|
|
|
print("Listening...", flush=True)
|
|
|
|
|
sock.listen(0)
|
|
|
|
|
connection, address = sock.accept()
|
|
|
|
|
print(f"Connection from: {address}", flush=True)
|
|
|
|
|
connection.close()
|
|
|
|
|
sock.close()
|
|
|
|
|
state_fn = state_map[state]
|
|
|
|
|
state, interval = state_fn(ctx, config)
|
|
|
|
|
if interval > 0:
|
|
|
|
|
time.sleep(interval)
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
"""
|
|
|
|
|
Главная функция: читает конфиг и запускает по процессу на каждый сервис.
|
|
|
|
|
"""
|
|
|
|
|
config = configparser.ConfigParser()
|
|
|
|
|
if not os.path.exists(CONFIG_FILE):
|
|
|
|
|
print(f"Error: Configuration file '{CONFIG_FILE}' not found.", file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
config.read(CONFIG_FILE)
|
|
|
|
|
|
|
|
|
|
service_names = config.sections()
|
|
|
|
|
if not service_names:
|
|
|
|
|
print(f"Error: No services defined in '{CONFIG_FILE}'.", file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
print(f"Found services: {', '.join(service_names)}", flush=True)
|
|
|
|
|
processes = []
|
|
|
|
|
|
|
|
|
|
for service_name in service_names:
|
|
|
|
|
try:
|
|
|
|
|
# Собираем конфигурацию для одного сервиса в словарь
|
|
|
|
|
service_config = {
|
|
|
|
|
"service_name": service_name,
|
|
|
|
|
"start_command": config.get(service_name, 'start_command'),
|
|
|
|
|
"stop_command": config.get(service_name, 'stop_command'),
|
|
|
|
|
"listen_port": config.getint(service_name, 'listen_port'),
|
|
|
|
|
"listen_host": config.get(service_name, 'listen_host'),
|
|
|
|
|
"check_command": config.get(service_name, 'check_command'),
|
|
|
|
|
"check_interval_sec": config.getint(service_name, 'check_interval_sec'),
|
|
|
|
|
"idle_limit_sec": config.getint(service_name, 'idle_limit_sec'),
|
|
|
|
|
"startup_delay_sec": config.getint(service_name, 'startup_delay_sec'),
|
|
|
|
|
}
|
|
|
|
|
# Создаем и запускаем отдельный процесс для каждого сервиса
|
|
|
|
|
process = multiprocessing.Process(target=run_service_butler, args=(service_config,))
|
|
|
|
|
process.start()
|
|
|
|
|
processes.append(process)
|
|
|
|
|
except (configparser.NoOptionError, configparser.NoSectionError) as e:
|
|
|
|
|
print(f"Error parsing config for service '{service_name}': {e}", file=sys.stderr)
|
|
|
|
|
# Можно остановить уже запущенные процессы, если один конфиг невалиден
|
|
|
|
|
for p in processes:
|
|
|
|
|
p.terminate()
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
# Главный процесс ждет завершения дочерних (бесконечных) процессов
|
|
|
|
|
for p in processes:
|
|
|
|
|
p.join()
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|
|
|
|
|
|