serverwatcher 4.0.1__tar.gz → 4.2__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 (21) hide show
  1. {serverwatcher-4.0.1/src/serverwatcher.egg-info → serverwatcher-4.2}/PKG-INFO +1 -1
  2. {serverwatcher-4.0.1 → serverwatcher-4.2}/pyproject.toml +1 -1
  3. serverwatcher-4.2/src/serverwatcher/autorepair.py +137 -0
  4. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/configclasses/global_config.py +10 -0
  5. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/configclasses/watcher.py +1 -10
  6. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/defaultconfigs/global.yaml +12 -0
  7. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/defaultconfigs/watcher.yaml +1 -13
  8. serverwatcher-4.2/src/serverwatcher/validation.py +125 -0
  9. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/watcher.py +16 -8
  10. {serverwatcher-4.0.1 → serverwatcher-4.2/src/serverwatcher.egg-info}/PKG-INFO +1 -1
  11. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher.egg-info/SOURCES.txt +2 -0
  12. {serverwatcher-4.0.1 → serverwatcher-4.2}/LICENSE +0 -0
  13. {serverwatcher-4.0.1 → serverwatcher-4.2}/README.md +0 -0
  14. {serverwatcher-4.0.1 → serverwatcher-4.2}/setup.cfg +0 -0
  15. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/__init__.py +0 -0
  16. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/configclasses/__init__.py +0 -0
  17. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/configclasses/messages.py +0 -0
  18. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher/defaultconfigs/messages.yaml +0 -0
  19. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher.egg-info/dependency_links.txt +0 -0
  20. {serverwatcher-4.0.1 → serverwatcher-4.2}/src/serverwatcher.egg-info/requires.txt +0 -0
  21. {serverwatcher-4.0.1 → serverwatcher-4.2}/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: 4.0.1
3
+ Version: 4.2
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 = "4.0.1"
10
+ version = "4.2"
11
11
  description = "A HungerLib-powered Minecraft server automation engine."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -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()
@@ -2,6 +2,8 @@ from dataclasses import dataclass
2
2
 
3
3
  @dataclass
4
4
  class GlobalConfig:
5
+ watch_interval: int
6
+
5
7
  panel_name: str
6
8
  panel_url: str
7
9
  panel_api_key: str
@@ -15,3 +17,11 @@ class GlobalConfig:
15
17
  rcon_port: int
16
18
  rcon_password: str
17
19
  tps_command: str
20
+
21
+ do_logging: bool
22
+ logger_name: str
23
+ log_path: str
24
+ timezone: str
25
+
26
+ console_backspaces: int
27
+ clear_terminal: bool
@@ -24,13 +24,4 @@ class WatcherConfig:
24
24
 
25
25
  restart_wait_seconds: int
26
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
33
-
34
- # New values (will reorder later)
35
- watch_interval: int
36
- clear_terminal: bool
27
+ restart_online_interval: int
@@ -1,3 +1,5 @@
1
+ watch_interval: 60
2
+
1
3
  panel:
2
4
  panel_name: My Panel
3
5
  panel_url: https://example.com
@@ -14,3 +16,13 @@ server:
14
16
  rcon_port: 25575
15
17
  rcon_password: password
16
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,12 +1,3 @@
1
- logging:
2
- logger_name_template: "Server Watcher"
3
- log_path: "/home/container/logs/"
4
- timezone: "America/Chicago"
5
-
6
- terminal_settings:
7
- console_backspaces: 8
8
- clear_terminal: True
9
-
10
1
  schedules:
11
2
  restart_soon_schedule_id: 0 # replace this with real schedule id
12
3
  origin_disable_schedule_id: 0 # replace this with real schedule id
@@ -33,7 +24,4 @@ gaps:
33
24
  restart_intervals:
34
25
  restart_wait_seconds: 45
35
26
  restart_online_timeout: 120
36
- restart_online_interval: 2
37
-
38
- watcher_intervals:
39
- watch_interval: 60
27
+ restart_online_interval: 2
@@ -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
 
@@ -64,18 +72,18 @@ class ServerWatcher:
64
72
  tpsCommand=self.global_cfg.tps_command,
65
73
  )
66
74
 
67
- logger_name = self.cfg.logger_name_template.format(
75
+ logger_name = self.global_cfg.logger_name.format(
68
76
  server_name=self.global_cfg.server_name
69
77
  )
70
78
 
71
79
  self.log = HungerLogger(
72
80
  name=logger_name,
73
81
  server=self.server,
74
- log_path=self.cfg.log_path,
75
- console_backspaces=self.cfg.console_backspaces,
82
+ log_path=self.global_cfg.log_path,
83
+ console_backspaces=self.global_cfg.console_backspaces,
76
84
  )
77
85
 
78
- self.tz = ZoneInfo(self.cfg.timezone)
86
+ self.tz = ZoneInfo(self.global_cfg.timezone)
79
87
 
80
88
  # utility
81
89
  def fmt(self, template: str, **kwargs):
@@ -101,7 +109,7 @@ class ServerWatcher:
101
109
 
102
110
  if alive:
103
111
  self.log.info(self.messages.server_back_online)
104
- self.server.sendBroadcast(self.messages.server_back_online_broadcast)
112
+ self.server.sendBroadcast(f"{self.messages.server_back_online_broadcast}")
105
113
  self.origin.enableSchedule(self.cfg.origin_disable_schedule_id)
106
114
  else:
107
115
  self.log.error(self.messages.server_failed_restart)
@@ -239,10 +247,10 @@ class ServerWatcher:
239
247
 
240
248
  # main loop
241
249
  def run(self):
242
- if self.cfg.clear_terminal:
250
+ if self.global_cfg.clear_terminal:
243
251
  clearTerminal()
244
252
  while True:
245
- if self.cfg.clear_terminal:
253
+ if self.global_cfg.clear_terminal:
246
254
  clearTerminal()
247
255
  self.evaluate()
248
- time.sleep(self.cfg.watch_interval)
256
+ time.sleep(self.global_cfg.watch_interval)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 4.0.1
3
+ Version: 4.2
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: GPL-3.0
@@ -2,6 +2,8 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/serverwatcher/__init__.py
5
+ src/serverwatcher/autorepair.py
6
+ src/serverwatcher/validation.py
5
7
  src/serverwatcher/watcher.py
6
8
  src/serverwatcher.egg-info/PKG-INFO
7
9
  src/serverwatcher.egg-info/SOURCES.txt
File without changes
File without changes
File without changes