cremote/daemon/daemon.go

4781 lines
131 KiB
Go

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 "<unavailable>"
}
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, use JavaScript to set the value
script := fmt.Sprintf(`
if (this.tagName.toLowerCase() === 'select') {
// Try to select by value first
for (let option of this.options) {
if (option.value === '%s') {
this.value = '%s';
this.dispatchEvent(new Event('change', { bubbles: true }));
break;
}
}
// Try to select by text if value didn't work
if (this.value !== '%s') {
for (let option of this.options) {
if (option.text === '%s') {
this.value = option.value;
this.dispatchEvent(new Event('change', { bubbles: true }));
break;
}
}
}
}
`, interaction.Value, interaction.Value, interaction.Value, interaction.Value)
_, err := element.Eval(script)
if err != nil {
interactionResult.Error = fmt.Sprintf("failed to execute select script: %v", err)
} else {
// Check if the value was actually set
currentValue, err := element.Property("value")
if err != nil {
interactionResult.Error = fmt.Sprintf("failed to get current value: %v", err)
} else if currentValue.Str() == interaction.Value {
interactionResult.Success = true
} else {
interactionResult.Error = fmt.Sprintf("failed to select option: %s (current value: %s)", interaction.Value, currentValue.Str())
}
}
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
}