serverwatcher 3.0__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: 3.0
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 = "3.0"
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"
@@ -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,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: 3.0
3
+ Version: 3.1
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: MIT
@@ -2,12 +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
7
5
  src/serverwatcher/schema.py
8
6
  src/serverwatcher/watcher.py
9
7
  src/serverwatcher.egg-info/PKG-INFO
10
8
  src/serverwatcher.egg-info/SOURCES.txt
11
9
  src/serverwatcher.egg-info/dependency_links.txt
12
10
  src/serverwatcher.egg-info/requires.txt
13
- 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,39 +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
34
-
35
- # logger / misc
36
- logger_name_template: str = "ServerWatcher-{server_name}"
37
- log_path: str = "/home/container/logs/"
38
- console_backspaces: int = 8
39
- timezone: str = "America/Chicago"
@@ -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,334 +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
- write_default_yaml,
17
- )
18
-
19
- from .config import WatcherConfig
20
- from .messages import WatcherMessages
21
- from .schema import validate_config_schema, validate_messages_schema
22
-
23
-
24
- CONFIG_PATH = "config.yaml"
25
- MESSAGES_PATH = "messages.yaml"
26
-
27
-
28
- DEFAULT_CONFIG = {
29
- "panel": {
30
- "name": "My Panel",
31
- "url": "https://example.com",
32
- "api_key": "CHANGE_ME",
33
- },
34
- "origin": {
35
- "server_id": "CHANGE_ME",
36
- },
37
- "server": {
38
- "name": "My SMP",
39
- "server_id": "CHANGE_ME",
40
- "domain": "example.com",
41
- "port": 25565,
42
- "rcon_port": 25575,
43
- "rcon_password": "password",
44
- "tps_command": "ticks",
45
- },
46
- "watcher": {},
47
- }
48
-
49
-
50
- DEFAULT_MESSAGES = {
51
- "prefix": "<aqua>[Server Watcher]",
52
- "broadcast_restart_at": "{prefix} The server will restart at {time} CDT.",
53
- "broadcast_minute": {
54
- 120: "{prefix} Restart in 2 hours!",
55
- 60: "{prefix} Restart in 1 hour!",
56
- 45: "{prefix} Restart in 45 minutes!",
57
- 30: "{prefix} Restart in 30 minutes!",
58
- 15: "{prefix} Restart in 15 minutes!",
59
- 5: "{prefix} Restart in 5 minutes!",
60
- 1: "{prefix} Restart in 1 minute!",
61
- },
62
- "broadcast_second": {
63
- s: "{prefix} Restart in " + str(s) + " seconds!"
64
- for s in range(10, 0, -1)
65
- },
66
- "log_start": "ServerWatcher is running!",
67
- "log_validation_fail": "Validation FAILED. Make sure you set up config.yaml.",
68
- "log_validation_ok": "All validation checks succeeded.",
69
- "log_immediate_restart": "Restarting immediately.",
70
- "log_no_restart": "The server does not need to restart.",
71
- "log_scheduled": "Restart needed, but anti-restart factors outweigh it.",
72
- "log_gap_low": "Gap {gap}. Scheduling restart in 2 hours.",
73
- "log_gap_high": "Gap {gap}. Scheduling restart in 1 hour.",
74
- "reason_restart_soon": "The server is set to restart soon",
75
- "reason_ram": "RAM usage ({ram}) is higher than {threshold} GB",
76
- "reason_cpu": "CPU usage ({cpu}) is higher than {threshold}%",
77
- "reason_uptime": "Uptime {uptime} exceeds {threshold}h",
78
- "reason_tps": "TPS {tps} is lower than {threshold}",
79
- "reason_low_uptime": "Uptime {uptime} is shorter than 30m",
80
- "reason_players": "There {verb} {count} {plural} online",
81
- }
82
-
83
-
84
- class ServerWatcher:
85
- @staticmethod
86
- def generate_default_files(
87
- config_path: str = CONFIG_PATH,
88
- messages_path: str = MESSAGES_PATH,
89
- overwrite: bool = False,
90
- ):
91
- write_default_yaml(config_path, DEFAULT_CONFIG, overwrite=overwrite)
92
- write_default_yaml(messages_path, DEFAULT_MESSAGES, overwrite=overwrite)
93
-
94
- def __init__(
95
- self,
96
- config_path: str = CONFIG_PATH,
97
- messages_path: str = MESSAGES_PATH,
98
- ):
99
- # ensure files exist
100
- ensure_yaml(config_path, DEFAULT_CONFIG)
101
- ensure_yaml(messages_path, DEFAULT_MESSAGES)
102
-
103
- # load raw YAML
104
- raw_config = load_yaml(config_path)
105
- raw_messages = load_yaml(messages_path)
106
-
107
- # validate schemas
108
- config_errors = validate_config_schema(raw_config)
109
- messages_errors = validate_messages_schema(raw_messages)
110
- all_errors = config_errors + messages_errors
111
- if all_errors:
112
- msg = "Configuration errors:\n" + "\n".join(f"- {e}" for e in all_errors)
113
- raise ValueError(msg)
114
-
115
- # map watcher + messages into dataclasses
116
- self.cfg: WatcherConfig = map_to_dataclass(
117
- raw_config.get("watcher", {}), WatcherConfig
118
- )
119
- self.msg: WatcherMessages = map_to_dataclass(
120
- raw_messages, WatcherMessages
121
- )
122
-
123
- # panel / origin / server wiring from YAML
124
- p = raw_config["panel"]
125
- self.panel = Panel(
126
- name=p["name"],
127
- url=p["url"],
128
- api_key=p["api_key"],
129
- )
130
-
131
- o = raw_config["origin"]
132
- self.origin = GenericServer(
133
- name="Origin",
134
- panel=self.panel,
135
- server_id=o["server_id"],
136
- )
137
-
138
- s = raw_config["server"]
139
- self.server = MinecraftServer(
140
- name=s["name"],
141
- panel=self.panel,
142
- server_id=s["server_id"],
143
- server_domain=s["domain"],
144
- server_port=s["port"],
145
- rcon_port=s["rcon_port"],
146
- rcon_password=s["rcon_password"],
147
- tpsCommand=s["tps_command"],
148
- )
149
-
150
- # logger now fully configurable
151
- logger_name = self.cfg.logger_name_template.format(server_name=s["name"])
152
- self.log = HungerLogger(
153
- name=logger_name,
154
- server=self.server,
155
- log_path=self.cfg.log_path,
156
- console_backspaces=self.cfg.console_backspaces,
157
- )
158
-
159
- # timezone
160
- self.tz = ZoneInfo(self.cfg.timezone)
161
-
162
- def fmt(self, template: str, **kwargs):
163
- return template.format(prefix=self.msg.prefix, **kwargs)
164
-
165
- def shutdown(self):
166
- self.log.info("Shutting down ServerWatcher.")
167
- raise SystemExit
168
-
169
- def restart_and_wait(self):
170
- self.origin.disableSchedule(self.cfg.origin_disable_schedule_id)
171
- self.server.restart()
172
- self.log.info("Restart action sent. Waiting...")
173
- time.sleep(self.cfg.restart_wait_seconds)
174
-
175
- self.log.warn("Checking server status...")
176
- alive = waitForOnline(
177
- self.server,
178
- timeout=self.cfg.restart_online_timeout,
179
- interval=self.cfg.restart_online_interval,
180
- )
181
-
182
- if alive:
183
- self.log.info("Server is back online!")
184
- self.server.sendBroadcast(f"{self.msg.prefix}<green>Restart successful!")
185
- self.origin.enableSchedule(self.cfg.origin_disable_schedule_id)
186
- else:
187
- self.log.error("Server failed to restart!")
188
-
189
- def schedule_restart(self, minutes):
190
- info = snapSchedule(minimumMinutes=minutes)
191
- scheduled = info["scheduled"]
192
-
193
- local_time = scheduled.astimezone(self.tz)
194
- time_str = local_time.strftime("%I:%M %p")
195
-
196
- self.server.sendBroadcast(
197
- self.fmt(self.msg.broadcast_restart_at, time=time_str)
198
- )
199
-
200
- minute_callbacks = {
201
- m: (lambda msg=self.fmt(self.msg.broadcast_minute[m]): self.server.sendBroadcast(msg))
202
- for m in self.msg.broadcast_minute
203
- }
204
-
205
- second_callbacks = {
206
- s: (lambda msg=self.fmt(self.msg.broadcast_second[s]): self.server.sendBroadcast(msg))
207
- for s in self.msg.broadcast_second
208
- }
209
-
210
- runCountdownEvents(
211
- target_time=scheduled,
212
- minute_callbacks=minute_callbacks,
213
- second_callbacks=second_callbacks,
214
- )
215
-
216
- def evaluate(self):
217
- self.log.info(self.msg.log_start)
218
-
219
- if not validateAll(self.panel, self.server):
220
- self.log.error(self.msg.log_validation_fail)
221
- self.shutdown()
222
-
223
- self.server.refresh()
224
- snap = Snapshot(self.server, 2, True)
225
-
226
- pro = 0
227
- anti = 0
228
- restart_reasons = []
229
- no_restart_reasons = []
230
-
231
- # PRO-RESTART
232
- if self.server.getSchedule(self.cfg.restart_soon_schedule_id)["is_active"]:
233
- restart_reasons.append(self.msg.reason_restart_soon)
234
- pro += self.cfg.weight_restart_soon
235
-
236
- if snap.ram >= self.cfg.ram_threshold:
237
- restart_reasons.append(
238
- self.fmt(
239
- self.msg.reason_ram,
240
- ram=snap.ram,
241
- threshold=self.cfg.ram_threshold,
242
- )
243
- )
244
- pro += round(snap.ram, 0) - 5
245
-
246
- if snap.cpu >= self.cfg.cpu_threshold:
247
- restart_reasons.append(
248
- self.fmt(
249
- self.msg.reason_cpu,
250
- cpu=snap.cpu,
251
- threshold=self.cfg.cpu_threshold,
252
- )
253
- )
254
- pro += self.cfg.weight_cpu
255
-
256
- if snap.uptime // 3600 >= self.cfg.uptime_hours_threshold:
257
- restart_reasons.append(
258
- self.fmt(
259
- self.msg.reason_uptime,
260
- uptime=snap.uptime_formatted,
261
- threshold=self.cfg.uptime_hours_threshold,
262
- )
263
- )
264
- pro += self.cfg.weight_uptime
265
-
266
- if snap.tps <= self.cfg.tps_threshold:
267
- restart_reasons.append(
268
- self.fmt(
269
- self.msg.reason_tps,
270
- tps=snap.tps,
271
- threshold=self.cfg.tps_threshold,
272
- )
273
- )
274
- pro += self.cfg.weight_tps
275
-
276
- # ANTI-RESTART
277
- if snap.uptime // 60 < 30:
278
- no_restart_reasons.append(
279
- self.fmt(
280
- self.msg.reason_low_uptime,
281
- uptime=snap.uptime_formatted,
282
- )
283
- )
284
- anti += self.cfg.weight_low_uptime
285
-
286
- if snap.players > 0:
287
- verb = "are" if snap.players != 1 else "is"
288
- plural = "players" if snap.players != 1 else "player"
289
- no_restart_reasons.append(
290
- self.fmt(
291
- self.msg.reason_players,
292
- verb=verb,
293
- count=snap.players,
294
- plural=plural,
295
- )
296
- )
297
- anti += snap.players * self.cfg.weight_per_player
298
-
299
- # LOGGING
300
- for r in restart_reasons:
301
- self.log.warn(f"- {r}")
302
- for r in no_restart_reasons:
303
- self.log.warn(f"- {r}")
304
-
305
- self.log.warn(f"Pro-restart: {pro}")
306
- self.log.warn(f"Anti-restart: {anti}")
307
-
308
- gap = abs(pro - anti)
309
-
310
- if pro == 0:
311
- self.log.info(self.msg.log_no_restart)
312
- return
313
-
314
- if pro > anti and snap.players == 0:
315
- self.log.info(self.msg.log_immediate_restart)
316
- self.restart_and_wait()
317
- return
318
-
319
- self.log.info(self.msg.log_scheduled)
320
-
321
- if gap <= 2:
322
- self.log.warn(self.fmt(self.msg.log_gap_low, gap=gap))
323
- self.schedule_restart(self.cfg.low_gap_minutes)
324
- else:
325
- self.log.warn(self.fmt(self.msg.log_gap_high, gap=gap))
326
- self.schedule_restart(self.cfg.high_gap_minutes)
327
-
328
- self.restart_and_wait()
329
-
330
- def run(self):
331
- clearTerminal()
332
- while True:
333
- self.evaluate()
334
- time.sleep(60)
File without changes
File without changes
File without changes