Repository navigation
[Bug] 更新器以"把整个安装目录改名到父目录"作为升级第一步,在 Windows 上极易失败且无法自诊断 #11119
Replies: 3 comments
|
经过检索发现别的讨论也遇到这个问题,但很可能由于目录被监听占用这个苛刻的复现条件,没有发现真正的原因就结束了讨论,所以这里开一个新讨论。 |
|
补充一个不需要任何第三方软件的触发条件:只要有一个资源管理器窗口停在安装目录的子目录上,就足以复现;而且占用者是否共享 现场
实测矩阵新建目录逐项隔离测试(
两点补充:
最小复现(无第三方工具)Add-Type -Namespace W -Name D -MemberDefinition @'
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool MoveFileExW(string a, string b, uint f);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern Microsoft.Win32.SafeHandles.SafeFileHandle CreateFileW(
string n, uint a, uint s, IntPtr sa, uint c, uint f, IntPtr t);
'@
New-Item -ItemType Directory -Force C:\t\parent\child | Out-Null
# share=3 (READ|WRITE),对子目录持句柄
$h = [W.D]::CreateFileW('C:\t\parent\child', 0x80000000, 3, [IntPtr]::Zero, 3, 0x02000000, [IntPtr]::Zero)
[W.D]::MoveFileExW('C:\t\parent', 'C:\t\parent.old', 0) # False,GetLastError() = 5
$h.Dispose()
[W.D]::MoveFileExW('C:\t\parent', 'C:\t\parent.old', 0) # True把 share 改成 7( 对修复与测试的影响
English summary. A plain Explorer window browsing a subdirectory of the install directory is enough to make the whole-tree rename fail with Related: #11527 shows the same Windows rule for dsh's own file watcher, #9989 covers the missing locker diagnostics, #10514 proposes the in-place publish fallback. |
|
补充一份独立复现,给出“任意子目录被打开即失败”的精确规则,以及一个属于产品自身的占用源(Windows 10.0.26300,DSH 0.2.1-rc.3 → 0.2.2-rc.1)。 1. 句柄矩阵 在
在 也就是说:句柄所在目录本身可以改名,但它的所有祖先都不能。安装树实测完全一致—— 2. 一个产品自身的占用源
同时逐文件用 这正好补上 #9989 说的“失败报告里没有占用者诊断”这一环:只查文件锁永远查不到。当前安装器的运行检测只覆盖 3. 建议
补充现象(同主贴): |
Uh oh!
There was an error while loading. Please reload this page.
摘要
DSH 桌面端更新时,安装器的第一个动作是:
即把整个安装目录改名,并在安装目录之外的父目录里创建
dsh.old-{GUID}。在 Windows 上这个前提过于脆弱:只要安装树里任意一个子目录被任何进程打开(文件监视器、编辑器、杀软扫描、Explorer 窗口、甚至某个进程的当前工作目录),这次改名就会失败,返回ERROR_ACCESS_DENIED (5)。失败后更新器只在本地留一份日志,用户侧表现为"点了更新,但应用再也没重启"。本次报告基于连续 5 次、跨 8 天的失败记录。
环境
deepseek-harness-0.2.1-rc.3-win-x64.exe)<install-dir>,其父目录为<parent-dir>)%LOCALAPPDATA%\@deepseek-aidsh-desktop-updaterapp-update.yml:provider: generic、channel: nightly、isAdminRightsRequired: false10.0.26300,NTFS现象
用户在应用内触发更新 → 安装包成功下载(289 MB,
pending\deepseek-harness-0.2.1-rc.3-win-x64.exe至今仍在缓存里)→ 应用按预期退出 → 安装器开始执行 → 改名失败 → 应用没有起来,也没有任何可见的错误提示。%LOCALAPPDATA%\@deepseek-aidsh-desktop-updater\installer-logs\下留了 5 份内容几乎相同的directory-failure-<timestamp>.log(8 天内,两次尝试分别产生 3 份和 2 份)。证据
其中一份
directory-failure-*.log:Restoration error: 0说明失败后安装器成功把目录名改了回来,因此安装本身没有被破坏 —— 但更新也就此中止。分析:这个前提为什么脆弱
1. 改名整棵目录,把"树里任何一处被打开"变成致命条件。
Windows 上重命名目录的语义是(本机实测,
MoveFileExW返回码):fs.watch/.NET FileSystemWatcher/ 裸CreateFileW)CreateFileW,且已授予FILE_SHARE_DELETE也就是说:授予删除共享权也救不了,只要有任意一个后代目录句柄存在,改名就报 5。而现实里这类句柄随处可见 —— 本次的直接触发者是 VS Code:它在窗口加载工作区后会把
PATH里每个目录都开一个非递归 watcher,而 DSH 的<install-dir>\resources\runtime\cli\bin正好在PATH上(dsh.cmd就在那里)。实测 trace 日志确认了该路径被 watch。对安装器的含义是:失败条件不受自己控制、范围无界(任何监视器/编辑器/杀软/Explorer 都可能触发),且错误码 5 与"权限不足"完全无法区分,自诊断极难。
2. 在安装目录之外创建目录,把父目录当成了自己的地盘。
dsh.old-{GUID}落在<parent-dir>—— 这是用户的目录,不属于应用。由此带来的情况:X:\),旧目录会落到X:\dsh.old-*;dsh.old-*垃圾目录(本次失败路径上靠Restoration回滚躲过了,但那是另一条路径)。3. 失败只写进
installer-logs,用户完全不知道发生了什么。用户能看到的现象只有"更新后应用没重启"。要定位到
MoveFileExW / 错误 5需要自己去翻%LOCALAPPDATA%下的隐藏日志;本次排查中,日志里也没有任何"谁在占用"的信息。4. 重试对"持续存在"的占用没有作用。
占用条件是持续的(只要那个 watcher 活着就一直成立),同一次失败里连续 10 次重试只会留下 10 条相同记录。
综合来看:8 天内 5 次更新尝试全部失败,用户始终停留在旧版本,289 MB 安装包长期滞留在缓存里;而"拒绝访问"的报错指向权限、实际原因是树里有句柄,用户会在权限、杀软、路径等方向上反复误判;这个前提对任何"在被打开目录树上改名"的升级方式都成立。
修改方向
如果可能,希望升级流程不要以"把整个安装目录改名"为前提 —— 用逐文件替换,或"版本化目录 + 指针切换",能把失败面从整棵树缩小到单个文件。
All reactions