serverwatcher 5.0__tar.gz → 5.1.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.
Files changed (27) hide show
  1. {serverwatcher-5.0/src/serverwatcher.egg-info → serverwatcher-5.1.1}/PKG-INFO +1 -1
  2. {serverwatcher-5.0 → serverwatcher-5.1.1}/pyproject.toml +1 -1
  3. {serverwatcher-5.0 → serverwatcher-5.1.1}/src/serverwatcher/configclasses/__init__.py +1 -1
  4. serverwatcher-5.1.1/src/serverwatcher/configclasses/config.py +31 -0
  5. serverwatcher-5.1.1/src/serverwatcher/configclasses/messages.py +62 -0
  6. serverwatcher-5.1.1/src/serverwatcher/configclasses/watcher.py +30 -0
  7. serverwatcher-5.1.1/src/serverwatcher/defaultconfigs/config.yaml +28 -0
  8. serverwatcher-5.1.1/src/serverwatcher/defaultconfigs/messages.yaml +58 -0
  9. serverwatcher-5.1.1/src/serverwatcher/defaultconfigs/watcher.yaml +27 -0
  10. serverwatcher-5.1.1/src/serverwatcher/validator.py +144 -0
  11. {serverwatcher-5.0 → serverwatcher-5.1.1}/src/serverwatcher/watcher.py +56 -56
  12. {serverwatcher-5.0 → serverwatcher-5.1.1/src/serverwatcher.egg-info}/PKG-INFO +1 -1
  13. {serverwatcher-5.0 → serverwatcher-5.1.1}/src/serverwatcher.egg-info/SOURCES.txt +2 -2
  14. serverwatcher-5.0/src/serverwatcher/configclasses/global_config.py +0 -30
  15. serverwatcher-5.0/src/serverwatcher/configclasses/messages.py +0 -65
  16. serverwatcher-5.0/src/serverwatcher/configclasses/watcher.py +0 -30
  17. serverwatcher-5.0/src/serverwatcher/defaultconfigs/global.yaml +0 -28
  18. serverwatcher-5.0/src/serverwatcher/defaultconfigs/messages.yaml +0 -58
  19. serverwatcher-5.0/src/serverwatcher/defaultconfigs/watcher.yaml +0 -27
  20. serverwatcher-5.0/src/serverwatcher/validator.py +0 -163
  21. {serverwatcher-5.0 → serverwatcher-5.1.1}/LICENSE +0 -0
  22. {serverwatcher-5.0 → serverwatcher-5.1.1}/README.md +0 -0
  23. {serverwatcher-5.0 → serverwatcher-5.1.1}/setup.cfg +0 -0
  24. {serverwatcher-5.0 → serverwatcher-5.1.1}/src/serverwatcher/__init__.py +0 -0
  25. {serverwatcher-5.0 → serverwatcher-5.1.1}/src/serverwatcher.egg-info/dependency_links.txt +0 -0
  26. {serverwatcher-5.0 → serverwatcher-5.1.1}/src/serverwatcher.egg-info/requires.txt +0 -0
  27. {serverwatcher-5.0 → serverwatcher-5.1.1}/src/serverwatcher.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 5.0
3
+ Version: 5.1.1
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: GPL-3.0
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "serverwatcher"
10
- version = "5.0"
10
+ version = "5.1.1"
11
11
  description = "A HungerLib-powered Minecraft server automation engine."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -1,4 +1,4 @@
1
- from .global_config import GlobalConfig
1
+ from .config import GlobalConfig
2
2
  from .messages import MessagesConfig
3
3
  from .watcher import WatcherConfig
4
4
 
@@ -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
+ reason_restart_soon: str = yaml_key("reasons.restart_soon")
48
+ reason_ram: str = yaml_key("reasons.ram")
49
+ reason_cpu: str = yaml_key("reasons.cpu")
50
+ reason_uptime: str = yaml_key("reasons.uptime")
51
+ reason_tps: str = yaml_key("reasons.tps")
52
+ reason_low_uptime: str = yaml_key("reasons.low_uptime")
53
+ reason_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
+ threshold_ram: int = yaml_key("thresholds.ram")
12
+ threshold_cpu: int = yaml_key("thresholds.cpu")
13
+ threshold_uptime_hours: int = yaml_key("thresholds.uptime")
14
+ threshold_tps: float = yaml_key("thresholds.tps")
15
+
16
+ weight_restart_soon: int = yaml_key("weights.restart_soon")
17
+ weight_ram: int = yaml_key("weights.ram")
18
+ weight_cpu: int = yaml_key("weights.cpu")
19
+ weight_uptime: int = yaml_key("weights.uptime")
20
+ weight_tps: int = yaml_key("weights.tps")
21
+
22
+ weight_low_uptime: int = yaml_key("weights.low_uptime")
23
+ weight_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.threshold_cpu <= 0:
67
+ errors.append(f"threshold_cpu: must not be less than 1 (got {watcherconfig.threshold_cpu})")
68
+
69
+ if watcherconfig.threshold_ram <= 0:
70
+ errors.append(f"threshold_ram: must be > 0 (got {watcherconfig.threshold_ram})")
71
+
72
+ if watcherconfig.threshold_tps <= 0 or watcherconfig.threshold_tps > 20:
73
+ errors.append(f"threshold_tps: must be 1–20 (got {watcherconfig.threshold_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.global_config import GlobalConfig
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.global_cfg = loadConfig(
29
- "config/global.yaml",
30
- "/defaultconfigs/global.yaml",
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.cfg = loadConfig(
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.global_cfg.panel_name,
48
- url=self.global_cfg.panel_url,
49
- api_key=self.global_cfg.panel_api_key,
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.global_cfg.origin_server_id
55
+ server_id=self.config.origin_server_id
56
56
  )
57
57
 
58
58
  self.server = MinecraftServer(
59
- name=self.global_cfg.server_name,
59
+ name=self.config.server_name,
60
60
  panel=self.panel,
61
- server_id=self.global_cfg.server_id,
62
- server_domain=self.global_cfg.server_domain,
63
- server_port=self.global_cfg.server_port,
64
- rcon_port=self.global_cfg.rcon_port,
65
- rcon_password=self.global_cfg.rcon_password,
66
- tpsCommand=self.global_cfg.tps_command,
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.global_cfg.logger_name.format(
70
- server_name=self.global_cfg.server_name
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.global_cfg.log_path,
77
- console_backspaces=self.global_cfg.console_backspaces,
76
+ log_path=self.config.log_path,
77
+ console_backspaces=self.config.console_backspaces,
78
78
  )
79
79
 
80
- self.tz = ZoneInfo(self.global_cfg.timezone)
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.log_shutdown)
92
+ self.say(self.messages.shutdown)
93
93
  raise SystemExit
94
94
 
95
95
  def restart_and_wait(self):
96
- if self.cfg.schedule_control:
97
- self.origin.disableSchedule(self.cfg.restart_soon_id)
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.cfg.restart_wait_seconds)
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.cfg.restart_online_timeout,
106
- interval=self.cfg.restart_online_interval,
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.log_start)
149
+ self.say(self.messages.startup)
150
150
 
151
151
  if not validateAll(self.panel, self.server):
152
- self.say(self.messages.log_validation_fail, level="error")
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.cfg.schedule_control and self.server.getSchedule(self.cfg.restart_soon_id)["is_active"]:
163
+ if self.watcherconfig.schedule_control and self.server.getSchedule(self.watcherconfig.restart_soon_id)["is_active"]:
164
164
  restart_reasons.append(self.messages.reason_restart_soon)
165
- pro += self.cfg.weight_restart_soon
165
+ pro += self.watcherconfig.weight_restart_soon
166
166
 
167
- if snap.ram >= self.cfg.ram_threshold:
168
- restart_reasons.append(self.fmt(self.messages.reason_ram, ram=snap.ram, threshold=self.cfg.ram_threshold))
169
- pro += int(round(snap.ram, 0) - (self.cfg.ram_threshold - 1))
167
+ if snap.ram >= self.watcherconfig.threshold_ram:
168
+ restart_reasons.append(self.fmt(self.messages.reason_ram, ram=snap.ram, threshold=self.watcherconfig.threshold_ram))
169
+ pro += int(round(snap.ram, 0) - (self.watcherconfig.threshold_ram - 1))
170
170
 
171
- if snap.cpu >= self.cfg.cpu_threshold:
172
- restart_reasons.append(self.fmt(self.messages.reason_cpu, cpu=snap.cpu, threshold=self.cfg.cpu_threshold))
173
- pro += self.cfg.weight_cpu
171
+ if snap.cpu >= self.watcherconfig.threshold_cpu:
172
+ restart_reasons.append(self.fmt(self.messages.reason_cpu, cpu=snap.cpu, threshold=self.watcherconfig.threshold_cpu))
173
+ pro += self.watcherconfig.weight_cpu
174
174
 
175
- if snap.uptime // 3600 >= self.cfg.uptime_hours_threshold:
175
+ if snap.uptime // 3600 >= self.watcherconfig.threshold_uptime:
176
176
  restart_reasons.append(
177
177
  self.fmt(self.messages.reason_uptime, uptime=snap.uptime_formatted,
178
- threshold=self.cfg.uptime_hours_threshold)
178
+ threshold=self.watcherconfig.threshold_uptime)
179
179
  )
180
- pro += self.cfg.weight_uptime
180
+ pro += self.watcherconfig.weight_uptime
181
181
 
182
- if (snap.tps if snap.tps is not None else 0) <= self.cfg.tps_threshold:
183
- restart_reasons.append(self.fmt(self.messages.reason_tps, tps=snap.tps, threshold=self.cfg.tps_threshold))
184
- pro += self.cfg.weight_tps
182
+ if (snap.tps if snap.tps is not None else 0) <= self.watcherconfig.threshold_tps:
183
+ restart_reasons.append(self.fmt(self.messages.reason_tps, tps=snap.tps, threshold=self.watcherconfig.threshold_tps))
184
+ pro += self.watcherconfig.weight_tps
185
185
 
186
186
  if snap.uptime // 60 < 30:
187
187
  no_restart_reasons.append(self.fmt(self.messages.reason_low_uptime, uptime=snap.uptime_formatted))
188
- anti += self.cfg.weight_low_uptime
188
+ anti += self.watcherconfig.weight_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
193
  no_restart_reasons.append(self.fmt(self.messages.reason_players, verb=verb, count=snap.players, plural=plural))
194
- anti += snap.players * self.cfg.weight_per_player
194
+ anti += snap.players * self.watcherconfig.weight_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.log_no_restart)
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.log_immediate_restart)
218
+ self.say(self.messages.immediate_restart)
219
219
  self.restart_and_wait()
220
220
  return
221
221
 
222
- self.say(self.messages.log_scheduled)
222
+ self.say(self.messages.scheduled)
223
223
 
224
224
  if gap <= 2:
225
- self.say(self.messages.log_gap_low, level="warn", gap=gap)
226
- self.schedule_restart(self.cfg.low_gap_minutes)
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.log_gap_high, level="warn", gap=gap)
229
- self.schedule_restart(self.cfg.high_gap_minutes)
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.global_cfg.clear_terminal:
234
+ if self.config.clear_terminal:
235
235
  clearTerminal()
236
236
  while True:
237
- if self.global_cfg.clear_terminal:
237
+ if self.config.clear_terminal:
238
238
  clearTerminal()
239
239
  self.evaluate()
240
- time.sleep(self.global_cfg.watch_interval)
240
+ time.sleep(self.config.watch_interval)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 5.0
3
+ Version: 5.1.1
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: GPL-3.0
@@ -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/global_config.py
13
+ src/serverwatcher/configclasses/config.py
14
14
  src/serverwatcher/configclasses/messages.py
15
15
  src/serverwatcher/configclasses/watcher.py
16
- src/serverwatcher/defaultconfigs/global.yaml
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