跳转至

WSL 探测失败语义

WSL 探测用于回答关于某个发行版的问题:是否安装了 git、$HOME 是什么、哪些发行版正在运行。这些探测中的每一个都可能因为与答案本身无关的原因而失败——发行版正在启动、wsl.exe 在负载下变慢、虚拟机刚刚被关闭。

这个子系统中反复出现的缺陷是把这种失败报告为否定答案。

形态

try {
  await execCommandInWslOrThrow(target, `${shellQuote(command)} --version`)
  return true
} catch {
  return false // "not installed" and "could not ask" are now the same value
}

下游无法区分这两者,因为到了这一步,它们已经不再是两回事。

为什么它总能被发布

仅仅吞掉异常本身是可以承受的。未缓存的调用方稍后会再次询问,答案会自行纠正,因此该缺陷在代码评审和手动测试中保持不可见。

当被吞掉的值被缓存或用于控制发现流程时,它才会变得对用户可见。此时,一个仅仅忙碌了一秒的发行版会报告没有 git,或者没有 agent 会话,直到应用重新启动。这种失败具有粘性、静默,并且与真实情况无法区分。

目前已有三个实例:

位置 用户看到的现象 状态
预检 CLI 探测 缓存结果会把“git 未安装”固定到重启为止 有界条目(#17350)
glab auth status 回退到 WSL 对于从不使用 GitLab 的用户,空闲 VM 被反复唤醒 开放(#8941)
listRunningWslDistrosAsync 失败时保守地返回 [],且没有最后已知良好值,每 2 秒轮询一次——持续损坏的 wsl.exe 会让所有 WSL 会话在整个应用中消失 开放(PR #17072 评审)

应该怎么做

选择适合调用点的最便宜方案。

  1. 不要固定它。 如果探测成本低且未缓存,吞掉异常是可以的——下一次调用会自愈。这是 src/ 中大多数代码的合理做法。
  2. 给条目加边界。 如果你缓存,请给它一个 TTL,让瞬时失败过期,而不是持续整个会话。成本低,无需改变签名,#17350 就是这么做的。
  3. 保留最后已知良好值。 如果探测控制发现流程,失败时应回退到上一次成功的答案,而不是回退为空。src/main/wsl.ts 中的 listWslDistrosAsync 已经这样做了——紧挨着它添加的 listRunningWslDistrosAsync 却没有。
  4. 传播第三种状态。 持久修复方案:返回 present | absent | unreachable 而不是布尔值,这样调用方就不会意外地把“无法询问”当作“否”。这会超出 WSL 范围,进入共享的 exec 代码,目前尚未实现。

无论你选择哪一种,都在注释中说明选了哪一种以及为什么——这句话正是下面防护真正要求的内容。

防护

src/main/wsl/wsl-probe-failure-semantics.test.ts 会扫描 WSL 和预检探测模块中的 catch { return false | [] | null },并将当前集合保存在一个只减不增的允许列表中。

它的局限性值得明确说明,因为它们决定了它值得被信任到什么程度:

  • 它看不到危险的部分。 被吞掉的值之后是否被缓存或控制发现流程属于数据流问题,而不是语法问题。每个允许列表中的条目目前都是安全的;防护不会验证这一点,也无法验证。
  • 它是范围受限的,不是全局的。 同样的形态在 src/ 中出现约 850 次,并且通常是对的,因为对大多数调用方来说,失败确实意味着不存在。在整个仓库中强制执行它只会产生噪音。它只在答案描述 WSL 发行版的地方才重要。
  • 它捕捉的是形态,而不是错误。 代码可以在从未写出 catch { return false } 的情况下混淆失败和不存在。

因此它并不能阻止该缺陷。它所做的是阻止在这些模块中出现新的吞掉异常的位置,而没有人说明为什么该值可以安全地固定下来——这正是三次都缺失的评审对话。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 orca48/orca