Skip to content

fix: 修复配置热重载时 Web 管理端端口冲突拖垮 AstrBot (#75)#77

Open
Ayleovelle wants to merge 2 commits into
DBJD-CR:mainfrom
Ayleovelle:fix/issue-75-web-admin-port-crash
Open

fix: 修复配置热重载时 Web 管理端端口冲突拖垮 AstrBot (#75)#77
Ayleovelle wants to merge 2 commits into
DBJD-CR:mainfrom
Ayleovelle:fix/issue-75-web-admin-port-crash

Conversation

@Ayleovelle

@Ayleovelle Ayleovelle commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

先占个坑,有其他的问题明日醒来再改。

📝 描述 / Description

修复 #75:在 AstrBot 主面板(WebUI)保存本插件配置时,整个 AstrBot 进程崩溃。

根本原因是两个缺陷叠加,缺一不会崩:

  1. 进程崩溃源:保存插件配置会触发 AstrBot 热重载(串行执行 terminateinitialize)。initialize 时新实例会在 4100 端口重新启动内嵌的 Uvicorn 服务;若此时旧端口尚未释放,Uvicorn 绑定失败会调用 sys.exit(1) 抛出 SystemExit。但 _serve() 协程用 except Exception 捕获,而 SystemExit 继承自 BaseException 而非 Exception,因此捕获不到;又因为该协程是 asyncio.create_task 起的、从不被 await,异常便作为“未检索的任务异常”冒泡到事件循环根部(run_until_complete),最终拖垮整个 AstrBot 进程。这与 issue 报错日志里 SystemExit: 1 经由 runners.py_serve 的调用栈完全吻合。

  2. 端口冲突源stop() 仅设置 should_exit = True。当存在未关闭的长连接(如 WebUI 的 WebSocket)时,Uvicorn 的优雅关闭会一直等待连接断开而永久挂起,导致 stop()await asyncio.wait_for(..., timeout=5) 必然超时;而超时异常又被 except Exception: pass 静默吞掉,旧监听 socket 始终没有释放。于是热重载时新实例绑定同端口必然失败。

🛠️ 改动点 / Modifications

仅修改 core/web_admin_server.py 一个文件(+18 / -4):

  • _serve()except Exception 改为 except BaseException,拦住 Uvicorn 绑定失败抛出的 SystemExit,避免拖垮宿主进程;同时单独 except asyncio.CancelledError: raise 放行取消信号,保持正常停止时的取消语义。
  • stop():补充 self.server.force_exit = True,跳过对未关闭长连接的等待、确保监听 socket 被释放;并把超时分支由 except Exception: pass 改为记录告警并取消任务,不再静默忽略。

影响 / Impact

  • 保存配置触发热重载时,AstrBot 不再崩溃。

  • 即便偶发端口未及时释放,最多导致本次 Web 管理端启动失败(仅 Web 端不可用,插件主体功能不受影响),而不会拖垮整个 AstrBot。

  • stop() 现在能可靠释放端口,热重载后 Web 管理端可正常重启。

  • 行为对既有用户无破坏:正常启动/停止流程不变,仅在异常路径上更健壮。

  • 不是一个破坏性变更 / This is NOT a breaking change.

📸 运行截图或测试结果 / Screenshots or Test Results

测试方法

由于崩溃的关键在于「asyncio.create_task 起的 _serve 任务、其内部的 except 层级、以及 stop() 的端口释放时序」,我们用一个独立脚本按 1:1 复刻了 start()/_serve()/stop() 的真实结构(相同的 create_task 且不 await、相同的 except 捕获层级、相同的 should_exit + force_exit),并搭配**真实的 Uvicorn(0.48.0)**运行,而非用 mock 假冒异常。

  • 场景 A(复现并验证崩溃修复):先用一个 socket 抢占目标端口,再启动复刻的 server,使 Uvicorn 真实触发 bind 失败 → sys.exit(1)SystemExit,观察进程是否还会被拖垮。
  • 场景 B(验证端口释放):正常启动后调用复刻的 stop(),再探测端口是否已可重新绑定,验证 force_exit 是否真的释放了 socket。
  • 场景 C(验证未引入回归):手动 cancel() 该任务,确认 CancelledError 仍能正常向上传播,没有被新的 except BaseException 误吞,从而不破坏正常卸载/重载的取消语义。

说明:这是针对崩溃机制的结构级复现验证,使用真实 Uvicorn 触发真实的 SystemExit;尚未在完整的 AstrBot + WebUI 桌面环境中做端到端点击复现,欢迎维护者在真实环境复测。

测试结果

[A] 端口被占时启动(模拟热重载抢端口):
   [_serve] intercepted: SystemExit(1)        # 异常被 except BaseException 拦住
   -> 进程存活(未崩溃): PASS
[B] 正常启动 -> stop -> 端口释放:
   启动后端口被占用: True
   stop 后端口已释放: True
   -> PASS
[C] CancelledError 放行(保持正常卸载语义):
   -> CancelledError 正常传播: PASS
进程正常退出 —— SystemExit 未再拉崩事件循环。

对照实验(修复前的写法):把 _serve 的捕获改回 except Exception,同一脚本下场景 A 即崩溃,asyncio.run 直接抛出 SystemExit 并打印 Task exception was never retrieved,与 issue #75 现象一致;改为 except BaseException 后进程存活。

✅ 检查清单 / Checklist

  • 😊 本 PR 为 bug 修复,无新增功能。
  • 👀 我的更改经过了良好的测试,并已在上方提供了测试日志。
  • 🤓 我确保没有引入新依赖库。
  • 😮 我的更改没有引入恶意代码。

❤️ CONTRIBUTING

  • 🥳 我已阅读并同意遵守该项目的贡献指南。

保存插件配置会触发 AstrBot 热重载(terminate→initialize)。原实现存在
两个缺陷叠加导致整个 AstrBot 进程崩溃(issue DBJD-CR#75):

1. _serve() 用 except Exception 捕获,接不住 Uvicorn 绑定端口失败时
   sys.exit() 抛出的 SystemExit(属 BaseException),该异常作为未检索的
   任务异常冒泡到事件循环根部,拖垮宿主进程。改为 except BaseException
   拦截,并单独放行 CancelledError 以保持正常停止的取消语义。

2. stop() 仅设置 should_exit,存在未关闭长连接时优雅关闭会永久挂起,5 秒
   超时又被 except Exception: pass 静默吞掉,旧监听 socket 未释放,重载时
   新实例绑定同端口失败。补充 force_exit=True 确保端口释放,超时改为告警
   并取消任务,不再静默忽略。

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@dosubot dosubot Bot added the size:S 修改了 10-29 行代码 (忽略生成文件) label Jun 6, 2026
@sourcery-ai

sourcery-ai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor
审阅者指南(在小型 PR 中折叠)

审阅者指南

此 PR 加固了 AstrBot Web 管理端服务器的启停生命周期:当 Uvicorn 绑定端口失败时,配置热重载不再导致整个 AstrBot 进程崩溃,并且即使存在遗留的 WebSocket 连接也能可靠释放端口。

加固后的 Web 管理端服务器启停生命周期时序图

sequenceDiagram
    actor AstrBot
    participant EventLoop
    participant WebAdminServer
    participant UvicornServer

    AstrBot->>WebAdminServer: start()
    WebAdminServer->>EventLoop: asyncio.create_task(_serve)
    EventLoop->>WebAdminServer: run _serve()
    WebAdminServer->>UvicornServer: serve()
    alt [Uvicorn binds port successfully]
        UvicornServer-->>WebAdminServer: serve() completes
    else [Port bind fails]
        UvicornServer-->>WebAdminServer: SystemExit
        WebAdminServer-->>WebAdminServer: except BaseException
        WebAdminServer->>WebAdminServer: logger.error(...)
        WebAdminServer-->>EventLoop: _serve completes without crashing AstrBot
    end

    AstrBot->>WebAdminServer: stop()
    WebAdminServer->>UvicornServer: set should_exit = True
    WebAdminServer->>UvicornServer: set force_exit = True
    WebAdminServer->>EventLoop: asyncio.wait_for(server_task, 5)
    alt [server_task stops within 5s]
        EventLoop-->>WebAdminServer: server_task finished
    else [Timeout]
        EventLoop-->>WebAdminServer: asyncio.TimeoutError
        WebAdminServer->>WebAdminServer: logger.warning(...)
        WebAdminServer->>EventLoop: server_task.cancel()
    end
    WebAdminServer-->>AstrBot: stop() returns without port leak
Loading

文件级变更

变更 详情 文件
让 Web 管理端的 Uvicorn 服务任务能够正确处理取消与致命错误,使得端口绑定失败时产生的 SystemExit 不再导致 AstrBot 崩溃。
  • 重构 _serve 协程,使其首先显式重新抛出 asyncio.CancelledError,以在关闭过程中保持正常的取消语义。
  • 将错误处理从捕获 Exception 扩展为捕获 BaseException,以拦截 Uvicorn 在绑定失败时通过 sys.exit() 引发的 SystemExit
  • 改进 _serve 中的错误日志记录,使用 {e!r} 记录完整异常表示,便于诊断。
core/web_admin_server.py
确保 Web 管理端服务器在关闭时可靠释放监听套接字,并将关闭问题显式暴露,而不是悄悄吞掉错误。
  • 在设置 should_exit 的同时设置 self.server.force_exit = True,以绕过在存在长连接(例如来自 Web UI 的 WebSocket)时无限等待优雅关闭,从而确保端口被释放。
  • 使用 asyncio.wait_for 为对 self.server_task 的等待包裹一个 5 秒超时;在出现 asyncio.TimeoutError 时,记录“Web 管理端未能按时停止”的警告并取消该任务,避免挂起。
  • 将宽泛的 except Exception: pass 关闭处理逻辑替换为针对性的 asyncio.TimeoutError 处理,再加上一个通用的 Exception 分支,用于记录警告,而不是默默忽略错误。
  • 清理已跟踪的 WebSocket 连接,并像之前一样记录一条最终的 info 日志,确认 Web 管理端已停止。
core/web_admin_server.py

可能关联的 Issue

  • [Bug] 配置热重载时端口冲突导致进程崩溃的问题 #75:PR 在 Web 管理端启动和停止中处理 SystemExit 并强制释放端口,直接修复配置热重载端口冲突导致的崩溃问题。
  • #unknown:PR 修改 Uvicorn 异常与停止逻辑,解决 issue 中配置端口 4100 导致 WebUI 启动失败的问题。

技巧与命令

与 Sourcery 交互

  • 触发一次新的审阅: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 从审阅评论生成 GitHub issue: 回复 Sourcery 的某条审阅评论,请它从该评论创建一个 issue。你也可以在该审阅评论下回复 @sourcery-ai issue 来从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文的任意位置写上 @sourcery-ai summary,即可在你想要的位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,即可在任何时候(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,以解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 忽略所有 Sourcery 审阅: 在 pull request 中评论 @sourcery-ai dismiss,以忽略所有现有的 Sourcery 审阅。若你希望从一次全新的审阅开始,这尤其有用——别忘了再评论 @sourcery-ai review 来触发新的审阅!

自定义你的体验

访问你的 控制面板 以:

  • 启用或禁用诸如 Sourcery 自动生成的 pull request 摘要、审阅者指南等审阅功能。
  • 更改审阅语言。
  • 添加、移除或编辑自定义审阅说明。
  • 调整其他审阅设置。

获取帮助

Original review guide in English
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

This PR hardens the AstrBot Web admin server’s lifecycle so that configuration hot-reload no longer crashes the whole AstrBot process when Uvicorn fails to bind its port, and ensures ports are reliably released even with lingering WebSocket connections.

Sequence diagram for hardened Web admin server start/stop lifecycle

sequenceDiagram
    actor AstrBot
    participant EventLoop
    participant WebAdminServer
    participant UvicornServer

    AstrBot->>WebAdminServer: start()
    WebAdminServer->>EventLoop: asyncio.create_task(_serve)
    EventLoop->>WebAdminServer: run _serve()
    WebAdminServer->>UvicornServer: serve()
    alt [Uvicorn binds port successfully]
        UvicornServer-->>WebAdminServer: serve() completes
    else [Port bind fails]
        UvicornServer-->>WebAdminServer: SystemExit
        WebAdminServer-->>WebAdminServer: except BaseException
        WebAdminServer->>WebAdminServer: logger.error(...)
        WebAdminServer-->>EventLoop: _serve completes without crashing AstrBot
    end

    AstrBot->>WebAdminServer: stop()
    WebAdminServer->>UvicornServer: set should_exit = True
    WebAdminServer->>UvicornServer: set force_exit = True
    WebAdminServer->>EventLoop: asyncio.wait_for(server_task, 5)
    alt [server_task stops within 5s]
        EventLoop-->>WebAdminServer: server_task finished
    else [Timeout]
        EventLoop-->>WebAdminServer: asyncio.TimeoutError
        WebAdminServer->>WebAdminServer: logger.warning(...)
        WebAdminServer->>EventLoop: server_task.cancel()
    end
    WebAdminServer-->>AstrBot: stop() returns without port leak
Loading

File-Level Changes

Change Details Files
Make the Web admin Uvicorn serving task correctly handle cancellation and fatal errors so SystemExit from port bind failures no longer crashes AstrBot.
  • Refactor the _serve coroutine to first explicitly re-raise asyncio.CancelledError to preserve normal cancellation semantics during shutdown.
  • Broaden the error handler from catching Exception to catching BaseException so that SystemExit raised by Uvicorn’s sys.exit() on bind failure is intercepted.
  • Improve error logging in _serve to log the full exception representation using {e!r} for better diagnostics.
core/web_admin_server.py
Ensure Web admin server shutdown reliably releases the listening socket and surfaces shutdown problems instead of silently swallowing them.
  • Set self.server.force_exit = True alongside should_exit to bypass indefinite graceful shutdown waits when there are long-lived connections (e.g., WebSocket from the Web UI), ensuring the port is freed.
  • Wrap the wait on self.server_task in asyncio.wait_for with a 5-second timeout, and on asyncio.TimeoutError, log a warning that the Web admin did not stop in time and cancel the task to avoid hanging.
  • Replace the broad except Exception: pass shutdown handler with targeted asyncio.TimeoutError handling plus a generic Exception branch that logs a warning instead of silently ignoring errors.
  • Clear tracked WebSocket connections and log a final info message confirming Web admin stop as before.
core/web_admin_server.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request improves the lifecycle management and exception handling of the Web Admin Server by correctly propagating asyncio.CancelledError, catching exceptions to prevent process crashes during port binding failures, and handling server shutdown timeouts. The review feedback points out two key issues: catching BaseException can swallow KeyboardInterrupt signals, so catching (SystemExit, Exception) is recommended instead; and when cancelling the server task during a timeout, the task should be explicitly awaited to ensure the port is fully released before the stop method returns.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread core/web_admin_server.py
Comment on lines +1347 to +1351
except asyncio.TimeoutError:
logger.warning(
"[主动消息] Web 管理端未在 5 秒内停止喵,端口可能仍被占用。"
)
self.server_task.cancel()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

在调用 self.server_task.cancel() 后,该任务只是被标记为取消,并不会立即执行取消流程。如果不 await 该任务,stop() 方法会立即返回,而此时 _serve 协程可能还没有真正退出并释放端口。在热重载(先 stop 后立即 start)的场景下,这仍然可能导致短暂的端口冲突。

建议在 cancel() 后,使用 try...except asyncio.CancelledError 显式 await self.server_task,确保任务完全退出、端口彻底释放后再结束 stop()

Suggested change
except asyncio.TimeoutError:
logger.warning(
"[主动消息] Web 管理端未在 5 秒内停止喵,端口可能仍被占用。"
)
self.server_task.cancel()
except asyncio.TimeoutError:
logger.warning(
"[主动消息] Web 管理端未在 5 秒内停止喵,端口可能仍被占用。"
)
self.server_task.cancel()
try:
await self.server_task
except asyncio.CancelledError:
pass

Comment thread core/web_admin_server.py Outdated
Comment on lines +1303 to +1307
except BaseException as e:
# Uvicorn 绑定端口失败会调用 sys.exit() 抛出 SystemExit(属
# BaseException 而非 Exception),若不在此拦截,该异常会作为未
# 检索的任务异常冒泡到事件循环根部,拖垮整个 AstrBot 进程。
logger.error(f"[主动消息] Web 管理端运行异常喵: {e!r}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

在 Python 中,直接捕获 BaseException 会连同 KeyboardInterrupt(如用户按下 Ctrl+C 终止程序)一起捕获并吞掉,这会导致程序无法响应中断信号正常退出。

既然这里的主要目的是拦截 Uvicorn 绑定端口失败时抛出的 SystemExit,建议显式捕获 (SystemExit, Exception),而不是使用过于宽泛的 BaseException。这样既能拦截 SystemExit 和普通异常,又不会影响 KeyboardInterrupt 的正常传播。

Suggested change
except BaseException as e:
# Uvicorn 绑定端口失败会调用 sys.exit() 抛出 SystemExit(属
# BaseException 而非 Exception),若不在此拦截,该异常会作为未
# 检索的任务异常冒泡到事件循环根部,拖垮整个 AstrBot 进程。
logger.error(f"[主动消息] Web 管理端运行异常喵: {e!r}")
except (SystemExit, Exception) as e:
# Uvicorn 绑定端口失败会调用 sys.exit() 抛出 SystemExit(属
# BaseException 而非 Exception),若不在此拦截,该异常会作为未
# 检索的任务异常冒泡到事件循环根部,拖垮整个 AstrBot 进程。
logger.error(f"[主动消息] Web 管理端运行异常喵: {e!r}")

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - 我在这里给出了一些整体性的反馈:

  • _serve 中捕获 BaseException 时,建议使用 logger.exception 而不是 logger.error,这样可以保留完整的回溯信息,便于调试诸如 SystemExit 这类少见故障。
  • 无条件设置 self.server.force_exit = True 会改变关闭流程的语义;你可能需要通过配置开关来控制它,或者只在实际出现 WebSocket 挂起问题的热重载/卸载路径中启用。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- When catching `BaseException` in `_serve`, consider using `logger.exception` instead of `logger.error` so you preserve the full traceback for debugging rare failures like `SystemExit`.
- Setting `self.server.force_exit = True` unconditionally changes shutdown semantics; you might want to gate this behind a configuration flag or only enable it in the hot-reload/unload path where the hanging WebSocket issue actually occurs.

Sourcery 对开源项目是免费的——如果你觉得我们的代码评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈来改进后续的评审。
Original comment in English

Hey - I've left some high level feedback:

  • When catching BaseException in _serve, consider using logger.exception instead of logger.error so you preserve the full traceback for debugging rare failures like SystemExit.
  • Setting self.server.force_exit = True unconditionally changes shutdown semantics; you might want to gate this behind a configuration flag or only enable it in the hot-reload/unload path where the hanging WebSocket issue actually occurs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When catching `BaseException` in `_serve`, consider using `logger.exception` instead of `logger.error` so you preserve the full traceback for debugging rare failures like `SystemExit`.
- Setting `self.server.force_exit = True` unconditionally changes shutdown semantics; you might want to gate this behind a configuration flag or only enable it in the hot-reload/unload path where the hanging WebSocket issue actually occurs.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

回应 PR DBJD-CR#77 的 review 反馈:

1. _serve() 的 except BaseException 收窄为 except (SystemExit, Exception),
   仍拦住 Uvicorn 绑定失败的 SystemExit,但不再波及 KeyboardInterrupt。
2. _serve() 改用 logger.exception 记录,保留完整回溯,便于诊断 SystemExit
   这类少见故障。
3. stop() 超时强制 cancel 任务后补充 await,确保取消真正完成、监听 socket
   在 stop 返回前已释放,杜绝热重载时端口残留。

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@Ayleovelle

Ayleovelle commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

review 的几条看了,最新一版提交(1a273d3)改了三处:

  • _serve 里的 except BaseException 收窄成了 except (SystemExit, Exception),照样拦得住 uvicorn 绑定失败的 SystemExit,但不会顺手把 Ctrl+C 也吃掉。CancelledError 前面单独 raise 放行。
  • stop() 超时 cancel 之后补了个 await,等它真取消完再返回,省得端口还没放掉就被下一次 start 抢。
  • log 改成 logger.exception 了,方便看完整堆栈。

force_exit 那条我先不加开关。这个 stop 只有卸载/热重载的时候才会走,本来就是要把服务关掉,不存在"想优雅关结果被强制打断"的情况;而且不开 force_exit 的话,WebUI 那边 WebSocket 长连不断,uvicorn 优雅关闭会一直等连接、直接卡满 5 秒超时,端口放不出来——这就是这个 issue 端口冲突的根。加开关反而容易被配错。要是你觉得还是想留个配置项,我再加。

另外本地又复现了一遍验证:直接 import 插件真实的 WebAdminServer,用它自己的 start/stop/_serve 配真 uvicorn,模拟热重载抢端口。改之前进程直接被 SystemExit 拉崩,跟 issue 现象一样;改之后同样冲突下进程能扛住,SystemExit 被 _serve 拦了,stop 完端口也能正常被新实例重新绑上。

@gemini-code-assist

Copy link
Copy Markdown
Contributor

感谢你的详细反馈和深入的本地复现验证。你对 force_exit = True 的权衡分析非常合理:在插件生命周期管理(特别是热重载)的语境下,确保端口释放的优先级确实高于等待潜在的优雅关闭,且将其作为默认行为能有效避免因 WebSocket 连接残留导致的端口冲突,这对于提升 AstrBot 的稳定性至关重要。

你的修复方案(包括对 SystemExit 的精准捕获、对 CancelledError 的正确传播,以及在 stop() 中对超时后的强制清理)逻辑严密,处理得非常专业。这些改动不仅解决了 #75 提到的崩溃问题,还显著增强了 Web 管理端生命周期的健壮性。

无需额外引入配置开关,保持当前的实现即可。非常感谢你为解决此问题所做的细致工作!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 修改了 10-29 行代码 (忽略生成文件)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant