mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
316 lines
12 KiB
Go
316 lines
12 KiB
Go
package backend
|
|
|
|
import (
|
|
"ant-chrome/backend/internal/browser"
|
|
"ant-chrome/backend/internal/logger"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type browserStartInput struct {
|
|
ProfileID string
|
|
ExtraLaunchArgs []string
|
|
StartURLs []string
|
|
SkipDefaultStartURLs bool
|
|
PreferVisibleWindow bool
|
|
ForceDirectProxy bool
|
|
TemporaryProxyID string
|
|
TemporaryProxyConfig string
|
|
}
|
|
|
|
type browserStartPlan struct {
|
|
profile *BrowserProfile
|
|
chromeBinaryPath string
|
|
userDataDir string
|
|
args []string
|
|
extensionDirs []string
|
|
deferredStartTargets []string
|
|
effectiveProxy string
|
|
acquiredProxyBridge profileProxyBridgeRef
|
|
releaseProxyBridge bool
|
|
assignedDebugPort int
|
|
startReadyTimeout time.Duration
|
|
startStableWindow time.Duration
|
|
maxStartAttempts int
|
|
totalReadyTimeout time.Duration
|
|
}
|
|
|
|
var clearBrowserSessionRestoreData = browser.ClearSessionRestoreData
|
|
|
|
func newBrowserStartInput(profileID string, extraLaunchArgs []string, startURLs []string, skipDefaultStartURLs bool, preferVisibleWindow bool, forceDirectProxy bool, proxyID string, proxyConfig string) browserStartInput {
|
|
normalizedExtraLaunchArgs := normalizeNonEmptyStrings(extraLaunchArgs)
|
|
|
|
return browserStartInput{
|
|
ProfileID: profileID,
|
|
ExtraLaunchArgs: normalizedExtraLaunchArgs,
|
|
StartURLs: normalizeNonEmptyStrings(startURLs),
|
|
SkipDefaultStartURLs: skipDefaultStartURLs,
|
|
PreferVisibleWindow: preferVisibleWindow,
|
|
ForceDirectProxy: forceDirectProxy,
|
|
TemporaryProxyID: strings.TrimSpace(proxyID),
|
|
TemporaryProxyConfig: strings.TrimSpace(proxyConfig),
|
|
}
|
|
}
|
|
|
|
func (input browserStartInput) hasTemporaryProxy() bool {
|
|
return strings.TrimSpace(input.TemporaryProxyID) != "" || strings.TrimSpace(input.TemporaryProxyConfig) != ""
|
|
}
|
|
|
|
func (plan *browserStartPlan) releaseBridgeIfNeeded(a *App) {
|
|
if plan == nil || a == nil {
|
|
return
|
|
}
|
|
if plan.releaseProxyBridge {
|
|
a.releaseProxyBridgeRef(plan.acquiredProxyBridge)
|
|
}
|
|
}
|
|
|
|
func (a *App) resolveBrowserStartProfile(input browserStartInput) (*BrowserProfile, bool, error) {
|
|
log := logger.New("Browser")
|
|
|
|
profile, exists := a.browserMgr.Profiles[input.ProfileID]
|
|
if !exists {
|
|
err := fmt.Errorf("实例启动失败:未找到实例配置(ID=%s)。请刷新列表后重试。", input.ProfileID)
|
|
log.Error("实例不存在", logger.F("profile_id", input.ProfileID), logger.F("reason", err.Error()))
|
|
return nil, false, err
|
|
}
|
|
a.ensureProfileLaunchCode(profile)
|
|
|
|
if !profile.Running {
|
|
return profile, false, nil
|
|
}
|
|
|
|
if !isBrowserProfileLive(profile, a.browserMgr.BrowserProcesses[input.ProfileID]) {
|
|
log.Info("检测到实例运行状态已失效,准备重新启动",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("pid", profile.Pid),
|
|
logger.F("debug_port", profile.DebugPort),
|
|
)
|
|
a.markProfileStoppedLocked(input.ProfileID, profile)
|
|
return profile, false, nil
|
|
}
|
|
|
|
if len(normalizeNonEmptyStrings(input.StartURLs)) == 0 && len(normalizeNonEmptyStrings(input.ExtraLaunchArgs)) == 0 {
|
|
if a.launchServer != nil && profile.DebugReady {
|
|
a.launchServer.SetActiveProfile(profile)
|
|
}
|
|
a.emitBrowserInstanceStarted(profile, true)
|
|
return profile, true, nil
|
|
}
|
|
|
|
if err := a.openBrowserTabForRunningProfile(profile, input.ExtraLaunchArgs, input.StartURLs); err != nil {
|
|
startErr := fmt.Errorf("实例已在运行,但新标签打开失败:%w", err)
|
|
log.Error("运行中实例新标签打开失败",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("debug_port", profile.DebugPort),
|
|
logger.F("error", err.Error()),
|
|
logger.F("reason", startErr.Error()),
|
|
)
|
|
profile.LastError = startErr.Error()
|
|
return profile, true, startErr
|
|
}
|
|
|
|
if a.launchServer != nil && profile.DebugReady {
|
|
a.launchServer.SetActiveProfile(profile)
|
|
}
|
|
a.emitBrowserInstanceStarted(profile, true)
|
|
return profile, true, nil
|
|
}
|
|
|
|
func (a *App) prepareBrowserStartPlan(input browserStartInput, profile *BrowserProfile) (*browserStartPlan, error) {
|
|
bookmarks := a.BookmarkList()
|
|
sanitizedProfileLaunchArgs, sanitizedExtraLaunchArgs, chromeBinaryPath, userDataDir, err := a.prepareBrowserLaunchContext(input, profile, bookmarks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
effectiveProxy, acquiredProxyBridge, releaseProxyBridge, err := a.resolveBrowserStartProxy(input, profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
startReadyTimeout, startStableWindow := a.browserStartTimingSettings()
|
|
maxStartAttempts := browserStartAttemptCount()
|
|
totalReadyTimeout := time.Duration(maxStartAttempts) * startReadyTimeout
|
|
restoreLastSession := browserRestoreLastSession(a.config)
|
|
extensionDirs := a.browserMgr.EnabledExtensionDirsForProfile(input.ProfileID)
|
|
defaultStartURLs := mergeStartURLs(browserDefaultStartURLs(a.config), bookmarkStartURLs(bookmarks))
|
|
launchTargets, deferredStartTargets := buildBrowserLaunchTargets(
|
|
input.StartURLs,
|
|
defaultStartURLs,
|
|
input.SkipDefaultStartURLs,
|
|
restoreLastSession,
|
|
browserLightStartEnabled(a.config),
|
|
)
|
|
|
|
assignedDebugPort, err := nextAvailablePort()
|
|
if err != nil {
|
|
startErr := fmt.Errorf("实例启动失败:本地调试端口分配失败。原因:%v。请关闭占用端口的程序后重试。", err)
|
|
logger.New("Browser").Error("调试端口分配失败",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("error", err.Error()),
|
|
logger.F("reason", startErr.Error()),
|
|
)
|
|
profile.LastError = startErr.Error()
|
|
return nil, startErr
|
|
}
|
|
|
|
return &browserStartPlan{
|
|
profile: profile,
|
|
chromeBinaryPath: chromeBinaryPath,
|
|
userDataDir: userDataDir,
|
|
extensionDirs: extensionDirs,
|
|
args: buildBrowserLaunchArgs(profile, userDataDir, assignedDebugPort, effectiveProxy, extensionDirs, sanitizedProfileLaunchArgs, sanitizedExtraLaunchArgs, launchTargets),
|
|
deferredStartTargets: deferredStartTargets,
|
|
effectiveProxy: effectiveProxy,
|
|
acquiredProxyBridge: acquiredProxyBridge,
|
|
releaseProxyBridge: releaseProxyBridge,
|
|
assignedDebugPort: assignedDebugPort,
|
|
startReadyTimeout: startReadyTimeout,
|
|
startStableWindow: startStableWindow,
|
|
maxStartAttempts: maxStartAttempts,
|
|
totalReadyTimeout: totalReadyTimeout,
|
|
}, nil
|
|
}
|
|
|
|
func (a *App) prepareBrowserLaunchContext(input browserStartInput, profile *BrowserProfile, bookmarks []BrowserBookmark) ([]string, []string, string, string, error) {
|
|
log := logger.New("Browser")
|
|
|
|
sanitizedProfileLaunchArgs, managedProfileArgs := sanitizeManagedLaunchArgs(profile.LaunchArgs)
|
|
sanitizedExtraLaunchArgs, managedExtraArgs := sanitizeManagedLaunchArgs(input.ExtraLaunchArgs)
|
|
logManagedLaunchArgOverrides(log, input.ProfileID, "profile.launchArgs", managedProfileArgs)
|
|
logManagedLaunchArgOverrides(log, input.ProfileID, "start.extraLaunchArgs", managedExtraArgs)
|
|
|
|
proxyChanged := a.browserMgr.ApplyDefaults(profile)
|
|
if proxyChanged {
|
|
_ = a.browserMgr.SaveProfiles()
|
|
}
|
|
|
|
chromeBinaryPath, err := a.browserMgr.ResolveChromeBinary(profile)
|
|
if err != nil {
|
|
startErr := fmt.Errorf("实例启动失败:%w", err)
|
|
log.Error("内核路径解析失败",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("error", err.Error()),
|
|
logger.F("reason", startErr.Error()),
|
|
)
|
|
profile.LastError = startErr.Error()
|
|
return nil, nil, "", "", startErr
|
|
}
|
|
|
|
userDataDir := a.browserMgr.ResolveUserDataDir(profile)
|
|
if err := os.MkdirAll(userDataDir, 0o755); err != nil {
|
|
startErr := fmt.Errorf("实例启动失败:无法创建用户数据目录 %s。原因:%w。请检查目录权限或路径配置。", userDataDir, err)
|
|
log.Error("用户数据目录创建失败",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("dir", userDataDir),
|
|
logger.F("error", err.Error()),
|
|
logger.F("reason", startErr.Error()),
|
|
)
|
|
profile.LastError = startErr.Error()
|
|
return nil, nil, "", "", startErr
|
|
}
|
|
|
|
if err := browser.EnsureDefaultBookmarks(userDataDir, bookmarks); err != nil {
|
|
log.Error("默认书签写入失败", logger.F("error", err.Error()))
|
|
}
|
|
|
|
if detection, ok := detectBrowserRuntimeByActivePort(userDataDir); ok && detection.DebugReady {
|
|
a.markProfileRunningLocked(input.ProfileID, profile, nil, detection.PID, detection.DebugPort, true, "")
|
|
log.Warn("检测到同一用户数据目录已有浏览器运行,已接管为当前实例状态",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("user_data_dir", userDataDir),
|
|
logger.F("pid", detection.PID),
|
|
logger.F("debug_port", detection.DebugPort),
|
|
)
|
|
if len(normalizeNonEmptyStrings(input.StartURLs)) == 0 && len(normalizeNonEmptyStrings(input.ExtraLaunchArgs)) == 0 {
|
|
return nil, nil, "", "", errBrowserStartHandledByRecoveredRuntime
|
|
}
|
|
if err := a.openBrowserTabForRunningProfile(profile, input.ExtraLaunchArgs, input.StartURLs); err != nil {
|
|
startErr := fmt.Errorf("实例已在运行,但新标签打开失败:%w", err)
|
|
profile.LastError = startErr.Error()
|
|
return nil, nil, "", "", startErr
|
|
}
|
|
return nil, nil, "", "", errBrowserStartHandledByRecoveredRuntime
|
|
}
|
|
|
|
if !browserRestoreLastSession(a.config) {
|
|
if err := clearBrowserSessionRestoreData(userDataDir); err != nil {
|
|
if terminated, terminateErr := terminateBrowserProcessesByUserDataDir(userDataDir, 5*time.Second); terminateErr == nil && terminated {
|
|
log.Warn("会话缓存被旧浏览器进程占用,已结束占用进程并重试清理",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("user_data_dir", userDataDir),
|
|
)
|
|
if retryErr := clearBrowserSessionRestoreData(userDataDir); retryErr == nil {
|
|
return sanitizedProfileLaunchArgs, sanitizedExtraLaunchArgs, chromeBinaryPath, userDataDir, nil
|
|
} else {
|
|
err = retryErr
|
|
}
|
|
} else if terminateErr != nil {
|
|
log.Warn("会话缓存清理失败后尝试结束占用进程失败",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("user_data_dir", userDataDir),
|
|
logger.F("error", terminateErr.Error()),
|
|
)
|
|
}
|
|
sessionDir := filepath.Join(userDataDir, "Default", "Sessions")
|
|
startErr := fmt.Errorf("实例启动失败:无法清理上次会话缓存 %s。原因:%w。请关闭占用该目录的浏览器进程后重试。", sessionDir, err)
|
|
log.Error("会话恢复缓存清理失败",
|
|
logger.F("profile_id", input.ProfileID),
|
|
logger.F("dir", sessionDir),
|
|
logger.F("error", err.Error()),
|
|
logger.F("reason", startErr.Error()),
|
|
)
|
|
profile.LastError = startErr.Error()
|
|
return nil, nil, "", "", startErr
|
|
}
|
|
}
|
|
|
|
return sanitizedProfileLaunchArgs, sanitizedExtraLaunchArgs, chromeBinaryPath, userDataDir, nil
|
|
}
|
|
|
|
func buildBrowserLaunchArgs(profile *BrowserProfile, userDataDir string, debugPort int, effectiveProxy string, extensionDirs []string, sanitizedProfileLaunchArgs []string, sanitizedExtraLaunchArgs []string, launchTargets []string) []string {
|
|
args := []string{
|
|
fmt.Sprintf("--user-data-dir=%s", userDataDir),
|
|
fmt.Sprintf("--remote-debugging-port=%d", debugPort),
|
|
"--disable-session-crashed-bubble",
|
|
}
|
|
|
|
hasFingerprint := false
|
|
for _, arg := range profile.FingerprintArgs {
|
|
if strings.HasPrefix(arg, "--fingerprint=") {
|
|
hasFingerprint = true
|
|
break
|
|
}
|
|
}
|
|
if !hasFingerprint {
|
|
seed := 0
|
|
for _, char := range profile.ProfileId {
|
|
seed = (seed << 5) - seed + int(char)
|
|
}
|
|
if seed < 0 {
|
|
seed = -seed
|
|
}
|
|
args = append(args, fmt.Sprintf("--fingerprint=%d", seed))
|
|
}
|
|
|
|
if effectiveProxy == "direct://" {
|
|
args = append(args, "--no-proxy-server")
|
|
} else if effectiveProxy != "" {
|
|
args = append(args, fmt.Sprintf("--proxy-server=%s", effectiveProxy))
|
|
}
|
|
|
|
if extensionArg := strings.Join(normalizeNonEmptyStrings(extensionDirs), ","); extensionArg != "" {
|
|
args = append(args, fmt.Sprintf("--disable-extensions-except=%s", extensionArg))
|
|
args = append(args, fmt.Sprintf("--load-extension=%s", extensionArg))
|
|
}
|
|
|
|
args = append(args, profile.FingerprintArgs...)
|
|
args = append(args, sanitizedProfileLaunchArgs...)
|
|
args = append(args, sanitizedExtraLaunchArgs...)
|
|
return browser.BuildLaunchArgs(args, launchTargets)
|
|
}
|