fix(host): _read_loop 跳过非 dict JSON 消息,不再静默死亡
收到合法 JSON 但值不是 dict(如 ""、123、[]、null)时,
旧代码走到 msg.get('params') 抛 AttributeError/TypeError,
被外层 except 吞掉后读线程退出,此后所有 request() 等到超时。
现校验后记 stderr 日志并跳过,读循环继续;脏消息不进
events、不当正常请求处理。畸形 JSON 本已由 json.loads 的
except continue 处理,本次未改动该路径。
回归:tests/test_host_bugfix_host.py ReadLoopNonDictTest(2 项)
This commit is contained in:
1 parent
e5d570c233
commit
714b83903a
2 files changed
+98
No files matched your search
@@ -486,6 +486,20 @@ class Server:
|
|||||||
msg = json.loads(raw)
|
msg = json.loads(raw)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
if not isinstance(msg, dict):
|
||||||
|
# 非 dict 的合法 JSON(如 ""、123、[]、null):
|
||||||
|
# 旧代码会走到下面的 msg.get("params") 抛
|
||||||
|
# AttributeError/TypeError,被外层 except 吞掉
|
||||||
|
# 后读线程退出,此后所有 request() 等到超时。
|
||||||
|
# 脏消息只跳过、不当正常请求处理,读循环继续。
|
||||||
|
try:
|
||||||
|
sys.stderr.write(
|
||||||
|
"host _read_loop: skip non-dict message "
|
||||||
|
f"({type(msg).__name__}): {str(msg)[:200]}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
continue
|
||||||
if "method" in msg and "id" in msg:
|
if "method" in msg and "id" in msg:
|
||||||
self._answer_server_request(msg)
|
self._answer_server_request(msg)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""host.py 覆盖率 bugfix 轨(host 分工)回归测试。
|
||||||
|
|
||||||
|
只覆盖本分工的三项:_read_loop 非 dict 消息、配置/init 的
|
||||||
|
open() 关闭、daemon 循环的 --state 透传。不碰其它轨。
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import queue
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
|
import host
|
||||||
|
|
||||||
|
|
||||||
|
def _make_read_loop_server(lines, pending_id=7):
|
||||||
|
"""不拉起真实 app-server 的 Server:只给 _read_loop 供料。
|
||||||
|
|
||||||
|
proc.stdout 直接用字节行列表;_read_loop 对它做
|
||||||
|
`for raw in self.proc.stdout`,与真实 PIPE 的逐行迭代
|
||||||
|
语义一致。脏消息后仍能送达正常响应,即证明读循环
|
||||||
|
没有在脏消息处提前退出(旧代码会在第一条非 dict 处
|
||||||
|
抛 AttributeError/TypeError,被外层 except 吞掉后退出)。
|
||||||
|
"""
|
||||||
|
srv = host.Server.__new__(host.Server)
|
||||||
|
srv.approval = "deny"
|
||||||
|
srv.pending_lock = threading.Lock()
|
||||||
|
srv.pending = {pending_id: queue.Queue()}
|
||||||
|
srv.events_lock = threading.Lock()
|
||||||
|
srv.events = []
|
||||||
|
srv.turn_queues = {}
|
||||||
|
srv.trace_path = None
|
||||||
|
srv._trace_lines = 0
|
||||||
|
|
||||||
|
class FakeProc:
|
||||||
|
def __init__(self, stdout_lines):
|
||||||
|
self.stdout = stdout_lines
|
||||||
|
|
||||||
|
srv.proc = FakeProc(lines)
|
||||||
|
return srv
|
||||||
|
|
||||||
|
|
||||||
|
class ReadLoopNonDictTest(unittest.TestCase):
|
||||||
|
def test_non_dict_json_then_normal_response(self):
|
||||||
|
lines = [
|
||||||
|
b'""\n', # 合法 JSON,值是 str(旧代码致死)
|
||||||
|
b'"hello"\n', # 合法 JSON,值是 str
|
||||||
|
b'123\n', # 合法 JSON,值是 int
|
||||||
|
b'[1,2]\n', # 合法 JSON,值是 list
|
||||||
|
b'null\n', # 合法 JSON,值是 None
|
||||||
|
b'true\n', # 合法 JSON,值是 bool
|
||||||
|
b'not-json{\n', # 畸形 JSON:旧代码本已 continue
|
||||||
|
b'{"jsonrpc": "2.0", "id": 7, '
|
||||||
|
b'"result": {"ok": true}}\n',
|
||||||
|
]
|
||||||
|
srv = _make_read_loop_server(lines)
|
||||||
|
err = io.StringIO()
|
||||||
|
with contextlib.redirect_stderr(err):
|
||||||
|
srv._read_loop()
|
||||||
|
resp = srv.pending[7].get(timeout=1)
|
||||||
|
self.assertEqual(resp["result"], {"ok": True})
|
||||||
|
# 脏消息没有被当成通知塞进 events,也没有被当成请求处理。
|
||||||
|
self.assertEqual(srv.events, [])
|
||||||
|
self.assertIn("skip non-dict message", err.getvalue())
|
||||||
|
|
||||||
|
def test_non_dict_json_then_normal_notification(self):
|
||||||
|
lines = [
|
||||||
|
b'""\n',
|
||||||
|
b'{"jsonrpc": "2.0", "method": "thread/started", '
|
||||||
|
b'"params": {"threadId": "t1"}}\n',
|
||||||
|
]
|
||||||
|
srv = _make_read_loop_server(lines)
|
||||||
|
srv._read_loop()
|
||||||
|
self.assertEqual(len(srv.events), 1)
|
||||||
|
self.assertEqual(srv.events[0]["method"], "thread/started")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user