fix(term, app): prevent TTY hang on exit and close MCP connections during shutdown#1985
fix(term, app): prevent TTY hang on exit and close MCP connections during shutdown#1985thecannabisapp wants to merge 3 commits intoMoonshotAI:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f53a65fc6e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| await toolset.cleanup() | ||
| except Exception: | ||
| logger.warning("Error during toolset cleanup; continuing exit", exc_info=True) |
There was a problem hiding this comment.
Handle CancelledError from toolset cleanup
If shutdown_background_tasks() runs while MCP startup is still in progress, KimiToolset.cleanup() cancels _mcp_loading_task and awaits it; that await can raise asyncio.CancelledError, which is a BaseException in modern Python and is not caught by this except Exception. In that case shutdown can be interrupted before the rest of the exit cleanup runs, producing cancellation-driven exits when users quit during MCP connection startup.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ecfe8116d0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return int(match.group(1)), int(match.group(2)) | ||
| finally: | ||
| with contextlib.suppress(OSError): | ||
| os.set_blocking(fd, True) |
There was a problem hiding this comment.
Restore prior stdin blocking mode on cursor probe exit
_cursor_position_unix() unconditionally sets stdin back to blocking mode in finally, but it never records whether the fd was already non-blocking before this function ran. If the shell/input layer had configured stdin with O_NONBLOCK (which is common in event-loop driven TTY readers), this call flips it to blocking and can cause subsequent reads to block unexpectedly after the first cursor-position check. Capture os.get_blocking(fd) before changing it and restore that original value instead of hardcoding True.
Useful? React with 👍 / 👎.
…onshotAI#1985 - Catch asyncio.CancelledError in shutdown_background_tasks() so MCP cleanup during startup cancellation does not interrupt exit. - Suppress asyncio.CancelledError in KimiToolset.cleanup() when awaiting the cancelled _mcp_loading_task, fixing the issue at the source. - Preserve original stdin blocking mode in _cursor_position_unix() instead of unconditionally restoring True. - Add per-iteration error handling in KimiToolset.cleanup() so one failing client.close() does not skip remaining MCP servers.
Fixes #1984
变更内容 / Changes
1.
src/kimi_cli/utils/term.py_cursor_position_unix():在tty.setcbreak(fd)之后添加os.set_blocking(fd, False),使os.read()变为非阻塞调用。这样即使 asyncio 任务被取消,或prompt_toolkit的 stdin reader 与 CPR 响应发生竞争,os.read()也不会陷入不可中断的系统调用阻塞。finally块中恢复阻塞模式,确保终端状态正确还原。_cursor_position_unix(): Addedos.set_blocking(fd, False)aftertty.setcbreak(fd)to makeos.read()non-blocking. This prevents the syscall from getting stuck in an uninterruptible blocking state during asyncio task cancellation or whenprompt_toolkit's stdin reader races for the CPR response. Blocking mode is restored in thefinallyblock.ensure_tty_sane():在恢复c_lflag标志位后,额外重置c_cc[VMIN]和c_cc[VTIME]为默认值(1 和 0)。此前若进程在 cbreak 模式下崩溃或挂起,终端会保持在VMIN=1, VTIME=0状态,导致 Ctrl+C 发送原始字节而非 SIGINT。ensure_tty_sane(): Resetsc_cc[VMIN]andc_cc[VTIME]to defaults (1 and 0) after restoringc_lflagflags. Previously, if the process crashed or hung in cbreak mode, the terminal remained inVMIN=1, VTIME=0, causing Ctrl+C to emit raw bytes instead of SIGINT.2.
src/kimi_cli/app.pyshutdown_background_tasks():在取消后台刷新任务后,调用KimiToolset.cleanup()关闭所有fastmcp.Client连接。此前该方法从未被调用,导致 stdio/WebSocket 传输在 CLI 进程退出后仍保持连接,触发系统防火墙关于孤立连接的警告。shutdown_background_tasks(): CallsKimiToolset.cleanup()after cancelling the background refresh task to close allfastmcp.Clientconnections. Previously this method was never invoked, leaving stdio/WebSocket transports connected after the CLI process exited and triggering system firewall warnings about orphaned connections.测试 / Testing
pyright src/kimi_cli/utils/term.py src/kimi_cli/app.py:0 errors, 0 warningsruff check src/kimi_cli/utils/term.py src/kimi_cli/app.py:All checks passed