package browser import ( "errors" "fmt" "sync" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" ) // Manager handles the connection to a Chrome browser instance type Manager struct { browser *rod.Browser tabs map[string]*rod.Page mu sync.Mutex isNewBrowser bool // Tracks if we launched a new browser or connected to existing one storage *TabStorage // Persistent storage for tab IDs } // NewManager creates a new browser manager // If launchNew is true, it will launch a new browser instance // Otherwise, it will try to connect to an existing browser instance func NewManager(launchNew bool) (*Manager, error) { var browser *rod.Browser var isNewBrowser bool // Initialize tab storage storage, err := NewTabStorage() if err != nil { return nil, fmt.Errorf("failed to initialize tab storage: %w", err) } if launchNew { // Launch a new browser instance u := launcher.New().MustLaunch() browser = rod.New().ControlURL(u).MustConnect() isNewBrowser = true } else { // Connect to an existing browser instance // This assumes Chrome is running with --remote-debugging-port=9222 u := launcher.MustResolveURL("") browser = rod.New().ControlURL(u) err := browser.Connect() if err != nil { return nil, fmt.Errorf("failed to connect to browser: %w\nMake sure Chrome is running with --remote-debugging-port=9222", err) } isNewBrowser = false } return &Manager{ browser: browser, tabs: make(map[string]*rod.Page), isNewBrowser: isNewBrowser, storage: storage, }, nil } // OpenTab opens a new tab and returns its ID func (m *Manager) OpenTab() (string, error) { m.mu.Lock() defer m.mu.Unlock() page, err := m.browser.Page(proto.TargetCreateTarget{URL: "about:blank"}) if err != nil { return "", fmt.Errorf("failed to create new tab: %w", err) } // Use the page ID as the tab ID tabID := string(page.TargetID) m.tabs[tabID] = page // Save the tab ID to persistent storage err = m.storage.SaveTab(tabID) if err != nil { return "", fmt.Errorf("failed to save tab ID: %w", err) } return tabID, nil } // GetTab returns a tab by its ID func (m *Manager) GetTab(tabID string) (*rod.Page, error) { m.mu.Lock() defer m.mu.Unlock() // First check in-memory cache page, exists := m.tabs[tabID] if exists { return page, nil } // If not in memory, check persistent storage storedID, exists := m.storage.GetTab(tabID) if !exists { return nil, errors.New("tab not found") } // Try to get the page from the browser pages, err := m.browser.Pages() if err != nil { return nil, fmt.Errorf("failed to get browser pages: %w", err) } // Find the page with the matching ID for _, p := range pages { if string(p.TargetID) == storedID { // Cache it for future use m.tabs[tabID] = p return p, nil } } // If we get here, the tab no longer exists m.storage.RemoveTab(tabID) return nil, errors.New("tab not found or was closed") } // CloseTab closes a tab by its ID func (m *Manager) CloseTab(tabID string) error { m.mu.Lock() defer m.mu.Unlock() // First check in-memory cache page, exists := m.tabs[tabID] if !exists { // If not in memory, check persistent storage storedID, exists := m.storage.GetTab(tabID) if !exists { return errors.New("tab not found") } // Try to get the page from the browser pages, err := m.browser.Pages() if err != nil { return fmt.Errorf("failed to get browser pages: %w", err) } // Find the page with the matching ID for _, p := range pages { if string(p.TargetID) == storedID { page = p exists = true break } } if !exists { // If we get here, the tab no longer exists, so just remove it from storage m.storage.RemoveTab(tabID) return errors.New("tab not found or was already closed") } } err := page.Close() if err != nil { return fmt.Errorf("failed to close tab: %w", err) } // Remove from in-memory cache and persistent storage delete(m.tabs, tabID) m.storage.RemoveTab(tabID) return nil } // Close closes the browser connection and all tabs func (m *Manager) Close() error { m.mu.Lock() defer m.mu.Unlock() // Clear the tabs map m.tabs = make(map[string]*rod.Page) // Only close the browser if we launched it if m.isNewBrowser { return m.browser.Close() } // For existing browsers, just disconnect without closing return nil } // LoadURL loads a URL in a tab func (m *Manager) LoadURL(tabID, url string) error { page, err := m.GetTab(tabID) if err != nil { return err } err = page.Navigate(url) if err != nil { return fmt.Errorf("failed to navigate to URL: %w", err) } // Wait for the page to be loaded err = page.WaitLoad() if err != nil { return fmt.Errorf("failed to wait for page load: %w", err) } return nil } // WaitNavigation waits for a navigation event to happen func (m *Manager) WaitNavigation(tabID string, timeout int) error { page, err := m.GetTab(tabID) if err != nil { return err } // Set a timeout for the navigation wait page = page.Timeout(time.Duration(timeout) * time.Second) // Wait for navigation page.WaitNavigation(proto.PageLifecycleEventNameLoad)() // Wait for the page to be fully loaded err = page.WaitLoad() if err != nil { return fmt.Errorf("navigation wait failed: %w", err) } return nil } // GetPageSource returns the entire source code of a page func (m *Manager) GetPageSource(tabID string) (string, error) { page, err := m.GetTab(tabID) if err != nil { return "", err } html, err := page.HTML() if err != nil { return "", fmt.Errorf("failed to get page HTML: %w", err) } return html, nil } // GetElementHTML returns the HTML of an element at the specified selector func (m *Manager) GetElementHTML(tabID, selector string) (string, error) { page, err := m.GetTab(tabID) if err != nil { return "", err } // Find the element element, err := page.Element(selector) if err != nil { return "", fmt.Errorf("failed to find element: %w", err) } // Get the HTML of the element html, err := element.HTML() if err != nil { return "", fmt.Errorf("failed to get element HTML: %w", err) } return html, nil }