stagehand 4.0.3a0.dev1491__tar.gz → 4.0.3a0.dev1493__tar.gz
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.
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/PKG-INFO +1 -1
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/pyproject.toml +1 -1
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/service-worker.js +2 -2
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_generated/protocol_version.py +1 -1
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/browser.py +331 -63
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/README.md +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/__init__.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/blank.html +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/content-script.js +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/manifest.json +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/wake-service-worker.html +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/wake-service-worker.js +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_generated/__init__.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_generated/input_types.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_generated/models.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_sdk_identity.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_validation.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/browser_clipboard.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/browser_context.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/browserbase_session.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/cdp_client.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/client_models.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/client_types.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/extension_assets.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/file_upload.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/locator.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/page.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/py.typed +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/response.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/rpc_client.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/stagehand.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/timeouts.py +0 -0
- {stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/webmcp.py +0 -0
{stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/service-worker.js
RENAMED
|
@@ -7359,7 +7359,7 @@ function isRecord$1(value) {
|
|
|
7359
7359
|
}
|
|
7360
7360
|
var package_default$1 = {
|
|
7361
7361
|
name: "@browserbasehq/stagehand-protocol",
|
|
7362
|
-
version: "2.0.0-alpha-
|
|
7362
|
+
version: "2.0.0-alpha-89c0fb83caedebc7f674561fc6405a03579842bd",
|
|
7363
7363
|
"private": true,
|
|
7364
7364
|
type: "module",
|
|
7365
7365
|
exports: {
|
|
@@ -53444,7 +53444,7 @@ var WebTracerProvider = class extends BasicTracerProvider {
|
|
|
53444
53444
|
};
|
|
53445
53445
|
var package_default = {
|
|
53446
53446
|
name: "@browserbasehq/stagehand-extension",
|
|
53447
|
-
version: "1.0.2-alpha-
|
|
53447
|
+
version: "1.0.2-alpha-89c0fb83caedebc7f674561fc6405a03579842bd",
|
|
53448
53448
|
"private": true,
|
|
53449
53449
|
type: "module",
|
|
53450
53450
|
scripts: {
|
{stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_generated/protocol_version.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"""Generated from packages/protocol/package.json. Do not edit."""
|
|
2
2
|
|
|
3
3
|
PROTOCOL_SEMVER_PATTERN = '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$'
|
|
4
|
-
STAGEHAND_PROTOCOL_VERSION = '2.0.0-alpha-
|
|
4
|
+
STAGEHAND_PROTOCOL_VERSION = '2.0.0-alpha-89c0fb83caedebc7f674561fc6405a03579842bd'
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import errno
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
4
7
|
import os
|
|
5
8
|
import shutil
|
|
6
9
|
import signal
|
|
7
10
|
import socket
|
|
8
11
|
import sys
|
|
9
12
|
import tempfile
|
|
13
|
+
import urllib.request
|
|
10
14
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
15
|
+
from contextlib import suppress
|
|
11
16
|
from dataclasses import dataclass, field
|
|
12
|
-
from pathlib import Path
|
|
17
|
+
from pathlib import Path, PureWindowsPath
|
|
13
18
|
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
14
19
|
|
|
15
20
|
if TYPE_CHECKING:
|
|
@@ -65,6 +70,8 @@ _DEFAULT_CHROME_FLAGS = (
|
|
|
65
70
|
)
|
|
66
71
|
|
|
67
72
|
_BROWSER_TOKEN = object()
|
|
73
|
+
_CHROME_POLL_INTERVAL_SECONDS = 0.1
|
|
74
|
+
_CHROME_REQUEST_TIMEOUT_SECONDS = 0.1
|
|
68
75
|
|
|
69
76
|
|
|
70
77
|
@dataclass(frozen=True)
|
|
@@ -84,14 +91,24 @@ class ResolvedBrowserSource:
|
|
|
84
91
|
cdp_url: str
|
|
85
92
|
keep_alive: bool
|
|
86
93
|
_close_callback: Callable[[], Awaitable[None]] | None = field(default=None, repr=False)
|
|
87
|
-
|
|
94
|
+
_close_task: asyncio.Task[None] | None = field(default=None, init=False, repr=False)
|
|
88
95
|
|
|
89
96
|
async def close(self) -> None:
|
|
90
|
-
if self.
|
|
97
|
+
if self._close_callback is None:
|
|
91
98
|
return
|
|
92
|
-
self.
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
if self._close_task is None:
|
|
100
|
+
self._close_task = asyncio.create_task(_invoke_close(self._close_callback))
|
|
101
|
+
await asyncio.shield(self._close_task)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def _invoke_close(callback: Callable[[], Awaitable[None]]) -> None:
|
|
105
|
+
await callback()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class _ChromeProfile:
|
|
110
|
+
path: Path
|
|
111
|
+
remove: bool
|
|
95
112
|
|
|
96
113
|
|
|
97
114
|
class StagehandBrowser:
|
|
@@ -258,6 +275,22 @@ class _LocalBrowserOptions(Protocol):
|
|
|
258
275
|
keep_alive: bool | None
|
|
259
276
|
|
|
260
277
|
|
|
278
|
+
class _WaitableChromeProcess(Protocol):
|
|
279
|
+
returncode: int | None
|
|
280
|
+
|
|
281
|
+
async def wait(self) -> int: ...
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class _ChromeProcess(_WaitableChromeProcess, Protocol):
|
|
285
|
+
pid: int
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class _TaskkillError(RuntimeError):
|
|
289
|
+
def __init__(self, returncode: int) -> None:
|
|
290
|
+
self.returncode = returncode
|
|
291
|
+
super().__init__(f"taskkill exited with code {returncode}")
|
|
292
|
+
|
|
293
|
+
|
|
261
294
|
async def _connect_browser(
|
|
262
295
|
*,
|
|
263
296
|
provider: Literal["local", "browserbase"],
|
|
@@ -622,54 +655,45 @@ browserbase = BrowserbaseBrowser()
|
|
|
622
655
|
|
|
623
656
|
|
|
624
657
|
async def _launch_local_browser(options: _LocalBrowserOptions) -> ResolvedBrowserSource:
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
658
|
+
_validate_local_browser_options(options)
|
|
659
|
+
chrome_path = _find_chrome_path(options.executable_path)
|
|
660
|
+
port = _resolve_chrome_port(options.port)
|
|
661
|
+
profile = _resolve_chrome_profile(options)
|
|
629
662
|
flags = _local_browser_flags(
|
|
630
663
|
options,
|
|
631
664
|
port=port,
|
|
632
|
-
user_data_dir=
|
|
633
|
-
|
|
665
|
+
user_data_dir=profile.path,
|
|
666
|
+
disable_sandbox=_should_disable_chromium_sandbox(options),
|
|
634
667
|
)
|
|
668
|
+
process: _ChromeProcess | None = None
|
|
635
669
|
try:
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
670
|
+
try:
|
|
671
|
+
process = await asyncio.create_subprocess_exec(
|
|
672
|
+
chrome_path,
|
|
673
|
+
*flags,
|
|
674
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
675
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
676
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
677
|
+
start_new_session=sys.platform != "win32",
|
|
678
|
+
)
|
|
679
|
+
except OSError as error:
|
|
680
|
+
raise RuntimeError(f"Failed to start Chrome: {error}") from error
|
|
681
|
+
await _wait_for_chrome(f"http://127.0.0.1:{port}", process)
|
|
682
|
+
except BaseException as launch_error:
|
|
683
|
+
try:
|
|
684
|
+
if process is not None:
|
|
685
|
+
await _close_local_chrome(process, profile)
|
|
686
|
+
elif profile.remove:
|
|
687
|
+
await _remove_chrome_profile(profile.path)
|
|
688
|
+
except BaseException as cleanup_error:
|
|
689
|
+
raise _combined_error(
|
|
690
|
+
"Chrome launch failed and browser cleanup also failed",
|
|
691
|
+
[launch_error, cleanup_error],
|
|
692
|
+
) from launch_error
|
|
647
693
|
raise
|
|
648
694
|
|
|
649
695
|
async def close() -> None:
|
|
650
|
-
|
|
651
|
-
if process.returncode is None:
|
|
652
|
-
try:
|
|
653
|
-
if sys.platform == "win32":
|
|
654
|
-
process.terminate()
|
|
655
|
-
else:
|
|
656
|
-
os.killpg(process.pid, signal.SIGTERM)
|
|
657
|
-
except ProcessLookupError:
|
|
658
|
-
pass
|
|
659
|
-
try:
|
|
660
|
-
await asyncio.wait_for(process.wait(), timeout=3)
|
|
661
|
-
except TimeoutError:
|
|
662
|
-
try:
|
|
663
|
-
if sys.platform == "win32":
|
|
664
|
-
process.kill()
|
|
665
|
-
else:
|
|
666
|
-
os.killpg(process.pid, signal.SIGKILL)
|
|
667
|
-
except ProcessLookupError:
|
|
668
|
-
pass
|
|
669
|
-
await process.wait()
|
|
670
|
-
finally:
|
|
671
|
-
if temporary_profile and options.preserve_user_data_dir is not True:
|
|
672
|
-
await asyncio.to_thread(shutil.rmtree, user_data_dir, True)
|
|
696
|
+
await _close_local_chrome(process, profile)
|
|
673
697
|
|
|
674
698
|
return ResolvedBrowserSource(
|
|
675
699
|
cdp_url=f"http://127.0.0.1:{port}",
|
|
@@ -678,12 +702,189 @@ async def _launch_local_browser(options: _LocalBrowserOptions) -> ResolvedBrowse
|
|
|
678
702
|
)
|
|
679
703
|
|
|
680
704
|
|
|
705
|
+
def _validate_local_browser_options(options: _LocalBrowserOptions) -> None:
|
|
706
|
+
if options.viewport is not None and (
|
|
707
|
+
options.viewport.width <= 0 or options.viewport.height <= 0
|
|
708
|
+
):
|
|
709
|
+
raise ValueError("Chrome viewport dimensions must be positive integers")
|
|
710
|
+
if options.device_scale_factor is not None and (
|
|
711
|
+
not math.isfinite(options.device_scale_factor) or options.device_scale_factor <= 0
|
|
712
|
+
):
|
|
713
|
+
raise ValueError("Chrome device scale factor must be positive and finite")
|
|
714
|
+
if options.proxy is not None:
|
|
715
|
+
if not options.proxy.server:
|
|
716
|
+
raise ValueError("Chrome proxy server is required")
|
|
717
|
+
if options.proxy.username is not None or options.proxy.password is not None:
|
|
718
|
+
raise NotImplementedError("Authenticated local browser proxies are not implemented yet")
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _resolve_chrome_profile(options: _LocalBrowserOptions) -> _ChromeProfile:
|
|
722
|
+
if options.user_data_dir not in (None, ""):
|
|
723
|
+
path = Path(options.user_data_dir)
|
|
724
|
+
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
725
|
+
return _ChromeProfile(path=path, remove=False)
|
|
726
|
+
path = Path(tempfile.mkdtemp(prefix="stagehand-chrome-"))
|
|
727
|
+
return _ChromeProfile(path=path, remove=options.preserve_user_data_dir is not True)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
async def _wait_for_chrome(
|
|
731
|
+
cdp_url: str,
|
|
732
|
+
process: _WaitableChromeProcess,
|
|
733
|
+
) -> None:
|
|
734
|
+
exited = asyncio.create_task(process.wait())
|
|
735
|
+
ready: asyncio.Task[bool] | None = None
|
|
736
|
+
delay: asyncio.Task[None] | None = None
|
|
737
|
+
try:
|
|
738
|
+
while True:
|
|
739
|
+
if process.returncode is not None:
|
|
740
|
+
raise _chrome_exited_before_ready_error(process.returncode)
|
|
741
|
+
|
|
742
|
+
ready = asyncio.create_task(_chrome_debugging_ready(cdp_url))
|
|
743
|
+
done, _ = await asyncio.wait((ready, exited), return_when=asyncio.FIRST_COMPLETED)
|
|
744
|
+
if exited in done:
|
|
745
|
+
ready.cancel()
|
|
746
|
+
with suppress(asyncio.CancelledError):
|
|
747
|
+
await ready
|
|
748
|
+
raise _chrome_exited_before_ready_error(exited.result())
|
|
749
|
+
if ready.result():
|
|
750
|
+
if process.returncode is not None or exited.done():
|
|
751
|
+
raise _chrome_exited_before_ready_error(process.returncode)
|
|
752
|
+
return
|
|
753
|
+
|
|
754
|
+
delay = asyncio.create_task(asyncio.sleep(_CHROME_POLL_INTERVAL_SECONDS))
|
|
755
|
+
done, _ = await asyncio.wait((delay, exited), return_when=asyncio.FIRST_COMPLETED)
|
|
756
|
+
if exited in done:
|
|
757
|
+
delay.cancel()
|
|
758
|
+
with suppress(asyncio.CancelledError):
|
|
759
|
+
await delay
|
|
760
|
+
raise _chrome_exited_before_ready_error(exited.result())
|
|
761
|
+
finally:
|
|
762
|
+
for pending in (ready, delay):
|
|
763
|
+
if pending is not None and not pending.done():
|
|
764
|
+
pending.cancel()
|
|
765
|
+
with suppress(asyncio.CancelledError):
|
|
766
|
+
await pending
|
|
767
|
+
if not exited.done():
|
|
768
|
+
exited.cancel()
|
|
769
|
+
with suppress(asyncio.CancelledError):
|
|
770
|
+
await exited
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
async def _chrome_debugging_ready(cdp_url: str) -> bool:
|
|
774
|
+
try:
|
|
775
|
+
version = await asyncio.to_thread(_read_chrome_version, cdp_url)
|
|
776
|
+
except Exception:
|
|
777
|
+
return False
|
|
778
|
+
debugger_url = version.get("webSocketDebuggerUrl")
|
|
779
|
+
return isinstance(debugger_url, str) and bool(debugger_url.strip())
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _read_chrome_version(cdp_url: str) -> dict[str, object]:
|
|
783
|
+
url = f"{cdp_url.rstrip('/')}/json/version"
|
|
784
|
+
with urllib.request.urlopen( # noqa: S310 -- The launcher owns this loopback URL.
|
|
785
|
+
url,
|
|
786
|
+
timeout=_CHROME_REQUEST_TIMEOUT_SECONDS,
|
|
787
|
+
) as response:
|
|
788
|
+
value: Any = json.load(response)
|
|
789
|
+
if not isinstance(value, dict):
|
|
790
|
+
raise RuntimeError("Chrome version endpoint returned invalid JSON")
|
|
791
|
+
return value
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _chrome_exited_before_ready_error(returncode: int | None) -> RuntimeError:
|
|
795
|
+
detail = "unknown" if returncode is None else str(returncode)
|
|
796
|
+
return RuntimeError(f"Chrome exited before its debugging port was ready with code {detail}")
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
async def _close_local_chrome(
|
|
800
|
+
process: _ChromeProcess,
|
|
801
|
+
profile: _ChromeProfile,
|
|
802
|
+
) -> None:
|
|
803
|
+
errors: list[BaseException] = []
|
|
804
|
+
try:
|
|
805
|
+
await _close_chrome_process(process)
|
|
806
|
+
except BaseException as error:
|
|
807
|
+
errors.append(error)
|
|
808
|
+
if profile.remove:
|
|
809
|
+
try:
|
|
810
|
+
await _remove_chrome_profile(profile.path)
|
|
811
|
+
except BaseException as error:
|
|
812
|
+
errors.append(error)
|
|
813
|
+
if errors:
|
|
814
|
+
raise _combined_error("Chrome termination and profile cleanup failed", errors)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
async def _close_chrome_process(process: _ChromeProcess) -> None:
|
|
818
|
+
if process.returncode is not None:
|
|
819
|
+
return
|
|
820
|
+
try:
|
|
821
|
+
await _terminate_chrome_process(process.pid, force=False)
|
|
822
|
+
except BaseException as error:
|
|
823
|
+
if not _is_finished_process_error(error):
|
|
824
|
+
raise
|
|
825
|
+
try:
|
|
826
|
+
await asyncio.wait_for(process.wait(), timeout=3)
|
|
827
|
+
return
|
|
828
|
+
except TimeoutError:
|
|
829
|
+
pass
|
|
830
|
+
try:
|
|
831
|
+
await _terminate_chrome_process(process.pid, force=True)
|
|
832
|
+
except BaseException as error:
|
|
833
|
+
if not _is_finished_process_error(error):
|
|
834
|
+
raise
|
|
835
|
+
await process.wait()
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
async def _terminate_chrome_process(pid: int, *, force: bool) -> None:
|
|
839
|
+
if sys.platform == "win32":
|
|
840
|
+
await _run_taskkill(pid, force=force)
|
|
841
|
+
return
|
|
842
|
+
os.killpg(pid, signal.SIGKILL if force else signal.SIGTERM)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
async def _run_taskkill(pid: int, *, force: bool) -> None:
|
|
846
|
+
taskkill = await asyncio.create_subprocess_exec(
|
|
847
|
+
"taskkill",
|
|
848
|
+
"/PID",
|
|
849
|
+
str(pid),
|
|
850
|
+
"/T",
|
|
851
|
+
*(["/F"] if force else []),
|
|
852
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
853
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
854
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
855
|
+
)
|
|
856
|
+
returncode = await taskkill.wait()
|
|
857
|
+
if returncode != 0:
|
|
858
|
+
raise _TaskkillError(returncode)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _is_finished_process_error(error: BaseException) -> bool:
|
|
862
|
+
return isinstance(error, ProcessLookupError) or (
|
|
863
|
+
isinstance(error, _TaskkillError) and error.returncode == 128
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _combined_error(message: str, errors: list[BaseException]) -> BaseException:
|
|
868
|
+
if len(errors) == 1:
|
|
869
|
+
return errors[0]
|
|
870
|
+
if all(isinstance(error, Exception) for error in errors):
|
|
871
|
+
return ExceptionGroup(
|
|
872
|
+
message,
|
|
873
|
+
[error for error in errors if isinstance(error, Exception)],
|
|
874
|
+
)
|
|
875
|
+
return BaseExceptionGroup(message, errors)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
async def _remove_chrome_profile(path: Path) -> None:
|
|
879
|
+
await asyncio.to_thread(shutil.rmtree, path)
|
|
880
|
+
|
|
881
|
+
|
|
681
882
|
def _local_browser_flags(
|
|
682
883
|
options: _LocalBrowserOptions,
|
|
683
884
|
*,
|
|
684
885
|
port: int,
|
|
685
886
|
user_data_dir: Path,
|
|
686
|
-
|
|
887
|
+
disable_sandbox: bool,
|
|
687
888
|
) -> list[str]:
|
|
688
889
|
ignored_default_args = options.ignore_default_args
|
|
689
890
|
ignored_flags = set(ignored_default_args) if isinstance(ignored_default_args, list) else set()
|
|
@@ -709,7 +910,7 @@ def _local_browser_flags(
|
|
|
709
910
|
f"--user-data-dir={user_data_dir}",
|
|
710
911
|
*(["--headless"] if options.headless is True else []),
|
|
711
912
|
*(["--auto-open-devtools-for-tabs"] if options.devtools is True else []),
|
|
712
|
-
*(["--no-sandbox"] if
|
|
913
|
+
*(["--no-sandbox"] if disable_sandbox else []),
|
|
713
914
|
*([f"--proxy-server={options.proxy.server}"] if options.proxy else []),
|
|
714
915
|
*(
|
|
715
916
|
[f"--proxy-bypass-list={options.proxy.bypass}"]
|
|
@@ -729,29 +930,69 @@ def _local_browser_flags(
|
|
|
729
930
|
]
|
|
730
931
|
|
|
731
932
|
|
|
732
|
-
def
|
|
733
|
-
|
|
734
|
-
|
|
933
|
+
def _should_disable_chromium_sandbox(
|
|
934
|
+
options: _LocalBrowserOptions,
|
|
935
|
+
*,
|
|
936
|
+
platform: str | None = None,
|
|
937
|
+
environment: Mapping[str, str] | None = None,
|
|
938
|
+
getuid: Callable[[], int] | None = None,
|
|
939
|
+
) -> bool:
|
|
940
|
+
platform = sys.platform if platform is None else platform
|
|
941
|
+
environment = os.environ if environment is None else environment
|
|
942
|
+
getuid = getattr(os, "getuid", None) if getuid is None else getuid
|
|
943
|
+
return (
|
|
944
|
+
bool(environment.get("CI"))
|
|
945
|
+
or options.chromium_sandbox is False
|
|
946
|
+
or (platform == "linux" and getuid is not None and getuid() == 0)
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _find_chrome_path(
|
|
951
|
+
explicit_path: str | None = None,
|
|
952
|
+
*,
|
|
953
|
+
platform: str | None = None,
|
|
954
|
+
environment: Mapping[str, str] | None = None,
|
|
955
|
+
which: Callable[[str], str | None] = shutil.which,
|
|
956
|
+
is_executable: Callable[[str, str], bool] | None = None,
|
|
957
|
+
) -> str:
|
|
958
|
+
platform = sys.platform if platform is None else platform
|
|
959
|
+
environment = os.environ if environment is None else environment
|
|
960
|
+
is_executable = _is_executable_file if is_executable is None else is_executable
|
|
961
|
+
|
|
962
|
+
if explicit_path is not None:
|
|
963
|
+
if is_executable(explicit_path, platform):
|
|
964
|
+
return explicit_path
|
|
965
|
+
raise RuntimeError(f"Chrome executable {json.dumps(explicit_path)} does not exist")
|
|
966
|
+
|
|
967
|
+
configured = environment.get("CHROME_PATH")
|
|
968
|
+
if configured and is_executable(configured, platform):
|
|
735
969
|
return configured
|
|
736
970
|
|
|
737
|
-
if
|
|
971
|
+
if platform == "darwin":
|
|
738
972
|
candidates = (
|
|
739
973
|
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
740
974
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
975
|
+
"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
|
|
976
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
741
977
|
)
|
|
742
|
-
elif
|
|
978
|
+
elif platform == "win32":
|
|
743
979
|
roots = filter(
|
|
744
980
|
None,
|
|
745
981
|
(
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
982
|
+
environment.get(name)
|
|
983
|
+
for name in (
|
|
984
|
+
"LOCALAPPDATA",
|
|
985
|
+
"PROGRAMFILES",
|
|
986
|
+
"PROGRAMFILES(X86)",
|
|
987
|
+
)
|
|
749
988
|
),
|
|
750
989
|
)
|
|
751
990
|
candidates = tuple(
|
|
752
|
-
str(
|
|
991
|
+
str(PureWindowsPath(root) / "Google" / product / "Application" / "chrome.exe")
|
|
992
|
+
for root in roots
|
|
993
|
+
for product in ("Chrome SxS", "Chrome")
|
|
753
994
|
)
|
|
754
|
-
|
|
995
|
+
elif platform == "linux":
|
|
755
996
|
candidates = tuple(
|
|
756
997
|
path
|
|
757
998
|
for name in (
|
|
@@ -760,16 +1001,43 @@ def _find_chrome_path() -> str:
|
|
|
760
1001
|
"chromium-browser",
|
|
761
1002
|
"chromium",
|
|
762
1003
|
)
|
|
763
|
-
if (path :=
|
|
1004
|
+
if (path := which(name)) is not None
|
|
764
1005
|
)
|
|
1006
|
+
else:
|
|
1007
|
+
raise RuntimeError(f"Chrome launching is not supported on {platform}")
|
|
765
1008
|
|
|
766
1009
|
for candidate in candidates:
|
|
767
|
-
if
|
|
1010
|
+
if is_executable(candidate, platform):
|
|
768
1011
|
return candidate
|
|
769
1012
|
raise RuntimeError("Chrome installation not found; set CHROME_PATH")
|
|
770
1013
|
|
|
771
1014
|
|
|
772
|
-
def
|
|
773
|
-
|
|
774
|
-
|
|
1015
|
+
def _is_executable_file(path: str, platform: str) -> bool:
|
|
1016
|
+
candidate = Path(path)
|
|
1017
|
+
return candidate.is_file() and (platform == "win32" or os.access(candidate, os.X_OK))
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def _resolve_chrome_port(requested_port: int | None) -> int:
|
|
1021
|
+
if requested_port is None:
|
|
1022
|
+
return _available_port()
|
|
1023
|
+
if requested_port < 1 or requested_port > 65_535:
|
|
1024
|
+
raise ValueError("Chrome port must be between 1 and 65535")
|
|
1025
|
+
try:
|
|
1026
|
+
_inspect_chrome_port(requested_port)
|
|
1027
|
+
except OSError as error:
|
|
1028
|
+
if error.errno == errno.EADDRINUSE:
|
|
1029
|
+
raise RuntimeError(
|
|
1030
|
+
f"Chrome debugging port {requested_port} is already in use"
|
|
1031
|
+
) from error
|
|
1032
|
+
raise
|
|
1033
|
+
return requested_port
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _inspect_chrome_port(port: int) -> int:
|
|
1037
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as candidate:
|
|
1038
|
+
candidate.bind(("127.0.0.1", port))
|
|
775
1039
|
return int(candidate.getsockname()[1])
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _available_port() -> int:
|
|
1043
|
+
return _inspect_chrome_port(0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/content-script.js
RENAMED
|
File without changes
|
{stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_extension/manifest.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_generated/__init__.py
RENAMED
|
File without changes
|
{stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/_generated/input_types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.0.3a0.dev1491 → stagehand-4.0.3a0.dev1493}/src/stagehand/browserbase_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|