11 KiB
Chromium Extension Bind Mount 最小实现
[2026-10-01] 本方案已被「静默安装」Policy 取代,仅作历史设计存档。 用户定版:不再坚持插件文件共享。插件改为每 Profile 静默安装 (External Extensions + CRX),扩展文件自然落在各 Profile 内部, 且 install 事件每 Profile 只发一次、磁盘与装载语义均优于 bind mount。 实测结论与现行实现见
extension-install-events-findings.md§7 与node/src/core/browser/extension.silent.ts。
1. 目标
在 Linux + CloakBrowser/Chromium 环境中,让多个独立 Browser Profile 共用同一份第三方 Extension 文件,减少每个 USER_DATA 重复保存 Extension 代码。
目标结构:
共享 Extension
/opt/serpmax/extensions/<extension-id>/<version>/
|
+---- bind mount ---- Profile 001/UserData/Default/Extensions/<id>/<version>
+---- bind mount ---- Profile 002/UserData/Default/Extensions/<id>/<version>
+---- bind mount ---- Profile 003/UserData/Default/Extensions/<id>/<version>
每个 Browser 仍然拥有独立的 USER_DATA、Cookie、Preferences、Extension State 等运行状态;只共享 Extension 代码目录。
本方案不修改 Chromium/CloakBrowser 源码。
2. 适用范围
- Linux
- Chromium/CloakBrowser
- 第三方 Extension,不修改 Extension 源码
- 多 Browser Profile
- Extension 代码版本固定
- 允许使用 root 或具备 mount 权限的服务执行
mount --bind
不使用:
--load-extension- CloakBrowser
extension_paths - 每个 Profile 复制一份 Extension 代码
3. 目录规划
示例:
/opt/serpmax/
├── extensions/
│ └── abcdefghijklmnopabcdefghijklmnop/
│ └── 1.0.0/
│ ├── manifest.json
│ ├── background.js
│ ├── content.js
│ └── ...
└── profiles/
├── 001/
│ └── UserData/
│ └── Default/
│ └── Extensions/
│ └── abcdefghijklmnopabcdefghijklmnop/
│ └── 1.0.0/ # bind mount
└── 002/
└── UserData/
└── Default/
└── Extensions/
└── abcdefghijklmnopabcdefghijklmnop/
└── 1.0.0/ # bind mount
建议共享目录设置为只读,避免某个 Browser 修改共享 Extension 文件影响其它 Browser。
4. 前置条件
先准备一份完整的 Extension 文件:
SHARED="/opt/serpmax/extensions/abcdefghijklmnopabcdefghijklmnop/1.0.0"
mkdir -p "$SHARED"
# 将已经确认可用的 Extension 文件放入 $SHARED
test -f "$SHARED/manifest.json"
检查版本:
cat "$SHARED/manifest.json" | grep '"version"'
Extension ID、目录名和版本目录必须与 Chromium 实际使用的 Extension 路径一致。
5. 为单个 Profile 创建 bind mount
假设:
EXT_ID="abcdefghijklmnopabcdefghijklmnop"
VERSION="1.0.0"
PROFILE="/opt/serpmax/profiles/001/UserData"
SRC="/opt/serpmax/extensions/$EXT_ID/$VERSION"
DST="$PROFILE/Default/Extensions/$EXT_ID/$VERSION"
创建目标父目录:
mkdir -p "$(dirname "$DST")"
目标目录第一次不能已经是普通文件目录。先确认:
ls -ld "$DST" 2>/dev/null || true
如果是 Chromium 已经复制出来的普通目录,需要先停止 Browser,再删除该目录:
rm -rf "$DST"
创建空目标目录:
mkdir -p "$DST"
执行 bind mount:
mount --bind "$SRC" "$DST"
建议再设置只读:
mount -o remount,bind,ro "$DST"
检查:
mountpoint "$DST"
findmnt "$DST"
6. 多 Profile
每个 Profile 都执行一次:
for ID in 001 002 003 004; do
PROFILE="/opt/serpmax/profiles/$ID/UserData"
DST="$PROFILE/Default/Extensions/$EXT_ID/$VERSION"
mkdir -p "$(dirname "$DST")"
if mountpoint -q "$DST"; then
continue
fi
rm -rf "$DST"
mkdir -p "$DST"
mount --bind "$SRC" "$DST"
mount -o remount,bind,ro "$DST"
done
7. Profile 生命周期与启动顺序
Bind mount 的生命周期跟随 Profile / USER_DATA,不跟随 Browser 进程。
正确流程:
创建 Profile
|
+-- 创建 USER_DATA
|
+-- 创建 Extension bind mount
|
+-- 启动 CloakBrowser
|
+-- Browser 正常运行
|
+-- Browser 退出
| |
| +-- 保留 USER_DATA
| +-- 保留 bind mount
|
+-- 下次启动
| |
| +-- 直接复用已有 bind mount
|
+-- 删除 Profile
|
+-- 确认 Browser 已退出
+-- umount
+-- 删除 USER_DATA
核心原则:
先 mount,再启动 Browser;Browser 退出不卸载;只有删除 Profile 时才卸载。
不要在 Browser 已经启动并加载 Extension 后动态替换目录。
8. 启动前校验
启动 Browser 前至少检查:
test -f "$SRC/manifest.json" || exit 1
mountpoint -q "$DST" || exit 1
test -f "$DST/manifest.json" || exit 1
确认实际文件来自共享目录:
readlink -f "$DST/manifest.json"
bind mount 下这里看到的是目标路径,但文件内容应与:
"$SRC/manifest.json"
一致。
9. 删除 Profile 时卸载
Browser 退出后不卸载 bind mount。
只要 Profile / USER_DATA 仍然存在,就保持 bind mount,这样下次启动 Browser 可以直接复用。
删除 Profile 时,必须先确认 Browser 已经完全退出,再执行卸载:
if mountpoint -q "$DST"; then
umount "$DST"
fi
rm -rf "$PROFILE"
如果存在多个 Extension,需要逐个卸载后再删除 USER_DATA:
删除 Profile
|
+-- Browser 是否已退出?
|
+-- umount 所有 Extension bind mount
|
+-- 删除 USER_DATA
不要在 Browser 运行期间删除 USER_DATA,也不要直接 rm -rf 包含活动 mount point 的 USER_DATA。
10. 关键实现:Profile Manager
SerpMax 后续可以只实现三个函数:
mountExtension(profileId, extensionId, version)
unmountExtension(profileId, extensionId, version)
isExtensionMounted(profileId, extensionId, version)
伪代码:
mountExtension():
1. 定位共享 Extension
2. 检查 manifest.json
3. 定位 Profile Extension 目录
4. 检查 Browser 是否运行
5. 删除原来的版本目录
6. 创建空目录
7. mount --bind
8. remount,bind,ro
9. 校验 mount
10. 返回成功
unmountExtension():
1. 检查 mountpoint
2. 确认 Browser 已退出
3. umount
4. 删除 Profile 时再删除 USER_DATA 中对应目录
11. 最小 Shell 实现
可以先使用下面脚本验证整个方案:
#!/usr/bin/env bash
set -euo pipefail
EXT_ID="abcdefghijklmnopabcdefghijklmnop"
VERSION="1.0.0"
SRC="/opt/serpmax/extensions/$EXT_ID/$VERSION"
PROFILE="/opt/serpmax/profiles/001/UserData"
DST="$PROFILE/Default/Extensions/$EXT_ID/$VERSION"
test -f "$SRC/manifest.json"
mkdir -p "$(dirname "$DST")"
if mountpoint -q "$DST"; then
echo "already mounted: $DST"
exit 0
fi
if [ -e "$DST" ]; then
rm -rf "$DST"
fi
mkdir -p "$DST"
mount --bind "$SRC" "$DST"
mount -o remount,bind,ro "$DST"
mountpoint "$DST"
echo "mounted: $SRC -> $DST"
12. 验证测试
12.1 Mount 是否成功
findmnt "$DST"
应该能看到 bind mount。
12.2 文件是否一致
sha256sum "$SRC/manifest.json"
sha256sum "$DST/manifest.json"
两个 Hash 应一致。
12.3 多 Profile 是否共用文件
分别执行:
stat "$SRC/manifest.json"
stat "/opt/serpmax/profiles/001/UserData/Default/Extensions/$EXT_ID/$VERSION/manifest.json"
stat "/opt/serpmax/profiles/002/UserData/Default/Extensions/$EXT_ID/$VERSION/manifest.json"
确认所有 Profile 都指向同一个共享源目录。
12.4 Extension 生命周期测试
使用正常安装方式让 Chromium 识别 Extension,不使用:
--load-extension
extension_paths
然后测试:
- 第一次启动 Profile。
- 检查 Extension 是否正常运行。
- 记录
chrome.runtime.onInstalled。 - 关闭 Browser。
- 再次启动同一个 Profile。
- 确认不会因为每次启动都重新作为 command-line unpacked extension 加载而触发
reason=install。
注意:bind mount 本身不会改变 Chromium 的 Extension 生命周期。它只解决文件存储层共享问题。要避免 --load-extension 每次启动产生的 install 行为,必须使用正常的 Extension 安装/注册方式。
13. 重要限制
13.1 不要多个 Browser 同时写共享目录
多个 Browser 使用同一份 Extension 代码时,共享目录必须视为只读资源。
推荐:
Extension Code 共享、只读
Extension State 每 Profile 独立
Cookies 每 Profile 独立
Preferences 每 Profile 独立
Secure Preferences 每 Profile 独立
Local Storage 每 Profile 独立
Service Worker State 每 Profile 独立
13.2 Extension 更新
不要直接覆盖正在使用的版本目录。
推荐:
1. 新版本写入新的共享目录
2. 停止相关 Browser
3. 更新 Profile 的 bind mount
4. 创建新版本 bind mount
5. 启动 Browser
如果不需要立即切换版本,也可以等 Profile 下次启动前更新 mount。
例如:
extensions/<id>/1.0.0/
extensions/<id>/1.1.0/
不要让 Browser 运行期间直接修改:
extensions/<id>/1.0.0/
13.3 权限
执行 mount --bind 通常需要 root 或 CAP_SYS_ADMIN。
SerpMax 如果本身不是 root 运行,建议单独提供一个最小权限的 mount helper,而不是让整个应用拥有完整 root 权限。
14. 最小生产结构
第一阶段不需要复杂的 Mount Manager:
SerpMax
|
+-- Extension Registry
| |
| +-- extension-id
| +-- version
| +-- shared-path
|
+-- Profile Manager
| |
| +-- profile-id
| +-- user-data-path
|
+-- Mount Manager
|
+-- mount
+-- unmount
+-- status
Mount Manager 只负责文件系统 Mount,不负责 Extension 生命周期、Cookie 或 Browser 状态。
15. 实施结论
本阶段只实现:
- 一个共享 Extension 目录
- 一个 Profile 一个 bind mount
- Profile 创建/初始化时完成 mount
- Browser 退出后保持 mount
- 删除 Profile 时才 unmount
- 共享 Extension 目录只读
- Profile 状态保持独立
- 不修改 Chromium/CloakBrowser 源码
先用单 Extension + 2 个 Profile 验证:
Shared Extension
|
+-- Profile 001
|
+-- Profile 002
确认 Extension 正常运行、两个 Profile 状态独立、USER_DATA 不再保存重复 Extension 文件后,再扩展到多 Extension / 多 Profile。