仓库初始化

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
+385
View File
@@ -0,0 +1,385 @@
# -*- coding: utf-8 -*-
"""Remote server database/RPC queries and role RPC helpers."""
from .shared import *
from .erlang import _ensure_epmd_daemon, get_erl_cmd, get_local_ip
def query_remote_servers(db_host: str, db_port: int, db_user: str, db_pass: str,
db_name: str) -> List[Dict[str, str]]:
"""从登录服数据库查询远程服务器列表
Args:
db_host: 数据库地址
db_port: 数据库端口
db_user: 数据库用户名
db_pass: 数据库密码
db_name: 数据库名(登录服数据库,如 ai002_login_s900
Returns:
服务器列表,每项包含 server_id, server_node, center_node 等信息
"""
try:
import pymysql
except ImportError:
# 尝试使用 mysql-connector
try:
import mysql.connector as pymysql
except ImportError:
raise ImportError("需要安装 pymysql 或 mysql-connector-python: pip install pymysql")
servers = []
try:
# 连接数据库
conn = pymysql.connect(
host=db_host,
port=db_port,
user=db_user,
password=db_pass,
database=db_name,
charset='utf8mb4'
)
cursor = conn.cursor()
# 查询 server_info 表
# 注意: running 字段是运行时字段(sync=false),不存储在数据库中
sql = """
SELECT server_id, server_node, center_node, game_db
FROM server_info
WHERE server_node IS NOT NULL AND server_node != ''
ORDER BY server_id
"""
cursor.execute(sql)
for row in cursor.fetchall():
server_id, server_node, center_node, game_db = row
servers.append({
'server_id': str(server_id),
'server_node': str(server_node) if server_node else '',
'center_node': str(center_node) if center_node else '',
'game_db': str(game_db) if game_db else '',
'running': False # 运行状态需要实时检测,这里默认为 False
})
cursor.close()
conn.close()
except Exception as e:
raise Exception(f"数据库查询失败: {str(e)}")
return servers
def _erl_quoted_atom(node: str) -> str:
"""将节点名转为 Erlang 源码中的单引号原子字面量。"""
n = (node or "").strip()
if not n:
raise ValueError("登录服节点为空")
return "'" + n.replace("\\", "\\\\").replace("'", "\\'") + "'"
# 由 file:script/1 加载。输出 SM_COUNT 行 + 每行「服务器ID\t名称base64\t节点明文」。
# 注意:file:script 生成的匿名 fun 属于 erl_eval,若本地与远程 OTP 版本不一致会 badfun。
# 需确保本地 erl 路径所指版本与登录服 OTP 版本相同。
_FETCH_REMOTE_RPC_SCRIPT = """begin
LN = __LOGIN_ATOM__,
R = rpc:call(LN, erlang, apply, [
fun() ->
ms_cache:tab2list_foldl(server_temp_info,
fun(OneServer, ResultAcc) ->
ServerId = element(2, server_temp_info_c:get_server_id(OneServer)),
ServerName = unicode:characters_to_binary([
element(2, server_temp_info_c:get_server_name(OneServer))
]),
case server_info_lib:get_server_node(ServerId) of
{_, Node} ->
[{ServerId, ServerName, Node} | ResultAcc];
_ ->
ResultAcc
end
end, [])
end, []]),
case R of
{badrpc, Err} ->
io:format(standard_io, "RPC_ERROR: ~p~n", [Err]),
erlang:halt(2, [{flush, true}]);
_ when is_list(R) ->
RowLine = fun({Id, NameBin, Node}) ->
Nb = case NameBin of B when is_binary(B) -> B; _ -> <<>> end,
NameB64 = binary_to_list(base64:encode(Nb)),
NodeStr = case Node of
N when is_atom(N) -> unicode:characters_to_list(atom_to_binary(N, utf8));
N when is_list(N) -> N;
N when is_binary(N) -> unicode:characters_to_list(N);
_ -> lists:flatten(io_lib:format("~p", [Node]))
end,
Sid = lists:flatten(io_lib:format("~w", [Id])),
lists:flatten([Sid, $\\t, NameB64, $\\t, NodeStr])
end,
io:format(standard_io, "SM_COUNT\\t~w~n", [length(R)]),
lists:foreach(
fun(Row) ->
io:format(standard_io, "~s~n", [RowLine(Row)])
end, R),
erlang:halt(0, [{flush, true}]);
Other ->
io:format(standard_io, "RPC_ERROR: ~p~n", [Other]),
erlang:halt(3, [{flush, true}])
end
end.
"""
def query_remote_servers_from_login_rpc(
login_node: str,
cookie: str,
erl_path: Optional[str] = None,
timeout: int = 120,
) -> List[Dict[str, Any]]:
"""通过向登录服节点 ``rpc:call`` 获取游戏服列表。
使用 ``erl`` + ``file:script/1`` 执行临时脚本,避免 Windows 超长 ``-eval``。
注意:脚本中的匿名 fun 属于 erl_eval,需要本地 erl 与登录服 OTP 版本一致,
否则远程执行会 badfun。
返回每项含 server_id、server_nameUTF-8 文本)、server_node、running(默认 False)。
Raises:
ValueError: 参数无效
Exception: erl 执行失败、RPC 错误或输出无法解析
"""
ln = _erl_quoted_atom(login_node)
ping_node = f"sm_ls_{int(time.time() * 1000) % 100000}"
cookie_arg = (cookie or "").strip() or "ddxq2-node"
if any(c in cookie_arg for c in " \t\r\n'\""):
raise ValueError("Cookie 不能包含空格或引号(请使用项目设置中的纯文本 cookie)")
erl = get_erl_cmd(erl_path)
_ensure_epmd_daemon(erl_path)
# 与 check_nodes_status 一致:本机节点优先用 get_local_ip(),再试 127.0.0.1
host_parts: List[str] = []
lip = get_local_ip()
if lip:
host_parts.append(lip)
if "127.0.0.1" not in host_parts:
host_parts.append("127.0.0.1")
if not host_parts:
host_parts = ["127.0.0.1"]
def _looks_like_vm_nodistribution(stderr: str, stdout: str) -> bool:
c = (stderr or "") + (stdout or "")
return any(
x in c
for x in (
"nodistribution",
"application_start_failure",
"failed_to_start_child,net_kernel",
"Kernel pid terminated",
)
)
script_body = _FETCH_REMOTE_RPC_SCRIPT.replace("__LOGIN_ATOM__", ln)
result: Optional[Any] = None
for idx, host_part in enumerate(host_parts):
tmp_path: Optional[str] = None
try:
with tempfile.NamedTemporaryFile(
mode="w",
suffix=".erl",
delete=False,
encoding="utf-8",
newline="\n",
) as tf:
tf.write(script_body)
tmp_path = tf.name
path_for_erl = str(Path(tmp_path).resolve()).replace("\\", "/")
if '"' in path_for_erl:
raise ValueError("临时脚本路径含引号,无法传给 Erlang")
# 脚本内已 halt;若脚本本身无法解析,file:script 返回 {error,_}
eval_launch = (
f'case file:script("{path_for_erl}") of '
"{{error, E}} -> io:format(\"SCRIPT_ERROR: ~p~n\", [E]), halt(1); "
"_ -> halt(0) end."
)
result = subprocess.run(
[
erl,
"-noshell",
"-name", f"{ping_node}@{host_part}",
"-setcookie", cookie_arg,
"-eval", eval_launch,
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if IS_WINDOWS else 0,
)
except subprocess.TimeoutExpired as e:
raise Exception(f"从登录服加载超时({timeout}s: {e}") from e
except FileNotFoundError:
raise Exception(
f"找不到 erl 可执行文件: {erl}"
"请确认已安装 Erlang/OTP,或在项目设置中填写正确的 Erlang 安装路径。"
)
except Exception as e:
raise Exception(f"执行 erl 失败: {e}") from e
finally:
if tmp_path:
try:
os.unlink(tmp_path)
except OSError:
pass
if result is None:
raise Exception("内部错误:未获得 erl 执行结果")
if (
result.returncode != 0
and _looks_like_vm_nodistribution(result.stderr, result.stdout)
and idx < len(host_parts) - 1
):
continue
break
if result is None:
raise Exception("内部错误:未获得 erl 执行结果")
out = (result.stdout or "").strip()
err = (result.stderr or "").strip()
# Windows 下 -noshell 时 io:format 有时落在 stderr;与 stdout 合并后再解析
combined_text = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
rpc_err_lines = [line for line in combined_text.splitlines() if "RPC_ERROR:" in line]
if rpc_err_lines:
raise Exception(f"登录服 RPC 失败: {rpc_err_lines[0]}")
if result.returncode != 0:
raise Exception(f"erl 退出码 {result.returncode}: {(err or out)[:2000]}")
servers: List[Dict[str, Any]] = []
for line in combined_text.splitlines():
line = line.strip()
if not line:
continue
if line.startswith("RPC_ERROR") or line.startswith("SCRIPT_ERROR"):
continue
if line.startswith("SM_COUNT\t"):
continue
# 跳过 Eshell/版本等无关行
if "Eshell" in line or "Erlang/OTP" in line:
continue
parts = line.split("\t", 2)
if len(parts) < 3:
continue
sid_s, name_b64, node_s = parts[0], parts[1], parts[2]
try:
raw = base64.b64decode(name_b64.encode("ascii"))
name_dec = raw.decode("utf-8")
except Exception:
try:
name_dec = base64.b64decode(name_b64.encode("ascii")).decode("utf-8", errors="replace")
except Exception:
name_dec = ""
servers.append(
{
"server_id": str(sid_s).strip(),
"server_name": name_dec,
"server_node": str(node_s).strip(),
"running": False,
}
)
sm_count: Optional[int] = None
for line in combined_text.splitlines():
if line.strip().startswith("SM_COUNT\t"):
try:
sm_count = int(line.strip().split("\t", 1)[1])
except (ValueError, IndexError):
pass
break
if sm_count is not None and sm_count > 0 and len(servers) == 0:
raise Exception(
f"登录服报告 SM_COUNT={sm_count} 条,但未解析出数据行(可能编码或输出分流异常)。"
f" 原始 stdout 前 500 字: {(result.stdout or '')[:500]!r}"
)
return servers
def get_login_db_name(prefix: str, login_server_id: int) -> str:
"""生成登录服数据库名
Args:
prefix: 项目前缀
login_server_id: 登录服ID
Returns:
数据库名,如 ai002_login_s900
"""
return f"{prefix}_login_s{login_server_id}"
def _format_target_node(server_name: str, target_ip: Optional[str]) -> str:
if '@' in server_name:
return server_name
ip = target_ip if target_ip else get_local_ip()
return f"{server_name}@{ip}"
def _argv_to_shell_line(args: List[str]) -> str:
"""将实际传给 subprocess 的参数列表格式化为可复制的命令行字符串。"""
if IS_WINDOWS:
return subprocess.list2cmdline(args)
return shlex.join(args)
def rpc_role_gs_trace_network(
server_name: str,
cookie: str,
role_id: int,
enable: bool,
target_ip: Optional[str] = None,
erl_path: Optional[str] = None,
) -> Tuple[int, str, str, str]:
"""远程调用 ``role_gs:trace_network/1`` 或 ``trace_network_close/1``。
Returns:
(returncode, stdout, stderr, full_command_line)
"""
erl = get_erl_cmd(erl_path)
ip = get_local_ip()
ping_node = f"sm_rpc_{int(time.time() * 1000) % 100000}"
target_node = _format_target_node(server_name, target_ip)
func = "trace_network" if enable else "trace_network_close"
eval_code = (
f"R = rpc:call('{target_node}', role_gs, {func}, [{int(role_id)}]), "
f"io:format(\"~p~n\", [R]), halt(0)."
)
args = [
erl,
"-noshell",
"-name", f"{ping_node}@{ip}",
"-setcookie", cookie,
"-eval", eval_code,
]
cmd_line = _argv_to_shell_line(args)
try:
r = subprocess.run(
args,
capture_output=True,
text=True,
timeout=45,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if IS_WINDOWS else 0,
)
return r.returncode, (r.stdout or ""), (r.stderr or ""), cmd_line
except subprocess.TimeoutExpired:
return -1, "", "RPC 超时", cmd_line
except Exception as e:
return -1, "", str(e), cmd_line