diff --git a/client/client.go b/client/client.go index c464e83..b9efd82 100644 --- a/client/client.go +++ b/client/client.go @@ -527,7 +527,8 @@ func (c *Client) TakeScreenshot(tabID, outputPath string, fullPage bool, timeout // SwitchToIframe switches the context to an iframe for subsequent commands // If tabID is empty, the current tab will be used -func (c *Client) SwitchToIframe(tabID, selector string) error { +// timeout is in seconds, 0 means no timeout +func (c *Client) SwitchToIframe(tabID, selector string, timeout int) error { params := map[string]string{ "selector": selector, } @@ -537,6 +538,11 @@ func (c *Client) SwitchToIframe(tabID, selector string) error { params["tab"] = tabID } + // Add timeout if specified + if timeout > 0 { + params["timeout"] = strconv.Itoa(timeout) + } + resp, err := c.SendCommand("switch-iframe", params) if err != nil { return err diff --git a/daemon/daemon.go b/daemon/daemon.go index dbbb6c6..aeb6e3e 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -462,8 +462,17 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) { case "switch-iframe": tabID := cmd.Params["tab"] selector := cmd.Params["selector"] + timeoutStr := cmd.Params["timeout"] - err := d.switchToIframe(tabID, selector) + // 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 { @@ -1678,10 +1687,13 @@ func (d *Daemon) takeScreenshot(tabID, outputPath string, fullPage bool, timeout } // switchToIframe switches the context to an iframe for subsequent commands -func (d *Daemon) switchToIframe(tabID, selector string) error { +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 } @@ -1691,49 +1703,155 @@ func (d *Daemon) switchToIframe(tabID, selector string) error { // 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 } - // Find the iframe element - iframeElement, err := mainPage.Element(selector) + 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) } - // Get the iframe's page context - iframePage, err := iframeElement.Frame() + 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() - // Remove the iframe context for this tab - delete(d.iframePages, actualTabID) + // 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 } diff --git a/main.go b/main.go index 9238f6a..124cda5 100644 --- a/main.go +++ b/main.go @@ -116,6 +116,7 @@ func main() { // switch-iframe flags switchIframeTabID := switchIframeCmd.String("tab", "", "Tab ID to switch iframe context in (optional, uses current tab if not specified)") switchIframeSelector := switchIframeCmd.String("selector", "", "CSS selector for the iframe element") + switchIframeTimeout := switchIframeCmd.Int("timeout", 5, "Timeout in seconds for iframe switching") switchIframeHost := switchIframeCmd.String("host", "localhost", "Daemon host") switchIframePort := switchIframeCmd.Int("port", 8989, "Daemon port") @@ -366,7 +367,7 @@ func main() { c := client.NewClient(*switchIframeHost, *switchIframePort) // Switch to iframe - err := c.SwitchToIframe(*switchIframeTabID, *switchIframeSelector) + err := c.SwitchToIframe(*switchIframeTabID, *switchIframeSelector, *switchIframeTimeout) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) diff --git a/mcp/LLM_USAGE_GUIDE.md b/mcp/LLM_USAGE_GUIDE.md index ea5ba2d..bc7cf4e 100644 --- a/mcp/LLM_USAGE_GUIDE.md +++ b/mcp/LLM_USAGE_GUIDE.md @@ -112,17 +112,21 @@ Switch iframe context for subsequent operations. - `action` (required): One of "enter", "exit" - `selector` (optional): Iframe CSS selector (required for "enter" action) - `tab` (optional): Specific tab ID to use +- `timeout` (optional): Timeout in seconds (default: 5) **Example Usage:** ``` web_iframe_cremotemcp: action: "enter" selector: "iframe#payment-form" + timeout: 10 web_iframe_cremotemcp: action: "exit" ``` +**Note:** The timeout parameter is particularly important for iframe operations as they can hang if the iframe takes time to load or if the selector doesn't match any elements. + ### 7. `file_upload_cremotemcp` Upload files from the client to the container for use in form uploads. diff --git a/mcp/backup/server-stdio.go b/mcp/backup/server-stdio.go index 99ee7ed..6a123f0 100644 --- a/mcp/backup/server-stdio.go +++ b/mcp/backup/server-stdio.go @@ -750,7 +750,7 @@ func (s *MCPServer) handleIframe(params map[string]interface{}) (ToolResult, err if selector == "" { return ToolResult{}, fmt.Errorf("selector parameter is required for enter action") } - err = s.client.SwitchToIframe(tab, selector) + err = s.client.SwitchToIframe(tab, selector, 5) // Default 5 second timeout s.iframeMode = true data = map[string]string{"action": "entered", "selector": selector} diff --git a/mcp/backup/server.go b/mcp/backup/server.go index 75151f4..e2c4baa 100644 --- a/mcp/backup/server.go +++ b/mcp/backup/server.go @@ -636,7 +636,7 @@ func (s *MCPServer) handleIframe(params map[string]interface{}) (ToolResult, err if selector == "" { return ToolResult{}, fmt.Errorf("selector parameter is required for enter action") } - err = s.client.SwitchToIframe(tab, selector) + err = s.client.SwitchToIframe(tab, selector, 5) // Default 5 second timeout s.iframeMode = true data = map[string]string{"action": "entered", "selector": selector} diff --git a/mcp/cremote-mcp b/mcp/cremote-mcp index e494e73..bef8c61 100755 Binary files a/mcp/cremote-mcp and b/mcp/cremote-mcp differ diff --git a/mcp/cremote-mcp2 b/mcp/cremote-mcp2 deleted file mode 100755 index 86bb0af..0000000 Binary files a/mcp/cremote-mcp2 and /dev/null differ diff --git a/mcp/main.go b/mcp/main.go index 4ebb290..223d1e7 100644 --- a/mcp/main.go +++ b/mcp/main.go @@ -538,6 +538,11 @@ func main() { "type": "string", "description": "Tab ID (optional)", }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, }, Required: []string{"action"}, }, @@ -551,6 +556,7 @@ func main() { action := getStringParam(params, "action", "") selector := getStringParam(params, "selector", "") tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) if action == "" { return nil, fmt.Errorf("action parameter is required") @@ -567,7 +573,7 @@ func main() { if selector == "" { return nil, fmt.Errorf("selector parameter is required for enter action") } - err = cremoteServer.client.SwitchToIframe(tab, selector) + err = cremoteServer.client.SwitchToIframe(tab, selector, timeout) cremoteServer.iframeMode = true message = fmt.Sprintf("Entered iframe: %s", selector) diff --git a/test-iframe-timeout.sh b/test-iframe-timeout.sh new file mode 100755 index 0000000..391ea0a --- /dev/null +++ b/test-iframe-timeout.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Test script for iframe timeout functionality +set -e + +echo "Starting iframe timeout test..." + +# Start the daemon in background +echo "Starting cremotedaemon..." +./cremotedaemon --debug & +DAEMON_PID=$! + +# Wait for daemon to start +sleep 2 + +# Function to cleanup +cleanup() { + echo "Cleaning up..." + kill $DAEMON_PID 2>/dev/null || true + wait $DAEMON_PID 2>/dev/null || true +} + +# Set trap for cleanup +trap cleanup EXIT + +# Test 1: Basic iframe switching with timeout +echo "Test 1: Basic iframe switching with timeout" +TAB_ID=$(./cremote open-tab --timeout 5 | grep -o '"[^"]*"' | tr -d '"') +echo "Created tab: $TAB_ID" + +# Load test page +./cremote load-url --tab "$TAB_ID" --url "file://$(pwd)/test-iframe.html" --timeout 10 +echo "Loaded test page" + +# Switch to iframe with timeout +echo "Switching to iframe with 5 second timeout..." +./cremote switch-iframe --tab "$TAB_ID" --selector "#test-iframe" --timeout 5 +echo "Successfully switched to iframe" + +# Try to click button in iframe +echo "Clicking button in iframe..." +./cremote click-element --tab "$TAB_ID" --selector "#iframe-button" --selection-timeout 5 --action-timeout 5 +echo "Successfully clicked iframe button" + +# Switch back to main +echo "Switching back to main context..." +./cremote switch-main --tab "$TAB_ID" +echo "Successfully switched back to main" + +# Try to click main button +echo "Clicking main page button..." +./cremote click-element --tab "$TAB_ID" --selector "#main-button" --selection-timeout 5 --action-timeout 5 +echo "Successfully clicked main button" + +# Test 2: Test timeout with non-existent iframe +echo "" +echo "Test 2: Testing timeout with non-existent iframe" +set +e # Allow command to fail +./cremote switch-iframe --tab "$TAB_ID" --selector "#non-existent-iframe" --timeout 2 +RESULT=$? +set -e + +if [ $RESULT -eq 0 ]; then + echo "ERROR: Expected timeout failure but command succeeded" + exit 1 +else + echo "SUCCESS: Timeout correctly handled for non-existent iframe" +fi + +echo "" +echo "All iframe timeout tests passed!" diff --git a/test-iframe.html b/test-iframe.html new file mode 100644 index 0000000..e13e38a --- /dev/null +++ b/test-iframe.html @@ -0,0 +1,20 @@ + + +
+This is the main page content.
+ + + + + + + +