diff --git a/agents.md b/agents.md index 14ea6f4..899173d 100644 --- a/agents.md +++ b/agents.md @@ -225,7 +225,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-06(用户定):OCR 随包语言数据用 tessdata_fast,英文、简体中文、繁体中文(eng、chi_sim、chi_tra)。 - 2026-10-06(用户定):ElyMail 在八个应用里最后做:任务表中 C12 移到 C15 之后、C16 之前;CEF 内容宿主随 ElyMark 先建(C14.0),邮件编号依次前移。 - 2026-10-06(用户定):`ely-office` 的外壳不再只服务 LOK:`Host` 带关联类型 `Engine`(`ely_office::Engine`:后台载入、视图、撤销历史的标记、写出、关闭,视图的落定、编辑计数、待落地、焦点与事件),`Tab` 与 `Core` 按引擎取文档、视图与 worker 类型。LOK 的实现在 `ely_office::lok`(`Lok`、`start`、只对 LOK 的 `Commands` 即 `run_active`/`run_in`;查找替换、面板、图表仍只限 LOK);MuPDF 的在 `apps/ely-pdf/src/engine.rs`(`Pdf`;worker 首次载入才起,`Mupdf::get`)。`Kind::macros` 为 `None` 表示该格式无宏,不扫文件。ElyPDF 每个标签一个 `PdfView` 实体(页面、图块、文字、选择、找到的词都在里面,自己绘制),`Viewer` 只管标签条、书签栏、查找框与状态栏;打开、密码、关闭询问与保存走外壳。外壳的密码框:对话框开着时后到的加锁文件排队(`Core::locked`),对话框关闭(含取消、提交)后下一个再问;每次询问一个身份(按输入框实体),连续询问的 Esc 各归各的;提交时对话框先关、再载入。ElyDoc 的加锁测试原先清的是应用自己的对话框字段,改为清外壳的。 -- 2026-10-07:本地 Web 内容宿主(C14.0,§17.3)。`crates/ely-web-content`:CEF 154.0.32(Chromium 154.0.8037.58,cef-rs a712ad9,`default-features = false`),`scripts/fetch-cef.sh` 取 minimal 包到 `engines/cef`,`.cargo/config.toml` 的 `CEF_PATH` 让构建用它。进程:应用的 `main` 先调 `ely_web_content::subprocess()`(参数里有 `--type=` 即为 CEF 子进程,载入框架后 `execute_process`,不碰 GPUI);开发构建子进程即应用本身的二进制;macOS 包里是五个辅助应用(`<二进制名> Helper`、`(GPU)`、`(Renderer)`、`(Plugin)`、`(Alerts)`,`Contents/Frameworks`,`LSUIElement`,可执行文件为 `ely-web-helper`),框架在 `Contents/Frameworks`,`packaging/macos/bundle.sh` 对依赖 ely-web-content 的应用自动放入并先签内层;查找次序 `ELY_CEF`、包内、辅助应用所在、构建时的 `engines/cef`。启动:`start` 在 GPUI 里只做准备(载入框架、给 NSApp 的类补上 Chromium 要的 `isHandlingSendEvent`、`setHandlingSendEvent:` 与包住 `sendEvent:`,并加 `CrAppProtocol`、`CrAppControlProtocol`),`initialize` 与建浏览器都在消息泵任务里、GPUI 的更新之外进行(实测:在更新里同步调 CEF,CEF 转动 AppKit 的循环,GPUI 报 RefCell 已借用、整窗不画);浏览器异步建(`on_after_created` 交回)。凡在 GPUI 里对 CEF 的调用(输入、焦点、尺寸、脚本、缩放、重新载入、关闭)都排进主线程的队列,由泵在下一轮、更新之外依次执行(实测:在实体释放时同步关浏览器,CEF 转动 AppKit 的循环去校验菜单,GPUI 的菜单回调遇已借用的应用状态而中止,约三成复现)。泵:外部消息泵,按 `on_schedule_message_pump_work` 的时间,另照 cefclient 每轮后以 1/30 秒兜底(实测:只按请求时第一轮后 CEF 再不请求,页面不载入);空闲时 CPU 约 1.5%。离屏渲染须用 Alloy 风格(`runtime_style`;Chrome 风格不出画面)。同一缓存目录只允许一个实例,第二个启动时报出占用它的进程号。画面:`on_paint` 的 BGRA 直接成 GPUI 图像,上一帧在下一次绘制时从图集释放(`drop_image`);光标随页面。输入:鼠标点击、移动、滚轮(行按 40 点),键照 cefclient 的 macOS 做法:键盘按下带字符与系统键码发 `KEYDOWN` 再发 `CHAR`(退格、回车、方向等用 AppKit 的字符),可打出字的键等 GPUI 的输入法回调后以所打的字发同样两个事件,多字或组字经 `ime_commit_text`/`ime_set_composition`(无替换范围时用 CEF 的“无范围”,两端为最大值;传空指针 CEF 不收);松开带同一字符(不带时 Chromium 当作又按一次,实测退格删两个字);拷贝、剪切、粘贴、全选走框架命令。焦点随 GPUI 的焦点给页面。页面与应用之间:CEF 的消息路由,页面调 `window.elyQuery({request, onSuccess, onFailure})`(`onSuccess` 必须有,缺了路由不发),应用收到 `Event::Query(请求, 回复)`;应用以 `run_js` 调页面。主题:每次载入与主题变化时把调色板、代码色、字体与字号比例、深浅写成 `:root` 的 `--ely-*` 变量与 `color-scheme`。缩放 `set_zoom(倍数)`。页面只许载入所在内容文件夹里的文件:按真实路径判定(`..`、百分号转义、指向外面的符号链接都算外面,文件须存在),导航与每个资源请求(脚本、图片、样式、请求等)都过这道门,资源另许 `data:` 与本页的 `blob:`,其余一律取消;另在每个 CEF 进程以 `--proxy-server=127.0.0.1:9` 与 `--proxy-bypass-list=<-loopback>` 把一切网络(含本机回环与 WebSocket,后者不经资源回调)送往不通的代理;WebRTC 走 UDP 不经代理,故另以 `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` 只许它经代理,并在每个页面上下文建立时、页面脚本之前删去 `RTCPeerConnection` 等构造(渲染进程的 `on_context_created`);页面的地址由真实路径逐字节转义生成(文件名含 `#`、`?` 照样载入);用户点出的链接以 `Event::Leave` 交给应用;页面的警告与错误只记行号(警告文字与来源都可由文档伪造,不进日志);页面自己的诊断经 `elyQuery` 交应用记录。载入失败(主框架,停止不算)、渲染进程退出、CEF 起不来都报 `Event::Failed`,页面区改显示组件库的错误视图与“重新载入”(浏览器在即重载,否则重建;CEF 起不来时只说明),载入成功才报 `Event::Loaded`。浏览器关闭时断开它与表面的互相持有,页面的内存随之释放(释放时写日志)。真窗口(macOS,开发构建与 `dist/ElyMark.app`):页面以应用深色主题画出,点选、键入中英文、回车、退格、方向键、拷贝粘贴、`elyQuery` 往返;`tests/scripts/webview.sh` 以输入法回调组“你好”、键入、读回焦点、主题变量与缩放 1.25,核对页面跳不出所在文件夹、外面的脚本不执行、本机 HTTP 收不到请求、`#` 名的页面载入、载入前被删的页面报失败、开关 20 个页面都释放内存、杀掉渲染进程后页面区不再显示旧画面、经辅助功能按下“Reload”后页面重新画出(截窗口量标记色),WebRTC 不存在且本机 UDP 收不到 STUN,并截窗口量页面画出的标记色(去掉绘制、去掉断开持有、去掉代理或不显示故障时该脚本都失败);需屏幕录制与辅助功能权限,缺即报失败。未验证:Windows 与 Linux;辅助技术(见缺口);树莓派(R-06)。 +- 2026-10-07:本地 Web 内容宿主(C14.0,§17.3)。`crates/ely-web-content`:CEF 154.0.32(Chromium 154.0.8037.58,cef-rs a712ad9,`default-features = false`),`scripts/fetch-cef.sh` 取 minimal 包到 `engines/cef`,`.cargo/config.toml` 的 `CEF_PATH` 让构建用它。进程:应用的 `main` 先调 `ely_web_content::subprocess()`(参数里有 `--type=` 即为 CEF 子进程,载入框架后 `execute_process`,不碰 GPUI);开发构建子进程即应用本身的二进制;macOS 包里是五个辅助应用(`<二进制名> Helper`、`(GPU)`、`(Renderer)`、`(Plugin)`、`(Alerts)`,`Contents/Frameworks`,`LSUIElement`,可执行文件为 `ely-web-helper`),框架在 `Contents/Frameworks`,`packaging/macos/bundle.sh` 对依赖 ely-web-content 的应用自动放入并先签内层;查找次序 `ELY_CEF`、包内、辅助应用所在、构建时的 `engines/cef`。启动:`start` 在 GPUI 里只做准备(载入框架、给 NSApp 的类补上 Chromium 要的 `isHandlingSendEvent`、`setHandlingSendEvent:` 与包住 `sendEvent:`,并加 `CrAppProtocol`、`CrAppControlProtocol`),`initialize` 与建浏览器都在消息泵任务里、GPUI 的更新之外进行(实测:在更新里同步调 CEF,CEF 转动 AppKit 的循环,GPUI 报 RefCell 已借用、整窗不画);浏览器在泵里同步建(`browser_host_create_browser_sync`;原为异步,建好之前视图已释放时浏览器无人关闭),视图要么在建之前已走(不建),要么有浏览器可关。凡在 GPUI 里对 CEF 的调用(输入、焦点、尺寸、脚本、缩放、重新载入、关闭)都排进主线程的队列,由泵在下一轮、更新之外依次执行(实测:在实体释放时同步关浏览器,CEF 转动 AppKit 的循环去校验菜单,GPUI 的菜单回调遇已借用的应用状态而中止,约三成复现)。泵:外部消息泵,按 `on_schedule_message_pump_work` 的时间,另照 cefclient 每轮后以 1/30 秒兜底(实测:只按请求时第一轮后 CEF 再不请求,页面不载入);空闲时 CPU 约 1.5%。离屏渲染须用 Alloy 风格(`runtime_style`;Chrome 风格不出画面)。同一缓存目录只允许一个实例,第二个启动时报出占用它的进程号。画面:`on_paint` 的 BGRA 直接成 GPUI 图像,上一帧在下一次绘制时从图集释放(`drop_image`);光标随页面。输入:鼠标点击、移动、滚轮(行按 40 点),键照 cefclient 的 macOS 做法:键盘按下带字符与系统键码发 `KEYDOWN` 再发 `CHAR`(退格、回车、方向等用 AppKit 的字符),可打出字的键等 GPUI 的输入法回调后以所打的字发同样两个事件,多字或组字经 `ime_commit_text`/`ime_set_composition`(无替换范围时用 CEF 的“无范围”,两端为最大值;传空指针 CEF 不收);松开带同一字符(不带时 Chromium 当作又按一次,实测退格删两个字);拷贝、剪切、粘贴、全选走框架命令。焦点随 GPUI 的焦点给页面。页面与应用之间:CEF 的消息路由,页面调 `window.elyQuery({request, onSuccess, onFailure})`(`onSuccess` 必须有,缺了路由不发),应用收到 `Event::Query(请求, 回复)`;应用以 `run_js` 调页面。主题:每次载入与主题变化时把调色板、代码色、字体与字号比例、深浅写成 `:root` 的 `--ely-*` 变量与 `color-scheme`。缩放 `set_zoom(倍数)`。页面只许载入所在内容文件夹里的文件:按真实路径判定(`..`、百分号转义、指向外面的符号链接都算外面,文件须存在),导航与每个资源请求(脚本、图片、样式、请求等)都过这道门,资源另许 `data:` 与本页的 `blob:`,其余一律取消;另在每个 CEF 进程以 `--proxy-server=127.0.0.1:9` 与 `--proxy-bypass-list=<-loopback>` 把一切网络(含本机回环与 WebSocket,后者不经资源回调)送往不通的代理;WebRTC 走 UDP 不经代理,故另以 `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` 只许它经代理,并在每个页面上下文建立时、页面脚本之前删去 `RTCPeerConnection` 等构造(渲染进程的 `on_context_created`);页面的地址由真实路径逐字节转义生成(文件名含 `#`、`?` 照样载入);用户点出的链接以 `Event::Leave` 交给应用;页面的警告与错误只记行号(警告文字与来源都可由文档伪造,不进日志);页面自己的诊断经 `elyQuery` 交应用记录。载入失败(主框架,停止不算)、渲染进程退出、CEF 起不来都报 `Event::Failed`,页面区改显示组件库的错误视图与“重新载入”(浏览器在即重载,否则重建;CEF 起不来时只说明),载入成功才报 `Event::Loaded`。浏览器关闭时断开它与表面的互相持有,页面的内存随之释放(释放时写日志)。真窗口(macOS,开发构建与 `dist/ElyMark.app`):页面以应用深色主题画出,点选、键入中英文、回车、退格、方向键、拷贝粘贴、`elyQuery` 往返;`tests/scripts/webview.sh` 以输入法回调组“你好”、键入、读回焦点、主题变量与缩放 1.25,核对页面跳不出所在文件夹、外面的脚本不执行、本机 HTTP 收不到请求、`#` 名的页面载入、载入前被删的页面报失败、开关 20 个页面、5 个在建浏览器之前丢弃、10 个在打开后 0 至 27 毫秒内丢弃,都在保持前释放内存、杀掉渲染进程后页面区不再显示旧画面、经辅助功能按下“Reload”后页面重新画出(截窗口量标记色),WebRTC 不存在且本机 UDP 收不到 STUN,并截窗口量页面画出的标记色(去掉绘制、去掉断开持有、去掉代理或不显示故障时该脚本都失败);需屏幕录制与辅助功能权限,缺即报失败。未验证:Windows 与 Linux;辅助技术(见缺口);树莓派(R-06)。 - 2026-10-07:ElyTree 恢复与清理、外部工具、操作规则(C13.6,TREE-13、14,§13.3,QA-19、20)。仓库页第二行:在终端中打开(macOS `open -a Terminal`,其他平台报未构建)、在访达中显示(`reveal_path`)、清理未跟踪的文件…;页底“引用日志”列出 HEAD 最近 200 次移动(`reflog show -z`,位置、Git 写的原因、时间),选中即中间显示该提交(同提交详情,`Whose::Reflog`),上方“在此新建分支…”(找回删掉的分支)与“把当前分支移到此…”:有已暂存的改动即拒绝(`reset --keep` 会丢掉它们,请先提交或取消暂存);否则在后台算出离开该分支的提交,对话框写明分支、从哪到哪、离开的提交(引用日志仍可找回)与“工作区改动保留”,确认后 `reset --keep`(Git 在改动冲突时拒绝;没有任何按钮做硬重置),执行前在写队列里核对 HEAD 仍在预览时的分支(完整引用名,或仍游离)与提交、仍无暂存,任一变了即什么也不做。引用日志的时间是该次移动的时间(`--date=unix` 下的 `%gd`),不是提交时间;选中按事件本身(提交、原因、时间与它在全部引用日志中自最早起的序号,后者新移动不改,同一秒同原因的两次也分得开),同一提交的多条不会一起选中;之后有新的移动时该事件的位置后移,选中与详情跟着它,事件已不在引用日志里即不显示。清理:对话框先由 `git clean -n -d`(勾“包括被忽略的文件”另加 `-x`)得出 Git 会删除的路径(`core.quotePath=false`,其余 C 引号照 Git 规则还原),其中的文件夹当场展开为其中的文件(`ls-files --others`,同一忽略规则,另一仓库不入),对话框逐个列出文件与文件夹;确认后先逐段查每个所列路径:仍在仓库里、途中没有符号链接、文件仍是文件、文件夹仍是文件夹(任一变了即整体拒绝,什么也不删),再以 `git clean -f [-x] -- 所列文件` 删去(只给文件,Git 不递归;此后被跟踪了的文件 Git 留下,也不穿过链接),最后自深而浅删去所列文件夹中已空的(预览后在其中新出现的文件不动,文件夹随之留下),每次都问。文件标题栏多出“在编辑器中打开”(用户在“仓库›选择编辑器…”选的程序,存在数据目录 `tools.json`,字段类型不对与读不出同样报错且不覆盖;macOS 的 .app 经 `open -a`,在后台等它结束,失败即显示其原话;未选时先问)与 Markdown 的“在 ElyMark 中打开”(`ely_platform::apps::open`:按安装布局找同级的应用,macOS 的 `名称.app/Contents/MacOS`,Windows 的 `Ely Office\名称`,开发构建的同一目录;找不到即报错)。锁(§13.3):任何写操作因 Git 的锁文件失败(`….lock': File exists`,或配置文件的 `could not lock config file …: File exists`)时,对话框说明另一个 Git 正在工作、Ely 从不删除锁文件,“重试”把同一写操作重新排队。QA-19:测试都以独立的 Git 命令核对索引、提交、引用与工作区。QA-20:冲突、钩子失败、凭据被拒(本机 401 服务器,显示 Git 的 “terminal prompts disabled” 原话)、锁、变基中断后中止、取消(停止取回)都有窗口测试,结束时仓库状态以 Git 读回核对。切换到系统 Git 目前经 `ELY_GIT`(设置页随 C18.1);状态栏显示所用 Git 的路径与版本。真窗口(macOS):仓库页工具行、清理预览对话框、文件标题栏的编辑器与 ElyMark 按钮,按下后同级的 ely-mark 以该文件启动。未验证:Windows 与 Linux 的终端;ElyMark 打开文件的内容(C14)。 - 2026-10-07:ElyTree 储藏、远程、工作树、子模块、LFS(C13.5,TREE-10 至 12,§13.3)。左栏第四页“仓库”:上方取回(`fetch --progress --all`)、拉取(`pull --progress`,照用户的 pull 设置)、推送、强制推送…;储藏(`stash list -z`,新的在前;“储藏改动…”问说明与是否包括未跟踪的文件,`stash push`),远程(名称与地址,`remote` 加 `config --get-regexp` 读出取回与推送地址),工作树(`worktree list --porcelain -z`,行名为文件夹名,有失效的可“清理”即 `worktree prune`),子模块(索引里的 gitlink 与文件夹自身 `.git` 的 HEAD:未检出、在记录的提交上、在别的提交上、冲突),LFS 文件(`git lfs ls-files --long --json`:工作树里是内容、对象在本机而工作树是指针、只有指针而对象不在本机);没有子模块或 LFS 文件时不列。各列表分别读出,一个读不出只在该处显示 Git 原话;切到此页时读,此后每次状态重读时重读,只取最新一次。选中一项,中间显示:储藏为提交详情的同一套(`hist::Picked`,历史与储藏共用读取与面板):改动的文件(对第一个父提交)与储藏的未跟踪文件(第三个父提交,各自对空树),选文件看差异,上方应用、弹出、删除…;远程为地址与推送地址,取回、修改…(改名 `remote rename`、改地址 `remote set-url`)、移除…(先问);工作树为路径与分支或提交、锁定与失效原因,打开(作为标签)、移除…(先问,主工作树与当前的不给);子模块为记录的与检出的提交,“检出记录的提交”(`submodule update --init`,经远程运行)与打开。用户输入的名称、地址、路径前一律加 `--`(推送的远程名同样)。表单与选择对话框(新建分支、标签、改名、身份、储藏、远程、工作树、选远程与上游)记下打开时的仓库,提交时写进那个仓库,不随当前标签变;储藏列表按每条三个字段读(说明可为空)。储藏的应用、弹出、删除按 `stash@{n}` 执行,执行前在写队列里核对它仍是所见的那个储藏(另有储藏插入即拒绝,什么也不做);弹出遇冲突时 Git 保留储藏,对话框显示 Git 原话。删除储藏、移除远程与工作树每次都问。分支页的本地分支多出“设置上游…”(列出远程分支,`branch --set-upstream-to=refs/remotes/名`)与“取消上游”。推送:有上游即 `push --progress`;没有上游时后台读出远程,问推到哪个远程(`push --set-upstream -- 远程 refs/heads/分支:refs/heads/分支`),没有远程或 HEAD 游离即说明。强制推送(§13.3):后台读出当前分支、上游的远程与远端分支、上次取回时远端的提交(租约)、此刻 HEAD 的提交(源)与远端有而源没有的提交,对话框写明远程、分支与将被覆盖的提交(或写明没有),确认后推送的就是所示的源提交(`push --force-with-lease=远端分支:租约提交 -- 远程 源提交:远端分支`),确认前本地分支再动也不改变推送内容;远端自上次取回后变了即被拒绝;没有上游或游离即说明不能。远程命令(取回、拉取、推送、子模块更新)是写队列里的一种(`Write::Remote`),经 `running::Running` 运行:Git 在自己的会话里(`setsid`,SSH 找不到终端,不会挂在终端提示上;凭据照用 credential helper 与 ssh-agent),进度逐行显示在中间上方的提示条,可停止(向整个进程组发 SIGTERM,Git 自行清理;已结束的不再发);用户停止时提示“已停止”、清掉其后的排队写,不当作失败;失败时对话框显示 Git 的 stdout 与 stderr(以回车结尾的进度行被下一行改写,只留最后一次;以换行结尾的行全部保留,钩子的每一行都在);拉取遇冲突照合并类命令判定为停下。关窗与退出时停下所有远程命令与克隆。克隆改用同一运行器。真窗口(macOS):仓库页五类列表、储藏详情与文件、工作树详情、分支行。未验证:Windows 与 Linux(停止远程命令在非 Unix 上报未构建);真实网络与 HTTPS、SSH 凭据;LFS 对象的下载(只显示状态)。 - 2026-10-07(用户定):跟随组件库最新的 main。组件库 K01 把 gpui 从 zed 仓库的提交换成 crates.io 的 `gpui-pre` =0.3.8(`gpui-pre-platform` 同版本),K02 让 Tab 绑在 FocusScope 的 ElyFocus 上下文下、更深的上下文先得;本项目工作区的 `gpui`、`gpui_platform` 随之改为这两个包(以 `package =` 保留原名,源码不变),组件库 pin 到 dabc1eb(其上加本项目一个提交:树与树行的无障碍角色)。升级后全工作区编译无改动。 diff --git a/crates/ely-web-content/examples/webprobe.rs b/crates/ely-web-content/examples/webprobe.rs index 87cf75c..b292a47 100644 --- a/crates/ely-web-content/examples/webprobe.rs +++ b/crates/ely-web-content/examples/webprobe.rs @@ -16,6 +16,10 @@ use gpui::{ /// How many views open and close to show their release. const CHURN: usize = 20; +/// Views dropped before the pump makes their browsers. +const UNMADE: usize = 5; +/// Views dropped within 3 ms steps of opening. +const EARLY: u64 = 10; struct Probe { view: Entity, @@ -182,14 +186,29 @@ impl Probe { /// Opens and drops pages; each must free its memory. fn churn(&mut self, window: &mut Window, cx: &mut Context) { + let page = self.fixtures.join("probe.html"); if self.churned == CHURN { println!("probe: closed {CHURN}"); + for _ in 0..UNMADE { + drop(WebView::open(&page, &self.fixtures, window, cx).expect("a view")); + } + for step in 0..EARLY { + let view = WebView::open(&page, &self.fixtures, window, cx).expect("a view"); + self.others.push(view.clone()); + later(cx.weak_entity(), step * 3, window, cx, move |probe, _, _| { + probe.others.retain(|other| *other != view); + }); + } + println!("probe: dropped {UNMADE} unmade, {EARLY} early"); let this = cx.weak_entity(); - later(this, 2000, window, cx, |_, _, _| println!("probe: hold")); + later(this, 2000, window, cx, |_, _, _| { + // The log marks what freed before the hold. + log::info!("probe: hold"); + println!("probe: hold"); + }); return; } self.churned += 1; - let page = self.fixtures.join("probe.html"); let view = WebView::open(&page, &self.fixtures, window, cx).expect("a view"); cx.subscribe_in(&view, window, |probe, view, event, window, cx| { if matches!(event, Event::Loaded) { diff --git a/crates/ely-web-content/src/engine.rs b/crates/ely-web-content/src/engine.rs index d9db4b1..43b7e2c 100644 --- a/crates/ely-web-content/src/engine.rs +++ b/crates/ely-web-content/src/engine.rs @@ -201,7 +201,8 @@ fn make_browsers() { windowless_frame_rate: 60, ..Default::default() }; - let made = browser_host_create_browser( + // Made at once: no view ever waits on a browser. + let made = browser_host_create_browser_sync( Some(&info), Some(&mut handlers::client(&surface)), Some(&url.as_str().into()), @@ -209,9 +210,15 @@ fn make_browsers() { None, None, ); - if made != 1 { - surface.lock().expect("the surface lock").failed = - Some(format!("CEF made no browser for {url}")); + match made { + Some(browser) => { + handlers::adopt(&browser, surface.clone()); + surface.lock().expect("the surface lock").browser = Some(browser); + } + None => { + surface.lock().expect("the surface lock").failed = + Some(format!("CEF made no browser for {url}")) + } } } } diff --git a/crates/ely-web-content/src/handlers.rs b/crates/ely-web-content/src/handlers.rs index fb213b9..f429dae 100644 --- a/crates/ely-web-content/src/handlers.rs +++ b/crates/ely-web-content/src/handlers.rs @@ -345,20 +345,9 @@ wrap_life_span_handler! { impl LifeSpanHandler { fn on_after_created(&self, browser: Option<&mut Browser>) { - let Some(browser) = browser.map(|b| b.clone()) else { return }; - log::info!("web: browser {} made", browser.identifier()); - // Its view went before it came: close it, keep nothing. - if self.surface.lock().expect("the surface lock").closed { - log::info!("web: browser {} closes: its view is gone", browser.identifier()); - crate::engine::later(move || { - if let Some(host) = browser.host() { - host.close_browser(true as _); - } - }); - return; + if let Some(browser) = browser { + log::info!("web: browser {} made", browser.identifier()); } - adopt(&browser, self.surface.clone()); - self.surface.lock().expect("the surface lock").browser = Some(browser); } fn on_before_close(&self, browser: Option<&mut Browser>) { diff --git a/tasks/BUGS.md b/tasks/BUGS.md index ca9d558..6511ebc 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -72,7 +72,7 @@ - [x] M-09 导出的保存面板出错被当作取消 — 面板出错走 panel_failed,取消只记日志(审查 1 轮) - [x] M-10 关闭查找框只清当前标签的高亮 — 关闭查找框对每个标签的页面清除高亮(审查 1 轮) - [x] M-11 页面把搜索词写进应用日志 — 控制台的文字与来源都可由文档伪造,只记行号;页面自己的诊断经 elyQuery;R1 前缀可伪造、R2 data 来源、R3 sourceURL、R4 编码路径与目录、R5 缺回归测试:已补并经变异核对(五轮上限,未再审) -- [ ] M-12 浏览器创建完成前 `WebView` 已释放,浏览器泄漏 +- [x] M-12 浏览器创建完成前 `WebView` 已释放,浏览器泄漏 — 浏览器在泵里同步建好即登记,视图要么在建前已走、要么有浏览器可关;webprobe 加建前丢弃与 0–27 毫秒丢弃,35 页都在保持前释放(审查 4 轮) - [ ] M-13 编辑器构建失败时页面空白且不上报 - [ ] M-14 `render_pdf` 在界面线程内嵌图片并写文件 - [ ] M-15 打印与导出 PDF 的暂存页执行文档里的脚本 diff --git a/tasks/reviews/M-12-r1.md b/tasks/reviews/M-12-r1.md new file mode 100644 index 0000000..7e08a6b --- /dev/null +++ b/tasks/reviews/M-12-r1.md @@ -0,0 +1,3 @@ +FAIL + +crates/ely-web-content/src/handlers.rs:347 — Missing regression coverage for M-12. No retained test closes a view after browser creation is submitted but before `on_after_created`; the existing `webprobe` churn closes only after `Event::Loaded`. Removing the entire `surface.closed` branch in a review copy leaves all nine WSL library tests passing, while real CEF probes on Windows and WSL reproduce the retained browser/surface on the first such close (the current implementation passes 20 cycles). The original PERF-08 leak can therefore return undetected. Add a regression exercising the production Drop, late creation callback and deferred close, asserting forced browser closure and release of the surface; verify it fails when this branch is removed. Evidence: `.work/m12-r1/linux-mutant-repository.log`, `.work/m12-r1/windows-native-mutant.log`, `.work/m12-r1/linux-native-mutant.log`. diff --git a/tasks/reviews/M-12-r2.md b/tasks/reviews/M-12-r2.md new file mode 100644 index 0000000..130c554 --- /dev/null +++ b/tasks/reviews/M-12-r2.md @@ -0,0 +1,3 @@ +FAIL + +crates/ely-web-content/examples/webprobe.rs:193 — R1's regression-coverage defect remains: the 0–27 ms timers do not ensure a view is dropped after browser creation is submitted but before `on_after_created`. In review copies on both Windows and WSL, restoring asynchronous creation and unconditional callback adoption (the original M-12 bug) still satisfies the new release assertion: 30/30 surfaces freed before hold. When the actual `WebView::Drop` and deferred close run deterministically between submission and the creation callback, that same mutant frees only 20/30; the current synchronous implementation frees 30/30. Thus the original PERF-08 leak can return without this regression probe detecting it. Retain a deterministic lifecycle regression covering that ordering and verify that it rejects the original leaking implementation. Evidence: `.work/m12-r2/windows-async-result.log`, `.work/m12-r2/linux-async-result.log`, `.work/m12-r2/windows-async-forced-result.log`, `.work/m12-r2/linux-async-forced-result.log`; probe method and cleanup are recorded in `.work/m12-r2/evidence.log`. diff --git a/tasks/reviews/M-12-r3.md b/tasks/reviews/M-12-r3.md new file mode 100644 index 0000000..0699e6f --- /dev/null +++ b/tasks/reviews/M-12-r3.md @@ -0,0 +1,3 @@ +FAIL + +crates/ely-web-content/examples/webprobe.rs:193 — The early-close regression does not ensure a drop before `make_browsers`. On Windows and WSL, the retained churn frees 30/30 surfaces, but every drop already has a browser; no closed surface is skipped. Removing only the pre-creation `surface.closed` guard, while retaining synchronous creation and adoption, still passes this probe with 30/30 released. Yet opening and dropping ten views in one GPUI update before the pump runs frees only 20/30 with that mutant, versus 30/30 with current code: the deferred close runs before creation, and the ten subsequently created browsers leak. This is a reachable ordering in the current design, unlike R2's removed asynchronous ordering. Add a deterministic pre-pump open/drop case and verify its release assertion rejects removal of the closed guard. Evidence: `.work/m12-r3/{windows,linux}-no-skip-{retained,pre}-result.log`; method, current-code results and cleanup: `.work/m12-r3/evidence.log`. diff --git a/tasks/reviews/M-12-r4.md b/tasks/reviews/M-12-r4.md new file mode 100644 index 0000000..0cdd613 --- /dev/null +++ b/tasks/reviews/M-12-r4.md @@ -0,0 +1 @@ +PASS diff --git a/tests/scripts/webview.sh b/tests/scripts/webview.sh index b3791b8..ea50b1c 100755 --- a/tests/scripts/webview.sh +++ b/tests/scripts/webview.sh @@ -65,11 +65,12 @@ probe= out=$(cat "$work/out") for want in "probe: value=你好a" "probe: focused=text" "probe: zoom=1.25" "probe: stayed" \ "probe: outside=false" "probe: network=refused" "probe: udp=silent" "probe: rtc=undefined" "probe: main failed" "probe: hash loaded" "probe: missing failed" \ - "probe: closed 20"; do + "probe: closed 20" "probe: dropped 5 unmade, 10 early"; do grep -qxF "$want" <<<"$out" || fail "expected '$want', got: $out" done grep -qE '^probe: fg=rgba\([0-9]+, [0-9]+, [0-9]+, ' <<<"$out" || fail "no theme token: $out" -freed=$(grep -c "a page's memory is freed" "$work/err" || true) -[ "$freed" -ge 20 ] || fail "only $freed of 20 closed pages freed their memory" +# Freed before the hold: 20 closed, 5 unmade, 10 early. +freed=$(awk "/probe: hold/ { exit } /a page's memory is freed/ { n++ } END { print n + 0 }" "$work/err") +[ "$freed" -ge 35 ] || fail "only $freed of 35 closed pages freed their memory" ! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")" echo "ok: webview (marker share $share, $freed pages freed)"