fix: Chrome 调试端口占用时顺位尝试 (#38)
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user