fix: Chrome 调试端口占用时顺位尝试 (#38)

This commit is contained in:
ila
2026-10-07 16:37:14 +08:00
parent 0aa930480d
commit badb0dd262
3 changed files with 64 additions and 14 deletions
+4 -4
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Troubleshooting
wiki_url: https://git.ilapage.cn/chengma/cmsp/wiki/Troubleshooting
wiki_revision: 75c52d039b32fa90b24e594d49df79eb5a7302ad
synchronized_at: 2026-10-07T08:13:41Z
wiki_revision: bb5e884ffc4c08b7b1872b251239268358a5f4ff
synchronized_at: 2026-10-07T08:35:19Z
<!-- gitea-wiki-mirror:end -->
# 故障排查
@@ -92,14 +92,14 @@ SYNC_IN_PROGRESS 表示已有商品同步,请等待结束;Go侧拒绝重叠
`docs/` 只能由同步工具生成。任何时候都不要先改本地镜像再反向覆盖 Wiki。
### 3. 检查专属 Chrome(计划,Go 实现后适用)
### 3. 检查专属 Chrome
1. 状态文件中的 PID 是否还存在。
2. 该进程的命令行是否包含本程序的专属 `--user-data-dir`。
3. `http://127.0.0.1:<端口>/json/version` 是否可访问。
4. `http://127.0.0.1:<端口>/json/list` 是否存在 `type` 为 `page` 的目标。
四项任一不通过,就关闭专属 Chrome 后重新启动。不要在同一个 Profile 上再启动第二个实例。
有效状态可复用现有浏览器。新启动时,配置范围内的端口若由其他程序或其他 Profile Chrome 占用,程序会顺位尝试下一个空闲端口;范围全部占用才报端口不可用。如果本项目专属 Profile 已有 Chrome 进程,但状态文件丢失或无法复用,程序会拒绝启动第二实例;先手动关闭该专属 Chrome 再重试。无法读取 Chrome 进程归属时也会停止,避免共用 Profile。不要关闭其他 Profile 的浏览器。
### 4. 检查淘宝登录
+40 -10
View File
@@ -62,20 +62,19 @@ func EnsureBrowser(ctx context.Context, cfg config.TaobaoConfig, openURL string)
return state, nil
}
// 状态文件缺失或失效时,检查真正使用本项目 Profile 的 Chrome。
// 其他 Profile 占用的端口由 scanFreePort 跳过,不应阻止启动。
commands, err := chromeProcessCommandLines()
if err != nil {
return BrowserState{}, fmt.Errorf("无法确认专属 Chrome 是否已运行,未启动第二个实例:%w", err)
}
if hasProfileChrome(commands, profile) {
return BrowserState{}, fmt.Errorf("专属 Chrome 已运行,但浏览器运行状态无法复用;请先关闭该专属浏览器后重试")
}
port, err := scanFreePort(cfg.DebugPortStart, cfg.DebugPortEnd, portIsFree)
if err != nil {
return BrowserState{}, err
}
// 状态文件丢失时无法证明已运行的调试浏览器属于哪个 Profile。
// 宁可让使用者手动关闭它,也不能冒险启动同一 Profile 的第二个实例。
for candidate := cfg.DebugPortStart; candidate <= cfg.DebugPortEnd; candidate++ {
if candidate == port {
continue
}
if cdpVersionAvailable(candidate) {
return BrowserState{}, fmt.Errorf("检测到端口 %d 已有浏览器,但缺少可验证归属的运行状态。请先关闭该专用浏览器后重试。", candidate)
}
}
args := []string{
"--user-data-dir=" + profile,
@@ -350,6 +349,37 @@ func scanFreePort(start, end int, isFree func(int) bool) (int, error) {
return 0, fmt.Errorf("调试端口范围 %d—%d 已全部占用,请关闭不用的程序后重试", start, end)
}
func chromeProcessCommandLines() ([]string, error) {
// PowerShell 的输出可能使用系统代码页,因此以 UTF-8 Base64 传递命令行。
const script = `$ErrorActionPreference='Stop'; ` +
`$processes=Get-CimInstance Win32_Process -Filter "Name='chrome.exe'"; ` +
`foreach ($process in $processes) { ` +
`if ([string]::IsNullOrWhiteSpace($process.CommandLine)) { throw 'Chrome command line unavailable' }; ` +
`[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($process.CommandLine)) }`
output, err := hiddenCommand("powershell", "-NoProfile", "-Command", script).Output()
if err != nil {
return nil, fmt.Errorf("读取 Chrome 进程列表失败:%w", err)
}
var commands []string
for _, encoded := range strings.Fields(string(output)) {
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("解析 Chrome 进程命令行失败:%w", err)
}
commands = append(commands, string(decoded))
}
return commands, nil
}
func hasProfileChrome(commands []string, profile string) bool {
for _, command := range commands {
if commandBelongsToProfile(command, profile) {
return true
}
}
return false
}
// commandBelongsToProfile 只比较启动参数中的 --user-data-dir。
// Windows 路径不区分大小写,命令行还可能混用斜杠或给路径加引号。
func commandBelongsToProfile(commandLine, profile string) bool {
+20
View File
@@ -191,6 +191,26 @@ func Test端口全占满时返回错误(t *testing.T) {
}
}
func Test专属Profile进程检查(t *testing.T) {
profile := `C:\Users\测试\淘宝浏览器`
cases := []struct {
name string
commands []string
want bool
}{
{"其他Chrome", []string{`chrome.exe --user-data-dir=C:\Users\其他\Chrome --remote-debugging-port=19666`}, false},
{"专属Chrome", []string{`chrome.exe --user-data-dir="C:\Users\测试\淘宝浏览器" --remote-debugging-port=19666`}, true},
{"专属Chrome在范围外", []string{`chrome.exe --user-data-dir=C:\Users\测试\淘宝浏览器 --remote-debugging-port=30000`}, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := hasProfileChrome(tc.commands, profile); got != tc.want {
t.Fatalf("专属 Profile 进程判断应为 %v,实际为 %v", tc.want, got)
}
})
}
}
// 防回归:profile 路径含中文时的归属判断。
//
// 曾经真实发生过:PowerShell 输出用系统 ANSI 代码页(GBK),