Skip to content

Releases: mateo-ki/ltool

ltool v1.5.1

Choose a tag to compare

@github-actions github-actions released this 25 Oct 11:43

🚀 ltool v1.5.1 发布说明 / Release Notes

🎉 欢迎使用 ltool 1.5.1!本次共 3 笔改动,重点是 修掉 1.5.1 自己引入的死锁、FileIndexer 两个并发崩溃窗口上锁(重建 rem…、发版 v1.5.0 那一趟的账。下面每一节是一笔提交,原因和实测数字都在里面。


🌟 本次改动

🐛 1. 修掉 1.5.1 自己引入的死锁:rebuildDeepAll 的写锁块里调 mapSnapshot,而它尾部又拿同一把非递归写锁(--storeexit 挂死、CI v1.5.1 那趟红的正是它);锁改为调用方持有,reindexAsync 首映射点补锁

  • (这一笔没写正文,标题就是全部。)

🐛 2. 1.5.1:FileIndexer 两个并发崩溃窗口上锁(重建 remap / USN 增量提交),--smoke 不再落盘改用户的 index/deep(裸跑改用进程内开关跳深扫);本机那份误出的第二份 1.5.0 安装包已删

  • (这一笔没写正文,标题就是全部。)

🐛 3. 发版 v1.5.0 那一趟的账,外加修掉复验脚本在这台机上必崩的一颗坑

  • tag v1.5.0 → 468f960,CI run 39951821904 completed success,Release 421810352、
  • 24 个资产(20 颗 dll + setup + sdk + plugins-index.json + sha256.txt)。公开面三查
  • POSTRELEASE_BAD=0:说明里家目录 0 行 / 清单 200 且 20 条 == 从那个 tag 数出来的颗数 /
  • sha256.txt 23 行 digest 与资产 API 逐字对上。
  • 修的是 tools/postrelease_check.py 的 get():资产那一跳 302 到 CDN 主机,schannel 取不到
  • 吊销服务器就整条 rc=35 CRYPT_E_REVOCATION_OFFLINE(直连和经代理都一样,而 api.github.com
  • 那一跳没事),读起来像"商店那条链没活"。Windows 上加 --ssl-no-revoke(证书链照验)。
  • 补完还崩过一次是另一种失败:rc=28 github.com:443 连不上,原样重跑第 2 次整条绿。
  • 只动工具 + 文档,产品代码一行没动 ⇒ 不升号。但 1.5.0 从此是已发布的号,
  • 下次动产品代码得往前挪。

📦 下载与安装

Windows 10 及以上(安装包已声明 MinVersion=10.0)。按用户安装,不需要管理员,默认装到 %LOCALAPPDATA%\Programs\ltool;双击安装向导走完即可,托盘常驻,Alt+Space 呼出。

文件 大小 sha256 前 8 位 说明
ltool-1.5.1-setup.exe 21.58 MB bb1e9853 就用这个:双击安装/升级(约 22 MB,开箱零插件,插件由商店按需拉)
ltool-sdk-1.5.1.zip 273 KB 16232d2b 插件 SDK 快照:头文件闭包 + 导入库,给独立编插件用
plugins-index.json 11 KB 4b558ab4 插件商店清单:这一版 app 读的就是它(地址按版本号钉死,见下面那条)
ltool-ads.dll 104 KB 91372b0d 商店里的插件 ads(v1.0.2,插件接口 4)
ltool-base64.dll 68 KB 35c5411e 商店里的插件 base64(v1.0.2,插件接口 4)
ltool-capture.dll 361 KB 6c74cd96 商店里的插件 capture(v1.0.7,插件接口 4)
ltool-clipboard.dll 135 KB 208651b3 商店里的插件 clipboard(v1.1.3,插件接口 4)
ltool-disk.dll 180 KB a6fbf5fc 商店里的插件 disk(v1.0.2,插件接口 4)
ltool-downloads.dll 66 KB dab65833 商店里的插件 downloads(v1.0.2,插件接口 4)
ltool-grid.dll 155 KB e9aae763 商店里的插件 grid(v1.0.4,插件接口 4)
ltool-json.dll 79 KB 3c721658 商店里的插件 json(v1.0.3,插件接口 4)
ltool-language.dll 174 KB 622d1232 商店里的插件 language(v1.0.2,插件接口 4)
ltool-linuxdo.dll 178 KB 780ebe45 商店里的插件 linuxdo(v1.0.2,插件接口 4)
ltool-network.dll 100 KB 15209232 商店里的插件 network(v1.0.1,插件接口 4)
ltool-random.dll 136 KB 1abf1daf 商店里的插件 random(v1.0.0,插件接口 4)
ltool-record.dll 276 KB 9dd6eb8c 商店里的插件 record(v1.0.2,插件接口 4)
ltool-registry.dll 94 KB 2d0d3007 商店里的插件 registry(v1.0.2,插件接口 4)
ltool-repair.dll 88 KB de2a26b3 商店里的插件 repair(v1.0.2,插件接口 4)
ltool-shell.dll 135 KB 477db904 商店里的插件 shell(v1.0.2,插件接口 4)
ltool-software.dll 237 KB 80d3ee85 商店里的插件 software(v1.0.2,插件接口 4)
ltool-startup.dll 150 KB f3689364 商店里的插件 startup(v1.1.0,插件接口 4)
ltool-tasks.dll 96 KB 26e16de2 商店里的插件 tasks(v1.0.1,插件接口 4)
ltool-user.dll 140 KB 37032817 商店里的插件 user(v1.0.3,插件接口 4)
sha256.txt 2 KB — 上面这些文件的 SHA-256 全表(Get-FileHash -Algorithm SHA256 逐行对得上)

一共 24 个文件。校验:Get-FileHash <文件> -Algorithm SHA256,或下 sha256.txt 对整批。

插件商店读的是 releases/download/v1.5.1/plugins-index.json —— 清单和这批插件 dll 都出自编出这一版 core 的那同一次构建,所以"商店里那颗和这台机器的 core 对不上"这件事从源头没了(对不上也只会是被拒绝并说清,不是闪退)。


Full Changelog: v1.5.0...v1.5.1

ltool v1.5.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 12:22

🚀 ltool v1.5.0 发布说明 / Release Notes

🎉 欢迎使用 ltool 1.5.0!本次共 12 笔改动,重点是 商店顶部那两行、「为什么时不时有大量磁盘 IO 和 CPU」这轮全部拿数回答、广告弹窗那页加了一颗「从正在跑的进程里挑一个」放行(ads…。下面每一节是一笔提交,原因和实测数字都在里面。


🌟 本次改动

🧩 1. 商店顶部那两行:短话上页面,全文进悬停说明和日志(他一句「不要放到插件详情页面」,选的是 A 档)

  • 他给了一张截图:那两行是「网络拉取未成功:Operation timed out(HTTP 0,走的是 https://…v1.5.0/plugins-index.json)—— 当前展示本地缓存清单(共 19 条)」和「自动更新:10-23 11:02 · 没有要换的(网络没成(Error transferring … 404 …)),拿本地缓存那 19 条清单试)」。三个落点摆给他(页上留短话 / 整行挪去「关于」/ 页面一行都不留),他选「页上留一句短话,全文进日志」。
  • 一条规矩只留一个出口:ToolsPage::setStoreLine(短话, 全文) 接管那两颗标签的全部 11 处写入(拉取中 / 失败有缓存 / 失败无缓存 / 成功那条 / 一键那五句 / 建墙那两句),setAutoNote() 管自动更新那一行。那五句一键状态原先各写各的 m_storeStatus->setText(...),一并改走这颗 —— 不是顺手:「上一句的悬停说明要收掉」这条规矩只有在一个出口的前提下才成立。页面那四句变成「正在拉清单…」「网络没通,先看本地缓存那 19 条」「清单没拉到,这一面退回本机装着的那几个,它们照常能用」「清单 20 条 · 这一面 20 张卡 · 能装或可更新 0 · 拒收 0 · 清单外 0 —— …」;URL、走的哪一档、试了几次、拒收的是谁都在悬停说明里,日志那三份本来就带原文,一处没减。
  • 两份唯一实现(不这么写就会漂):
  • 自动更新那句里"哪条链拿到的"从 note 搬进新键 store/autoUpdateWhy,note 只留「清单 19 条」这种短话;autoUpdateSummary 那颗纯函数没动(判据一直喂它短 src)。
  • PluginStore::autoBrief() = "配置里存的那条 note → 页面上那句短话"那颗谓词,读的时候也要过一刀:1.5.0 之前 note 存的就是整条原文,只改写入点的话他屏幕上那行要一直难看到明天早上跑完下一次自动更新。切法:带 http 的从第一个「(」切掉;切完还带 http(老格式里没括号那句)就退成「上次那一趟没成(原文在悬停说明里)」—— 宁可退这一句也不把原文推上页面。原文一个字不丢:没写 why 时悬停说明拿回那条老 note。
  • 数字:--storetest 109 条 0 红 / 9,661 ms(新 5 条:带 http 的切短、不带 http 的原样过不许多切一刀、没括号的退固定短话、老 note 进悬停说明、写了 why 之后两份各管各的;另有一条改名 —— 原来问"清单从哪条拿到的要带出来",现在带出来的是短话、链在 why 那份),--plugintest 396 → 399 条 0 红 / 8,125 ms(windows 平台,新 3 条:建墙时短话带悬停说明、短话接管后收掉、页面上那句不许有 http),离线九面旗全套 859 条 0 红(--rectest 在 offscreen 必红 7 条是"那颗要真窗口才录得到",单独在 windows 平台量到 0 红,不是回归),闸六道全 0。
  • 我自己这一笔带出来的两条真红,先说清再改:① 把 check() 第三个位置当"报告文本"用了,它是 want ⇒ 那条自己判自己红(got=[yes] want=[没有要换的]);② 在"没跑过时那一行说今天还没跑"之前先把 note 写进了配置,它读到上一句留下的值 —— 记忆里那条"判据读到上一查询留下的状态"这次是红给我看的,改法是那条判据前先 remove 两个键。
  • 反向验两条各咬住一处:autoBrief 改恒等 ⇒ storetest 红 3 条;"清悬停说明"改成只在非空时才设 ⇒ plugintest 红 1 条;改回来重编(grep -c MUTATION = 0 才继续)两边各 0 红。
  • 顺带把 --storenet 那栏的等待认了新短句:它以前只等「清单…」开头的状态行,而失败有缓存那句现在改成「网络没通…」开头 —— 不认它,这一栏在未发版那次只会白等 20 秒,而"页面上不许有 URL"那条判据就永远只在一条跑不到的成功路径上量(装饰性的绿)。现在正则读 文本 + 悬停说明 合起来那份,"不许有 http"读短的那份。
  • 版本:不升号。core 仍在未发布的 1.5.0(这一档吸收同批功能),插件一颗没动(plugin_version_gate 认),API 仍 4;多了一个 store/autoUpdateWhy 键但老配置读得动(没这个键就走"悬停说明拿回老 note"),不算不兼容。这轮没装机 —— 装着的还是 11:54 那份(1900d67),它照旧显示整条原文,装完才会变短。

📦 2. 「为什么时不时有大量磁盘 IO 和 CPU」这轮全部拿数回答,没动产品代码一行

  • 先给结论的三条依据:
  • 静置的常驻那颗(pid 23336)连采 12 个窗口 \Process(*)\IO Read/Write Bytes/sec = 读 0.0 / 写 0.0 KB/s,整榜排不进前 10;20 秒 TotalProcessorTime 增量 16 ms = 单核 0.1%;工作集 152→152 MB。⇒ 5 秒的窗口扫描、2 秒的剪贴板轮询都不是来源。
  • 同窗整机排名:qoder cn 1,221/1,217 KB/s(就是我这个会话)、chrome 967/970、qqpctray 54/6、wechatappex 13/13。他看见的盘灯此刻主要不是 ltool 给的。
  • 一次完整扫描(沙箱数据目录里跑产品同一条 startScan()):墙钟 2,952 ms、CPU 1.11 核·秒、索引 9,409 条目,硬缺页只有 0–28 次/秒(累计约 9 页)⇒ 目录枚举读的是 NTFS 元数据,一在全系统文件缓存里,二记在 System/缓存管理器头上、不记在 ltool 自己的进程 IO 计数里 —— "进程读 0"和"盘灯亮"两件事不矛盾。同一份探针里 PhysicalDisk 那栏采样器没匹配上实例名,是空的,所以这一格我拿硬缺页那列说事,不拿它。
  • 节奏和形状(他日志原时间戳):12:06 / 12:27 / 12:45 / 13:16 / 13:58 / 14:44 各一次 scan finished: 350 apps ⇒ 3.5 小时 6 次,约每 20–45 分钟;点火的是挂在开始菜单 116 个目录上的 watcher,节流只保证 ≥60 秒,另有一颗和 index/refreshHours=7 同周期的。要点是重扫不只重扫快捷方式:startScan() 尾巴上挂着 reindexAsync(),桌面 4 层 / 文档·下载 3 层 / 图片·音乐·视频 2 层 / 主目录 1 层每次跟着整棵再走一遍 —— 这就是"时不时一下":一次约 1 核·秒的 CPU 脉冲,盘上很轻。
  • 排除掉的一条:deep index / MFT / volume 在他这份日志(10-21 16:12 起)里 0 次命中,配置 index/deep=false ⇒ 那条每卷直读 $MFT 的路根本没跑;deepindex.dat 那 335 MB 是 10-03 留下的死文件,占地方不产生 IO。
  • 两条我自己的账,写进文档而不是只在这儿说:
  • 15:00:26–15:02:09 那串 v1.5.0/plugins-index.json 三镜像 404 + 直连两次 20 秒超时是我造成的:--smoke、--mfttest 不在 src/main.cpp:622-632 那份沙箱名单里,直接落进他真实的数据目录,把"一天一次拉清单"点着了,还往他的缓存里写了东西。
  • 顺着这条查出一个真问题:src/main.cpp:227 让非提权的 --smoke 把 index/deep 写成 false(本意只该管 CI 那份配置)。全项目只有两处写这个键(另一处是配置页那颗勾),而他现在是 false ⇒ 如果他自己没关过,深索引就是我某次不带沙箱的 --smoke 给他关掉的。把两面旗挪进沙箱名单 + 要不要替他把深索引打开,等他拍。
  • 探针留在仓库:build/verify/io_probe2.ps1(沙箱跑一趟同时采 CPU/硬缺页)、build/verify/io_top.ps1(整机按进程排名 IO)。两张都走 $env:TEMP,加进 git 后重跑 tracked_leak_check:580 张扫,TRACKED_LEAK_HITS=0。
  • 版本:没动产品代码 ⇒ 不升号,core 仍 1.5.0、插件接口仍 API 4。

✨ 3. 广告弹窗那页加了一颗「从正在跑的进程里挑一个」放行(ads 1.0.1 → 1.0.2):手打进程名这件事不用他记

  • 他一句话:"广告弹窗的忽略进程可用点击一个按钮选择进程列表的一个进程进行忽略"。落在那页「编辑规则…」里 那一栏底下,不开新页:按下弹一张此刻在跑的进程,双击一行或选中按「加进去」就写进「忽略的进程」那一栏,可以连着挑几颗,最后照样按「保存」才生效 —— 和旁边那四栏同一个草稿/保存模型,不搞第二套。挑中一颗本来就在名单里的,当场点名「被「wechat」盖着」,不加第二遍;列不出进程的机器上那句话照实说,不摆一张空表当"没进程在跑"。筛是藏行不重建(重建会把选中项和滚动位置一起丢掉,软件页那条老规矩)。
  • 两份唯一实现:① 映像名 → 名单那一行的规整法(小写、去 .exe)原来只长在"右键放行这一颗"那一句里,现在两条路都要用,抽成 AdBlocker::allowProcFor(),两处各写一遍迟早会不一样;② "这颗是不是已经被盖住"抽成 coveredByAllowProc(),方向照那一侧的真匹配来(进程名包含片段),不许反过来。
  • 列进程加在平台缝第五道上:Platform::runningImageNames() —— Windows 用 CreateToolhelp32Snapshot,按映像名去重(这台机 260 个进程压成 89 行:浏览器、IM、svchost 都是一个 exe 跑好几份)+ 不区分大小写排序;POSIX 那颗如实返回空,注释写明"是没写、不是做不到"(Linux 上读 /proc//exe 可行,谁要接就接,别让它假装成功)。
  • 数字:--plugintest 389 → 396 条 0 红(这一栏 8 条),offscreen 和 windows 两个平台各跑一遍都 0 红,整趟实测 7650 ms;离线全套 1058 条 0 红 / 3 条点名 SKIP;闸全绿(theme 0、proxy 0、plugin_version NEEDS_BUMP=0、core_abi 0 ⇒ API 仍 4、tracked_leak 0、notes 两查 0);插件号 ads 1.0.1 → 1.0.2。
  • 反向验抓出了我自己写的一条假绿:那条写的是"名单里那行空的不能盖住一切",我把 continue 改成不跳过空片段,函数回的还是空串、调用方照样当"没盖住" ⇒ 396 条全绿,判据一动不动。两个原因叠在一起:那个输入根本到不了这儿(存进名单前 linesToPatterns 就把空行丢了),而"空串"这一个值同时表示"没盖住"和"被一条空的盖住了" —— 两个意思挤在一个返回值里,谁也不能把它判红。换成一条打得动的:名单里那条比这颗更长 ⇒ 不算盖住(包含方向不能反),再把 one.contains(p) 翻成 p.contains(one) 重编 ⇒ 那条当场红。这是今天第二次撞到"判据在 0 个对象上空转"那一族(第一次是 has(..., true) 那种永远真不了)。
  • 没做的:那颗按钮和那张表本身没有判据 —— plugins/ads 没有自带判据文件(--gridtest / --randtest 那种),要补得先给它加一颗 --adstest 并接进 CI 那栏;纯函数那两颗钉住了,界面这半现在是"我本机没点过、CI 也没数"。
  • 版本:1.5.0 不动(未发版,同批功能吸收);插件 API 仍 4(新增的都是 core 侧函数,ProcInfo.h 不在 SDK 那 26 份头的闭包里)。没装机。

🎨 4. 「一键下载全部」那颗按钮的预留宽度改成量 sizeHint(),不按字数估

  • 本机 windows 平台下这条判据红了三次才认账:两版标签都塞得进预留的那块 got=[0/1]。上一版留宽度是按「几个字 × 一个字宽」估的,而且只按「确认这 12 颗」那一版留 —— 可基线那句「一键下载全部」是六个纯汉字,比夹了数字和空格的「确认这 12 颗」还宽,装不下 ⇒ 点第一下换字时按钮变宽,把页眉旁边那两颗挤走,正是他否过两轮的"一点就重新排"。
  • 改成拿产品自己那颗 sizeHint() 量两版标签取大的(QSS 的内边距和边框它都算,我算不准)。判据也跟着换成量 sizeHint(),不再自己数字符;顺带补一条"探针借道试宽标签之后要把那句还回去"—— 试宽度这件事本身不许留下状态。
  • CI 看不见这条:它跑 offscreen,字体度量不一样,上一版在那儿是绿的。这是今天第二条"只在本机才红"的判据(前一条是 grid 那颗角标读一个像素)。
  • 数字:--plugintest 389 条 0 红,两个平台各两趟(offscreen / windows)都 0 红;离线全套 1051 条 0 红 / 3 条点名 SKIP;theme / proxy / plugin_version / core_abi / tracked_leak 全 0。版本 1.5.0 不动(未发版,同批吸收),没装机。

🧩 5. 剩下几条边界接上同一层 SEH 壳:全仓库一份 __try,五种"跨进插件"的调用都从它过

  • 他拍的"继续做剩下的边界保护"。没按"每条边界套一次 __try"写 —— 收敛成一份壳:一个 POD 的 Cross(要调谁、参数、结果口子、哪一类)+ 一个 crossCall() 的 switch + 一个 crossSeh()。五种调用全从这颗过:createPage / feedText / invoke / 那个 extern "C" 的激活函数 / apiVersion / actions。
  • 为什么非得是一份:__try 那个函数里不能有需要析构的局部对象(C2712),四份就是四次同样的指针舞蹈,漏改一份就有一条边界没人守 —— #94 那次「--storenet 四条红没人看见」是同一个形状。
  • 产品那四个入口现在各自只剩"查一下,交出去":page() → buildPage()、feed() → feedInto()、invoke() → actionsInto() + invokeInto()、startBackground() → activateInto()、plugin() 装载那一问 → apiVersionInto()。判据直接调的也是这同一批函数,不是另写一个近似的外壳。
  • 契约写死在头文件注释里:返回值 = 这一趟回没回来,插件自己答"是/否"走 accepted / didIt —— 两种"否"在界面上是两句话。三条具体的:① 撞了不许被算成"插件答了「是」"(判据拿 ans 初值 true 去量,回来必须是 0);② why 进来先 clear()(上一笔那条教训的推广:不清就会把上一趟的失败念成这一趟的);③ 各条撞了之后的产品行为不一样 —— 装载那一问撞了 = 这颗 dll 直接拒掉并写进 g_loadErrors(10-22 那批"名字对得上、一用就崩"就死在这一脚);路由时 actions()/invoke() 撞了 = 跳过这一家但照样把 who 给出去(不然"按了 F1 什么都没发生"又回来);后台激活撞了 = 不算进 started 那个数并点名(悄悄少一个后台功能是最难查的)。
  • 数字:--plugintest 377 → 388 条 0 红(五条边界共 20 条:建页 9 + 另四条 11),整趟实测 7706 ms(两趟 7706/7660);离线全套 1050 条 0 红 / 3 条点名 SKIP;闸全绿 —— theme 0、proxy 0、plugin_version NEEDS_BUMP=0、core_abi 0(PluginHost.h 不在 SDK 那 26 份头里,加函数不动插件布局,API 仍 4)、tracked_leak 0、notes 两查 0。
  • A/B 做在「递文本」这条上,真插件、真走产品入口 feed(),只改一个变量:把 plugins/json 的 feedText 开头改成写坏内存 ——
  • 有壳:rc=1、386 条落地、红只有 2 条且都点名 json 递文本(「把一段 JSON 递给 json 工具,插件接住了」/「那段文本真的进了工具」),其余 19 颗照常,stdout 37916 字节,沙箱日志里那句实话出现 6 次:plugin: json 递文本时撞在异常 0xC0000005(指令地址 7ff8d9ee4d44)· 这一趟没干成,程序没跟着死。
  • 把 feed() 换回直调(绕过壳):rc=139、只有 181 条落地、stdout 16566 字节,后面连同尾巴被吃。
  • 两处都原样改回,plugins/json 一字未动(提交里也没有它)。
  • 两条没做 A/B,如实说:invoke() 与 startBackground() 的接线只靠"产品入口各调一次 + 同一份壳 + 建页/递文本两次已证的机制"。真路由一个动作得走 --fileinfo 那面旗,而那颗真插件会把对话框弹到他屏幕上(自测不许碰他的桌面);后台那颗要在自测里真 activate 就会把 linuxdo 的轮询定时器起起来,判据又变成看网络脸色。要补得先给自测造一个不弹 UI、不联网的动作口子。
  • 还有一条判据自己写错被当场抓的:QStringLiteral("%1/%2").arg(一个 bool) 少填第二个洞,那条报 got=[1/%2] —— 格式串的 %N 和 .arg() 个数这轮又欠了一次。
  • 版本:1.5.0 不动(未发版,同批功能吸收;这轮动的只有 core 的一份壳 + 判据 + 文档,一颗插件产物都没改)。没装机。

🧩 6. 建页那一条边界包了一层 SEH 外壳:插件里读坏内存(0xc0000005),程序不再跟着它一起死

  • 他问"有没有办法插件有报错、但程序不崩溃"。三档里拍的是第二档(包边界),第三档(插件进子进程)要重写整个插件模型 —— 插件交回来的是 QWidget 页,跨进程就得把控件树和事件走 IPC,不划算。这一轮按他说的只铺建页那一条:历史上真崩过的就这一条。
  • 落点只有一颗:PluginHost::buildPage() 是全仓库唯一一处跨进插件代码造页的地方(原来那句 p->createPage(parent) 长在 page() 里)。page() 现在只做"查缓存 → 找接口 → 交给这一颗 → 成了才缓存"。外壳的形状是被编译器逼出来的:__try/__except 只能待在没有需要析构的局部对象的函数里(C2712),所以那层是一个纯指针进出的小壳,QString 全留在外面拼;异常码和指令地址先落进 POD 的 SehHit,出壳之后才写日志 —— 异常展开里再分配大对象,第二次崩会把线索一起吃掉(和 SetUnhandledExceptionFilter 那条同一个理由)。
  • 界面那一行走的是早就有的那句实话: 建页时撞在异常 0xC0000005(指令地址 7ff8…)· 这一趟没给你页,程序没跟着死 —— ToolsPage 开不成页时本来就把原因写在页眉,不是只进日志。
  • 买到什么、没买到什么写在头文件里:买到窗口不消失 + 证据留下来 + 半建那张不当好页缓存;没买到"这个 bug 被关住了" —— 那一趟已经碰过坏内存,后面是带着未知损伤继续跑,所以 WER 转储和 pe_rva 那条取证路一律保留。一律接住也意味着栈溢出(0xC00000FD)和 C++ 异常(0xE06D7402)同样被接住并如实写码。非 Windows 那边这层如实不装(#else 就是直调 + 一句注释:接信号要 sigsetjmp,从信号里继续跑更不可靠,真要做得走进程隔离),那几条判据也跟着 #ifdef Q_OS_WIN —— 在 Linux 上拿故意崩的接口去喂它不是"红",是假红。
  • 决定性的一条是 A/B,只改一个变量:把 plugins/json 的 createPage 改成开头就写坏内存(真插件、真装载、真走 page()),编两颗对着跑 —— 有外壳:rc=1、359 条落地、13 条红全点在 json 那颗、其余 19 颗照常、日志 36120 字节 384 行尾巴完整;把 page() 换回直调(外壳绕过):rc=139、只有 55 条落地、日志 4152 字节,后面连同尾巴被吃掉。这一对才是"接线了"的证据 —— 合成接口那 9 条自己量不到(判据直接调 buildPage,产品绕过它也不会红)。两处都原样改回,plugins/json 一字未动。
  • 数字:--plugintest 368 → 377 条 0 红(SEH 这一栏 9 条),整趟实测 8702 ms(三趟 8801/8634/8702);离线全套(CI 那 19 面旗、offscreen)1039 条 0 红 / 3 条点名 SKIP;闸全绿 —— core_abi_gate 查的是 SDK 那 26 份头,PluginHost.h 不在里面 ⇒ 加一颗函数不动插件布局、API 仍 4;plugin_version NEEDS_BUMP=0(这轮一颗插件源码都没动);theme/proxy/tracked_leak/notes 两查都 0。
  • 探针自己犯的错是判据撞出来的,不是我看出来的:第一版为了"外壳别挡好人"直接拿真插件走 buildPage(plugin("json"), …),那张页没进 g_pages 缓存、parent 又是空 ⇒ 那颗壳里的 m_page 被我这张孤儿页顶掉,跑在后面的"整扇窗关掉后再拿"当场从 ...
Read more

ltool v1.4.1

Choose a tag to compare

@github-actions github-actions released this 22 Oct 11:27

🚀 ltool v1.4.1 发布说明 / Release Notes

🎉 欢迎使用 ltool 1.4.1!本次共 11 笔改动,重点是 build/verify 里 16 个一次性探针、17 处…、文档里那行写死的家目录换成 %USERPROFILE%(发…、「测试全部插件保证没有闪退」这一趟补的两件事。下面每一节是一笔提交,原因和实测数字都在里面。


🌟 本次改动

🐛 1. 方案 A:build/verify 里 16 个一次性探针、17 处真账号换成各语言自己的运行时取法(python expanduser / PS 用 $env:APPDATA 与 GetFolderPath Desktop / node process.env.USERPROFILE / shell $HOME),抹成字面量会把还能用的探针弄坏;顺带把桌面上那个私人夹具名和两份会话转录的绝对路径一起换成占位。根因不是忘改,是 .gitignore 那条只挡 *.sh、对已跟踪文件根本不生效,所以补的是会红的闸(tools/tracked_leak_check.py,扫 git ls-files 573 张,占位白名单在里面)而不是注释。闸自己红过一次:token 正则把反引号顿号一起吃掉,文档里那句「占位(x、zzname)」被判成一个叫 x、zzname 的用户 —— 收窄成只吃账号名字符,反向验塞一颗非白名单的探针仍红 1 条。逐张过语法:py_compile 4、node --check 4、bash -n 1、PowerShell 只解析不执行 7(PS_PARSE_BAD=0,BOM/LF 原样);CI YAML 本地过 js-yaml:jobs=1 steps=25。一次踩坑:那条"用了 expanduser 却没 import os"的检查排在写盘之后,于是 16 张已落地第 17 张红着退出 —— 收尾检查要排在写盘前面。

  • (这一笔没写正文,标题就是全部。)

✅ 2. 文档里那行写死的家目录换成 %USERPROFILE%(发版说明的脱敏闸管的是"生成出来的那份",文档本体一直没人管过)。顺手拿同一颗图案把 git 真带走的每一张文件扫了一遍:src/docs/tools 里剩下的全是占位(x / someone / zzname),但 build/verify 那堆一次性探针里有 16 个文件、17 处带的是真用户名 —— 那个目录 .gitignore 只挡了 *.sh,探针脚本没挡住,所以它们早就在公开仓库里了。这一笔只改了文档那一行,那 16 个怎么处理等他拍。

  • (这一笔没写正文,标题就是全部。)

🐛 3. 「测试全部插件保证没有闪退」这一趟补的两件事:① 「页被删后再拿」从一遍改成三遍 —— 这类生命周期问题和堆怎么复用有关,第一趟的 INFO 就抓到 grid、language 两颗"新造那张正好落在刚释放的地址上"(只跑一遍 + 按地址判就是假红,假红会诱人去改本来对的那半句);② 反向验按新判据重跑:g_pages 退回裸指针 ⇒ 第一趟红 4 条、进程死在半路,改回来 300 条 0 红、5964 ms。离线全套 696 条 0 红(plugintest 300 / storetest 104 / cliptest 88 / startuptest 55 / pathcfg 37 / pinyin 68 / linuxdotest 44)

  • (这一笔没写正文,标题就是全部。)

🐛 4. 闪退那三条不该只钉 base64 一颗:g_pages、壳里那颗 m_page、卡片墙那一次 deleteLater,对 19 颗是同一套代码 —— 现在三趟都按颗数着过(页被单删 / 拆墙后再直达 / 整扇窗关掉后再拿),--plugintest 257→298,反向验把 g_pages 退回裸指针重编=红 5 条后 rc=139

  • (这一笔没写正文,标题就是全部。)

✅ 5. ci_wait.sh 自己挪完家之后那两行没跟上:用法提示还写 build/verify,cd 的 ../.. 会从 tools/ 走到仓库外面去(今天靠绝对 API URL 侥幸没坏)。

  • (这一笔没写正文,标题就是全部。)

🐛 6. 把 ci_wait.sh 从 build/verify 挪进 tools/:.gitignore 里 build/verify/*.sh 是被排除的,那个轮询脚本一直只活在我本地(这次它被一截被掐断的 JSON 弄崩过一次,已改成读不到就下一轮再问 —— 这种东西进不了仓库等于没有)

  • (这一笔没写正文,标题就是全部。)

📝 7. migrate 脚本两处我自己的错:卸载键名猜错(真名是 Inno 那串 GUID)导致不出声地退回写死路径;又给无 BOM 的 .ps1 加了中文注释直接 ParserError

  • 改成按 DisplayName 现搜两棵 hive + 把 target 的来源打进结论行(查不到就明说 HARDCODED FALLBACK);全文件回到纯 ASCII。另记一条:exe 字节里 grep 不到 1.4.x(ANSI/UTF-16 都 0),短字面量在 release 产物里会被拆开 —— 装了哪版看注册表 DisplayVersion 安装包文件名,别看 exe 里那串字。

🐛 8. 这一处钉死了:同一条直达路径在旧写法上必崩(rc=139 段错误),在新写法上 257 条 0 红

  • 他给了两条:每次都会、从搜索框直达进去 —— 路径就收窄到 SearchWindow.cpp:1474
  • openTool(id, extra) → ToolsPage::showTool → PluginHost::page() + feed()。
  • "每次都会"排除了偶发时序,所以那条路必须能进判据:加了 4 条走真链
  • (拿 ToolsPage 直接 showTool("base64", "aGVsbG8="),判输入框 / 结果框 / stack 当前格)。
  • 决定性的一步是反向验:把三处缓存(g_pages 和 base64/json 两份 m_page)退回旧的裸指针写法、
  • 判据一字不动重编 → --plugintest 段错误退出 rc=139,
  • 死在"宿主不会把已释放的页再发一次"报红之后的下一步(拿那块已释放的页去 m_stack->indexOf/addWidget)。
  • 和他事件日志那个"崩在 Qt6Core + 读地址 0x8"同一个形状。改回来 → 257 条 0 红。
  • 所以这次不是"修了一处看着像的",是同一条路旧写法必崩、新写法不崩。
  • 判据自己也被修过一次:findChildren<QPlainTextEdit*>() 的下标不是界面顺序(第 0 个是只读的结果框),
  • 第一版按"0 是输入"写,红两条 —— 红的是判据;现在按角色认(只读 = 结果框)。
  • 总数 --plugintest 250 → 253(页缓存 3 条)→ 257(直达 4 条);离线全套 0 红,
  • plugin_version_gate NEEDS_BUMP=0(这一小步只动 core 判据侧)。
  • 交付:install MATCH B0198AF98C29、19 颗对齐、装好的那份 --plugintest 257 过 / 0 红、relaunch pid=3500。

🐛 9. 「b64 会闪退」:插件页的命不在宿主手里,可两边都存了裸指针(core 1.4.1)

  • 取证两层都走了:事件日志 Id=1001 说 2026-10-22 12:00:17 ltool.exe 真崩,
  • faulting=Qt6Core.dll 6.10.2.0 + RVA 0x3fc2b8,code=c0000005、访问地址 0x8;
  • CrashDumps 里同一分钟有 ltool.exe.17412.dmp。和昨天那一批不是一个签名
  • (那 10 次全是 0x1489c9 = 已修掉的 app.arguments() 那个)—— 这是新的一个偏移。
  • 判据脚本 build/verify/crashsig.ps1 把这条链留在了仓库里(bash 里传 $_ 会被吃,所以走 .ps1 纯 ASCII)。
  • 修的那一处是代码上真站得住的:插件页是 createPage(parent) 交给调用方那棵树拥有的,
  • 而 ToolsPage::rebuildGrid() 会把 stack 摘掉那一格 deleteLater();
  • 宿主这边 PluginHost::g_pages、各壳里那份 m_page 存的都是裸指针,谁都不知道对方已经没了
  • ⇒ 下一次 page(id) 或 feedText() 就是 use-after-free,形状正好是"在 Qt 里读偏移 8 的成员"。
  • g_pages 换成 QHash<QString, QPointer>:缓存里那颗跟着页一起死,page() 遇到死的就重造;
  • base64 / json 的 m_page 换成 QPointer,页没了 feedText 老实返回 false(不假装收下);
  • grid 那份 m_page 只写不读,连着删掉(不留空开关)。
  • 判据:--plugintest 加 3 条 —— 把 base64 那张页真删一次再问宿主要什么。
  • 修完 dead/new + yes;反向验(page() 换回"缓存里有就发出去")当场红 2 条(dead/same、no),
  • 其余 251 条照绿。总数 250 → 253。
  • 号:base64 1.0.2、json 1.0.3、grid 1.0.3(行为修复走补丁位);core 公开面没动布局
  • (core_abi_gate GATE_HITS=0,API 仍 4),ltool.iss 1.4.0 → 1.4.1 —— 1.4.0 已发出去,
  • 发出去的号不能被后来的产物复用。副作用照实说:1.4.1 的商店清单地址要等你推 v1.4.1 的 tag
  • 才不 404,在那之前应用退回本地缓存那 19 条并明说(设计里的行为)。
  • 老实一句:这处补丁我没能证明就是他这一次崩的那一处 —— 偏移在 Qt6Core 内部、
  • 附近没有可读导出(pe_rva.py 量到的是 QBluetoothPermission 那个 staticMetaObject 之后 23KB 的未导出代码),
  • 单凭一个数定不到调用方。所以补了两件让下次自己能说话的东西:
  • 崩溃处理里加栈回溯:CaptureStackBackTrace 打最多 12 帧,每帧一行 "CRASH FRAME ## 模块名+0xRVA" 进 log.txt,行号可以喂回 pe_rva.py 问旁边是谁; 只用固定缓冲区 + fromLatin1(异常处理里再多分配一次,第二次崩会把线索一起吃掉)。
  • store: 重排卡片墙 那行加「摘掉的那格=<类名>」:正常永远该是卡片墙那一类, 哪天摘到一张插件页,那行会先替崩溃说话。
  • 交付:install MATCH EB3EE0CCCF9F、19 颗对齐、装好的那份 --plugintest 253 过 / 0 红、
  • relaunch pid=13408;四道闸绿;离线全套 0 红。

🧩 10. 插件仓库同步完:461a394(三颗源码 + sdk 26 头 + dist 自洽)+ b4ba2e1(去掉两处写死的 12),那边 CI 终态 success

  • 本地 clone 落后 origin 三笔(191fb80 早同步过 19 颗),第一遍在旧 base 上做出"新放 36",
  • 真实增量只有"新放 1 / 改动 10":clipboard 1.1.3、startup 1.1.0、json 1.0.2。
  • sdk 重铺成 26 个头(多的那颗是 Theme.h —— 我先按"应该是 ClipboardStore.h"写进 README,
  • 拿 git show 191fb80:sdk/INFO.txt 一比才发现,别按应该是哪颗写文档)。
  • 独立编(那边没主仓库的 PCH):新建 build-sync,19 颗 dll / 0 error;dist 的 dll + index.json 一起重算,
  • 按他们 CI 的量法自洽(19 条、api 全 4、sha256 与字节逐个对上)。
  • 那边 run 39645316369 = success:验 SDK 快照 / dist 自洽 19 / 一个 dll 都不许缺(日志列出的产物正好 19 行)/ Qt 版本。
  • b4ba2e1 只是把他们两处写死的"12"改成按 plugins/*/ 现枚举(第二次同步就过期,这次就是)。
  • push 到插件仓库连吃两次 443 reset,第三次直连才过 —— 推完拿 ls-remote 对上 sha 才算成功(b4ba2e1 已对上)。
  • 版本:插件仓库不升任何号(它是主仓库的镜像);core 这轮只有文档 + 两个工具。

🧩 11. v1.4.0 发出去了:23 个资产、商店那条 404 的链随发版活了,并对着公开那一面复验过

  • run 39641826096 = success。Release id 419844618:安装包 22,572,344 B + 19 颗 dll +
  • ltool-sdk-1.4.0.zip + plugins-index.json + sha256.txt(共 23 个资产)。
  • tools/postrelease_check.py(新)打的是公开 URL而不是 CI 日志,三件事:
  • 公开说明里带家目录的行 = 0(用的就是发版说明那把同一颗图案,21,792 字);
  • releases/download/v1.4.0/plugins-index.json = HTTP 200、19 条(装 1.4.0 之后那条 404 的链活了);
  • sha256.txt 里安装包那一行 == 资产 API 的 digest(1895560fd733…)。
  • 对外一律 subprocess curl + 重试:这台机的 msys python 走 urllib 是
  • CERTIFICATE_VERIFY_FAILED(没有可信 CA 那份),一次抖动不算"那条链没活"。
  • tag 走过一次弯路并如实记下来:第一次推的 v1.4.0 指在 e6128e8(带那颗写宽的判据),
  • CI 在我自己那道闸上红、Release 没建成(当时查 404);按拍定的"移到修复那一笔"删远端同名 tag、
  • 在 bf53b66 重打再推。删时没有 Release 挂在下面,无连带副作用。
  • README 的 --autoupdatenow 那一行改成现在的状态:v1.4.0 已发、清单可取,
  • 但这一轮没有跑那一栏 —— 跑了会把装着的 19 颗换成商店那批(MSVC 每次字节不同),
  • 而"以哪份为准"那条路还没拍。
  • tools/sync_plugins.py(新,插件仓库那一半):类名从主仓库 CMakeLists 现问,
  • dry/apply 两档,报每颗新放/改动/已一致;这次 新放 36 / 改动 36 / 已一致 34,
  • 6 颗整个缺的连 CMakeLists.txt 一起补。sdk/ 重铺成 26 个头 + 4 个 onnxruntime 头 + ltoolcore.lib。
  • 插件仓库还没推:要先在本地把 19 颗独立编一遍(那边没 PCH,少 include 只有这条路看得见)。

📦 下载与安装

Windows 10 及以上(安装包已声明 MinVersion=10.0)。按用户安装,不需要管理员,默认装到 %LOCALAPPDATA%\Programs\ltool;双击安装向导走完即可,托盘常驻,Alt+Space 呼出。

文件 大小 sha256 前 8 位 说明
ltool-1.4.1-setup.exe 21.53 MB 4feb2231 就用这个:双击安装/升级(约 22 MB,开箱零插件,插件由商店按需拉)
ltool-sdk-1.4.1.zip 270 KB d24ed685 插件 SDK 快照:头文件闭包 + 导入库,给独立编插件用
plugins-index.json 11 KB 8439cfff 插件商店清单:这一版 app 读的就是它(地址按版本号钉死,见下面那条)
ltool-ads.dll 93 KB ab5e0d2b 商店里的插件 ads(v1.0.1,插件接口 4)
ltool-base64.dll 68 KB fe70eeee 商店里的插件 base64(v1.0.2,插件接口 4)
ltool-capture.dll 361 KB b16f7cd7 商店里的插件 capture(v1.0.7,插件接口 4)
ltool-clipboard.dll 135 KB e494b170 商店里的插件 clipboard(v1.1.3,插件接口 4)
ltool-disk.dll 180 KB 4f257c9c 商店里的插件 disk(v1.0.2,插件接口 4)
ltool-downloads.dll 66 KB 02f4c556 商店里的插件 downloads(v1.0.2,插件接口 4)
ltool-grid.dll 154 KB 560d2c3d 商店里的插件 grid(v1.0.3,插件接口 4)
ltool-json.dll 79 KB b379c8df 商店里的插件 json(v1.0.3,插件接口 4)
ltool-language.dll 174 KB f8546eca 商店里的插件 language(v1.0.2,插件接口 4)
ltool-linuxdo.dll 178 KB 40c62de9 商店里的插件 linuxdo(v1.0.2,插件接口 4)
ltool-network.dll 100 KB 9887c5f9 商店里的插件 network(v1.0.1,插件接口 4)
ltool-record.dll 276 KB 665b64b9 商店里的插件 record(v1.0.2,插件接口 4)
ltool-registry.dll 94 KB 0f921ea9 商店里的插件 registry(v1.0.2,插件接口 4)
ltool-repair.dll 88 KB 7f1d7504 商店里的插件 repair(v1.0.2,插件接口 4)
ltool-shell.dll 135 KB f86b9640 商店里的插件 shell(v1.0.2,插件接口 4)
ltool-software.dll 237 KB 4497a04f 商店里的插件 software(v1.0.2,插件接口 4)
ltool-startup.dll 150 KB 7fe5b5a3 商店里的插件 startup(v1.1.0,插件接口 4)
ltool-tasks.dll 96 KB 4eb7bde4 商店里的插件 tasks(v1.0.1,插件接口 4)
ltool-user.dll 140 KB cacb81f3 商店里的插件 user(v1.0.3,插件接口 4)
sha256.txt 2 KB — 上面这些文件的 SHA-256 全表(Get-FileHash -Algorithm SHA256 逐行对得上)

一共 23 个文件。校验:Get-FileHash <文件> -Algorithm SHA256,或下 sha256.txt 对整批。

插件商店读的是 releases/download/v1.4.1/plugins-index.json —— 清单和这 19 颗 dll 都出自编出这一版 core 的那同一次构建,所以"商店里那颗和这台机器的 core 对不上"这件事从源头没了(对不上也只会是被拒绝并说清,不是闪退)。


Full Changelog: v1.4.0...v1.4.1

ltool v1.4.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 17:21

🚀 ltool v1.4.0 发布说明 / Release Notes

🎉 欢迎使用 ltool 1.4.0!本次共 19 笔改动,重点是 闸自己踩红过一次、推 v1.4.0 之前先给发版说明加一道脱敏、把「runner 上每条旗标量到几个」数出来并钉住。下面每一节是一笔提交,原因和实测数字都在里面。


🌟 本次改动

🐛 1. 闸自己踩红过一次:判据的图案要和脱敏的图案是同一颗

  • 刚加的那道「发版说明脱敏闸」在 main 上就红了(run 39641227629 第 6 步):
  • NOTES_CHECK 6 条全过,而 leak check 报 带家目录的行=1,残留 C:\Users\<用户名>\AppData\Roaming\ltool。
  • 红的原因不是漏了真用户名,是我自己这两颗图案不一致:
  • scrub() 认的是"家目录 + 一段 [A-Za-z0-9._-]+ 的用户名";
  • leak check 里另写了一条更宽的(C:\Users\ 后面接任意非空白)。
  • 于是 e6128e8 那笔提交正文里为了说明这件事而写的占位符 C:\Users\<用户名>\...
  • 脱敏不动它(本来就不该动),却被闸当命中拦下 —— 一条合法的说明文字把每推都跑的闸踩红。
  • 改法:leak check 直接 import make_release_notes.USER_HOME_RE,判据和脱敏共用一颗图案
  • (这仓库的规矩:同一个判断不许有第二份答案)。闸判的应该是"有一个真用户名活着出来了",
  • 不是"出现了 C:\Users 这几个字"。
  • 两边都验过:
  • 真区间 v1.3.3..HEAD 180 行 → 命中 0(占位符那条不再红);
  • 拿一份临时文件塞三行(合法路径 / 占位符 / 真用户名 zzrealname)→ 只点名为 1 条、退出码 1。

📝 2. 推 v1.4.0 之前先给发版说明加一道脱敏:家目录 + 用户名不进公开 Release 页

  • CI 的发布步骤用 tools/make_release_notes.py 把 v1.3.3..v1.4.0 每笔提交的正文原样拼进 Release 页,
  • 而真历史里就有一句带用户机的路径(9313288「数据目录搬到软件目录下」的正文:
  • 「旧目录 C:\Users<用户名>\AppData\Roaming\ltool 还是 378 MB 原样在那儿」)。
  • 同一串字符在 git log 里是一行历史,印在发版封面上是另一回事 —— 这一区间数过就 1 处。
  • scrub() 只吃"家目录 + 用户名"那一段(反斜杠、正斜杠两种写法都认,后者是 Qt 归一化后的形状), 换成 %USERPROFILE%;D:\software\ltool、%LOCALAPPDATA%\Programs\ltool 这类文档要的路径一个字不动。 接在 commits() 的标题和 body_lines() 的每一项上。
  • 两道闸放在每推都跑那一栏,不是只在发版那一次(只在推 tag 才量的话,漏出去的那版已经发出去了): make_release_notes.py --check(6 条)+ tools/notes_history_leak_check.py(真区间 17 笔 / 171 行 → 命中 0; 发布步骤再拿将要发出去的那份 release-notes.md 量一遍)。
  • 反向验过:scrub 改成原样返回 → --check 当场红 4 条;往临时文件塞一句 %USERPROFILE%\AppData → leak check 退出码 1 并点名那一行。判据脚本里不写死任何用户名,只按形状判(那文件自己要进公开仓库)。
  • 顺手留 build/verify/yaml_check.js:这台机的 msys python 没有 pip,yml 用 $TEMP/jsy 那份 js-yaml 校; 现在 1 个 job / 24 步,新那一步排在配色闸后面。

📦 3. 把「runner 上每条旗标量到几个」数出来并钉住

  • run 39636325299(= bbd6265,终态 success)按旗标分节数出来的:
  • --pathcfg 40 / 本机 37 过 + 1 跳 —— 差的正是冷启动子进程 + 交接那三条:本机有常驻实例,
  • 那种子进程会把启动器叫到屏幕上所以点名跳过;runner 上它们真跑了,
  • 凭据是日志里那行 INFO 冷启动子进程 verdict=alive ms=6005。
  • --pinyin 本机 68 / runner 60:少的 8 条是按环境点名跳过(这台机器的 webs.json 没配 gg、
  • 索引里没有 Antigravity / QQ / 中文名应用 / ChatGPT 那颗),而这一节的"跳过"是 INFO 开头
  • 不是 SKIP 开头 —— 只 grep ^SKIP 就会把"少 8 条"读成"莫名少了"。
  • 顺手留了工具 build/verify/ci_log_pathcfg.py:从 Actions 的日志 zip 里按旗标分节数
  • PASS / FAIL / SKIP / INFO,认 --cfgcheck 的 ok 行首(不认就会报 PASS=0,看着像一面空旗标),
  • 也认 INFO 跳过 那种点名。--smoke 那 0 条判据是本来如此(它是诊断,CI 那道闸判"有输出且退出码 0")。

📦 4. 迁移脚本把「补过来的图有没有被历史引用」自己说出来:那 4 个是孤儿,不是没搬干净

  • dry 复测同一台机器:扫 992 / 缺 4 / 都有 988;那 4 个(两张图+缩略图)现在这份 clipboard.json 一条都不引用,程序按天清理又把它们收走了(日志两条 swept 4 orphan,正好在 -Apply 之后)。不说清就会让人以为迁移没做完。脚本的复制语义没动(老目录里有而新目录里缺的一律补,字节不同的一个字不碰)。

📦 5. 把"往还没搬的目录里播默认值"那一手连根拔掉 + 一次性迁移脚本

  • 上一条修的是后果,这条找的是"谁会写"。答案在代码里不是推测:
  • 沙箱名单里没有 --plugintest,而 ensureDefaultWebs() 对所有路径都要跑 ——
  • deliver 的 installed 那一栏跑的正是 装好的 exe --plugintest,那趟把 2 条的默认
  • webs.json 播进了还没搬的数据目录,于是"同名不覆盖"把你那份 24 条挡在老目录里。
  • --plugintest 进沙箱名单(先实测:数据目录指到临时目录时它仍然 250 过 / 0 红, 那 19 颗 dll 从 exe 目录\ltool-plugins 装,跟数据目录不是一条线)。
  • ensureDefaultWebs() / DevEnv::ensureDefaultPlugins() 先问 Utils::legacyMigrationPending(): 还没接住真数据的目录一个字节默认值都不写。判断只有一份,拆成纯函数 migrationPendingFor(legacy, now) 让判据能拿两个临时目录直接问(--pathcfg 5 条), 另加一条钉"沙箱设着 LTOOL_DATA_DIR 时 pending 必须是 false"——少它任何离屏夹具 都会被自己的守卫挡住、默认表播不进去。
  • 搬过之后那趟补一次 backfillFromLegacy:只把现在这份 clipboard.json 引用着、而本地缺的 剪贴板图从老目录补回来(按文件名认,不按记着的那串路径)。有一条本来要加、最后决定不加: "目标等于内置默认就拿老目录那份换掉"也搬进这一趟 —— 依据是本机 plugins\java.json 那两份 (老 538 / 新 379)逐字节 diff 之后差的只有程序自带默认文字自己的改版措辞, "等于这一版的默认"推不出"老目录那份是用户数据",它可能是上一版的默认,换过去是把新文字退回旧的。 1.4.0 还没发 ⇒ 没有需要救的现场,要救的已经在搬家之前堵住。
  • 一次性脚本 build/verify/migrate_ltool_data.ps1:老目录有而新目录缺的按 sha256 补进去, 两边都有但字节不同的一个字都不碰、只列出来;默认 dry run,-Apply 才写盘; 落点从 Inno 卸载键 InstallLocation 推。纯 ASCII(.ps1 放中文又没 BOM 这仓库炸过)。 本机 dry 跑:扫 992 / 缺 4 / 都有 988(同 981、不同 7);-Apply 补齐那 4 个、失败 0。
  • 判据 --pathcfg 26 → 37 过 + 1 跳。反向验过:摘掉补图那步的调用处,红的正是最后 4 条 (got=[0] want=[2]、got=[n,n]),前面 33 条全绿 —— 又是"helper 绿了而漏调用处"那个形状。 如实点名一处量不到:那两句守卫本身没判据(离屏全设着 LTOOL_DATA_DIR = pending 必然 false, 不带它的进程会往 build\Release\data 落文件被安装包收走),兜住它的是另外三层。
  • 顺手(上一条留下的):deliver 的 relaunch 这一栏这次第一次读对了文件(\data\log.txt)。
  • 全套本机离屏 0 红(plugintest 250 / cfgcheck 28 / cliptest 88+1 / startuptest 55 / pathcfg 37+1),
  • 四道闸绿(core_abi_gate 动布局的头 0 个 ⇒ 接口号 4 不升)。
  • 交付 install MATCH 9F2B7ACB7854、19 颗对齐、装好的那份 --plugintest 250/0、relaunch pid=25060。
  • 版本:core 不升号(仍未发布的 1.4.0 这批,定性是"把搬家做对"不是新功能);插件号不动。

📦 6. 搬家那条"同名不覆盖"被程序自己写的默认值满足了:网站关键字 24 条被默认 2 条挡在老目录

  • 他报「网站关键字直达只有这么点数据了」。量出来:新目录 webs.json 345 B / 2 条,
  • 老目录 4286 B / 24 条 —— 不是没搬,是搬的时候目标里已经有同名文件(deliver 的 installed
  • 那一栏先拿装好的 exe 跑 --plugintest,那一趟在还没搬的目录里写下了默认表),
  • "已有同名不覆盖"就把它跳过了;而 .migrated-from-appdata 照写,第二次启动也不再搬 = 永久性。
  • 改:搬家复制之前,先把目标里那些"内容逐字节等于内置默认"的文件请走
  • (Utils::clearStockDefaultsForMigration,判"什么算默认"只有一份答案 = 再算一遍 stockWebsJson())。
  • 判据 --pathcfg 加 8 条:3 条问 helper、5 条拿两个临时目录跑整条链
  • (新拆的 Utils::migrateBetween —— 漏调用处才是这次的形状,反向验过:删掉那一步红的正是链那 3 条,
  • helper 那 3 条全绿)。中途我先写过一条"按源码顺序闸住"的判据,用的 SRC_ROOT 这个宏仓库里根本没有,
  • 作废换成真的那条。
  • 复测同一份真数据时挖出同家的第二半:clipboard.json 记的是绝对路径,搬家后 39 条图条目里
  • 37 条还指着 %APPDATA%\ltool\clipimg,而 load() 那句"图不在了就不留空条目"会把它们连条目一起丢
  • (日志里 sweep 已经两次把新目录里那 78 个副本当孤儿收走)。改:resolveImagePath 先按文件名在
  • 现在这个 clipimg 认一次(文件名本来就是内容的 sha1),认不到才原样返回;--cliptest 加 6 条,
  • 其中 2 条真改写 clipboard.json 再 load() 一遍(反向验过:换回原样抄路径,红的正是那 2 条)。
  • 他的数据也修好了:78 个副本按 sha256 补回新目录,装上新版重启后 39 条 path 全变成新目录。
  • 顺手:deliver 的 relaunch 那一栏一直读 %APPDATA% 那份老日志(1.4.0 之后日志在 \data),
  • 报的是上一版留下的那条 hide —— 改成先读现在这份,并把读的是哪一份打进那一栏。
  • 全套判据本机 0 红(cliptest 88+1 跳、pathcfg 26+1 跳、plugintest 250),四道闸绿
  • (新加的都是自由/静态纯函数,不动布局 ⇒ 接口号 4 不升)。core 不升号:1.4.0 还没发,
  • 这是这批里的一处数据丢失修复;clipboard 插件号不动(只改了它的判据文件)。

✅ 7. CI 那圈补上 --pathcfg:这次那三条"冷启动/交接"判据本来一次都没被执行过

  • 拿 run 39607595058 的日志复核 --startuptest 时发现的:那一圈里没有 --pathcfg。
  • 于是新加的三条(无旗标子进程不许崩 + 只连不写的连接不许叫出窗口 + 真写 show 的要照常叫)
  • 本机因为有常驻实例全部点名 SKIP、CI 又根本不跑 —— 等于没执行过就进仓库。
  • 加进去之后那一步是 16 面旗标;yml 拿 js-yaml 过了一遍(1 个 job,不是 0 个 job 的那种"没过校验")。
  • 同一份日志里读到的:--startuptest 在 runner 上 54 过 / 0 红 / 1 跳,跳的是 hklm_refusal_reason
  • (runner 提权,HKLM 真写成功了,判据按设计删掉那个键并说明"正常权限下才有得测");扫描 51 行 / 577 ms。

🐛 8. 剪贴板:点一张卡就复制(clipboard 1.1.3,1.4.0 同批)

  • 原来是双击才复制,他说"点击剪贴板卡片就是复制",所以接 itemClicked,并把 itemDoubleClicked 那根
  • 同时摘掉 —— 不是留着兼容:单击已经是复制,双击的第一下就完成了,再挂一条等于同一件事两条通道,
  • 还会让一次操作写两遍剪贴板、多两行日志。下面那行说明跟着改成「点一张卡 = 放回剪贴板」。
  • 判据这次换了量法:以前是自己调 cs.restore(...) 再比剪贴板,那量的只是 core 那颗函数,
  • 页面把信号接回双击照样绿。现在送一对真鼠标按下 + 抬到那张卡的矩形上(发给列表的 viewport),
  • 走的才是它自己那根 itemClicked。配两条护栏:点之前先把剪贴板清成别的值(不然上一段留下的同一个
  • 字符串会让这条白绿 —— 反向验时红出来的正是"占位还在"),以及一条"那块卡片矩形量得到"的前置判据。
  • 再加一条连点两张不同的卡:第一下之后整墙会重建,重建后信号接不回来就只复制得动第一次。
  • 反向验:把 itemClicked 改回 itemDoubleClicked 重编 → 单击那两条当场红,其余全绿。
  • 真剪贴板那一档顺手复测了"自我那次写不把历史顺序搅动"(2026-10-16 那次闪退的形状),仍然绿。
  • 数字:--cliptest 离屏 82 过 + 1 点名跳过(offscreen 的剪贴板是内存桩),QT_QPA_PLATFORM=windows 87 过 0 红。
  • 装的那份复验:--plugintest 250/0、--cliptest 0 红、19 颗里只换了 clipboard 这一颗。
  • 版本:clipboard 1.1.2 → 1.1.3;core 不升号(还是未发布的 1.4.0 这批)。

🐛 9. 开机启动分析能关掉应用自启 + 修掉那个"双击图标必崩"(startup 1.1.0,1.4.0 同批)

  • 两件事,一个是他要的功能,一个是我上一轮带进去的崩溃。
    1. 开机启动分析这一页从"只读"变成能动一件:把某一条应用启动项开/关。 走的就是任务管理器 · 启动 那同一个存储(StartupApproved{Run|StartupFolder} 下的 REG_BINARY), 不动 Run 值本身,所以随时反得回来;服务 / 计划任务 / 加载项一律拒绝并说清为什么。 顺带修了一个方向反了的读法:原注释写"0x03=启用"、代码照它写,界面于是把开着的说成关掉的。 本机拿真注册表对回来(ltool/utools = 02 00…、Steam/NeatDM/Cisco/Edge = 03 + FILETIME): 首字节 0x02=开着、0x03=被关掉、没有这个值=从没碰过=开着。写回去只改首字节、长度照原来那份。 系统服务默认不列(他点名),但藏着多少条必须在汇总、分组标题和那颗开关三处说实话。 确认走"就地两段",不弹模态框。StartupItem 那两颗新字段放在结构体最后,老字段偏移一个没动。 新判据 --startuptest 55 条(真写走自造的临时键 HKCU\Software\ltool-startuptest-,跑完删掉; 页面那半只点第一下,并用三个 StartupApproved 键的整棵快照逐字节证明第一下一个字节都没写)。 反向验:把 02/03 认反 → 当场红 9 条。
    1. 崩溃:双击图标 / 开机自启那次启动必崩(c0000005,Qt6Core.dll+0x1489c9),而所有离线判据全绿。 根因是我 16:17 那笔搬家守卫:std::none_of(app.arguments().cbegin() + 1, app.arguments().cend(), ...)。 QCoreApplication::arguments() 在 Qt 6.10 按值返回 QStringList,那两个迭代器来自两个临时容器, 区间头尾不在同一份数据上;没有旗标时头指针正好越过末尾,于是解引用别人家的堆。 带任意 -- 旗标反而不炸(predicate 第一个元素就命中)—— 而每一条自测都带旗标,所以 250 条绿灯照绿。 修:拿一份局部 QStringList 再用它的首尾。 补判据 3 条(--pathcfg 末尾):起一个"一个参数都不给"的子进程 6 秒内不许崩; 只连上不写字节的连接不许被当成"把启动窗口叫出来";真写了 show 的还得照常叫。 反向验:改回坏形状 → 冷启动那条当场红 crashed-0xc0000005(26 ms)。 同一族里我自己造过一次打扰:那三条第一版用 QLocalSocket 连管道问"有没有实例", 而服务端把"只连不写"也当成交接 ⇒ 21:15 把他那份启动器叫出来了(日志留着那行)。 已换成 WaitNamedPipeW(不建立连接)+ 服务端忽略空消息;改完复跑,他日志里不再多一行。

✨ 10. JSON 格式化那页改成「左边输入、右边看效果」(json 1.0.2)

  • 他这句是「json 格式化可以直接在左边的区域输入内容,右边是看效果」。
  • 以前 QSplitter 里是树在左、文本框在右 —— 要粘 JSON 得先把光标挪到右半边,
  • 而左边那块先被解析结果占住。改成文本框在左、树在右,两边 1:1。
  • 实测(900 宽的页):输入框 x=10 宽=467 · 结果树 x=481 宽=409。
  • 判据在 --plugintest 加 3 条,量的是真实控件的几何(不是"splitter 第 0 个是谁"那种自己造的断言): 输入框 x 小于树 / 输入框 !isReadOnly() / 两边各拿到不少于页宽 1/4 的宽度。
  • 两个自己踩的坑:① 隐藏着的控件不跑布局,第一次量到"输入框 48px、树 48px"——那是 minimumSizeHint 不是布局,判据必须先 show() + resize() + 跑一轮事件循环再量;② 阈值别写死像素, 离屏那个虚拟屏比真机窄,写死 >=300 会被环境先夹掉,改成按 页宽/4 算。
  • 版本:json 插件 1.0.1→1.0.2(插件源码改了,plugin_version_gate.sh 那道闸要它自己递增, 现在 NEEDS_BUMP=0);core 不升号 —— 跟着还没发的 1.4.0 同一批。 deliver 九栏全绿(plugins 那一栏:18 颗本来就是这份哈希、换成新的 1 颗 = 只有 json)、 装好的 exe 跑 --plugintest 250 过 0 红。

📦 11. 数据目录搬到软件目录下:日志/配置/索引/剪贴板图/OCR 模型都不再压 C 盘(1.4.0 同批)

  • 他这句是「所有的日志和数据目录都放在软件目录下,不要放在 C 盘」。改的是一颗函数:
  • Utils::dataDir()(88 处调用点都问它),三档优先抽成纯函数 resolveDataDir:
  • ① LTOOL_DATA_DIR(自测沙箱 / 便携模式)→ ② <exe>\data → ③ 只读位置才退回 %APPDATA%\ltool 并留一条日志。
  • 搬家一次性:老目录还在 + 新目录里没有真配置 → 整棵树复制,每个文件按 sha256 对过, 对不上的删半成品;旧目录一个字都不动,搬完留一颗 .migrated-from-appdata 标记。
  • 本机实测:983 个文件 / 360 MB,旧目录 %USERPROFILE%\AppData\Roaming\ltool 还是 378 MB 原样在那儿。 耗时约 9 秒(16:12:49→16:12:58),排在托盘和窗口之前 —— 升级后第一次启动会慢这 9 秒,只有一次。
  • 判据:--pathcfg 11 → 18 条(三档优先 + "这一趟 dataDir() 真等于沙箱" + 搬家五条含"目标已有同名文件不覆盖")。
  • 踩到并修掉的三个(都是判据/守卫的坑,不是业务代码): ① 守卫第一版写"新目录非空就别搬" —— dataDir() 一被问到就 mkpath,Utils::log() 立刻写下 log.txt, 而 deliver 的 installed 那一栏先拿装好的 exe 跑 --plugintest ⇒ 新目录已有日志 ⇒ relaunch 那次判定"搬过了",378 MB 一个字节没动,界面和日志都没说。改成只认真信号, 并且每个提前返回都把"为什么没搬"写进 note(data: 没搬:…)。 ② "不覆盖"那条第一版白绿:夹具里源目录根本没有 log.txt,跳过分支一次都没走到。 ③ 另一条红是我连着 new 了两个 QFile,第二个没 open 就 readAll。
  • 自测沙箱从挪 APPDATA 改成挪 LTOOL_DATA_DIR(数据默认已在软件目录下,挪 APPDATA 管不着了); ltool.iss 的...
Read more

ltool v1.3.3

Choose a tag to compare

@github-actions github-actions released this 20 Oct 06:44

Full Changelog: v1.3.2...v1.3.3

ltool v1.3.2

Choose a tag to compare

@github-actions github-actions released this 20 Oct 06:18

Full Changelog: v1.3.1...v1.3.2

ltool v1.3.1

Choose a tag to compare

@github-actions github-actions released this 20 Oct 05:45

Full Changelog: v1.3.0...v1.3.1

ltool v1.3.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 05:29

Full Changelog: v1.1.1...v1.3.0

ltool v1.1.1

Choose a tag to compare

@github-actions github-actions released this 19 Oct 14:54

Full Changelog: v1.1.0...v1.1.1

ltool v1.1.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 14:05

Full Changelog: v1.0.3...v1.1.0