package daemon import ( "context" "encoding/json" "fmt" "io" "log" "net" "net/http" "os" "path/filepath" "regexp" "strconv" "strings" "sync" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" ) const Version = "2.0.0" // 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 "version": response = Response{ Success: true, Data: Version, } 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} } case "check-element": d.debugLog("Processing check-element command") tabID := cmd.Params["tab"] selector := cmd.Params["selector"] checkType := cmd.Params["type"] // exists, visible, enabled, focused, selected 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.checkElement(tabID, selector, checkType, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "get-element-attributes": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] attributes := cmd.Params["attributes"] // comma-separated list or "all" 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.getElementAttributes(tabID, selector, attributes, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "count-elements": 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 } } count, err := d.countElements(tabID, selector, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: count} } case "extract-multiple": tabID := cmd.Params["tab"] selectors := cmd.Params["selectors"] // JSON array of selectors 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.extractMultiple(tabID, selectors, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "extract-links": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] // Optional: filter links by container selector hrefPattern := cmd.Params["href-pattern"] // Optional: regex pattern for href textPattern := cmd.Params["text-pattern"] // Optional: regex pattern for link text 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.extractLinks(tabID, selector, hrefPattern, textPattern, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "extract-table": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] includeHeaders := cmd.Params["include-headers"] == "true" 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.extractTable(tabID, selector, includeHeaders, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "extract-text": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] pattern := cmd.Params["pattern"] // Optional: regex pattern to match within text extractType := cmd.Params["type"] // text, innerText, textContent (default: textContent) 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.extractText(tabID, selector, pattern, extractType, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "analyze-form": 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 } } result, err := d.analyzeForm(tabID, selector, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "interact-multiple": tabID := cmd.Params["tab"] interactionsJSON := cmd.Params["interactions"] 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.interactMultiple(tabID, interactionsJSON, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "fill-form-bulk": tabID := cmd.Params["tab"] formSelector := cmd.Params["form-selector"] fieldsJSON := cmd.Params["fields"] 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.fillFormBulk(tabID, formSelector, fieldsJSON, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "get-page-info": 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 } } result, err := d.getPageInfo(tabID, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "get-viewport-info": 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 } } result, err := d.getViewportInfo(tabID, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "get-performance": 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 } } result, err := d.getPerformance(tabID, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "check-content": tabID := cmd.Params["tab"] contentType := cmd.Params["type"] 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.checkContent(tabID, contentType, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "screenshot-element": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] outputPath := cmd.Params["output"] 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.screenshotElement(tabID, selector, outputPath, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true} } case "screenshot-enhanced": 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 } } result, err := d.screenshotEnhanced(tabID, outputPath, fullPage, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "bulk-files": operationType := cmd.Params["operation"] // "upload" or "download" filesJSON := cmd.Params["files"] timeoutStr := cmd.Params["timeout"] // Parse timeout (default to 30 seconds for bulk operations) timeout := 30 if timeoutStr != "" { if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { timeout = parsedTimeout } } result, err := d.bulkFiles(operationType, filesJSON, timeout) if err != nil { response = Response{Success: false, Error: err.Error()} } else { response = Response{Success: true, Data: result} } case "manage-files": operation := cmd.Params["operation"] // "cleanup", "list", "info" pattern := cmd.Params["pattern"] // file pattern for cleanup/list maxAge := cmd.Params["max-age"] // max age in hours for cleanup result, err := d.manageFiles(operation, pattern, maxAge) 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) } // ElementCheckResult represents the result of an element check type ElementCheckResult struct { Exists bool `json:"exists"` Visible bool `json:"visible,omitempty"` Enabled bool `json:"enabled,omitempty"` Focused bool `json:"focused,omitempty"` Selected bool `json:"selected,omitempty"` Count int `json:"count,omitempty"` } // MultipleExtractionResult represents the result of extracting from multiple selectors type MultipleExtractionResult struct { Results map[string]interface{} `json:"results"` Errors map[string]string `json:"errors,omitempty"` } // LinkInfo represents information about a link type LinkInfo struct { Href string `json:"href"` Text string `json:"text"` Title string `json:"title,omitempty"` Target string `json:"target,omitempty"` } // LinksExtractionResult represents the result of extracting links type LinksExtractionResult struct { Links []LinkInfo `json:"links"` Count int `json:"count"` } // TableExtractionResult represents the result of extracting table data type TableExtractionResult struct { Headers []string `json:"headers,omitempty"` Rows [][]string `json:"rows"` Data []map[string]string `json:"data,omitempty"` // Only if headers are included Count int `json:"count"` } // TextExtractionResult represents the result of extracting text type TextExtractionResult struct { Text string `json:"text"` Matches []string `json:"matches,omitempty"` // If pattern was used Count int `json:"count"` // Number of elements matched } // checkElement checks various states of an element func (d *Daemon) checkElement(tabID, selector, checkType string, timeout int) (*ElementCheckResult, error) { // 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") } page, exists := d.tabs[tabID] if !exists { return nil, fmt.Errorf("tab %s not found", tabID) } // Check if we're in iframe mode for this tab if iframePage, inIframe := d.iframePages[tabID]; inIframe { page = iframePage } result := &ElementCheckResult{} // First check if element exists elements, err := page.Elements(selector) if err != nil { // If we can't find elements, it means they don't exist result.Exists = false return result, nil } result.Exists = len(elements) > 0 result.Count = len(elements) // If no elements exist, return early if !result.Exists { return result, nil } // For additional checks, use the first element element := elements[0] switch checkType { case "exists": // Already handled above case "visible": visible, err := element.Visible() if err != nil { return nil, fmt.Errorf("failed to check visibility: %w", err) } result.Visible = visible case "enabled": // Check if element is enabled (not disabled) disabled, err := element.Attribute("disabled") if err != nil { return nil, fmt.Errorf("failed to check enabled state: %w", err) } result.Enabled = disabled == nil case "focused": // Check if element is focused jsCode := fmt.Sprintf("document.activeElement === document.querySelector('%s')", selector) focusResult, err := page.Eval(jsCode) if err != nil { return nil, fmt.Errorf("failed to check focus state: %w", err) } result.Focused = focusResult.Value.Bool() case "selected": // Check if element is selected (for checkboxes, radio buttons, options) selected, err := element.Attribute("selected") if err == nil && selected != nil { result.Selected = true } else { // Also check 'checked' attribute for checkboxes and radio buttons checked, err := element.Attribute("checked") if err == nil && checked != nil { result.Selected = true } else { result.Selected = false } } case "all": // Check all states visible, _ := element.Visible() result.Visible = visible disabled, _ := element.Attribute("disabled") result.Enabled = disabled == nil jsCode := fmt.Sprintf("document.activeElement === document.querySelector('%s')", selector) focusResult, _ := page.Eval(jsCode) if focusResult != nil { result.Focused = focusResult.Value.Bool() } selected, _ := element.Attribute("selected") if selected != nil { result.Selected = true } else { checked, _ := element.Attribute("checked") result.Selected = checked != nil } default: return nil, fmt.Errorf("unknown check type: %s", checkType) } return result, nil } // getElementAttributes gets attributes, properties, and computed styles of an element func (d *Daemon) getElementAttributes(tabID, selector, attributes string, timeout int) (map[string]interface{}, error) { // 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") } page, exists := d.tabs[tabID] if !exists { return nil, fmt.Errorf("tab %s not found", tabID) } // Check if we're in iframe mode for this tab if iframePage, inIframe := d.iframePages[tabID]; inIframe { page = iframePage } // Find the element with timeout var element *rod.Element if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() var err error element, err = page.Context(ctx).Element(selector) if err != nil { return nil, fmt.Errorf("element not found: %w", err) } } else { var err error element, err = page.Element(selector) if err != nil { return nil, fmt.Errorf("element not found: %w", err) } } result := make(map[string]interface{}) if attributes == "all" { // Get all common attributes and properties commonAttrs := []string{ "id", "class", "name", "type", "value", "href", "src", "alt", "title", "disabled", "checked", "selected", "readonly", "required", "placeholder", "data-*", "aria-*", } // Get HTML attributes for _, attr := range commonAttrs { if attr == "data-*" || attr == "aria-*" { // Skip wildcard attributes for now continue } value, err := element.Attribute(attr) if err == nil && value != nil { result[attr] = *value } } // Get common properties via JavaScript jsCode := ` (function(el) { return { tagName: el.tagName, textContent: el.textContent, innerHTML: el.innerHTML, outerHTML: el.outerHTML, offsetWidth: el.offsetWidth, offsetHeight: el.offsetHeight, scrollWidth: el.scrollWidth, scrollHeight: el.scrollHeight, clientWidth: el.clientWidth, clientHeight: el.clientHeight }; })(arguments[0]) ` jsResult, err := element.Eval(jsCode) if err == nil { if props := jsResult.Value.Map(); props != nil { for key, value := range props { result[key] = value } } } // Get computed styles for common properties styleProps := []string{ "display", "visibility", "opacity", "position", "top", "left", "width", "height", "margin", "padding", "border", "background-color", "color", "font-size", "font-family", } for _, prop := range styleProps { jsCode := fmt.Sprintf("getComputedStyle(arguments[0]).%s", prop) styleResult, err := element.Eval(jsCode) if err == nil { result["style_"+prop] = styleResult.Value.Str() } } } else { // Get specific attributes (comma-separated) attrList := []string{} if attributes != "" { // Split by comma and trim spaces for _, attr := range strings.Split(attributes, ",") { attrList = append(attrList, strings.TrimSpace(attr)) } } for _, attr := range attrList { if strings.HasPrefix(attr, "style_") { // Get computed style styleProp := strings.TrimPrefix(attr, "style_") jsCode := fmt.Sprintf("getComputedStyle(arguments[0]).%s", styleProp) styleResult, err := element.Eval(jsCode) if err == nil { result[attr] = styleResult.Value.Str() } } else if strings.HasPrefix(attr, "prop_") { // Get JavaScript property propName := strings.TrimPrefix(attr, "prop_") jsCode := fmt.Sprintf("arguments[0].%s", propName) propResult, err := element.Eval(jsCode) if err == nil { result[attr] = propResult.Value.Raw } } else { // Get HTML attribute value, err := element.Attribute(attr) if err == nil && value != nil { result[attr] = *value } } } } return result, nil } // countElements counts the number of elements matching a selector func (d *Daemon) countElements(tabID, selector string, timeout int) (int, error) { // Use current tab if not specified if tabID == "" { tabID = d.currentTab } if tabID == "" { return 0, fmt.Errorf("no tab specified and no current tab available") } page, exists := d.tabs[tabID] if !exists { return 0, fmt.Errorf("tab %s not found", tabID) } // Check if we're in iframe mode for this tab if iframePage, inIframe := d.iframePages[tabID]; inIframe { page = iframePage } // Find elements with timeout var elements rod.Elements var err error if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() elements, err = page.Context(ctx).Elements(selector) } else { elements, err = page.Elements(selector) } if err != nil { // If we can't find elements, return 0 (not an error) return 0, nil } return len(elements), nil } // extractMultiple extracts data from multiple selectors in a single call func (d *Daemon) extractMultiple(tabID, selectorsJSON string, timeout int) (*MultipleExtractionResult, error) { // 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") } page, exists := d.tabs[tabID] if !exists { return nil, fmt.Errorf("tab %s not found", tabID) } // Check if we're in iframe mode for this tab if iframePage, inIframe := d.iframePages[tabID]; inIframe { page = iframePage } // Parse selectors JSON var selectors map[string]string if err := json.Unmarshal([]byte(selectorsJSON), &selectors); err != nil { return nil, fmt.Errorf("invalid selectors JSON: %w", err) } result := &MultipleExtractionResult{ Results: make(map[string]interface{}), Errors: make(map[string]string), } // Extract from each selector for key, selector := range selectors { var elements rod.Elements var err error if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() elements, err = page.Context(ctx).Elements(selector) } else { elements, err = page.Elements(selector) } if err != nil { result.Errors[key] = err.Error() continue } if len(elements) == 0 { result.Results[key] = nil continue } // Extract content from all matching elements var values []string for _, element := range elements { var value string var err error // Check if it's a form input element and get its value tagName, _ := element.Eval("() => this.tagName.toLowerCase()") if tagName.Value.Str() == "input" || tagName.Value.Str() == "textarea" || tagName.Value.Str() == "select" { // For form elements, get the value property valueProp, err := element.Property("value") if err == nil && valueProp.Str() != "" { value = valueProp.Str() } else { // Fallback to text content value, err = element.Text() } } else { // For non-form elements, get text content value, err = element.Text() } if err != nil { result.Errors[key] = fmt.Sprintf("failed to get content: %v", err) break } values = append(values, value) } if len(values) == 1 { result.Results[key] = values[0] } else { result.Results[key] = values } } return result, nil } // extractLinks extracts all links from the page with optional filtering func (d *Daemon) extractLinks(tabID, containerSelector, hrefPattern, textPattern string, timeout int) (*LinksExtractionResult, error) { // 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") } page, exists := d.tabs[tabID] if !exists { return nil, fmt.Errorf("tab %s not found", tabID) } // Check if we're in iframe mode for this tab if iframePage, inIframe := d.iframePages[tabID]; inIframe { page = iframePage } // Build selector for links linkSelector := "a[href]" if containerSelector != "" { linkSelector = containerSelector + " " + linkSelector } // Find all links var elements rod.Elements var err error if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() elements, err = page.Context(ctx).Elements(linkSelector) } else { elements, err = page.Elements(linkSelector) } if err != nil { return nil, fmt.Errorf("failed to find links: %w", err) } result := &LinksExtractionResult{ Links: make([]LinkInfo, 0), Count: 0, } // Compile regex patterns if provided var hrefRegex, textRegex *regexp.Regexp if hrefPattern != "" { hrefRegex, err = regexp.Compile(hrefPattern) if err != nil { return nil, fmt.Errorf("invalid href pattern: %w", err) } } if textPattern != "" { textRegex, err = regexp.Compile(textPattern) if err != nil { return nil, fmt.Errorf("invalid text pattern: %w", err) } } // Extract link information for _, element := range elements { href, err := element.Attribute("href") if err != nil || href == nil { continue } text, err := element.Text() if err != nil { text = "" } // Apply filters if hrefRegex != nil && !hrefRegex.MatchString(*href) { continue } if textRegex != nil && !textRegex.MatchString(text) { continue } // Get additional attributes title, _ := element.Attribute("title") target, _ := element.Attribute("target") linkInfo := LinkInfo{ Href: *href, Text: text, } if title != nil { linkInfo.Title = *title } if target != nil { linkInfo.Target = *target } result.Links = append(result.Links, linkInfo) } result.Count = len(result.Links) return result, nil } // extractTable extracts table data as structured JSON func (d *Daemon) extractTable(tabID, selector string, includeHeaders bool, timeout int) (*TableExtractionResult, error) { // 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") } page, exists := d.tabs[tabID] if !exists { return nil, fmt.Errorf("tab %s not found", tabID) } // Check if we're in iframe mode for this tab if iframePage, inIframe := d.iframePages[tabID]; inIframe { page = iframePage } // Find the table var table *rod.Element var err error if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() table, err = page.Context(ctx).Element(selector) } else { table, err = page.Element(selector) } if err != nil { return nil, fmt.Errorf("failed to find table: %w", err) } result := &TableExtractionResult{ Rows: make([][]string, 0), Count: 0, } // Extract headers if requested if includeHeaders { headerRows, err := table.Elements("thead tr, tr:first-child") if err == nil && len(headerRows) > 0 { headerCells, err := headerRows[0].Elements("th, td") if err == nil { headers := make([]string, 0) for _, cell := range headerCells { text, err := cell.Text() if err != nil { text = "" } headers = append(headers, strings.TrimSpace(text)) } result.Headers = headers } } } // Extract all rows rows, err := table.Elements("tbody tr, tr") if err != nil { return nil, fmt.Errorf("failed to find table rows: %w", err) } // Skip header row if we extracted headers startIndex := 0 if includeHeaders && len(result.Headers) > 0 { startIndex = 1 } for i := startIndex; i < len(rows); i++ { cells, err := rows[i].Elements("td, th") if err != nil { continue } rowData := make([]string, 0) for _, cell := range cells { text, err := cell.Text() if err != nil { text = "" } rowData = append(rowData, strings.TrimSpace(text)) } if len(rowData) > 0 { result.Rows = append(result.Rows, rowData) } } // Create structured data if headers are available if includeHeaders && len(result.Headers) > 0 { result.Data = make([]map[string]string, 0) for _, row := range result.Rows { rowMap := make(map[string]string) for i, header := range result.Headers { if i < len(row) { rowMap[header] = row[i] } else { rowMap[header] = "" } } result.Data = append(result.Data, rowMap) } } result.Count = len(result.Rows) return result, nil } // extractText extracts text content with optional pattern matching func (d *Daemon) extractText(tabID, selector, pattern, extractType string, timeout int) (*TextExtractionResult, error) { // 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") } page, exists := d.tabs[tabID] if !exists { return nil, fmt.Errorf("tab %s not found", tabID) } // Check if we're in iframe mode for this tab if iframePage, inIframe := d.iframePages[tabID]; inIframe { page = iframePage } // Default extract type if extractType == "" { extractType = "textContent" } // Find elements var elements rod.Elements var err error if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() elements, err = page.Context(ctx).Elements(selector) } else { elements, err = page.Elements(selector) } if err != nil { return nil, fmt.Errorf("failed to find elements: %w", err) } result := &TextExtractionResult{ Count: len(elements), } // Compile regex pattern if provided var textRegex *regexp.Regexp if pattern != "" { textRegex, err = regexp.Compile(pattern) if err != nil { return nil, fmt.Errorf("invalid text pattern: %w", err) } } // Extract text from all elements var allTexts []string for _, element := range elements { var text string switch extractType { case "text": text, err = element.Text() case "innerText": // Use JavaScript to get innerText jsResult, jsErr := element.Eval("() => this.innerText") if jsErr == nil && jsResult.Value.Str() != "" { text = jsResult.Value.Str() } else { text, err = element.Text() // Fallback } case "textContent": // Use JavaScript to get textContent jsResult, jsErr := element.Eval("() => this.textContent") if jsErr == nil && jsResult.Value.Str() != "" { text = jsResult.Value.Str() } else { text, err = element.Text() // Fallback } default: text, err = element.Text() } if err != nil { continue } allTexts = append(allTexts, text) } // Join all texts result.Text = strings.Join(allTexts, "\n") // Apply pattern matching if provided if textRegex != nil { matches := textRegex.FindAllString(result.Text, -1) result.Matches = matches } return result, nil } // FormField represents a form field with its properties type FormField struct { Name string `json:"name"` Type string `json:"type"` Value string `json:"value"` Placeholder string `json:"placeholder,omitempty"` Required bool `json:"required"` Disabled bool `json:"disabled"` ReadOnly bool `json:"readonly"` Selector string `json:"selector"` Label string `json:"label,omitempty"` Options []FormFieldOption `json:"options,omitempty"` // For select/radio/checkbox } // FormFieldOption represents an option in a select, radio, or checkbox group type FormFieldOption struct { Value string `json:"value"` Text string `json:"text"` Selected bool `json:"selected"` } // FormAnalysisResult represents the result of analyzing a form type FormAnalysisResult struct { Action string `json:"action,omitempty"` Method string `json:"method,omitempty"` Fields []FormField `json:"fields"` FieldCount int `json:"field_count"` CanSubmit bool `json:"can_submit"` SubmitText string `json:"submit_text,omitempty"` } // InteractionItem represents a single interaction to perform type InteractionItem struct { Selector string `json:"selector"` Action string `json:"action"` // click, fill, select, check, uncheck Value string `json:"value,omitempty"` } // InteractionResult represents the result of a single interaction type InteractionResult struct { Selector string `json:"selector"` Action string `json:"action"` Success bool `json:"success"` Error string `json:"error,omitempty"` } // MultipleInteractionResult represents the result of multiple interactions type MultipleInteractionResult struct { Results []InteractionResult `json:"results"` SuccessCount int `json:"success_count"` ErrorCount int `json:"error_count"` TotalCount int `json:"total_count"` } // FormBulkFillResult represents the result of bulk form filling type FormBulkFillResult struct { FilledFields []InteractionResult `json:"filled_fields"` SuccessCount int `json:"success_count"` ErrorCount int `json:"error_count"` TotalCount int `json:"total_count"` } // analyzeForm analyzes a form and returns detailed information about its fields func (d *Daemon) analyzeForm(tabID, selector string, timeout int) (*FormAnalysisResult, error) { page, err := d.getTab(tabID) if err != nil { return nil, err } // Find the form element var form *rod.Element if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() form, err = page.Context(ctx).Element(selector) } else { form, err = page.Element(selector) } if err != nil { return nil, fmt.Errorf("failed to find form: %w", err) } result := &FormAnalysisResult{ Fields: make([]FormField, 0), } // Get form action and method if action, err := form.Attribute("action"); err == nil && action != nil { result.Action = *action } if method, err := form.Attribute("method"); err == nil && method != nil { result.Method = *method } else { result.Method = "GET" // Default } // Find all form fields fieldSelectors := []string{ "input", "textarea", "select", "button[type='submit']", "input[type='submit']", } for _, fieldSelector := range fieldSelectors { elements, err := form.Elements(fieldSelector) if err != nil { continue } for _, element := range elements { field := FormField{} // Get basic attributes if name, err := element.Attribute("name"); err == nil && name != nil { field.Name = *name } if id, err := element.Attribute("id"); err == nil && id != nil && field.Name == "" { field.Name = *id } if fieldType, err := element.Attribute("type"); err == nil && fieldType != nil { field.Type = *fieldType } else { // Get tag name if no type if tagName, err := element.Eval("() => this.tagName.toLowerCase()"); err == nil { field.Type = tagName.Value.Str() } } // Skip submit buttons for field analysis but note them for submission info if field.Type == "submit" { result.CanSubmit = true if value, err := element.Attribute("value"); err == nil && value != nil { result.SubmitText = *value } else if text, err := element.Text(); err == nil { result.SubmitText = text } continue } // Get current value if value, err := element.Attribute("value"); err == nil && value != nil { field.Value = *value } // Get placeholder if placeholder, err := element.Attribute("placeholder"); err == nil && placeholder != nil { field.Placeholder = *placeholder } // Get boolean attributes if required, err := element.Attribute("required"); err == nil && required != nil { field.Required = true } if disabled, err := element.Attribute("disabled"); err == nil && disabled != nil { field.Disabled = true } if readonly, err := element.Attribute("readonly"); err == nil && readonly != nil { field.ReadOnly = true } // Generate selector for this field if field.Name != "" { field.Selector = fmt.Sprintf("[name='%s']", field.Name) } else if id, err := element.Attribute("id"); err == nil && id != nil { field.Selector = fmt.Sprintf("#%s", *id) } // Try to find associated label if field.Name != "" { if label, err := form.Element(fmt.Sprintf("label[for='%s']", field.Name)); err == nil { if labelText, err := label.Text(); err == nil { field.Label = labelText } } } // Handle select options if field.Type == "select" { options, err := element.Elements("option") if err == nil { field.Options = make([]FormFieldOption, 0) for _, option := range options { opt := FormFieldOption{} if value, err := option.Attribute("value"); err == nil && value != nil { opt.Value = *value } if text, err := option.Text(); err == nil { opt.Text = text } if selected, err := option.Attribute("selected"); err == nil && selected != nil { opt.Selected = true } field.Options = append(field.Options, opt) } } } result.Fields = append(result.Fields, field) } } result.FieldCount = len(result.Fields) // Check if form can be submitted (has submit button or can be submitted via JS) if !result.CanSubmit { // Look for any button that might submit if buttons, err := form.Elements("button"); err == nil { for _, button := range buttons { if buttonType, err := button.Attribute("type"); err == nil && buttonType != nil { if *buttonType == "submit" || *buttonType == "" { result.CanSubmit = true if text, err := button.Text(); err == nil { result.SubmitText = text } break } } } } } return result, nil } // interactMultiple performs multiple interactions in sequence func (d *Daemon) interactMultiple(tabID, interactionsJSON string, timeout int) (*MultipleInteractionResult, error) { page, err := d.getTab(tabID) if err != nil { return nil, err } // Parse interactions JSON var interactions []InteractionItem err = json.Unmarshal([]byte(interactionsJSON), &interactions) if err != nil { return nil, fmt.Errorf("failed to parse interactions JSON: %w", err) } result := &MultipleInteractionResult{ Results: make([]InteractionResult, 0), TotalCount: len(interactions), } // Perform each interaction for _, interaction := range interactions { interactionResult := InteractionResult{ Selector: interaction.Selector, Action: interaction.Action, Success: false, } // Find the element without timeout to avoid context cancellation issues var element *rod.Element element, err = page.Element(interaction.Selector) if err != nil { interactionResult.Error = fmt.Sprintf("failed to find element: %v", err) result.Results = append(result.Results, interactionResult) result.ErrorCount++ continue } // Perform the action switch interaction.Action { case "click": err = element.Click(proto.InputMouseButtonLeft, 1) // Retry once if context was canceled if err != nil && strings.Contains(err.Error(), "context canceled") { // Try to find element again and click element, err = page.Element(interaction.Selector) if err == nil { err = element.Click(proto.InputMouseButtonLeft, 1) } } if err != nil { interactionResult.Error = fmt.Sprintf("failed to click: %v", err) } else { interactionResult.Success = true } case "fill": // Clear field first err = element.SelectAllText() if err == nil { err = element.Input("") } if err == nil { err = element.Input(interaction.Value) } // Retry once if context was canceled if err != nil && strings.Contains(err.Error(), "context canceled") { // Try to find element again and fill element, err = page.Element(interaction.Selector) if err == nil { err = element.SelectAllText() if err == nil { err = element.Input("") } if err == nil { err = element.Input(interaction.Value) } } } if err != nil { interactionResult.Error = fmt.Sprintf("failed to fill: %v", err) } else { interactionResult.Success = true } case "select": // For select elements, set the value using JavaScript script := fmt.Sprintf("(this.value = '%s', this.dispatchEvent(new Event('change', { bubbles: true })), true)", interaction.Value) result, err := element.Eval(script) if err != nil { interactionResult.Error = fmt.Sprintf("failed to select option: %v", err) } else if result.Value.Bool() { interactionResult.Success = true } else { interactionResult.Error = fmt.Sprintf("failed to select option: %s", interaction.Value) } case "check": // Check if it's already checked checked, err := element.Property("checked") if err == nil && checked.Bool() { interactionResult.Success = true // Already checked } else { err = element.Click(proto.InputMouseButtonLeft, 1) // Retry once if context was canceled if err != nil && strings.Contains(err.Error(), "context canceled") { // Try to find element again and click element, err = page.Element(interaction.Selector) if err == nil { err = element.Click(proto.InputMouseButtonLeft, 1) } } if err != nil { interactionResult.Error = fmt.Sprintf("failed to check: %v", err) } else { interactionResult.Success = true } } case "uncheck": // Check if it's already unchecked checked, err := element.Property("checked") if err == nil && !checked.Bool() { interactionResult.Success = true // Already unchecked } else { err = element.Click(proto.InputMouseButtonLeft, 1) if err != nil { interactionResult.Error = fmt.Sprintf("failed to uncheck: %v", err) } else { interactionResult.Success = true } } default: interactionResult.Error = fmt.Sprintf("unknown action: %s", interaction.Action) } result.Results = append(result.Results, interactionResult) if interactionResult.Success { result.SuccessCount++ } else { result.ErrorCount++ } } return result, nil } // fillFormBulk fills multiple form fields in a single operation func (d *Daemon) fillFormBulk(tabID, formSelector, fieldsJSON string, timeout int) (*FormBulkFillResult, error) { page, err := d.getTab(tabID) if err != nil { return nil, err } // Parse fields JSON var fields map[string]string err = json.Unmarshal([]byte(fieldsJSON), &fields) if err != nil { return nil, fmt.Errorf("failed to parse fields JSON: %w", err) } result := &FormBulkFillResult{ FilledFields: make([]InteractionResult, 0), TotalCount: len(fields), } // Find the form element if selector is provided var form *rod.Element if formSelector != "" { if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) form, err = page.Context(ctx).Element(formSelector) cancel() } else { form, err = page.Element(formSelector) } if err != nil { return nil, fmt.Errorf("failed to find form: %w", err) } } // Fill each field for fieldName, fieldValue := range fields { fieldResult := InteractionResult{ Selector: fieldName, Action: "fill", Success: false, } // Try different selector strategies for the field var element *rod.Element var selectors []string // If we have a form, search within it first if form != nil { selectors = []string{ fmt.Sprintf("[name='%s']", fieldName), fmt.Sprintf("#%s", fieldName), fmt.Sprintf("[id='%s']", fieldName), fieldName, // In case it's already a full selector } for _, selector := range selectors { if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) element, err = form.Context(ctx).Element(selector) // Don't cancel yet if element found - we need context for filling if err == nil { fieldResult.Selector = selector // Fill the field while context is still valid err = element.SelectAllText() if err == nil { err = element.Input("") } if err == nil { err = element.Input(fieldValue) } cancel() // Now we can cancel break } cancel() // Cancel if element not found } else { element, err = form.Element(selector) if err == nil { fieldResult.Selector = selector // Fill the field err = element.SelectAllText() if err == nil { err = element.Input("") } if err == nil { err = element.Input(fieldValue) } break } } } } // If not found in form or no form, search in entire page if element == nil { // Generate selectors if not already done if selectors == nil { selectors = []string{ fmt.Sprintf("[name='%s']", fieldName), fmt.Sprintf("#%s", fieldName), fmt.Sprintf("[id='%s']", fieldName), fieldName, // In case it's already a full selector } } for _, selector := range selectors { if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) element, err = page.Context(ctx).Element(selector) // Don't cancel yet - we need the context for filling if err == nil { fieldResult.Selector = selector // Fill the field while context is still valid err = element.SelectAllText() if err == nil { err = element.Input("") } if err == nil { err = element.Input(fieldValue) } cancel() // Now we can cancel break } cancel() // Cancel if element not found } else { element, err = page.Element(selector) if err == nil { fieldResult.Selector = selector // Fill the field err = element.SelectAllText() if err == nil { err = element.Input("") } if err == nil { err = element.Input(fieldValue) } break } } } } if element == nil { fieldResult.Error = fmt.Sprintf("failed to find field: %s", fieldName) result.FilledFields = append(result.FilledFields, fieldResult) result.ErrorCount++ continue } if err != nil { fieldResult.Error = fmt.Sprintf("failed to fill field: %v", err) result.ErrorCount++ } else { fieldResult.Success = true result.SuccessCount++ } result.FilledFields = append(result.FilledFields, fieldResult) } return result, nil } // PageInfo represents page metadata and state information type PageInfo struct { Title string `json:"title"` URL string `json:"url"` LoadingState string `json:"loading_state"` ReadyState string `json:"ready_state"` Referrer string `json:"referrer"` Domain string `json:"domain"` Protocol string `json:"protocol"` Charset string `json:"charset"` ContentType string `json:"content_type"` LastModified string `json:"last_modified"` CookieEnabled bool `json:"cookie_enabled"` OnlineStatus bool `json:"online_status"` } // ViewportInfo represents viewport and scroll information type ViewportInfo struct { Width int `json:"width"` Height int `json:"height"` ScrollX int `json:"scroll_x"` ScrollY int `json:"scroll_y"` ScrollWidth int `json:"scroll_width"` ScrollHeight int `json:"scroll_height"` ClientWidth int `json:"client_width"` ClientHeight int `json:"client_height"` DevicePixelRatio float64 `json:"device_pixel_ratio"` Orientation string `json:"orientation"` } // PerformanceMetrics represents page performance data type PerformanceMetrics struct { NavigationStart int64 `json:"navigation_start"` LoadEventEnd int64 `json:"load_event_end"` DOMContentLoaded int64 `json:"dom_content_loaded"` FirstPaint int64 `json:"first_paint"` FirstContentfulPaint int64 `json:"first_contentful_paint"` LoadTime int64 `json:"load_time"` DOMLoadTime int64 `json:"dom_load_time"` ResourceCount int `json:"resource_count"` JSHeapSizeLimit int64 `json:"js_heap_size_limit"` JSHeapSizeTotal int64 `json:"js_heap_size_total"` JSHeapSizeUsed int64 `json:"js_heap_size_used"` } // ContentCheck represents content verification results type ContentCheck struct { Type string `json:"type"` ImagesLoaded int `json:"images_loaded,omitempty"` ImagesTotal int `json:"images_total,omitempty"` ScriptsLoaded int `json:"scripts_loaded,omitempty"` ScriptsTotal int `json:"scripts_total,omitempty"` StylesLoaded int `json:"styles_loaded,omitempty"` StylesTotal int `json:"styles_total,omitempty"` FormsPresent int `json:"forms_present,omitempty"` LinksPresent int `json:"links_present,omitempty"` IframesPresent int `json:"iframes_present,omitempty"` HasErrors bool `json:"has_errors,omitempty"` ErrorCount int `json:"error_count,omitempty"` ErrorMessages []string `json:"error_messages,omitempty"` } // getPageInfo retrieves comprehensive page metadata and state information func (d *Daemon) getPageInfo(tabID string, timeout int) (*PageInfo, error) { d.debugLog("Getting page info for tab: %s with timeout: %d", tabID, timeout) page, err := d.getTab(tabID) if err != nil { return nil, fmt.Errorf("failed to get page: %v", err) } result := &PageInfo{} // Get basic page information using JavaScript jsCode := ` (() => { return { title: document.title, url: window.location.href, readyState: document.readyState, referrer: document.referrer, domain: document.domain, protocol: window.location.protocol, charset: document.characterSet || document.charset, contentType: document.contentType, lastModified: document.lastModified, cookieEnabled: navigator.cookieEnabled, onlineStatus: navigator.onLine }; })() ` jsResult, err := page.Eval(jsCode) if err != nil { return nil, fmt.Errorf("failed to execute JavaScript: %v", err) } // Parse the JavaScript result if props := jsResult.Value.Map(); props != nil { if title, ok := props["title"]; ok && title.Str() != "" { result.Title = title.Str() } if url, ok := props["url"]; ok && url.Str() != "" { result.URL = url.Str() } if readyState, ok := props["readyState"]; ok && readyState.Str() != "" { result.ReadyState = readyState.Str() } if referrer, ok := props["referrer"]; ok && referrer.Str() != "" { result.Referrer = referrer.Str() } if domain, ok := props["domain"]; ok && domain.Str() != "" { result.Domain = domain.Str() } if protocol, ok := props["protocol"]; ok && protocol.Str() != "" { result.Protocol = protocol.Str() } if charset, ok := props["charset"]; ok && charset.Str() != "" { result.Charset = charset.Str() } if contentType, ok := props["contentType"]; ok && contentType.Str() != "" { result.ContentType = contentType.Str() } if lastModified, ok := props["lastModified"]; ok && lastModified.Str() != "" { result.LastModified = lastModified.Str() } if cookieEnabled, ok := props["cookieEnabled"]; ok { result.CookieEnabled = cookieEnabled.Bool() } if onlineStatus, ok := props["onlineStatus"]; ok { result.OnlineStatus = onlineStatus.Bool() } } // Determine loading state if result.ReadyState == "complete" { result.LoadingState = "complete" } else if result.ReadyState == "interactive" { result.LoadingState = "interactive" } else { result.LoadingState = "loading" } d.debugLog("Successfully retrieved page info for tab: %s", tabID) return result, nil } // getViewportInfo retrieves viewport and scroll information func (d *Daemon) getViewportInfo(tabID string, timeout int) (*ViewportInfo, error) { d.debugLog("Getting viewport info for tab: %s with timeout: %d", tabID, timeout) page, err := d.getTab(tabID) if err != nil { return nil, fmt.Errorf("failed to get page: %v", err) } result := &ViewportInfo{} // Get viewport and scroll information using JavaScript jsCode := ` (() => { return { width: window.innerWidth, height: window.innerHeight, scrollX: window.scrollX || window.pageXOffset, scrollY: window.scrollY || window.pageYOffset, scrollWidth: document.documentElement.scrollWidth, scrollHeight: document.documentElement.scrollHeight, clientWidth: document.documentElement.clientWidth, clientHeight: document.documentElement.clientHeight, devicePixelRatio: window.devicePixelRatio, orientation: screen.orientation ? screen.orientation.type : 'unknown' }; })() ` jsResult, err := page.Eval(jsCode) if err != nil { return nil, fmt.Errorf("failed to execute JavaScript: %v", err) } // Parse the JavaScript result if props := jsResult.Value.Map(); props != nil { if width, ok := props["width"]; ok { result.Width = int(width.Num()) } if height, ok := props["height"]; ok { result.Height = int(height.Num()) } if scrollX, ok := props["scrollX"]; ok { result.ScrollX = int(scrollX.Num()) } if scrollY, ok := props["scrollY"]; ok { result.ScrollY = int(scrollY.Num()) } if scrollWidth, ok := props["scrollWidth"]; ok { result.ScrollWidth = int(scrollWidth.Num()) } if scrollHeight, ok := props["scrollHeight"]; ok { result.ScrollHeight = int(scrollHeight.Num()) } if clientWidth, ok := props["clientWidth"]; ok { result.ClientWidth = int(clientWidth.Num()) } if clientHeight, ok := props["clientHeight"]; ok { result.ClientHeight = int(clientHeight.Num()) } if devicePixelRatio, ok := props["devicePixelRatio"]; ok { result.DevicePixelRatio = devicePixelRatio.Num() } if orientation, ok := props["orientation"]; ok && orientation.Str() != "" { result.Orientation = orientation.Str() } } d.debugLog("Successfully retrieved viewport info for tab: %s", tabID) return result, nil } // getPerformance retrieves page performance metrics func (d *Daemon) getPerformance(tabID string, timeout int) (*PerformanceMetrics, error) { d.debugLog("Getting performance metrics for tab: %s with timeout: %d", tabID, timeout) page, err := d.getTab(tabID) if err != nil { return nil, fmt.Errorf("failed to get page: %v", err) } result := &PerformanceMetrics{} // Get performance metrics using JavaScript jsCode := ` (() => { const perf = window.performance; const timing = perf.timing; const navigation = perf.navigation; const memory = perf.memory; // Get paint metrics if available let firstPaint = 0; let firstContentfulPaint = 0; if (perf.getEntriesByType) { const paintEntries = perf.getEntriesByType('paint'); for (const entry of paintEntries) { if (entry.name === 'first-paint') { firstPaint = entry.startTime; } else if (entry.name === 'first-contentful-paint') { firstContentfulPaint = entry.startTime; } } } // Count resources let resourceCount = 0; if (perf.getEntriesByType) { resourceCount = perf.getEntriesByType('resource').length; } return { navigationStart: timing.navigationStart, loadEventEnd: timing.loadEventEnd, domContentLoaded: timing.domContentLoadedEventEnd, firstPaint: firstPaint, firstContentfulPaint: firstContentfulPaint, loadTime: timing.loadEventEnd - timing.navigationStart, domLoadTime: timing.domContentLoadedEventEnd - timing.navigationStart, resourceCount: resourceCount, jsHeapSizeLimit: memory ? memory.jsHeapSizeLimit : 0, jsHeapSizeTotal: memory ? memory.totalJSHeapSize : 0, jsHeapSizeUsed: memory ? memory.usedJSHeapSize : 0 }; })() ` jsResult, err := page.Eval(jsCode) if err != nil { return nil, fmt.Errorf("failed to execute JavaScript: %v", err) } // Parse the JavaScript result if props := jsResult.Value.Map(); props != nil { if navigationStart, ok := props["navigationStart"]; ok { result.NavigationStart = int64(navigationStart.Num()) } if loadEventEnd, ok := props["loadEventEnd"]; ok { result.LoadEventEnd = int64(loadEventEnd.Num()) } if domContentLoaded, ok := props["domContentLoaded"]; ok { result.DOMContentLoaded = int64(domContentLoaded.Num()) } if firstPaint, ok := props["firstPaint"]; ok { result.FirstPaint = int64(firstPaint.Num()) } if firstContentfulPaint, ok := props["firstContentfulPaint"]; ok { result.FirstContentfulPaint = int64(firstContentfulPaint.Num()) } if loadTime, ok := props["loadTime"]; ok { result.LoadTime = int64(loadTime.Num()) } if domLoadTime, ok := props["domLoadTime"]; ok { result.DOMLoadTime = int64(domLoadTime.Num()) } if resourceCount, ok := props["resourceCount"]; ok { result.ResourceCount = int(resourceCount.Num()) } if jsHeapSizeLimit, ok := props["jsHeapSizeLimit"]; ok { result.JSHeapSizeLimit = int64(jsHeapSizeLimit.Num()) } if jsHeapSizeTotal, ok := props["jsHeapSizeTotal"]; ok { result.JSHeapSizeTotal = int64(jsHeapSizeTotal.Num()) } if jsHeapSizeUsed, ok := props["jsHeapSizeUsed"]; ok { result.JSHeapSizeUsed = int64(jsHeapSizeUsed.Num()) } } d.debugLog("Successfully retrieved performance metrics for tab: %s", tabID) return result, nil } // checkContent verifies specific content types and loading states func (d *Daemon) checkContent(tabID string, contentType string, timeout int) (*ContentCheck, error) { d.debugLog("Checking content type '%s' for tab: %s with timeout: %d", contentType, tabID, timeout) page, err := d.getTab(tabID) if err != nil { return nil, fmt.Errorf("failed to get page: %v", err) } result := &ContentCheck{ Type: contentType, } var jsCode string switch contentType { case "images": jsCode = ` (() => { const images = document.querySelectorAll('img'); let loaded = 0; let total = images.length; images.forEach(img => { if (img.complete && img.naturalHeight !== 0) { loaded++; } }); return { imagesLoaded: loaded, imagesTotal: total }; })() ` case "scripts": jsCode = ` (() => { const scripts = document.querySelectorAll('script[src]'); let loaded = 0; let total = scripts.length; scripts.forEach(script => { if (script.readyState === 'loaded' || script.readyState === 'complete' || !script.readyState) { loaded++; } }); return { scriptsLoaded: loaded, scriptsTotal: total }; })() ` case "styles": jsCode = ` (() => { const styles = document.querySelectorAll('link[rel="stylesheet"]'); let loaded = 0; let total = styles.length; styles.forEach(style => { if (style.sheet) { loaded++; } }); return { stylesLoaded: loaded, stylesTotal: total }; })() ` case "forms": jsCode = ` (() => { return { formsPresent: document.querySelectorAll('form').length }; })() ` case "links": jsCode = ` (() => { return { linksPresent: document.querySelectorAll('a[href]').length }; })() ` case "iframes": jsCode = ` (() => { return { iframesPresent: document.querySelectorAll('iframe').length }; })() ` case "errors": jsCode = ` (() => { const errors = []; // Check for JavaScript errors in console (if available) if (window.console && window.console.error) { // This is limited - we can't access console history // But we can check for common error indicators } // Check for broken images const brokenImages = Array.from(document.querySelectorAll('img')).filter(img => !img.complete || img.naturalHeight === 0 ); if (brokenImages.length > 0) { errors.push('Broken images detected: ' + brokenImages.length); } // Check for missing stylesheets const brokenStyles = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter(link => !link.sheet ); if (brokenStyles.length > 0) { errors.push('Missing stylesheets detected: ' + brokenStyles.length); } return { hasErrors: errors.length > 0, errorCount: errors.length, errorMessages: errors }; })() ` default: return nil, fmt.Errorf("unknown content type: %s", contentType) } jsResult, err := page.Eval(jsCode) if err != nil { return nil, fmt.Errorf("failed to execute JavaScript: %v", err) } // Parse the JavaScript result if props := jsResult.Value.Map(); props != nil { if imagesLoaded, ok := props["imagesLoaded"]; ok { result.ImagesLoaded = int(imagesLoaded.Num()) } if imagesTotal, ok := props["imagesTotal"]; ok { result.ImagesTotal = int(imagesTotal.Num()) } if scriptsLoaded, ok := props["scriptsLoaded"]; ok { result.ScriptsLoaded = int(scriptsLoaded.Num()) } if scriptsTotal, ok := props["scriptsTotal"]; ok { result.ScriptsTotal = int(scriptsTotal.Num()) } if stylesLoaded, ok := props["stylesLoaded"]; ok { result.StylesLoaded = int(stylesLoaded.Num()) } if stylesTotal, ok := props["stylesTotal"]; ok { result.StylesTotal = int(stylesTotal.Num()) } if formsPresent, ok := props["formsPresent"]; ok { result.FormsPresent = int(formsPresent.Num()) } if linksPresent, ok := props["linksPresent"]; ok { result.LinksPresent = int(linksPresent.Num()) } if iframesPresent, ok := props["iframesPresent"]; ok { result.IframesPresent = int(iframesPresent.Num()) } if hasErrors, ok := props["hasErrors"]; ok { result.HasErrors = hasErrors.Bool() } if errorCount, ok := props["errorCount"]; ok { result.ErrorCount = int(errorCount.Num()) } if errorMessages, ok := props["errorMessages"]; ok { if arr := errorMessages.Arr(); arr != nil { for _, msg := range arr { if msg.Str() != "" { result.ErrorMessages = append(result.ErrorMessages, msg.Str()) } } } } } d.debugLog("Successfully checked content type '%s' for tab: %s", contentType, tabID) return result, nil } // screenshotElement takes a screenshot of a specific element func (d *Daemon) screenshotElement(tabID, selector, outputPath string, timeout int) error { d.debugLog("Taking element screenshot for tab: %s, selector: %s", tabID, selector) page, err := d.getTab(tabID) if err != nil { return err } // Find the element var element *rod.Element if timeout > 0 { element, err = page.Timeout(time.Duration(timeout) * time.Second).Element(selector) if err != nil { return fmt.Errorf("failed to find element (timeout after %ds): %w", timeout, err) } } else { element, err = page.Element(selector) if err != nil { return fmt.Errorf("failed to find element: %w", err) } } // Scroll element into view err = element.ScrollIntoView() if err != nil { return fmt.Errorf("failed to scroll element into view: %w", err) } // Wait for element to be stable err = element.WaitStable(500 * time.Millisecond) if err != nil { d.debugLog("Warning: element not stable: %v", err) } // Take screenshot of the element screenshotBytes, err := element.Screenshot(proto.PageCaptureScreenshotFormatPng, 0) if err != nil { return fmt.Errorf("failed to capture element screenshot: %w", err) } // Write the screenshot to file err = os.WriteFile(outputPath, screenshotBytes, 0644) if err != nil { return fmt.Errorf("failed to save element screenshot to %s: %w", outputPath, err) } d.debugLog("Successfully captured element screenshot for tab: %s", tabID) return nil } // ScreenshotMetadata represents metadata for enhanced screenshots type ScreenshotMetadata struct { Timestamp string `json:"timestamp"` URL string `json:"url"` Title string `json:"title"` ViewportSize struct { Width int `json:"width"` Height int `json:"height"` } `json:"viewport_size"` FullPage bool `json:"full_page"` FilePath string `json:"file_path"` FileSize int64 `json:"file_size"` Resolution struct { Width int `json:"width"` Height int `json:"height"` } `json:"resolution"` } // screenshotEnhanced takes a screenshot with metadata func (d *Daemon) screenshotEnhanced(tabID, outputPath string, fullPage bool, timeout int) (*ScreenshotMetadata, error) { d.debugLog("Taking enhanced screenshot for tab: %s", tabID) page, err := d.getTab(tabID) if err != nil { return nil, err } // Get page info for metadata pageInfo, err := page.Info() if err != nil { return nil, fmt.Errorf("failed to get page info: %w", err) } // Get viewport size viewport, err := page.Eval(`() => ({ width: window.innerWidth, height: window.innerHeight })`) if err != nil { return nil, fmt.Errorf("failed to get viewport: %w", err) } viewportData := viewport.Value.Map() viewportWidth := int(viewportData["width"].Num()) viewportHeight := int(viewportData["height"].Num()) // Take screenshot with timeout handling var screenshotBytes []byte if timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() done := make(chan error, 1) go func() { bytes, err := page.Screenshot(fullPage, &proto.PageCaptureScreenshot{ Format: proto.PageCaptureScreenshotFormatPng, }) screenshotBytes = bytes done <- err }() select { case err := <-done: if err != nil { return nil, fmt.Errorf("failed to capture screenshot: %w", err) } case <-ctx.Done(): return nil, fmt.Errorf("taking screenshot timed out after %d seconds", timeout) } } else { screenshotBytes, err = page.Screenshot(fullPage, &proto.PageCaptureScreenshot{ Format: proto.PageCaptureScreenshotFormatPng, }) if err != nil { return nil, fmt.Errorf("failed to capture screenshot: %w", err) } } // Write the screenshot to file err = os.WriteFile(outputPath, screenshotBytes, 0644) if err != nil { return nil, fmt.Errorf("failed to save screenshot to %s: %w", outputPath, err) } // Get file info fileInfo, err := os.Stat(outputPath) if err != nil { return nil, fmt.Errorf("failed to get file info: %w", err) } // Create metadata metadata := &ScreenshotMetadata{ Timestamp: time.Now().Format(time.RFC3339), URL: pageInfo.URL, Title: pageInfo.Title, FullPage: fullPage, FilePath: outputPath, FileSize: fileInfo.Size(), } metadata.ViewportSize.Width = viewportWidth metadata.ViewportSize.Height = viewportHeight // Get actual image dimensions (approximate based on viewport or full page) if fullPage { // For full page, we'd need to calculate the full document size // For now, use viewport size as approximation metadata.Resolution.Width = viewportWidth metadata.Resolution.Height = viewportHeight } else { metadata.Resolution.Width = viewportWidth metadata.Resolution.Height = viewportHeight } d.debugLog("Successfully captured enhanced screenshot for tab: %s", tabID) return metadata, nil } // FileOperation represents a single file operation type FileOperation struct { LocalPath string `json:"local_path"` ContainerPath string `json:"container_path"` Operation string `json:"operation"` // "upload" or "download" } // BulkFileResult represents the result of bulk file operations type BulkFileResult struct { Successful []FileOperationResult `json:"successful"` Failed []FileOperationError `json:"failed"` Summary struct { Total int `json:"total"` Successful int `json:"successful"` Failed int `json:"failed"` } `json:"summary"` } // FileOperationResult represents a successful file operation type FileOperationResult struct { LocalPath string `json:"local_path"` ContainerPath string `json:"container_path"` Operation string `json:"operation"` Size int64 `json:"size"` } // FileOperationError represents a failed file operation type FileOperationError struct { LocalPath string `json:"local_path"` ContainerPath string `json:"container_path"` Operation string `json:"operation"` Error string `json:"error"` } // bulkFiles performs bulk file operations (upload/download) func (d *Daemon) bulkFiles(operationType, filesJSON string, timeout int) (*BulkFileResult, error) { d.debugLog("Performing bulk file operations: %s", operationType) // Parse the files JSON var operations []FileOperation err := json.Unmarshal([]byte(filesJSON), &operations) if err != nil { return nil, fmt.Errorf("failed to parse files JSON: %w", err) } result := &BulkFileResult{ Successful: make([]FileOperationResult, 0), Failed: make([]FileOperationError, 0), } // Set up timeout context ctx := context.Background() if timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() } // Process each file operation for _, op := range operations { select { case <-ctx.Done(): // Timeout reached, add remaining operations as failed for i := len(result.Successful) + len(result.Failed); i < len(operations); i++ { result.Failed = append(result.Failed, FileOperationError{ LocalPath: operations[i].LocalPath, ContainerPath: operations[i].ContainerPath, Operation: operations[i].Operation, Error: "operation timed out", }) } break default: // Perform the operation if op.Operation == "upload" || (op.Operation == "" && operationType == "upload") { err := d.performFileUpload(op.LocalPath, op.ContainerPath) if err != nil { result.Failed = append(result.Failed, FileOperationError{ LocalPath: op.LocalPath, ContainerPath: op.ContainerPath, Operation: "upload", Error: err.Error(), }) } else { // Get file size fileInfo, _ := os.Stat(op.ContainerPath) size := int64(0) if fileInfo != nil { size = fileInfo.Size() } result.Successful = append(result.Successful, FileOperationResult{ LocalPath: op.LocalPath, ContainerPath: op.ContainerPath, Operation: "upload", Size: size, }) } } else if op.Operation == "download" || (op.Operation == "" && operationType == "download") { err := d.performFileDownload(op.ContainerPath, op.LocalPath) if err != nil { result.Failed = append(result.Failed, FileOperationError{ LocalPath: op.LocalPath, ContainerPath: op.ContainerPath, Operation: "download", Error: err.Error(), }) } else { // Get file size fileInfo, _ := os.Stat(op.LocalPath) size := int64(0) if fileInfo != nil { size = fileInfo.Size() } result.Successful = append(result.Successful, FileOperationResult{ LocalPath: op.LocalPath, ContainerPath: op.ContainerPath, Operation: "download", Size: size, }) } } else { result.Failed = append(result.Failed, FileOperationError{ LocalPath: op.LocalPath, ContainerPath: op.ContainerPath, Operation: op.Operation, Error: "unknown operation type", }) } } } // Update summary result.Summary.Total = len(operations) result.Summary.Successful = len(result.Successful) result.Summary.Failed = len(result.Failed) d.debugLog("Bulk file operations completed: %d successful, %d failed", result.Summary.Successful, result.Summary.Failed) return result, nil } // performFileUpload handles a single file upload operation func (d *Daemon) performFileUpload(localPath, containerPath string) error { // Open the source file sourceFile, err := os.Open(localPath) if err != nil { return fmt.Errorf("failed to open source file: %w", err) } defer sourceFile.Close() // Create the destination file destFile, err := os.Create(containerPath) if err != nil { return fmt.Errorf("failed to create destination file: %w", err) } defer destFile.Close() // Copy the file _, err = io.Copy(destFile, sourceFile) if err != nil { return fmt.Errorf("failed to copy file: %w", err) } return nil } // performFileDownload handles a single file download operation func (d *Daemon) performFileDownload(containerPath, localPath string) error { // Open the source file sourceFile, err := os.Open(containerPath) if err != nil { return fmt.Errorf("failed to open source file: %w", err) } defer sourceFile.Close() // Create the destination file destFile, err := os.Create(localPath) if err != nil { return fmt.Errorf("failed to create destination file: %w", err) } defer destFile.Close() // Copy the file _, err = io.Copy(destFile, sourceFile) if err != nil { return fmt.Errorf("failed to copy file: %w", err) } return nil } // FileManagementResult represents the result of file management operations type FileManagementResult struct { Operation string `json:"operation"` Files []FileInfo `json:"files,omitempty"` Cleaned []string `json:"cleaned,omitempty"` Summary map[string]interface{} `json:"summary"` } // FileInfo represents information about a file type FileInfo struct { Path string `json:"path"` Size int64 `json:"size"` ModTime time.Time `json:"mod_time"` IsDir bool `json:"is_dir"` Permissions string `json:"permissions"` } // manageFiles performs file management operations func (d *Daemon) manageFiles(operation, pattern, maxAge string) (*FileManagementResult, error) { d.debugLog("Performing file management operation: %s", operation) result := &FileManagementResult{ Operation: operation, Summary: make(map[string]interface{}), } switch operation { case "cleanup": return d.cleanupFiles(pattern, maxAge, result) case "list": return d.listFiles(pattern, result) case "info": return d.getFileInfo(pattern, result) default: return nil, fmt.Errorf("unknown file management operation: %s", operation) } } // cleanupFiles removes files matching pattern and age criteria func (d *Daemon) cleanupFiles(pattern, maxAge string, result *FileManagementResult) (*FileManagementResult, error) { // Parse max age (default to 24 hours if not specified) maxAgeHours := 24 if maxAge != "" { if parsed, err := strconv.Atoi(maxAge); err == nil && parsed > 0 { maxAgeHours = parsed } } cutoffTime := time.Now().Add(-time.Duration(maxAgeHours) * time.Hour) // Default pattern if not specified if pattern == "" { pattern = "/tmp/cremote-*" } // Find files matching pattern matches, err := filepath.Glob(pattern) if err != nil { return nil, fmt.Errorf("failed to find files matching pattern: %w", err) } var cleaned []string var totalSize int64 for _, filePath := range matches { fileInfo, err := os.Stat(filePath) if err != nil { continue // Skip files we can't stat } // Check if file is older than cutoff time if fileInfo.ModTime().Before(cutoffTime) { totalSize += fileInfo.Size() err = os.Remove(filePath) if err != nil { d.debugLog("Failed to remove file %s: %v", filePath, err) } else { cleaned = append(cleaned, filePath) } } } result.Cleaned = cleaned result.Summary["files_cleaned"] = len(cleaned) result.Summary["total_size_freed"] = totalSize result.Summary["cutoff_time"] = cutoffTime.Format(time.RFC3339) d.debugLog("Cleanup completed: %d files removed, %d bytes freed", len(cleaned), totalSize) return result, nil } // listFiles lists files matching pattern func (d *Daemon) listFiles(pattern string, result *FileManagementResult) (*FileManagementResult, error) { // Default pattern if not specified if pattern == "" { pattern = "/tmp/*" } // Find files matching pattern matches, err := filepath.Glob(pattern) if err != nil { return nil, fmt.Errorf("failed to find files matching pattern: %w", err) } var files []FileInfo var totalSize int64 for _, filePath := range matches { fileInfo, err := os.Stat(filePath) if err != nil { continue // Skip files we can't stat } files = append(files, FileInfo{ Path: filePath, Size: fileInfo.Size(), ModTime: fileInfo.ModTime(), IsDir: fileInfo.IsDir(), Permissions: fileInfo.Mode().String(), }) if !fileInfo.IsDir() { totalSize += fileInfo.Size() } } result.Files = files result.Summary["total_files"] = len(files) result.Summary["total_size"] = totalSize d.debugLog("Listed %d files matching pattern: %s", len(files), pattern) return result, nil } // getFileInfo gets detailed information about a specific file func (d *Daemon) getFileInfo(filePath string, result *FileManagementResult) (*FileManagementResult, error) { if filePath == "" { return nil, fmt.Errorf("file path is required for info operation") } fileInfo, err := os.Stat(filePath) if err != nil { return nil, fmt.Errorf("failed to get file info: %w", err) } files := []FileInfo{{ Path: filePath, Size: fileInfo.Size(), ModTime: fileInfo.ModTime(), IsDir: fileInfo.IsDir(), Permissions: fileInfo.Mode().String(), }} result.Files = files result.Summary["exists"] = true result.Summary["size"] = fileInfo.Size() result.Summary["is_directory"] = fileInfo.IsDir() result.Summary["last_modified"] = fileInfo.ModTime().Format(time.RFC3339) d.debugLog("Retrieved info for file: %s", filePath) return result, nil }