Skip to content

fix(term, app): prevent TTY hang on exit and close MCP connections during shutdown#1985

Open
thecannabisapp wants to merge 3 commits intoMoonshotAI:mainfrom
thecannabisapp:main
Open

fix(term, app): prevent TTY hang on exit and close MCP connections during shutdown#1985
thecannabisapp wants to merge 3 commits intoMoonshotAI:mainfrom
thecannabisapp:main

Conversation

@thecannabisapp
Copy link
Copy Markdown

@thecannabisapp thecannabisapp commented Apr 21, 2026

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(): Added os.set_blocking(fd, False) after tty.setcbreak(fd) to make os.read() non-blocking. This prevents the syscall from getting stuck in an uninterruptible blocking state during asyncio task cancellation or when prompt_toolkit's stdin reader races for the CPR response. Blocking mode is restored in the finally block.

  • ensure_tty_sane():在恢复 c_lflag 标志位后,额外重置 c_cc[VMIN]c_cc[VTIME] 为默认值(1 和 0)。此前若进程在 cbreak 模式下崩溃或挂起,终端会保持在 VMIN=1, VTIME=0 状态,导致 Ctrl+C 发送原始字节而非 SIGINT。

  • ensure_tty_sane(): Resets c_cc[VMIN] and c_cc[VTIME] to defaults (1 and 0) after restoring c_lflag flags. Previously, if the process crashed or hung in cbreak mode, the terminal remained in VMIN=1, VTIME=0, causing Ctrl+C to emit raw bytes instead of SIGINT.

2. src/kimi_cli/app.py

  • shutdown_background_tasks():在取消后台刷新任务后,调用 KimiToolset.cleanup() 关闭所有 fastmcp.Client 连接。此前该方法从未被调用,导致 stdio/WebSocket 传输在 CLI 进程退出后仍保持连接,触发系统防火墙关于孤立连接的警告。

  • shutdown_background_tasks(): Calls KimiToolset.cleanup() after cancelling the background refresh task to close all fastmcp.Client connections. 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 warnings
  • ruff check src/kimi_cli/utils/term.py src/kimi_cli/app.py:All checks passed

Open in Devin Review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/kimi_cli/app.py
Comment on lines +406 to +408
await toolset.cleanup()
except Exception:
logger.warning("Error during toolset cleanup; continuing exit", exc_info=True)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/kimi_cli/utils/term.py Outdated
return int(match.group(1)), int(match.group(2))
finally:
with contextlib.suppress(OSError):
os.set_blocking(fd, True)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

devin-ai-integration[bot]

This comment was marked as resolved.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: terminal hang on exit and MCP connection leak

1 participant