265 lines
6.1 KiB
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
|
|
}
|