Files
soft_quay/docs/tasks/T-607.md

11 KiB

id, title, phase, deps, status, created, issue, context_ref, claim_branch, work_branch, write_paths
id title phase deps status created issue context_ref claim_branch work_branch write_paths
T-607 建立图标后台结果的 UI 线程事件投递 2
T-606
DONE 2026-07-17 null fba672381e null agent/codex/T-607
docs/tasks/T-607.md
core/application/
core/catalog/
app-modern/cmd/softbox/
app-modern/ui/gio/
app-win7/cmd/softbox/
app-win7/ui/gio/
docs/api.md
docs/routes.md
docs/04-architecture.md
docs/05-coding-rules.md
docs/review/phase2-review.md
docs/00-ai-start-here.md
docs/06-tasks.md
docs/current-state.md

问题 / 背景

T-606 已关闭图标缓存的全局锁 convoy、远端响应无界读取和内存无上限问题,但 IconCache.Load 仍没有生产调用者。modern/win7 的 AppShell.ApplyIcon 会直接写 shell.icons map,Layout 同时读取该 map;当前只有同 goroutine 测试调用,所以尚未发生数据竞争。

正式图标后台加载若从 worker goroutine 直接调用 ApplyIcon,既会产生 map race,也违反 Gio 的 UI 更新模型。现有两个 main.run 只处理 window event,尚未把 application.Runtime.Events() 转成可唤醒窗口并由 Frame/UI goroutine 应用的事件流。Catalog 更新或相同 app 的新图标请求还可能使迟到结果过期;若只依靠 T-606 的 SetItems 剪枝,旧结果会在剪枝后把已删除或已换图标的 app 重新写回内存。

本任务先冻结并实现后台结果到 UI 的线程契约,让后续真实 HTTP 图标源和惰性调度只能复用该通道,不能绕过 application event 直接修改 Gio 状态。

方案

  1. 在 core/application 增加图标事件类型与强类型 payload:
    • 至少区分 ready 与 failed,并纳入 EventType.Valid() 白名单。
    • 事件身份包含 RequestID、AppID、IconRef 与 DPI;ready payload 携带后台已解码的标准库 image.Image,不得携带 Gio 类型。
    • failed payload 提供稳定、可诊断且不包含敏感 URL/query 的失败信息;失败不伪装为成功或静默吞掉。
    • 提供统一构造/发布入口并校验空 ID、非法 sha256: 引用、非法 DPI、nil image 和 payload 类型,避免生产者各自拼装 any。
  2. 在 core/catalog 增加可注入的后台结果发布用例:
    • 接受已经完整确定的 IconFetchRequest、AppID/RequestID 和 application event publisher,在调用方提供的后台 context 中执行 IconCache.Load 与 DecodeIcon。
    • 成功发布 ready;加载、校验或解码失败发布 failed;调用 context 取消/超时直接结束且不发布可能过期的 failed;事件发布失败显式返回/报告,不得改变 T-606 的缓存可信性语义。
    • 用例本身不创建无限 goroutine或无界 worker 队列;并发/生命周期由上层调度者和 context 控制。
    • 继续使用 T-606 的流式 Fetcher 合约,不新增图标 URL/CDN 规则。
  3. 为 modern/win7 建立相同的 application event relay:
    • 后台 pump 只接收事件、放入有界线程安全 inbox 并请求 Window.Invalidate;两个仓库锁定的 Gio 版本都明确允许并发调用 Invalidate,但 pump 不得调用 ApplyIcon、paint.NewImageOp 或修改 shell/model。
    • inbox 使用 FIFO、lossless backpressure:满队列时等待 UI drain 或 context/relay 关闭,不得静默丢 ready/failed;成功入队后必须请求重绘。
    • Gio Frame/UI goroutine 非阻塞 drain inbox,通过 ApplyEvent 校验强类型 payload 后调用 ApplyIcon;非图标事件不得因 T-607 被破坏或误判为图标事件。
    • window 销毁、runtime 关闭和 context 取消时 relay 可确定退出,不泄漏 goroutine,不在关闭后自旋或永久阻塞。
  4. 在两个 AppShell 中记录每个 app 最新期望与已应用的图标身份或等价 generation:
    • 只有 AppID 仍存在、IconRef/DPI 仍匹配且 RequestID 仍是最新请求时才应用 ready 结果。
    • 新请求覆盖旧请求;Catalog 删除 app、图标引用变化或请求取消后,迟到 ready/failed 均被丢弃。相同 AppID 的 IconRef 变化还必须清除旧 ImageOp/已应用身份,不能继续显示上一版图标。
    • failed 只保留与当前 IconRef/DPI 匹配的既有可信图标,否则保持占位状态,并结束对应 pending 状态;不得让旧失败覆盖新请求状态。
  5. 补 core 与双 Gio 的线程边界测试:
    • fake publisher/loader 证明 Load 和 Decode 不在 UI apply 路径执行,ready/failed 身份与错误传播正确。
    • 后台 submit 后 shell 尚未变化;只有 UI drain/ApplyEvent 后图标 map 才更新并触发重绘请求。
    • 覆盖删除 app、IconRef 变化并清理旧 ImageOp、同 app 新请求覆盖旧请求、非法 payload、满队列 backpressure、失败、取消与关闭 relay。
    • 使用 channel/barrier 等确定性同步;若环境支持 race detector则执行并记录,否则记录真实工具链限制。
  6. 同步事件协议、架构、路由和编码规则,明确 ApplyIcon/ApplyEvent 是 UI goroutine-only,后台唯一入口是 application event。

验收要点

  • EventIconReady/EventIconFailed 是有效 application event,强类型 payload 不 import Gio,非法事件身份或 payload 被稳定拒绝。
  • 后台图标用例调用现有 IconCache.Load 与 DecodeIcon,成功/非取消失败发布带 RequestID、AppID、IconRef、DPI 的对应事件;发布失败可观察,context 取消及时结束且不发布迟到失败。
  • modern/win7 后台 relay 收到事件时只入队并请求 invalidate,不会直接改变 shell.icons;Frame/UI goroutine drain 后才创建 paint.ImageOp。
  • relay 使用有界 FIFO 队列和可取消的 lossless backpressure,满队列不静默丢事件;window/runtime 退出后无 goroutine 泄漏、死锁或 busy loop。
  • 已删除 app、IconRef/DPI 已变化、RequestID 已被新请求取代的迟到 ready/failed 均被忽略;不会在 SetItems 剪枝后重新插入旧图标。相同 AppID 的 IconRef 变化会清除旧 ImageOp,直到新图标就绪前显示占位。
  • 图标失败保留可用的既有图标或占位状态,错误可诊断;非法 payload 不 panic、不污染 UI 状态。
  • 两个 Gio 适配器使用同一事件语义但不互相 import,modern Gio v0.10.1 与 win7 Gio v0.6.0 的 workspace 隔离保持不变。
  • Layout 路径继续无磁盘、网络、哈希和图片解码;ApplyIcon 文档与测试明确 UI goroutine-only,不通过给 map 加锁把后台直写合法化。
  • Go 1.20.14 + GOWORK=off 下 go vet ./...、go test -count=1 ./... 通过。
  • modern 根 workspace 与 win7 独立 workspace 的 UI/cmd 测试及 Windows amd64 构建通过;./scripts/verify_phase0.ps1 全绿。
  • python scripts/validate_agent_context.py、python scripts/validate_harness_governance.py 与提交前差异检查通过。

边界(不改什么)

  • 不决定 sha256: 图标引用到 URL、CDN 路径或 DPI 变体的发布协议,不增加 Catalog 字段,不实现具体 HTTP IconFetcher。
  • 不在 Layout 中启动网络/磁盘 IO;不实现按 viewport 预取、滚动取消、优先级或完整生产调度策略,本任务只建立可复用的后台结果发布与 UI 投递通道。
  • 不把 ApplyIcon 改成可从任意 goroutine 调用的线程安全 API,不以 mutex 掩盖错误线程归属。
  • 不补 Phase 2 后续完整适配器交互契约矩阵,不修改 VisibleItems 快照 API,不拆分 shell.go;这些继续按审核顺序另立任务。
  • 不修改 T-606 的内存 LRU、磁盘缓存安全策略或 Fetcher 读取上限,不处理 ErrIconCacheUnsafe 自动隔离。
  • 不处理 ZIP 中央目录预扫描、断电耐久、Catalog 签名向量或 T-302 安装整合。
  • 不升级 Go/Gio 或合并 modern/win7 workspace,不修改、提交或删除用户的 soft_quay.code-workspace。

协作约束

  • 按仓库当前规则由单 Agent 串行执行,不启动子 Agent。
  • 本任务只允许修改 frontmatter 中的 write_paths;若接线需要新增图标 URL/清单协议或从后台直接触碰 Gio 状态,必须停止并记录,不得在 T-607 内扩大边界。
  • T-607 完成、完整验证并提交前,不落成或领取 Phase 2 审核顺序中的后续任务。

执行记录

  • 2026-07-17:根据 docs/review/phase2-review.md 交叉复核定稿的第二优先级整改落成任务;现有全局最大任务为 T-606,因此取 T-607,依赖已完成的 T-606。
  • 2026-07-17:任务冻结后台 Load/Decode→application event→有界 relay/Invalidate→Gio UI drain/ApplyEvent 的线程边界;真实 URL 映射、viewport 调度、适配器全量契约、VisibleItems 快照和文件拆分继续按审核顺序串行处理。
  • 2026-07-17:用两个隔离 workspace 的本地 go doc gioui.org/app.Window.Invalidate 核对 Gio v0.10.1 与 v0.6.0,两者都明确 Invalidate 可并发调用;因此只允许后台 relay 调 Invalidate,所有 shell/ImageOp 更新仍限定在 UI goroutine。
  • 2026-07-17:在 agent/codex/T-607 分支领取任务,基线为 fba672381e9be69d8423026245ddf46ee52b16f6;保持单 Agent 串行执行。
  • 2026-07-17:基线 ./init.ps1 通过,包含治理/上下文/边界/依赖版本检查、Go 1.20.14 core vet/test、modern Go 1.25 与 win7 Go 1.20.14 的测试和 Windows amd64 构建。
  • 2026-07-17:core/application 新增 IconReady/IconFailed 强类型事件、规范化 request/app/icon_ref/DPI 身份、三类非敏感失败码,以及带 FIFO lossless backpressure 的有界 EventRelay。relay 的 slot+mutex 关闭线性化保证 Close 后不再接收新事件,满队列等待可由 context/close 解除;PumpEvents 只入队并调用 Invalidate。
  • 2026-07-17:core/catalog.IconEventDelivery 在调用方拥有的后台 context 中调用现有 IconCache.Load 与 DecodeIcon,成功发布 ready,非取消失败发布分类后的 failed,原始网络错误不进入 payload;取消不发布迟到失败,发布错误和非致命磁盘 warning 都返回调用方观察。
  • 2026-07-17:modern/win7 主循环均接入 32-entry runtime/relay 与 Frame 前 drain;后台 pump 退出时由 context/runtime/relay 确定收尾。两个 AppShell 只在 UI goroutine ApplyEvent/ApplyIcon,按最新 RequestID + AppID + IconRef + DPI 拒绝迟到结果;删除 app、IconRef 变化、DPI 变化和取消会失效旧请求,IconRef 变化清理旧 ImageOp。
  • 2026-07-17:定向验证通过:Go 1.20.14 go vet ./application ./catalog、go test -count=10 ./application ./catalog;modern/win7 go test -count=5 ./ui/gio,并验证对应 cmd 包编译。用例覆盖事件/payload 拒绝、后台 submit 在 UI drain 前不改 shell、FIFO 背压、关闭/取消、失败分类、发布失败、加载中取消、最新请求、IconRef/DPI 变化、删除 app 与取消后的迟到结果。
  • 2026-07-17:尝试 Go 1.20.14 go test -race -count=1 ./application ./catalog;当前 Windows 环境缺少 GCC(cgo: C compiler "gcc" not found),race detector 不可用。按任务边界如实记录;确定性线程边界测试在 core 重复 20 次、双 UI 重复 5 次通过。
  • 2026-07-17:完整 ./scripts/verify_phase0.ps1 通过,包含治理/上下文/边界/版本校验、Go 1.20.14 core vet/test、modern Go 1.25 与 win7 Go 1.20.14 的 UI/平台测试及 Windows amd64 构建;协议、架构、路由、编码规则、审核追踪和当前状态已同步。