4862 lines
134 KiB
Go
4862 lines
134 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 "select-element":
|
|
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.selectElement(tabID, selector, value, 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
|
|
}
|
|
|
|
// selectElement selects an option in a select dropdown
|
|
func (d *Daemon) selectElement(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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// For select elements, use rod's built-in Select method
|
|
// Try to select by text first (most common case)
|
|
err = element.Select([]string{value}, true, rod.SelectorTypeText)
|
|
if err != nil {
|
|
// If text selection failed, use JavaScript as fallback
|
|
// Use a simple single statement that works with rod's evaluation
|
|
script := fmt.Sprintf("document.querySelector(\"%s\").value = \"%s\"", selector, value)
|
|
|
|
// Execute the value assignment (ignore evaluation errors, rod has issues with some JS patterns)
|
|
page.Eval(script)
|
|
|
|
// Dispatch the change event separately
|
|
changeScript := fmt.Sprintf("document.querySelector(\"%s\").dispatchEvent(new Event(\"change\", { bubbles: true }))", selector)
|
|
page.Eval(changeScript)
|
|
|
|
// Verify the selection worked by checking the element's value property directly
|
|
currentValue, err := element.Property("value")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify selection: %w", err)
|
|
}
|
|
|
|
// Check if the selection actually worked
|
|
if currentValue.Str() != value {
|
|
return fmt.Errorf("failed to select option '%s' in element (current value: %s)", value, currentValue.Str())
|
|
}
|
|
}
|
|
|
|
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 rod's built-in Select method
|
|
// Try to select by text first (most common case)
|
|
err = element.Select([]string{interaction.Value}, true, rod.SelectorTypeText)
|
|
if err != nil {
|
|
// If text selection failed, use JavaScript as fallback
|
|
// Use double quotes for the JavaScript string so single quotes in selectors work
|
|
script := fmt.Sprintf("document.querySelector(\"%s\").value = \"%s\"; document.querySelector(\"%s\").dispatchEvent(new Event(\"change\", { bubbles: true })); document.querySelector(\"%s\").value", interaction.Selector, interaction.Value, interaction.Selector, interaction.Selector)
|
|
|
|
// Execute JavaScript and get the result
|
|
result, err := page.Eval(script)
|
|
if err != nil {
|
|
interactionResult.Error = fmt.Sprintf("failed to execute JavaScript selection: %v", err)
|
|
} else if result.Value.Nil() || result.Value.String() == "" {
|
|
interactionResult.Error = fmt.Sprintf("failed to select option '%s'", interaction.Value)
|
|
} else {
|
|
interactionResult.Success = true
|
|
}
|
|
} else {
|
|
interactionResult.Success = true
|
|
}
|
|
|
|
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", // Default action, will be updated based on element type
|
|
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)
|
|
if err == nil {
|
|
fieldResult.Selector = selector
|
|
cancel() // Cancel context now that we found the element
|
|
break
|
|
}
|
|
cancel() // Cancel if element not found
|
|
} else {
|
|
element, err = form.Element(selector)
|
|
if err == nil {
|
|
fieldResult.Selector = selector
|
|
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)
|
|
if err == nil {
|
|
fieldResult.Selector = selector
|
|
cancel() // Cancel context now that we found the element
|
|
break
|
|
}
|
|
cancel() // Cancel if element not found
|
|
} else {
|
|
element, err = page.Element(selector)
|
|
if err == nil {
|
|
fieldResult.Selector = selector
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if element == nil {
|
|
fieldResult.Error = fmt.Sprintf("failed to find field: %s", fieldName)
|
|
result.FilledFields = append(result.FilledFields, fieldResult)
|
|
result.ErrorCount++
|
|
continue
|
|
}
|
|
|
|
// Determine the element type and use appropriate action
|
|
tagName, err := element.Eval("() => this.tagName.toLowerCase()")
|
|
if err != nil {
|
|
fieldResult.Error = fmt.Sprintf("failed to get element tag name: %v", err)
|
|
result.FilledFields = append(result.FilledFields, fieldResult)
|
|
result.ErrorCount++
|
|
continue
|
|
}
|
|
|
|
// Handle different element types
|
|
if tagName.Value.String() == "select" {
|
|
// Use select action for select elements
|
|
fieldResult.Action = "select"
|
|
err = element.Select([]string{fieldValue}, true, rod.SelectorTypeText)
|
|
if err != nil {
|
|
// If text selection failed, use JavaScript as fallback
|
|
// Use double quotes for the JavaScript string so single quotes in selectors work
|
|
script := fmt.Sprintf("document.querySelector(\"%s\").value = \"%s\"; document.querySelector(\"%s\").dispatchEvent(new Event(\"change\", { bubbles: true })); document.querySelector(\"%s\").value", fieldResult.Selector, fieldValue, fieldResult.Selector, fieldResult.Selector)
|
|
|
|
// Execute JavaScript and get the result
|
|
jsResult, err := page.Eval(script)
|
|
if err != nil {
|
|
fieldResult.Error = fmt.Sprintf("failed to execute JavaScript selection: %v", err)
|
|
result.ErrorCount++
|
|
} else if jsResult.Value.Nil() || jsResult.Value.String() == "" {
|
|
fieldResult.Error = fmt.Sprintf("failed to select option '%s'", fieldValue)
|
|
result.ErrorCount++
|
|
} else {
|
|
fieldResult.Success = true
|
|
result.SuccessCount++
|
|
}
|
|
} else {
|
|
fieldResult.Success = true
|
|
result.SuccessCount++
|
|
}
|
|
} else {
|
|
// Use fill action for input, textarea, etc.
|
|
fieldResult.Action = "fill"
|
|
err = element.SelectAllText()
|
|
if err == nil {
|
|
err = element.Input("")
|
|
}
|
|
if err == nil {
|
|
err = element.Input(fieldValue)
|
|
}
|
|
|
|
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
|
|
}
|