1.77.6: SERP sorry-重定向按验证码语义(不换号+15min冷却);浏览池保温释放跳过+60min持有上限;速率-门壳实验与监控工具
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
# 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 代码。
|
||||
|
||||
目标结构:
|
||||
|
||||
```text
|
||||
共享 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. 目录规划
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
/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 文件:
|
||||
|
||||
```bash
|
||||
SHARED="/opt/serpmax/extensions/abcdefghijklmnopabcdefghijklmnop/1.0.0"
|
||||
|
||||
mkdir -p "$SHARED"
|
||||
# 将已经确认可用的 Extension 文件放入 $SHARED
|
||||
test -f "$SHARED/manifest.json"
|
||||
```
|
||||
|
||||
检查版本:
|
||||
|
||||
```bash
|
||||
cat "$SHARED/manifest.json" | grep '"version"'
|
||||
```
|
||||
|
||||
Extension ID、目录名和版本目录必须与 Chromium 实际使用的 Extension 路径一致。
|
||||
|
||||
## 5. 为单个 Profile 创建 bind mount
|
||||
|
||||
假设:
|
||||
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
创建目标父目录:
|
||||
|
||||
```bash
|
||||
mkdir -p "$(dirname "$DST")"
|
||||
```
|
||||
|
||||
目标目录第一次不能已经是普通文件目录。先确认:
|
||||
|
||||
```bash
|
||||
ls -ld "$DST" 2>/dev/null || true
|
||||
```
|
||||
|
||||
如果是 Chromium 已经复制出来的普通目录,需要先停止 Browser,再删除该目录:
|
||||
|
||||
```bash
|
||||
rm -rf "$DST"
|
||||
```
|
||||
|
||||
创建空目标目录:
|
||||
|
||||
```bash
|
||||
mkdir -p "$DST"
|
||||
```
|
||||
|
||||
执行 bind mount:
|
||||
|
||||
```bash
|
||||
mount --bind "$SRC" "$DST"
|
||||
```
|
||||
|
||||
建议再设置只读:
|
||||
|
||||
```bash
|
||||
mount -o remount,bind,ro "$DST"
|
||||
```
|
||||
|
||||
检查:
|
||||
|
||||
```bash
|
||||
mountpoint "$DST"
|
||||
findmnt "$DST"
|
||||
```
|
||||
|
||||
## 6. 多 Profile
|
||||
|
||||
每个 Profile 都执行一次:
|
||||
|
||||
```bash
|
||||
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 进程。
|
||||
|
||||
正确流程:
|
||||
|
||||
```text
|
||||
创建 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 前至少检查:
|
||||
|
||||
```bash
|
||||
test -f "$SRC/manifest.json" || exit 1
|
||||
mountpoint -q "$DST" || exit 1
|
||||
test -f "$DST/manifest.json" || exit 1
|
||||
```
|
||||
|
||||
确认实际文件来自共享目录:
|
||||
|
||||
```bash
|
||||
readlink -f "$DST/manifest.json"
|
||||
```
|
||||
|
||||
bind mount 下这里看到的是目标路径,但文件内容应与:
|
||||
|
||||
```bash
|
||||
"$SRC/manifest.json"
|
||||
```
|
||||
|
||||
一致。
|
||||
|
||||
## 9. 删除 Profile 时卸载
|
||||
|
||||
Browser 退出后**不卸载** bind mount。
|
||||
|
||||
只要 Profile / USER_DATA 仍然存在,就保持 bind mount,这样下次启动 Browser 可以直接复用。
|
||||
|
||||
删除 Profile 时,必须先确认 Browser 已经完全退出,再执行卸载:
|
||||
|
||||
```bash
|
||||
if mountpoint -q "$DST"; then
|
||||
umount "$DST"
|
||||
fi
|
||||
|
||||
rm -rf "$PROFILE"
|
||||
```
|
||||
|
||||
如果存在多个 Extension,需要逐个卸载后再删除 USER_DATA:
|
||||
|
||||
```text
|
||||
删除 Profile
|
||||
|
|
||||
+-- Browser 是否已退出?
|
||||
|
|
||||
+-- umount 所有 Extension bind mount
|
||||
|
|
||||
+-- 删除 USER_DATA
|
||||
```
|
||||
|
||||
**不要在 Browser 运行期间删除 USER_DATA,也不要直接 rm -rf 包含活动 mount point 的 USER_DATA。**
|
||||
|
||||
## 10. 关键实现:Profile Manager
|
||||
|
||||
SerpMax 后续可以只实现三个函数:
|
||||
|
||||
```text
|
||||
mountExtension(profileId, extensionId, version)
|
||||
unmountExtension(profileId, extensionId, version)
|
||||
isExtensionMounted(profileId, extensionId, version)
|
||||
```
|
||||
|
||||
伪代码:
|
||||
|
||||
```text
|
||||
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 实现
|
||||
|
||||
可以先使用下面脚本验证整个方案:
|
||||
|
||||
```bash
|
||||
#!/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 是否成功
|
||||
|
||||
```bash
|
||||
findmnt "$DST"
|
||||
```
|
||||
|
||||
应该能看到 bind mount。
|
||||
|
||||
### 12.2 文件是否一致
|
||||
|
||||
```bash
|
||||
sha256sum "$SRC/manifest.json"
|
||||
sha256sum "$DST/manifest.json"
|
||||
```
|
||||
|
||||
两个 Hash 应一致。
|
||||
|
||||
### 12.3 多 Profile 是否共用文件
|
||||
|
||||
分别执行:
|
||||
|
||||
```bash
|
||||
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,不使用:
|
||||
|
||||
```text
|
||||
--load-extension
|
||||
extension_paths
|
||||
```
|
||||
|
||||
然后测试:
|
||||
|
||||
1. 第一次启动 Profile。
|
||||
2. 检查 Extension 是否正常运行。
|
||||
3. 记录 `chrome.runtime.onInstalled`。
|
||||
4. 关闭 Browser。
|
||||
5. 再次启动同一个 Profile。
|
||||
6. 确认不会因为每次启动都重新作为 command-line unpacked extension 加载而触发 `reason=install`。
|
||||
|
||||
注意:bind mount 本身不会改变 Chromium 的 Extension 生命周期。它只解决文件存储层共享问题。要避免 `--load-extension` 每次启动产生的 install 行为,必须使用正常的 Extension 安装/注册方式。
|
||||
|
||||
## 13. 重要限制
|
||||
|
||||
### 13.1 不要多个 Browser 同时写共享目录
|
||||
|
||||
多个 Browser 使用同一份 Extension 代码时,共享目录必须视为只读资源。
|
||||
|
||||
推荐:
|
||||
|
||||
```text
|
||||
Extension Code 共享、只读
|
||||
Extension State 每 Profile 独立
|
||||
Cookies 每 Profile 独立
|
||||
Preferences 每 Profile 独立
|
||||
Secure Preferences 每 Profile 独立
|
||||
Local Storage 每 Profile 独立
|
||||
Service Worker State 每 Profile 独立
|
||||
```
|
||||
|
||||
### 13.2 Extension 更新
|
||||
|
||||
不要直接覆盖正在使用的版本目录。
|
||||
|
||||
推荐:
|
||||
|
||||
```text
|
||||
1. 新版本写入新的共享目录
|
||||
2. 停止相关 Browser
|
||||
3. 更新 Profile 的 bind mount
|
||||
4. 创建新版本 bind mount
|
||||
5. 启动 Browser
|
||||
|
||||
如果不需要立即切换版本,也可以等 Profile 下次启动前更新 mount。
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
extensions/<id>/1.0.0/
|
||||
extensions/<id>/1.1.0/
|
||||
```
|
||||
|
||||
不要让 Browser 运行期间直接修改:
|
||||
|
||||
```text
|
||||
extensions/<id>/1.0.0/
|
||||
```
|
||||
|
||||
### 13.3 权限
|
||||
|
||||
执行 `mount --bind` 通常需要 root 或 CAP_SYS_ADMIN。
|
||||
|
||||
SerpMax 如果本身不是 root 运行,建议单独提供一个最小权限的 mount helper,而不是让整个应用拥有完整 root 权限。
|
||||
|
||||
## 14. 最小生产结构
|
||||
|
||||
第一阶段不需要复杂的 Mount Manager:
|
||||
|
||||
```text
|
||||
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 验证:
|
||||
|
||||
```text
|
||||
Shared Extension
|
||||
|
|
||||
+-- Profile 001
|
||||
|
|
||||
+-- Profile 002
|
||||
```
|
||||
|
||||
确认 Extension 正常运行、两个 Profile 状态独立、`USER_DATA` 不再保存重复 Extension 文件后,再扩展到多 Extension / 多 Profile。
|
||||
Reference in New Issue
Block a user