serverwatcher 4.1__tar.gz → 4.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {serverwatcher-4.1/src/serverwatcher.egg-info → serverwatcher-4.2.1}/PKG-INFO +1 -1
- {serverwatcher-4.1 → serverwatcher-4.2.1}/pyproject.toml +1 -1
- serverwatcher-4.2.1/src/serverwatcher/autorepair.py +137 -0
- serverwatcher-4.2.1/src/serverwatcher/validator.py +125 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/watcher.py +8 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1/src/serverwatcher.egg-info}/PKG-INFO +1 -1
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher.egg-info/SOURCES.txt +2 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/LICENSE +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/README.md +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/setup.cfg +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/__init__.py +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/configclasses/__init__.py +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/configclasses/global_config.py +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/configclasses/messages.py +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/configclasses/watcher.py +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/defaultconfigs/global.yaml +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/defaultconfigs/messages.yaml +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher/defaultconfigs/watcher.yaml +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher.egg-info/dependency_links.txt +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher.egg-info/requires.txt +0 -0
- {serverwatcher-4.1 → serverwatcher-4.2.1}/src/serverwatcher.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
from dataclasses import fields, MISSING
|
|
4
|
+
|
|
5
|
+
from hungerlib.addons import load_yaml, flatten_nested
|
|
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
|
+
# Helpers
|
|
13
|
+
# ---------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
def load_yaml_default(schema, default_path):
|
|
16
|
+
"""
|
|
17
|
+
Load the default YAML file located inside the schema's package.
|
|
18
|
+
"""
|
|
19
|
+
module_file = schema.__module__.replace(".", "/")
|
|
20
|
+
pkg_root = os.path.dirname(os.path.dirname(__import__(schema.__module__).__file__))
|
|
21
|
+
abs_default = os.path.join(pkg_root, default_path.lstrip("/"))
|
|
22
|
+
|
|
23
|
+
if os.path.exists(abs_default):
|
|
24
|
+
return flatten_nested(load_yaml(abs_default))
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_schema_defaults(schema):
|
|
29
|
+
"""
|
|
30
|
+
Extract default values from dataclass fields.
|
|
31
|
+
"""
|
|
32
|
+
defaults = {}
|
|
33
|
+
for f in fields(schema):
|
|
34
|
+
if f.default is not MISSING:
|
|
35
|
+
defaults[f.name] = f.default
|
|
36
|
+
elif f.default_factory is not MISSING: # type: ignore
|
|
37
|
+
defaults[f.name] = f.default_factory() # type: ignore
|
|
38
|
+
return defaults
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def repair_value(name, value, expected_type, schema_defaults, yaml_defaults, repairs):
|
|
42
|
+
"""
|
|
43
|
+
Determine the correct value for a field using priority:
|
|
44
|
+
1. User value (if valid)
|
|
45
|
+
2. Schema default
|
|
46
|
+
3. YAML default
|
|
47
|
+
"""
|
|
48
|
+
# If value is correct type and valid, keep it
|
|
49
|
+
if isinstance(value, expected_type):
|
|
50
|
+
if expected_type in (int, float) and value < 0:
|
|
51
|
+
repairs.append(f"{name}: negative → repaired to default")
|
|
52
|
+
else:
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
# Try schema default
|
|
56
|
+
if name in schema_defaults:
|
|
57
|
+
repairs.append(f"{name}: repaired using schema default")
|
|
58
|
+
return schema_defaults[name]
|
|
59
|
+
|
|
60
|
+
# Try YAML default
|
|
61
|
+
if name in yaml_defaults:
|
|
62
|
+
repairs.append(f"{name}: repaired using YAML default")
|
|
63
|
+
return yaml_defaults[name]
|
|
64
|
+
|
|
65
|
+
# Fallback: empty string or zero
|
|
66
|
+
repairs.append(f"{name}: no defaults found → set to safe fallback")
|
|
67
|
+
if expected_type is str:
|
|
68
|
+
return ""
|
|
69
|
+
if expected_type in (int, float):
|
|
70
|
+
return 0
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def repair_config(path, default_path, schema):
|
|
75
|
+
"""
|
|
76
|
+
Load, repair, and rewrite a config file.
|
|
77
|
+
"""
|
|
78
|
+
print(f"\n🔧 Repairing {path}...")
|
|
79
|
+
|
|
80
|
+
# Load user config (raw YAML)
|
|
81
|
+
if os.path.exists(path):
|
|
82
|
+
raw = flatten_nested(load_yaml(path))
|
|
83
|
+
else:
|
|
84
|
+
raw = {}
|
|
85
|
+
|
|
86
|
+
# Load defaults
|
|
87
|
+
schema_defaults = get_schema_defaults(schema)
|
|
88
|
+
yaml_defaults = load_yaml_default(schema, default_path)
|
|
89
|
+
|
|
90
|
+
repaired = {}
|
|
91
|
+
repairs = []
|
|
92
|
+
|
|
93
|
+
# Validate each field in schema
|
|
94
|
+
for f in fields(schema):
|
|
95
|
+
name = f.name
|
|
96
|
+
expected_type = f.type
|
|
97
|
+
value = raw.get(name, None)
|
|
98
|
+
|
|
99
|
+
repaired[name] = repair_value(
|
|
100
|
+
name, value, expected_type, schema_defaults, yaml_defaults, repairs
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Write repaired YAML back to disk
|
|
104
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
105
|
+
with open(path, "w") as f:
|
|
106
|
+
yaml.dump(repaired, f, sort_keys=False)
|
|
107
|
+
|
|
108
|
+
if repairs:
|
|
109
|
+
print(" ✔ Repairs applied:")
|
|
110
|
+
for r in repairs:
|
|
111
|
+
print(" -", r)
|
|
112
|
+
else:
|
|
113
|
+
print(" ✔ No repairs needed")
|
|
114
|
+
|
|
115
|
+
return repairs
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------
|
|
119
|
+
# Main
|
|
120
|
+
# ---------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def autorepair_all():
|
|
123
|
+
print("=== ServerWatcher Auto‑Repair ===")
|
|
124
|
+
|
|
125
|
+
repairs = []
|
|
126
|
+
repairs += repair_config("config/global.yaml", "/defaultconfigs/global.yaml", GlobalConfig)
|
|
127
|
+
repairs += repair_config("config/messages.yaml", "/defaultconfigs/messages.yaml", MessagesConfig)
|
|
128
|
+
repairs += repair_config("config/watcher.yaml", "/defaultconfigs/watcher.yaml", WatcherConfig)
|
|
129
|
+
|
|
130
|
+
if repairs:
|
|
131
|
+
print("\n✅ Auto‑repair completed with fixes.")
|
|
132
|
+
else:
|
|
133
|
+
print("\n✅ All configs already valid — no repairs needed.")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
autorepair_all()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from dataclasses import fields
|
|
3
|
+
|
|
4
|
+
from hungerlib.addons.simpleloader 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
|
+
# Example: ensure ports are valid
|
|
60
|
+
if cfg.server_port <= 0 or cfg.server_port > 65535:
|
|
61
|
+
errors.append(f"server_port: must be 1–65535 (got {cfg.server_port})")
|
|
62
|
+
|
|
63
|
+
if cfg.rcon_port <= 0 or cfg.rcon_port > 65535:
|
|
64
|
+
errors.append(f"rcon_port: must be 1–65535 (got {cfg.rcon_port})")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_watcher_config(cfg, errors):
|
|
68
|
+
if cfg.watch_interval < 1:
|
|
69
|
+
errors.append(f"watch_interval: must be >= 1 (got {cfg.watch_interval})")
|
|
70
|
+
|
|
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 or cfg.cpu_threshold > 100:
|
|
75
|
+
errors.append(f"cpu_threshold: must be 1–100 (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
|
+
|
|
92
|
+
# -----------------------------
|
|
93
|
+
# Main validator
|
|
94
|
+
# -----------------------------
|
|
95
|
+
|
|
96
|
+
def validate_all():
|
|
97
|
+
errors = []
|
|
98
|
+
|
|
99
|
+
# Load configs
|
|
100
|
+
global_cfg = loadConfig("config/global.yaml", "/defaultconfigs/global.yaml", GlobalConfig)
|
|
101
|
+
messages_cfg = loadConfig("config/messages.yaml", "/defaultconfigs/messages.yaml", MessagesConfig)
|
|
102
|
+
watcher_cfg = loadConfig("config/watcher.yaml", "/defaultconfigs/watcher.yaml", WatcherConfig)
|
|
103
|
+
|
|
104
|
+
# Generic dataclass validation
|
|
105
|
+
validate_dataclass(global_cfg, GlobalConfig, errors)
|
|
106
|
+
validate_dataclass(messages_cfg, MessagesConfig, errors)
|
|
107
|
+
validate_dataclass(watcher_cfg, WatcherConfig, errors)
|
|
108
|
+
|
|
109
|
+
# Config-specific validation
|
|
110
|
+
validate_global_config(global_cfg, errors)
|
|
111
|
+
validate_messages_config(messages_cfg, errors)
|
|
112
|
+
validate_watcher_config(watcher_cfg, errors)
|
|
113
|
+
|
|
114
|
+
# Print results
|
|
115
|
+
if errors:
|
|
116
|
+
print("❌ CONFIG VALIDATION FAILED:")
|
|
117
|
+
for e in errors:
|
|
118
|
+
print(" -", e)
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
print("✅ All configs are valid.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
validate_all()
|
|
@@ -18,6 +18,14 @@ from serverwatcher.configclasses.global_config import GlobalConfig
|
|
|
18
18
|
from serverwatcher.configclasses.messages import MessagesConfig
|
|
19
19
|
from serverwatcher.configclasses.watcher import WatcherConfig
|
|
20
20
|
|
|
21
|
+
# Validate and autorepair configs before watcher starts
|
|
22
|
+
from serverwatcher.validator import validate_all
|
|
23
|
+
from serverwatcher.autorepair import autorepair_all
|
|
24
|
+
|
|
25
|
+
validate_all()
|
|
26
|
+
autorepair_all()
|
|
27
|
+
validate_all()
|
|
28
|
+
|
|
21
29
|
class ServerWatcher:
|
|
22
30
|
def __init__(self):
|
|
23
31
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|