refucktoring
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
*.swp
|
||||||
@@ -1,2 +1,9 @@
|
|||||||
|
install:
|
||||||
|
@sudo cp -v main.py /usr/local/bin/butler.py
|
||||||
|
@sudo cp -v port-butler.service /etc/systemd/system/
|
||||||
|
@sudo cp -v config.ini /etc/butler.ini
|
||||||
|
sudo chmod a+x /usr/local/bin/butler.py
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
run:
|
run:
|
||||||
bash -c 'nohup python3 main.py >nohup.out 2>&1 & disown'
|
sudo bash -c 'nohup python3 main.py >nohup.out 2>&1 & disown'
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
[minecraft]
|
||||||
|
start_command = service minecraft start
|
||||||
|
stop_command = service minecraft stop
|
||||||
|
listen_port = 25565
|
||||||
|
listen_host = 0.0.0.0
|
||||||
|
check_command = grep "There are [1-9][0-9]* of" <(/var/local/minecraft/mcrcon -H 127.0.0.1 -p password list)
|
||||||
|
check_interval_sec = 10
|
||||||
|
idle_limit_sec = 60
|
||||||
|
startup_delay_sec = 20
|
||||||
|
|
||||||
|
[palworld]
|
||||||
|
start_command = service palworld start
|
||||||
|
stop_command = service palworld stop
|
||||||
|
listen_port = 8211
|
||||||
|
listen_host = 0.0.0.0
|
||||||
|
check_command = grep -q '^[1-9][0-9]*$' <(curl -sL -X GET 'http://admin:poopa@192.168.1.21:8212/v1/api/players' -H 'Accept: application/json' | jq -r '.players | length')
|
||||||
|
check_interval_sec = 60
|
||||||
|
idle_limit_sec = 300
|
||||||
|
startup_delay_sec = 60
|
||||||
@@ -1,61 +1,169 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import configparser
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
CHECK_USERS_CMD = ["/root/mcrcon/mcrcon", "-H", "192.168.0.21", "-p", "password", "list"]
|
CONFIG_FILE = "/etc/butler.ini"
|
||||||
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:
|
try:
|
||||||
output = subprocess.check_output(CHECK_USERS_CMD)
|
# shell=True позволяет использовать пайпы (|) и другие возможности shell
|
||||||
# Maybe, server isn't running?
|
# check=True бросит исключение CalledProcessError при ненулевом коде возврата
|
||||||
except subprocess.CalledProcessError as err:
|
print(f"[{service_name}] run {check_cmd}.", flush=True)
|
||||||
print(f"Cannot access mcrcon: {err}", flush=True)
|
subprocess.run(
|
||||||
|
check_cmd,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
executable='/bin/bash'
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
header = output.decode().split("\n")[0]
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
if header.startswith("There are 0 of"):
|
# CalledProcessError: команда вернула ненулевой код (считаем, что активности нет)
|
||||||
|
# FileNotFoundError: команда не найдена (например, опечатка в пути)
|
||||||
return False
|
return False
|
||||||
return True
|
|
||||||
|
|
||||||
def server_shutdown():
|
def server_shutdown(stop_cmd, service_name):
|
||||||
subprocess.run("service minecraft stop".split(" "))
|
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():
|
def server_startup(start_cmd, service_name):
|
||||||
subprocess.run("service minecraft start".split(" "))
|
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)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
state_fn = state_map[state]
|
||||||
|
state, interval = state_fn(ctx, config)
|
||||||
|
if interval > 0:
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
wasted_cnt_seconds = 0
|
"""
|
||||||
while True:
|
Главная функция: читает конфиг и запускает по процессу на каждый сервис.
|
||||||
if are_users_there():
|
"""
|
||||||
wasted_cnt_seconds = 0
|
config = configparser.ConfigParser()
|
||||||
print("Users ok", flush=True)
|
if not os.path.exists(CONFIG_FILE):
|
||||||
time.sleep(CHECK_INTERVAL)
|
print(f"Error: Configuration file '{CONFIG_FILE}' not found.", file=sys.stderr)
|
||||||
continue
|
sys.exit(1)
|
||||||
|
|
||||||
wasted_cnt_seconds += CHECK_INTERVAL
|
config.read(CONFIG_FILE)
|
||||||
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)
|
service_names = config.sections()
|
||||||
|
if not service_names:
|
||||||
|
print(f"Error: No services defined in '{CONFIG_FILE}'.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def wait_client():
|
print(f"Found services: {', '.join(service_names)}", flush=True)
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
processes = []
|
||||||
sock.bind(("0.0.0.0", 25565))
|
|
||||||
print("Listening...", flush=True)
|
for service_name in service_names:
|
||||||
sock.listen(0)
|
try:
|
||||||
connection, address = sock.accept()
|
# Собираем конфигурацию для одного сервиса в словарь
|
||||||
print(f"Connection from: {address}", flush=True)
|
service_config = {
|
||||||
connection.close()
|
"service_name": service_name,
|
||||||
sock.close()
|
"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__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Port Butler
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 /usr/local/bin/butler.py
|
||||||
|
Restart=always
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user