Skip to content

Commit 318a7b6

Browse files
committed
adding auto sub-module loading
1 parent 042b0f1 commit 318a7b6

4 files changed

Lines changed: 110 additions & 4 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "modular-server-manager"
3-
version = "0.1.3"
3+
version = "0.1.5"
44
description = "Modular Server Manager"
55
authors = [
66
{ name = "Antoine BUIREY", email = "antoine.buirey@gmail.com" }

server/src/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from gamuLogger import Logger, config_argparse, config_logger
66

7+
from . import __all__ # load all server modules to avoid loading them later from a submodule
8+
79
Logger.show_pid()
810
Logger.show_threads_name()
911

server/src/core/core.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
from gamuLogger import Logger
1212
from version import Version
1313

14+
from .submodules import import_all_interfaces
1415
from ..bus import Bus, BusDispatcher, Events
1516
from ..utils.misc import gen_id
1617
from ..minecraft import (McInstallersModules, McServersModules, McInstallersUrls,
1718
BaseMcServer, ServerStatus, WebInterface)
18-
from ..user_interface import UserInterfaceModules
1919

2020
Logger.set_module("Core.Core")
2121

@@ -112,10 +112,13 @@ def __start_user_interfaces(self):
112112
if self.__config.get("user_interface_modules", {}, True) == {}:
113113
Logger.warning("No user interface modules configured.")
114114
return
115+
116+
user_interface_modules = import_all_interfaces()
117+
115118
to_load : dict[str, dict[str, Any]] = self.__config.get("user_interface_modules") #type: ignore
116119
for module_type, config in to_load.items():
117120
Logger.info(f"Initializing user interface module {config['name']} of type {module_type}...")
118-
if module_type not in UserInterfaceModules:
121+
if module_type not in user_interface_modules:
119122
Logger.warning(f"User interface module {module_type} unknown. Skipping.")
120123
continue
121124
if not config['enabled']:
@@ -128,7 +131,7 @@ def __start_user_interfaces(self):
128131
module_conf.pop("name") # Remove 'name' key
129132
module_conf["database_path"] = self.__config.get("client_database_path")
130133
try:
131-
module_class = UserInterfaceModules[module_type]
134+
module_class = user_interface_modules[module_type]
132135
def __start_ui_module():
133136
module_instance = module_class(bus_data=bus_data, **module_conf)
134137
module_instance.start()

server/src/core/submodules.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import sys
2+
import os
3+
import re
4+
import json
5+
import importlib
6+
from version import Version
7+
8+
from gamuLogger import Logger
9+
10+
from ..user_interface import BaseInterface
11+
12+
Logger.set_module("Core.Submodules")
13+
14+
PYTHON_BASE_PATH = sys.prefix
15+
PYTHON_LIB_PATH = f"{PYTHON_BASE_PATH}/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages"
16+
17+
18+
def __get_module_name(dirname: str, version : Version) -> str:
19+
abs_path = f"{PYTHON_LIB_PATH}/{dirname}-{str(version)}.dist-info"
20+
if not os.path.exists(abs_path):
21+
raise ValueError(f"Module path does not exist: {abs_path}")
22+
with open(f"{abs_path}/METADATA", "r") as f:
23+
for line in f:
24+
if line.startswith("Name: "):
25+
return line[len("Name: "):].strip()
26+
raise ValueError(f"Could not find module name in METADATA for {dirname}-{str(version)}")
27+
28+
def __get_msm_version() -> Version:
29+
candidates = [c for c in os.listdir(PYTHON_LIB_PATH) if c.startswith("modular_server_manager-") and c.endswith(".dist-info")]
30+
if not candidates:
31+
raise ValueError("Could not find modular_server_manager module in site-packages")
32+
if len(candidates) > 1:
33+
raise ValueError("Multiple modular_server_manager modules found in site-packages")
34+
match = re.match(r"modular_server_manager-([\d\.]+)\.dist-info", candidates[0])
35+
if not match:
36+
raise ValueError("Could not parse modular_server_manager version from directory name")
37+
return Version.from_string(match.groups()[0])
38+
39+
def __load_compatibility_map(module_dir : str) -> dict[Version, dict[str, Version]]:
40+
abs_path = f"{PYTHON_LIB_PATH}/{module_dir}"
41+
if not os.path.exists(abs_path):
42+
raise ValueError(f"Module path does not exist: {abs_path}")
43+
with open(f"{abs_path}/compatibility.json", "r") as f:
44+
data = json.load(f)
45+
if "compatibility" not in data:
46+
raise ValueError(f"compatibility.json does not contain 'compatibility' key in {module_dir}")
47+
compatibility_map : dict[Version, dict[str, Version]] = {}
48+
for msm_version_str, compat_info in data["compatibility"].items():
49+
msm_version = Version.from_string(msm_version_str)
50+
min_module_version = Version.from_string(compat_info["min_module_version"])
51+
max_module_version = Version.from_string(compat_info["max_module_version"])
52+
compatibility_map[msm_version] = {
53+
"min_module_version": min_module_version,
54+
"max_module_version": max_module_version
55+
}
56+
return compatibility_map
57+
58+
def __is_version_allowed(module_dir : str, module_version: Version) -> bool:
59+
msm_version = __get_msm_version()
60+
compatibility_map = __load_compatibility_map(module_dir)
61+
if msm_version not in compatibility_map:
62+
return False
63+
min_version = compatibility_map[msm_version]["min_module_version"]
64+
max_version = compatibility_map[msm_version]["max_module_version"]
65+
return min_version <= module_version <= max_version
66+
67+
def __list_mods() -> list[tuple[str, Version]]:
68+
result : list[tuple[str, Version]] = []
69+
for path in os.listdir(PYTHON_LIB_PATH):
70+
if path.startswith("modular_server_manager_") and path.endswith(".dist-info"):
71+
print(f"Found module: {path}")
72+
match = re.match(r"(modular_server_manager_[\w\d_+-]+)-([\d\.]+)\.dist-info", path)
73+
if not match:
74+
continue
75+
mod_name, mod_version = match.groups()
76+
result.append((mod_name, Version.from_string(mod_version)))
77+
return result
78+
79+
80+
def __import_interface_module(module_dir: str) -> type[BaseInterface]:
81+
Logger.debug(f"Importing interface module: {module_dir}")
82+
try:
83+
module = importlib.import_module(module_dir)
84+
Interface : type[BaseInterface] = getattr(module, "Interface")
85+
except ImportError as e:
86+
raise ImportError(f"Could not import module {module_dir}: {str(e)}")
87+
else:
88+
Logger.info(f"Imported module: {module_dir}")
89+
return Interface
90+
91+
def import_all_interfaces() -> dict[str, type[BaseInterface]]:
92+
interfaces : dict[str, type[BaseInterface]] = {}
93+
mods = __list_mods()
94+
Logger.debug(f"Found {len(mods)} sub-modules in site-packages:\n" + "\n".join([f"- {mod[0]} {str(mod[1])}" for mod in mods]))
95+
for mod_dir, mod_version in mods:
96+
if __is_version_allowed(mod_dir, mod_version):
97+
interface_class = __import_interface_module(mod_dir)
98+
name = __get_module_name(mod_dir, mod_version)
99+
interfaces[name] = interface_class
100+
Logger.info(f"Imported {len(interfaces)} interface modules")
101+
return interfaces

0 commit comments

Comments
 (0)