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.
- browser_use/__init__.py +157 -0
- browser_use/actor/__init__.py +11 -0
- browser_use/actor/element.py +1175 -0
- browser_use/actor/mouse.py +134 -0
- browser_use/actor/page.py +561 -0
- browser_use/actor/playground/flights.py +41 -0
- browser_use/actor/playground/mixed_automation.py +54 -0
- browser_use/actor/playground/playground.py +236 -0
- browser_use/actor/utils.py +176 -0
- browser_use/agent/cloud_events.py +282 -0
- browser_use/agent/gif.py +424 -0
- browser_use/agent/judge.py +170 -0
- browser_use/agent/message_manager/service.py +473 -0
- browser_use/agent/message_manager/utils.py +52 -0
- browser_use/agent/message_manager/views.py +98 -0
- browser_use/agent/prompts.py +413 -0
- browser_use/agent/service.py +2316 -0
- browser_use/agent/system_prompt.md +185 -0
- browser_use/agent/system_prompt_flash.md +10 -0
- browser_use/agent/system_prompt_no_thinking.md +183 -0
- browser_use/agent/views.py +743 -0
- browser_use/browser/__init__.py +41 -0
- browser_use/browser/cloud/cloud.py +203 -0
- browser_use/browser/cloud/views.py +89 -0
- browser_use/browser/events.py +578 -0
- browser_use/browser/profile.py +1158 -0
- browser_use/browser/python_highlights.py +548 -0
- browser_use/browser/session.py +3225 -0
- browser_use/browser/session_manager.py +399 -0
- browser_use/browser/video_recorder.py +162 -0
- browser_use/browser/views.py +200 -0
- browser_use/browser/watchdog_base.py +260 -0
- browser_use/browser/watchdogs/__init__.py +0 -0
- browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
- browser_use/browser/watchdogs/crash_watchdog.py +335 -0
- browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
- browser_use/browser/watchdogs/dom_watchdog.py +817 -0
- browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
- browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
- browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
- browser_use/browser/watchdogs/popups_watchdog.py +143 -0
- browser_use/browser/watchdogs/recording_watchdog.py +126 -0
- browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
- browser_use/browser/watchdogs/security_watchdog.py +280 -0
- browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
- browser_use/cli.py +2359 -0
- browser_use/code_use/__init__.py +16 -0
- browser_use/code_use/formatting.py +192 -0
- browser_use/code_use/namespace.py +665 -0
- browser_use/code_use/notebook_export.py +276 -0
- browser_use/code_use/service.py +1340 -0
- browser_use/code_use/system_prompt.md +574 -0
- browser_use/code_use/utils.py +150 -0
- browser_use/code_use/views.py +171 -0
- browser_use/config.py +505 -0
- browser_use/controller/__init__.py +3 -0
- browser_use/dom/enhanced_snapshot.py +161 -0
- browser_use/dom/markdown_extractor.py +169 -0
- browser_use/dom/playground/extraction.py +312 -0
- browser_use/dom/playground/multi_act.py +32 -0
- browser_use/dom/serializer/clickable_elements.py +200 -0
- browser_use/dom/serializer/code_use_serializer.py +287 -0
- browser_use/dom/serializer/eval_serializer.py +478 -0
- browser_use/dom/serializer/html_serializer.py +212 -0
- browser_use/dom/serializer/paint_order.py +197 -0
- browser_use/dom/serializer/serializer.py +1170 -0
- browser_use/dom/service.py +825 -0
- browser_use/dom/utils.py +129 -0
- browser_use/dom/views.py +906 -0
- browser_use/exceptions.py +5 -0
- browser_use/filesystem/__init__.py +0 -0
- browser_use/filesystem/file_system.py +619 -0
- browser_use/init_cmd.py +376 -0
- browser_use/integrations/gmail/__init__.py +24 -0
- browser_use/integrations/gmail/actions.py +115 -0
- browser_use/integrations/gmail/service.py +225 -0
- browser_use/llm/__init__.py +155 -0
- browser_use/llm/anthropic/chat.py +242 -0
- browser_use/llm/anthropic/serializer.py +312 -0
- browser_use/llm/aws/__init__.py +36 -0
- browser_use/llm/aws/chat_anthropic.py +242 -0
- browser_use/llm/aws/chat_bedrock.py +289 -0
- browser_use/llm/aws/serializer.py +257 -0
- browser_use/llm/azure/chat.py +91 -0
- browser_use/llm/base.py +57 -0
- browser_use/llm/browser_use/__init__.py +3 -0
- browser_use/llm/browser_use/chat.py +201 -0
- browser_use/llm/cerebras/chat.py +193 -0
- browser_use/llm/cerebras/serializer.py +109 -0
- browser_use/llm/deepseek/chat.py +212 -0
- browser_use/llm/deepseek/serializer.py +109 -0
- browser_use/llm/exceptions.py +29 -0
- browser_use/llm/google/__init__.py +3 -0
- browser_use/llm/google/chat.py +542 -0
- browser_use/llm/google/serializer.py +120 -0
- browser_use/llm/groq/chat.py +229 -0
- browser_use/llm/groq/parser.py +158 -0
- browser_use/llm/groq/serializer.py +159 -0
- browser_use/llm/messages.py +238 -0
- browser_use/llm/models.py +271 -0
- browser_use/llm/oci_raw/__init__.py +10 -0
- browser_use/llm/oci_raw/chat.py +443 -0
- browser_use/llm/oci_raw/serializer.py +229 -0
- browser_use/llm/ollama/chat.py +97 -0
- browser_use/llm/ollama/serializer.py +143 -0
- browser_use/llm/openai/chat.py +264 -0
- browser_use/llm/openai/like.py +15 -0
- browser_use/llm/openai/serializer.py +165 -0
- browser_use/llm/openrouter/chat.py +211 -0
- browser_use/llm/openrouter/serializer.py +26 -0
- browser_use/llm/schema.py +176 -0
- browser_use/llm/views.py +48 -0
- browser_use/logging_config.py +330 -0
- browser_use/mcp/__init__.py +18 -0
- browser_use/mcp/__main__.py +12 -0
- browser_use/mcp/client.py +544 -0
- browser_use/mcp/controller.py +264 -0
- browser_use/mcp/server.py +1114 -0
- browser_use/observability.py +204 -0
- browser_use/py.typed +0 -0
- browser_use/sandbox/__init__.py +41 -0
- browser_use/sandbox/sandbox.py +637 -0
- browser_use/sandbox/views.py +132 -0
- browser_use/screenshots/__init__.py +1 -0
- browser_use/screenshots/service.py +52 -0
- browser_use/sync/__init__.py +6 -0
- browser_use/sync/auth.py +357 -0
- browser_use/sync/service.py +161 -0
- browser_use/telemetry/__init__.py +51 -0
- browser_use/telemetry/service.py +112 -0
- browser_use/telemetry/views.py +101 -0
- browser_use/tokens/__init__.py +0 -0
- browser_use/tokens/custom_pricing.py +24 -0
- browser_use/tokens/mappings.py +4 -0
- browser_use/tokens/service.py +580 -0
- browser_use/tokens/views.py +108 -0
- browser_use/tools/registry/service.py +572 -0
- browser_use/tools/registry/views.py +174 -0
- browser_use/tools/service.py +1675 -0
- browser_use/tools/utils.py +82 -0
- browser_use/tools/views.py +100 -0
- browser_use/utils.py +670 -0
- optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
- optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
- optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
- optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
- optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""About:blank watchdog for managing about:blank tabs with DVD screensaver."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
4
|
+
|
|
5
|
+
from bubus import BaseEvent
|
|
6
|
+
from cdp_use.cdp.target import TargetID
|
|
7
|
+
from pydantic import PrivateAttr
|
|
8
|
+
|
|
9
|
+
from browser_use.browser.events import (
|
|
10
|
+
AboutBlankDVDScreensaverShownEvent,
|
|
11
|
+
BrowserStopEvent,
|
|
12
|
+
BrowserStoppedEvent,
|
|
13
|
+
CloseTabEvent,
|
|
14
|
+
NavigateToUrlEvent,
|
|
15
|
+
TabClosedEvent,
|
|
16
|
+
TabCreatedEvent,
|
|
17
|
+
)
|
|
18
|
+
from browser_use.browser.watchdog_base import BaseWatchdog
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AboutBlankWatchdog(BaseWatchdog):
|
|
25
|
+
"""Ensures there's always exactly one about:blank tab with DVD screensaver."""
|
|
26
|
+
|
|
27
|
+
# Event contracts
|
|
28
|
+
LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [
|
|
29
|
+
BrowserStopEvent,
|
|
30
|
+
BrowserStoppedEvent,
|
|
31
|
+
TabCreatedEvent,
|
|
32
|
+
TabClosedEvent,
|
|
33
|
+
]
|
|
34
|
+
EMITS: ClassVar[list[type[BaseEvent]]] = [
|
|
35
|
+
NavigateToUrlEvent,
|
|
36
|
+
CloseTabEvent,
|
|
37
|
+
AboutBlankDVDScreensaverShownEvent,
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_stopping: bool = PrivateAttr(default=False)
|
|
41
|
+
|
|
42
|
+
async def on_BrowserStopEvent(self, event: BrowserStopEvent) -> None:
|
|
43
|
+
"""Handle browser stop request - stop creating new tabs."""
|
|
44
|
+
# logger.info('[AboutBlankWatchdog] Browser stop requested, stopping tab creation')
|
|
45
|
+
self._stopping = True
|
|
46
|
+
|
|
47
|
+
async def on_BrowserStoppedEvent(self, event: BrowserStoppedEvent) -> None:
|
|
48
|
+
"""Handle browser stopped event."""
|
|
49
|
+
# logger.info('[AboutBlankWatchdog] Browser stopped')
|
|
50
|
+
self._stopping = True
|
|
51
|
+
|
|
52
|
+
async def on_TabCreatedEvent(self, event: TabCreatedEvent) -> None:
|
|
53
|
+
"""Check tabs when a new tab is created."""
|
|
54
|
+
# logger.debug(f'[AboutBlankWatchdog] ➕ New tab created: {event.url}')
|
|
55
|
+
|
|
56
|
+
# If an about:blank tab was created, show DVD screensaver on all about:blank tabs
|
|
57
|
+
if event.url == 'about:blank':
|
|
58
|
+
await self._show_dvd_screensaver_on_about_blank_tabs()
|
|
59
|
+
|
|
60
|
+
async def on_TabClosedEvent(self, event: TabClosedEvent) -> None:
|
|
61
|
+
"""Check tabs when a tab is closed and proactively create about:blank if needed."""
|
|
62
|
+
# logger.debug('[AboutBlankWatchdog] Tab closing, checking if we need to create about:blank tab')
|
|
63
|
+
|
|
64
|
+
# Don't create new tabs if browser is shutting down
|
|
65
|
+
if self._stopping:
|
|
66
|
+
# logger.debug('[AboutBlankWatchdog] Browser is stopping, not creating new tabs')
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Check if we're about to close the last tab (event happens BEFORE tab closes)
|
|
70
|
+
# Use _cdp_get_all_pages for quick check without fetching titles
|
|
71
|
+
page_targets = await self.browser_session._cdp_get_all_pages()
|
|
72
|
+
if len(page_targets) <= 1:
|
|
73
|
+
self.logger.debug(
|
|
74
|
+
'[AboutBlankWatchdog] Last tab closing, creating new about:blank tab to avoid closing entire browser'
|
|
75
|
+
)
|
|
76
|
+
# Create the animation tab since no tabs should remain
|
|
77
|
+
navigate_event = self.event_bus.dispatch(NavigateToUrlEvent(url='about:blank', new_tab=True))
|
|
78
|
+
await navigate_event
|
|
79
|
+
# Show DVD screensaver on the new tab
|
|
80
|
+
await self._show_dvd_screensaver_on_about_blank_tabs()
|
|
81
|
+
else:
|
|
82
|
+
# Multiple tabs exist, check after close
|
|
83
|
+
await self._check_and_ensure_about_blank_tab()
|
|
84
|
+
|
|
85
|
+
async def attach_to_target(self, target_id: TargetID) -> None:
|
|
86
|
+
"""AboutBlankWatchdog doesn't monitor individual targets."""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
async def _check_and_ensure_about_blank_tab(self) -> None:
|
|
90
|
+
"""Check current tabs and ensure exactly one about:blank tab with animation exists."""
|
|
91
|
+
try:
|
|
92
|
+
# For quick checks, just get page targets without titles to reduce noise
|
|
93
|
+
page_targets = await self.browser_session._cdp_get_all_pages()
|
|
94
|
+
|
|
95
|
+
# If no tabs exist at all, create one to keep browser alive
|
|
96
|
+
if len(page_targets) == 0:
|
|
97
|
+
# Only create a new tab if there are no tabs at all
|
|
98
|
+
self.logger.debug('[AboutBlankWatchdog] No tabs exist, creating new about:blank DVD screensaver tab')
|
|
99
|
+
navigate_event = self.event_bus.dispatch(NavigateToUrlEvent(url='about:blank', new_tab=True))
|
|
100
|
+
await navigate_event
|
|
101
|
+
# Show DVD screensaver on the new tab
|
|
102
|
+
await self._show_dvd_screensaver_on_about_blank_tabs()
|
|
103
|
+
# Otherwise there are tabs, don't create new ones to avoid interfering
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
self.logger.error(f'[AboutBlankWatchdog] Error ensuring about:blank tab: {e}')
|
|
107
|
+
|
|
108
|
+
async def _show_dvd_screensaver_on_about_blank_tabs(self) -> None:
|
|
109
|
+
"""Show DVD screensaver on all about:blank pages only."""
|
|
110
|
+
try:
|
|
111
|
+
# Get just the page targets without expensive title fetching
|
|
112
|
+
page_targets = await self.browser_session._cdp_get_all_pages()
|
|
113
|
+
browser_session_label = str(self.browser_session.id)[-4:]
|
|
114
|
+
|
|
115
|
+
for page_target in page_targets:
|
|
116
|
+
target_id = page_target['targetId']
|
|
117
|
+
url = page_target['url']
|
|
118
|
+
|
|
119
|
+
# Only target about:blank pages specifically
|
|
120
|
+
if url == 'about:blank':
|
|
121
|
+
await self._show_dvd_screensaver_loading_animation_cdp(target_id, browser_session_label)
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
self.logger.error(f'[AboutBlankWatchdog] Error showing DVD screensaver: {e}')
|
|
125
|
+
|
|
126
|
+
async def _show_dvd_screensaver_loading_animation_cdp(self, target_id: TargetID, browser_session_label: str) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Injects a DVD screensaver-style bouncing logo loading animation overlay into the target using CDP.
|
|
129
|
+
This is used to visually indicate that the browser is setting up or waiting.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
# Create temporary session for this target without switching focus
|
|
133
|
+
temp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)
|
|
134
|
+
|
|
135
|
+
# Inject the DVD screensaver script (from main branch with idempotency added)
|
|
136
|
+
script = f"""
|
|
137
|
+
(function(browser_session_label) {{
|
|
138
|
+
// Idempotency check
|
|
139
|
+
if (window.__dvdAnimationRunning) {{
|
|
140
|
+
return; // Already running, don't add another
|
|
141
|
+
}}
|
|
142
|
+
window.__dvdAnimationRunning = true;
|
|
143
|
+
|
|
144
|
+
// Ensure document.body exists before proceeding
|
|
145
|
+
if (!document.body) {{
|
|
146
|
+
// Try again after DOM is ready
|
|
147
|
+
window.__dvdAnimationRunning = false; // Reset flag to retry
|
|
148
|
+
if (document.readyState === 'loading') {{
|
|
149
|
+
document.addEventListener('DOMContentLoaded', () => arguments.callee(browser_session_label));
|
|
150
|
+
}}
|
|
151
|
+
return;
|
|
152
|
+
}}
|
|
153
|
+
|
|
154
|
+
const animated_title = `Starting agent ${{browser_session_label}}...`;
|
|
155
|
+
if (document.title === animated_title) {{
|
|
156
|
+
return; // already run on this tab, dont run again
|
|
157
|
+
}}
|
|
158
|
+
document.title = animated_title;
|
|
159
|
+
|
|
160
|
+
// Create the main overlay
|
|
161
|
+
const loadingOverlay = document.createElement('div');
|
|
162
|
+
loadingOverlay.id = 'pretty-loading-animation';
|
|
163
|
+
loadingOverlay.style.position = 'fixed';
|
|
164
|
+
loadingOverlay.style.top = '0';
|
|
165
|
+
loadingOverlay.style.left = '0';
|
|
166
|
+
loadingOverlay.style.width = '100vw';
|
|
167
|
+
loadingOverlay.style.height = '100vh';
|
|
168
|
+
loadingOverlay.style.background = '#000';
|
|
169
|
+
loadingOverlay.style.zIndex = '99999';
|
|
170
|
+
loadingOverlay.style.overflow = 'hidden';
|
|
171
|
+
|
|
172
|
+
// Create the image element
|
|
173
|
+
const img = document.createElement('img');
|
|
174
|
+
img.src = 'https://cf.browser-use.com/logo.svg';
|
|
175
|
+
img.alt = 'Browser-Use';
|
|
176
|
+
img.style.width = '200px';
|
|
177
|
+
img.style.height = 'auto';
|
|
178
|
+
img.style.position = 'absolute';
|
|
179
|
+
img.style.left = '0px';
|
|
180
|
+
img.style.top = '0px';
|
|
181
|
+
img.style.zIndex = '2';
|
|
182
|
+
img.style.opacity = '0.8';
|
|
183
|
+
|
|
184
|
+
loadingOverlay.appendChild(img);
|
|
185
|
+
document.body.appendChild(loadingOverlay);
|
|
186
|
+
|
|
187
|
+
// DVD screensaver bounce logic
|
|
188
|
+
let x = Math.random() * (window.innerWidth - 300);
|
|
189
|
+
let y = Math.random() * (window.innerHeight - 300);
|
|
190
|
+
let dx = 1.2 + Math.random() * 0.4; // px per frame
|
|
191
|
+
let dy = 1.2 + Math.random() * 0.4;
|
|
192
|
+
// Randomize direction
|
|
193
|
+
if (Math.random() > 0.5) dx = -dx;
|
|
194
|
+
if (Math.random() > 0.5) dy = -dy;
|
|
195
|
+
|
|
196
|
+
function animate() {{
|
|
197
|
+
const imgWidth = img.offsetWidth || 300;
|
|
198
|
+
const imgHeight = img.offsetHeight || 300;
|
|
199
|
+
x += dx;
|
|
200
|
+
y += dy;
|
|
201
|
+
|
|
202
|
+
if (x <= 0) {{
|
|
203
|
+
x = 0;
|
|
204
|
+
dx = Math.abs(dx);
|
|
205
|
+
}} else if (x + imgWidth >= window.innerWidth) {{
|
|
206
|
+
x = window.innerWidth - imgWidth;
|
|
207
|
+
dx = -Math.abs(dx);
|
|
208
|
+
}}
|
|
209
|
+
if (y <= 0) {{
|
|
210
|
+
y = 0;
|
|
211
|
+
dy = Math.abs(dy);
|
|
212
|
+
}} else if (y + imgHeight >= window.innerHeight) {{
|
|
213
|
+
y = window.innerHeight - imgHeight;
|
|
214
|
+
dy = -Math.abs(dy);
|
|
215
|
+
}}
|
|
216
|
+
|
|
217
|
+
img.style.left = `${{x}}px`;
|
|
218
|
+
img.style.top = `${{y}}px`;
|
|
219
|
+
|
|
220
|
+
requestAnimationFrame(animate);
|
|
221
|
+
}}
|
|
222
|
+
animate();
|
|
223
|
+
|
|
224
|
+
// Responsive: update bounds on resize
|
|
225
|
+
window.addEventListener('resize', () => {{
|
|
226
|
+
x = Math.min(x, window.innerWidth - img.offsetWidth);
|
|
227
|
+
y = Math.min(y, window.innerHeight - img.offsetHeight);
|
|
228
|
+
}});
|
|
229
|
+
|
|
230
|
+
// Add a little CSS for smoothness
|
|
231
|
+
const style = document.createElement('style');
|
|
232
|
+
style.textContent = `
|
|
233
|
+
#pretty-loading-animation {{
|
|
234
|
+
/*backdrop-filter: blur(2px) brightness(0.9);*/
|
|
235
|
+
}}
|
|
236
|
+
#pretty-loading-animation img {{
|
|
237
|
+
user-select: none;
|
|
238
|
+
pointer-events: none;
|
|
239
|
+
}}
|
|
240
|
+
`;
|
|
241
|
+
document.head.appendChild(style);
|
|
242
|
+
}})('{browser_session_label}');
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
await temp_session.cdp_client.send.Runtime.evaluate(params={'expression': script}, session_id=temp_session.session_id)
|
|
246
|
+
|
|
247
|
+
# No need to detach - session is cached
|
|
248
|
+
|
|
249
|
+
# Dispatch event
|
|
250
|
+
self.event_bus.dispatch(AboutBlankDVDScreensaverShownEvent(target_id=target_id))
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
self.logger.error(f'[AboutBlankWatchdog] Error injecting DVD screensaver: {e}')
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Browser watchdog for monitoring crashes and network timeouts using CDP."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
6
|
+
|
|
7
|
+
import psutil
|
|
8
|
+
from bubus import BaseEvent
|
|
9
|
+
from cdp_use.cdp.target import SessionID, TargetID
|
|
10
|
+
from cdp_use.cdp.target.events import TargetCrashedEvent
|
|
11
|
+
from pydantic import Field, PrivateAttr
|
|
12
|
+
|
|
13
|
+
from browser_use.browser.events import (
|
|
14
|
+
BrowserConnectedEvent,
|
|
15
|
+
BrowserErrorEvent,
|
|
16
|
+
BrowserStoppedEvent,
|
|
17
|
+
TabClosedEvent,
|
|
18
|
+
TabCreatedEvent,
|
|
19
|
+
)
|
|
20
|
+
from browser_use.browser.watchdog_base import BaseWatchdog
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NetworkRequestTracker:
|
|
27
|
+
"""Tracks ongoing network requests."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, request_id: str, start_time: float, url: str, method: str, resource_type: str | None = None):
|
|
30
|
+
self.request_id = request_id
|
|
31
|
+
self.start_time = start_time
|
|
32
|
+
self.url = url
|
|
33
|
+
self.method = method
|
|
34
|
+
self.resource_type = resource_type
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CrashWatchdog(BaseWatchdog):
|
|
38
|
+
"""Monitors browser health for crashes and network timeouts using CDP."""
|
|
39
|
+
|
|
40
|
+
# Event contracts
|
|
41
|
+
LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [
|
|
42
|
+
BrowserConnectedEvent,
|
|
43
|
+
BrowserStoppedEvent,
|
|
44
|
+
TabCreatedEvent,
|
|
45
|
+
TabClosedEvent,
|
|
46
|
+
]
|
|
47
|
+
EMITS: ClassVar[list[type[BaseEvent]]] = [BrowserErrorEvent]
|
|
48
|
+
|
|
49
|
+
# Configuration
|
|
50
|
+
network_timeout_seconds: float = Field(default=10.0)
|
|
51
|
+
check_interval_seconds: float = Field(default=5.0) # Reduced frequency to reduce noise
|
|
52
|
+
|
|
53
|
+
# Private state
|
|
54
|
+
_active_requests: dict[str, NetworkRequestTracker] = PrivateAttr(default_factory=dict)
|
|
55
|
+
_monitoring_task: asyncio.Task | None = PrivateAttr(default=None)
|
|
56
|
+
_last_responsive_checks: dict[str, float] = PrivateAttr(default_factory=dict) # target_url -> timestamp
|
|
57
|
+
_cdp_event_tasks: set[asyncio.Task] = PrivateAttr(default_factory=set) # Track CDP event handler tasks
|
|
58
|
+
_targets_with_listeners: set[str] = PrivateAttr(default_factory=set) # Track targets that already have event listeners
|
|
59
|
+
|
|
60
|
+
async def on_BrowserConnectedEvent(self, event: BrowserConnectedEvent) -> None:
|
|
61
|
+
"""Start monitoring when browser is connected."""
|
|
62
|
+
# logger.debug('[CrashWatchdog] Browser connected event received, beginning monitoring')
|
|
63
|
+
|
|
64
|
+
asyncio.create_task(self._start_monitoring())
|
|
65
|
+
# logger.debug(f'[CrashWatchdog] Monitoring task started: {self._monitoring_task and not self._monitoring_task.done()}')
|
|
66
|
+
|
|
67
|
+
async def on_BrowserStoppedEvent(self, event: BrowserStoppedEvent) -> None:
|
|
68
|
+
"""Stop monitoring when browser stops."""
|
|
69
|
+
# logger.debug('[CrashWatchdog] Browser stopped, ending monitoring')
|
|
70
|
+
await self._stop_monitoring()
|
|
71
|
+
|
|
72
|
+
async def on_TabCreatedEvent(self, event: TabCreatedEvent) -> None:
|
|
73
|
+
"""Attach to new tab."""
|
|
74
|
+
assert self.browser_session.agent_focus is not None, 'No current target ID'
|
|
75
|
+
await self.attach_to_target(self.browser_session.agent_focus.target_id)
|
|
76
|
+
|
|
77
|
+
async def on_TabClosedEvent(self, event: TabClosedEvent) -> None:
|
|
78
|
+
"""Clean up tracking when tab closes."""
|
|
79
|
+
# Remove target from listener tracking to prevent memory leak
|
|
80
|
+
if event.target_id in self._targets_with_listeners:
|
|
81
|
+
self._targets_with_listeners.discard(event.target_id)
|
|
82
|
+
self.logger.debug(f'[CrashWatchdog] Removed target {event.target_id[:8]}... from monitoring')
|
|
83
|
+
|
|
84
|
+
async def attach_to_target(self, target_id: TargetID) -> None:
|
|
85
|
+
"""Set up crash monitoring for a specific target using CDP."""
|
|
86
|
+
try:
|
|
87
|
+
# Check if we already have listeners for this target
|
|
88
|
+
if target_id in self._targets_with_listeners:
|
|
89
|
+
self.logger.debug(f'[CrashWatchdog] Event listeners already exist for target: {target_id[:8]}...')
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Create temporary session for monitoring without switching focus
|
|
93
|
+
cdp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)
|
|
94
|
+
|
|
95
|
+
# Register crash event handler
|
|
96
|
+
def on_target_crashed(event: TargetCrashedEvent, session_id: SessionID | None = None):
|
|
97
|
+
# Create and track the task
|
|
98
|
+
task = asyncio.create_task(self._on_target_crash_cdp(target_id))
|
|
99
|
+
self._cdp_event_tasks.add(task)
|
|
100
|
+
# Remove from set when done
|
|
101
|
+
task.add_done_callback(lambda t: self._cdp_event_tasks.discard(t))
|
|
102
|
+
|
|
103
|
+
cdp_session.cdp_client.register.Target.targetCrashed(on_target_crashed)
|
|
104
|
+
|
|
105
|
+
# Track that we've added listeners to this target
|
|
106
|
+
self._targets_with_listeners.add(target_id)
|
|
107
|
+
|
|
108
|
+
# Get target info for logging
|
|
109
|
+
targets = await cdp_session.cdp_client.send.Target.getTargets()
|
|
110
|
+
target_info = next((t for t in targets['targetInfos'] if t['targetId'] == target_id), None)
|
|
111
|
+
if target_info:
|
|
112
|
+
self.logger.debug(f'[CrashWatchdog] Added target to monitoring: {target_info.get("url", "unknown")}')
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.logger.warning(f'[CrashWatchdog] Failed to attach to target {target_id}: {e}')
|
|
116
|
+
|
|
117
|
+
async def _on_request_cdp(self, event: dict) -> None:
|
|
118
|
+
"""Track new network request from CDP event."""
|
|
119
|
+
request_id = event.get('requestId', '')
|
|
120
|
+
request = event.get('request', {})
|
|
121
|
+
|
|
122
|
+
self._active_requests[request_id] = NetworkRequestTracker(
|
|
123
|
+
request_id=request_id,
|
|
124
|
+
start_time=time.time(),
|
|
125
|
+
url=request.get('url', ''),
|
|
126
|
+
method=request.get('method', ''),
|
|
127
|
+
resource_type=event.get('type'),
|
|
128
|
+
)
|
|
129
|
+
# logger.debug(f'[CrashWatchdog] Tracking request: {request.get("method", "")} {request.get("url", "")[:50]}...')
|
|
130
|
+
|
|
131
|
+
def _on_response_cdp(self, event: dict) -> None:
|
|
132
|
+
"""Remove request from tracking on response."""
|
|
133
|
+
request_id = event.get('requestId', '')
|
|
134
|
+
if request_id in self._active_requests:
|
|
135
|
+
elapsed = time.time() - self._active_requests[request_id].start_time
|
|
136
|
+
response = event.get('response', {})
|
|
137
|
+
self.logger.debug(f'[CrashWatchdog] Request completed in {elapsed:.2f}s: {response.get("url", "")[:50]}...')
|
|
138
|
+
# Don't remove yet - wait for loadingFinished
|
|
139
|
+
|
|
140
|
+
def _on_request_failed_cdp(self, event: dict) -> None:
|
|
141
|
+
"""Remove request from tracking on failure."""
|
|
142
|
+
request_id = event.get('requestId', '')
|
|
143
|
+
if request_id in self._active_requests:
|
|
144
|
+
elapsed = time.time() - self._active_requests[request_id].start_time
|
|
145
|
+
self.logger.debug(
|
|
146
|
+
f'[CrashWatchdog] Request failed after {elapsed:.2f}s: {self._active_requests[request_id].url[:50]}...'
|
|
147
|
+
)
|
|
148
|
+
del self._active_requests[request_id]
|
|
149
|
+
|
|
150
|
+
def _on_request_finished_cdp(self, event: dict) -> None:
|
|
151
|
+
"""Remove request from tracking when loading is finished."""
|
|
152
|
+
request_id = event.get('requestId', '')
|
|
153
|
+
self._active_requests.pop(request_id, None)
|
|
154
|
+
|
|
155
|
+
async def _on_target_crash_cdp(self, target_id: TargetID) -> None:
|
|
156
|
+
"""Handle target crash detected via CDP."""
|
|
157
|
+
self.logger.debug(f'[CrashWatchdog] Target crashed: {target_id[:8]}..., waiting for detach event')
|
|
158
|
+
|
|
159
|
+
# Get target info for logging
|
|
160
|
+
cdp_client = self.browser_session.cdp_client
|
|
161
|
+
targets = await cdp_client.send.Target.getTargets()
|
|
162
|
+
target_info = next((t for t in targets['targetInfos'] if t['targetId'] == target_id), None)
|
|
163
|
+
|
|
164
|
+
is_agent_focus = (
|
|
165
|
+
target_info
|
|
166
|
+
and self.browser_session.agent_focus
|
|
167
|
+
and target_info['targetId'] == self.browser_session.agent_focus.target_id
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if is_agent_focus and target_info:
|
|
171
|
+
self.logger.error(
|
|
172
|
+
f'[CrashWatchdog] 💥 Agent focus tab crashed: {target_info.get("url", "unknown")} '
|
|
173
|
+
f'(SessionManager will auto-recover)'
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Emit browser error event
|
|
177
|
+
self.event_bus.dispatch(
|
|
178
|
+
BrowserErrorEvent(
|
|
179
|
+
error_type='TargetCrash',
|
|
180
|
+
message=f'Target crashed: {target_id}',
|
|
181
|
+
details={
|
|
182
|
+
'url': target_info.get('url') if target_info else None,
|
|
183
|
+
'target_id': target_id,
|
|
184
|
+
'was_agent_focus': is_agent_focus,
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def _start_monitoring(self) -> None:
|
|
190
|
+
"""Start the monitoring loop."""
|
|
191
|
+
assert self.browser_session.cdp_client is not None, 'Root CDP client not initialized - browser may not be connected yet'
|
|
192
|
+
|
|
193
|
+
if self._monitoring_task and not self._monitoring_task.done():
|
|
194
|
+
# logger.info('[CrashWatchdog] Monitoring already running')
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
self._monitoring_task = asyncio.create_task(self._monitoring_loop())
|
|
198
|
+
# logger.debug('[CrashWatchdog] Monitoring loop created and started')
|
|
199
|
+
|
|
200
|
+
async def _stop_monitoring(self) -> None:
|
|
201
|
+
"""Stop the monitoring loop and clean up all tracking."""
|
|
202
|
+
if self._monitoring_task and not self._monitoring_task.done():
|
|
203
|
+
self._monitoring_task.cancel()
|
|
204
|
+
try:
|
|
205
|
+
await self._monitoring_task
|
|
206
|
+
except asyncio.CancelledError:
|
|
207
|
+
pass
|
|
208
|
+
self.logger.debug('[CrashWatchdog] Monitoring loop stopped')
|
|
209
|
+
|
|
210
|
+
# Cancel all CDP event handler tasks
|
|
211
|
+
for task in list(self._cdp_event_tasks):
|
|
212
|
+
if not task.done():
|
|
213
|
+
task.cancel()
|
|
214
|
+
# Wait for all tasks to complete cancellation
|
|
215
|
+
if self._cdp_event_tasks:
|
|
216
|
+
await asyncio.gather(*self._cdp_event_tasks, return_exceptions=True)
|
|
217
|
+
self._cdp_event_tasks.clear()
|
|
218
|
+
|
|
219
|
+
# Clear all tracking
|
|
220
|
+
self._active_requests.clear()
|
|
221
|
+
self._targets_with_listeners.clear()
|
|
222
|
+
self._last_responsive_checks.clear()
|
|
223
|
+
|
|
224
|
+
async def _monitoring_loop(self) -> None:
|
|
225
|
+
"""Main monitoring loop."""
|
|
226
|
+
await asyncio.sleep(10) # give browser time to start up and load the first page after first LLM call
|
|
227
|
+
while True:
|
|
228
|
+
try:
|
|
229
|
+
await self._check_network_timeouts()
|
|
230
|
+
await self._check_browser_health()
|
|
231
|
+
await asyncio.sleep(self.check_interval_seconds)
|
|
232
|
+
except asyncio.CancelledError:
|
|
233
|
+
break
|
|
234
|
+
except Exception as e:
|
|
235
|
+
self.logger.error(f'[CrashWatchdog] Error in monitoring loop: {e}')
|
|
236
|
+
|
|
237
|
+
async def _check_network_timeouts(self) -> None:
|
|
238
|
+
"""Check for network requests exceeding timeout."""
|
|
239
|
+
current_time = time.time()
|
|
240
|
+
timed_out_requests = []
|
|
241
|
+
|
|
242
|
+
# Debug logging
|
|
243
|
+
if self._active_requests:
|
|
244
|
+
self.logger.debug(
|
|
245
|
+
f'[CrashWatchdog] Checking {len(self._active_requests)} active requests for timeouts (threshold: {self.network_timeout_seconds}s)'
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
for request_id, tracker in self._active_requests.items():
|
|
249
|
+
elapsed = current_time - tracker.start_time
|
|
250
|
+
self.logger.debug(
|
|
251
|
+
f'[CrashWatchdog] Request {tracker.url[:30]}... elapsed: {elapsed:.1f}s, timeout: {self.network_timeout_seconds}s'
|
|
252
|
+
)
|
|
253
|
+
if elapsed >= self.network_timeout_seconds:
|
|
254
|
+
timed_out_requests.append((request_id, tracker))
|
|
255
|
+
|
|
256
|
+
# Emit events for timed out requests
|
|
257
|
+
for request_id, tracker in timed_out_requests:
|
|
258
|
+
self.logger.warning(
|
|
259
|
+
f'[CrashWatchdog] Network request timeout after {self.network_timeout_seconds}s: '
|
|
260
|
+
f'{tracker.method} {tracker.url[:100]}...'
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self.event_bus.dispatch(
|
|
264
|
+
BrowserErrorEvent(
|
|
265
|
+
error_type='NetworkTimeout',
|
|
266
|
+
message=f'Network request timed out after {self.network_timeout_seconds}s',
|
|
267
|
+
details={
|
|
268
|
+
'url': tracker.url,
|
|
269
|
+
'method': tracker.method,
|
|
270
|
+
'resource_type': tracker.resource_type,
|
|
271
|
+
'elapsed_seconds': current_time - tracker.start_time,
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Remove from tracking
|
|
277
|
+
del self._active_requests[request_id]
|
|
278
|
+
|
|
279
|
+
async def _check_browser_health(self) -> None:
|
|
280
|
+
"""Check if browser and targets are still responsive."""
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
self.logger.debug(f'[CrashWatchdog] Checking browser health for target {self.browser_session.agent_focus}')
|
|
284
|
+
cdp_session = await self.browser_session.get_or_create_cdp_session()
|
|
285
|
+
|
|
286
|
+
for target in (await self.browser_session.cdp_client.send.Target.getTargets()).get('targetInfos', []):
|
|
287
|
+
if target.get('type') == 'page':
|
|
288
|
+
cdp_session = await self.browser_session.get_or_create_cdp_session(target_id=target.get('targetId'))
|
|
289
|
+
if self._is_new_tab_page(target.get('url')) and target.get('url') != 'about:blank':
|
|
290
|
+
self.logger.debug(
|
|
291
|
+
f'[CrashWatchdog] Redirecting chrome://new-tab-page/ to about:blank {target.get("url")}'
|
|
292
|
+
)
|
|
293
|
+
await cdp_session.cdp_client.send.Page.navigate(
|
|
294
|
+
params={'url': 'about:blank'}, session_id=cdp_session.session_id
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Quick ping to check if session is alive
|
|
298
|
+
self.logger.debug(f'[CrashWatchdog] Attempting to run simple JS test expression in session {cdp_session} 1+1')
|
|
299
|
+
await asyncio.wait_for(
|
|
300
|
+
cdp_session.cdp_client.send.Runtime.evaluate(params={'expression': '1+1'}, session_id=cdp_session.session_id),
|
|
301
|
+
timeout=1.0,
|
|
302
|
+
)
|
|
303
|
+
self.logger.debug(f'[CrashWatchdog] Browser health check passed for target {self.browser_session.agent_focus}')
|
|
304
|
+
except Exception as e:
|
|
305
|
+
self.logger.error(
|
|
306
|
+
f'[CrashWatchdog] ❌ Crashed/unresponsive session detected for target {self.browser_session.agent_focus} '
|
|
307
|
+
f'error: {type(e).__name__}: {e} (Chrome will send detach event, SessionManager will auto-recover)'
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Check browser process if we have PID
|
|
311
|
+
if self.browser_session._local_browser_watchdog and (proc := self.browser_session._local_browser_watchdog._subprocess):
|
|
312
|
+
try:
|
|
313
|
+
if proc.status() in (psutil.STATUS_ZOMBIE, psutil.STATUS_DEAD):
|
|
314
|
+
self.logger.error(f'[CrashWatchdog] Browser process {proc.pid} has crashed')
|
|
315
|
+
|
|
316
|
+
# Browser process crashed - SessionManager will clean up via detach events
|
|
317
|
+
# Just dispatch error event and stop monitoring
|
|
318
|
+
self.event_bus.dispatch(
|
|
319
|
+
BrowserErrorEvent(
|
|
320
|
+
error_type='BrowserProcessCrashed',
|
|
321
|
+
message=f'Browser process {proc.pid} has crashed',
|
|
322
|
+
details={'pid': proc.pid, 'status': proc.status()},
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
self.logger.warning('[CrashWatchdog] Browser process dead - stopping health monitoring')
|
|
327
|
+
await self._stop_monitoring()
|
|
328
|
+
return
|
|
329
|
+
except Exception:
|
|
330
|
+
pass # psutil not available or process doesn't exist
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def _is_new_tab_page(url: str) -> bool:
|
|
334
|
+
"""Check if URL is a new tab page."""
|
|
335
|
+
return url in ['about:blank', 'chrome://new-tab-page/', 'chrome://newtab/']
|