零占用的 Koishi
这篇文章描述了 Koishi 插件设计模式中「零占用的 Koishi」部分的相关内容和实现方法。我们鼓励开发者在编写插件时遵守该模式。
一、定义
零占用的 Koishi 是指,给定一个正在运行的 Koishi 实例,移除该实例项目目录下的所有内容,实例应按照预期的方式保持工作。
此处的「保持工作」是指,实例内的所有模块,包括 Koishi 本体及所有插件,均在设计时对此情况做了考虑,并编写了相应的处理逻辑。如:
- Koishi 本体在启动时读取
koishi.yml
文件。但在启动后删除该文件,Koishi 保持工作。 - 存储大文件的插件在要求时加载、解析大文件并返回结果给用户。删除该文件后要求解析,插件无法完成请求,但插件返回可读的错误文本或输出可读的错误日志,不会抛出错误。
- 外部程序包装插件依赖外部的可执行程序进行工作。可执行程序在工作时无法解除占用,于是在启动时将可执行程序复制到项目目录之外后进行调用。删除项目目录内的可执行程序,插件保持工作。
二、动机
实现了「零占用的 Koishi」模式的 Koishi 项目将会获得以下优点:
2.1 自更新
可以编写插件更新 Koishi 及其依赖。在更新依赖的整个过程中,Koishi 及所有插件仍保持可用。
现已有 market 插件实现。
2.2 更强的健壮性
文件的暂时无法访问不会导致 Koishi 停止运行。这对项目文件夹使用网络映射的场景更友好。
2.3 滚动更新
可以先升级项目文件夹,然后滚动更新 Koishi。这将极大地提高 Koishi 的高可用性(并未实现「高可用(HA)」)。
三、现状
3.1 依赖
下列功能依赖此模式工作:
- 基于 market 插件的 Koishi 完全重载
- 基于 market 插件的 Koishi 热重载
- Koishi 桌面的联机实例修改、克隆和重命名(预期)
3.2 实现
截至当前,Koishi 本体已实现此模式,Koishi 插件市场内的绝大多数插件也均实现了此模式。下面仅列举一些未实现此模式的情况:
- assets-local:无法实现
- petpet:依赖 gyp 包,已被标记为不安全插件
此外,一些现已实现了此模式的插件也曾经未实现此模式,如:
- database-sqlite
- go-cqhttp
四、实现
一个 Koishi 插件要想实现「零占用的 Koishi」模式,只需要保证不占用项目目录内的任何内容。
作为一般情况,只要插件不使用项目文件夹内的任何文件,那么该插件就已自动实现了此模式。
实现「零占用的 Koishi」模式需要同时满足两点:不依赖 和 不占用 项目目录内的文件。
4.1 不依赖
要避免对项目目录内文件的依赖,有以下三种途径实现:
4.1.1 预读取
如果依赖的文件体积较小,则可以在插件启动时预先读取文件的全部内容到内存。文件体积较大时不建议采用此途径。
4.1.2 缓存
可以缓存热点文件,使文件在硬盘上缺失时仍能命中缓存。
4.1.3 错误处理
作为一种兜底的途径,可以在文件缺失时将可读的错误提示输出到用户侧或日志。
4.2 不占用
避免占用的场景和方法在各类型的插件之间有较大的不同,下面列举几个典型的插件作为常见场景的代表进行说明。
4.2.1 gocqhttp
gocqhttp 是 Koishi 插件市场中广受好评的插件之一,也是少有的被包含在 Koishi 模板项目中的非官方插件。gocqhttp 插件实现了此模式。实现使用以下四步:
- 插件的 postinstall 脚本检测
node_modules
下面的自身的包(此处即为koishi-plugin-gocqhttp
)的名称下的go-cqhttp
二进制文件的以对应版本为名称的目录下是否存在所需文件(如果目录不存在则创建),没有的话开始下载此文件;在此期间插件无法使用,日志输出警告; - 插件的 postinstall 脚本检测
HOME
下面的包名下的go-cqhttp
二进制文件的以对应版本为名称的目录下是否存在所需文件(如果目录不存在则创建),没有的话将node_modules
中的同文件复制过去;在此期间插件无法使用,日志输出警告 - 插件启动后在
apply
函数内检测HOME
下面的包名下的go-cqhttp
二进制文件的以对应版本为名称的目录下是否存在所需文件(如果目录不存在则创建),没有的话将node_modules
中的同文件复制过去;在此期间插件无法使用,日志输出警告 - 最后,使用
HOME
下面的包名下的go-cqhttp
二进制文件的以对应版本为名称的目录下的go-cqhttp
二进制文件。
4.2.2 petpet
作为 Koishi 插件市场内评分最低而被安装次数和安装失败次数均居高位的不安全插件,petpet 足够作为典型进行分析和实现。
petpet 未实现此模式的原因是它依赖了 sharp,这也是它被列为不安全插件的原因。sharp 是一个 Node.js 的图像处理库,它使用 Node 的 Addon 功能实现了 JS 封装层与 C++ 实现层的互操作,以期实现 Node.js 平台的高效图像处理。然而 Node Addon 文件(.node
文件)只要加载便会被占用,由于 node-gyp 将此文件输出到包目录下,因此造成了项目目录中的文件占用。
petpet 实现此模式的方法和 gocqhttp 基本相同,然而以下两点需要注意:
- 不能直接使用 sharp 包。node-gyp 生态中的 postinstall 脚本会请求连接性较差的配布平台下载 Node Addon 文件,并在下载失败时回滚到本机编译。不应使用此方法,应使用上述 gocqhttp 方法完全重写 node-gyp 负责的所有逻辑。
- 不能在复制文件前加载 Node Addon,这会导致找不到文件。因此,必须使用 const-require(
const xxx = require('xxx')
)模式,即使在 TypeScript 中也是如此。另外,在 ESM 中,对应的加载模式变更为 Dynamic Import,因此使用了 const-require 模式的代码将无法对多目标模块模式进行转译,即使是在 TypeScript 中也是如此。
五、缺陷
「零占用的 Koishi」模式目前仍存在以下缺陷:
- 一些插件无法完全避免影响,如 assets-local。
六、备选方案
6.1 可预期的重载
「可预期的重载」是指,Koishi 在收到项目文件变更请求后,预先卸载可能受到影响的插件并要求插件释放占用的文件。插件必须在卸载时释放文件。
该方案将会带来较大的 downtime,且实现成本相较「零占用的 Koishi」模式更高。Koishi 已经实现了「零占用的 Koishi」模式的一部分,没有必要再切换到其他方案。
七、FAQ
7.1 「零占用的 Koishi」与 Koishi 热重载功能的关系是?
如上文所述,Koishi 的热重载功能和完全重载功能均依赖此设计模式工作。但热重载功能本身是 Koishi 的自有逻辑,与此模式无关。
7.2 不按照此模式编写 Koishi 插件将会遇到哪些缺陷?
你将可能遇到以下两种情况:
- 如果你依赖的文件在
node_modules
以外,Koishi 桌面的实例管理功能(克隆和重命名等)将会失败。 - 如果你依赖的文件在
node_modules
内,market 进行的任何依赖更改都将失败。此种情况下你的插件将应该被标记为不安全。
7.3 为何不指定项目内的一个或多个目录用于逃逸?
指定有限个数的目录用于逃逸,其本质仍然是限制了开发者随意使用项目目录内任何文件的权力,与本设计模式没有区别,也不会使开发成本降低。
特别地,网络上的一些社区约定了使用某个位于项目文件夹外的特定目录作为逃逸目录,可以参考此链接: