feat(lsp): 支持 ping 并宽容未知协议方法

健康检查在 initialize 前即可响应;未知请求记录 stderr 后返回 null,未知通知记录后忽略,避免插件因 MethodNotFound 判定服务端故障。
This commit is contained in:
TermiNexus committed 2026-10-26 16:31:52 +08:00
1 parent 1b21ef14d5
commit 4aba50d60e
2 files changed
+101 -8

No files matched your search

+74 -5
View File
@@ -247,6 +247,59 @@ def _HandlePosition(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
return LspRpc.WriteResponse(pool, tr, id_node, result)
def _IsPingMethod(method: str) -> int:
"""Health checks used by plugins; answered before initialize so a probe can never block startup."""
if method is None:
return 0
if string.strcmp(method, "$/ping") == 0:
return 1
if string.strcmp(method, "ping") == 0:
return 1
if string.strcmp(method, "viper/ping") == 0:
return 1
if string.strcmp(method, "viper/pingCompiler") == 0:
return 1
if string.strcmp(method, "$/health") == 0:
return 1
if string.strcmp(method, "$/healthcheck") == 0:
return 1
if string.strcmp(method, "health") == 0:
return 1
return 0
def _IsQuietNotification(method: str) -> int:
"""Client notifications that this server accepts without action and without logging."""
if method is None:
return 0
if string.strcmp(method, "$/cancelRequest") == 0:
return 1
if string.strcmp(method, "$/setTrace") == 0:
return 1
if string.strcmp(method, "window/workDoneProgress/cancel") == 0:
return 1
if string.strcmp(method, "workspace/didChangeConfiguration") == 0:
return 1
if string.strcmp(method, "workspace/didChangeWatchedFiles") == 0:
return 1
if string.strcmp(method, "textDocument/willSave") == 0:
return 1
return 0
def _WritePingResult(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
id_node: json.JsonValue*) -> int:
if id_node is None:
return 0
result: json.JsonValue* = json.object(pool)
if result is None:
return -1
json.object_set(pool, result, "status", json.string_val(pool, "ok"))
json.object_set(pool, result, "server", json.string_val(pool, LSP_SERVER_NAME))
json.object_set(pool, result, "version", json.string_val(pool, LSP_SERVER_VERSION))
return LspRpc.WriteResponse(pool, tr, id_node, result)
def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int:
os._mbuddy = perm_pool
os._win32._mbuddy = perm_pool
@@ -301,7 +354,9 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int:
params: json.JsonValue* = msg["params"]
rc: int = 0
if string.strcmp(method, LspProtocol.LSP_METHOD_INITIALIZE) == 0:
if _IsPingMethod(method) != 0:
rc = _WritePingResult(pool, tr, id_node)
elif string.strcmp(method, LspProtocol.LSP_METHOD_INITIALIZE) == 0:
rc = _HandleInitialize(pool, tr, ws, id_node, params)
elif string.strcmp(method, LspProtocol.LSP_METHOD_INITIALIZED) == 0:
pass
@@ -322,6 +377,12 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int:
rc = LspNavigation.WorkspaceSymbol(pool, tr, docs, ws, id_node, params)
elif string.strcmp(method, "textDocument/definition") == 0:
rc = LspNavigation.Definition(pool, tr, docs, ws, id_node, params)
elif string.strcmp(method, "textDocument/declaration") == 0:
rc = LspNavigation.Definition(pool, tr, docs, ws, id_node, params)
elif string.strcmp(method, "textDocument/typeDefinition") == 0:
rc = LspNavigation.Definition(pool, tr, docs, ws, id_node, params)
elif string.strcmp(method, "textDocument/implementation") == 0:
rc = LspNavigation.Definition(pool, tr, docs, ws, id_node, params)
elif string.strcmp(method, "textDocument/references") == 0:
rc = LspNavigation.References(pool, tr, docs, ws, id_node, params)
elif string.strcmp(method, "textDocument/documentHighlight") == 0:
@@ -339,11 +400,19 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int:
rc = LspRpc.WriteResponse(pool, tr, id_node, json.null(pool))
elif string.strcmp(method, LspProtocol.LSP_METHOD_EXIT) == 0:
return 0 if shutdown_requested != 0 else 1
elif _IsQuietNotification(method) != 0:
pass
elif id_node is not None:
# LSP clients treat MethodNotFound as a broken server. Unsupported features reply with a
# valid empty result and are reported on stderr instead, so the editor keeps working.
LspTransport.StderrWrite("viperc-lsp: unsupported request '")
LspTransport.StderrWrite(method)
LspTransport.StderrWrite("' (answered with null)\n")
rc = LspRpc.WriteResponse(pool, tr, id_node, json.null(pool))
else:
if id_node is not None:
rc = LspRpc.WriteError(pool, tr, id_node,
LspProtocol.LSP_ERROR_METHOD_NOT_FOUND,
"Method not found")
LspTransport.StderrWrite("viperc-lsp: ignored notification '")
LspTransport.StderrWrite(method)
LspTransport.StderrWrite("'\n")
if rc != 0:
return 1
+27 -3
View File
@@ -121,6 +121,15 @@ class Client:
def lifecycle(exe: str) -> int:
client = Client(exe)
# A health probe must answer before initialize; plugins use it to confirm the server is alive.
for method in ("$/ping", "viper/ping", "ping"):
ping = client.request(method, {})
ping_result = ping.get("result")
if not isinstance(ping_result, dict) or ping_result.get("status") != "ok":
raise AssertionError(f"{method} did not return an ok result: {ping}")
if ping_result.get("server") != "viperc-lsp":
raise AssertionError(f"{method} returned the wrong server name: {ping}")
response = client.request("initialize", {"processId": os.getpid(), "capabilities": {}})
result = response.get("result")
if not isinstance(result, dict):
@@ -134,10 +143,24 @@ def lifecycle(exe: str) -> int:
client.notify("initialized", {})
# Unsupported features must not surface as protocol errors to the client.
unknown = client.request("unknown/method", {})
error = unknown.get("error")
if not isinstance(error, dict) or error.get("code") != -32601:
raise AssertionError(f"unknown method did not return MethodNotFound: {unknown}")
if "error" in unknown:
raise AssertionError(f"unknown method returned an error: {unknown}")
if "result" not in unknown or unknown.get("result") is not None:
raise AssertionError(f"unknown method did not return a null result: {unknown}")
unsupported = client.request(
"textDocument/completion",
{"textDocument": {"uri": "file:///unsupported.vp"}, "position": {"line": 0, "character": 0}},
)
if "error" in unsupported or unsupported.get("result") is not None:
raise AssertionError(f"unsupported feature should answer null: {unsupported}")
client.notify("unknown/notification", {})
ping = client.request("$/ping", {})
if ping.get("result", {}).get("status") != "ok":
raise AssertionError(f"server stopped answering after an unknown notification: {ping}")
shutdown = client.request("shutdown")
if shutdown.get("result") is not None:
@@ -147,6 +170,7 @@ def lifecycle(exe: str) -> int:
if code != 0:
raise AssertionError(f"exit code is {code}, expected 0")
print("lifecycle: OK")
print("ping: OK")
return 0