serverwatcher 2.2.3__tar.gz → 3.0__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-2.2.3/src/serverwatcher.egg-info → serverwatcher-3.0}/PKG-INFO +1 -1
- {serverwatcher-2.2.3 → serverwatcher-3.0}/pyproject.toml +1 -1
- {serverwatcher-2.2.3 → serverwatcher-3.0}/src/serverwatcher/__init__.py +3 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/src/serverwatcher/config.py +6 -0
- serverwatcher-3.0/src/serverwatcher/schema.py +45 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/src/serverwatcher/watcher.py +91 -18
- {serverwatcher-2.2.3 → serverwatcher-3.0/src/serverwatcher.egg-info}/PKG-INFO +1 -1
- {serverwatcher-2.2.3 → serverwatcher-3.0}/src/serverwatcher.egg-info/SOURCES.txt +1 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/LICENSE +0 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/README.md +0 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/setup.cfg +0 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/src/serverwatcher/messages.py +0 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/src/serverwatcher.egg-info/dependency_links.txt +0 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/src/serverwatcher.egg-info/requires.txt +0 -0
- {serverwatcher-2.2.3 → serverwatcher-3.0}/src/serverwatcher.egg-info/top_level.txt +0 -0
|
@@ -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
|
]
|
|
@@ -31,3 +31,9 @@ class WatcherConfig:
|
|
|
31
31
|
restart_wait_seconds: int = 45
|
|
32
32
|
restart_online_timeout: int = 120
|
|
33
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"
|
|
@@ -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
|
|
@@ -10,11 +10,19 @@ from hungerlib.addons import (
|
|
|
10
10
|
waitForOnline,
|
|
11
11
|
validateAll,
|
|
12
12
|
runCountdownEvents,
|
|
13
|
+
ensure_yaml,
|
|
14
|
+
load_yaml,
|
|
15
|
+
map_to_dataclass,
|
|
16
|
+
write_default_yaml,
|
|
13
17
|
)
|
|
14
|
-
from hungerlib.addons import configloader
|
|
15
18
|
|
|
16
19
|
from .config import WatcherConfig
|
|
17
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"
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
DEFAULT_CONFIG = {
|
|
@@ -36,38 +44,98 @@ DEFAULT_CONFIG = {
|
|
|
36
44
|
"tps_command": "ticks",
|
|
37
45
|
},
|
|
38
46
|
"watcher": {},
|
|
39
|
-
|
|
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",
|
|
40
81
|
}
|
|
41
82
|
|
|
42
83
|
|
|
43
84
|
class ServerWatcher:
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
100
|
ensure_yaml(config_path, DEFAULT_CONFIG)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
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
|
+
)
|
|
51
119
|
self.msg: WatcherMessages = map_to_dataclass(
|
|
52
|
-
|
|
120
|
+
raw_messages, WatcherMessages
|
|
53
121
|
)
|
|
54
122
|
|
|
55
123
|
# panel / origin / server wiring from YAML
|
|
56
|
-
p =
|
|
124
|
+
p = raw_config["panel"]
|
|
57
125
|
self.panel = Panel(
|
|
58
126
|
name=p["name"],
|
|
59
127
|
url=p["url"],
|
|
60
128
|
api_key=p["api_key"],
|
|
61
129
|
)
|
|
62
130
|
|
|
63
|
-
o =
|
|
131
|
+
o = raw_config["origin"]
|
|
64
132
|
self.origin = GenericServer(
|
|
65
133
|
name="Origin",
|
|
66
134
|
panel=self.panel,
|
|
67
135
|
server_id=o["server_id"],
|
|
68
136
|
)
|
|
69
137
|
|
|
70
|
-
s =
|
|
138
|
+
s = raw_config["server"]
|
|
71
139
|
self.server = MinecraftServer(
|
|
72
140
|
name=s["name"],
|
|
73
141
|
panel=self.panel,
|
|
@@ -79,13 +147,18 @@ class ServerWatcher:
|
|
|
79
147
|
tpsCommand=s["tps_command"],
|
|
80
148
|
)
|
|
81
149
|
|
|
150
|
+
# logger now fully configurable
|
|
151
|
+
logger_name = self.cfg.logger_name_template.format(server_name=s["name"])
|
|
82
152
|
self.log = HungerLogger(
|
|
83
|
-
name=
|
|
153
|
+
name=logger_name,
|
|
84
154
|
server=self.server,
|
|
85
|
-
log_path=
|
|
86
|
-
console_backspaces=
|
|
155
|
+
log_path=self.cfg.log_path,
|
|
156
|
+
console_backspaces=self.cfg.console_backspaces,
|
|
87
157
|
)
|
|
88
158
|
|
|
159
|
+
# timezone
|
|
160
|
+
self.tz = ZoneInfo(self.cfg.timezone)
|
|
161
|
+
|
|
89
162
|
def fmt(self, template: str, **kwargs):
|
|
90
163
|
return template.format(prefix=self.msg.prefix, **kwargs)
|
|
91
164
|
|
|
@@ -117,11 +190,11 @@ class ServerWatcher:
|
|
|
117
190
|
info = snapSchedule(minimumMinutes=minutes)
|
|
118
191
|
scheduled = info["scheduled"]
|
|
119
192
|
|
|
120
|
-
|
|
121
|
-
|
|
193
|
+
local_time = scheduled.astimezone(self.tz)
|
|
194
|
+
time_str = local_time.strftime("%I:%M %p")
|
|
122
195
|
|
|
123
196
|
self.server.sendBroadcast(
|
|
124
|
-
self.fmt(self.msg.broadcast_restart_at, time=
|
|
197
|
+
self.fmt(self.msg.broadcast_restart_at, time=time_str)
|
|
125
198
|
)
|
|
126
199
|
|
|
127
200
|
minute_callbacks = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|