diff --git a/doc/dev/P0复盘.md b/doc/dev/P0复盘.md index 4a7df5d..ee83220 100644 --- a/doc/dev/P0复盘.md +++ b/doc/dev/P0复盘.md @@ -3,6 +3,22 @@ > 每次 P0 修完在这里记一笔:症状 → 根因 → 为什么会发生(流程层面) → 对应守则。 > 目的不是追责,是让同一类错误只交一次学费。新条目追加在最上面。 +## 2026-07-07 · v1.4.1 实例镜像发不出 + 版本耦合把面板锁死(连环) + +- **症状**:v1.4.1 发布后,用户升级面板成功,但升级指示器消失、实例卡在 1.4.0(Chromium 150)、浏览器仍黑屏。 +- **根因链(两个 bug 叠加)**: + 1. **CI 实例镜像 job 失败**:门禁探针过了,但最终多架构构建在 **QEMU arm64** 下第二个 apt 层 OOM(`Cannot allocate memory`)——两次 `apt-get update` + snapshot 源在后续层被反复拉取,内存压力在模拟环境爆掉。结果 panel job 成功、wechat job 失败 → **只发布了面板、没发布实例镜像**。 + 2. **版本耦合无兜底**:面板 1.4.1 因耦合去找 `wechat-on-cloud:1.4.1`(不存在)→ `latestInstanceImageId` 恒 null → 可升级检测全灭;升级也拉不到。一个「防错配」的功能,自己造成了更严重的错配瘫痪。 +- **为什么会发生**: + - 产物门禁只覆盖 amd64(gate build 是 `load:true` 单架构),arm64 只在最终 push 步骤才构建 → 门禁没拦住 arm64 的失败; + - 版本耦合是「乐观锁」——假定同版本实例镜像一定存在,没考虑 CI 部分失败/发布时序; + - matrix 两个 job 相互独立,一个失败另一个照发 → 产生「面板新、实例旧」的错配(正是耦合想防的)。 +- **修复**: + 1. Dockerfile 合并两个 apt 层为一次 update+install、装完即移除 snapshot 源、`Acquire::Languages=none`+`Retries=3` → 砍掉 QEMU 下的内存/网络峰值。**本机原生 arm64 构建 + 全套探针验证通过**; + 2. `resolveInstanceImage()` 兜底:偏好的版本 tag 在本地和 registry 都不可达 → 回退 `:latest`,保证功能永不因版本错配而瘫痪(已运行时验证)。 +- **仍待办(未在本次)**:改用**原生 arm64 runner**(`ubuntu-24.04-arm`)替代 QEMU,从根上消除模拟内存问题并提速;matrix 失败应联动(实例发不出就别单独发面板)。 +- **守则**:R1;门禁原则补充——**门禁必须覆盖所有发布架构**,不能只测 amd64。 + ## 2026-07-07 · v1.4.0 CI 镜像浏览器实例仍黑屏(Chromium 150 漂移) - **症状**:v1.4.0 发布后,微信恢复正常,但浏览器(Chromium)实例升级后依然黑屏/只见壁纸无窗口,崩溃循环(每 2s 一次,裸 `Trace/breakpoint trap`,无任何错误输出,**没有** DPI 事故的 "not invertible" 特征)。 diff --git a/docker/Dockerfile b/docker/Dockerfile index 07a7e45..2278c64 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -16,13 +16,16 @@ ENV DEBIAN_FRONTEND=noninteractive # 版本号),并跑通 doc/dev/发布门禁.md 的全部探针后才允许合并。 ARG CHROMIUM_SNAPSHOT=20260704T180435Z ARG CHROMIUM_VERSION=149.0.7827.196-1~deb12u1 +# 单层完成所有 apt 安装(此前拆成两层 → 两次 apt-get update;在 CI 的 QEMU arm64 模拟下第二次 +# update 处理全部源时 OOM "Cannot allocate memory",实例镜像发不出去)。合并为一次 update + 一次 +# install,并在装完立刻移除 snapshot 源,避免后续 apt 再去慢速快照站;Languages=none 跳过翻译文件、 +# Retries 抗网络抖动——共同把 QEMU 下的内存/网络压力降到最低。 RUN set -eux; \ echo "deb [check-valid-until=no] https://snapshot.debian.org/archive/debian-security/${CHROMIUM_SNAPSHOT}/ bookworm-security main" \ > /etc/apt/sources.list.d/woc-chromium-pin.list; \ printf 'Package: chromium*\nPin: version %s\nPin-Priority: 1001\n' "${CHROMIUM_VERSION}" \ > /etc/apt/preferences.d/woc-chromium-pin; \ - # 中文字体(否则界面/消息显示方块)+ 语言环境 + 下载/解压工具 - apt-get update; \ + apt-get -o Acquire::Languages=none -o Acquire::Retries=3 update; \ apt-get install -y --no-install-recommends \ curl ca-certificates locales dpkg \ fonts-wqy-zenhei fonts-wqy-microhei fonts-noto-cjk fonts-noto-color-emoji \ @@ -30,42 +33,20 @@ RUN set -eux; \ xz-utils \ chromium="${CHROMIUM_VERSION}" \ xdotool xclip \ - xwallpaper xsettingsd; \ + xwallpaper xsettingsd \ + # 微信原生版是 Qt 程序,依赖一组 xcb 平台库;libxcb-cursor0 由 Qt 动态 dlopen,ldd 查不到,需主动装。 + libatomic1 libxdamage1 libxkbcommon-x11-0 \ + libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xkb1 libxcb-cursor0 \ + # WeChatAppEx 是 Chromium 内核,需 GTK3 全家桶 + 一组 X 扩展 + cups + libgtk-3-0 libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libcups2 \ + libxcomposite1 libxrandr2 libxfixes3 libxtst6 libxshmfence1 libdrm2; \ chromium --version; \ + rm -f /etc/apt/sources.list.d/woc-chromium-pin.list /etc/apt/preferences.d/woc-chromium-pin; \ sed -i 's/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen; \ locale-gen; \ apt-get clean; \ rm -rf /var/lib/apt/lists/* -# 微信运行时需要、但官方 deb 未声明的额外库(单独成层,避免动到上面缓存的安装层)。 -# 微信原生版是 Qt 程序,依赖一组 xcb 平台库;libxcb-cursor0 由 Qt 动态 dlopen,ldd 查不到,需主动装。 -RUN set -eux; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - libatomic1 \ - libxdamage1 \ - libxkbcommon-x11-0 \ - libxcb-icccm4 \ - libxcb-image0 \ - libxcb-keysyms1 \ - libxcb-render-util0 \ - libxcb-xkb1 \ - libxcb-cursor0 \ - # WeChatAppEx 是 Chromium 内核,需 GTK3 全家桶 + 一组 X 扩展 + cups - libgtk-3-0 \ - libatk1.0-0 \ - libatk-bridge2.0-0 \ - libatspi2.0-0 \ - libcups2 \ - libxcomposite1 \ - libxrandr2 \ - libxfixes3 \ - libxtst6 \ - libxshmfence1 \ - libdrm2; \ - apt-get clean; \ - rm -rf /var/lib/apt/lists/* - # 字体/字体配置目录都在会话 HOME=/config 下(数据卷,运行时才挂载),故不在此预建; # 由 autostart 在启动时按需 mkdir + 建 ~/.local/share/fonts → /config/.fonts 软链(见 docker/autostart)。 diff --git a/panel/server/src/docker.ts b/panel/server/src/docker.ts index dd72c35..14a886c 100644 --- a/panel/server/src/docker.ts +++ b/panel/server/src/docker.ts @@ -20,7 +20,36 @@ function resolveWechatImage(): string { const repo = m ? m[1] : noDigest; return `${repo}:${ver}`; } -const WECHAT_IMAGE = resolveWechatImage(); +// 注意:可被 resolveInstanceImage() 在启动时改写为 :latest 兜底(见下),故用 let。 +let WECHAT_IMAGE = resolveWechatImage(); + +// 版本耦合的安全兜底(P0:issue #112 后续)。版本耦合让面板偏好「与自身同版本」的实例镜像 tag, +// 但若该 tag 在本地和 registry 都不存在——典型如某次 CI 只发布了面板、实例镜像构建失败—— +// 面板就会指向一个不存在的 tag,导致:可升级检测恒空(指示器消失)、创建/升级实例拉取失败。 +// 故启动时校验一次:偏好的具体版本 tag 不可达 → 回退 :latest,保证功能永不因版本错配而瘫痪。 +let imageResolved = false; +export async function resolveInstanceImage(): Promise { + if (imageResolved) return; + imageResolved = true; + const preferred = WECHAT_IMAGE; + const tagM = preferred.split('@')[0].match(/:([^/:]+)$/); + if (!tagM || tagM[1] === 'latest') return; // 已是 latest 或无 tag → 无需兜底 + try { + await docker.getImage(preferred).inspect(); + return; // 本地已有该版本镜像 → 用它 + } catch { + /* 本地没有,继续查 registry */ + } + const ref = parseImageRef(preferred); + const remote = ref ? await fetchManifestDigest(ref) : null; + if (remote) return; // registry 上存在该版本 → 用它(ensureImage 会拉) + const fallback = preferred.replace(/:[^/:]+$/, ':latest'); + appendPanelLog( + 'WARN', + `实例镜像 ${preferred} 在本地与镜像仓库均不存在,回退使用 ${fallback}(很可能该版本的实例镜像未成功发布;升级/检测将基于 :latest)`, + ); + WECHAT_IMAGE = fallback; +} const PUID = process.env.PUID || '1000'; const PGID = process.env.PGID || '1000'; const TZ = process.env.TZ || 'Asia/Shanghai'; @@ -196,6 +225,7 @@ function envList(inst: Instance): string[] { // 确保微信镜像在本地存在;缺失则从 GHCR 拉取(首次新建实例时镜像通常还没拉过)。 async function ensureImage(): Promise { + await resolveInstanceImage(); // 版本兜底:若耦合的版本 tag 不可达则先回退 :latest try { await docker.getImage(WECHAT_IMAGE).inspect(); return; diff --git a/panel/server/src/index.ts b/panel/server/src/index.ts index 2d34e27..400a475 100644 --- a/panel/server/src/index.ts +++ b/panel/server/src/index.ts @@ -48,6 +48,7 @@ import { pullImage, pruneDanglingImages, remoteInstanceImageNewer, + resolveInstanceImage, removeInstance as removeInstanceContainer, instanceRuntime, instanceImageVersion, @@ -1440,6 +1441,9 @@ app.server.on('upgrade', (req: IncomingMessage, socket: Socket, head: Buffer) => }); }); +// 版本兜底:若面板偏好的「同版本实例镜像 tag」不可达则回退 :latest(见 docker.ts)。 +// 须在实例检测/升级/启动之前解析好,否则升级指示器会因指向不存在的 tag 而恒空。 +await resolveInstanceImage().catch(() => {}); // 探测面板网络 + 重启后把已登记实例的容器拉起来 await ensureNetwork().catch(() => {}); for (const pub of listInstances()) {