# 构建防御性软件

URL: https://www.mattsenter.com/zh/blog/building-defensive-software

*防御性软件指的是当周围环境出现异常时仍然让人能够理解的软件，而不只是校验输入的软件。Comoji 是我做的 Mac 表情符号自动补全应用，在一次重启之后它看起来坏掉了，原因是 macOS 的 Secure Input 卡在了开启状态，先是被 Chrome 持有，之后又被 Apple 的登录进程持有。键盘输入受保护时 Comoji 主动退让，这个行为是正确的，但没有任何东西把这一点告诉用户，于是一次有意的暂停看起来和崩溃一模一样。根本原因在我的代码之外，但这份困惑依然归我负责。修复很小，也没有削弱保护：只要 Secure Input 处于活动状态，菜单栏图标上就显示一个锁形徽标，并且一键就能打开一篇帮助文章，解释这个状态意味着什么以及如何清除它。当产品看起来坏了的时候，技术上正确并不能带来安慰。有时候正确的行为就是停下来，而好的软件应该说明原因。*

前不久，我的笔记本电脑电量耗尽关机了。插上电源重启之后，Comoji 不工作了。我输入一个表情符号快捷键，什么也没有发生。没有解释，没有任何迹象表明有什么变了。只有一个关机前还好好的、现在显然不再工作的应用。

[Comoji](https://www.comoji.io/?utm_source=mattsenter.com&utm_medium=referral&utm_campaign=building-defensive-software&utm_content=comoji-inline) 是我做的一个小型 Mac 应用，在你已经在用的文本框里提供表情符号自动补全。它的工作很简单：识别一个快捷键，给出匹配的表情符号，然后别碍事。当这一切停止发生时，这个产品就没剩下多少可以交互的东西了。

只不过 Comoji 其实并没有坏。它是在尊重一条安全边界，而我电脑上的另一个东西卡在了这条边界后面。

## 为什么做正确的事看起来会像什么都没做？

macOS 有一项叫作 [Secure Input](https://developer.apple.com/library/archive/technotes/tn2150/_index.html)（安全输入）的功能，用来防止敏感的键盘输入被其他进程截获。这对 Comoji 这样的应用很重要，因为它需要识别你输入的内容才能提供自动补全。一个表情符号工具没有任何理由在你输入密码时旁观，所以在 Secure Input 处于活动状态时，Comoji 会主动退让。

这一次，Chrome 显示为持有 Secure Input 的进程，尽管我早已不在输入密码了。我强制退出了 Chrome，以为这样就能解决问题。结果 Secure Input 仍然处于活动状态，而且现在关联到了 Apple 的登录进程上。不管重启之后哪里出了岔子，关掉浏览器都不足以解决它。

这是 Apple 自己也记录在案的一类问题。一个应用在不再需要 Secure Input 时仍让它保持开启，[可能会干扰其他依赖键盘事件的应用](https://developer.apple.com/library/archive/technotes/tn2150/_index.html)，即使肇事的应用已经退到了后台。安全机制继续尽职尽责，机器上其他地方的软件却要承担后果。

Comoji 当时就处在这样的境地。系统说键盘输入需要保护，它就正确地拒绝工作。但这一切对我来说都是不可见的。这个应用是我写的，可就连我一开始也只看到它不工作了。

用户不应该需要去调查操作系统的状态，才能弄明白为什么自己的表情符号快捷键突然什么都不做了。

## 为什么「不是我的 bug」不算一个完整的回答？

一旦确定问题源自你的代码之外，就停止追查，这种诱惑可以理解。Chrome 持有 Secure Input。然后 Apple 的登录进程也牵涉其中。Comoji 的行为完全符合设计。结案了，对吧？

并不是。这解释了是谁造成了问题，却帮不到那个正试图使用我的应用的人。他们不会去看进程归属，也不会去判断哪家公司该收到 bug 报告。他们看着 Comoji，纳闷它为什么不工作。

底层的输入问题在 Comoji 之外。而缺少解释这件事，是我可以修的。

防御性软件开发正是在这里超越了检查非法参数和捕获异常。你还得考虑，当周围的环境不再正常运转时，你的应用看起来是什么样子。用户能区分有意的暂停和崩溃吗？他们知道是什么在阻止应用工作吗？你有没有给他们一个有用的下一步去处？

当你的产品看起来坏了的时候，技术上正确并不能带来多少安慰。

## 我改了什么？用一把小锁取代一个谜团

我给 Comoji 的菜单栏图标加了一个小小的锁形指示，只要 Secure Input 处于活动状态就会显示。现在，自动补全缺席有了一个看得见的原因：Comoji 已暂停，因为键盘输入受到保护。这个应用不再默不作声地把一个不可用的功能摆在那里，好像它本该在工作一样。

我还把托盘菜单链接到了[一篇解释什么是 Secure Input 的帮助文章](https://www.comoji.io/blog/secure-input-mac?utm_source=mattsenter.com&utm_medium=referral&utm_campaign=building-defensive-software&utm_content=secure-input-help)，以及 Comoji 为什么要尊重它。卡住的人可以直接从应用里找到这个解释，而不必猜测正确的搜索词，或者联系我才发现原来系统里有这么一项功能。

重要的是我**没有**试图解决什么。答案不是为了让表情符号自动补全能用而绕过密码保护。Secure Input 处于活动状态本身并不是错误。它的存在有充分的理由，Apple 也明确警告过，[开启安全键盘输入可能会影响其他应用](https://support.apple.com/guide/terminal/use-secure-keyboard-entry-trml109/mac)，也就是那些需要这些按键的应用。

所以这把锁传达的是一种状态，而不是一场危机。在正常输入密码期间，它解释了为什么 Comoji 暂时不可用。当这种状态意外地持续下去时，它给了用户一个理解发生了什么的起点。

我没有让 Chrome 或 macOS 变得不会卡住。我只是让它们卡住时，对使用 Comoji 的人来说不那么令人困惑，同时没有削弱 Comoji 本应尊重的那层保护。

## 为什么大公司依然是外部依赖？

为了 Google 的浏览器和 Apple 的操作系统牵扯出的问题，去加固一个微不足道的小工具，这件事有点让人恼火。它们不是我不假思索拖进项目里的冷门组件。它们是两家巨型科技公司的产品，而它们的行为依然成功地让我的小应用显得有缺陷。

但它们的体量并不改变工程上的决定。我不能在「凡是比我大的公司做出来的东西都永远表现正确」这个假设之上构建 Comoji。我也不能对一个沮丧的用户说，这个问题属于一家足够气派的公司，然后就认为我的工作完成了。

我不知道这种特定的事件序列发生得有多频繁。它也许很少见。但我遇到了，而结果暴露出一个有用的区分：Comoji 已经安全地处理了这个安全条件；它还没有处理好由此产生的困惑。

这并不意味着每一个假想的故障都值得一个新设置、一个警告对话框或一个恢复子系统。在这个案例里，应对措施很小，而且直接针对问题本身。一个锁形图标让这个状态可见。一篇帮助文章让它可以被理解。两者都不需要假装我能从自己的应用内部修好另一个应用。

对我来说，这是构建防御性软件的一个重要部分。你的责任不止于确保周围一切正常时你的代码能正常工作。你还需要决定，当它无法完成自己的工作时会发生什么，包括原因出在别人的错误上的时候。

有时候，正确的行为就是停下来。好的软件应该能告诉你为什么。
