package bridge import ( "context" "encoding/json" "fmt" "log/slog" "os" "path/filepath" "strings" "time" "github.com/chromedp/chromedp" ) var crashedPrefsReplacer = strings.NewReplacer( `"exit_type":"Crashed"`, `"exit_type":"Normal"`, `"exit_type": "Crashed"`, `"exit_type": "Normal"`, `"exited_cleanly":false`, `"exited_cleanly":true`, `"exited_cleanly": false`, `"exited_cleanly": true`, ) type TabState struct { ID string `json:"id"` URL string `json:"url"` Title string `json:"title"` } type SessionState struct { Tabs []TabState `json:"tabs"` SavedAt string `json:"savedAt"` } func isTransientURL(url string) bool { switch url { case "about:blank", "chrome://newtab/", "chrome://new-tab-page/": return true } return strings.HasPrefix(url, "chrome://") || strings.HasPrefix(url, "chrome-extension://") || strings.HasPrefix(url, "devtools://") || strings.HasPrefix(url, "file://") || strings.Contains(url, "localhost:") } func MarkCleanExit(profileDir string) { prefsPath := filepath.Join(profileDir, "Default", "Preferences") data, err := os.ReadFile(prefsPath) if err != nil { return } patched := crashedPrefsReplacer.Replace(string(data)) if patched != string(data) { if err := os.WriteFile(prefsPath, []byte(patched), 0644); err != nil { slog.Error("patch prefs", "err", err) } } } func WasUncleanExit(profileDir string) bool { prefsPath := filepath.Join(profileDir, "Default", "Preferences") data, err := os.ReadFile(prefsPath) if err != nil { return false } prefs := string(data) return strings.Contains(prefs, `"exit_type":"Crashed"`) || strings.Contains(prefs, `"exit_type": "Crashed"`) } var sessionRestoreFiles = []string{ "Current Session", "Current Tabs", "Last Session", "Last Tabs", } func ClearChromeSessions(profileDir string) { sessionsDir := filepath.Join(profileDir, "Default", "Sessions") if _, err := os.Stat(sessionsDir); os.IsNotExist(err) { return } var failed []string for _, name := range sessionRestoreFiles { p := filepath.Join(sessionsDir, name) if err := retryRemove(p, 3); err != nil { failed = append(failed, name) slog.Warn("failed to remove session file", "file", name, "err", err) } } if len(failed) == 0 { slog.Info("cleared Chrome session restore files") } } func retryRemove(path string, maxRetries int) error { var err error for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { time.Sleep(time.Duration(50*(1< 0 { time.Sleep(200 * time.Millisecond) } ctx, cancel := chromedp.NewContext(b.BrowserCtx) if err := chromedp.Run(ctx); err != nil { cancel() <-tabSem slog.Warn("restore tab failed", "url", tab.URL, "err", err) continue } newID := string(chromedp.FromContext(ctx).Target.TargetID) b.tabSetup(ctx) b.mu.Lock() b.tabs[newID] = &TabEntry{Ctx: ctx, Cancel: cancel} b.mu.Unlock() restored++ go func(tabCtx context.Context, url string) { defer func() { <-tabSem }() navSem <- struct{}{} defer func() { <-navSem }() tCtx, tCancel := context.WithTimeout(tabCtx, 15*time.Second) defer tCancel() _ = chromedp.Run(tCtx, chromedp.ActionFunc(func(ctx context.Context) error { p := map[string]any{"url": url} return chromedp.FromContext(ctx).Target.Execute(ctx, "Page.navigate", p, nil) })) }(ctx, tab.URL) } if restored > 0 { slog.Info("restored tabs", "count", restored, "concurrent_limit", maxConcurrentTabs) } }