仓库初始化

This commit is contained in:
lwt
2026-05-22 00:16:08 +08:00
commit 95c96dce42
118 changed files with 23005 additions and 0 deletions
View File
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
软件配置(与软件一起走):存放于本机固定目录,与当前打开的项目无关。
- 最近打开的项目列表、应用级偏好、日志等。
项目配置(与项目一起走):存放于项目目录 .server_manager/config/ 下,如 tool.config、default.kv(首次打开从软件安装目录 config/ 复制)。
"""
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict, List
from services.commands import project_has_default_kv_for_manager
def get_app_config_dir() -> Path:
"""软件配置根目录(与软件一起,不随项目或运行目录变化)。
Windows: %%LocalAppData%%\\ServerManager
其他: ~/.config/ServerManager
打包后与未打包使用同一规则,保证配置持久且唯一。
"""
if sys.platform == "win32":
base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
root = Path(base) / "ServerManager"
else:
root = Path.home() / ".config" / "ServerManager"
root.mkdir(parents=True, exist_ok=True)
return root
def get_app_config_path() -> Path:
"""软件配置文件路径(JSON)。"""
return get_app_config_dir() / "app_config.json"
def load_app_config() -> Dict[str, Any]:
"""加载软件配置。若不存在或损坏则返回空字典。"""
path = get_app_config_path()
if not path.exists():
return {}
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except Exception:
return {}
def save_app_config(data: Dict[str, Any]) -> None:
"""保存软件配置。"""
path = get_app_config_path()
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception:
pass
def get_recent_projects() -> List[str]:
"""从软件配置中读取最近打开的项目路径列表(最多 10 个,仅返回仍存在的有效项目)。"""
data = load_app_config()
raw = data.get("recent_projects") or []
if not isinstance(raw, list):
return []
result = []
for item in raw[:15]:
s = str(item).strip()
if not s or s in result:
continue
p = Path(s)
if p.exists() and project_has_default_kv_for_manager(p):
result.append(s)
if len(result) >= 10:
break
return result
def save_recent_project(project_path: str) -> None:
"""将项目路径加入软件配置的最近列表并保存。"""
data = load_app_config()
current = get_recent_projects()
project_path = str(Path(project_path).resolve())
if project_path in current:
current.remove(project_path)
current.insert(0, project_path)
data["recent_projects"] = current[:10]
save_app_config(data)
def get_app_log_dir() -> Path:
"""软件日志目录(与软件一起,用于存放 server_manager_*.log)。"""
log_dir = get_app_config_dir() / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
return log_dir
+414
View File
@@ -0,0 +1,414 @@
# -*- coding: utf-8 -*-
"""Project configuration model."""
from util.runtime import *
def _empty_config_data():
"""无项目时的默认配置结构,避免 get() 报错"""
return {
"workspace": {"server_root": "", "run_dir": ""},
"database": {"host": "", "user": "", "password": "", "port": 3306},
"server": {
"prefix": "ddxq2", "login_server_node": "", "center_server_node": "",
"default_server_id": "1", "server_type": "game_server", "server_name": "",
"ip": get_local_ip(), "game_host": get_local_ip(), "cookie": "ddxq2-node",
},
"erlang": {"r25_path": "", "erl": "erl", "werl": "werl", "escript": "escript"},
"login_db": {
"name": "", "server_id": 900, "host": "", "port": 0,
"user": "", "password": "",
},
"commands": {"rebar": REBAR3_CMD, "svn": "svn"},
"terminal": {"launch_mode": "auto", "exe_path": ""},
}
class Config:
"""项目配置管理类:读写当前打开项目下的 .server_manager/config/tool.config(与项目一起走)。
打开项目时会将软件目录下受管模板同步到 .server_manager/config,并保留项目自己的 tool.config。
支持“无项目”启动:project_root=None 时仅显示欢迎页;通过 open_project(path) 打开项目后加载该项目配置。
"""
def __init__(self, project_root: Optional[Path] = None):
if getattr(sys, 'frozen', False):
self._exe_dir = Path(sys.executable).parent
self._script_dir = self._exe_dir
else:
self._script_dir = SRC_DIR
self._exe_dir = self._script_dir.parent.parent
self.tool_dir = None
self.tool_config_path = None
self.config_error = None
self.data = _empty_config_data()
if project_root is not None:
self.open_project(Path(project_root))
else:
self.config_error = "未打开项目"
def has_project(self) -> bool:
"""是否已打开有效项目(存在 .server_manager/config"""
if self.tool_dir is None:
return False
return get_server_manager_config_dir(self.tool_dir).is_dir()
def open_project(self, project_root: Path) -> bool:
"""打开项目目录并同步受管配置模板到 .server_manager/config。成功返回 True。"""
project_root = Path(project_root).resolve()
cfg_dir, err = ensure_and_get_server_manager_config_dir(project_root)
if not cfg_dir:
self.config_error = err
return False
self.tool_dir = project_root
self.tool_config_path = cfg_dir / "tool.config"
self.config_error = None
self.data = self._load_config()
return True
def _find_server_root(self) -> Path:
"""查找有效的服务器根目录
查找顺序:优先运行目录(当前工作目录),再考虑 exe/脚本所在位置。
Returns:
有效的服务器根目录路径,如果都找不到返回 None
"""
candidates = [
Path.cwd(), # 运行目录优先
self._exe_dir,
self._script_dir,
]
for path in candidates:
if (get_server_manager_config_dir(path) / "default.kv").exists():
return path
if (path / "config" / "default.kv").exists():
return path
return None
def _load_tool_config(self) -> dict:
"""加载 tool.config 文件"""
if self.tool_config_path is None or not self.tool_config_path.exists():
return {}
config = {}
try:
with open(self.tool_config_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
except Exception:
return {}
return config
def _save_tool_config(self, config: dict):
"""保存 tool.config 文件"""
lines = [
"# Server Manager config",
"# 此文件配置工具运行所需的环境路径",
"",
"# === 工作目录配置 ===",
f"server_root={config.get('server_root', '')}",
f"run_dir={config.get('run_dir', '')}",
"",
"# === 启动窗口配置 ===",
f"terminal_launch_mode={config.get('terminal_launch_mode', 'auto')}",
f"terminal_exe_path={config.get('terminal_exe_path', '')}",
"",
"# === Erlang 配置 ===",
f"r25_path={config.get('r25_path', '')}",
"",
"# === 数据库配置 ===",
f"db_host={config.get('db_host', '')}",
f"db_port={config.get('db_port', '3306')}",
f"db_user={config.get('db_user', '')}",
f"db_pass={config.get('db_pass', '')}",
"",
"# === 登录服数据库配置(远程连接用)===",
f"login_db_name={config.get('login_db_name', '')}",
f"login_db_server_id={config.get('login_db_server_id', '900')}",
f"login_db_host={config.get('login_db_host', '')}",
f"login_db_port={config.get('login_db_port', '0')}",
f"login_db_user={config.get('login_db_user', '')}",
f"login_db_pass={config.get('login_db_pass', '')}",
"",
"# === 服务器基础配置 ===",
f"prefix={config.get('prefix', 'ddxq2')}",
f"default_server_id={config.get('default_server_id', '1')}",
f"server_type={config.get('server_type', 'game_server')}",
f"server_name={config.get('server_name', '')}",
f"login_node={config.get('login_node', '')}",
f"center_node={config.get('center_node', '')}",
f"cookie={config.get('cookie', 'ddxq2-node')}",
]
self.tool_config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.tool_config_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
def _load_default_kv(self, server_root: str) -> dict:
"""从 default.kv 读取默认配置"""
if not server_root:
return {}
default_kv_path = get_server_manager_config_dir(server_root) / 'default.kv'
if not default_kv_path.exists():
return {}
config = {}
try:
with open(default_kv_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
except Exception:
return {}
return config
def _load_config(self):
"""加载配置,从 tool.config 读取,空值从 default.kv 补充。
以当前已打开的项目目录(self.tool_dir)为 server_root 默认值,tool.config 可覆盖。
"""
if self.tool_dir is None or self.tool_config_path is None:
return self.data
tool_cfg = self._load_tool_config()
is_first_run = not self.tool_config_path.exists()
# 以当前项目目录为默认,tool.config 可覆盖
server_root = tool_cfg.get('server_root', '') or str(self.tool_dir)
run_dir = tool_cfg.get('run_dir', '')
if not run_dir and server_root:
run_dir = str(Path(server_root) / "run")
# 首次运行时,创建运行目录
if is_first_run and run_dir:
run_path = Path(run_dir)
if not run_path.exists():
try:
run_path.mkdir(parents=True, exist_ok=True)
except Exception:
pass
# 如果有 server_root,尝试从 default.kv 读取默认值
default_kv = self._load_default_kv(server_root)
# 辅助函数:优先使用 tool.config 值,为空则使用 default.kv 值
def get_value(tool_key, default_key, fallback=''):
val = tool_cfg.get(tool_key, '')
if not val and default_kv:
val = default_kv.get(default_key, fallback)
# 去掉可能的引号
if isinstance(val, str) and val.startswith("'") and val.endswith("'"):
val = val[1:-1]
return val if val else fallback
db_host = get_value('db_host', 'db_host', '')
db_user = get_value('db_user', 'db_user', '')
db_pass = get_value('db_pass', 'db_pass', '')
db_port = get_value('db_port', 'db_port', '3306')
cookie = get_value('cookie', 'cookie', 'ddxq2-node')
login_db_name = get_value('login_db_name', 'login_db_name', '')
login_db_server_id = get_value('login_db_server_id', 'login_db_server_id', '900')
login_db_host = get_value('login_db_host', 'login_db_host', '')
login_db_port = get_value('login_db_port', 'login_db_port', '0')
login_db_user = get_value('login_db_user', 'login_db_user', '')
login_db_pass = get_value('login_db_pass', 'login_db_pass', '')
r25_path = get_value('r25_path', 'r25_path', '')
terminal_launch_mode = (tool_cfg.get('terminal_launch_mode', '') or 'auto').strip().lower()
terminal_exe_path = tool_cfg.get('terminal_exe_path', '')
if not terminal_exe_path:
legacy_exe = tool_cfg.get('terminal_custom_command', '')
legacy_cwd = tool_cfg.get('terminal_custom_cwd', '')
if legacy_exe and legacy_cwd and not any(sep in legacy_exe for sep in ('/', '\\')):
legacy_path = Path(os.path.expandvars(os.path.expanduser(legacy_cwd))) / legacy_exe
if legacy_path.is_file():
terminal_exe_path = str(legacy_path.resolve())
if not terminal_exe_path:
terminal_exe_path = legacy_exe
# 服务器基础配置
prefix = get_value('prefix', 'prefix', 'ddxq2')
default_server_id = get_value('default_server_id', 'server_id', '1')
server_type = get_value('server_type', 'server_type', 'game_server')
server_name = get_value('server_name', 'server_name', '')
local_ip = get_local_ip()
login_node = get_value('login_node', 'login_node', '')
center_node = get_value('center_node', 'center_node', '')
config_data = {
"workspace": {
"server_root": server_root,
"run_dir": run_dir,
},
"database": {
"host": db_host,
"user": db_user,
"password": db_pass,
"port": int(db_port) if db_port else 3306,
},
"server": {
"prefix": prefix,
"login_server_node": login_node,
"center_server_node": center_node,
"default_server_id": default_server_id,
"server_type": server_type,
"server_name": server_name,
"ip": local_ip,
"game_host": local_ip,
"cookie": cookie,
},
"erlang": {
"r25_path": r25_path,
"erl": "erl",
"werl": "werl",
"escript": "escript"
},
"login_db": {
"name": login_db_name,
"server_id": int(login_db_server_id) if login_db_server_id else 900,
"host": login_db_host,
"port": int(login_db_port) if login_db_port else 0,
"user": login_db_user,
"password": login_db_pass,
},
"commands": {
"rebar": REBAR3_CMD,
"svn": "svn"
},
"terminal": {
"launch_mode": terminal_launch_mode,
"exe_path": terminal_exe_path,
},
}
# 每次启动都检查配置完整性,从 default.kv 补充缺失的值
need_save = is_first_run
if default_kv:
# 检查是否有从 default.kv 补充的值
fields_to_check = [
('db_host', db_host),
('db_user', db_user),
('db_pass', db_pass),
('prefix', prefix),
('login_node', login_node),
('center_node', center_node),
('cookie', cookie),
]
for field_name, new_value in fields_to_check:
old_value = tool_cfg.get(field_name, '')
if not old_value and new_value:
need_save = True
break
if need_save:
self.data = config_data
self.save()
return config_data
def has_default_kv(self) -> bool:
"""检查 default.kv 是否存在"""
server_root = self.get('workspace', 'server_root', default='')
if not server_root:
return False
default_kv_path = get_server_manager_config_dir(server_root) / 'default.kv'
return default_kv_path.exists()
def is_config_complete(self) -> bool:
"""检查工具配置是否完整"""
# 检查必要的配置项
db_host = self.get('database', 'host', default='')
db_user = self.get('database', 'user', default='')
prefix = self.get('server', 'prefix', default='')
return bool(db_host and db_user and prefix)
def save(self):
"""保存配置到 tool.config"""
if self.tool_config_path is None:
return
local_ip = get_local_ip()
self.set(local_ip, 'server', 'ip')
self.set(local_ip, 'server', 'game_host')
tool_cfg = {
'server_root': self.get('workspace', 'server_root', default=''),
'run_dir': self.get('workspace', 'run_dir', default=''),
'r25_path': self.get('erlang', 'r25_path', default=''),
'db_host': self.get('database', 'host', default=''),
'db_port': str(self.get('database', 'port', default=3306)),
'db_user': self.get('database', 'user', default=''),
'db_pass': self.get('database', 'password', default=''),
'login_db_name': self.get('login_db', 'name', default=''),
'login_db_server_id': str(self.get('login_db', 'server_id', default=900)),
'login_db_host': self.get('login_db', 'host', default=''),
'login_db_port': str(self.get('login_db', 'port', default=0)),
'login_db_user': self.get('login_db', 'user', default=''),
'login_db_pass': self.get('login_db', 'password', default=''),
'prefix': self.get('server', 'prefix', default='ddxq2'),
'default_server_id': self.get('server', 'default_server_id', default='1'),
'server_type': self.get('server', 'server_type', default='game_server'),
'server_name': self.get('server', 'server_name', default=''),
'login_node': self.get('server', 'login_server_node', default=''),
'center_node': self.get('server', 'center_server_node', default=''),
'cookie': self.get('server', 'cookie', default='ddxq2-node'),
'terminal_launch_mode': self.get('terminal', 'launch_mode', default='auto'),
'terminal_exe_path': self.get('terminal', 'exe_path', default=''),
}
self._save_tool_config(tool_cfg)
def get(self, *keys, default=None):
"""获取配置项"""
result = self.data
for key in keys:
if isinstance(result, dict) and key in result:
result = result[key]
else:
return default
return result
def set(self, value, *keys):
"""设置配置项"""
target = self.data
for key in keys[:-1]:
if key not in target:
target[key] = {}
target = target[key]
target[keys[-1]] = value
def is_configured(self) -> bool:
"""检查必要配置是否已设置"""
server_root = self.get('workspace', 'server_root', default='')
return bool(server_root and Path(server_root).exists())
def get_missing_config(self) -> list:
"""获取缺失的必要配置项"""
missing = []
server_root = self.get('workspace', 'server_root', default='')
if not server_root:
missing.append("服务器根目录 (server_root)")
elif not Path(server_root).exists():
missing.append(f"服务器根目录不存在: {server_root}")
return missing
def apply_terminal_launch_config(config: Config) -> None:
"""Apply project terminal launch settings to the process environment."""
mode = (config.get('terminal', 'launch_mode', default='auto') or 'auto').strip().lower() or 'auto'
terminal_exe = (config.get('terminal', 'exe_path', default='') or '').strip()
os.environ['SERVER_MANAGER_TERMINAL'] = mode
if terminal_exe:
os.environ['SERVER_MANAGER_TERMINAL_EXE'] = terminal_exe
else:
os.environ.pop('SERVER_MANAGER_TERMINAL_EXE', None)
os.environ.pop('SERVER_MANAGER_TERMINAL_COMMAND', None)
os.environ.pop('SERVER_MANAGER_TERMINAL_CWD', None)