From fba672381e9be69d8423026245ddf46ee52b16f6 Mon Sep 17 00:00:00 2001 From: ila Date: Fri, 17 Jul 2026 09:54:47 +0800 Subject: [PATCH] Define icon UI event delivery task (T-607) --- docs/00-ai-start-here.md | 4 +- docs/06-tasks.md | 3 +- docs/current-state.md | 8 +-- docs/review/phase2-review.md | 3 +- docs/tasks/T-607.md | 100 +++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 docs/tasks/T-607.md diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 8e934c5..13e29c5 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -45,7 +45,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, ## 当前阶段 -当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-606`。Windows 安全路径阻断项与 Phase 2 首个图标缓存资源整改已关闭;下一步按 Phase 2 交叉审核顺序落成后台图标结果经 application event 回到 UI goroutine 的接线任务,其余审核整改与 Phase 1 中央目录预扫描继续串行处理,T-302 暂后置。 +当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-606`。Windows 安全路径阻断项与 Phase 2 首个图标缓存资源整改已关闭;Phase 2 第二个整改 `T-607` 已落成待领取,下一步实现后台图标结果经 application event 回到 UI goroutine 的线程接线,其余审核整改与 Phase 1 中央目录预扫描继续串行处理,T-302 暂后置。 优先路径: @@ -53,7 +53,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, 2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。 3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。 4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。 -5. 已完成 T-606:图标缓存按 key 去重、流式有界读取、memory LRU 与双 shell 图标剪枝;下一步按审核顺序落成 UI 线程事件接线整改,再串行处理其余整改与 T-302/T-303、Phase 4-6。 +5. 已完成 T-606:图标缓存按 key 去重、流式有界读取、memory LRU 与双 shell 图标剪枝;下一步实现 T-607 的 UI 线程事件接线,再串行处理其余整改与 T-302/T-303、Phase 4-6。 ## 领取任务规则 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index b9aa22d..f591e9b 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -59,11 +59,12 @@ Phase 1 安全整改按 `docs/review/phase1-security-review.md` 的交叉复核 #### Phase 2 交叉审核加固 -Phase 2 整改按 `docs/review/phase2-review.md` 的交叉复核定稿顺序串行落成。T-606 先关闭正式图标接入前的并发、读取和内存边界;UI 线程投递、适配器契约、VisibleItems 快照与 shell 拆分在前一整改完成并提交后再正式编号。 +Phase 2 整改按 `docs/review/phase2-review.md` 的交叉复核定稿顺序串行落成。T-606 已关闭正式图标接入前的并发、读取和内存边界;T-607 建立后台图标结果经 application event 回到 Gio UI goroutine 的线程契约。适配器契约、VisibleItems 快照与 shell 拆分在前一整改完成并提交后再正式编号。 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | | T-606 | 收紧图标缓存并发与内存边界 | T-204, T-605 | 按 key in-flight 去重且不同 key 并行;Fetcher 流式有界读取;memory LRU 同时限制字节/条目;modern/win7 删除 app 时剪枝内存 ImageOp | +| T-607 | 建立图标后台结果的 UI 线程事件投递 | T-606 | 后台 Load/Decode 只发布 application event;有界 relay 请求重绘;UI goroutine drain 后 ApplyIcon;过期结果不回写已删除或已换图标的 app | ### Phase 3 · 下载与安装 diff --git a/docs/current-state.md b/docs/current-state.md index 49aba84..275869f 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -13,7 +13,7 @@ ## 当前快照 - 日期:2026-07-17 -- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-606 已完成,T-302 继续暂后置 +- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-606 已完成,T-607 已落成待领取,T-302 继续暂后置 - 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析 - 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略、安全 ZIP 解压/回滚原型、无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存,以及默认并发 2 的持久可恢复下载队列;modern/win7 AppShell 已实现搜索/分类/视图、惰性列表、详情右栏与随 Catalog 剪枝的内存图标 - 测试:core 覆盖 Catalog、SemVer/12 状态、本地安装记录、Windows dot-space/设备名/Unicode 折叠路径攻击、ZIP destination 包含性、列表/图标并发/取消/读取边界/LRU、下载并发/暂停/取消/重试/Range/断连/恢复/事件失败与文件身份替换;两个 app 覆盖 500 项虚拟列表、ID 控件/图标剪枝稳定性、详情/ApplyIcon 与平台 stub;安装恢复矩阵保持通过 @@ -21,14 +21,14 @@ - 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、执行完整 Phase 0 闸门、打印双目标构建命令) - 标准验证路径:`bash scripts/verify_phase0.sh` / `./scripts/verify_phase0.ps1` - 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交 -- 当前 blocker:无;下一步按 `docs/review/phase2-review.md` 最终顺序落成图标后台结果经 application event 回到 UI goroutine 的接线任务;适配器契约、VisibleItems 快照、Phase 1 中央目录预扫描等继续串行,T-302 继续后置 +- 当前 blocker:无;下一步领取 T-607,建立图标后台 Load/Decode 经 application event、有界 relay 与 Invalidate 回到 Gio UI goroutine 的接线;适配器契约、VisibleItems 快照、Phase 1 中央目录预扫描等继续串行,T-302 继续后置 ## 当前目录要点 | 路径 | 状态 | 说明 | | --- | --- | --- | | `docs/` | 已有 | harness coding 文档集(本次初始化完成) | -| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-606 已完成;其余审核整改尚未编号,T-302 暂后置 | +| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-606 已完成;T-607 已落成待领取,其余审核整改尚未编号,T-302 暂后置 | | `scripts/` | 已有 | harness 治理、core 边界、Go 版本检查与 Phase 0 双平台验证入口 | | `core/` | 已建 | Go 1.20 兼容;已有正式 Catalog、本地状态/存储、共享 Windows safepath、列表模型、有界并发图标缓存、可恢复下载队列与 Phase 1 安装安全原型 | | `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;Modern AppShell 已接入虚拟列表、详情和随 Catalog 剪枝的内存图标 | @@ -42,7 +42,7 @@ - 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204`;Phase 3 的 `T-301`;审核整改 `T-604`~`T-606`。 - 正在进行:无。 -- 下一个可领取任务:无;先按 `docs/review/phase2-review.md` 最终顺序落成 UI 线程事件接线整改任务。 +- 下一个可领取任务:`T-607`(建立图标后台结果的 UI 线程事件投递),依赖 `T-606` 已完成。 ## 当前可运行内容 diff --git a/docs/review/phase2-review.md b/docs/review/phase2-review.md index 62e04e3..ca29265 100644 --- a/docs/review/phase2-review.md +++ b/docs/review/phase2-review.md @@ -284,4 +284,5 @@ modern/win7 的 `ApplyIcon` 都直接写 `shell.icons` map,Layout 同时读取 ## 任务落地追踪 - `T-606` 已完成最终处理顺序第 1 项:按 key in-flight 去重、Fetcher 流式有界读取、32 MiB/256-key memory LRU 与 modern/win7 `shell.icons` 剪枝已实现并通过完整双 workspace 闸门。 -- UI 线程投递、适配器交互契约、`VisibleItems` 快照、`shell.go` 拆分和 unsafe cache 诊断尚未编号;下一任务从 UI 线程投递开始,继续按顺序串行落成。 +- `T-607` 已按最终处理顺序第 2 项落成待领取:后台图标 Load/Decode 只发布 application event,有界 relay 请求重绘,由 Gio UI goroutine drain 后执行 `ApplyEvent`/`ApplyIcon`,并拒绝过期结果回写。 +- 适配器交互契约、`VisibleItems` 快照、`shell.go` 拆分和 unsafe cache 诊断尚未编号;必须等待 T-607 完成并提交后再按顺序落成。 diff --git a/docs/tasks/T-607.md b/docs/tasks/T-607.md new file mode 100644 index 0000000..9eff193 --- /dev/null +++ b/docs/tasks/T-607.md @@ -0,0 +1,100 @@ +--- +id: T-607 +title: 建立图标后台结果的 UI 线程事件投递 +phase: 2 +deps: [T-606] +status: TODO +created: 2026-07-17 +issue: null +context_ref: null +claim_branch: null +work_branch: null +write_paths: + - 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;事件发布失败显式返回/报告,不得改变 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。