fix: 退出前安全关闭淘宝专属 Chrome (#36)
This commit is contained in:
@@ -56,6 +56,7 @@ type App struct {
|
||||
uploadProgressMu sync.Mutex
|
||||
uploadProgress UploadProgress
|
||||
uploadStarted time.Time
|
||||
exitNoticeOnce sync.Once
|
||||
}
|
||||
|
||||
// NewApp 创建应用对象。真正的初始化在 startup 里做。
|
||||
@@ -119,7 +120,35 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown 在窗口关闭时被 Wails 调用,用来收尾。
|
||||
// beforeClose 在关闭窗口前安全停止取视频任务并清理专属 Chrome。
|
||||
func (a *App) beforeClose(ctx context.Context) bool {
|
||||
a.videoTaskMu.Lock()
|
||||
runner := a.videoTask
|
||||
a.videoTaskMu.Unlock()
|
||||
if runner != nil && runner.Snapshot().State == task.StateRunning {
|
||||
runner.Stop()
|
||||
a.log.Warn("取视频任务仍在运行,已请求停止;当前下载完成后请再次关闭程序")
|
||||
a.exitNoticeOnce.Do(func() {
|
||||
go func() {
|
||||
_, _ = runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.WarningDialog, Title: "正在停止取视频任务",
|
||||
Message: "已请求停止任务。请等待当前下载完成,再关闭程序。",
|
||||
})
|
||||
}()
|
||||
})
|
||||
return true
|
||||
}
|
||||
stateFile := config.ResolveDataPath("浏览器运行状态.json")
|
||||
closed, err := taobao.CloseBrowserOnExit(stateFile, a.cfg.Taobao.UserDataDir)
|
||||
if err != nil {
|
||||
a.log.Warn("退出时保留未能确认归属或关闭的淘宝专属 Chrome:%v", err)
|
||||
} else if closed {
|
||||
a.log.Info("退出前已关闭淘宝专属 Chrome")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shutdown 在窗口关闭后释放 SQLite。
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
if a.db != nil {
|
||||
if err := a.db.Close(); err != nil {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cmsp/internal/task"
|
||||
)
|
||||
|
||||
func Test退出前先停止取视频任务(t *testing.T) {
|
||||
started := make(chan struct{})
|
||||
runner := task.NewRunner(task.Options{
|
||||
Load: func(_ context.Context, id string) (task.Product, error) {
|
||||
return task.Product{ID: id}, nil
|
||||
},
|
||||
Prepare: func(ctx context.Context, _ task.Product) (task.Work, error) {
|
||||
close(started)
|
||||
<-ctx.Done()
|
||||
return task.Work{}, ctx.Err()
|
||||
},
|
||||
})
|
||||
if err := runner.Start(context.Background(), []string{"fictional-product"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("取视频任务未启动")
|
||||
}
|
||||
a := NewApp()
|
||||
a.videoTask = runner
|
||||
// 此测试没有 Wails 前端;只检查退出门,不打开系统消息框。
|
||||
a.exitNoticeOnce.Do(func() {})
|
||||
if !a.beforeClose(context.Background()) {
|
||||
t.Fatal("任务仍在运行时必须阻止本次退出")
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for runner.Snapshot().State == task.StateRunning && time.Now().Before(deadline) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if got := runner.Snapshot().State; got != task.StateStopped {
|
||||
t.Fatalf("退出前未停止任务:%s", got)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,16 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/chengma/cmsp/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: bbb42cb37cc4703c3953c021a7054f862ae312e0
|
||||
synchronized_at: 2026-10-05T10:01:40Z
|
||||
wiki_revision: 6deec49c2ba48aaf96a5842b4d525352afd6cf87
|
||||
synchronized_at: 2026-10-06T07:37:40Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
|
||||
## 程序退出与淘宝专属 Chrome(2026-10-06)
|
||||
|
||||
关闭 cmsp 时,若取视频任务仍在运行,先请求停止并阻止本次退出;当前商品处理结束后,使用者再次关闭程序。没有运行中的取视频任务时,仅当运行状态文件中的 PID、Chrome 路径、专属 Profile、调试端口与当前进程及 CDP 页面均可验证,才通过 CDP 请求 Chrome 正常退出;超时后重新核对 PID 归属,再强制结束该专属进程树。归属不明或调试端口不可验证时保留浏览器并记录原因,不影响 cmsp 退出。不会关闭日常 Chrome,也不删除 Chrome Profile 或读取登录凭据。
|
||||
|
||||
## 指定商品视频上传规则(2026-09-30)
|
||||
|
||||
单商品显示远端已有视频的覆盖警告;批量发现远端已有视频则跳过。上传文件取 Shopee ID 子目录下排序首个 mp4,先完成时长、格式、像素与大小校验。写入前在 SQLite 持久化幂等键。HTTP 202 和 `processing`/`unknown` 均不代表成功;再次操作仅查询原键,`succeeded` 才更新本地视频与上传状态。ERPGo 预检返回 200 且 `video: []` 时才确认远端当前无视频;返回 502 / `HHH_UPSTREAM_ERROR` / `stage=check` 表示视频状态未知,显示“视频状态无法确认,上传未提交”,不创建上传操作、不发送 PUT,保留本地 MP4 与 requestId。批量路径的该商品上传状态为 `unconfirmed`,不记作上传失败。`tempVideoUrl` 或 `videoUploadIdStr` 非空表示远端仍有处理中标记:单商品预览停止,执行前再次检查;批量遇到该商品标为待处理,不创建新操作、不发送 PUT。只有普通已存在视频且没有处理中标记时,单商品才可在明确覆盖确认后继续。用户确认后弹窗立即关闭,上传按钮在调用期间继续显示运行中;弹窗关闭不代表远端成功。单商品已有本地未决操作时,确认框改为“恢复原上传操作”,只查询或恢复原键,不提示覆盖、不发新 PUT;操作明确失败后才按新上传流程检查当前视频。远端操作 processing/unknown 或操作查询异常保留原键,商品上传状态为 `unconfirmed`,列表显示“待确认”;只有本次操作返回 `succeeded` 才显示“已上传”。
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Local-Development-and-Verification
|
||||
wiki_url: https://git.ilapage.cn/chengma/cmsp/wiki/Local-Development-and-Verification.-
|
||||
wiki_revision: 69f9ab4712b8efd0aa6f31e08d39b51503c88957
|
||||
synchronized_at: 2026-10-06T07:03:18Z
|
||||
wiki_revision: 697d2e353c3b4b3f6648b7a08e4752235c42c7e7
|
||||
synchronized_at: 2026-10-06T07:37:41Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 本地开发与验证
|
||||
|
||||
## 退出前关闭淘宝专属 Chrome 的验证(2026-10-06,#36)
|
||||
|
||||
`app_close_test.go` 验证运行中的取视频任务先请求停止且本次窗口关闭被阻止;`internal/taobao/chrome_test.go` 验证专属 PID/Profile/调试端口匹配时正常关闭、超时后重新核对 PID 才强制结束,以及归属或 CDP 不明时保留浏览器;`internal/taobao/cdp_test.go` 验证 `Browser.close` 发送至本机调试端口,拒绝外部 WebSocket 地址。从仓库根目录执行 `go test ./... -count=1`、`go test -race ./...`、`go vet ./...`、`python dev_scripts/harness.py check --strict` 和 Wails Windows 构建。
|
||||
|
||||
真机验收:启动 cmsp 专属 Chrome,关闭 cmsp 后确认该专属进程退出、日常 Chrome 仍在、Profile 保留;取视频任务中关闭程序时确认先停任务、待当前处理结束再次关闭。模拟测试和构建不能代替真机进程退出及 Wails 窗口交互验证。
|
||||
|
||||
## 批量上传进度验证(2026-10-06)
|
||||
|
||||
`UploadVideos` 使用后端内存快照与 `upload:progress` 事件报告当前批次,前端每秒轮询 `GetUploadProgress` 兜底。每个商品处理返回后从 SQLite 读取上传状态并累计一次;并发事件用批次号和已处理数避免旧事件回退显示,列表刷新共用串行合并队列并在批次结束时等待最后一次刷新。`TestUploadBatchRespectsConfiguredConcurrency` 覆盖并发 1/3 与去重总数,`TestUploadProgressClassifiesSQLiteOutcomesConcurrently` 覆盖终态分类,`TestUploadProgressAdvancesBeforeBatchFinishes` 覆盖未完成批次的逐商品进度及待确认不计入已上传。运行 `go test ./...`、`go test -race ./...`、`go vet ./...`、`npm --prefix frontend run build` 和 Wails Windows 构建;真实店铺上传仍需使用者明确确认并单独验收。
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -248,6 +249,31 @@ func cdpVersionAvailable(port int) bool {
|
||||
return fetchDebugJSON(port, "json/version", &payload) == nil
|
||||
}
|
||||
|
||||
// requestBrowserClose 只连接已验证的本机调试端口,请求 Chrome 正常退出。
|
||||
func requestBrowserClose(port int) error {
|
||||
var version struct {
|
||||
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
|
||||
}
|
||||
if err := fetchDebugJSON(port, "json/version", &version); err != nil {
|
||||
return err
|
||||
}
|
||||
endpoint, err := url.Parse(version.WebSocketDebuggerURL)
|
||||
if err != nil || endpoint.Scheme != "ws" || (endpoint.Hostname() != "127.0.0.1" && endpoint.Hostname() != "localhost") || endpoint.Port() != strconv.Itoa(port) {
|
||||
return fmt.Errorf("Chrome 浏览器调试地址不属于已验证的本机端口")
|
||||
}
|
||||
dialer := *websocket.DefaultDialer
|
||||
dialer.HandshakeTimeout = cdpRequestTimeout
|
||||
conn, _, err := dialer.Dial(endpoint.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接 Chrome 浏览器调试端口失败:%w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
if err := conn.SetWriteDeadline(time.Now().Add(cdpRequestTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.WriteJSON(map[string]any{"id": 1, "method": "Browser.close"})
|
||||
}
|
||||
|
||||
func cdpHasPage(port int) bool {
|
||||
targets, err := fetchDebugTargets(port)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,9 +2,71 @@ package taobao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func Test正常关闭Chrome通过浏览器CDP端点(t *testing.T) {
|
||||
methods := make(chan string, 1)
|
||||
upgrader := websocket.Upgrader{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/json/version":
|
||||
fmt.Fprintf(w, `{"webSocketDebuggerUrl":"ws://%s/devtools/browser/test"}`, r.Host)
|
||||
case "/devtools/browser/test":
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
t.Errorf("WebSocket 升级失败:%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
var request struct {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
if err := conn.ReadJSON(&request); err != nil {
|
||||
t.Errorf("读取关闭请求失败:%v", err)
|
||||
return
|
||||
}
|
||||
methods <- request.Method
|
||||
default:
|
||||
t.Errorf("访问了意外端点:%s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
parsed, _ := url.Parse(server.URL)
|
||||
port, _ := strconv.Atoi(parsed.Port())
|
||||
if err := requestBrowserClose(port); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case method := <-methods:
|
||||
if method != "Browser.close" {
|
||||
t.Fatalf("关闭命令错误:%s", method)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("未收到 Browser.close 命令")
|
||||
}
|
||||
}
|
||||
|
||||
func Test关闭Chrome拒绝外部WebSocket地址(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"webSocketDebuggerUrl":"ws://example.invalid/devtools/browser/test"}`)
|
||||
}))
|
||||
defer server.Close()
|
||||
parsed, _ := url.Parse(server.URL)
|
||||
port, _ := strconv.Atoi(parsed.Port())
|
||||
if err := requestBrowserClose(port); err == nil {
|
||||
t.Fatal("浏览器关闭请求不能连接外部地址")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDP响应跳过事件并匹配目标ID(t *testing.T) {
|
||||
messages := [][]byte{
|
||||
[]byte(`{"method":"Page.loadEventFired","params":{"timestamp":1}}`),
|
||||
|
||||
@@ -146,6 +146,107 @@ func CloseBrowser(stateFile, profile string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type exitBrowserOps struct {
|
||||
exists func(int) bool
|
||||
command func(int) (string, error)
|
||||
cdpReady func(int) bool
|
||||
graceful func(int) error
|
||||
force func(int) error
|
||||
waitExit func(int, time.Duration) bool
|
||||
}
|
||||
|
||||
// CloseBrowserOnExit 只在退出前关闭可验证归属的专属 Chrome。
|
||||
// 正常关闭超时后再验证一次 PID,才允许强制结束该进程树。
|
||||
func CloseBrowserOnExit(stateFile, profile string) (bool, error) {
|
||||
return closeBrowserOnExitWith(stateFile, profile, exitBrowserOps{
|
||||
exists: processExists,
|
||||
command: processCommandLine,
|
||||
cdpReady: func(port int) bool { return cdpVersionAvailable(port) && cdpHasPage(port) },
|
||||
graceful: requestBrowserClose,
|
||||
force: killProcessTree,
|
||||
waitExit: waitForProcessExit,
|
||||
})
|
||||
}
|
||||
|
||||
func closeBrowserOnExitWith(stateFile, profile string, ops exitBrowserOps) (bool, error) {
|
||||
state, found, err := readBrowserState(stateFile)
|
||||
if err != nil || !found {
|
||||
return false, err
|
||||
}
|
||||
if !ops.exists(state.PID) {
|
||||
return false, nil
|
||||
}
|
||||
if err := verifyExitBrowserOwner(state, profile, ops.command); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if state.Port <= 0 || !ops.cdpReady(state.Port) {
|
||||
return false, fmt.Errorf("无法确认专属 Chrome 的调试端口和页面,未关闭浏览器")
|
||||
}
|
||||
_ = ops.graceful(state.Port)
|
||||
if !ops.waitExit(state.PID, 3*time.Second) && ops.exists(state.PID) {
|
||||
// 正常关闭期间 PID 可能退出并被复用,强制结束前必须重新核对命令行。
|
||||
if err := verifyExitBrowserOwner(state, profile, ops.command); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := ops.force(state.PID); err != nil {
|
||||
return false, fmt.Errorf("关闭专属 Chrome 失败:%w", err)
|
||||
}
|
||||
if !ops.waitExit(state.PID, 2*time.Second) {
|
||||
return false, fmt.Errorf("专属 Chrome 在强制关闭后仍未退出")
|
||||
}
|
||||
}
|
||||
if err := os.Remove(stateFile); err != nil && !os.IsNotExist(err) {
|
||||
return true, fmt.Errorf("浏览器已关闭,但删除运行状态文件失败:%w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func verifyExitBrowserOwner(state BrowserState, profile string, command func(int) (string, error)) error {
|
||||
if state.PID <= 0 || strings.TrimSpace(profile) == "" || strings.TrimSpace(state.ProfilePath) == "" || strings.TrimSpace(state.ChromePath) == "" || normalizeWindowsPath(state.ProfilePath) != normalizeWindowsPath(profile) {
|
||||
return fmt.Errorf("浏览器运行状态与当前专属 Profile 不匹配,未关闭浏览器")
|
||||
}
|
||||
line, err := command(state.PID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法校验专属 Chrome 的进程归属,未关闭浏览器:%w", err)
|
||||
}
|
||||
chrome := normalizeWindowsPath(state.ChromePath)
|
||||
normalized := strings.TrimSpace(normalizeWindowsText(line))
|
||||
if !commandBelongsToProfile(line, profile) || !commandHasDebugPort(line, state.Port) || !(normalized == chrome || strings.HasPrefix(normalized, chrome+" ")) {
|
||||
return fmt.Errorf("PID %d 不属于本程序专属 Chrome,未关闭浏览器", state.PID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func commandHasDebugPort(line string, port int) bool {
|
||||
if port <= 0 {
|
||||
return false
|
||||
}
|
||||
command := normalizeWindowsText(line)
|
||||
target := "--remote-debugging-port=" + strconv.Itoa(port)
|
||||
for offset := 0; ; {
|
||||
index := strings.Index(command[offset:], target)
|
||||
if index < 0 {
|
||||
return false
|
||||
}
|
||||
end := offset + index + len(target)
|
||||
if end == len(command) || command[end] == ' ' || command[end] == '\t' {
|
||||
return true
|
||||
}
|
||||
offset = end
|
||||
}
|
||||
}
|
||||
|
||||
func waitForProcessExit(pid int, timeout time.Duration) bool {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for processExists(pid) {
|
||||
if time.Now().After(deadline) {
|
||||
return false
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validateBrowserConfig(cfg config.TaobaoConfig, openURL string) error {
|
||||
if strings.TrimSpace(cfg.ChromePath) == "" {
|
||||
return fmt.Errorf("Chrome 可执行文件路径不能为空")
|
||||
|
||||
@@ -1,10 +1,153 @@
|
||||
package taobao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test退出时只关闭确认归属的专属Chrome(t *testing.T) {
|
||||
profile := `C:\Users\测试\淘宝专属浏览器`
|
||||
chrome := `C:\Program Files\Google\Chrome\Application\chrome.exe`
|
||||
stateFile := filepath.Join(t.TempDir(), "浏览器运行状态.json")
|
||||
if err := writeBrowserState(stateFile, BrowserState{PID: 1234, Port: 19666, ProfilePath: profile, ChromePath: chrome}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
commandLine := `"` + chrome + `" --user-data-dir="` + profile + `" --remote-debugging-port=19666`
|
||||
commandCalls, gracefulCalls, forceCalls := 0, 0, 0
|
||||
ops := exitBrowserOps{
|
||||
exists: func(int) bool { return true },
|
||||
command: func(int) (string, error) {
|
||||
commandCalls++
|
||||
return commandLine, nil
|
||||
},
|
||||
cdpReady: func(int) bool { return true },
|
||||
graceful: func(int) error {
|
||||
gracefulCalls++
|
||||
return nil
|
||||
},
|
||||
force: func(int) error {
|
||||
forceCalls++
|
||||
return nil
|
||||
},
|
||||
waitExit: func(_ int, timeout time.Duration) bool { return timeout == 3*time.Second },
|
||||
}
|
||||
closed, err := closeBrowserOnExitWith(stateFile, profile, ops)
|
||||
if err != nil || !closed || commandCalls != 1 || gracefulCalls != 1 || forceCalls != 0 {
|
||||
t.Fatalf("正常关闭路径错误:closed=%v err=%v command=%d graceful=%d force=%d", closed, err, commandCalls, gracefulCalls, forceCalls)
|
||||
}
|
||||
if _, err := os.Stat(stateFile); !os.IsNotExist(err) {
|
||||
t.Fatalf("成功关闭后应删除运行状态文件:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test退出强制关闭前再次核对PID归属(t *testing.T) {
|
||||
profile := `C:\Users\测试\淘宝专属浏览器`
|
||||
chrome := `C:\Program Files\Google\Chrome\Application\chrome.exe`
|
||||
for _, changed := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "仍归属本程序", true: "PID已复用"}[changed], func(t *testing.T) {
|
||||
stateFile := filepath.Join(t.TempDir(), "浏览器运行状态.json")
|
||||
if err := writeBrowserState(stateFile, BrowserState{PID: 1234, Port: 19666, ProfilePath: profile, ChromePath: chrome}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checks, forced := 0, 0
|
||||
ops := exitBrowserOps{
|
||||
exists: func(int) bool { return true },
|
||||
command: func(int) (string, error) {
|
||||
checks++
|
||||
if changed && checks == 2 {
|
||||
return `"C:\Program Files\Google\Chrome\Application\chrome.exe" --user-data-dir=C:\Users\其他用户`, nil
|
||||
}
|
||||
return `"` + chrome + `" --user-data-dir="` + profile + `" --remote-debugging-port=19666`, nil
|
||||
},
|
||||
cdpReady: func(int) bool { return true },
|
||||
graceful: func(int) error { return errors.New("CDP 关闭失败") },
|
||||
force: func(int) error { forced++; return nil },
|
||||
waitExit: func(_ int, timeout time.Duration) bool { return timeout == 2*time.Second },
|
||||
}
|
||||
closed, err := closeBrowserOnExitWith(stateFile, profile, ops)
|
||||
if checks != 2 || forced != map[bool]int{false: 1, true: 0}[changed] {
|
||||
t.Fatalf("强制关闭前归属校验错误:closed=%v err=%v checks=%d forced=%d", closed, err, checks, forced)
|
||||
}
|
||||
if changed && (closed || err == nil) {
|
||||
t.Fatalf("PID 复用时必须保留浏览器:closed=%v err=%v", closed, err)
|
||||
}
|
||||
if !changed && (!closed || err != nil) {
|
||||
t.Fatalf("归属一致时应关闭:closed=%v err=%v", closed, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test退出时归属不明不发送关闭命令(t *testing.T) {
|
||||
stateFile := filepath.Join(t.TempDir(), "浏览器运行状态.json")
|
||||
if err := writeBrowserState(stateFile, BrowserState{PID: 1234, Port: 19666, ProfilePath: `C:\Users\甲\专属`, ChromePath: `C:\Chrome\chrome.exe`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
called := false
|
||||
ops := exitBrowserOps{
|
||||
exists: func(int) bool { return true },
|
||||
command: func(int) (string, error) { return `C:\Chrome\chrome.exe --user-data-dir=C:\Users\甲\专属`, nil },
|
||||
cdpReady: func(int) bool { called = true; return true },
|
||||
graceful: func(int) error { called = true; return nil },
|
||||
force: func(int) error { called = true; return nil },
|
||||
waitExit: func(int, time.Duration) bool { called = true; return true },
|
||||
}
|
||||
closed, err := closeBrowserOnExitWith(stateFile, `C:\Users\乙\专属`, ops)
|
||||
if closed || err == nil || called {
|
||||
t.Fatalf("Profile 不匹配不应关闭:closed=%v err=%v called=%v", closed, err, called)
|
||||
}
|
||||
}
|
||||
|
||||
func Test退出时CDP不可验证则保留Chrome(t *testing.T) {
|
||||
profile := `C:\Users\测试\淘宝专属浏览器`
|
||||
stateFile := filepath.Join(t.TempDir(), "浏览器运行状态.json")
|
||||
if err := writeBrowserState(stateFile, BrowserState{PID: 1234, Port: 19666, ProfilePath: profile, ChromePath: `C:\Chrome\chrome.exe`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
called := false
|
||||
ops := exitBrowserOps{
|
||||
exists: func(int) bool { return true },
|
||||
command: func(int) (string, error) {
|
||||
return `C:\Chrome\chrome.exe --user-data-dir=` + profile + ` --remote-debugging-port=19666`, nil
|
||||
},
|
||||
cdpReady: func(int) bool { return false },
|
||||
graceful: func(int) error { called = true; return nil },
|
||||
force: func(int) error { called = true; return nil },
|
||||
waitExit: func(int, time.Duration) bool { called = true; return true },
|
||||
}
|
||||
closed, err := closeBrowserOnExitWith(stateFile, profile, ops)
|
||||
if closed || err == nil || called {
|
||||
t.Fatalf("CDP 不可验证时不得关闭:closed=%v err=%v called=%v", closed, err, called)
|
||||
}
|
||||
}
|
||||
|
||||
func Test退出时调试端口不匹配则保留Chrome(t *testing.T) {
|
||||
profile := `C:\Users\测试\淘宝专属浏览器`
|
||||
stateFile := filepath.Join(t.TempDir(), "浏览器运行状态.json")
|
||||
if err := writeBrowserState(stateFile, BrowserState{PID: 1234, Port: 19666, ProfilePath: profile, ChromePath: `C:\Chrome\chrome.exe`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
called := false
|
||||
ops := exitBrowserOps{
|
||||
exists: func(int) bool { return true },
|
||||
command: func(int) (string, error) {
|
||||
return `C:\Chrome\chrome.exe --user-data-dir=` + profile + ` --remote-debugging-port=19667`, nil
|
||||
},
|
||||
cdpReady: func(int) bool { called = true; return true },
|
||||
graceful: func(int) error { called = true; return nil },
|
||||
force: func(int) error { called = true; return nil },
|
||||
waitExit: func(int, time.Duration) bool { called = true; return true },
|
||||
}
|
||||
closed, err := closeBrowserOnExitWith(stateFile, profile, ops)
|
||||
if closed || err == nil || called {
|
||||
t.Fatalf("调试端口不匹配时不得关闭:closed=%v err=%v called=%v", closed, err, called)
|
||||
}
|
||||
}
|
||||
|
||||
func Test命令行归属判断(t *testing.T) {
|
||||
profile := `C:\Users\测试 用户\淘宝浏览器\默认账号`
|
||||
cases := []struct {
|
||||
|
||||
@@ -60,8 +60,9 @@ func main() {
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
OnStartup: app.startup,
|
||||
OnShutdown: app.shutdown,
|
||||
OnStartup: app.startup,
|
||||
OnBeforeClose: app.beforeClose,
|
||||
OnShutdown: app.shutdown,
|
||||
// 打开浏览器默认右键菜单,这样在商品主图弹窗上右键才有「图片另存为」。
|
||||
//
|
||||
// 代价:整个界面右键都会出现浏览器菜单,其中包含「检查」,
|
||||
|
||||
Reference in New Issue
Block a user