File size: 12,065 Bytes
eb794cb
 
 
 
 
 
d95f261
a407c96
3b82595
eb794cb
a407c96
3b82595
 
 
eb794cb
82964ab
3b82595
 
 
 
a407c96
3b82595
 
 
a407c96
3b82595
 
 
 
 
 
a407c96
3b82595
 
 
 
82964ab
 
eb794cb
3b82595
82964ab
3b82595
 
 
 
 
 
 
 
 
 
 
 
82964ab
3b82595
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb794cb
3b82595
 
 
c640d46
 
 
3b82595
 
 
 
82964ab
3b82595
 
 
a407c96
3b82595
 
 
 
 
 
 
 
 
 
 
82964ab
 
3b82595
 
82964ab
3b82595
 
 
 
 
 
82964ab
3b82595
 
 
eb794cb
3b82595
 
 
 
 
a407c96
 
 
 
 
c640d46
a407c96
3b82595
 
 
 
82964ab
3b82595
 
 
 
a407c96
3b82595
82964ab
a407c96
3b82595
 
 
c640d46
3b82595
 
 
 
 
 
 
 
 
 
 
 
 
c640d46
3b82595
 
 
 
 
a407c96
 
3b82595
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c640d46
3b82595
 
 
82964ab
3b82595
 
 
 
a407c96
3b82595
 
 
82964ab
3b82595
 
 
 
 
 
c640d46
3b82595
 
 
 
c640d46
 
eb794cb
82964ab
c640d46
3b82595
 
c640d46
3b82595
 
82964ab
 
3b82595
 
eb794cb
c640d46
82964ab
c640d46
3b82595
 
 
 
c640d46
a407c96
3b82595
 
 
 
 
 
eb794cb
82964ab
3b82595
c640d46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b82595
c640d46
 
 
 
3b82595
 
c640d46
 
 
3b82595
 
c640d46
 
3b82595
 
c640d46
3b82595
 
 
c640d46
3b82595
eb794cb
 
c640d46
a407c96
3b82595
 
c640d46
a407c96
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
import gradio as gr
import subprocess
import time
import os
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.support.ui import WebDriverWait
import psutil
import socket
from pathlib import Path
from PIL import Image
import io

class SimpleChromeController:
    def __init__(self):
        self.chrome_process = None
        self.is_running = False
        self.setup_directories()
        
    def setup_directories(self):
        """Setup required directories with proper permissions"""
        home_dir = Path.home()
        
        directories = [
            Path('/app/chrome_profile'),
            Path('/app/chrome_crashes'),
            home_dir / '.local' / 'share' / 'applications',
            home_dir / '.config'
        ]
        
        for path in directories:
            try:
                path.mkdir(parents=True, exist_ok=True)
                os.chmod(path, 0o755)
            except (PermissionError, OSError):
                pass  # Continue silently

    def get_chrome_args(self):
        """Get Chrome arguments for containerized environments"""
        return [
            "google-chrome",
            "--remote-debugging-port=9222",
            "--user-data-dir=/app/chrome_profile",
            "--crash-dumps-dir=/app/chrome_crashes",
            "--no-sandbox",
            "--disable-dev-shm-usage",
            "--disable-gpu",
            "--disable-software-rasterizer",
            "--disable-background-timer-throttling",
            "--disable-renderer-backgrounding",
            "--disable-backgrounding-occluded-windows",
            "--disable-features=TranslateUI,BlinkGenPropertyTrees",
            "--disable-ipc-flooding-protection",
            "--disable-default-apps",
            "--disable-extensions",
            "--disable-component-extensions-with-background-pages",
            "--disable-background-networking",
            "--disable-sync",
            "--disable-translate",
            "--hide-scrollbars",
            "--mute-audio",
            "--no-first-run",
            "--disable-notifications",
            "--disable-popup-blocking",
            "--disable-prompt-on-repost",
            "--disable-hang-monitor",
            "--disable-logging",
            "--disable-login-animations",
            "--disable-modal-animations",
            "--disable-infobars",
            "--headless=new",
            "--window-size=1920,1080",
            "--virtual-time-budget=25000",
            "--disable-crash-reporter",
            "--disable-crashpad",
            "--disable-breakpad",
            "--no-crash-upload",
            "--single-process"
        ]

    def is_chrome_accessible(self):
        """Check if Chrome debugging port is accessible"""
        try:
            with socket.create_connection(("127.0.0.1", 9222), timeout=2) as sock:
                return True
        except (socket.timeout, ConnectionRefusedError):
            return False

    def start_chrome(self):
        """Start Chrome browser process"""
        if self.is_running and self.is_chrome_accessible():
            return "⚠️ Chrome is already running"
            
        self.kill_chrome()
        
        try:
            chrome_args = self.get_chrome_args()
            
            self.chrome_process = subprocess.Popen(
                chrome_args,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                preexec_fn=os.setsid if hasattr(os, 'setsid') else None
            )
            
            # Wait for Chrome to start
            for attempt in range(15):
                time.sleep(2)
                if self.is_chrome_accessible():
                    self.is_running = True
                    return f"βœ… Chrome started successfully (attempt {attempt+1}/15)"
                    
                if self.chrome_process.poll() is not None:
                    stdout, stderr = self.chrome_process.communicate()
                    error_msg = stderr.decode('utf-8', errors='ignore')[:300]
                    return f"❌ Chrome failed to start: {error_msg}"
            
            return "⚠️ Chrome may have started but debugging port not accessible. Try taking a screenshot to test."
                
        except Exception as e:
            return f"❌ Failed to start Chrome: {str(e)}"

    def get_driver(self):
        """Get a WebDriver instance connected to running Chrome"""
        if not self.is_chrome_accessible():
            raise Exception("Chrome is not running or not accessible")
            
        chrome_options = Options()
        chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
        chrome_options.add_argument("--no-sandbox")
        chrome_options.add_argument("--disable-dev-shm-usage")
        
        # Use a cached driver install
        service = Service(ChromeDriverManager().install())
        return webdriver.Chrome(service=service, options=chrome_options)

    def navigate_to_url(self, url):
        """Navigate to a specific URL"""
        if not url or not url.strip():
            return "❌ Please enter a valid URL"
            
        if not self.is_chrome_accessible():
            return "❌ Chrome is not running. Please start Chrome first."
        
        if not url.startswith(('http://', 'https://')):
            url = 'https://' + url.strip()
        
        driver = None
        try:
            driver = self.get_driver()
            driver.set_page_load_timeout(20)
            driver.get(url)
            
            WebDriverWait(driver, 10).until(
                lambda d: d.execute_script("return document.readyState") == "complete"
            )
            
            page_title = driver.title or "Unknown"
            return f"🧭 Navigated to: {url}\nπŸ“„ Page title: {page_title}"
            
        except Exception as e:
            return f"❌ Error navigating to URL: {str(e)}"
        finally:
            if driver:
                driver.quit()

    def take_screenshot(self):
        """Take a screenshot of the current page"""
        if not self.is_chrome_accessible():
            return None, "❌ Chrome is not running. Please start Chrome first."
        
        driver = None
        try:
            driver = self.get_driver()
            
            current_url = driver.current_url
            if not current_url or current_url == "data:,":
                driver.get("https://www.google.com")
                WebDriverWait(driver, 10).until(
                    lambda d: d.execute_script("return document.readyState") == "complete"
                )
            
            screenshot_data = driver.get_screenshot_as_png()
            screenshot_image = Image.open(io.BytesIO(screenshot_data))
            
            page_title = driver.title or "Unknown"
            page_url = driver.current_url or "Unknown"
            
            return screenshot_image, f"πŸ“Έ Screenshot captured!\nπŸ“„ Page: {page_title}\nπŸ”— URL: {page_url}"
            
        except Exception as e:
            return None, f"❌ Error taking screenshot: {str(e)}"
        finally:
            if driver:
                driver.quit()

    def execute_javascript(self, js_code):
        """Execute JavaScript code in the browser"""
        if not js_code or not js_code.strip():
            return "❌ Please enter JavaScript code to execute"
            
        if not self.is_chrome_accessible():
            return "❌ Chrome is not running. Please start Chrome first."
        
        driver = None
        try:
            driver = self.get_driver()
            result = driver.execute_script(js_code.strip())
            return f"βœ… JavaScript executed successfully\nResult: {str(result)[:500]}"
            
        except Exception as e:
            return f"❌ Error executing JavaScript: {str(e)}"
        finally:
            if driver:
                driver.quit()

    def kill_chrome(self):
        """Kill all Chrome processes"""
        self.is_running = False
        killed_processes = 0

        try:
            for proc in psutil.process_iter(['pid', 'name']):
                if 'chrome' in proc.info['name'].lower():
                    try:
                        proc.terminate()
                        killed_processes += 1
                    except (psutil.NoSuchProcess, psutil.AccessDenied):
                        pass
        except Exception:
            pass
        
        if self.chrome_process:
            try:
                os.killpg(os.getpgid(self.chrome_process.pid), 9)
            except:
                 pass
            self.chrome_process = None
        
        time.sleep(1)
        
        return f"πŸ›‘ Chrome processes terminated ({killed_processes} killed)"

    def get_status(self):
        """Get current Chrome status"""
        if self.is_chrome_accessible():
            return "βœ… Chrome is running and accessible on port 9222"
        else:
            return "β­• Chrome is not running or not accessible"

chrome_controller = SimpleChromeController()

def create_interface():
    with gr.Blocks(theme=gr.themes.Soft(), title="Chrome Controller") as demo:
        gr.Markdown("# 🌐 Chrome Controller Pro")
        gr.Markdown("Control a headless Chrome instance for advanced web automation.")

        with gr.Tabs():
            with gr.TabItem("πŸš€ Control & Status"):
                with gr.Row():
                    start_btn = gr.Button("πŸš€ Start Chrome", variant="primary", scale=1)
                    stop_btn = gr.Button("πŸ›‘ Stop Chrome", variant="stop", scale=1)
                    status_btn = gr.Button("πŸ“Š Check Status", scale=1)
                
                status_output = gr.Textbox(
                    label="Status", 
                    interactive=False,
                    lines=5,
                    value="Chrome is not running. Click 'Start Chrome' to begin."
                )

            with gr.TabItem("🧭 Navigate & Screenshot"):
                with gr.Row():
                    url_input = gr.Textbox(label="URL", placeholder="e.g., google.com", scale=4)
                    navigate_btn = gr.Button("🧭 Navigate", variant="primary", scale=1)
                
                screenshot_btn = gr.Button("πŸ“Έ Take Screenshot", variant="secondary")
                
                screenshot_output = gr.Image(label="Screenshot", type="pil", height=500)
                screenshot_status = gr.Textbox(label="Action Status", interactive=False)

            with gr.TabItem("⚑ Execute JavaScript"):
                js_input = gr.Textbox(
                    label="JavaScript Code",
                    lines=8,
                    value="// Example: return document.title;"
                )
                execute_btn = gr.Button("⚑ Execute JS", variant="primary")
                js_output = gr.Textbox(label="JS Execution Result", interactive=False, lines=8)
        
        # Event Handlers
        start_btn.click(chrome_controller.start_chrome, outputs=status_output)
        stop_btn.click(chrome_controller.kill_chrome, outputs=status_output)
        status_btn.click(chrome_controller.get_status, outputs=status_output)
        
        navigate_btn.click(
            chrome_controller.navigate_to_url, 
            inputs=url_input, 
            outputs=screenshot_status
        )
        screenshot_btn.click(
            chrome_controller.take_screenshot, 
            outputs=[screenshot_output, screenshot_status]
        )
        execute_btn.click(
            chrome_controller.execute_javascript,
            inputs=js_input,
            outputs=js_output
        )

    return demo

if __name__ == "__main__":
    demo = create_interface()
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=True  # Important for Hugging Face Spaces
    )