serverwatcher 2.2.4__tar.gz → 3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 2.2.4
3
+ Version: 3.1
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: MIT
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "serverwatcher"
10
- version = "2.2.4"
10
+ version = "3.1"
11
11
  description = "A HungerLib-powered Minecraft server automation engine."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -9,6 +9,7 @@ except PackageNotFoundError:
9
9
  from .watcher import ServerWatcher
10
10
  from .config import WatcherConfig
11
11
  from .messages import WatcherMessages
12
+ from .schema import validate_config_schema, validate_messages_schema
12
13
 
13
14
 
14
15
  __all__ = [
@@ -16,4 +17,6 @@ __all__ = [
16
17
  'ServerWatcher',
17
18
  'WatcherConfig',
18
19
  'WatcherMessages',
20
+ 'validate_config_schema',
21
+ 'validate_messages_schema',
19
22
  ]
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class GlobalConfig:
5
+ panel: dict
6
+ origin: dict
7
+ server: dict
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ @dataclass
4
+ class MessagesConfig:
5
+ prefix: str
6
+ broadcast_restart_at: str
7
+ broadcast_minute: dict = field(default_factory=dict)
8
+ broadcast_second: dict = field(default_factory=dict)
9
+
10
+ log_start: str = ""
11
+ log_validation_fail: str = ""
12
+ log_validation_ok: str = ""
13
+ log_immediate_restart: str = ""
14
+ log_no_restart: str = ""
15
+ log_scheduled: str = ""
16
+ log_gap_low: str = ""
17
+ log_gap_high: str = ""
18
+
19
+ reason_restart_soon: str = ""
20
+ reason_ram: str = ""
21
+ reason_cpu: str = ""
22
+ reason_uptime: str = ""
23
+ reason_tps: str = ""
24
+ reason_low_uptime: str = ""
25
+ reason_players: str = ""
@@ -0,0 +1,32 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class WatcherConfig:
5
+ restart_soon_schedule_id: int
6
+ origin_disable_schedule_id: int
7
+
8
+ ram_threshold: int
9
+ cpu_threshold: int
10
+ uptime_hours_threshold: int
11
+ tps_threshold: float
12
+
13
+ weight_restart_soon: int
14
+ weight_ram: int
15
+ weight_cpu: int
16
+ weight_uptime: int
17
+ weight_tps: int
18
+
19
+ weight_low_uptime: int
20
+ weight_per_player: int
21
+
22
+ low_gap_minutes: int
23
+ high_gap_minutes: int
24
+
25
+ restart_wait_seconds: int
26
+ restart_online_timeout: int
27
+ restart_online_interval: int
28
+
29
+ logger_name_template: str
30
+ log_path: str
31
+ console_backspaces: int
32
+ timezone: str
@@ -0,0 +1,45 @@
1
+ from typing import Dict, Any, List
2
+
3
+ from hungerlib.addons import validate_required_keys
4
+
5
+
6
+ def validate_config_schema(raw: Dict[str, Any]) -> list[str]:
7
+ errors: List[str] = []
8
+
9
+ # top-level sections
10
+ errors += validate_required_keys(raw, ["panel", "origin", "server", "watcher"], "root")
11
+
12
+ panel = raw.get("panel", {})
13
+ origin = raw.get("origin", {})
14
+ server = raw.get("server", {})
15
+ watcher = raw.get("watcher", {})
16
+
17
+ errors += validate_required_keys(panel, ["name", "url", "api_key"], "panel")
18
+ errors += validate_required_keys(origin, ["server_id"], "origin")
19
+ errors += validate_required_keys(
20
+ server,
21
+ ["name", "server_id", "domain", "port", "rcon_port", "rcon_password", "tps_command"],
22
+ "server",
23
+ )
24
+
25
+ # simple sanity checks (optional but nice)
26
+ if "port" in server and not isinstance(server["port"], int):
27
+ errors.append("[server] 'port' must be an integer")
28
+ if "rcon_port" in server and not isinstance(server["rcon_port"], int):
29
+ errors.append("[server] 'rcon_port' must be an integer")
30
+
31
+ # watcher numeric sanity
32
+ if "ram_threshold" in watcher and watcher["ram_threshold"] <= 0:
33
+ errors.append("[watcher] 'ram_threshold' must be > 0")
34
+ if "cpu_threshold" in watcher and watcher["cpu_threshold"] <= 0:
35
+ errors.append("[watcher] 'cpu_threshold' must be > 0")
36
+
37
+ return errors
38
+
39
+
40
+ def validate_messages_schema(raw: Dict[str, Any]) -> list[str]:
41
+ # you can make this as strict as you want; for now just ensure prefix exists
42
+ errors: List[str] = []
43
+ if "prefix" not in raw:
44
+ errors.append("[messages] Missing required key: 'prefix'")
45
+ return errors
@@ -0,0 +1,280 @@
1
+ import os
2
+ import time
3
+ from zoneinfo import ZoneInfo
4
+
5
+ from hungerlib import Panel, HungerLogger
6
+ from hungerlib.servers import MinecraftServer, GenericServer
7
+ from hungerlib.addons import (
8
+ clearTerminal,
9
+ Snapshot,
10
+ snapSchedule,
11
+ waitForOnline,
12
+ validateAll,
13
+ runCountdownEvents,
14
+ load_yaml,
15
+ map_to_dataclass,
16
+ )
17
+
18
+ # NEW CONFIG DATACLASSES
19
+ from config.global import GlobalConfig
20
+ from config.messages import MessagesConfig
21
+ from config.watcher import WatcherConfig
22
+
23
+
24
+ # ---------------------------------------------------------
25
+ # Utility: load config or copy default from defaultconfigs/
26
+ # ---------------------------------------------------------
27
+ def load_or_default(path: str, default_path: str, schema):
28
+ """
29
+ Loads YAML from path. If missing, copies default_path → path.
30
+ Then maps YAML → dataclass.
31
+ """
32
+ if not os.path.exists(path):
33
+ os.makedirs(os.path.dirname(path), exist_ok=True)
34
+ with open(default_path, "r") as src, open(path, "w") as dst:
35
+ dst.write(src.read())
36
+
37
+ raw = load_yaml(path)
38
+ return map_to_dataclass(raw, schema)
39
+
40
+
41
+ # ---------------------------------------------------------
42
+ # Main Watcher
43
+ # ---------------------------------------------------------
44
+ class ServerWatcher:
45
+ def __init__(self):
46
+ # Load all 3 configs
47
+ self.global_cfg: GlobalConfig = load_or_default(
48
+ "config/global.yaml",
49
+ "defaultconfigs/global.yaml",
50
+ GlobalConfig
51
+ )
52
+
53
+ self.messages: MessagesConfig = load_or_default(
54
+ "config/messages.yaml",
55
+ "defaultconfigs/messages.yaml",
56
+ MessagesConfig
57
+ )
58
+
59
+ self.cfg: WatcherConfig = load_or_default(
60
+ "config/watcher.yaml",
61
+ "defaultconfigs/watcher.yaml",
62
+ WatcherConfig
63
+ )
64
+
65
+ # PANEL
66
+ p = self.global_cfg.panel
67
+ self.panel = Panel(
68
+ name=p["name"],
69
+ url=p["url"],
70
+ api_key=p["api_key"],
71
+ )
72
+
73
+ # ORIGIN
74
+ o = self.global_cfg.origin
75
+ self.origin = GenericServer(
76
+ name="Origin",
77
+ panel=self.panel,
78
+ server_id=o["server_id"],
79
+ )
80
+
81
+ # SERVER
82
+ s = self.global_cfg.server
83
+ self.server = MinecraftServer(
84
+ name=s["name"],
85
+ panel=self.panel,
86
+ server_id=s["server_id"],
87
+ server_domain=s["domain"],
88
+ server_port=s["port"],
89
+ rcon_port=s["rcon_port"],
90
+ rcon_password=s["rcon_password"],
91
+ tpsCommand=s["tps_command"],
92
+ )
93
+
94
+ # LOGGER (fully configurable)
95
+ logger_name = self.cfg.logger_name_template.format(
96
+ server_name=s["name"]
97
+ )
98
+
99
+ self.log = HungerLogger(
100
+ name=logger_name,
101
+ server=self.server,
102
+ log_path=self.cfg.log_path,
103
+ console_backspaces=self.cfg.console_backspaces,
104
+ )
105
+
106
+ # TIMEZONE
107
+ self.tz = ZoneInfo(self.cfg.timezone)
108
+
109
+ # -----------------------------------------------------
110
+ # Utility: format messages with prefix
111
+ # -----------------------------------------------------
112
+ def fmt(self, template: str, **kwargs):
113
+ return template.format(prefix=self.messages.prefix, **kwargs)
114
+
115
+ # -----------------------------------------------------
116
+ # Shutdown
117
+ # -----------------------------------------------------
118
+ def shutdown(self):
119
+ self.log.info("Shutting down ServerWatcher.")
120
+ raise SystemExit
121
+
122
+ # -----------------------------------------------------
123
+ # Restart logic
124
+ # -----------------------------------------------------
125
+ def restart_and_wait(self):
126
+ self.origin.disableSchedule(self.cfg.restart_soon_schedule_id)
127
+ self.server.restart()
128
+ self.log.info("Restart action sent. Waiting...")
129
+ time.sleep(self.cfg.restart_wait_seconds)
130
+
131
+ self.log.warn("Checking server status...")
132
+ alive = waitForOnline(
133
+ self.server,
134
+ timeout=self.cfg.restart_online_timeout,
135
+ interval=self.cfg.restart_online_interval,
136
+ )
137
+
138
+ if alive:
139
+ self.log.info("Server is back online!")
140
+ self.server.sendBroadcast(
141
+ f"{self.messages.prefix}<green>Restart successful!"
142
+ )
143
+ self.origin.enableSchedule(self.cfg.origin_disable_schedule_id)
144
+ else:
145
+ self.log.error("Server failed to restart!")
146
+
147
+ # -----------------------------------------------------
148
+ # Schedule restart
149
+ # -----------------------------------------------------
150
+ def schedule_restart(self, minutes):
151
+ info = snapSchedule(minimumMinutes=minutes)
152
+ scheduled = info["scheduled"]
153
+
154
+ local_time = scheduled.astimezone(self.tz)
155
+ time_str = local_time.strftime("%I:%M %p")
156
+
157
+ self.server.sendBroadcast(
158
+ self.fmt(self.messages.broadcast_restart_at, time=time_str)
159
+ )
160
+
161
+ minute_callbacks = {
162
+ m: (lambda msg=self.fmt(self.messages.broadcast_minute[m]):
163
+ self.server.sendBroadcast(msg))
164
+ for m in self.messages.broadcast_minute
165
+ }
166
+
167
+ second_callbacks = {
168
+ s: (lambda msg=self.fmt(self.messages.broadcast_second[s]):
169
+ self.server.sendBroadcast(msg))
170
+ for s in self.messages.broadcast_second
171
+ }
172
+
173
+ runCountdownEvents(
174
+ target_time=scheduled,
175
+ minute_callbacks=minute_callbacks,
176
+ second_callbacks=second_callbacks,
177
+ )
178
+
179
+ # -----------------------------------------------------
180
+ # Main evaluation logic
181
+ # -----------------------------------------------------
182
+ def evaluate(self):
183
+ self.log.info(self.messages.log_start)
184
+
185
+ if not validateAll(self.panel, self.server):
186
+ self.log.error(self.messages.log_validation_fail)
187
+ self.shutdown()
188
+
189
+ self.server.refresh()
190
+ snap = Snapshot(self.server, 2, True)
191
+
192
+ pro = 0
193
+ anti = 0
194
+ restart_reasons = []
195
+ no_restart_reasons = []
196
+
197
+ # PRO-RESTART
198
+ if self.server.getSchedule(self.cfg.restart_soon_schedule_id)["is_active"]:
199
+ restart_reasons.append(self.messages.reason_restart_soon)
200
+ pro += self.cfg.weight_restart_soon
201
+
202
+ if snap.ram >= self.cfg.ram_threshold:
203
+ restart_reasons.append(
204
+ self.fmt(self.messages.reason_ram, ram=snap.ram, threshold=self.cfg.ram_threshold)
205
+ )
206
+ pro += round(snap.ram, 0) - 5
207
+
208
+ if snap.cpu >= self.cfg.cpu_threshold:
209
+ restart_reasons.append(
210
+ self.fmt(self.messages.reason_cpu, cpu=snap.cpu, threshold=self.cfg.cpu_threshold)
211
+ )
212
+ pro += self.cfg.weight_cpu
213
+
214
+ if snap.uptime // 3600 >= self.cfg.uptime_hours_threshold:
215
+ restart_reasons.append(
216
+ self.fmt(self.messages.reason_uptime, uptime=snap.uptime_formatted,
217
+ threshold=self.cfg.uptime_hours_threshold)
218
+ )
219
+ pro += self.cfg.weight_uptime
220
+
221
+ if snap.tps <= self.cfg.tps_threshold:
222
+ restart_reasons.append(
223
+ self.fmt(self.messages.reason_tps, tps=snap.tps, threshold=self.cfg.tps_threshold)
224
+ )
225
+ pro += self.cfg.weight_tps
226
+
227
+ # ANTI-RESTART
228
+ if snap.uptime // 60 < 30:
229
+ no_restart_reasons.append(
230
+ self.fmt(self.messages.reason_low_uptime, uptime=snap.uptime_formatted)
231
+ )
232
+ anti += self.cfg.weight_low_uptime
233
+
234
+ if snap.players > 0:
235
+ verb = "are" if snap.players != 1 else "is"
236
+ plural = "players" if snap.players != 1 else "player"
237
+ no_restart_reasons.append(
238
+ self.fmt(self.messages.reason_players, verb=verb, count=snap.players, plural=plural)
239
+ )
240
+ anti += snap.players * self.cfg.weight_per_player
241
+
242
+ # LOGGING
243
+ for r in restart_reasons:
244
+ self.log.warn(f"- {r}")
245
+ for r in no_restart_reasons:
246
+ self.log.warn(f"- {r}")
247
+
248
+ self.log.warn(f"Pro-restart: {pro}")
249
+ self.log.warn(f"Anti-restart: {anti}")
250
+
251
+ gap = abs(pro - anti)
252
+
253
+ if pro == 0:
254
+ self.log.info(self.messages.log_no_restart)
255
+ return
256
+
257
+ if pro > anti and snap.players == 0:
258
+ self.log.info(self.messages.log_immediate_restart)
259
+ self.restart_and_wait()
260
+ return
261
+
262
+ self.log.info(self.messages.log_scheduled)
263
+
264
+ if gap <= 2:
265
+ self.log.warn(self.fmt(self.messages.log_gap_low, gap=gap))
266
+ self.schedule_restart(self.cfg.low_gap_minutes)
267
+ else:
268
+ self.log.warn(self.fmt(self.messages.log_gap_high, gap=gap))
269
+ self.schedule_restart(self.cfg.high_gap_minutes)
270
+
271
+ self.restart_and_wait()
272
+
273
+ # -----------------------------------------------------
274
+ # Main loop
275
+ # -----------------------------------------------------
276
+ def run(self):
277
+ clearTerminal()
278
+ while True:
279
+ self.evaluate()
280
+ time.sleep(60)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 2.2.4
3
+ Version: 3.1
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: MIT
@@ -2,11 +2,13 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/serverwatcher/__init__.py
5
- src/serverwatcher/config.py
6
- src/serverwatcher/messages.py
5
+ src/serverwatcher/schema.py
7
6
  src/serverwatcher/watcher.py
8
7
  src/serverwatcher.egg-info/PKG-INFO
9
8
  src/serverwatcher.egg-info/SOURCES.txt
10
9
  src/serverwatcher.egg-info/dependency_links.txt
11
10
  src/serverwatcher.egg-info/requires.txt
12
- src/serverwatcher.egg-info/top_level.txt
11
+ src/serverwatcher.egg-info/top_level.txt
12
+ src/serverwatcher/config/global.py
13
+ src/serverwatcher/config/messages.py
14
+ src/serverwatcher/config/watcher.py
@@ -1,33 +0,0 @@
1
- from dataclasses import dataclass
2
-
3
- @dataclass
4
- class WatcherConfig:
5
- # schedule IDs
6
- restart_soon_schedule_id: int = 13
7
- origin_disable_schedule_id: int = 11
8
-
9
- # thresholds
10
- ram_threshold: int = 6
11
- cpu_threshold: int = 150
12
- uptime_hours_threshold: int = 12
13
- tps_threshold: float = 19.5
14
-
15
- # weights (pro)
16
- weight_restart_soon: int = 3
17
- weight_ram: int = 1
18
- weight_cpu: int = 1
19
- weight_uptime: int = 1
20
- weight_tps: int = 1
21
-
22
- # weights (anti)
23
- weight_low_uptime: int = 5
24
- weight_per_player: int = 1
25
-
26
- # scheduling
27
- low_gap_minutes: int = 120
28
- high_gap_minutes: int = 60
29
-
30
- # restart timing
31
- restart_wait_seconds: int = 45
32
- restart_online_timeout: int = 120
33
- restart_online_interval: int = 2
@@ -1,43 +0,0 @@
1
- from dataclasses import dataclass, field
2
-
3
- @dataclass
4
- class WatcherMessages:
5
- prefix: str = "<aqua>[Server Watcher]"
6
-
7
- # broadcast templates
8
- broadcast_restart_at: str = "{prefix} The server will restart at {time} CDT."
9
-
10
- broadcast_minute: dict = field(default_factory=lambda: {
11
- 120: "{prefix} Restart in 2 hours!",
12
- 60: "{prefix} Restart in 1 hour!",
13
- 45: "{prefix} Restart in 45 minutes!",
14
- 30: "{prefix} Restart in 30 minutes!",
15
- 15: "{prefix} Restart in 15 minutes!",
16
- 5: "{prefix} Restart in 5 minutes!",
17
- 1: "{prefix} Restart in 1 minute!",
18
- })
19
-
20
- broadcast_second: dict = field(default_factory=lambda: {
21
- s: "{prefix} Restart in " + str(s) + " seconds!"
22
- for s in range(10, 0, -1)
23
- })
24
-
25
- # log messages
26
- log_start: str = "ServerWatcher is running!"
27
- log_validation_fail: str = "Validation FAILED"
28
- log_validation_ok: str = "All validation checks succeeded."
29
- log_immediate_restart: str = "Restarting immediately."
30
- log_no_restart: str = "The server does not need to restart."
31
- log_scheduled: str = "Restart needed, but anti-restart factors outweigh it."
32
- log_gap_low: str = "Gap {gap}. Scheduling restart in 2 hours."
33
- log_gap_high: str = "Gap {gap}. Scheduling restart in 1 hour."
34
-
35
- # reason messages
36
- reason_restart_soon: str = "The server is set to restart soon"
37
- reason_ram: str = "RAM usage ({ram}) is higher than {threshold} GB"
38
- reason_cpu: str = "CPU usage ({cpu}) is higher than {threshold}%"
39
- reason_uptime: str = "Uptime {uptime} exceeds {threshold}h"
40
- reason_tps: str = "TPS {tps} is lower than {threshold}"
41
-
42
- reason_low_uptime: str = "Uptime {uptime} is shorter than 30m"
43
- reason_players: str = "There {verb} {count} {plural} online"
@@ -1,263 +0,0 @@
1
- import time
2
- from zoneinfo import ZoneInfo
3
-
4
- from hungerlib import Panel, HungerLogger
5
- from hungerlib.servers import MinecraftServer, GenericServer
6
- from hungerlib.addons import (
7
- clearTerminal,
8
- Snapshot,
9
- snapSchedule,
10
- waitForOnline,
11
- validateAll,
12
- runCountdownEvents,
13
- ensure_yaml,
14
- load_yaml,
15
- map_to_dataclass
16
- )
17
-
18
- from .config import WatcherConfig
19
- from .messages import WatcherMessages
20
-
21
-
22
- DEFAULT_CONFIG = {
23
- "panel": {
24
- "name": "My Panel",
25
- "url": "https://example.com",
26
- "api_key": "CHANGE_ME",
27
- },
28
- "origin": {
29
- "server_id": "CHANGE_ME",
30
- },
31
- "server": {
32
- "name": "My SMP",
33
- "server_id": "CHANGE_ME",
34
- "domain": "example.com",
35
- "port": 25565,
36
- "rcon_port": 25575,
37
- "rcon_password": "password",
38
- "tps_command": "ticks",
39
- },
40
- "watcher": {},
41
- "messages": {},
42
- }
43
-
44
-
45
- class ServerWatcher:
46
- def __init__(self, config_path: str = "config.yaml"):
47
- # ensure config exists, then load it
48
- ensure_yaml(config_path, DEFAULT_CONFIG)
49
- raw = load_yaml(config_path)
50
-
51
- # map watcher + messages sections into your dataclasses
52
- self.cfg: WatcherConfig = map_to_dataclass(raw.get("watcher", {}), WatcherConfig)
53
- self.msg: WatcherMessages = map_to_dataclass(
54
- raw.get("messages", {}), WatcherMessages
55
- )
56
-
57
- # panel / origin / server wiring from YAML
58
- p = raw["panel"]
59
- self.panel = Panel(
60
- name=p["name"],
61
- url=p["url"],
62
- api_key=p["api_key"],
63
- )
64
-
65
- o = raw["origin"]
66
- self.origin = GenericServer(
67
- name="Origin",
68
- panel=self.panel,
69
- server_id=o["server_id"],
70
- )
71
-
72
- s = raw["server"]
73
- self.server = MinecraftServer(
74
- name=s["name"],
75
- panel=self.panel,
76
- server_id=s["server_id"],
77
- server_domain=s["domain"],
78
- server_port=s["port"],
79
- rcon_port=s["rcon_port"],
80
- rcon_password=s["rcon_password"],
81
- tpsCommand=s["tps_command"],
82
- )
83
-
84
- self.log = HungerLogger(
85
- name=f"ServerWatcher-{s['name']}",
86
- server=self.server,
87
- log_path="/home/container/logs/",
88
- console_backspaces=8,
89
- )
90
-
91
- def fmt(self, template: str, **kwargs):
92
- return template.format(prefix=self.msg.prefix, **kwargs)
93
-
94
- def shutdown(self):
95
- self.log.info("Shutting down ServerWatcher.")
96
- raise SystemExit
97
-
98
- def restart_and_wait(self):
99
- self.origin.disableSchedule(self.cfg.origin_disable_schedule_id)
100
- self.server.restart()
101
- self.log.info("Restart action sent. Waiting...")
102
- time.sleep(self.cfg.restart_wait_seconds)
103
-
104
- self.log.warn("Checking server status...")
105
- alive = waitForOnline(
106
- self.server,
107
- timeout=self.cfg.restart_online_timeout,
108
- interval=self.cfg.restart_online_interval,
109
- )
110
-
111
- if alive:
112
- self.log.info("Server is back online!")
113
- self.server.sendBroadcast(f"{self.msg.prefix}<green>Restart successful!")
114
- self.origin.enableSchedule(self.cfg.origin_disable_schedule_id)
115
- else:
116
- self.log.error("Server failed to restart!")
117
-
118
- def schedule_restart(self, minutes):
119
- info = snapSchedule(minimumMinutes=minutes)
120
- scheduled = info["scheduled"]
121
-
122
- cst = scheduled.astimezone(ZoneInfo("America/Chicago"))
123
- time_in_cdt = cst.strftime("%I:%M %p")
124
-
125
- self.server.sendBroadcast(
126
- self.fmt(self.msg.broadcast_restart_at, time=time_in_cdt)
127
- )
128
-
129
- minute_callbacks = {
130
- m: (lambda msg=self.fmt(self.msg.broadcast_minute[m]): self.server.sendBroadcast(msg))
131
- for m in self.msg.broadcast_minute
132
- }
133
-
134
- second_callbacks = {
135
- s: (lambda msg=self.fmt(self.msg.broadcast_second[s]): self.server.sendBroadcast(msg))
136
- for s in self.msg.broadcast_second
137
- }
138
-
139
- runCountdownEvents(
140
- target_time=scheduled,
141
- minute_callbacks=minute_callbacks,
142
- second_callbacks=second_callbacks,
143
- )
144
-
145
- def evaluate(self):
146
- self.log.info(self.msg.log_start)
147
-
148
- if not validateAll(self.panel, self.server):
149
- self.log.error(self.msg.log_validation_fail)
150
- self.shutdown()
151
-
152
- self.server.refresh()
153
- snap = Snapshot(self.server, 2, True)
154
-
155
- pro = 0
156
- anti = 0
157
- restart_reasons = []
158
- no_restart_reasons = []
159
-
160
- # PRO-RESTART
161
- if self.server.getSchedule(self.cfg.restart_soon_schedule_id)["is_active"]:
162
- restart_reasons.append(self.msg.reason_restart_soon)
163
- pro += self.cfg.weight_restart_soon
164
-
165
- if snap.ram >= self.cfg.ram_threshold:
166
- restart_reasons.append(
167
- self.fmt(
168
- self.msg.reason_ram,
169
- ram=snap.ram,
170
- threshold=self.cfg.ram_threshold,
171
- )
172
- )
173
- pro += round(snap.ram, 0) - 5
174
-
175
- if snap.cpu >= self.cfg.cpu_threshold:
176
- restart_reasons.append(
177
- self.fmt(
178
- self.msg.reason_cpu,
179
- cpu=snap.cpu,
180
- threshold=self.cfg.cpu_threshold,
181
- )
182
- )
183
- pro += self.cfg.weight_cpu
184
-
185
- if snap.uptime // 3600 >= self.cfg.uptime_hours_threshold:
186
- restart_reasons.append(
187
- self.fmt(
188
- self.msg.reason_uptime,
189
- uptime=snap.uptime_formatted,
190
- threshold=self.cfg.uptime_hours_threshold,
191
- )
192
- )
193
- pro += self.cfg.weight_uptime
194
-
195
- if snap.tps <= self.cfg.tps_threshold:
196
- restart_reasons.append(
197
- self.fmt(
198
- self.msg.reason_tps,
199
- tps=snap.tps,
200
- threshold=self.cfg.tps_threshold,
201
- )
202
- )
203
- pro += self.cfg.weight_tps
204
-
205
- # ANTI-RESTART
206
- if snap.uptime // 60 < 30:
207
- no_restart_reasons.append(
208
- self.fmt(
209
- self.msg.reason_low_uptime,
210
- uptime=snap.uptime_formatted,
211
- )
212
- )
213
- anti += self.cfg.weight_low_uptime
214
-
215
- if snap.players > 0:
216
- verb = "are" if snap.players != 1 else "is"
217
- plural = "players" if snap.players != 1 else "player"
218
- no_restart_reasons.append(
219
- self.fmt(
220
- self.msg.reason_players,
221
- verb=verb,
222
- count=snap.players,
223
- plural=plural,
224
- )
225
- )
226
- anti += snap.players * self.cfg.weight_per_player
227
-
228
- # LOGGING
229
- for r in restart_reasons:
230
- self.log.warn(f"- {r}")
231
- for r in no_restart_reasons:
232
- self.log.warn(f"- {r}")
233
-
234
- self.log.warn(f"Pro-restart: {pro}")
235
- self.log.warn(f"Anti-restart: {anti}")
236
-
237
- gap = abs(pro - anti)
238
-
239
- if pro == 0:
240
- self.log.info(self.msg.log_no_restart)
241
- return
242
-
243
- if pro > anti and snap.players == 0:
244
- self.log.info(self.msg.log_immediate_restart)
245
- self.restart_and_wait()
246
- return
247
-
248
- self.log.info(self.msg.log_scheduled)
249
-
250
- if gap <= 2:
251
- self.log.warn(self.fmt(self.msg.log_gap_low, gap=gap))
252
- self.schedule_restart(self.cfg.low_gap_minutes)
253
- else:
254
- self.log.warn(self.fmt(self.msg.log_gap_high, gap=gap))
255
- self.schedule_restart(self.cfg.high_gap_minutes)
256
-
257
- self.restart_and_wait()
258
-
259
- def run(self):
260
- clearTerminal()
261
- while True:
262
- self.evaluate()
263
- time.sleep(60)
File without changes
File without changes
File without changes