optexity-browser-use 0.9.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,461 @@
1
+ """Local browser watchdog for managing browser subprocess lifecycle."""
2
+
3
+ import asyncio
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, ClassVar
9
+
10
+ import psutil
11
+ from bubus import BaseEvent
12
+ from pydantic import PrivateAttr
13
+
14
+ from browser_use.browser.events import (
15
+ BrowserKillEvent,
16
+ BrowserLaunchEvent,
17
+ BrowserLaunchResult,
18
+ BrowserStopEvent,
19
+ )
20
+ from browser_use.browser.watchdog_base import BaseWatchdog
21
+ from browser_use.observability import observe_debug
22
+
23
+ if TYPE_CHECKING:
24
+ pass
25
+
26
+
27
+ class LocalBrowserWatchdog(BaseWatchdog):
28
+ """Manages local browser subprocess lifecycle."""
29
+
30
+ # Events this watchdog listens to
31
+ LISTENS_TO: ClassVar[list[type[BaseEvent[Any]]]] = [
32
+ BrowserLaunchEvent,
33
+ BrowserKillEvent,
34
+ BrowserStopEvent,
35
+ ]
36
+
37
+ # Events this watchdog emits
38
+ EMITS: ClassVar[list[type[BaseEvent[Any]]]] = []
39
+
40
+ # Private state for subprocess management
41
+ _subprocess: psutil.Process | None = PrivateAttr(default=None)
42
+ _owns_browser_resources: bool = PrivateAttr(default=True)
43
+ _temp_dirs_to_cleanup: list[Path] = PrivateAttr(default_factory=list)
44
+ _original_user_data_dir: str | None = PrivateAttr(default=None)
45
+
46
+ @observe_debug(ignore_input=True, ignore_output=True, name='browser_launch_event')
47
+ async def on_BrowserLaunchEvent(self, event: BrowserLaunchEvent) -> BrowserLaunchResult:
48
+ """Launch a local browser process."""
49
+
50
+ try:
51
+ self.logger.debug('[LocalBrowserWatchdog] Received BrowserLaunchEvent, launching local browser...')
52
+
53
+ # self.logger.debug('[LocalBrowserWatchdog] Calling _launch_browser...')
54
+ process, cdp_url = await self._launch_browser()
55
+ self._subprocess = process
56
+ # self.logger.debug(f'[LocalBrowserWatchdog] _launch_browser returned: process={process}, cdp_url={cdp_url}')
57
+
58
+ return BrowserLaunchResult(cdp_url=cdp_url)
59
+ except Exception as e:
60
+ self.logger.error(f'[LocalBrowserWatchdog] Exception in on_BrowserLaunchEvent: {e}', exc_info=True)
61
+ raise
62
+
63
+ async def on_BrowserKillEvent(self, event: BrowserKillEvent) -> None:
64
+ """Kill the local browser subprocess."""
65
+ self.logger.debug('[LocalBrowserWatchdog] Killing local browser process')
66
+
67
+ if self._subprocess:
68
+ await self._cleanup_process(self._subprocess)
69
+ self._subprocess = None
70
+
71
+ # Clean up temp directories if any were created
72
+ for temp_dir in self._temp_dirs_to_cleanup:
73
+ self._cleanup_temp_dir(temp_dir)
74
+ self._temp_dirs_to_cleanup.clear()
75
+
76
+ # Restore original user_data_dir if it was modified
77
+ if self._original_user_data_dir is not None:
78
+ self.browser_session.browser_profile.user_data_dir = self._original_user_data_dir
79
+ self._original_user_data_dir = None
80
+
81
+ self.logger.debug('[LocalBrowserWatchdog] Browser cleanup completed')
82
+
83
+ async def on_BrowserStopEvent(self, event: BrowserStopEvent) -> None:
84
+ """Listen for BrowserStopEvent and dispatch BrowserKillEvent without awaiting it."""
85
+ if self.browser_session.is_local and self._subprocess:
86
+ self.logger.debug('[LocalBrowserWatchdog] BrowserStopEvent received, dispatching BrowserKillEvent')
87
+ # Dispatch BrowserKillEvent without awaiting so it gets processed after all BrowserStopEvent handlers
88
+ self.event_bus.dispatch(BrowserKillEvent())
89
+
90
+ @observe_debug(ignore_input=True, ignore_output=True, name='launch_browser_process')
91
+ async def _launch_browser(self, max_retries: int = 3) -> tuple[psutil.Process, str]:
92
+ """Launch browser process and return (process, cdp_url).
93
+
94
+ Handles launch errors by falling back to temporary directories if needed.
95
+
96
+ Returns:
97
+ Tuple of (psutil.Process, cdp_url)
98
+ """
99
+ # Keep track of original user_data_dir to restore if needed
100
+ profile = self.browser_session.browser_profile
101
+ self._original_user_data_dir = str(profile.user_data_dir) if profile.user_data_dir else None
102
+ self._temp_dirs_to_cleanup = []
103
+
104
+ for attempt in range(max_retries):
105
+ try:
106
+ # Get launch args from profile
107
+ launch_args = profile.get_args()
108
+
109
+ # Add debugging port
110
+ debug_port = self._find_free_port()
111
+ launch_args.extend(
112
+ [
113
+ f'--remote-debugging-port={debug_port}',
114
+ ]
115
+ )
116
+ assert '--user-data-dir' in str(launch_args), (
117
+ 'User data dir must be set somewhere in launch args to a non-default path, otherwise Chrome will not let us attach via CDP'
118
+ )
119
+
120
+ # Get browser executable
121
+ # Priority: custom executable > fallback paths > playwright subprocess
122
+ if profile.executable_path:
123
+ browser_path = profile.executable_path
124
+ self.logger.debug(f'[LocalBrowserWatchdog] 📦 Using custom local browser executable_path= {browser_path}')
125
+ else:
126
+ # self.logger.debug('[LocalBrowserWatchdog] 🔍 Looking for local browser binary path...')
127
+ # Try fallback paths first (system browsers preferred)
128
+ browser_path = self._find_installed_browser_path()
129
+ if not browser_path:
130
+ self.logger.error(
131
+ '[LocalBrowserWatchdog] ⚠️ No local browser binary found, installing browser using playwright subprocess...'
132
+ )
133
+ browser_path = await self._install_browser_with_playwright()
134
+
135
+ self.logger.debug(f'[LocalBrowserWatchdog] 📦 Found local browser installed at executable_path= {browser_path}')
136
+ if not browser_path:
137
+ raise RuntimeError('No local Chrome/Chromium install found, and failed to install with playwright')
138
+
139
+ # Launch browser subprocess directly
140
+ self.logger.debug(f'[LocalBrowserWatchdog] 🚀 Launching browser subprocess with {len(launch_args)} args...')
141
+ self.logger.debug(
142
+ f'[LocalBrowserWatchdog] 📂 user_data_dir={profile.user_data_dir}, profile_directory={profile.profile_directory}'
143
+ )
144
+ subprocess = await asyncio.create_subprocess_exec(
145
+ browser_path,
146
+ *launch_args,
147
+ stdout=asyncio.subprocess.PIPE,
148
+ stderr=asyncio.subprocess.PIPE,
149
+ )
150
+ self.logger.debug(
151
+ f'[LocalBrowserWatchdog] 🎭 Browser running with browser_pid= {subprocess.pid} 🔗 listening on CDP port :{debug_port}'
152
+ )
153
+
154
+ # Convert to psutil.Process
155
+ process = psutil.Process(subprocess.pid)
156
+
157
+ # Wait for CDP to be ready and get the URL
158
+ cdp_url = await self._wait_for_cdp_url(debug_port)
159
+
160
+ # Success! Clean up any temp dirs we created but didn't use
161
+ for tmp_dir in self._temp_dirs_to_cleanup:
162
+ try:
163
+ shutil.rmtree(tmp_dir, ignore_errors=True)
164
+ except Exception:
165
+ pass
166
+
167
+ return process, cdp_url
168
+
169
+ except Exception as e:
170
+ error_str = str(e).lower()
171
+
172
+ # Check if this is a user_data_dir related error
173
+ if any(err in error_str for err in ['singletonlock', 'user data directory', 'cannot create', 'already in use']):
174
+ self.logger.warning(f'Browser launch failed (attempt {attempt + 1}/{max_retries}): {e}')
175
+
176
+ if attempt < max_retries - 1:
177
+ # Create a temporary directory for next attempt
178
+ tmp_dir = Path(tempfile.mkdtemp(prefix='browseruse-tmp-'))
179
+ self._temp_dirs_to_cleanup.append(tmp_dir)
180
+
181
+ # Update profile to use temp directory
182
+ profile.user_data_dir = str(tmp_dir)
183
+ self.logger.debug(f'Retrying with temporary user_data_dir: {tmp_dir}')
184
+
185
+ # Small delay before retry
186
+ await asyncio.sleep(0.5)
187
+ continue
188
+
189
+ # Not a recoverable error or last attempt failed
190
+ # Restore original user_data_dir before raising
191
+ if self._original_user_data_dir is not None:
192
+ profile.user_data_dir = self._original_user_data_dir
193
+
194
+ # Clean up any temp dirs we created
195
+ for tmp_dir in self._temp_dirs_to_cleanup:
196
+ try:
197
+ shutil.rmtree(tmp_dir, ignore_errors=True)
198
+ except Exception:
199
+ pass
200
+
201
+ raise
202
+
203
+ # Should not reach here, but just in case
204
+ if self._original_user_data_dir is not None:
205
+ profile.user_data_dir = self._original_user_data_dir
206
+ raise RuntimeError(f'Failed to launch browser after {max_retries} attempts')
207
+
208
+ @staticmethod
209
+ def _find_installed_browser_path() -> str | None:
210
+ """Try to find browser executable from common fallback locations.
211
+
212
+ Prioritizes:
213
+ 1. System Chrome Stable
214
+ 1. Playwright chromium
215
+ 2. Other system native browsers (Chromium -> Chrome Canary/Dev -> Brave)
216
+ 3. Playwright headless-shell fallback
217
+
218
+ Returns:
219
+ Path to browser executable or None if not found
220
+ """
221
+ import glob
222
+ import platform
223
+ from pathlib import Path
224
+
225
+ system = platform.system()
226
+ patterns = []
227
+
228
+ # Get playwright browsers path from environment variable if set
229
+ playwright_path = os.environ.get('PLAYWRIGHT_BROWSERS_PATH')
230
+
231
+ if system == 'Darwin': # macOS
232
+ if not playwright_path:
233
+ playwright_path = '~/Library/Caches/ms-playwright'
234
+ patterns = [
235
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
236
+ f'{playwright_path}/chromium-*/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
237
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
238
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
239
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
240
+ f'{playwright_path}/chromium_headless_shell-*/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
241
+ ]
242
+ elif system == 'Linux':
243
+ if not playwright_path:
244
+ playwright_path = '~/.cache/ms-playwright'
245
+ patterns = [
246
+ '/usr/bin/google-chrome-stable',
247
+ '/usr/bin/google-chrome',
248
+ '/usr/local/bin/google-chrome',
249
+ f'{playwright_path}/chromium-*/chrome-linux/chrome',
250
+ '/usr/bin/chromium',
251
+ '/usr/bin/chromium-browser',
252
+ '/usr/local/bin/chromium',
253
+ '/snap/bin/chromium',
254
+ '/usr/bin/google-chrome-beta',
255
+ '/usr/bin/google-chrome-dev',
256
+ '/usr/bin/brave-browser',
257
+ f'{playwright_path}/chromium_headless_shell-*/chrome-linux/chrome',
258
+ ]
259
+ elif system == 'Windows':
260
+ if not playwright_path:
261
+ playwright_path = r'%LOCALAPPDATA%\ms-playwright'
262
+ patterns = [
263
+ r'C:\Program Files\Google\Chrome\Application\chrome.exe',
264
+ r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
265
+ r'%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe',
266
+ r'%PROGRAMFILES%\Google\Chrome\Application\chrome.exe',
267
+ r'%PROGRAMFILES(X86)%\Google\Chrome\Application\chrome.exe',
268
+ f'{playwright_path}\\chromium-*\\chrome-win\\chrome.exe',
269
+ r'C:\Program Files\Chromium\Application\chrome.exe',
270
+ r'C:\Program Files (x86)\Chromium\Application\chrome.exe',
271
+ r'%LOCALAPPDATA%\Chromium\Application\chrome.exe',
272
+ r'C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe',
273
+ r'C:\Program Files (x86)\BraveSoftware\Brave-Browser\Application\brave.exe',
274
+ r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
275
+ r'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
276
+ r'%LOCALAPPDATA%\Microsoft\Edge\Application\msedge.exe',
277
+ f'{playwright_path}\\chromium_headless_shell-*\\chrome-win\\chrome.exe',
278
+ ]
279
+
280
+ for pattern in patterns:
281
+ # Expand user home directory
282
+ expanded_pattern = Path(pattern).expanduser()
283
+
284
+ # Handle Windows environment variables
285
+ if system == 'Windows':
286
+ pattern_str = str(expanded_pattern)
287
+ for env_var in ['%LOCALAPPDATA%', '%PROGRAMFILES%', '%PROGRAMFILES(X86)%']:
288
+ if env_var in pattern_str:
289
+ env_key = env_var.strip('%').replace('(X86)', ' (x86)')
290
+ env_value = os.environ.get(env_key, '')
291
+ if env_value:
292
+ pattern_str = pattern_str.replace(env_var, env_value)
293
+ expanded_pattern = Path(pattern_str)
294
+
295
+ # Convert to string for glob
296
+ pattern_str = str(expanded_pattern)
297
+
298
+ # Check if pattern contains wildcards
299
+ if '*' in pattern_str:
300
+ # Use glob to expand the pattern
301
+ matches = glob.glob(pattern_str)
302
+ if matches:
303
+ # Sort matches and take the last one (alphanumerically highest version)
304
+ matches.sort()
305
+ browser_path = matches[-1]
306
+ if Path(browser_path).exists() and Path(browser_path).is_file():
307
+ return browser_path
308
+ else:
309
+ # Direct path check
310
+ if expanded_pattern.exists() and expanded_pattern.is_file():
311
+ return str(expanded_pattern)
312
+
313
+ return None
314
+
315
+ async def _install_browser_with_playwright(self) -> str:
316
+ """Get browser executable path from playwright in a subprocess to avoid thread issues."""
317
+ import platform
318
+
319
+ # Build command - only use --with-deps on Linux (it fails on Windows/macOS)
320
+ cmd = ['uvx', 'playwright', 'install', 'chrome']
321
+ if platform.system() == 'Linux':
322
+ cmd.append('--with-deps')
323
+
324
+ # Run in subprocess with timeout
325
+ process = await asyncio.create_subprocess_exec(
326
+ *cmd,
327
+ stdout=asyncio.subprocess.PIPE,
328
+ stderr=asyncio.subprocess.PIPE,
329
+ )
330
+
331
+ try:
332
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=60.0)
333
+ self.logger.debug(f'[LocalBrowserWatchdog] 📦 Playwright install output: {stdout}')
334
+ browser_path = self._find_installed_browser_path()
335
+ if browser_path:
336
+ return browser_path
337
+ self.logger.error(f'[LocalBrowserWatchdog] ❌ Playwright local browser installation error: \n{stdout}\n{stderr}')
338
+ raise RuntimeError('No local browser path found after: uvx playwright install chrome')
339
+ except TimeoutError:
340
+ # Kill the subprocess if it times out
341
+ process.kill()
342
+ await process.wait()
343
+ raise RuntimeError('Timeout getting browser path from playwright')
344
+ except Exception as e:
345
+ # Make sure subprocess is terminated
346
+ if process.returncode is None:
347
+ process.kill()
348
+ await process.wait()
349
+ raise RuntimeError(f'Error getting browser path: {e}')
350
+
351
+ @staticmethod
352
+ def _find_free_port() -> int:
353
+ """Find a free port for the debugging interface."""
354
+ import socket
355
+
356
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
357
+ s.bind(('127.0.0.1', 0))
358
+ s.listen(1)
359
+ port = s.getsockname()[1]
360
+ return port
361
+
362
+ @staticmethod
363
+ async def _wait_for_cdp_url(port: int, timeout: float = 30) -> str:
364
+ """Wait for the browser to start and return the CDP URL."""
365
+ import aiohttp
366
+
367
+ start_time = asyncio.get_event_loop().time()
368
+
369
+ while asyncio.get_event_loop().time() - start_time < timeout:
370
+ try:
371
+ async with aiohttp.ClientSession() as session:
372
+ async with session.get(f'http://localhost:{port}/json/version') as resp:
373
+ if resp.status == 200:
374
+ # Chrome is ready
375
+ return f'http://localhost:{port}/'
376
+ else:
377
+ # Chrome is starting up and returning 502/500 errors
378
+ await asyncio.sleep(0.1)
379
+ except Exception:
380
+ # Connection error - Chrome might not be ready yet
381
+ await asyncio.sleep(0.1)
382
+
383
+ raise TimeoutError(f'Browser did not start within {timeout} seconds')
384
+
385
+ @staticmethod
386
+ async def _cleanup_process(process: psutil.Process) -> None:
387
+ """Clean up browser process.
388
+
389
+ Args:
390
+ process: psutil.Process to terminate
391
+ """
392
+ if not process:
393
+ return
394
+
395
+ try:
396
+ # Try graceful shutdown first
397
+ process.terminate()
398
+
399
+ # Use async wait instead of blocking wait
400
+ for _ in range(50): # Wait up to 5 seconds (50 * 0.1)
401
+ if not process.is_running():
402
+ return
403
+ await asyncio.sleep(0.1)
404
+
405
+ # If still running after 5 seconds, force kill
406
+ if process.is_running():
407
+ process.kill()
408
+ # Give it a moment to die
409
+ await asyncio.sleep(0.1)
410
+
411
+ except psutil.NoSuchProcess:
412
+ # Process already gone
413
+ pass
414
+ except Exception:
415
+ # Ignore any other errors during cleanup
416
+ pass
417
+
418
+ def _cleanup_temp_dir(self, temp_dir: Path | str) -> None:
419
+ """Clean up temporary directory.
420
+
421
+ Args:
422
+ temp_dir: Path to temporary directory to remove
423
+ """
424
+ if not temp_dir:
425
+ return
426
+
427
+ try:
428
+ temp_path = Path(temp_dir)
429
+ # Only remove if it's actually a temp directory we created
430
+ if 'browseruse-tmp-' in str(temp_path):
431
+ shutil.rmtree(temp_path, ignore_errors=True)
432
+ except Exception as e:
433
+ self.logger.debug(f'Failed to cleanup temp dir {temp_dir}: {e}')
434
+
435
+ @property
436
+ def browser_pid(self) -> int | None:
437
+ """Get the browser process ID."""
438
+ if self._subprocess:
439
+ return self._subprocess.pid
440
+ return None
441
+
442
+ @staticmethod
443
+ async def get_browser_pid_via_cdp(browser) -> int | None:
444
+ """Get the browser process ID via CDP SystemInfo.getProcessInfo.
445
+
446
+ Args:
447
+ browser: Playwright Browser instance
448
+
449
+ Returns:
450
+ Process ID or None if failed
451
+ """
452
+ try:
453
+ cdp_session = await browser.new_browser_cdp_session()
454
+ result = await cdp_session.send('SystemInfo.getProcessInfo')
455
+ process_info = result.get('processInfo', {})
456
+ pid = process_info.get('id')
457
+ await cdp_session.detach()
458
+ return pid
459
+ except Exception:
460
+ # If we can't get PID via CDP, it's not critical
461
+ return None
@@ -0,0 +1,43 @@
1
+ """Permissions watchdog for granting browser permissions on connection."""
2
+
3
+ from typing import TYPE_CHECKING, ClassVar
4
+
5
+ from bubus import BaseEvent
6
+
7
+ from browser_use.browser.events import BrowserConnectedEvent
8
+ from browser_use.browser.watchdog_base import BaseWatchdog
9
+
10
+ if TYPE_CHECKING:
11
+ pass
12
+
13
+
14
+ class PermissionsWatchdog(BaseWatchdog):
15
+ """Grants browser permissions when browser connects."""
16
+
17
+ # Event contracts
18
+ LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [
19
+ BrowserConnectedEvent,
20
+ ]
21
+ EMITS: ClassVar[list[type[BaseEvent]]] = []
22
+
23
+ async def on_BrowserConnectedEvent(self, event: BrowserConnectedEvent) -> None:
24
+ """Grant permissions when browser connects."""
25
+ permissions = self.browser_session.browser_profile.permissions
26
+
27
+ if not permissions:
28
+ self.logger.debug('No permissions to grant')
29
+ return
30
+
31
+ self.logger.debug(f'🔓 Granting browser permissions: {permissions}')
32
+
33
+ try:
34
+ # Grant permissions using CDP Browser.grantPermissions
35
+ # origin=None means grant to all origins
36
+ # Browser domain commands don't use session_id
37
+ await self.browser_session.cdp_client.send.Browser.grantPermissions(
38
+ params={'permissions': permissions} # type: ignore
39
+ )
40
+ self.logger.debug(f'✅ Successfully granted permissions: {permissions}')
41
+ except Exception as e:
42
+ self.logger.error(f'❌ Failed to grant permissions: {str(e)}')
43
+ # Don't raise - permissions are not critical to browser operation
@@ -0,0 +1,143 @@
1
+ """Watchdog for handling JavaScript dialogs (alert, confirm, prompt) automatically."""
2
+
3
+ import asyncio
4
+ from typing import ClassVar
5
+
6
+ from bubus import BaseEvent
7
+ from pydantic import PrivateAttr
8
+
9
+ from browser_use.browser.events import TabCreatedEvent
10
+ from browser_use.browser.watchdog_base import BaseWatchdog
11
+
12
+
13
+ class PopupsWatchdog(BaseWatchdog):
14
+ """Handles JavaScript dialogs (alert, confirm, prompt) by automatically accepting them immediately."""
15
+
16
+ # Events this watchdog listens to and emits
17
+ LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [TabCreatedEvent]
18
+ EMITS: ClassVar[list[type[BaseEvent]]] = []
19
+
20
+ # Track which targets have dialog handlers registered
21
+ _dialog_listeners_registered: set[str] = PrivateAttr(default_factory=set)
22
+
23
+ def __init__(self, **kwargs):
24
+ super().__init__(**kwargs)
25
+ self.logger.debug(f'🚀 PopupsWatchdog initialized with browser_session={self.browser_session}, ID={id(self)}')
26
+
27
+ async def on_TabCreatedEvent(self, event: TabCreatedEvent) -> None:
28
+ """Set up JavaScript dialog handling when a new tab is created."""
29
+ target_id = event.target_id
30
+ self.logger.debug(f'🎯 PopupsWatchdog received TabCreatedEvent for target {target_id}')
31
+
32
+ # Skip if we've already registered for this target
33
+ if target_id in self._dialog_listeners_registered:
34
+ self.logger.debug(f'Already registered dialog handlers for target {target_id}')
35
+ return
36
+
37
+ self.logger.debug(f'📌 Starting dialog handler setup for target {target_id}')
38
+ try:
39
+ # Get all CDP sessions for this target and any child frames
40
+ cdp_session = await self.browser_session.get_or_create_cdp_session(
41
+ target_id, focus=False
42
+ ) # don't auto-focus new tabs! sometimes we need to open tabs in background
43
+
44
+ # CRITICAL: Enable Page domain to receive dialog events
45
+ try:
46
+ await cdp_session.cdp_client.send.Page.enable(session_id=cdp_session.session_id)
47
+ self.logger.debug(f'✅ Enabled Page domain for session {cdp_session.session_id[-8:]}')
48
+ except Exception as e:
49
+ self.logger.debug(f'Failed to enable Page domain: {e}')
50
+
51
+ # Also register for the root CDP client to catch dialogs from any frame
52
+ if self.browser_session._cdp_client_root:
53
+ self.logger.debug('📌 Also registering handler on root CDP client')
54
+ try:
55
+ # Enable Page domain on root client too
56
+ await self.browser_session._cdp_client_root.send.Page.enable()
57
+ self.logger.debug('✅ Enabled Page domain on root CDP client')
58
+ except Exception as e:
59
+ self.logger.debug(f'Failed to enable Page domain on root: {e}')
60
+
61
+ # Set up async handler for JavaScript dialogs - accept immediately without event dispatch
62
+ async def handle_dialog(event_data, session_id: str | None = None):
63
+ """Handle JavaScript dialog events - accept immediately."""
64
+ try:
65
+ dialog_type = event_data.get('type', 'alert')
66
+ message = event_data.get('message', '')
67
+
68
+ # Store the popup message in browser session for inclusion in browser state
69
+ if message:
70
+ formatted_message = f'[{dialog_type}] {message}'
71
+ self.browser_session._closed_popup_messages.append(formatted_message)
72
+ self.logger.debug(f'📝 Stored popup message: {formatted_message[:100]}')
73
+
74
+ # Choose action based on dialog type:
75
+ # - alert: accept=true (click OK to dismiss)
76
+ # - confirm: accept=true (click OK to proceed - safer for automation)
77
+ # - prompt: accept=false (click Cancel since we can't provide input)
78
+ # - beforeunload: accept=true (allow navigation)
79
+ should_accept = dialog_type in ('alert', 'confirm', 'beforeunload')
80
+
81
+ action_str = 'accepting (OK)' if should_accept else 'dismissing (Cancel)'
82
+ self.logger.info(f"🔔 JavaScript {dialog_type} dialog: '{message[:100]}' - {action_str}...")
83
+
84
+ dismissed = False
85
+
86
+ # Approach 1: Use the session that detected the dialog (most reliable)
87
+ if self.browser_session._cdp_client_root and session_id:
88
+ try:
89
+ self.logger.debug(f'🔄 Approach 1: Using detecting session {session_id[-8:]}')
90
+ await asyncio.wait_for(
91
+ self.browser_session._cdp_client_root.send.Page.handleJavaScriptDialog(
92
+ params={'accept': should_accept},
93
+ session_id=session_id,
94
+ ),
95
+ timeout=0.5,
96
+ )
97
+ dismissed = True
98
+ self.logger.info('✅ Dialog handled successfully via detecting session')
99
+ except (TimeoutError, Exception) as e:
100
+ self.logger.debug(f'Approach 1 failed: {type(e).__name__}')
101
+
102
+ # Approach 2: Try with current agent focus session
103
+ if not dismissed and self.browser_session._cdp_client_root and self.browser_session.agent_focus:
104
+ try:
105
+ self.logger.debug(
106
+ f'🔄 Approach 2: Using agent focus session {self.browser_session.agent_focus.session_id[-8:]}'
107
+ )
108
+ await asyncio.wait_for(
109
+ self.browser_session._cdp_client_root.send.Page.handleJavaScriptDialog(
110
+ params={'accept': should_accept},
111
+ session_id=self.browser_session.agent_focus.session_id,
112
+ ),
113
+ timeout=0.5,
114
+ )
115
+ dismissed = True
116
+ self.logger.info('✅ Dialog handled successfully via agent focus session')
117
+ except (TimeoutError, Exception) as e:
118
+ self.logger.debug(f'Approach 2 failed: {type(e).__name__}')
119
+
120
+ except Exception as e:
121
+ self.logger.error(f'❌ Critical error in dialog handler: {type(e).__name__}: {e}')
122
+
123
+ # Register handler on the specific session
124
+ cdp_session.cdp_client.register.Page.javascriptDialogOpening(handle_dialog) # type: ignore[arg-type]
125
+ self.logger.debug(
126
+ f'Successfully registered Page.javascriptDialogOpening handler for session {cdp_session.session_id}'
127
+ )
128
+
129
+ # Also register on root CDP client to catch dialogs from any frame
130
+ if hasattr(self.browser_session._cdp_client_root, 'register'):
131
+ try:
132
+ self.browser_session._cdp_client_root.register.Page.javascriptDialogOpening(handle_dialog) # type: ignore[arg-type]
133
+ self.logger.debug('Successfully registered dialog handler on root CDP client for all frames')
134
+ except Exception as root_error:
135
+ self.logger.warning(f'Failed to register on root CDP client: {root_error}')
136
+
137
+ # Mark this target as having dialog handling set up
138
+ self._dialog_listeners_registered.add(target_id)
139
+
140
+ self.logger.debug(f'Set up JavaScript dialog handling for tab {target_id}')
141
+
142
+ except Exception as e:
143
+ self.logger.warning(f'Failed to set up popup handling for tab {target_id}: {e}')