package daemon import ( "context" "encoding/json" "fmt" "io" "log" "net" "net/http" "os" "strconv" "sync" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" ) // Daemon is the main server that manages browser connections type Daemon struct { browser *rod.Browser tabs map[string]*rod.Page iframePages map[string]*rod.Page // Maps tab ID to iframe page context currentTab string // ID of the current/last used tab tabHistory []string // Stack of tab IDs in order of activation (LIFO) consoleLogs map[string][]ConsoleLog // Maps tab ID to console logs debug bool // Enable debug logging mu sync.Mutex server *http.Server } // ConsoleLog represents a console log entry type ConsoleLog struct { Level string `json:"level"` // log, warn, error, info, debug Message string `json:"message"` // The log message Timestamp time.Time `json:"timestamp"` // When the log occurred Source string `json:"source"` // Source location if available } // Command represents a command sent from the client to the daemon type Command struct { Action string `json:"action"` Params map[string]string `json:"params"` } // Response represents a response from the daemon to the client type Response struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } // checkChromeRunning checks if Chrome is running on the debug port func checkChromeRunning(port int) bool { conn, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", port), 2*time.Second) if err != nil { return false } conn.Close() return true } // checkChromeDevTools checks if Chrome DevTools protocol is responding func checkChromeDevTools(port int) bool { resp, err := http.Get(fmt.Sprintf("http://localhost:%d/json/version", port)) if err != nil { return false } defer resp.Body.Close() return resp.StatusCode == 200 } // debugLog logs a message only if debug mode is enabled func (d *Daemon) debugLog(format string, args ...interface{}) { if d.debug { log.Printf("[DEBUG] "+format, args...) } } // NewDaemon creates a new daemon instance func NewDaemon(host string, port int, debug bool) (*Daemon, error) { if debug { log.Printf("[DEBUG] Creating new daemon on %s:%d", host, port) } // Check if Chrome is running on the debug port chromePort := 9222 // Default Chrome debug port if debug { log.Printf("[DEBUG] Checking if Chrome is running on port %d", chromePort) } if !checkChromeRunning(chromePort) { return nil, fmt.Errorf("Chromium is not running with remote debugging enabled on port %d.\n\nTo start Chromium with remote debugging:\n chromium --remote-debugging-port=%d --user-data-dir=/tmp/chromium-debug &\n # or\n google-chrome --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug &\n\nNote: The --user-data-dir flag is required to avoid conflicts with existing browser instances.", chromePort, chromePort, chromePort) } // Check if Chromium DevTools protocol is responding if !checkChromeDevTools(chromePort) { return nil, fmt.Errorf("Something is listening on port %d but it's not Chromium DevTools protocol.\n\nThis might be:\n1. Chromium running without --remote-debugging-port=%d\n2. Another application using port %d\n\nTry stopping the process on port %d and starting Chromium with:\n chromium --remote-debugging-port=%d --user-data-dir=/tmp/chromium-debug &", chromePort, chromePort, chromePort, chromePort, chromePort) } // Connect to the existing browser instance u := launcher.MustResolveURL("") browser := rod.New().ControlURL(u) err := browser.Connect() if err != nil { return nil, fmt.Errorf("Chromium DevTools is responding on port %d but rod connection failed: %w\n\nThis is unexpected. Try restarting Chromium with:\n chromium --remote-debugging-port=%d --user-data-dir=/tmp/chromium-debug &", chromePort, err, chromePort) } if debug { log.Printf("[DEBUG] Successfully connected to browser via rod") } daemon := &Daemon{ browser: browser, tabs: make(map[string]*rod.Page), iframePages: make(map[string]*rod.Page), tabHistory: make([]string, 0), consoleLogs: make(map[string][]ConsoleLog), debug: debug, } daemon.debugLog("Daemon struct initialized") // Create HTTP server daemon.debugLog("Setting up HTTP server") mux := http.NewServeMux() mux.HandleFunc("/command", daemon.handleCommand) mux.HandleFunc("/status", daemon.handleStatus) mux.HandleFunc("/upload", daemon.handleFileUpload) mux.HandleFunc("/download", daemon.handleFileDownload) daemon.server = &http.Server{ Addr: fmt.Sprintf("%s:%d", host, port), Handler: mux, } daemon.debugLog("HTTP server configured on %s:%d", host, port) return daemon, nil } // Start starts the daemon server func (d *Daemon) Start() error { log.Printf("Starting daemon server on %s", d.server.Addr) d.debugLog("About to call ListenAndServe()") err := d.server.ListenAndServe() d.debugLog("ListenAndServe() returned with error: %v", err) return err } // Stop stops the daemon server func (d *Daemon) Stop() error { log.Println("Stopping daemon server") return d.server.Close() } // handleStatus handles status requests func (d *Daemon) handleStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } d.mu.Lock() tabCount := len(d.tabs) tabs := make(map[string]string) currentTab := d.currentTab tabHistory := make([]string, len(d.tabHistory)) copy(tabHistory, d.tabHistory) // Get info about each tab for id, page := range d.tabs { try := func() string { info, err := page.Info() if err != nil { return "" } return info.URL } tabs[id] = try() } d.mu.Unlock() response := Response{ Success: true, Data: map[string]interface{}{ "status": "running", "tab_count": tabCount, "tabs": tabs, "current_tab": currentTab, "tab_history": tabHistory, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // handleCommand handles command requests func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) { d.debugLog("Received HTTP request: %s %s", r.Method, r.URL.Path) if r.Method != http.MethodPost { d.debugLog("Invalid method: %s", r.Method) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var cmd Command err := json.NewDecoder(r.Body).Decode(&cmd) if err != nil { d.debugLog("Failed to decode JSON: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } d.debugLog("Processing command: %s with params: %+v", cmd.Action, cmd.Params) var response Response switch cmd.Action { case "open-tab": timeoutStr := cmd.Params["timeout"] // Parse timeout (default to 5 seconds if not specified) timeout := 5 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } tabID, err := d.openTab(timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: tabID} } case "load-url": tabID := cmd.Params["tab"] url := cmd.Params["url"] timeoutStr := cmd.Params["timeout"] // Parse timeout (default to 5 seconds if not specified) timeout := 5 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } err := d.loadURL(tabID, url, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "fill-form": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] value := cmd.Params["value"] // Parse timeouts selectionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["selection-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { selectionTimeout = parsedTimeout } } actionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["action-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { actionTimeout = parsedTimeout } } err := d.fillFormField(tabID, selector, value, selectionTimeout, actionTimeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "upload-file": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] filePath := cmd.Params["file"] // Parse timeouts selectionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["selection-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { selectionTimeout = parsedTimeout } } actionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["action-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { actionTimeout = parsedTimeout } } err := d.uploadFile(tabID, selector, filePath, selectionTimeout, actionTimeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "submit-form": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] // Parse timeouts selectionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["selection-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { selectionTimeout = parsedTimeout } } actionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["action-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { actionTimeout = parsedTimeout } } err := d.submitForm(tabID, selector, selectionTimeout, actionTimeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "get-source": tabID := cmd.Params["tab"] timeoutStr := cmd.Params["timeout"] // Parse timeout (default to 5 seconds if not specified) timeout := 5 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } source, err := d.getPageSource(tabID, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: source} } case "get-element": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] // Parse timeouts selectionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["selection-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { selectionTimeout = parsedTimeout } } html, err := d.getElementHTML(tabID, selector, selectionTimeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: html} } case "close-tab": tabID := cmd.Params["tab"] timeoutStr := cmd.Params["timeout"] // Parse timeout (default to 5 seconds if not specified) timeout := 5 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } err := d.closeTab(tabID, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "wait-navigation": tabID := cmd.Params["tab"] timeout := 5 // Default timeout if timeoutStr, ok := cmd.Params["timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } err := d.waitNavigation(tabID, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "click-element": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] // Parse timeouts selectionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["selection-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { selectionTimeout = parsedTimeout } } actionTimeout := 5 // Default: 5 seconds if timeoutStr, ok := cmd.Params["action-timeout"]; ok { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { actionTimeout = parsedTimeout } } err := d.clickElement(tabID, selector, selectionTimeout, actionTimeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "eval-js": tabID := cmd.Params["tab"] jsCode := cmd.Params["code"] timeoutStr := cmd.Params["timeout"] // Parse timeout (default to 5 seconds if not specified) timeout := 5 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } result, err := d.evalJS(tabID, jsCode, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "switch-iframe": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] timeoutStr := cmd.Params["timeout"] // Parse timeout (default to 5 seconds if not specified) timeout := 5 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } err := d.switchToIframe(tabID, selector, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "switch-main": tabID := cmd.Params["tab"] err := d.switchToMain(tabID) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "screenshot": tabID := cmd.Params["tab"] outputPath := cmd.Params["output"] fullPageStr := cmd.Params["full-page"] timeoutStr := cmd.Params["timeout"] // Parse full-page flag fullPage := false if fullPageStr == "true" { fullPage = true } // Parse timeout (default to 5 seconds if not specified) timeout := 5 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } err := d.takeScreenshot(tabID, outputPath, fullPage, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "console-logs": tabID := cmd.Params["tab"] clearStr := cmd.Params["clear"] // Parse clear flag clear := false if clearStr == "true" { clear = true } logs, err := d.getConsoleLogs(tabID, clear) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: logs} } case "console-command": tabID := cmd.Params["tab"] command := cmd.Params["command"] timeoutStr := cmd.Params["timeout"] // Parse timeout (default to 5 seconds if not specified) timeout := 5 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } result, err := d.executeConsoleCommand(tabID, command, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } default: d.debugLog("Unknown action: %s", cmd.Action) response = Response{Success: false, Error: "Unknown action"} } d.debugLog("Command %s completed, sending response: success=%v", cmd.Action, response.Success) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) d.debugLog("Response sent for command: %s", cmd.Action) } // openTab opens a new tab and returns its ID func (d *Daemon) openTab(timeout int) (string, error) { d.debugLog("Opening new tab with timeout: %d", timeout) d.mu.Lock() defer d.mu.Unlock() // Create a context with timeout if specified if timeout > 0 { d.debugLog("Using timeout context: %d seconds", timeout) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan struct { page *rod.Page tabID string err error }, 1) // Execute the tab creation in a goroutine go func() { page, err := d.browser.Page(proto.TargetCreateTarget{URL: "about:blank"}) var tabID string if err == nil { tabID = string(page.TargetID) } done <- struct { page *rod.Page tabID string err error }{page, tabID, err} }() // Wait for either completion or timeout select { case res := <-done: if res.err != nil { return "", fmt.Errorf("failed to create new tab: %w", res.err) } // Store the tab and update history d.tabs[res.tabID] = res.page d.tabHistory = append(d.tabHistory, res.tabID) d.currentTab = res.tabID // Set up console logging for this tab d.setupConsoleLogging(res.tabID, res.page) return res.tabID, nil case <-ctx.Done(): return "", fmt.Errorf("opening tab timed out after %d seconds", timeout) } } else { // No timeout page, err := d.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) d.tabs[tabID] = page // Add to tab history stack and set as current tab d.tabHistory = append(d.tabHistory, tabID) d.currentTab = tabID // Set up console logging for this tab d.setupConsoleLogging(tabID, page) return tabID, nil } } // getTabID returns the tab ID to use, falling back to the current tab if none is provided func (d *Daemon) getTabID(tabID string) (string, error) { d.mu.Lock() defer d.mu.Unlock() // If no tab ID is provided, use the current tab if tabID == "" { if d.currentTab == "" { return "", fmt.Errorf("no current tab available, please open a tab first") } return d.currentTab, nil } // Otherwise, use the provided tab ID return tabID, nil } // updateTabHistory updates the tab history stack when a tab is activated func (d *Daemon) updateTabHistory(tabID string) { // Set as current tab d.currentTab = tabID // Remove the tab from history if it's already there for i, id := range d.tabHistory { if id == tabID { // Remove this tab from history d.tabHistory = append(d.tabHistory[:i], d.tabHistory[i+1:]...) break } } // Add the tab to the end of history (most recent) d.tabHistory = append(d.tabHistory, tabID) } // findPageByID finds a page by its ID without updating the current tab or cache func (d *Daemon) findPageByID(tabID string) (*rod.Page, error) { // If not in memory, try to get the page from the browser pages, err := d.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) == tabID { return p, nil } } return nil, fmt.Errorf("tab not found: %s", tabID) } // getTab returns a tab by its ID, checking for iframe context first func (d *Daemon) getTab(tabID string) (*rod.Page, error) { // Get the tab ID to use (may be the current tab) actualTabID, err := d.getTabID(tabID) if err != nil { return nil, err } d.mu.Lock() defer d.mu.Unlock() // First check if we have an iframe context for this tab if iframePage, exists := d.iframePages[actualTabID]; exists { // Update tab history and current tab d.updateTabHistory(actualTabID) return iframePage, nil } // Check in-memory cache for main page page, exists := d.tabs[actualTabID] if exists { // Update tab history and current tab d.updateTabHistory(actualTabID) return page, nil } // If not in memory, try to find it page, err = d.findPageByID(actualTabID) if err != nil { return nil, err } // If found, cache it for future use if page != nil { d.tabs[actualTabID] = page // Update tab history and current tab d.updateTabHistory(actualTabID) return page, nil } return nil, fmt.Errorf("tab not found: %s", actualTabID) } // closeTab closes a tab by its ID func (d *Daemon) closeTab(tabID string, timeout int) error { // Get the tab ID to use (may be the current tab) actualTabID, err := d.getTabID(tabID) if err != nil { return err } // First remove from our internal map to avoid future references d.mu.Lock() page, exists := d.tabs[actualTabID] delete(d.tabs, actualTabID) // Remove the tab from history for i, id := range d.tabHistory { if id == actualTabID { // Remove this tab from history d.tabHistory = append(d.tabHistory[:i], d.tabHistory[i+1:]...) break } } // If we closed the current tab, set it to the previous tab in history if d.currentTab == actualTabID { if len(d.tabHistory) > 0 { // Set current tab to the most recent tab in history d.currentTab = d.tabHistory[len(d.tabHistory)-1] } else { // No tabs left in history, clear the current tab d.currentTab = "" } } d.mu.Unlock() // If the page doesn't exist in our cache, try to find it if !exists { var err error page, err = d.findPageByID(actualTabID) if err != nil { // If we can't find the page, it might already be closed return nil } } if timeout > 0 { // Use timeout for closing the tab ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan error, 1) // Execute the close in a goroutine go func() { err := page.Close() done <- err }() // Wait for either completion or timeout select { case err := <-done: if err != nil { // Log the error but don't return it, as we've already removed it from our map fmt.Printf("Warning: failed to close tab %s: %v\n", actualTabID, err) } return nil case <-ctx.Done(): return fmt.Errorf("closing tab timed out after %d seconds", timeout) } } else { // No timeout - try to close the page, but don't fail if it's already closed err = page.Close() if err != nil { // Log the error but don't return it, as we've already removed it from our map fmt.Printf("Warning: failed to close tab %s: %v\n", actualTabID, err) } return nil } } // loadURL loads a URL in a tab func (d *Daemon) loadURL(tabID, url string, timeout int) error { d.debugLog("Loading URL: %s in tab: %s with timeout: %d", url, tabID, timeout) page, err := d.getTab(tabID) if err != nil { d.debugLog("Failed to get tab %s: %v", tabID, err) return err } d.debugLog("Got tab %s, starting navigation", tabID) if timeout > 0 { // Use timeout for the URL loading ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan error, 1) // Execute the navigation in a goroutine go func() { err := page.Navigate(url) if err != nil { done <- fmt.Errorf("failed to navigate to URL: %w", err) return } // Wait for the page to be loaded err = page.WaitLoad() if err != nil { done <- fmt.Errorf("failed to wait for page load: %w", err) return } done <- nil }() // Wait for either completion or timeout select { case err := <-done: return err case <-ctx.Done(): return fmt.Errorf("loading URL timed out after %d seconds", timeout) } } else { // No timeout 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 } } // isPageStable checks if a page is stable and not currently loading func (d *Daemon) isPageStable(page *rod.Page) (bool, error) { // Check if page is loading result, err := page.Eval(`() => document.readyState === 'complete'`) if err != nil { return false, err } isComplete := result.Value.Bool() if !isComplete { return false, nil } // Additional check: ensure no pending network requests // This is a simple heuristic - if the page has been stable for a short time err = page.WaitStable(500 * time.Millisecond) if err != nil { return false, nil // Page is not stable } return true, nil } // detectNavigationInProgress monitors the page for a short period to detect if navigation starts func (d *Daemon) detectNavigationInProgress(page *rod.Page, monitorDuration time.Duration) (bool, error) { // Get current URL and readyState currentURL, err := page.Eval(`() => window.location.href`) if err != nil { return false, err } currentReadyState, err := page.Eval(`() => document.readyState`) if err != nil { return false, err } startURL := currentURL.Value.Str() startReadyState := currentReadyState.Value.Str() // Monitor for changes over the specified duration ctx, cancel := context.WithTimeout(context.Background(), monitorDuration) defer cancel() ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): // No navigation detected during monitoring period return false, nil case <-ticker.C: // Check if URL or readyState changed newURL, err := page.Eval(`() => window.location.href`) if err != nil { continue // Ignore errors during monitoring } newReadyState, err := page.Eval(`() => document.readyState`) if err != nil { continue // Ignore errors during monitoring } if newURL.Value.Str() != startURL { // URL changed, navigation is happening return true, nil } if newReadyState.Value.Str() != startReadyState && newReadyState.Value.Str() == "loading" { // Page started loading return true, nil } } } } // waitNavigation waits for a navigation event to happen func (d *Daemon) waitNavigation(tabID string, timeout int) error { page, err := d.getTab(tabID) if err != nil { return err } // First, check if the page is already stable and loaded // If so, we don't need to wait for navigation isStable, err := d.isPageStable(page) if err == nil && isStable { // Page is already stable, no navigation happening return nil } // Check if navigation is actually in progress by monitoring for a short period navigationDetected, err := d.detectNavigationInProgress(page, 2*time.Second) if err != nil { return fmt.Errorf("failed to detect navigation state: %w", err) } if !navigationDetected { // No navigation detected, check if page is stable now isStable, err := d.isPageStable(page) if err == nil && isStable { return nil } // If we can't determine stability, proceed with waiting } // Navigation is in progress, wait for it to complete ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Wait for navigation with timeout done := make(chan error, 1) go func() { defer func() { if r := recover(); r != nil { done <- fmt.Errorf("navigation wait panicked: %v", r) } }() // Wait for navigation event page.WaitNavigation(proto.PageLifecycleEventNameLoad)() // Wait for the page to be fully loaded err := page.WaitLoad() done <- err }() select { case err := <-done: if err != nil { return fmt.Errorf("navigation wait failed: %w", err) } return nil case <-ctx.Done(): // Timeout occurred, check if page is now stable isStable, err := d.isPageStable(page) if err == nil && isStable { // Page is stable, consider navigation complete return nil } return fmt.Errorf("navigation wait timed out after %d seconds", timeout) } } // getPageSource returns the entire source code of a page func (d *Daemon) getPageSource(tabID string, timeout int) (string, error) { page, err := d.getTab(tabID) if err != nil { return "", err } if timeout > 0 { // Use timeout for getting page source ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan struct { html string err error }, 1) // Execute the HTML retrieval in a goroutine go func() { html, err := page.HTML() done <- struct { html string err error }{html, err} }() // Wait for either completion or timeout select { case res := <-done: if res.err != nil { return "", fmt.Errorf("failed to get page HTML: %w", res.err) } return res.html, nil case <-ctx.Done(): return "", fmt.Errorf("getting page source timed out after %d seconds", timeout) } } else { // No timeout 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 (d *Daemon) getElementHTML(tabID, selector string, selectionTimeout int) (string, error) { page, err := d.getTab(tabID) if err != nil { return "", err } // Find the element with optional timeout var element *rod.Element if selectionTimeout > 0 { // Use timeout if specified element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector) if err != nil { return "", fmt.Errorf("failed to find element (timeout after %ds): %w", selectionTimeout, err) } } else { // No timeout 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 } // fillFormField fills a form field with the specified value func (d *Daemon) fillFormField(tabID, selector, value string, selectionTimeout, actionTimeout int) error { page, err := d.getTab(tabID) if err != nil { return err } // Find the element with optional timeout var element *rod.Element if selectionTimeout > 0 { // Use timeout if specified element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector) if err != nil { return fmt.Errorf("failed to find element (timeout after %ds): %w", selectionTimeout, err) } } else { // No timeout element, err = page.Element(selector) if err != nil { return fmt.Errorf("failed to find element: %w", err) } } // Get the element type tagName, err := element.Eval(`() => this.tagName.toLowerCase()`) if err != nil { return fmt.Errorf("failed to get element type: %w", err) } // Get the element type attribute inputType, err := element.Eval(`() => this.type ? this.type.toLowerCase() : ''`) if err != nil { return fmt.Errorf("failed to get element type attribute: %w", err) } // Handle different input types tagNameStr := tagName.Value.String() typeStr := inputType.Value.String() // Handle checkbox and radio inputs if tagNameStr == "input" && (typeStr == "checkbox" || typeStr == "radio") { // Convert value to boolean checked := false if value == "true" || value == "1" || value == "yes" || value == "on" || value == "checked" { checked = true } // Set the checked state with optional timeout if actionTimeout > 0 { // Use timeout for the action ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan error, 1) // Execute the action in a goroutine go func() { _, err := element.Eval(fmt.Sprintf(`() => { this.checked = %t; return true; }`, checked)) done <- err }() // Wait for either completion or timeout select { case err := <-done: if err != nil { return fmt.Errorf("failed to set checkbox state: %w", err) } case <-ctx.Done(): return fmt.Errorf("setting checkbox state timed out after %d seconds", actionTimeout) } // Create a channel for the event trigger done = make(chan error, 1) // Trigger change event with timeout go func() { _, err := element.Eval(`() => { const event = new Event('change', { bubbles: true }); this.dispatchEvent(event); return true; }`) done <- err }() // Wait for either completion or timeout select { case err := <-done: if err != nil { return fmt.Errorf("failed to trigger change event: %w", err) } case <-ctx.Done(): return fmt.Errorf("triggering change event timed out after %d seconds", actionTimeout) } } else { // No timeout _, err := element.Eval(fmt.Sprintf(`() => { this.checked = %t; return true; }`, checked)) if err != nil { return fmt.Errorf("failed to set checkbox state: %w", err) } // Trigger change event _, err = element.Eval(`() => { const event = new Event('change', { bubbles: true }); this.dispatchEvent(event); return true; }`) if err != nil { return fmt.Errorf("failed to trigger change event: %w", err) } } return nil } // For regular text inputs if actionTimeout > 0 { // Use timeout for the action ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan error, 1) // Clear the field first with timeout go func() { _ = element.SelectAllText() err := element.Input("") done <- err }() // Wait for either completion or timeout select { case err := <-done: if err != nil { return fmt.Errorf("failed to clear field: %w", err) } case <-ctx.Done(): return fmt.Errorf("clearing field timed out after %d seconds", actionTimeout) } // Create a channel for the input action done = make(chan error, 1) // Input the value with timeout go func() { err := element.Input(value) done <- err }() // Wait for either completion or timeout select { case err := <-done: if err != nil { return fmt.Errorf("failed to input value: %w", err) } case <-ctx.Done(): return fmt.Errorf("inputting value timed out after %d seconds", actionTimeout) } } else { // No timeout // Clear the field first _ = element.SelectAllText() err = element.Input("") if err != nil { return fmt.Errorf("failed to clear field: %w", err) } // Input the value err = element.Input(value) if err != nil { return fmt.Errorf("failed to input value: %w", err) } } return nil } // uploadFile uploads a file to a file input element func (d *Daemon) uploadFile(tabID, selector, filePath string, selectionTimeout, actionTimeout int) error { page, err := d.getTab(tabID) if err != nil { return err } // Find the element with optional timeout var element *rod.Element if selectionTimeout > 0 { // Use timeout if specified element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector) if err != nil { return fmt.Errorf("failed to find file input element (timeout after %ds): %w", selectionTimeout, err) } } else { // No timeout element, err = page.Element(selector) if err != nil { return fmt.Errorf("failed to find file input element: %w", err) } } // Set the file with optional timeout if actionTimeout > 0 { // Use timeout for the action ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan error, 1) // Execute the action in a goroutine go func() { err := element.SetFiles([]string{filePath}) done <- err }() // Wait for either completion or timeout select { case err := <-done: if err != nil { return fmt.Errorf("failed to set file: %w", err) } case <-ctx.Done(): return fmt.Errorf("setting file timed out after %d seconds", actionTimeout) } } else { // No timeout err = element.SetFiles([]string{filePath}) if err != nil { return fmt.Errorf("failed to set file: %w", err) } } return nil } // submitForm submits a form func (d *Daemon) submitForm(tabID, selector string, selectionTimeout, actionTimeout int) error { page, err := d.getTab(tabID) if err != nil { return err } // Find the element with optional timeout var form *rod.Element if selectionTimeout > 0 { // Use timeout if specified form, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector) if err != nil { return fmt.Errorf("failed to find form element (timeout after %ds): %w", selectionTimeout, err) } } else { // No timeout form, err = page.Element(selector) if err != nil { return fmt.Errorf("failed to find form element: %w", err) } } // Get the current URL to detect navigation currentURL := page.MustInfo().URL // Create a context for navigation timeout var ctx context.Context var cancel context.CancelFunc // Submit the form with optional timeout if actionTimeout > 0 { // Use timeout for the action submitCtx, submitCancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second) defer submitCancel() // Create a channel to signal completion done := make(chan error, 1) // Execute the action in a goroutine go func() { _, err := form.Eval(`() => { try { this.submit(); return true; } catch(e) { return false; } }`) done <- err }() // Wait for either completion or timeout select { case err := <-done: if err != nil { // Log the error but continue fmt.Printf("Warning: error during form submission: %v\n", err) } case <-submitCtx.Done(): return fmt.Errorf("form submission timed out after %d seconds", actionTimeout) } // Wait for navigation to complete (with the same timeout) ctx, cancel = context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second) } else { // No timeout for submission try := func() (bool, error) { _, err := form.Eval(`() => { try { this.submit(); return true; } catch(e) { return false; } }`) return err == nil, err } // Try to submit the form, but don't fail if it's already been submitted _, err = try() if err != nil { // Log the error but continue fmt.Printf("Warning: error during form submission: %v\n", err) } // Wait for navigation to complete (with default timeout) ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) } defer cancel() // Wait for the page to navigate away from the current URL waitNav := func() error { for { select { case <-ctx.Done(): return ctx.Err() case <-time.After(100 * time.Millisecond): // Check if the page has navigated try := func() (string, error) { info, err := page.Info() if err != nil { return "", err } return info.URL, nil } newURL, err := try() if err != nil { // Page might be navigating, wait a bit more continue } if newURL != currentURL { // Navigation completed return nil } } } } // Wait for navigation but don't fail if it times out err = waitNav() if err != nil { // Log the error but don't fail fmt.Printf("Warning: navigation after form submission may not have completed: %v\n", err) } return nil } // clickElement clicks on an element func (d *Daemon) clickElement(tabID, selector string, selectionTimeout, actionTimeout int) error { page, err := d.getTab(tabID) if err != nil { return err } // Find the element with optional timeout var element *rod.Element if selectionTimeout > 0 { // Use timeout if specified element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector) if err != nil { return fmt.Errorf("failed to find element (timeout after %ds): %w", selectionTimeout, err) } } else { // No timeout element, err = page.Element(selector) if err != nil { return fmt.Errorf("failed to find element: %w", err) } } // Make sure the element is visible and scrolled into view err = element.ScrollIntoView() if err != nil { return fmt.Errorf("failed to scroll element into view: %w", err) } // Click the element with optional timeout if actionTimeout > 0 { // Use timeout for the click action ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan error, 1) // Execute the click in a goroutine go func() { err := element.Click(proto.InputMouseButtonLeft, 1) // 1 click done <- err }() // Wait for either completion or timeout select { case err := <-done: if err != nil { return fmt.Errorf("failed to click element: %w", err) } case <-ctx.Done(): return fmt.Errorf("click action timed out after %d seconds", actionTimeout) } } else { // No timeout err = element.Click(proto.InputMouseButtonLeft, 1) // 1 click if err != nil { return fmt.Errorf("failed to click element: %w", err) } } // Wait a moment for any navigation to start time.Sleep(100 * time.Millisecond) // Wait for any potential page load or DOM changes err = page.WaitStable(1 * time.Second) if err != nil { // This is not a critical error, so we'll just log it log.Printf("Warning: page not stable after click: %v", err) } return nil } // evalJS executes JavaScript code in a tab and returns the result func (d *Daemon) evalJS(tabID, jsCode string, timeout int) (string, error) { page, err := d.getTab(tabID) if err != nil { return "", err } // Create a comprehensive wrapper that handles both expressions and statements // and properly formats the result wrappedCode := `() => { var result; try { // Try to evaluate as an expression first result = eval(` + "`" + jsCode + "`" + `); } catch(e) { // If that fails, try to execute as statements try { eval(` + "`" + jsCode + "`" + `); result = undefined; } catch(e2) { throw e; // Re-throw the original error } } // Format the result for return if (typeof result === 'undefined') return 'undefined'; if (result === null) return 'null'; if (typeof result === 'string') return result; if (typeof result === 'number' || typeof result === 'boolean') return String(result); try { return JSON.stringify(result); } catch(e) { return String(result); } }` // Execute the wrapped JavaScript code with timeout if timeout > 0 { // Use timeout for the JavaScript execution ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan struct { result string err error }, 1) // Execute the JavaScript in a goroutine go func() { result, err := page.Eval(wrappedCode) var resultStr string if err == nil { // Convert the result to a string representation if result.Value.Nil() { resultStr = "null" } else { resultStr = result.Value.String() } } done <- struct { result string err error }{resultStr, err} }() // Wait for either completion or timeout select { case res := <-done: if res.err != nil { return "", fmt.Errorf("failed to execute JavaScript: %w", res.err) } return res.result, nil case <-ctx.Done(): return "", fmt.Errorf("JavaScript execution timed out after %d seconds", timeout) } } else { // No timeout result, err := page.Eval(wrappedCode) if err != nil { return "", fmt.Errorf("failed to execute JavaScript: %w", err) } // Convert the result to a string representation if result.Value.Nil() { return "null", nil } return result.Value.String(), nil } } // takeScreenshot takes a screenshot of a tab and saves it to a file func (d *Daemon) takeScreenshot(tabID, outputPath string, fullPage bool, timeout int) error { page, err := d.getTab(tabID) if err != nil { return err } if timeout > 0 { // Use timeout for taking screenshot ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan error, 1) // Execute the screenshot in a goroutine go func() { // Take screenshot and save it screenshotBytes, err := page.Screenshot(fullPage, &proto.PageCaptureScreenshot{ Format: proto.PageCaptureScreenshotFormatPng, }) if err != nil { done <- fmt.Errorf("failed to capture screenshot: %w", err) return } // Write the screenshot to file err = os.WriteFile(outputPath, screenshotBytes, 0644) if err != nil { done <- fmt.Errorf("failed to save screenshot to %s: %w", outputPath, err) return } done <- nil }() // Wait for either completion or timeout select { case err := <-done: if err != nil { return fmt.Errorf("failed to save screenshot: %w", err) } return nil case <-ctx.Done(): return fmt.Errorf("taking screenshot timed out after %d seconds", timeout) } } else { // No timeout - take screenshot directly screenshotBytes, err := page.Screenshot(fullPage, &proto.PageCaptureScreenshot{ Format: proto.PageCaptureScreenshotFormatPng, }) if err != nil { return fmt.Errorf("failed to capture screenshot: %w", err) } // Write the screenshot to file err = os.WriteFile(outputPath, screenshotBytes, 0644) if err != nil { return fmt.Errorf("failed to save screenshot to %s: %w", outputPath, err) } return nil } } // switchToIframe switches the context to an iframe for subsequent commands func (d *Daemon) switchToIframe(tabID, selector string, timeout int) error { d.debugLog("Switching to iframe: selector=%s, tab=%s, timeout=%d", selector, tabID, timeout) // Get the main page first (not iframe context) actualTabID, err := d.getTabID(tabID) if err != nil { d.debugLog("Failed to get tab ID: %v", err) return err } d.mu.Lock() defer d.mu.Unlock() // Get the main page (bypass iframe context) mainPage, exists := d.tabs[actualTabID] if !exists { d.debugLog("Tab %s not in cache, trying to find it", actualTabID) // Try to find it mainPage, err = d.findPageByID(actualTabID) if err != nil { d.debugLog("Failed to find tab %s: %v", actualTabID, err) return err } if mainPage == nil { d.debugLog("Tab %s not found", actualTabID) return fmt.Errorf("tab not found: %s", actualTabID) } d.tabs[actualTabID] = mainPage } d.debugLog("Found main page for tab %s, looking for iframe element", actualTabID) // Find the iframe element with timeout var iframeElement *rod.Element if timeout > 0 { // Use timeout context for finding the iframe element ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan struct { element *rod.Element err error }, 1) // Execute the element search in a goroutine go func() { defer func() { if r := recover(); r != nil { done <- struct { element *rod.Element err error }{nil, fmt.Errorf("iframe element search panicked: %v", r)} } }() element, err := mainPage.Timeout(time.Duration(timeout) * time.Second).Element(selector) done <- struct { element *rod.Element err error }{element, err} }() // Wait for either completion or timeout select { case result := <-done: iframeElement = result.element err = result.err case <-ctx.Done(): d.debugLog("Iframe element search timed out after %d seconds", timeout) return fmt.Errorf("failed to find iframe element (timeout after %ds): %s", timeout, selector) } } else { // No timeout iframeElement, err = mainPage.Element(selector) } if err != nil { d.debugLog("Failed to find iframe element: %v", err) return fmt.Errorf("failed to find iframe element: %w", err) } d.debugLog("Found iframe element, getting frame context") // Get the iframe's page context with timeout var iframePage *rod.Page if timeout > 0 { // Use timeout context for getting the frame ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() // Create a channel to signal completion done := make(chan struct { page *rod.Page err error }, 1) // Execute the frame access in a goroutine go func() { defer func() { if r := recover(); r != nil { done <- struct { page *rod.Page err error }{nil, fmt.Errorf("iframe frame access panicked: %v", r)} } }() page, err := iframeElement.Frame() done <- struct { page *rod.Page err error }{page, err} }() // Wait for either completion or timeout select { case result := <-done: iframePage = result.page err = result.err case <-ctx.Done(): d.debugLog("Iframe frame access timed out after %d seconds", timeout) return fmt.Errorf("failed to get iframe context (timeout after %ds)", timeout) } } else { // No timeout iframePage, err = iframeElement.Frame() } if err != nil { d.debugLog("Failed to get iframe context: %v", err) return fmt.Errorf("failed to get iframe context: %w", err) } // Store the iframe page context d.iframePages[actualTabID] = iframePage d.debugLog("Successfully switched to iframe context for tab %s", actualTabID) return nil } // switchToMain switches back to the main page context func (d *Daemon) switchToMain(tabID string) error { d.debugLog("Switching back to main context: tab=%s", tabID) // Get the tab ID to use (may be the current tab) actualTabID, err := d.getTabID(tabID) if err != nil { d.debugLog("Failed to get tab ID: %v", err) return err } d.mu.Lock() defer d.mu.Unlock() // Check if there was an iframe context to remove if _, exists := d.iframePages[actualTabID]; exists { d.debugLog("Removing iframe context for tab %s", actualTabID) // Remove the iframe context for this tab delete(d.iframePages, actualTabID) } else { d.debugLog("No iframe context found for tab %s", actualTabID) } d.debugLog("Successfully switched back to main context for tab %s", actualTabID) return nil } // handleFileUpload handles file upload requests from clients func (d *Daemon) handleFileUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse multipart form (32MB max memory) err := r.ParseMultipartForm(32 << 20) if err != nil { http.Error(w, "Failed to parse multipart form", http.StatusBadRequest) return } // Get the uploaded file file, header, err := r.FormFile("file") if err != nil { http.Error(w, "Failed to get uploaded file", http.StatusBadRequest) return } defer file.Close() // Get the target path (optional, defaults to /tmp/) targetPath := r.FormValue("path") if targetPath == "" { targetPath = "/tmp/" + header.Filename } // Create the target file targetFile, err := os.Create(targetPath) if err != nil { http.Error(w, fmt.Sprintf("Failed to create target file: %v", err), http.StatusInternalServerError) return } defer targetFile.Close() // Copy the uploaded file to the target location _, err = io.Copy(targetFile, file) if err != nil { http.Error(w, fmt.Sprintf("Failed to save file: %v", err), http.StatusInternalServerError) return } // Return success response response := Response{ Success: true, Data: map[string]interface{}{ "filename": header.Filename, "size": header.Size, "target_path": targetPath, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // handleFileDownload handles file download requests from clients func (d *Daemon) handleFileDownload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Get the file path from query parameter filePath := r.URL.Query().Get("path") if filePath == "" { http.Error(w, "File path is required", http.StatusBadRequest) return } // Check if file exists and get info fileInfo, err := os.Stat(filePath) if err != nil { if os.IsNotExist(err) { http.Error(w, "File not found", http.StatusNotFound) } else { http.Error(w, fmt.Sprintf("Failed to access file: %v", err), http.StatusInternalServerError) } return } // Open the file file, err := os.Open(filePath) if err != nil { http.Error(w, fmt.Sprintf("Failed to open file: %v", err), http.StatusInternalServerError) return } defer file.Close() // Set headers for file download w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileInfo.Name())) w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)) // Stream the file to the client _, err = io.Copy(w, file) if err != nil { log.Printf("Error streaming file to client: %v", err) } } // setupConsoleLogging sets up console log capture for a tab func (d *Daemon) setupConsoleLogging(tabID string, page *rod.Page) { // Initialize console logs for this tab d.consoleLogs[tabID] = make([]ConsoleLog, 0) // Listen for console events go page.EachEvent(func(e *proto.RuntimeConsoleAPICalled) { d.mu.Lock() defer d.mu.Unlock() // Convert console level level := string(e.Type) // Build message from arguments var message string for i, arg := range e.Args { if i > 0 { message += " " } // Handle different argument types if !arg.Value.Nil() { message += arg.Value.String() } else if arg.Description != "" { message += arg.Description } else { message += "[object]" } } // Create console log entry logEntry := ConsoleLog{ Level: level, Message: message, Timestamp: time.Now(), Source: "", // Could be enhanced with stack trace info } // Add to console logs (keep last 1000 entries per tab) logs := d.consoleLogs[tabID] logs = append(logs, logEntry) if len(logs) > 1000 { logs = logs[1:] // Remove oldest entry } d.consoleLogs[tabID] = logs })() } // getConsoleLogs retrieves console logs for a tab func (d *Daemon) getConsoleLogs(tabID string, clear bool) ([]ConsoleLog, error) { d.mu.Lock() defer d.mu.Unlock() // Use current tab if not specified if tabID == "" { tabID = d.currentTab } if tabID == "" { return nil, fmt.Errorf("no tab specified and no current tab available") } // Check if tab exists if _, exists := d.tabs[tabID]; !exists { return nil, fmt.Errorf("tab not found: %s", tabID) } // Get logs for this tab logs, exists := d.consoleLogs[tabID] if !exists { logs = make([]ConsoleLog, 0) } // Clear logs if requested if clear { d.consoleLogs[tabID] = make([]ConsoleLog, 0) } return logs, nil } // executeConsoleCommand executes a command in the browser console func (d *Daemon) executeConsoleCommand(tabID, command string, timeout int) (string, error) { // Use current tab if not specified if tabID == "" { tabID = d.currentTab } if tabID == "" { return "", fmt.Errorf("no tab specified and no current tab available") } // Execute the command as JavaScript and return the result // This is similar to evalJS but specifically for console commands return d.evalJS(tabID, command, timeout) }