sentienceapi 0.92.2__py3-none-any.whl → 0.98.0__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.
Potentially problematic release.
This version of sentienceapi might be problematic. Click here for more details.
- sentience/__init__.py +107 -2
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +2 -0
- sentience/actions.py +354 -9
- sentience/agent.py +4 -0
- sentience/agent_runtime.py +840 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +8 -1
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +372 -0
- sentience/backends/browser_use_adapter.py +241 -0
- sentience/backends/cdp_backend.py +393 -0
- sentience/backends/exceptions.py +211 -0
- sentience/backends/playwright_backend.py +194 -0
- sentience/backends/protocol.py +216 -0
- sentience/backends/sentience_context.py +469 -0
- sentience/backends/snapshot.py +483 -0
- sentience/browser.py +230 -74
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +65 -24
- sentience/constants.py +6 -0
- sentience/cursor_policy.py +142 -0
- sentience/extension/content.js +35 -0
- sentience/extension/injected_api.js +310 -15
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +192 -144
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +29 -29
- sentience/failure_artifacts.py +241 -0
- sentience/integrations/__init__.py +6 -0
- sentience/integrations/langchain/__init__.py +12 -0
- sentience/integrations/langchain/context.py +18 -0
- sentience/integrations/langchain/core.py +326 -0
- sentience/integrations/langchain/tools.py +180 -0
- sentience/integrations/models.py +46 -0
- sentience/integrations/pydanticai/__init__.py +15 -0
- sentience/integrations/pydanticai/deps.py +20 -0
- sentience/integrations/pydanticai/toolset.py +468 -0
- sentience/llm_provider.py +695 -18
- sentience/models.py +536 -3
- sentience/ordinal.py +280 -0
- sentience/query.py +66 -4
- sentience/schemas/trace_v1.json +27 -1
- sentience/snapshot.py +384 -93
- sentience/snapshot_diff.py +39 -54
- sentience/text_search.py +1 -0
- sentience/trace_event_builder.py +20 -1
- sentience/trace_indexing/indexer.py +3 -49
- sentience/tracer_factory.py +1 -3
- sentience/verification.py +618 -0
- sentience/visual_agent.py +3 -1
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +198 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/utils.py +0 -296
- sentienceapi-0.92.2.dist-info/RECORD +0 -65
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
sentience/browser.py
CHANGED
|
@@ -3,7 +3,9 @@ Playwright browser harness with extension loading
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import logging
|
|
6
7
|
import os
|
|
8
|
+
import platform
|
|
7
9
|
import shutil
|
|
8
10
|
import tempfile
|
|
9
11
|
import time
|
|
@@ -18,8 +20,11 @@ from playwright.async_api import async_playwright
|
|
|
18
20
|
from playwright.sync_api import BrowserContext, Page, Playwright, sync_playwright
|
|
19
21
|
|
|
20
22
|
from sentience._extension_loader import find_extension_path
|
|
23
|
+
from sentience.constants import SENTIENCE_API_URL
|
|
21
24
|
from sentience.models import ProxyConfig, StorageState, Viewport
|
|
22
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
23
28
|
# Import stealth for bot evasion (optional - graceful fallback if not available)
|
|
24
29
|
try:
|
|
25
30
|
from playwright_stealth import stealth_async, stealth_sync
|
|
@@ -86,7 +91,7 @@ class SentienceBrowser:
|
|
|
86
91
|
# Only set api_url if api_key is provided, otherwise None (free tier)
|
|
87
92
|
# Defaults to production API if key is present but url is missing
|
|
88
93
|
if self.api_key and not api_url:
|
|
89
|
-
self.api_url =
|
|
94
|
+
self.api_url = SENTIENCE_API_URL
|
|
90
95
|
else:
|
|
91
96
|
self.api_url = api_url
|
|
92
97
|
|
|
@@ -145,14 +150,16 @@ class SentienceBrowser:
|
|
|
145
150
|
|
|
146
151
|
# Validate scheme
|
|
147
152
|
if parsed.scheme not in ("http", "https", "socks5"):
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
logger.warning(
|
|
154
|
+
f"Unsupported proxy scheme: {parsed.scheme}. Supported: http, https, socks5"
|
|
155
|
+
)
|
|
150
156
|
return None
|
|
151
157
|
|
|
152
158
|
# Validate host and port
|
|
153
159
|
if not parsed.hostname or not parsed.port:
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
logger.warning(
|
|
161
|
+
"Proxy URL must include hostname and port. Expected format: http://username:password@host:port"
|
|
162
|
+
)
|
|
156
163
|
return None
|
|
157
164
|
|
|
158
165
|
# Build server URL
|
|
@@ -166,8 +173,9 @@ class SentienceBrowser:
|
|
|
166
173
|
)
|
|
167
174
|
|
|
168
175
|
except Exception as e:
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
logger.warning(
|
|
177
|
+
f"Invalid proxy configuration: {e}. Expected format: http://username:password@host:port"
|
|
178
|
+
)
|
|
171
179
|
return None
|
|
172
180
|
|
|
173
181
|
def start(self) -> None:
|
|
@@ -187,13 +195,41 @@ class SentienceBrowser:
|
|
|
187
195
|
f"--disable-extensions-except={self._extension_path}",
|
|
188
196
|
f"--load-extension={self._extension_path}",
|
|
189
197
|
"--disable-blink-features=AutomationControlled", # Hides 'navigator.webdriver'
|
|
190
|
-
"--no-sandbox",
|
|
191
198
|
"--disable-infobars",
|
|
192
199
|
# WebRTC leak protection (prevents real IP exposure when using proxies/VPNs)
|
|
193
200
|
"--disable-features=WebRtcHideLocalIpsWithMdns",
|
|
194
201
|
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp",
|
|
195
202
|
]
|
|
196
203
|
|
|
204
|
+
# Only add --no-sandbox on Linux (causes crashes on macOS)
|
|
205
|
+
# macOS sandboxing works fine and the flag actually causes crashes
|
|
206
|
+
if platform.system() == "Linux":
|
|
207
|
+
args.append("--no-sandbox")
|
|
208
|
+
|
|
209
|
+
# Add GPU-disabling flags for macOS to prevent Chrome for Testing crash-on-exit
|
|
210
|
+
# These flags help avoid EXC_BAD_ACCESS crashes during browser shutdown
|
|
211
|
+
if platform.system() == "Darwin": # macOS
|
|
212
|
+
args.extend(
|
|
213
|
+
[
|
|
214
|
+
"--disable-gpu",
|
|
215
|
+
"--disable-software-rasterizer",
|
|
216
|
+
"--disable-dev-shm-usage",
|
|
217
|
+
"--disable-breakpad", # Disable crash reporter to prevent macOS crash dialogs
|
|
218
|
+
"--disable-crash-reporter", # Disable crash reporter UI
|
|
219
|
+
"--disable-crash-handler", # Disable crash handler completely
|
|
220
|
+
"--disable-in-process-stack-traces", # Disable stack trace collection
|
|
221
|
+
"--disable-hang-monitor", # Disable hang detection
|
|
222
|
+
"--disable-background-networking", # Disable background networking
|
|
223
|
+
"--disable-background-timer-throttling", # Disable background throttling
|
|
224
|
+
"--disable-backgrounding-occluded-windows", # Disable backgrounding
|
|
225
|
+
"--disable-renderer-backgrounding", # Disable renderer backgrounding
|
|
226
|
+
"--disable-features=TranslateUI", # Disable translate UI
|
|
227
|
+
"--disable-ipc-flooding-protection", # Disable IPC flooding protection
|
|
228
|
+
"--disable-logging", # Disable logging to reduce stderr noise
|
|
229
|
+
"--log-level=3", # Set log level to fatal only (suppresses warnings)
|
|
230
|
+
]
|
|
231
|
+
)
|
|
232
|
+
|
|
197
233
|
# Handle headless mode correctly for extensions
|
|
198
234
|
# 'headless=True' DOES NOT support extensions in standard Chrome
|
|
199
235
|
# We must use 'headless="new"' (Chrome 112+) or run visible
|
|
@@ -219,6 +255,8 @@ class SentienceBrowser:
|
|
|
219
255
|
"viewport": {"width": self.viewport.width, "height": self.viewport.height},
|
|
220
256
|
# Remove "HeadlessChrome" from User Agent automatically
|
|
221
257
|
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
|
258
|
+
# Note: Don't set "channel" - let Playwright use its default managed Chromium
|
|
259
|
+
# Setting channel=None doesn't force bundled Chromium and can still pick Chrome for Testing
|
|
222
260
|
}
|
|
223
261
|
|
|
224
262
|
# Add device scale factor if configured
|
|
@@ -230,7 +268,7 @@ class SentienceBrowser:
|
|
|
230
268
|
launch_params["proxy"] = proxy_config.to_playwright_dict()
|
|
231
269
|
# Ignore HTTPS errors when using proxy (many residential proxies use self-signed certs)
|
|
232
270
|
launch_params["ignore_https_errors"] = True
|
|
233
|
-
|
|
271
|
+
logger.info(f"Using proxy: {proxy_config.server}")
|
|
234
272
|
|
|
235
273
|
# Add video recording if configured
|
|
236
274
|
if self.record_video_dir:
|
|
@@ -238,9 +276,8 @@ class SentienceBrowser:
|
|
|
238
276
|
video_dir.mkdir(parents=True, exist_ok=True)
|
|
239
277
|
launch_params["record_video_dir"] = str(video_dir)
|
|
240
278
|
launch_params["record_video_size"] = self.record_video_size
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
f" Resolution: {self.record_video_size['width']}x{self.record_video_size['height']}"
|
|
279
|
+
logger.info(
|
|
280
|
+
f"Recording video to: {video_dir} (Resolution: {self.record_video_size['width']}x{self.record_video_size['height']})"
|
|
244
281
|
)
|
|
245
282
|
|
|
246
283
|
# Launch persistent context (required for extensions)
|
|
@@ -346,7 +383,7 @@ class SentienceBrowser:
|
|
|
346
383
|
playwright_cookies.append(playwright_cookie)
|
|
347
384
|
|
|
348
385
|
self.context.add_cookies(playwright_cookies)
|
|
349
|
-
|
|
386
|
+
logger.debug(f"Injected {len(state.cookies)} cookie(s)")
|
|
350
387
|
|
|
351
388
|
# Inject LocalStorage (requires navigation to each domain)
|
|
352
389
|
if state.origins:
|
|
@@ -373,11 +410,11 @@ class SentienceBrowser:
|
|
|
373
410
|
}""",
|
|
374
411
|
localStorage_dict,
|
|
375
412
|
)
|
|
376
|
-
|
|
377
|
-
f"
|
|
413
|
+
logger.debug(
|
|
414
|
+
f"Injected {len(origin_data.localStorage)} localStorage item(s) for {origin}"
|
|
378
415
|
)
|
|
379
416
|
except Exception as e:
|
|
380
|
-
|
|
417
|
+
logger.warning(f"Failed to inject localStorage for {origin}: {e}")
|
|
381
418
|
|
|
382
419
|
def _wait_for_extension(self, timeout_sec: float = 5.0) -> bool:
|
|
383
420
|
"""Poll for window.sentience to be available"""
|
|
@@ -438,30 +475,15 @@ class SentienceBrowser:
|
|
|
438
475
|
Note: Video files are saved automatically by Playwright when context closes.
|
|
439
476
|
If multiple pages exist, returns the path to the first page's video.
|
|
440
477
|
"""
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
#
|
|
444
|
-
# Note: Playwright saves videos when pages/context close, but we can get the
|
|
445
|
-
# expected path before closing. The actual file will be available after close.
|
|
446
|
-
if self.record_video_dir:
|
|
447
|
-
try:
|
|
448
|
-
# Try to get video path from the first page
|
|
449
|
-
if self.page and self.page.video:
|
|
450
|
-
temp_video_path = self.page.video.path()
|
|
451
|
-
# If that fails, check all pages in the context
|
|
452
|
-
elif self.context:
|
|
453
|
-
for page in self.context.pages:
|
|
454
|
-
if page.video:
|
|
455
|
-
temp_video_path = page.video.path()
|
|
456
|
-
break
|
|
457
|
-
except Exception:
|
|
458
|
-
# Video path might not be available until after close
|
|
459
|
-
# In that case, we'll return None and user can check the directory
|
|
460
|
-
pass
|
|
478
|
+
# CRITICAL: Don't access page.video.path() BEFORE closing context
|
|
479
|
+
# This can poke the video subsystem at an awkward time and cause crashes on macOS
|
|
480
|
+
# Instead, we'll locate the video file after context closes
|
|
461
481
|
|
|
462
482
|
# Close context (this triggers video file finalization)
|
|
463
483
|
if self.context:
|
|
464
484
|
self.context.close()
|
|
485
|
+
# Small grace period to ensure video file is fully flushed to disk
|
|
486
|
+
time.sleep(0.5)
|
|
465
487
|
|
|
466
488
|
# Close playwright
|
|
467
489
|
if self.playwright:
|
|
@@ -471,8 +493,24 @@ class SentienceBrowser:
|
|
|
471
493
|
if self._extension_path and os.path.exists(self._extension_path):
|
|
472
494
|
shutil.rmtree(self._extension_path)
|
|
473
495
|
|
|
496
|
+
# NOW resolve video path after context is closed and video is finalized
|
|
497
|
+
temp_video_path = None
|
|
498
|
+
if self.record_video_dir:
|
|
499
|
+
try:
|
|
500
|
+
# Locate the newest .webm file in record_video_dir
|
|
501
|
+
# This avoids touching page.video during teardown
|
|
502
|
+
video_dir = Path(self.record_video_dir)
|
|
503
|
+
if video_dir.exists():
|
|
504
|
+
webm_files = list(video_dir.glob("*.webm"))
|
|
505
|
+
if webm_files:
|
|
506
|
+
# Get the most recently modified file
|
|
507
|
+
temp_video_path = max(webm_files, key=lambda p: p.stat().st_mtime)
|
|
508
|
+
logger.debug(f"Found video file: {temp_video_path}")
|
|
509
|
+
except Exception as e:
|
|
510
|
+
logger.warning(f"Could not locate video file: {e}")
|
|
511
|
+
|
|
474
512
|
# Rename/move video if output_path is specified
|
|
475
|
-
final_path = temp_video_path
|
|
513
|
+
final_path = str(temp_video_path) if temp_video_path else None
|
|
476
514
|
if temp_video_path and output_path and os.path.exists(temp_video_path):
|
|
477
515
|
try:
|
|
478
516
|
output_path = str(output_path)
|
|
@@ -485,7 +523,7 @@ class SentienceBrowser:
|
|
|
485
523
|
|
|
486
524
|
warnings.warn(f"Failed to rename video file: {e}")
|
|
487
525
|
# Return original path if rename fails
|
|
488
|
-
final_path = temp_video_path
|
|
526
|
+
final_path = str(temp_video_path)
|
|
489
527
|
|
|
490
528
|
return final_path
|
|
491
529
|
|
|
@@ -605,6 +643,7 @@ class AsyncSentienceBrowser:
|
|
|
605
643
|
record_video_size: dict[str, int] | None = None,
|
|
606
644
|
viewport: Viewport | dict[str, int] | None = None,
|
|
607
645
|
device_scale_factor: float | None = None,
|
|
646
|
+
executable_path: str | None = None,
|
|
608
647
|
):
|
|
609
648
|
"""
|
|
610
649
|
Initialize Async Sentience browser
|
|
@@ -629,11 +668,15 @@ class AsyncSentienceBrowser:
|
|
|
629
668
|
2.0 (Retina/high-DPI, like MacBook Pro)
|
|
630
669
|
3.0 (very high DPI)
|
|
631
670
|
If None, defaults to 1.0 (standard DPI).
|
|
671
|
+
executable_path: Optional path to Chromium executable. If provided, forces use of
|
|
672
|
+
this specific browser binary instead of Playwright's managed browser.
|
|
673
|
+
Useful to guarantee Chromium (not Chrome for Testing) on macOS.
|
|
674
|
+
Example: "/path/to/playwright/chromium-1234/chrome-mac/Chromium.app/Contents/MacOS/Chromium"
|
|
632
675
|
"""
|
|
633
676
|
self.api_key = api_key
|
|
634
677
|
# Only set api_url if api_key is provided, otherwise None (free tier)
|
|
635
678
|
if self.api_key and not api_url:
|
|
636
|
-
self.api_url =
|
|
679
|
+
self.api_url = SENTIENCE_API_URL
|
|
637
680
|
else:
|
|
638
681
|
self.api_url = api_url
|
|
639
682
|
|
|
@@ -666,6 +709,9 @@ class AsyncSentienceBrowser:
|
|
|
666
709
|
# Device scale factor for high-DPI emulation
|
|
667
710
|
self.device_scale_factor = device_scale_factor
|
|
668
711
|
|
|
712
|
+
# Executable path override (for forcing specific Chromium binary)
|
|
713
|
+
self.executable_path = executable_path
|
|
714
|
+
|
|
669
715
|
self.playwright: AsyncPlaywright | None = None
|
|
670
716
|
self.context: AsyncBrowserContext | None = None
|
|
671
717
|
self.page: AsyncPage | None = None
|
|
@@ -689,14 +735,16 @@ class AsyncSentienceBrowser:
|
|
|
689
735
|
|
|
690
736
|
# Validate scheme
|
|
691
737
|
if parsed.scheme not in ("http", "https", "socks5"):
|
|
692
|
-
|
|
693
|
-
|
|
738
|
+
logger.warning(
|
|
739
|
+
f"Unsupported proxy scheme: {parsed.scheme}. Supported: http, https, socks5"
|
|
740
|
+
)
|
|
694
741
|
return None
|
|
695
742
|
|
|
696
743
|
# Validate host and port
|
|
697
744
|
if not parsed.hostname or not parsed.port:
|
|
698
|
-
|
|
699
|
-
|
|
745
|
+
logger.warning(
|
|
746
|
+
"Proxy URL must include hostname and port. Expected format: http://username:password@host:port"
|
|
747
|
+
)
|
|
700
748
|
return None
|
|
701
749
|
|
|
702
750
|
# Build server URL
|
|
@@ -710,8 +758,9 @@ class AsyncSentienceBrowser:
|
|
|
710
758
|
)
|
|
711
759
|
|
|
712
760
|
except Exception as e:
|
|
713
|
-
|
|
714
|
-
|
|
761
|
+
logger.warning(
|
|
762
|
+
f"Invalid proxy configuration: {e}. Expected format: http://username:password@host:port"
|
|
763
|
+
)
|
|
715
764
|
return None
|
|
716
765
|
|
|
717
766
|
async def start(self) -> None:
|
|
@@ -730,12 +779,40 @@ class AsyncSentienceBrowser:
|
|
|
730
779
|
f"--disable-extensions-except={self._extension_path}",
|
|
731
780
|
f"--load-extension={self._extension_path}",
|
|
732
781
|
"--disable-blink-features=AutomationControlled",
|
|
733
|
-
"--no-sandbox",
|
|
734
782
|
"--disable-infobars",
|
|
735
783
|
"--disable-features=WebRtcHideLocalIpsWithMdns",
|
|
736
784
|
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp",
|
|
737
785
|
]
|
|
738
786
|
|
|
787
|
+
# Only add --no-sandbox on Linux (causes crashes on macOS)
|
|
788
|
+
# macOS sandboxing works fine and the flag actually causes crashes
|
|
789
|
+
if platform.system() == "Linux":
|
|
790
|
+
args.append("--no-sandbox")
|
|
791
|
+
|
|
792
|
+
# Add GPU-disabling flags for macOS to prevent Chrome for Testing crash-on-exit
|
|
793
|
+
# These flags help avoid EXC_BAD_ACCESS crashes during browser shutdown
|
|
794
|
+
if platform.system() == "Darwin": # macOS
|
|
795
|
+
args.extend(
|
|
796
|
+
[
|
|
797
|
+
"--disable-gpu",
|
|
798
|
+
"--disable-software-rasterizer",
|
|
799
|
+
"--disable-dev-shm-usage",
|
|
800
|
+
"--disable-breakpad", # Disable crash reporter to prevent macOS crash dialogs
|
|
801
|
+
"--disable-crash-reporter", # Disable crash reporter UI
|
|
802
|
+
"--disable-crash-handler", # Disable crash handler completely
|
|
803
|
+
"--disable-in-process-stack-traces", # Disable stack trace collection
|
|
804
|
+
"--disable-hang-monitor", # Disable hang detection
|
|
805
|
+
"--disable-background-networking", # Disable background networking
|
|
806
|
+
"--disable-background-timer-throttling", # Disable background throttling
|
|
807
|
+
"--disable-backgrounding-occluded-windows", # Disable backgrounding
|
|
808
|
+
"--disable-renderer-backgrounding", # Disable renderer backgrounding
|
|
809
|
+
"--disable-features=TranslateUI", # Disable translate UI
|
|
810
|
+
"--disable-ipc-flooding-protection", # Disable IPC flooding protection
|
|
811
|
+
"--disable-logging", # Disable logging to reduce stderr noise
|
|
812
|
+
"--log-level=3", # Set log level to fatal only (suppresses warnings)
|
|
813
|
+
]
|
|
814
|
+
)
|
|
815
|
+
|
|
739
816
|
if self.headless:
|
|
740
817
|
args.append("--headless=new")
|
|
741
818
|
|
|
@@ -756,8 +833,16 @@ class AsyncSentienceBrowser:
|
|
|
756
833
|
"args": args,
|
|
757
834
|
"viewport": {"width": self.viewport.width, "height": self.viewport.height},
|
|
758
835
|
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
|
836
|
+
# Note: Don't set "channel" - let Playwright use its default managed Chromium
|
|
837
|
+
# Setting channel=None doesn't force bundled Chromium and can still pick Chrome for Testing
|
|
759
838
|
}
|
|
760
839
|
|
|
840
|
+
# If executable_path is provided, use it to force specific Chromium binary
|
|
841
|
+
# This guarantees we use Chromium (not Chrome for Testing) on macOS
|
|
842
|
+
if self.executable_path:
|
|
843
|
+
launch_params["executable_path"] = self.executable_path
|
|
844
|
+
logger.info(f"Using explicit executable: {self.executable_path}")
|
|
845
|
+
|
|
761
846
|
# Add device scale factor if configured
|
|
762
847
|
if self.device_scale_factor is not None:
|
|
763
848
|
launch_params["device_scale_factor"] = self.device_scale_factor
|
|
@@ -766,7 +851,7 @@ class AsyncSentienceBrowser:
|
|
|
766
851
|
if proxy_config:
|
|
767
852
|
launch_params["proxy"] = proxy_config.to_playwright_dict()
|
|
768
853
|
launch_params["ignore_https_errors"] = True
|
|
769
|
-
|
|
854
|
+
logger.info(f"Using proxy: {proxy_config.server}")
|
|
770
855
|
|
|
771
856
|
# Add video recording if configured
|
|
772
857
|
if self.record_video_dir:
|
|
@@ -774,9 +859,8 @@ class AsyncSentienceBrowser:
|
|
|
774
859
|
video_dir.mkdir(parents=True, exist_ok=True)
|
|
775
860
|
launch_params["record_video_dir"] = str(video_dir)
|
|
776
861
|
launch_params["record_video_size"] = self.record_video_size
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
f" Resolution: {self.record_video_size['width']}x{self.record_video_size['height']}"
|
|
862
|
+
logger.info(
|
|
863
|
+
f"Recording video to: {video_dir} (Resolution: {self.record_video_size['width']}x{self.record_video_size['height']})"
|
|
780
864
|
)
|
|
781
865
|
|
|
782
866
|
# Launch persistent context
|
|
@@ -867,7 +951,7 @@ class AsyncSentienceBrowser:
|
|
|
867
951
|
playwright_cookies.append(playwright_cookie)
|
|
868
952
|
|
|
869
953
|
await self.context.add_cookies(playwright_cookies)
|
|
870
|
-
|
|
954
|
+
logger.debug(f"Injected {len(state.cookies)} cookie(s)")
|
|
871
955
|
|
|
872
956
|
# Inject LocalStorage
|
|
873
957
|
if state.origins:
|
|
@@ -891,11 +975,11 @@ class AsyncSentienceBrowser:
|
|
|
891
975
|
}""",
|
|
892
976
|
localStorage_dict,
|
|
893
977
|
)
|
|
894
|
-
|
|
895
|
-
f"
|
|
978
|
+
logger.debug(
|
|
979
|
+
f"Injected {len(origin_data.localStorage)} localStorage item(s) for {origin}"
|
|
896
980
|
)
|
|
897
981
|
except Exception as e:
|
|
898
|
-
|
|
982
|
+
logger.warning(f"Failed to inject localStorage for {origin}: {e}")
|
|
899
983
|
|
|
900
984
|
async def _wait_for_extension(self, timeout_sec: float = 5.0) -> bool:
|
|
901
985
|
"""Poll for window.sentience to be available (async)"""
|
|
@@ -933,7 +1017,7 @@ class AsyncSentienceBrowser:
|
|
|
933
1017
|
|
|
934
1018
|
return False
|
|
935
1019
|
|
|
936
|
-
async def close(self, output_path: str | Path | None = None) -> str | None:
|
|
1020
|
+
async def close(self, output_path: str | Path | None = None) -> tuple[str | None, bool]:
|
|
937
1021
|
"""
|
|
938
1022
|
Close browser and cleanup (async)
|
|
939
1023
|
|
|
@@ -941,29 +1025,88 @@ class AsyncSentienceBrowser:
|
|
|
941
1025
|
output_path: Optional path to rename the video file to
|
|
942
1026
|
|
|
943
1027
|
Returns:
|
|
944
|
-
|
|
1028
|
+
Tuple of (video_path, shutdown_clean)
|
|
1029
|
+
- video_path: Path to video file if recording was enabled, None otherwise
|
|
1030
|
+
- shutdown_clean: True if shutdown completed without errors, False if there were issues
|
|
1031
|
+
|
|
1032
|
+
Note: Video path is resolved AFTER context close to avoid touching video
|
|
1033
|
+
subsystem during teardown, which can cause crashes on macOS.
|
|
945
1034
|
"""
|
|
946
|
-
|
|
1035
|
+
# CRITICAL: Don't access page.video.path() BEFORE closing context
|
|
1036
|
+
# This can poke the video subsystem at an awkward time and cause crashes
|
|
1037
|
+
# Instead, we'll locate the video file after context closes
|
|
1038
|
+
|
|
1039
|
+
# CRITICAL: Wait before closing to ensure all operations are complete
|
|
1040
|
+
# This is especially important for video recording - we need to ensure
|
|
1041
|
+
# all frames are written and the encoder is ready to finalize
|
|
1042
|
+
if platform.system() == "Darwin": # macOS
|
|
1043
|
+
# On macOS, give extra time for video encoder to finish writing frames
|
|
1044
|
+
# 4K video recording needs more time to flush buffers
|
|
1045
|
+
logger.debug("Waiting for video recording to stabilize before closing (macOS)...")
|
|
1046
|
+
await asyncio.sleep(2.0)
|
|
1047
|
+
else:
|
|
1048
|
+
await asyncio.sleep(1.0)
|
|
947
1049
|
|
|
948
|
-
|
|
1050
|
+
# Graceful shutdown: close context first, then playwright
|
|
1051
|
+
# Use longer timeouts on macOS where video finalization can take longer
|
|
1052
|
+
context_close_success = True
|
|
1053
|
+
if self.context:
|
|
1054
|
+
try:
|
|
1055
|
+
# Give context time to close gracefully (especially for video finalization)
|
|
1056
|
+
# Increased timeout for macOS where 4K video finalization can take longer
|
|
1057
|
+
await asyncio.wait_for(self.context.close(), timeout=30.0)
|
|
1058
|
+
logger.debug("Context closed successfully")
|
|
1059
|
+
except TimeoutError:
|
|
1060
|
+
logger.warning("Context close timed out, continuing with cleanup...")
|
|
1061
|
+
context_close_success = False
|
|
1062
|
+
except Exception as e:
|
|
1063
|
+
logger.warning(f"Error closing context: {e}")
|
|
1064
|
+
context_close_success = False
|
|
1065
|
+
finally:
|
|
1066
|
+
self.context = None
|
|
1067
|
+
|
|
1068
|
+
# Give Chrome a moment to fully flush video + release resources
|
|
1069
|
+
# This avoids stopping the driver while the browser is still finishing the .webm write/encoder shutdown
|
|
1070
|
+
# Increased grace period on macOS to allow more time for process cleanup
|
|
1071
|
+
grace_period = 2.0 if platform.system() == "Darwin" else 1.0
|
|
1072
|
+
await asyncio.sleep(grace_period)
|
|
1073
|
+
|
|
1074
|
+
playwright_stop_success = True
|
|
1075
|
+
if self.playwright:
|
|
949
1076
|
try:
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
except Exception:
|
|
958
|
-
|
|
1077
|
+
# Give playwright time to stop gracefully
|
|
1078
|
+
# Increased timeout to match context close timeout
|
|
1079
|
+
await asyncio.wait_for(self.playwright.stop(), timeout=15.0)
|
|
1080
|
+
logger.debug("Playwright stopped successfully")
|
|
1081
|
+
except TimeoutError:
|
|
1082
|
+
logger.warning("Playwright stop timed out, continuing with cleanup...")
|
|
1083
|
+
playwright_stop_success = False
|
|
1084
|
+
except Exception as e:
|
|
1085
|
+
logger.warning(f"Error stopping playwright: {e}")
|
|
1086
|
+
playwright_stop_success = False
|
|
1087
|
+
finally:
|
|
1088
|
+
self.playwright = None
|
|
959
1089
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1090
|
+
# Additional cleanup: On macOS, wait a bit more to ensure all browser processes are terminated
|
|
1091
|
+
# This helps prevent crash dialogs from appearing
|
|
1092
|
+
if platform.system() == "Darwin":
|
|
1093
|
+
await asyncio.sleep(0.5)
|
|
963
1094
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1095
|
+
# NOW resolve video path after context is closed and video is finalized
|
|
1096
|
+
temp_video_path = None
|
|
1097
|
+
if self.record_video_dir:
|
|
1098
|
+
try:
|
|
1099
|
+
# Locate the newest .webm file in record_video_dir
|
|
1100
|
+
# This avoids touching page.video during teardown
|
|
1101
|
+
video_dir = Path(self.record_video_dir)
|
|
1102
|
+
if video_dir.exists():
|
|
1103
|
+
webm_files = list(video_dir.glob("*.webm"))
|
|
1104
|
+
if webm_files:
|
|
1105
|
+
# Get the most recently modified file
|
|
1106
|
+
temp_video_path = max(webm_files, key=lambda p: p.stat().st_mtime)
|
|
1107
|
+
logger.debug(f"Found video file: {temp_video_path}")
|
|
1108
|
+
except Exception as e:
|
|
1109
|
+
logger.warning(f"Could not locate video file: {e}")
|
|
967
1110
|
|
|
968
1111
|
if self._extension_path and os.path.exists(self._extension_path):
|
|
969
1112
|
shutil.rmtree(self._extension_path)
|
|
@@ -984,7 +1127,19 @@ class AsyncSentienceBrowser:
|
|
|
984
1127
|
warnings.warn(f"Failed to rename video file: {e}")
|
|
985
1128
|
final_path = temp_video_path
|
|
986
1129
|
|
|
987
|
-
|
|
1130
|
+
# Log shutdown status (useful for detecting crashes in headless mode)
|
|
1131
|
+
shutdown_clean = context_close_success and playwright_stop_success
|
|
1132
|
+
if not shutdown_clean:
|
|
1133
|
+
logger.warning(
|
|
1134
|
+
f"Browser shutdown had issues - may indicate a crash "
|
|
1135
|
+
f"(context_close: {context_close_success}, playwright_stop: {playwright_stop_success})"
|
|
1136
|
+
)
|
|
1137
|
+
else:
|
|
1138
|
+
logger.debug("Browser shutdown completed cleanly")
|
|
1139
|
+
|
|
1140
|
+
# Return tuple: (video_path, shutdown_clean)
|
|
1141
|
+
# This allows callers to detect crashes even in headless mode
|
|
1142
|
+
return (final_path, shutdown_clean)
|
|
988
1143
|
|
|
989
1144
|
async def __aenter__(self):
|
|
990
1145
|
"""Async context manager entry"""
|
|
@@ -993,6 +1148,7 @@ class AsyncSentienceBrowser:
|
|
|
993
1148
|
|
|
994
1149
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
995
1150
|
"""Async context manager exit"""
|
|
1151
|
+
# Ignore return value in context manager exit
|
|
996
1152
|
await self.close()
|
|
997
1153
|
|
|
998
1154
|
@classmethod
|