仓库初始化
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Run ``python -m mcp`` for the Server Manager MCP stdio server."""
|
||||
|
||||
from mcp.server import run_stdio_server
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_stdio_server()
|
||||
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Minimal MCP stdio server for Server Manager.
|
||||
|
||||
The transport is newline-delimited JSON-RPC over stdin/stdout. Keep stdout
|
||||
strictly reserved for MCP messages; diagnostics go to stderr.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from mcp.tools import ServerManagerToolbox
|
||||
|
||||
|
||||
PROTOCOL_VERSION = "2025-11-25"
|
||||
SERVER_NAME = "server-manager"
|
||||
SERVER_VERSION = "1.0.0"
|
||||
|
||||
|
||||
JsonDict = Dict[str, Any]
|
||||
|
||||
|
||||
class McpStdioServer:
|
||||
def __init__(self, server_root: Optional[str] = None, config_path: Optional[str] = None,
|
||||
cookie: Optional[str] = None):
|
||||
self.toolbox = ServerManagerToolbox(server_root=server_root, config_path=config_path, cookie=cookie)
|
||||
self._tools = self.toolbox.tool_map()
|
||||
|
||||
def serve_forever(self) -> None:
|
||||
for raw_line in sys.stdin:
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
message = json.loads(line)
|
||||
response = self.handle_message(message)
|
||||
except Exception as e:
|
||||
response = self._error(None, -32700, f"Parse error: {e}")
|
||||
if response is not None:
|
||||
self._send(response)
|
||||
|
||||
def handle_message(self, message: JsonDict) -> Optional[JsonDict]:
|
||||
if not isinstance(message, dict):
|
||||
return self._error(None, -32600, "Invalid Request")
|
||||
request_id = message.get("id")
|
||||
method = message.get("method")
|
||||
if not method:
|
||||
return self._error(request_id, -32600, "Invalid Request")
|
||||
|
||||
# Notifications do not receive responses.
|
||||
if request_id is None:
|
||||
if method in {
|
||||
"notifications/initialized",
|
||||
"notifications/cancelled",
|
||||
"notifications/progress",
|
||||
}:
|
||||
return None
|
||||
return None
|
||||
|
||||
params = message.get("params") or {}
|
||||
try:
|
||||
if method == "initialize":
|
||||
return self._result(request_id, self._initialize_result(params))
|
||||
if method == "ping":
|
||||
return self._result(request_id, {})
|
||||
if method == "tools/list":
|
||||
return self._result(request_id, {
|
||||
"tools": [tool.to_mcp_tool() for tool in self._tools.values()],
|
||||
})
|
||||
if method == "tools/call":
|
||||
return self._result(request_id, self._call_tool(params))
|
||||
return self._error(request_id, -32601, f"Method not found: {method}")
|
||||
except Exception as e:
|
||||
return self._error(request_id, -32603, str(e))
|
||||
|
||||
def _initialize_result(self, params: JsonDict) -> JsonDict:
|
||||
requested = str(params.get("protocolVersion") or PROTOCOL_VERSION)
|
||||
protocol = requested if requested <= PROTOCOL_VERSION else PROTOCOL_VERSION
|
||||
return {
|
||||
"protocolVersion": protocol,
|
||||
"capabilities": {
|
||||
"tools": {"listChanged": False},
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": SERVER_NAME,
|
||||
"version": SERVER_VERSION,
|
||||
},
|
||||
"instructions": (
|
||||
"Use the server_manager_* tools to inspect and operate local Server Manager "
|
||||
"projects. Operations that start/stop/create servers can modify local state."
|
||||
),
|
||||
}
|
||||
|
||||
def _call_tool(self, params: JsonDict) -> JsonDict:
|
||||
name = params.get("name")
|
||||
if not isinstance(name, str) or not name:
|
||||
return {
|
||||
"content": [{"type": "text", "text": "tools/call requires params.name"}],
|
||||
"isError": True,
|
||||
}
|
||||
arguments = params.get("arguments") or {}
|
||||
if not isinstance(arguments, dict):
|
||||
return {
|
||||
"content": [{"type": "text", "text": "tools/call params.arguments must be an object"}],
|
||||
"isError": True,
|
||||
}
|
||||
return self.toolbox.call_tool(name, arguments)
|
||||
|
||||
def _send(self, response: JsonDict) -> None:
|
||||
sys.stdout.write(json.dumps(response, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
def _result(self, request_id: Any, result: JsonDict) -> JsonDict:
|
||||
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
||||
|
||||
def _error(self, request_id: Any, code: int, message: str) -> JsonDict:
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {"code": code, "message": message},
|
||||
}
|
||||
|
||||
|
||||
def run_stdio_server(server_root: Optional[str] = None, config_path: Optional[str] = None,
|
||||
cookie: Optional[str] = None) -> None:
|
||||
McpStdioServer(server_root=server_root, config_path=config_path, cookie=cookie).serve_forever()
|
||||
@@ -0,0 +1,502 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""MCP tool definitions backed by Server Manager services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from entrypoints.cli.manager import ServerManagerCLI
|
||||
from services.commands import (
|
||||
check_nodes_status,
|
||||
flatten_server_dirs,
|
||||
generate_start_config,
|
||||
get_server_list,
|
||||
read_merged_config,
|
||||
read_text_file_best_effort,
|
||||
resolve_log_file_path,
|
||||
REBAR3_CMD,
|
||||
)
|
||||
from services.rg_search import rg_search_directory
|
||||
|
||||
|
||||
JsonDict = Dict[str, Any]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolDefinition:
|
||||
name: str
|
||||
title: str
|
||||
description: str
|
||||
input_schema: JsonDict
|
||||
handler: Callable[[JsonDict], JsonDict]
|
||||
annotations: Optional[JsonDict] = None
|
||||
|
||||
def to_mcp_tool(self) -> JsonDict:
|
||||
data: JsonDict = {
|
||||
"name": self.name,
|
||||
"title": self.title,
|
||||
"description": self.description,
|
||||
"inputSchema": self.input_schema,
|
||||
}
|
||||
if self.annotations:
|
||||
data["annotations"] = self.annotations
|
||||
return data
|
||||
|
||||
|
||||
class ToolError(Exception):
|
||||
"""Expected tool failure returned as an MCP tool error."""
|
||||
|
||||
|
||||
def _schema(properties: JsonDict, required: Optional[List[str]] = None) -> JsonDict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required or [],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
def _text_result(data: Any, *, is_error: bool = False) -> JsonDict:
|
||||
text = data if isinstance(data, str) else json.dumps(data, ensure_ascii=False, indent=2)
|
||||
result: JsonDict = {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
"isError": is_error,
|
||||
}
|
||||
if not is_error and not isinstance(data, str):
|
||||
result["structuredContent"] = data
|
||||
return result
|
||||
|
||||
|
||||
def _mask_sensitive(config: JsonDict) -> JsonDict:
|
||||
masked = dict(config)
|
||||
for key in list(masked):
|
||||
lowered = key.lower()
|
||||
if "pass" in lowered or "password" in lowered or "secret" in lowered:
|
||||
masked[key] = "***"
|
||||
return masked
|
||||
|
||||
|
||||
def _truncate(text: str, max_chars: int) -> str:
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
return text[:max_chars] + f"\n... <truncated {len(text) - max_chars} chars>"
|
||||
|
||||
|
||||
class ServerManagerToolbox:
|
||||
"""Server Manager operations exposed to MCP tools."""
|
||||
|
||||
def __init__(self, server_root: Optional[str] = None, config_path: Optional[str] = None,
|
||||
cookie: Optional[str] = None):
|
||||
self.server_root = server_root
|
||||
self.config_path = config_path
|
||||
self.cookie = cookie
|
||||
|
||||
def list_tools(self) -> List[ToolDefinition]:
|
||||
return [
|
||||
ToolDefinition(
|
||||
name="server_manager_list_servers",
|
||||
title="List Servers",
|
||||
description="List server directories grouped by type. Optionally include online status.",
|
||||
input_schema=_schema({
|
||||
"server_root": {"type": "string", "description": "Optional server project root override."},
|
||||
"config_path": {"type": "string", "description": "Optional config file override."},
|
||||
"include_status": {"type": "boolean", "description": "Whether to check Erlang node status."},
|
||||
"target_ip": {"type": "string", "description": "Optional target host/IP for status checks."},
|
||||
}),
|
||||
handler=self.list_servers,
|
||||
annotations={"readOnlyHint": True, "destructiveHint": False},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_check_status",
|
||||
title="Check Node Status",
|
||||
description="Check whether one or more Erlang server nodes are online.",
|
||||
input_schema=_schema({
|
||||
"servers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Server directory or node names. If omitted, all discovered servers are checked.",
|
||||
},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
"target_ip": {"type": "string"},
|
||||
"cookie": {"type": "string"},
|
||||
}),
|
||||
handler=self.check_status,
|
||||
annotations={"readOnlyHint": True, "destructiveHint": False},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_read_config",
|
||||
title="Read Config",
|
||||
description="Read merged Server Manager config, optionally including a specific server kv.config.",
|
||||
input_schema=_schema({
|
||||
"server": {"type": "string", "description": "Optional server directory name."},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
"include_sensitive": {
|
||||
"type": "boolean",
|
||||
"description": "Return passwords/secrets instead of masking them. Defaults to false.",
|
||||
},
|
||||
}),
|
||||
handler=self.read_config,
|
||||
annotations={"readOnlyHint": True, "destructiveHint": False},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_create_server",
|
||||
title="Create Server",
|
||||
description="Create a game/login/center/cross/client server from project templates.",
|
||||
input_schema=_schema({
|
||||
"server_type": {
|
||||
"type": "string",
|
||||
"enum": ["game", "login", "center", "cross", "client"],
|
||||
},
|
||||
"server_id": {"type": "integer"},
|
||||
"prefix": {"type": "string"},
|
||||
"db_host": {"type": "string"},
|
||||
"db_user": {"type": "string"},
|
||||
"db_pass": {"type": "string"},
|
||||
"db_port": {"type": "integer"},
|
||||
"login_node": {"type": "string"},
|
||||
"center_node": {"type": "string"},
|
||||
"server_name": {"type": "string"},
|
||||
"tcp_port": {"type": "integer"},
|
||||
"http_port": {"type": "integer"},
|
||||
"open_time": {"type": "string"},
|
||||
"overwrite": {"type": "boolean"},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
}, required=["server_type", "server_id"]),
|
||||
handler=self.create_server,
|
||||
annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_regenerate_config",
|
||||
title="Regenerate Sys Config",
|
||||
description="Regenerate sys.config for a server from default/tool/kv config.",
|
||||
input_schema=_schema({
|
||||
"server": {"type": "string"},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
}, required=["server"]),
|
||||
handler=self.regenerate_config,
|
||||
annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": True},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_start_server",
|
||||
title="Start Server",
|
||||
description="Start a server in a background terminal/window. Foreground mode is not exposed over MCP.",
|
||||
input_schema=_schema({
|
||||
"server": {"type": "string"},
|
||||
"quick": {"type": "boolean", "description": "Use direct erl quick start instead of rebar3 shell."},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
}, required=["server"]),
|
||||
handler=self.start_server,
|
||||
annotations={"readOnlyHint": False, "destructiveHint": False, "openWorldHint": True},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_stop_server",
|
||||
title="Stop Server",
|
||||
description="Stop a running Erlang server node.",
|
||||
input_schema=_schema({
|
||||
"server": {"type": "string"},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
}, required=["server"]),
|
||||
handler=self.stop_server,
|
||||
annotations={"readOnlyHint": False, "destructiveHint": True, "idempotentHint": True},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_compile",
|
||||
title="Compile Server",
|
||||
description="Run a rebar3 compile command and return captured output.",
|
||||
input_schema=_schema({
|
||||
"target": {
|
||||
"type": "string",
|
||||
"enum": ["all", "code", "proto", "table", "tbllog"],
|
||||
"description": "Compile target. Defaults to all.",
|
||||
},
|
||||
"server_type": {
|
||||
"type": "string",
|
||||
"enum": ["game", "login"],
|
||||
"description": "Compile profile family. Defaults to game.",
|
||||
},
|
||||
"timeout_seconds": {"type": "integer", "description": "Timeout, max 3600. Defaults to 1200."},
|
||||
"max_output_chars": {"type": "integer", "description": "Output truncation size. Defaults to 20000."},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
}),
|
||||
handler=self.compile,
|
||||
annotations={"readOnlyHint": False, "destructiveHint": False, "openWorldHint": True},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_search_logs",
|
||||
title="Search Logs",
|
||||
description="Search a server log directory using ripgrep when available.",
|
||||
input_schema=_schema({
|
||||
"server": {"type": "string"},
|
||||
"pattern": {"type": "string"},
|
||||
"case_sensitive": {"type": "boolean"},
|
||||
"use_regex": {"type": "boolean"},
|
||||
"whole_word": {"type": "boolean"},
|
||||
"glob": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Optional rg glob filters, e.g. ['*.log*'].",
|
||||
},
|
||||
"max_output_chars": {"type": "integer"},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
}, required=["server", "pattern"]),
|
||||
handler=self.search_logs,
|
||||
annotations={"readOnlyHint": True, "destructiveHint": False},
|
||||
),
|
||||
ToolDefinition(
|
||||
name="server_manager_read_log",
|
||||
title="Read Log",
|
||||
description="Read a specific log file from a server log directory with a byte limit.",
|
||||
input_schema=_schema({
|
||||
"server": {"type": "string"},
|
||||
"base_filename": {
|
||||
"type": "string",
|
||||
"description": "Base log file name, e.g. error.log or info.log.",
|
||||
},
|
||||
"date_yyyymmdd": {"type": "string", "description": "Optional date suffix, e.g. 20260516."},
|
||||
"subdir": {"type": "string", "description": "Optional subdir under log, e.g. tag_log."},
|
||||
"max_bytes": {"type": "integer", "description": "Read limit. Defaults to 1048576."},
|
||||
"server_root": {"type": "string"},
|
||||
"config_path": {"type": "string"},
|
||||
}, required=["server", "base_filename"]),
|
||||
handler=self.read_log,
|
||||
annotations={"readOnlyHint": True, "destructiveHint": False},
|
||||
),
|
||||
]
|
||||
|
||||
def tool_map(self) -> Dict[str, ToolDefinition]:
|
||||
return {tool.name: tool for tool in self.list_tools()}
|
||||
|
||||
def call_tool(self, name: str, arguments: Optional[JsonDict]) -> JsonDict:
|
||||
tool = self.tool_map().get(name)
|
||||
if not tool:
|
||||
return _text_result(f"未知工具: {name}", is_error=True)
|
||||
try:
|
||||
payload = tool.handler(dict(arguments or {}))
|
||||
return _text_result(payload)
|
||||
except ToolError as e:
|
||||
return _text_result(str(e), is_error=True)
|
||||
except Exception:
|
||||
return _text_result(traceback.format_exc(), is_error=True)
|
||||
|
||||
def _arg(self, arguments: JsonDict, key: str, default: Any = None) -> Any:
|
||||
value = arguments.get(key, default)
|
||||
if value is None:
|
||||
return default
|
||||
return value
|
||||
|
||||
def _make_cli(self, arguments: JsonDict) -> tuple[ServerManagerCLI, str]:
|
||||
server_root = self._arg(arguments, "server_root", self.server_root)
|
||||
config_path = self._arg(arguments, "config_path", self.config_path)
|
||||
cookie = self._arg(arguments, "cookie", self.cookie)
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
cli = ServerManagerCLI(server_root=server_root, config_path=config_path, quiet=True)
|
||||
if cookie:
|
||||
cli.cookie = str(cookie)
|
||||
return cli, buf.getvalue()
|
||||
|
||||
def _capture_cli_call(self, cli: ServerManagerCLI, func: Callable[[], Any]) -> tuple[Any, str]:
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
result = func()
|
||||
return result, buf.getvalue()
|
||||
|
||||
def list_servers(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
servers = get_server_list(cli.run_dir)
|
||||
status = None
|
||||
if bool(arguments.get("include_status", False)):
|
||||
names = flatten_server_dirs(cli.run_dir)
|
||||
status = check_nodes_status(
|
||||
names,
|
||||
cli.cookie,
|
||||
target_ip=arguments.get("target_ip"),
|
||||
erl_path=cli.config.get("erl_path"),
|
||||
)
|
||||
return {
|
||||
"server_root": cli.server_root,
|
||||
"run_dir": cli.run_dir,
|
||||
"servers": servers,
|
||||
"status": status,
|
||||
"output": init_output.strip(),
|
||||
}
|
||||
|
||||
def check_status(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
servers = arguments.get("servers") or flatten_server_dirs(cli.run_dir)
|
||||
if not isinstance(servers, list):
|
||||
raise ToolError("servers 必须是字符串数组")
|
||||
results = check_nodes_status(
|
||||
[str(item) for item in servers],
|
||||
str(arguments.get("cookie") or cli.cookie),
|
||||
target_ip=arguments.get("target_ip"),
|
||||
erl_path=cli.config.get("erl_path"),
|
||||
)
|
||||
return {
|
||||
"server_root": cli.server_root,
|
||||
"run_dir": cli.run_dir,
|
||||
"status": results,
|
||||
"output": init_output.strip(),
|
||||
}
|
||||
|
||||
def read_config(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
server = arguments.get("server")
|
||||
config = read_merged_config(cli.server_root, server_dir=server, run_dir=cli.run_dir)
|
||||
if not bool(arguments.get("include_sensitive", False)):
|
||||
config = _mask_sensitive(config)
|
||||
return {
|
||||
"server_root": cli.server_root,
|
||||
"run_dir": cli.run_dir,
|
||||
"server": server,
|
||||
"config": config,
|
||||
"output": init_output.strip(),
|
||||
}
|
||||
|
||||
def create_server(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
success, output = self._capture_cli_call(
|
||||
cli,
|
||||
lambda: cli.create_server_cmd(
|
||||
server_type=str(arguments["server_type"]),
|
||||
server_id=int(arguments["server_id"]),
|
||||
prefix=arguments.get("prefix"),
|
||||
db_host=arguments.get("db_host"),
|
||||
db_user=arguments.get("db_user"),
|
||||
db_pass=arguments.get("db_pass"),
|
||||
db_port=arguments.get("db_port"),
|
||||
login_node=arguments.get("login_node"),
|
||||
center_node=arguments.get("center_node"),
|
||||
server_name=arguments.get("server_name"),
|
||||
tcp_port=arguments.get("tcp_port"),
|
||||
http_port=arguments.get("http_port"),
|
||||
open_time=arguments.get("open_time"),
|
||||
overwrite=bool(arguments.get("overwrite", False)),
|
||||
),
|
||||
)
|
||||
return {"success": bool(success), "output": (init_output + output).strip()}
|
||||
|
||||
def regenerate_config(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
server = str(arguments["server"])
|
||||
success, output = self._capture_cli_call(cli, lambda: cli.regenerate_config(server))
|
||||
return {"success": bool(success), "server": server, "output": (init_output + output).strip()}
|
||||
|
||||
def start_server(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
server = str(arguments["server"])
|
||||
quick = bool(arguments.get("quick", False))
|
||||
success, output = self._capture_cli_call(
|
||||
cli,
|
||||
lambda: cli.start_server(server, use_rebar=not quick, foreground=False),
|
||||
)
|
||||
return {
|
||||
"success": bool(success),
|
||||
"server": server,
|
||||
"quick": quick,
|
||||
"output": (init_output + output).strip(),
|
||||
}
|
||||
|
||||
def stop_server(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
server = str(arguments["server"])
|
||||
success, output = self._capture_cli_call(cli, lambda: cli.stop_server(server))
|
||||
return {"success": bool(success), "server": server, "output": (init_output + output).strip()}
|
||||
|
||||
def compile(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
target = str(arguments.get("target") or "all")
|
||||
server_type = str(arguments.get("server_type") or "game")
|
||||
timeout = min(max(int(arguments.get("timeout_seconds") or 1200), 1), 3600)
|
||||
max_chars = min(max(int(arguments.get("max_output_chars") or 20000), 1000), 200000)
|
||||
profile = "login_server_dev" if server_type == "login" else "game_server_dev"
|
||||
cmd_map = {
|
||||
"all": f"{REBAR3_CMD} as {profile} compile",
|
||||
"code": f"{REBAR3_CMD} as {profile} compile",
|
||||
"proto": f"{REBAR3_CMD} protobuf compile",
|
||||
"table": f"{REBAR3_CMD} cache compile",
|
||||
"tbllog": f"{REBAR3_CMD} tbllog compile",
|
||||
}
|
||||
cmd = cmd_map.get(target)
|
||||
if not cmd:
|
||||
raise ToolError(f"未知编译目标: {target}")
|
||||
env = os.environ.copy()
|
||||
env["ESCRIPT_EMULATOR"] = "erl"
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
shell=True,
|
||||
cwd=cli.server_root,
|
||||
env=env,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=timeout,
|
||||
)
|
||||
return {
|
||||
"success": proc.returncode == 0,
|
||||
"return_code": proc.returncode,
|
||||
"command": cmd,
|
||||
"server_root": cli.server_root,
|
||||
"output": _truncate((init_output + "\n" + (proc.stdout or "")).strip(), max_chars),
|
||||
}
|
||||
|
||||
def search_logs(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
server = str(arguments["server"])
|
||||
search_root = Path(cli.run_dir) / server / "log"
|
||||
max_chars = min(max(int(arguments.get("max_output_chars") or 20000), 1000), 200000)
|
||||
out, err = rg_search_directory(
|
||||
search_root=search_root,
|
||||
pattern=str(arguments["pattern"]),
|
||||
case_sensitive=bool(arguments.get("case_sensitive", False)),
|
||||
use_regex=bool(arguments.get("use_regex", False)),
|
||||
whole_word=bool(arguments.get("whole_word", False)),
|
||||
glob_globs=arguments.get("glob"),
|
||||
)
|
||||
if err:
|
||||
raise ToolError(err)
|
||||
return {
|
||||
"server": server,
|
||||
"search_root": str(search_root),
|
||||
"matches": _truncate(out, max_chars),
|
||||
"output": init_output.strip(),
|
||||
}
|
||||
|
||||
def read_log(self, arguments: JsonDict) -> JsonDict:
|
||||
cli, init_output = self._make_cli(arguments)
|
||||
server = str(arguments["server"])
|
||||
max_bytes = min(max(int(arguments.get("max_bytes") or 1048576), 1024), 10 * 1024 * 1024)
|
||||
path = resolve_log_file_path(
|
||||
cli.run_dir,
|
||||
server,
|
||||
str(arguments["base_filename"]),
|
||||
date_yyyymmdd=arguments.get("date_yyyymmdd"),
|
||||
subdir=arguments.get("subdir") or "",
|
||||
)
|
||||
content, err = read_text_file_best_effort(path, max_bytes=max_bytes)
|
||||
if err:
|
||||
raise ToolError(err)
|
||||
return {
|
||||
"server": server,
|
||||
"path": str(path),
|
||||
"content": content,
|
||||
"output": init_output.strip(),
|
||||
}
|
||||
Reference in New Issue
Block a user