cremote/browser/browser.go

265 lines
6.1 KiB
Go

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
}