serverwatcher 5.0__tar.gz → 5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {serverwatcher-5.0/src/serverwatcher.egg-info → serverwatcher-5.1}/PKG-INFO +1 -1
- {serverwatcher-5.0 → serverwatcher-5.1}/pyproject.toml +1 -1
- {serverwatcher-5.0 → serverwatcher-5.1}/src/serverwatcher/configclasses/__init__.py +1 -1
- serverwatcher-5.1/src/serverwatcher/configclasses/config.py +31 -0
- serverwatcher-5.1/src/serverwatcher/configclasses/messages.py +62 -0
- serverwatcher-5.1/src/serverwatcher/configclasses/watcher.py +30 -0
- serverwatcher-5.1/src/serverwatcher/defaultconfigs/config.yaml +28 -0
- serverwatcher-5.1/src/serverwatcher/defaultconfigs/messages.yaml +58 -0
- serverwatcher-5.1/src/serverwatcher/defaultconfigs/watcher.yaml +27 -0
- serverwatcher-5.1/src/serverwatcher/validator.py +144 -0
- {serverwatcher-5.0 → serverwatcher-5.1}/src/serverwatcher/watcher.py +60 -60
- {serverwatcher-5.0 → serverwatcher-5.1/src/serverwatcher.egg-info}/PKG-INFO +1 -1
- {serverwatcher-5.0 → serverwatcher-5.1}/src/serverwatcher.egg-info/SOURCES.txt +2 -2
- serverwatcher-5.0/src/serverwatcher/configclasses/global_config.py +0 -30
- serverwatcher-5.0/src/serverwatcher/configclasses/messages.py +0 -65
- serverwatcher-5.0/src/serverwatcher/configclasses/watcher.py +0 -30
- serverwatcher-5.0/src/serverwatcher/defaultconfigs/global.yaml +0 -28
- serverwatcher-5.0/src/serverwatcher/defaultconfigs/messages.yaml +0 -58
- serverwatcher-5.0/src/serverwatcher/defaultconfigs/watcher.yaml +0 -27
- serverwatcher-5.0/src/serverwatcher/validator.py +0 -163
- {serverwatcher-5.0 → serverwatcher-5.1}/LICENSE +0 -0
- {serverwatcher-5.0 → serverwatcher-5.1}/README.md +0 -0
- {serverwatcher-5.0 → serverwatcher-5.1}/setup.cfg +0 -0
- {serverwatcher-5.0 → serverwatcher-5.1}/src/serverwatcher/__init__.py +0 -0
- {serverwatcher-5.0 → serverwatcher-5.1}/src/serverwatcher.egg-info/dependency_links.txt +0 -0
- {serverwatcher-5.0 → serverwatcher-5.1}/src/serverwatcher.egg-info/requires.txt +0 -0
- {serverwatcher-5.0 → serverwatcher-5.1}/src/serverwatcher.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
def yaml_key(path: str):
|
|
4
|
+
return field(metadata={"yaml_key": path})
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class GlobalConfig:
|
|
8
|
+
watch_interval: int = yaml_key("watch_interval")
|
|
9
|
+
|
|
10
|
+
panel_name: str = yaml_key("panel.name")
|
|
11
|
+
panel_url: str = yaml_key("panel.url")
|
|
12
|
+
panel_api_key: str = yaml_key("panel.api_key")
|
|
13
|
+
|
|
14
|
+
origin_server_id: str = yaml_key("origin.server_id")
|
|
15
|
+
|
|
16
|
+
server_name: str = yaml_key("server.name")
|
|
17
|
+
server_id: str = yaml_key("server.server_id")
|
|
18
|
+
server_domain: str = yaml_key("server.domain")
|
|
19
|
+
server_port: int = yaml_key("server.port")
|
|
20
|
+
|
|
21
|
+
rcon_port: int = yaml_key("server.rcon_port")
|
|
22
|
+
rcon_password: str = yaml_key("server.rcon_password")
|
|
23
|
+
tps_command: str = yaml_key("server.tps_command")
|
|
24
|
+
|
|
25
|
+
enable_logging: bool = yaml_key("logger.enabled")
|
|
26
|
+
logger_name: str = yaml_key("logger.name")
|
|
27
|
+
log_path: str = yaml_key("logger.log_path")
|
|
28
|
+
timezone: str = yaml_key("logger.timezone")
|
|
29
|
+
|
|
30
|
+
console_backspaces: int = yaml_key("terminal.backspaces")
|
|
31
|
+
clear_terminal: bool = yaml_key("terminal.enable_clearing")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
def yaml_key(path: str):
|
|
4
|
+
return field(metadata={"yaml_key": path})
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class MessagesConfig:
|
|
8
|
+
prefix: str = yaml_key("prefix")
|
|
9
|
+
broadcast_restart_at: str = yaml_key("broadcast_restart_at")
|
|
10
|
+
bullet: str = yaml_key("bullet")
|
|
11
|
+
|
|
12
|
+
# minute messages
|
|
13
|
+
minute_120: str = yaml_key("broadcast_minutes.120")
|
|
14
|
+
minute_60: str = yaml_key("broadcast_minutes.60")
|
|
15
|
+
minute_45: str = yaml_key("broadcast_minutes.45")
|
|
16
|
+
minute_30: str = yaml_key("broadcast_minutes.30")
|
|
17
|
+
minute_15: str = yaml_key("broadcast_minutes.15")
|
|
18
|
+
|
|
19
|
+
# second messages
|
|
20
|
+
second_10: str = yaml_key("broadcast_seconds.10")
|
|
21
|
+
second_9: str = yaml_key("broadcast_seconds.9")
|
|
22
|
+
second_8: str = yaml_key("broadcast_seconds.8")
|
|
23
|
+
second_7: str = yaml_key("broadcast_seconds.7")
|
|
24
|
+
second_6: str = yaml_key("broadcast_seconds.6")
|
|
25
|
+
second_5: str = yaml_key("broadcast_seconds.5")
|
|
26
|
+
second_4: str = yaml_key("broadcast_seconds.4")
|
|
27
|
+
second_3: str = yaml_key("broadcast_seconds.3")
|
|
28
|
+
second_2: str = yaml_key("broadcast_seconds.2")
|
|
29
|
+
second_1: str = yaml_key("broadcast_seconds.1")
|
|
30
|
+
|
|
31
|
+
# logging
|
|
32
|
+
startup: str = yaml_key("logging.startup")
|
|
33
|
+
status_check: str = yaml_key("logging.status_check")
|
|
34
|
+
validation_fail: str = yaml_key("logging.validation_fail")
|
|
35
|
+
validation_ok: str = yaml_key("logging.validation_ok")
|
|
36
|
+
shutdown: str = yaml_key("logging.shutdown")
|
|
37
|
+
immediate_restart: str = yaml_key("logging.immediate_restart")
|
|
38
|
+
no_restart: str = yaml_key("logging.no_restart")
|
|
39
|
+
scheduled: str = yaml_key("logging.scheduled")
|
|
40
|
+
gap_low: str = yaml_key("logging.gap_low")
|
|
41
|
+
gap_high: str = yaml_key("logging.gap_high")
|
|
42
|
+
|
|
43
|
+
# reasons
|
|
44
|
+
pro_restart_splash: str = yaml_key("reasons.pro_restart_splash")
|
|
45
|
+
anti_restart_splash: str = yaml_key("reasons.anti_restart_splash")
|
|
46
|
+
|
|
47
|
+
reasons.restart_soon: str = yaml_key("reasons.restart_soon")
|
|
48
|
+
reasons.ram: str = yaml_key("reasons.ram")
|
|
49
|
+
reasons.cpu: str = yaml_key("reasons.cpu")
|
|
50
|
+
reasons.uptime: str = yaml_key("reasons.uptime")
|
|
51
|
+
reasons.tps: str = yaml_key("reasons.tps")
|
|
52
|
+
reasons.low_uptime: str = yaml_key("reasons.low_uptime")
|
|
53
|
+
reasons.players: str = yaml_key("reasons.players")
|
|
54
|
+
|
|
55
|
+
pro_restart_number: str = yaml_key("reasons.pro_restart_number")
|
|
56
|
+
anti_restart_number: str = yaml_key("reasons.anti_restart_number")
|
|
57
|
+
|
|
58
|
+
# restarts
|
|
59
|
+
restart_action_sent: str = yaml_key("restarts.restart_action_sent")
|
|
60
|
+
server_back_online: str = yaml_key("restarts.back_online")
|
|
61
|
+
server_back_online_broadcast: str = yaml_key("restarts.back_online_broadcast")
|
|
62
|
+
server_failed_restart: str = yaml_key("restarts.failed_restart")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
def yaml_key(path: str):
|
|
4
|
+
return field(metadata={"yaml_key": path})
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class WatcherConfig:
|
|
8
|
+
schedule_control: bool = yaml_key("schedule_control.enabled")
|
|
9
|
+
restart_soon_id: int = yaml_key("schedule_control.restart_soon_id")
|
|
10
|
+
|
|
11
|
+
thresholds.ram: int = yaml_key("thresholds.ram")
|
|
12
|
+
thresholds.cpu: int = yaml_key("thresholds.cpu")
|
|
13
|
+
thresholds.uptime_hours: int = yaml_key("thresholds.uptime")
|
|
14
|
+
thresholds.tps: float = yaml_key("thresholds.tps")
|
|
15
|
+
|
|
16
|
+
weights.restart_soon: int = yaml_key("weights.restart_soon")
|
|
17
|
+
weights.ram: int = yaml_key("weights.ram")
|
|
18
|
+
weights.cpu: int = yaml_key("weights.cpu")
|
|
19
|
+
weights.uptime: int = yaml_key("weights.uptime")
|
|
20
|
+
weights.tps: int = yaml_key("weights.tps")
|
|
21
|
+
|
|
22
|
+
weights.low_uptime: int = yaml_key("weights.low_uptime")
|
|
23
|
+
weights.per_player: int = yaml_key("weights.per_player")
|
|
24
|
+
|
|
25
|
+
low_gap_minutes: int = yaml_key("gaps.low_gap_minutes")
|
|
26
|
+
high_gap_minutes: int = yaml_key("gaps.high_gap_minutes")
|
|
27
|
+
|
|
28
|
+
restart.wait_seconds: int = yaml_key("restart.wait_seconds")
|
|
29
|
+
restart.timeout: int = yaml_key("restart.online_timeout")
|
|
30
|
+
restart.online_interval: int = yaml_key("restart.online_interval")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
watch_interval: 300
|
|
2
|
+
|
|
3
|
+
panel:
|
|
4
|
+
name: "My Panel"
|
|
5
|
+
url: "https://example.com"
|
|
6
|
+
api_key: "CHANGE_ME"
|
|
7
|
+
|
|
8
|
+
origin:
|
|
9
|
+
server_id: "CHANGE_ME"
|
|
10
|
+
|
|
11
|
+
server:
|
|
12
|
+
name: "My SMP"
|
|
13
|
+
server_id: "CHANGE_ME"
|
|
14
|
+
domain: "mc.example.com"
|
|
15
|
+
port: 25565
|
|
16
|
+
rcon_port: 25575
|
|
17
|
+
rcon_password: "password"
|
|
18
|
+
tps_command: "ticks"
|
|
19
|
+
|
|
20
|
+
logger:
|
|
21
|
+
enabled: True
|
|
22
|
+
name: "Server Watcher"
|
|
23
|
+
log_path: "/home/container/logs/"
|
|
24
|
+
timezone: "America/Chicago"
|
|
25
|
+
|
|
26
|
+
terminal:
|
|
27
|
+
backspaces: 8
|
|
28
|
+
enable_clearing: True
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
prefix: "<aqua>[Server Watcher]"
|
|
2
|
+
|
|
3
|
+
broadcast_restart_at: "{prefix} The server will restart at {time} CDT."
|
|
4
|
+
bullet: "-"
|
|
5
|
+
|
|
6
|
+
broadcast_minutes:
|
|
7
|
+
120: "{prefix} Restart in 2 hours!"
|
|
8
|
+
60: "{prefix} Restart in 1 hour!"
|
|
9
|
+
5: "{prefix} Restart in 45 minutes!"
|
|
10
|
+
30: "{prefix} Restart in 30 minutes!"
|
|
11
|
+
15: "{prefix} Restart in 15 minutes!"
|
|
12
|
+
10: "{prefix} Restart in 5 minutes!"
|
|
13
|
+
1: "{prefix} Restart in 1 minute!"
|
|
14
|
+
|
|
15
|
+
broadcast_seconds:
|
|
16
|
+
10: "{prefix} Restart in 10 seconds!"
|
|
17
|
+
9: "{prefix} Restart in 9 seconds!"
|
|
18
|
+
8: "{prefix} Restart in 8 seconds!"
|
|
19
|
+
7: "{prefix} Restart in 7 seconds!"
|
|
20
|
+
6: "{prefix} Restart in 6 seconds!"
|
|
21
|
+
5: "{prefix} Restart in 5 seconds!"
|
|
22
|
+
4: "{prefix} Restart in 4 seconds!"
|
|
23
|
+
3: "{prefix} Restart in 3 seconds!"
|
|
24
|
+
2: "{prefix} Restart in 2 seconds!"
|
|
25
|
+
1: "{prefix} Restart in 1 second!"
|
|
26
|
+
|
|
27
|
+
logging:
|
|
28
|
+
startup: "ServerWatcher is running!"
|
|
29
|
+
status_check: "Checking server status..."
|
|
30
|
+
validation_fail: "Validation FAILED."
|
|
31
|
+
validation_ok: "All validation checks succeeded."
|
|
32
|
+
shutdown: "Shutting down ServerWatcher."
|
|
33
|
+
immediate_restart: "Restarting immediately."
|
|
34
|
+
no_restart: "The server does not need to restart."
|
|
35
|
+
scheduled: "Restart needed, but anti-restart factors outweigh it."
|
|
36
|
+
gap_low: "Gap {gap}. Scheduling restart in 2 hours."
|
|
37
|
+
gap_high: "Gap {gap}. Scheduling restart in 1 hour."
|
|
38
|
+
|
|
39
|
+
reasons:
|
|
40
|
+
pro_splash: "PRO-RESTART REASONS:"
|
|
41
|
+
anti_splash: "ANTI-RESTART REASONS:"
|
|
42
|
+
|
|
43
|
+
restart_soon: "The server is set to restart soon"
|
|
44
|
+
ram: "RAM usage ({ram}) is higher than {threshold} GB"
|
|
45
|
+
cpu: "CPU usage ({cpu}) is higher than {threshold}%"
|
|
46
|
+
uptime: "Uptime {uptime} exceeds {threshold}h"
|
|
47
|
+
tps: "TPS {tps} is lower than {threshold}"
|
|
48
|
+
low_uptime: "Uptime {uptime} is shorter than 30m"
|
|
49
|
+
players: "There {verb} {count} {plural} online"
|
|
50
|
+
|
|
51
|
+
pro_restart_number: "Pro-restart: "
|
|
52
|
+
anti_restart_number: "Anti-restart:"
|
|
53
|
+
|
|
54
|
+
restarts:
|
|
55
|
+
restart_action_sent: "Restart action sent. Waiting..."
|
|
56
|
+
back_online: "Server is back online!"
|
|
57
|
+
back_online_broadcast: "{prefix} <green>Restart successful!"
|
|
58
|
+
failed_restart: "Server failed to restart!"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
schedule_control:
|
|
2
|
+
enabled: False
|
|
3
|
+
restart_soon_id: 0 # replace this with a real schedule id
|
|
4
|
+
|
|
5
|
+
thresholds:
|
|
6
|
+
ram: 6
|
|
7
|
+
cpu: 150
|
|
8
|
+
uptime: 12
|
|
9
|
+
tps: 19.5
|
|
10
|
+
|
|
11
|
+
weights:
|
|
12
|
+
restart_soon: 3
|
|
13
|
+
ram: 1
|
|
14
|
+
cpu: 1
|
|
15
|
+
uptime: 1
|
|
16
|
+
tps: 1
|
|
17
|
+
low_uptime: 5
|
|
18
|
+
per_player: 1
|
|
19
|
+
|
|
20
|
+
gaps:
|
|
21
|
+
low_gap_minutes: 120
|
|
22
|
+
high_gap_minutes: 60
|
|
23
|
+
|
|
24
|
+
restart:
|
|
25
|
+
wait_seconds: 30
|
|
26
|
+
online_timeout: 120
|
|
27
|
+
online_interval: 2
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from dataclasses import fields
|
|
3
|
+
|
|
4
|
+
from hungerlib.addons import loadConfig
|
|
5
|
+
|
|
6
|
+
from serverwatcher.configclasses.config import GlobalConfig
|
|
7
|
+
from serverwatcher.configclasses.messages import MessagesConfig
|
|
8
|
+
from serverwatcher.configclasses.watcher import WatcherConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def deep_get_attr(obj, dotted):
|
|
12
|
+
parts = dotted.split(".")
|
|
13
|
+
cur = obj
|
|
14
|
+
for p in parts:
|
|
15
|
+
if not hasattr(cur, p):
|
|
16
|
+
return None
|
|
17
|
+
cur = getattr(cur, p)
|
|
18
|
+
return cur
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_type(name, value, expected_type, errors):
|
|
22
|
+
if not isinstance(value, expected_type):
|
|
23
|
+
errors.append(f"{name}: expected {expected_type.__name__}, got {type(value).__name__}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_positive(name, value, errors):
|
|
27
|
+
if isinstance(value, (int, float)) and value < 0:
|
|
28
|
+
errors.append(f"{name}: must be >= 0 (got {value})")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_nonempty(name, value, errors):
|
|
32
|
+
if isinstance(value, str) and value.strip() == "":
|
|
33
|
+
errors.append(f"{name}: cannot be empty")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_dataclass(config_obj, schema, errors):
|
|
37
|
+
for f in fields(schema):
|
|
38
|
+
name = f.name
|
|
39
|
+
expected_type = f.type
|
|
40
|
+
value = deep_get_attr(config_obj, name)
|
|
41
|
+
|
|
42
|
+
validate_type(name, value, expected_type, errors)
|
|
43
|
+
|
|
44
|
+
if expected_type is str:
|
|
45
|
+
validate_nonempty(name, value, errors)
|
|
46
|
+
|
|
47
|
+
if expected_type in (int, float):
|
|
48
|
+
validate_positive(name, value, errors)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_global_config(config, errors):
|
|
52
|
+
if config.watch_interval < 1:
|
|
53
|
+
errors.append(f"watch_interval: must be >= 1 (got {config.watch_interval})")
|
|
54
|
+
|
|
55
|
+
if config.server_port <= 0 or config.server_port > 65535:
|
|
56
|
+
errors.append(f"server_port: must be 1–65535 (got {config.server_port})")
|
|
57
|
+
|
|
58
|
+
if config.rcon_port <= 0 or config.rcon_port > 65535:
|
|
59
|
+
errors.append(f"rcon_port: must be 1–65535 (got {config.rcon_port})")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_watcher_config(watcherconfig, errors):
|
|
63
|
+
if watcherconfig.restart.wait_seconds < 1:
|
|
64
|
+
errors.append(f"restart.wait_seconds: must be >= 1 (got {watcherconfig.restart.wait_seconds})")
|
|
65
|
+
|
|
66
|
+
if watcherconfig.thresholds.cpu <= 0:
|
|
67
|
+
errors.append(f"thresholds.cpu: must not be less than 1 (got {watcherconfig.thresholds.cpu})")
|
|
68
|
+
|
|
69
|
+
if watcherconfig.thresholds.ram <= 0:
|
|
70
|
+
errors.append(f"thresholds.ram: must be > 0 (got {watcherconfig.thresholds.ram})")
|
|
71
|
+
|
|
72
|
+
if watcherconfig.thresholds.tps <= 0 or watcherconfig.thresholds.tps > 20:
|
|
73
|
+
errors.append(f"thresholds.tps: must be 1–20 (got {watcherconfig.thresholds.tps})")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_messages_config(messages, errors):
|
|
77
|
+
for name, value in vars(messages).items():
|
|
78
|
+
if isinstance(value, str) and "{prefix}" not in value:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def ensure_no_global_defaults(config, defaults):
|
|
83
|
+
if config.panel_url == "https://example.com":
|
|
84
|
+
defaults.append('panel_url')
|
|
85
|
+
|
|
86
|
+
if config.panel_api_key == 'CHANGE_ME':
|
|
87
|
+
defaults.append('panel_api_key')
|
|
88
|
+
|
|
89
|
+
if config.origin_server_id == 'CHANGE_ME':
|
|
90
|
+
defaults.append('origin_server_id')
|
|
91
|
+
|
|
92
|
+
if config.server_id == 'CHANGE_ME':
|
|
93
|
+
defaults.append('server_id')
|
|
94
|
+
|
|
95
|
+
if config.server_domain == 'mc.example.com':
|
|
96
|
+
defaults.append('server_domain')
|
|
97
|
+
|
|
98
|
+
if config.rcon_password == 'password':
|
|
99
|
+
defaults.append('rcon_password')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ensure_no_watcher_defaults(watcherconfig, defaults):
|
|
103
|
+
if watcherconfig.schedule_control and watcherconfig.restart_soon_id == 0:
|
|
104
|
+
defaults.append('restart_soon_id')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def validate_all():
|
|
108
|
+
errors = []
|
|
109
|
+
defaults = []
|
|
110
|
+
|
|
111
|
+
config = loadConfig("config/global.yaml", "/defaultconfigs/global.yaml", GlobalConfig)
|
|
112
|
+
messages = loadConfig("config/messages.yaml", "/defaultconfigs/messages.yaml", MessagesConfig)
|
|
113
|
+
watcher = loadConfig("config/watcher.yaml", "/defaultconfigs/watcher.yaml", WatcherConfig)
|
|
114
|
+
|
|
115
|
+
validate_dataclass(config, GlobalConfig, errors)
|
|
116
|
+
validate_dataclass(messages, MessagesConfig, errors)
|
|
117
|
+
validate_dataclass(watcher, WatcherConfig, errors)
|
|
118
|
+
|
|
119
|
+
validate_global_config(config, errors)
|
|
120
|
+
validate_messages_config(messages, errors)
|
|
121
|
+
validate_watcher_config(watcher, errors)
|
|
122
|
+
|
|
123
|
+
ensure_no_global_defaults(config, defaults)
|
|
124
|
+
ensure_no_watcher_defaults(watcher, defaults)
|
|
125
|
+
|
|
126
|
+
if len(defaults) >= 7:
|
|
127
|
+
print("❌ CONFIG VALIDATION FAILED:\nIt looks like you haven't configured this yet! Please change these defaults:")
|
|
128
|
+
for d in defaults:
|
|
129
|
+
print(" -", d)
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
if errors or defaults:
|
|
133
|
+
print("❌ CONFIG VALIDATION FAILED:")
|
|
134
|
+
for e in errors:
|
|
135
|
+
print(" -", e)
|
|
136
|
+
for d in defaults:
|
|
137
|
+
print(" -", d, ": must not be left default")
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
print("✅ All configs are valid.")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
validate_all()
|
|
@@ -14,7 +14,7 @@ from hungerlib.addons import (
|
|
|
14
14
|
loadConfig,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
from serverwatcher.configclasses.
|
|
17
|
+
from serverwatcher.configclasses.config import GlobalConfig
|
|
18
18
|
from serverwatcher.configclasses.messages import MessagesConfig
|
|
19
19
|
from serverwatcher.configclasses.watcher import WatcherConfig
|
|
20
20
|
|
|
@@ -25,9 +25,9 @@ validate_all()
|
|
|
25
25
|
class ServerWatcher:
|
|
26
26
|
def __init__(self):
|
|
27
27
|
|
|
28
|
-
self.
|
|
29
|
-
"config/
|
|
30
|
-
"/defaultconfigs/
|
|
28
|
+
self.config = loadConfig(
|
|
29
|
+
"config/config.yaml",
|
|
30
|
+
"/defaultconfigs/config.yaml",
|
|
31
31
|
GlobalConfig
|
|
32
32
|
)
|
|
33
33
|
|
|
@@ -37,47 +37,47 @@ class ServerWatcher:
|
|
|
37
37
|
MessagesConfig
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
self.
|
|
40
|
+
self.watcherconfig = loadConfig(
|
|
41
41
|
"config/watcher.yaml",
|
|
42
42
|
"/defaultconfigs/watcher.yaml",
|
|
43
43
|
WatcherConfig
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
self.panel = Panel(
|
|
47
|
-
name=self.
|
|
48
|
-
url=self.
|
|
49
|
-
api_key=self.
|
|
47
|
+
name=self.config.panel_name,
|
|
48
|
+
url=self.config.panel_url,
|
|
49
|
+
api_key=self.config.panel_api_key,
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
self.origin = GenericServer(
|
|
53
53
|
name="Origin",
|
|
54
54
|
panel=self.panel,
|
|
55
|
-
server_id=self.
|
|
55
|
+
server_id=self.config.origin_server_id
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
self.server = MinecraftServer(
|
|
59
|
-
name=self.
|
|
59
|
+
name=self.config.server_name,
|
|
60
60
|
panel=self.panel,
|
|
61
|
-
server_id=self.
|
|
62
|
-
server_domain=self.
|
|
63
|
-
server_port=self.
|
|
64
|
-
rcon_port=self.
|
|
65
|
-
rcon_password=self.
|
|
66
|
-
tpsCommand=self.
|
|
61
|
+
server_id=self.config.server_id,
|
|
62
|
+
server_domain=self.config.server_domain,
|
|
63
|
+
server_port=self.config.server_port,
|
|
64
|
+
rcon_port=self.config.rcon_port,
|
|
65
|
+
rcon_password=self.config.rcon_password,
|
|
66
|
+
tpsCommand=self.config.tps_command,
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
-
logger_name = self.
|
|
70
|
-
server_name=self.
|
|
69
|
+
logger_name = self.config.logger_name.format(
|
|
70
|
+
server_name=self.config.server_name
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
self.log = HungerLogger(
|
|
74
|
-
name=logger_name,
|
|
74
|
+
name=self.config.logger_name,
|
|
75
75
|
server=self.server,
|
|
76
|
-
log_path=self.
|
|
77
|
-
console_backspaces=self.
|
|
76
|
+
log_path=self.config.log_path,
|
|
77
|
+
console_backspaces=self.config.console_backspaces,
|
|
78
78
|
)
|
|
79
79
|
|
|
80
|
-
self.tz = ZoneInfo(self.
|
|
80
|
+
self.tz = ZoneInfo(self.config.timezone)
|
|
81
81
|
|
|
82
82
|
def fmt(self, template: str, **kwargs):
|
|
83
83
|
return template.format(prefix=self.messages.prefix, **kwargs)
|
|
@@ -89,21 +89,21 @@ class ServerWatcher:
|
|
|
89
89
|
getattr(self.log, level)(text)
|
|
90
90
|
|
|
91
91
|
def shutdown(self):
|
|
92
|
-
self.say(self.messages.
|
|
92
|
+
self.say(self.messages.shutdown)
|
|
93
93
|
raise SystemExit
|
|
94
94
|
|
|
95
95
|
def restart_and_wait(self):
|
|
96
|
-
if self.
|
|
97
|
-
self.origin.disableSchedule(self.
|
|
96
|
+
if self.watcherconfig.schedule_control:
|
|
97
|
+
self.origin.disableSchedule(self.watcherconfig.restart_soon_id)
|
|
98
98
|
self.server.restart()
|
|
99
99
|
self.say(self.messages.restart_action_sent)
|
|
100
|
-
time.sleep(self.
|
|
100
|
+
time.sleep(self.watcherconfig.restart.wait_seconds)
|
|
101
101
|
|
|
102
102
|
self.say(self.messages.log_status_check, level="warn")
|
|
103
103
|
alive = waitForOnline(
|
|
104
104
|
self.server,
|
|
105
|
-
timeout=self.
|
|
106
|
-
interval=self.
|
|
105
|
+
timeout=self.watcherconfig.restart.timeout,
|
|
106
|
+
interval=self.watcherconfig.restart.online_interval,
|
|
107
107
|
)
|
|
108
108
|
|
|
109
109
|
if alive:
|
|
@@ -146,10 +146,10 @@ class ServerWatcher:
|
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
def evaluate(self):
|
|
149
|
-
self.say(self.messages.
|
|
149
|
+
self.say(self.messages.startup)
|
|
150
150
|
|
|
151
151
|
if not validateAll(self.panel, self.server):
|
|
152
|
-
self.say(self.messages.
|
|
152
|
+
self.say(self.messages.validation_fail, level="error")
|
|
153
153
|
self.shutdown()
|
|
154
154
|
|
|
155
155
|
self.server.refresh()
|
|
@@ -160,38 +160,38 @@ class ServerWatcher:
|
|
|
160
160
|
restart_reasons = []
|
|
161
161
|
no_restart_reasons = []
|
|
162
162
|
|
|
163
|
-
if self.
|
|
164
|
-
restart_reasons.append(self.messages.
|
|
165
|
-
pro += self.
|
|
163
|
+
if self.watcherconfig.schedule_control and self.server.getSchedule(self.watcherconfig.restart_soon_id)["is_active"]:
|
|
164
|
+
restart_reasons.append(self.messages.reasons.restart_soon)
|
|
165
|
+
pro += self.watcherconfig.weights.restart_soon
|
|
166
166
|
|
|
167
|
-
if snap.ram >= self.
|
|
168
|
-
restart_reasons.append(self.fmt(self.messages.
|
|
169
|
-
pro += int(round(snap.ram, 0) - (self.
|
|
167
|
+
if snap.ram >= self.watcherconfig.thresholds.ram:
|
|
168
|
+
restart_reasons.append(self.fmt(self.messages.reasons.ram, ram=snap.ram, threshold=self.watcherconfig.thresholds.ram))
|
|
169
|
+
pro += int(round(snap.ram, 0) - (self.watcherconfig.thresholds.ram - 1))
|
|
170
170
|
|
|
171
|
-
if snap.cpu >= self.
|
|
172
|
-
restart_reasons.append(self.fmt(self.messages.
|
|
173
|
-
pro += self.
|
|
171
|
+
if snap.cpu >= self.watcherconfig.thresholds.cpu:
|
|
172
|
+
restart_reasons.append(self.fmt(self.messages.reasons.cpu, cpu=snap.cpu, threshold=self.watcherconfig.thresholds.cpu))
|
|
173
|
+
pro += self.watcherconfig.weights.cpu
|
|
174
174
|
|
|
175
|
-
if snap.uptime // 3600 >= self.
|
|
175
|
+
if snap.uptime // 3600 >= self.watcherconfig.thresholds.uptime:
|
|
176
176
|
restart_reasons.append(
|
|
177
|
-
self.fmt(self.messages.
|
|
178
|
-
threshold=self.
|
|
177
|
+
self.fmt(self.messages.reasons.uptime, uptime=snap.uptime_formatted,
|
|
178
|
+
threshold=self.watcherconfig.thresholds.uptime)
|
|
179
179
|
)
|
|
180
|
-
pro += self.
|
|
180
|
+
pro += self.watcherconfig.weight_uptime
|
|
181
181
|
|
|
182
|
-
if (snap.tps if snap.tps is not None else 0) <= self.
|
|
183
|
-
restart_reasons.append(self.fmt(self.messages.
|
|
184
|
-
pro += self.
|
|
182
|
+
if (snap.tps if snap.tps is not None else 0) <= self.watcherconfig.thresholds.tps:
|
|
183
|
+
restart_reasons.append(self.fmt(self.messages.reasons.tps, tps=snap.tps, threshold=self.watcherconfig.thresholds.tps))
|
|
184
|
+
pro += self.watcherconfig.weights.tps
|
|
185
185
|
|
|
186
186
|
if snap.uptime // 60 < 30:
|
|
187
|
-
no_restart_reasons.append(self.fmt(self.messages.
|
|
188
|
-
anti += self.
|
|
187
|
+
no_restart_reasons.append(self.fmt(self.messages.reasons.low_uptime, uptime=snap.uptime_formatted))
|
|
188
|
+
anti += self.watcherconfig.weights.low_uptime
|
|
189
189
|
|
|
190
190
|
if snap.players > 0:
|
|
191
191
|
verb = "are" if snap.players != 1 else "is"
|
|
192
192
|
plural = "players" if snap.players != 1 else "player"
|
|
193
|
-
no_restart_reasons.append(self.fmt(self.messages.
|
|
194
|
-
anti += snap.players * self.
|
|
193
|
+
no_restart_reasons.append(self.fmt(self.messages.reasons.players, verb=verb, count=snap.players, plural=plural))
|
|
194
|
+
anti += snap.players * self.watcherconfig.weights.per_player
|
|
195
195
|
|
|
196
196
|
if restart_reasons:
|
|
197
197
|
self.say(self.messages.pro_restart_splash, level="warn")
|
|
@@ -211,30 +211,30 @@ class ServerWatcher:
|
|
|
211
211
|
gap = abs(pro - anti)
|
|
212
212
|
|
|
213
213
|
if pro == 0:
|
|
214
|
-
self.say(self.messages.
|
|
214
|
+
self.say(self.messages.no_restart)
|
|
215
215
|
return
|
|
216
216
|
|
|
217
217
|
if pro > anti and snap.players == 0:
|
|
218
|
-
self.say(self.messages.
|
|
218
|
+
self.say(self.messages.immediate_restart)
|
|
219
219
|
self.restart_and_wait()
|
|
220
220
|
return
|
|
221
221
|
|
|
222
|
-
self.say(self.messages.
|
|
222
|
+
self.say(self.messages.scheduled)
|
|
223
223
|
|
|
224
224
|
if gap <= 2:
|
|
225
|
-
self.say(self.messages.
|
|
226
|
-
self.schedule_restart(self.
|
|
225
|
+
self.say(self.messages.gap_low, level="warn", gap=gap)
|
|
226
|
+
self.schedule_restart(self.watcherconfig.low_gap_minutes)
|
|
227
227
|
else:
|
|
228
|
-
self.say(self.messages.
|
|
229
|
-
self.schedule_restart(self.
|
|
228
|
+
self.say(self.messages.gap_high, level="warn", gap=gap)
|
|
229
|
+
self.schedule_restart(self.watcherconfig.high_gap_minutes)
|
|
230
230
|
|
|
231
231
|
self.restart_and_wait()
|
|
232
232
|
|
|
233
233
|
def run(self):
|
|
234
|
-
if self.
|
|
234
|
+
if self.config.clear_terminal:
|
|
235
235
|
clearTerminal()
|
|
236
236
|
while True:
|
|
237
|
-
if self.
|
|
237
|
+
if self.config.clear_terminal:
|
|
238
238
|
clearTerminal()
|
|
239
239
|
self.evaluate()
|
|
240
|
-
time.sleep(self.
|
|
240
|
+
time.sleep(self.config.watch_interval)
|
|
@@ -10,9 +10,9 @@ src/serverwatcher.egg-info/dependency_links.txt
|
|
|
10
10
|
src/serverwatcher.egg-info/requires.txt
|
|
11
11
|
src/serverwatcher.egg-info/top_level.txt
|
|
12
12
|
src/serverwatcher/configclasses/__init__.py
|
|
13
|
-
src/serverwatcher/configclasses/
|
|
13
|
+
src/serverwatcher/configclasses/config.py
|
|
14
14
|
src/serverwatcher/configclasses/messages.py
|
|
15
15
|
src/serverwatcher/configclasses/watcher.py
|
|
16
|
-
src/serverwatcher/defaultconfigs/
|
|
16
|
+
src/serverwatcher/defaultconfigs/config.yaml
|
|
17
17
|
src/serverwatcher/defaultconfigs/messages.yaml
|
|
18
18
|
src/serverwatcher/defaultconfigs/watcher.yaml
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
|
|
3
|
-
def yaml_key(name: str):
|
|
4
|
-
return field(metadata={"yaml_key": name})
|
|
5
|
-
|
|
6
|
-
@dataclass
|
|
7
|
-
class GlobalConfig:
|
|
8
|
-
watch_interval: int = yaml_key("watch_interval")
|
|
9
|
-
|
|
10
|
-
panel_name: str = yaml_key("panel_name")
|
|
11
|
-
panel_url: str = yaml_key("panel_url")
|
|
12
|
-
panel_api_key: str = yaml_key("panel_api_key")
|
|
13
|
-
|
|
14
|
-
origin_server_id: str = yaml_key("origin_server_id")
|
|
15
|
-
|
|
16
|
-
server_name: str = yaml_key("server_name")
|
|
17
|
-
server_id: str = yaml_key("server_id")
|
|
18
|
-
server_domain: str = yaml_key("server_domain")
|
|
19
|
-
server_port: int = yaml_key("server_port")
|
|
20
|
-
rcon_port: int = yaml_key("rcon_port")
|
|
21
|
-
rcon_password: str = yaml_key("rcon_password")
|
|
22
|
-
tps_command: str = yaml_key("tps_command")
|
|
23
|
-
|
|
24
|
-
do_logging: bool = yaml_key("do_logging")
|
|
25
|
-
logger_name: str = yaml_key("logger_name")
|
|
26
|
-
log_path: str = yaml_key("log_path")
|
|
27
|
-
timezone: str = yaml_key("timezone")
|
|
28
|
-
|
|
29
|
-
console_backspaces: int = yaml_key("console_backspaces")
|
|
30
|
-
clear_terminal: bool = yaml_key("clear_terminal")
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
|
|
3
|
-
def yaml_key(name: str):
|
|
4
|
-
return field(metadata={"yaml_key": name})
|
|
5
|
-
|
|
6
|
-
@dataclass
|
|
7
|
-
class MessagesConfig:
|
|
8
|
-
prefix: str = yaml_key("prefix")
|
|
9
|
-
|
|
10
|
-
broadcast_restart_at: str = yaml_key("broadcast_restart_at")
|
|
11
|
-
bullet: str = yaml_key("bullet")
|
|
12
|
-
|
|
13
|
-
# minute messages
|
|
14
|
-
minute_120: str = yaml_key("minute_120")
|
|
15
|
-
minute_60: str = yaml_key("minute_60")
|
|
16
|
-
minute_45: str = yaml_key("minute_45")
|
|
17
|
-
minute_30: str = yaml_key("minute_30")
|
|
18
|
-
minute_15: str = yaml_key("minute_15")
|
|
19
|
-
minute_5: str = yaml_key("minute_5")
|
|
20
|
-
minute_1: str = yaml_key("minute_1")
|
|
21
|
-
|
|
22
|
-
# second messages
|
|
23
|
-
second_10: str = yaml_key("second_10")
|
|
24
|
-
second_9: str = yaml_key("second_9")
|
|
25
|
-
second_8: str = yaml_key("second_8")
|
|
26
|
-
second_7: str = yaml_key("second_7")
|
|
27
|
-
second_6: str = yaml_key("second_6")
|
|
28
|
-
second_5: str = yaml_key("second_5")
|
|
29
|
-
second_4: str = yaml_key("second_4")
|
|
30
|
-
second_3: str = yaml_key("second_3")
|
|
31
|
-
second_2: str = yaml_key("second_2")
|
|
32
|
-
second_1: str = yaml_key("second_1")
|
|
33
|
-
|
|
34
|
-
# logging
|
|
35
|
-
log_start: str = yaml_key("log_start")
|
|
36
|
-
log_status_check: str = yaml_key("log_status_check")
|
|
37
|
-
log_validation_fail: str = yaml_key("log_validation_fail")
|
|
38
|
-
log_validation_ok: str = yaml_key("log_validation_ok")
|
|
39
|
-
log_shutdown: str = yaml_key("log_shutdown")
|
|
40
|
-
log_immediate_restart: str = yaml_key("log_immediate_restart")
|
|
41
|
-
log_no_restart: str = yaml_key("log_no_restart")
|
|
42
|
-
log_scheduled: str = yaml_key("log_scheduled")
|
|
43
|
-
log_gap_low: str = yaml_key("log_gap_low")
|
|
44
|
-
log_gap_high: str = yaml_key("log_gap_high")
|
|
45
|
-
|
|
46
|
-
# reasons
|
|
47
|
-
pro_restart_splash: str = yaml_key("pro_restart_splash")
|
|
48
|
-
anti_restart_splash: str = yaml_key("anti_restart_splash")
|
|
49
|
-
|
|
50
|
-
reason_restart_soon: str = yaml_key("reason_restart_soon")
|
|
51
|
-
reason_ram: str = yaml_key("reason_ram")
|
|
52
|
-
reason_cpu: str = yaml_key("reason_cpu")
|
|
53
|
-
reason_uptime: str = yaml_key("reason_uptime")
|
|
54
|
-
reason_tps: str = yaml_key("reason_tps")
|
|
55
|
-
reason_low_uptime: str = yaml_key("reason_low_uptime")
|
|
56
|
-
reason_players: str = yaml_key("reason_players")
|
|
57
|
-
|
|
58
|
-
pro_restart_number: str = yaml_key("pro_restart_number")
|
|
59
|
-
anti_restart_number: str = yaml_key("anti_restart_number")
|
|
60
|
-
|
|
61
|
-
# restarts
|
|
62
|
-
restart_action_sent: str = yaml_key("restart_action_sent")
|
|
63
|
-
server_back_online: str = yaml_key("server_back_online")
|
|
64
|
-
server_back_online_broadcast: str = yaml_key("server_back_online_broadcast")
|
|
65
|
-
server_failed_restart: str = yaml_key("server_failed_restart")
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
|
|
3
|
-
def yaml_key(name: str):
|
|
4
|
-
return field(metadata={"yaml_key": name})
|
|
5
|
-
|
|
6
|
-
@dataclass
|
|
7
|
-
class WatcherConfig:
|
|
8
|
-
schedule_control: bool = yaml_key("schedule_control")
|
|
9
|
-
restart_soon_id: int = yaml_key("restart_soon_id")
|
|
10
|
-
|
|
11
|
-
ram_threshold: int = yaml_key("ram_threshold")
|
|
12
|
-
cpu_threshold: int = yaml_key("cpu_threshold")
|
|
13
|
-
uptime_hours_threshold: int = yaml_key("uptime_hours_threshold")
|
|
14
|
-
tps_threshold: float = yaml_key("tps_threshold")
|
|
15
|
-
|
|
16
|
-
weight_restart_soon: int = yaml_key("weight_restart_soon")
|
|
17
|
-
weight_ram: int = yaml_key("weight_ram")
|
|
18
|
-
weight_cpu: int = yaml_key("weight_cpu")
|
|
19
|
-
weight_uptime: int = yaml_key("weight_uptime")
|
|
20
|
-
weight_tps: int = yaml_key("weight_tps")
|
|
21
|
-
|
|
22
|
-
weight_low_uptime: int = yaml_key("weight_low_uptime")
|
|
23
|
-
weight_per_player: int = yaml_key("weight_per_player")
|
|
24
|
-
|
|
25
|
-
low_gap_minutes: int = yaml_key("low_gap_minutes")
|
|
26
|
-
high_gap_minutes: int = yaml_key("high_gap_minutes")
|
|
27
|
-
|
|
28
|
-
restart_wait_seconds: int = yaml_key("restart_wait_seconds")
|
|
29
|
-
restart_online_timeout: int = yaml_key("restart_online_timeout")
|
|
30
|
-
restart_online_interval: int = yaml_key("restart_online_interval")
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
watch_interval: 300
|
|
2
|
-
|
|
3
|
-
panel:
|
|
4
|
-
panel_name: "My Panel"
|
|
5
|
-
panel_url: "https://example.com"
|
|
6
|
-
panel_api_key: "CHANGE_ME"
|
|
7
|
-
|
|
8
|
-
origin:
|
|
9
|
-
origin_server_id: "CHANGE_ME"
|
|
10
|
-
|
|
11
|
-
server:
|
|
12
|
-
server_name: "My SMP"
|
|
13
|
-
server_id: "CHANGE_ME"
|
|
14
|
-
server_domain: "mc.example.com"
|
|
15
|
-
server_port: 25565
|
|
16
|
-
rcon_port: 25575
|
|
17
|
-
rcon_password: "password"
|
|
18
|
-
tps_command: "ticks"
|
|
19
|
-
|
|
20
|
-
logging:
|
|
21
|
-
do_logging: True
|
|
22
|
-
logger_name: "Server Watcher"
|
|
23
|
-
log_path: "/home/container/logs/"
|
|
24
|
-
timezone: "America/Chicago"
|
|
25
|
-
|
|
26
|
-
terminal:
|
|
27
|
-
console_backspaces: 8
|
|
28
|
-
clear_terminal: True
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
prefix: "<reset><aqua>[Server Watcher]"
|
|
2
|
-
|
|
3
|
-
broadcast_restart_at: "{prefix} The server will restart at {time} CDT."
|
|
4
|
-
bullet: "-"
|
|
5
|
-
|
|
6
|
-
broadcast_minute:
|
|
7
|
-
minute_120: "{prefix} Restart in 2 hours!"
|
|
8
|
-
minute_60: "{prefix} Restart in 1 hour!"
|
|
9
|
-
minute_45: "{prefix} Restart in 45 minutes!"
|
|
10
|
-
minute_30: "{prefix} Restart in 30 minutes!"
|
|
11
|
-
minute_15: "{prefix} Restart in 15 minutes!"
|
|
12
|
-
minute_5: "{prefix} Restart in 5 minutes!"
|
|
13
|
-
minute_1: "{prefix} Restart in 1 minute!"
|
|
14
|
-
|
|
15
|
-
broadcast_second:
|
|
16
|
-
second_10: "{prefix} Restart in 10 seconds!"
|
|
17
|
-
second_9: "{prefix} Restart in 9 seconds!"
|
|
18
|
-
second_8: "{prefix} Restart in 8 seconds!"
|
|
19
|
-
second_7: "{prefix} Restart in 7 seconds!"
|
|
20
|
-
second_6: "{prefix} Restart in 6 seconds!"
|
|
21
|
-
second_5: "{prefix} Restart in 5 seconds!"
|
|
22
|
-
second_4: "{prefix} Restart in 4 seconds!"
|
|
23
|
-
second_3: "{prefix} Restart in 3 seconds!"
|
|
24
|
-
second_2: "{prefix} Restart in 2 seconds!"
|
|
25
|
-
second_1: "{prefix} Restart in 1 second!"
|
|
26
|
-
|
|
27
|
-
logging:
|
|
28
|
-
log_start: "ServerWatcher is running!"
|
|
29
|
-
log_status_check: "Checking server status..."
|
|
30
|
-
log_validation_fail: "Validation FAILED."
|
|
31
|
-
log_validation_ok: "All validation checks succeeded."
|
|
32
|
-
log_shutdown: "Shutting down ServerWatcher."
|
|
33
|
-
log_immediate_restart: "Restarting immediately."
|
|
34
|
-
log_no_restart: "The server does not need to restart."
|
|
35
|
-
log_scheduled: "Restart needed, but anti-restart factors outweigh it."
|
|
36
|
-
log_gap_low: "Gap {gap}. Scheduling restart in 2 hours."
|
|
37
|
-
log_gap_high: "Gap {gap}. Scheduling restart in 1 hour."
|
|
38
|
-
|
|
39
|
-
reasons:
|
|
40
|
-
pro_restart_splash: "PRO-RESTART REASONS:"
|
|
41
|
-
anti_restart_splash: "ANTI-RESTART REASONS:"
|
|
42
|
-
|
|
43
|
-
reason_restart_soon: "The server is set to restart soon"
|
|
44
|
-
reason_ram: "RAM usage ({ram}) is higher than {threshold} GB"
|
|
45
|
-
reason_cpu: "CPU usage ({cpu}) is higher than {threshold}%"
|
|
46
|
-
reason_uptime: "Uptime {uptime} exceeds {threshold}h"
|
|
47
|
-
reason_tps: "TPS {tps} is lower than {threshold}"
|
|
48
|
-
reason_low_uptime: "Uptime {uptime} is shorter than 30m"
|
|
49
|
-
reason_players: "There {verb} {count} {plural} online"
|
|
50
|
-
|
|
51
|
-
pro_restart_number: "Pro-restart: "
|
|
52
|
-
anti_restart_number: "Anti-restart:"
|
|
53
|
-
|
|
54
|
-
restarts:
|
|
55
|
-
restart_action_sent: "Restart action sent. Waiting..."
|
|
56
|
-
server_back_online: "Server is back online!"
|
|
57
|
-
server_back_online_broadcast: "{prefix} <green>Restart successful!"
|
|
58
|
-
server_failed_restart: "Server failed to restart!"
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
schedules:
|
|
2
|
-
schedule_control: False
|
|
3
|
-
restart_soon_id: 0 # replace this with a real schedule id
|
|
4
|
-
|
|
5
|
-
thresholds:
|
|
6
|
-
ram_threshold: 6
|
|
7
|
-
cpu_threshold: 150
|
|
8
|
-
uptime_hours_threshold: 12
|
|
9
|
-
tps_threshold: 19.5
|
|
10
|
-
|
|
11
|
-
weights:
|
|
12
|
-
weight_restart_soon: 3
|
|
13
|
-
weight_ram: 1
|
|
14
|
-
weight_cpu: 1
|
|
15
|
-
weight_uptime: 1
|
|
16
|
-
weight_tps: 1
|
|
17
|
-
weight_low_uptime: 5
|
|
18
|
-
weight_per_player: 1
|
|
19
|
-
|
|
20
|
-
gaps:
|
|
21
|
-
low_gap_minutes: 120
|
|
22
|
-
high_gap_minutes: 60
|
|
23
|
-
|
|
24
|
-
restart_intervals:
|
|
25
|
-
restart_wait_seconds: 30
|
|
26
|
-
restart_online_timeout: 120
|
|
27
|
-
restart_online_interval: 2
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from dataclasses import fields
|
|
3
|
-
|
|
4
|
-
from hungerlib.addons import loadConfig
|
|
5
|
-
|
|
6
|
-
from serverwatcher.configclasses.global_config import GlobalConfig
|
|
7
|
-
from serverwatcher.configclasses.messages import MessagesConfig
|
|
8
|
-
from serverwatcher.configclasses.watcher import WatcherConfig
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# -----------------------------
|
|
12
|
-
# Generic validation helpers
|
|
13
|
-
# -----------------------------
|
|
14
|
-
|
|
15
|
-
def validate_type(name, value, expected_type, errors):
|
|
16
|
-
if not isinstance(value, expected_type):
|
|
17
|
-
errors.append(f"{name}: expected {expected_type.__name__}, got {type(value).__name__}")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def validate_positive(name, value, errors):
|
|
21
|
-
if isinstance(value, (int, float)) and value < 0:
|
|
22
|
-
errors.append(f"{name}: must be >= 0 (got {value})")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def validate_nonempty(name, value, errors):
|
|
26
|
-
if isinstance(value, str) and value.strip() == "":
|
|
27
|
-
errors.append(f"{name}: cannot be empty")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def validate_dataclass(config_obj, schema, errors):
|
|
31
|
-
"""
|
|
32
|
-
Validate all fields in a dataclass:
|
|
33
|
-
- type correctness
|
|
34
|
-
- non-negative numbers
|
|
35
|
-
- non-empty strings
|
|
36
|
-
"""
|
|
37
|
-
for f in fields(schema):
|
|
38
|
-
name = f.name
|
|
39
|
-
expected_type = f.type
|
|
40
|
-
value = getattr(config_obj, name)
|
|
41
|
-
|
|
42
|
-
# Type check
|
|
43
|
-
validate_type(name, value, expected_type, errors)
|
|
44
|
-
|
|
45
|
-
# String checks
|
|
46
|
-
if expected_type is str:
|
|
47
|
-
validate_nonempty(name, value, errors)
|
|
48
|
-
|
|
49
|
-
# Numeric checks
|
|
50
|
-
if expected_type in (int, float):
|
|
51
|
-
validate_positive(name, value, errors)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# -----------------------------
|
|
55
|
-
# Config-specific validation
|
|
56
|
-
# -----------------------------
|
|
57
|
-
|
|
58
|
-
def validate_global_config(cfg, errors):
|
|
59
|
-
if cfg.watch_interval < 1:
|
|
60
|
-
errors.append(f"watch_interval: must be >= 1 (got {cfg.watch_interval})")
|
|
61
|
-
|
|
62
|
-
# Example: ensure ports are valid
|
|
63
|
-
if cfg.server_port <= 0 or cfg.server_port > 65535:
|
|
64
|
-
errors.append(f"server_port: must be 1–65535 (got {cfg.server_port})")
|
|
65
|
-
|
|
66
|
-
if cfg.rcon_port <= 0 or cfg.rcon_port > 65535:
|
|
67
|
-
errors.append(f"rcon_port: must be 1–65535 (got {cfg.rcon_port})")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def validate_watcher_config(cfg, errors):
|
|
71
|
-
if cfg.restart_wait_seconds < 1:
|
|
72
|
-
errors.append(f"restart_wait_seconds: must be >= 1 (got {cfg.restart_wait_seconds})")
|
|
73
|
-
|
|
74
|
-
if cfg.cpu_threshold <= 0:
|
|
75
|
-
errors.append(f"cpu_threshold: must not be less than 1 (got {cfg.cpu_threshold})")
|
|
76
|
-
|
|
77
|
-
if cfg.ram_threshold <= 0:
|
|
78
|
-
errors.append(f"ram_threshold: must be > 0 (got {cfg.ram_threshold})")
|
|
79
|
-
|
|
80
|
-
if cfg.tps_threshold <= 0 or cfg.tps_threshold > 20:
|
|
81
|
-
errors.append(f"tps_threshold: must be 1–20 (got {cfg.tps_threshold})")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def validate_messages_config(cfg, errors):
|
|
85
|
-
# Ensure all message templates contain {prefix}
|
|
86
|
-
for name, value in vars(cfg).items():
|
|
87
|
-
if isinstance(value, str) and "{prefix}" not in value:
|
|
88
|
-
# Not required for every field, but warn if missing
|
|
89
|
-
pass
|
|
90
|
-
|
|
91
|
-
def ensure_no_global_defaults(cfg, defaults):
|
|
92
|
-
if cfg.panel_url == "https://example.com":
|
|
93
|
-
defaults.append('panel_url')
|
|
94
|
-
|
|
95
|
-
if cfg.panel_api_key == 'CHANGE_ME':
|
|
96
|
-
defaults.append('panel_api_key')
|
|
97
|
-
|
|
98
|
-
if cfg.origin_server_id == 'CHANGE_ME':
|
|
99
|
-
defaults.append('origin_server_id')
|
|
100
|
-
|
|
101
|
-
if cfg.server_id == 'CHANGE_ME':
|
|
102
|
-
defaults.append('server_id')
|
|
103
|
-
|
|
104
|
-
if cfg.server_domain == 'mc.example.com':
|
|
105
|
-
defaults.append('server_domain')
|
|
106
|
-
|
|
107
|
-
if cfg.rcon_password == 'password':
|
|
108
|
-
defaults.append('rcon_password')
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def ensure_no_watcher_defaults(cfg, defaults):
|
|
112
|
-
if cfg.schedule_control and cfg.restart_soon_id == 0:
|
|
113
|
-
defaults.append('restart_soon_id')
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# -----------------------------
|
|
117
|
-
# Main validator
|
|
118
|
-
# -----------------------------
|
|
119
|
-
|
|
120
|
-
def validate_all():
|
|
121
|
-
errors = []
|
|
122
|
-
defaults = []
|
|
123
|
-
|
|
124
|
-
# Load configs
|
|
125
|
-
global_cfg = loadConfig("config/global.yaml", "/defaultconfigs/global.yaml", GlobalConfig)
|
|
126
|
-
messages_cfg = loadConfig("config/messages.yaml", "/defaultconfigs/messages.yaml", MessagesConfig)
|
|
127
|
-
watcher_cfg = loadConfig("config/watcher.yaml", "/defaultconfigs/watcher.yaml", WatcherConfig)
|
|
128
|
-
|
|
129
|
-
# Generic dataclass validation
|
|
130
|
-
validate_dataclass(global_cfg, GlobalConfig, errors)
|
|
131
|
-
validate_dataclass(messages_cfg, MessagesConfig, errors)
|
|
132
|
-
validate_dataclass(watcher_cfg, WatcherConfig, errors)
|
|
133
|
-
|
|
134
|
-
# Config-specific validation
|
|
135
|
-
validate_global_config(global_cfg, errors)
|
|
136
|
-
validate_messages_config(messages_cfg, errors)
|
|
137
|
-
validate_watcher_config(watcher_cfg, errors)
|
|
138
|
-
|
|
139
|
-
# Check for defaults
|
|
140
|
-
ensure_no_global_defaults(global_cfg, defaults)
|
|
141
|
-
ensure_no_watcher_defaults(watcher_cfg, defaults)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# Print results
|
|
145
|
-
if len(defaults) >= 7:
|
|
146
|
-
print("❌ CONFIG VALIDATION FAILED:\nIt looks like you haven't configured this yet! Please change these defaults:")
|
|
147
|
-
for d in defaults:
|
|
148
|
-
print(" -", d)
|
|
149
|
-
sys.exit(1)
|
|
150
|
-
|
|
151
|
-
if errors or defaults:
|
|
152
|
-
print("❌ CONFIG VALIDATION FAILED:")
|
|
153
|
-
for e in errors:
|
|
154
|
-
print(" -", e)
|
|
155
|
-
for d in defaults:
|
|
156
|
-
print(" -", d, ": must not be left default")
|
|
157
|
-
sys.exit(1)
|
|
158
|
-
|
|
159
|
-
print("✅ All configs are valid.")
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if __name__ == "__main__":
|
|
163
|
-
validate_all()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|