iflow-mcp_janspoerer-mcp_browser_use 0.1.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.
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/METADATA +26 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/RECORD +50 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/WHEEL +5 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/licenses/LICENSE +201 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/top_level.txt +1 -0
- mcp_browser_use/__init__.py +2 -0
- mcp_browser_use/__main__.py +1347 -0
- mcp_browser_use/actions/__init__.py +1 -0
- mcp_browser_use/actions/elements.py +173 -0
- mcp_browser_use/actions/extraction.py +864 -0
- mcp_browser_use/actions/keyboard.py +43 -0
- mcp_browser_use/actions/navigation.py +73 -0
- mcp_browser_use/actions/screenshots.py +85 -0
- mcp_browser_use/browser/__init__.py +1 -0
- mcp_browser_use/browser/chrome.py +150 -0
- mcp_browser_use/browser/chrome_executable.py +204 -0
- mcp_browser_use/browser/chrome_launcher.py +330 -0
- mcp_browser_use/browser/chrome_process.py +104 -0
- mcp_browser_use/browser/devtools.py +230 -0
- mcp_browser_use/browser/driver.py +322 -0
- mcp_browser_use/browser/process.py +133 -0
- mcp_browser_use/cleaners.py +530 -0
- mcp_browser_use/config/__init__.py +30 -0
- mcp_browser_use/config/environment.py +155 -0
- mcp_browser_use/config/paths.py +97 -0
- mcp_browser_use/constants.py +68 -0
- mcp_browser_use/context.py +150 -0
- mcp_browser_use/context_pack.py +85 -0
- mcp_browser_use/decorators/__init__.py +13 -0
- mcp_browser_use/decorators/ensure.py +84 -0
- mcp_browser_use/decorators/envelope.py +83 -0
- mcp_browser_use/decorators/locking.py +172 -0
- mcp_browser_use/helpers.py +173 -0
- mcp_browser_use/helpers_context.py +261 -0
- mcp_browser_use/locking/__init__.py +1 -0
- mcp_browser_use/locking/action_lock.py +190 -0
- mcp_browser_use/locking/file_mutex.py +139 -0
- mcp_browser_use/locking/window_registry.py +178 -0
- mcp_browser_use/tools/__init__.py +59 -0
- mcp_browser_use/tools/browser_management.py +260 -0
- mcp_browser_use/tools/debugging.py +195 -0
- mcp_browser_use/tools/extraction.py +58 -0
- mcp_browser_use/tools/interaction.py +323 -0
- mcp_browser_use/tools/navigation.py +84 -0
- mcp_browser_use/tools/screenshots.py +116 -0
- mcp_browser_use/utils/__init__.py +1 -0
- mcp_browser_use/utils/diagnostics.py +85 -0
- mcp_browser_use/utils/html_utils.py +118 -0
- mcp_browser_use/utils/retry.py +57 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""WebDriver creation and window management."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from selenium import webdriver
|
|
9
|
+
from selenium.common.exceptions import (
|
|
10
|
+
NoSuchWindowException,
|
|
11
|
+
WebDriverException,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Import context for state management
|
|
18
|
+
from ..context import get_context
|
|
19
|
+
from .devtools import _ensure_debugger_ready, _handle_for_target
|
|
20
|
+
from .process import make_process_tag, ensure_process_tag, chromedriver_log_path
|
|
21
|
+
from ..locking.window_registry import (
|
|
22
|
+
cleanup_orphaned_windows,
|
|
23
|
+
_register_window,
|
|
24
|
+
_unregister_window,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ensure_driver() -> None:
|
|
29
|
+
"""Attach Selenium to the debuggable Chrome instance (headed by default)."""
|
|
30
|
+
ctx = get_context()
|
|
31
|
+
|
|
32
|
+
if ctx.driver is not None:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
_ensure_debugger_ready(ctx.config)
|
|
36
|
+
|
|
37
|
+
if not (ctx.debugger_host and ctx.debugger_port):
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
ctx.driver = create_webdriver(
|
|
41
|
+
ctx.debugger_host,
|
|
42
|
+
ctx.debugger_port,
|
|
43
|
+
ctx.config
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validate_window_context(driver: webdriver.Chrome, expected_target_id: str) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Validate that the current window context matches the expected target.
|
|
50
|
+
Returns True if validation passes, False otherwise.
|
|
51
|
+
Handles NoSuchWindowException gracefully.
|
|
52
|
+
"""
|
|
53
|
+
if not expected_target_id:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Check if current window handle exists and matches expected target
|
|
58
|
+
current_handle = driver.current_window_handle
|
|
59
|
+
if current_handle and current_handle.endswith(expected_target_id):
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
# Double-check by getting target info via CDP
|
|
63
|
+
try:
|
|
64
|
+
info = driver.execute_cdp_cmd("Target.getTargetInfo", {}) or {}
|
|
65
|
+
current_target = (info.get("targetInfo") or {}).get("targetId") or info.get("targetId")
|
|
66
|
+
return current_target == expected_target_id
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
return False
|
|
71
|
+
except Exception:
|
|
72
|
+
# NoSuchWindowException or other window-related exceptions
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ensure_singleton_window(driver: webdriver.Chrome):
|
|
77
|
+
"""Ensure we have a singleton window for this process."""
|
|
78
|
+
ctx = get_context()
|
|
79
|
+
|
|
80
|
+
# 0) If we already have a target, validate context
|
|
81
|
+
if ctx.target_id:
|
|
82
|
+
if _validate_window_context(driver, ctx.target_id):
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Context validation failed - attempt recovery
|
|
86
|
+
h = _handle_for_target(driver, ctx.target_id)
|
|
87
|
+
if h:
|
|
88
|
+
try:
|
|
89
|
+
driver.switch_to.window(h)
|
|
90
|
+
if _validate_window_context(driver, ctx.target_id):
|
|
91
|
+
return
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# Recovery failed - clear target and recreate
|
|
96
|
+
ctx.reset_window_state()
|
|
97
|
+
|
|
98
|
+
# 1) Create new window if we don't have a target
|
|
99
|
+
if not ctx.target_id:
|
|
100
|
+
# Cleanup orphaned windows
|
|
101
|
+
try:
|
|
102
|
+
cleanup_orphaned_windows(driver)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.debug(f"Window cleanup failed (non-critical): {e}")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
win = driver.execute_cdp_cmd("Browser.createWindow", {"state": "normal"})
|
|
108
|
+
if not isinstance(win, dict):
|
|
109
|
+
raise RuntimeError(f"Browser.createWindow returned {win!r}")
|
|
110
|
+
|
|
111
|
+
ctx.window_id = win.get("windowId")
|
|
112
|
+
ctx.target_id = win.get("targetId")
|
|
113
|
+
|
|
114
|
+
if not ctx.target_id:
|
|
115
|
+
# Fallback
|
|
116
|
+
t = driver.execute_cdp_cmd("Target.createTarget", {"url": "about:blank", "newWindow": True})
|
|
117
|
+
if not isinstance(t, dict) or "targetId" not in t:
|
|
118
|
+
raise RuntimeError(f"Target.createTarget returned {t!r}")
|
|
119
|
+
|
|
120
|
+
ctx.target_id = t["targetId"]
|
|
121
|
+
|
|
122
|
+
if not ctx.window_id:
|
|
123
|
+
try:
|
|
124
|
+
w = driver.execute_cdp_cmd("Browser.getWindowForTarget", {"targetId": ctx.target_id}) or {}
|
|
125
|
+
ctx.window_id = w.get("windowId")
|
|
126
|
+
except Exception:
|
|
127
|
+
ctx.window_id = None
|
|
128
|
+
except Exception:
|
|
129
|
+
# Last resort
|
|
130
|
+
t = driver.execute_cdp_cmd("Target.createTarget", {"url": "about:blank", "newWindow": True})
|
|
131
|
+
if not isinstance(t, dict) or "targetId" not in t:
|
|
132
|
+
raise RuntimeError(f"Target.createTarget returned {t!r}")
|
|
133
|
+
|
|
134
|
+
ctx.target_id = t["targetId"]
|
|
135
|
+
try:
|
|
136
|
+
w = driver.execute_cdp_cmd("Browser.getWindowForTarget", {"targetId": ctx.target_id}) or {}
|
|
137
|
+
ctx.window_id = w.get("windowId")
|
|
138
|
+
except Exception:
|
|
139
|
+
ctx.window_id = None
|
|
140
|
+
|
|
141
|
+
# 2) Map targetId -> Selenium handle
|
|
142
|
+
h = _handle_for_target(driver, ctx.target_id)
|
|
143
|
+
if not h:
|
|
144
|
+
for _ in range(20):
|
|
145
|
+
time.sleep(0.05)
|
|
146
|
+
h = _handle_for_target(driver, ctx.target_id)
|
|
147
|
+
if h:
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
if h:
|
|
151
|
+
driver.switch_to.window(h)
|
|
152
|
+
|
|
153
|
+
if not _validate_window_context(driver, ctx.target_id):
|
|
154
|
+
raise RuntimeError(f"Failed to establish correct window context for target {ctx.target_id}")
|
|
155
|
+
|
|
156
|
+
# Register window
|
|
157
|
+
try:
|
|
158
|
+
owner = ensure_process_tag()
|
|
159
|
+
_register_window(owner, ctx.target_id, ctx.window_id)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.debug(f"Window registration failed (non-critical): {e}")
|
|
162
|
+
else:
|
|
163
|
+
raise RuntimeError(f"Failed to find window handle for target {ctx.target_id}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _ensure_driver_and_window() -> None:
|
|
167
|
+
"""Ensure both driver and window are ready."""
|
|
168
|
+
_ensure_driver()
|
|
169
|
+
|
|
170
|
+
ctx = get_context()
|
|
171
|
+
if ctx.driver is None:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
_ensure_singleton_window(ctx.driver)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _close_extra_blank_windows_safe(driver, exclude_handles=None) -> int:
|
|
178
|
+
"""Close extra blank windows, only within our own OS window."""
|
|
179
|
+
exclude = set(exclude_handles or ())
|
|
180
|
+
|
|
181
|
+
ctx = get_context()
|
|
182
|
+
own_window_id = ctx.window_id
|
|
183
|
+
if own_window_id is None:
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
keep = driver.current_window_handle
|
|
188
|
+
except Exception:
|
|
189
|
+
keep = None
|
|
190
|
+
|
|
191
|
+
closed = 0
|
|
192
|
+
for h in list(getattr(driver, "window_handles", [])):
|
|
193
|
+
if h in exclude or (keep and h == keep):
|
|
194
|
+
continue
|
|
195
|
+
try:
|
|
196
|
+
driver.switch_to.window(h)
|
|
197
|
+
# Map this handle -> targetId -> windowId
|
|
198
|
+
info = driver.execute_cdp_cmd("Target.getTargetInfo", {}) or {}
|
|
199
|
+
tid = (info.get("targetInfo") or {}).get("targetId") or info.get("targetId")
|
|
200
|
+
if not tid:
|
|
201
|
+
continue
|
|
202
|
+
w = driver.execute_cdp_cmd("Browser.getWindowForTarget", {"targetId": tid}) or {}
|
|
203
|
+
if w.get("windowId") != own_window_id:
|
|
204
|
+
# Belongs to another agent's OS window; do not touch
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
url = (driver.current_url or "").lower()
|
|
208
|
+
title = (driver.title or "").strip()
|
|
209
|
+
if url in ("about:blank", "chrome://newtab/") or (not url and not title):
|
|
210
|
+
driver.close()
|
|
211
|
+
closed += 1
|
|
212
|
+
except Exception:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# Restore our original window if it still exists
|
|
216
|
+
if keep and keep in getattr(driver, "window_handles", []):
|
|
217
|
+
try:
|
|
218
|
+
driver.switch_to.window(keep)
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
return closed
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def close_singleton_window() -> bool:
|
|
225
|
+
"""Close the singleton window without quitting Chrome."""
|
|
226
|
+
ctx = get_context()
|
|
227
|
+
|
|
228
|
+
if ctx.driver is None or not ctx.target_id:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
closed = False
|
|
232
|
+
try:
|
|
233
|
+
ctx.driver.execute_cdp_cmd("Target.closeTarget", {"targetId": ctx.target_id})
|
|
234
|
+
closed = True
|
|
235
|
+
except Exception:
|
|
236
|
+
# Fallback
|
|
237
|
+
try:
|
|
238
|
+
h = _handle_for_target(ctx.driver, ctx.target_id)
|
|
239
|
+
if h:
|
|
240
|
+
ctx.driver.switch_to.window(h)
|
|
241
|
+
ctx.driver.close()
|
|
242
|
+
closed = True
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
# Unregister window
|
|
247
|
+
if closed:
|
|
248
|
+
try:
|
|
249
|
+
owner = ensure_process_tag()
|
|
250
|
+
_unregister_window(owner)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.debug(f"Window unregistration failed (non-critical): {e}")
|
|
253
|
+
|
|
254
|
+
ctx.reset_window_state()
|
|
255
|
+
return closed
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def create_webdriver(debugger_host: str, debugger_port: int, config: dict) -> webdriver.Chrome:
|
|
259
|
+
from selenium.webdriver.chrome.options import Options
|
|
260
|
+
from selenium.webdriver.chrome.service import Service as ChromeService
|
|
261
|
+
|
|
262
|
+
options = Options()
|
|
263
|
+
chrome_path = config.get("chrome_path")
|
|
264
|
+
if chrome_path:
|
|
265
|
+
options.binary_location = chrome_path
|
|
266
|
+
options.add_experimental_option("debuggerAddress", f"{debugger_host}:{debugger_port}")
|
|
267
|
+
|
|
268
|
+
# Handle differing Selenium versions that accept log_output vs. log_path
|
|
269
|
+
log_file = chromedriver_log_path(config)
|
|
270
|
+
try:
|
|
271
|
+
service = ChromeService(log_output=log_file) # newer Selenium
|
|
272
|
+
except TypeError:
|
|
273
|
+
service = ChromeService(log_path=log_file) # older Selenium
|
|
274
|
+
|
|
275
|
+
driver = webdriver.Chrome(service=service, options=options)
|
|
276
|
+
return driver
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _cleanup_own_blank_tabs(driver):
|
|
280
|
+
handle = getattr(driver, "current_window_handle", None)
|
|
281
|
+
try:
|
|
282
|
+
_close_extra_blank_windows_safe(
|
|
283
|
+
driver,
|
|
284
|
+
exclude_handles={handle} if handle else None,
|
|
285
|
+
)
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def get_chromedriver_capability_version(driver: Optional[webdriver.Chrome] = None) -> Optional[str]:
|
|
291
|
+
"""
|
|
292
|
+
Best effort Chromedriver version string.
|
|
293
|
+
- If a driver is provided, prefer driver.capabilities['chromedriverVersion'].
|
|
294
|
+
- Else, fall back to `chromedriver --version` if available in PATH.
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
if driver:
|
|
298
|
+
v = driver.capabilities.get("chromedriverVersion")
|
|
299
|
+
if isinstance(v, str) and v:
|
|
300
|
+
# Typically like "114.0.5735.90 (some hash)"
|
|
301
|
+
return v.split(" ")[0]
|
|
302
|
+
path = shutil.which("chromedriver")
|
|
303
|
+
if path:
|
|
304
|
+
out = subprocess.check_output([path, "--version"], stderr=subprocess.STDOUT).decode().strip()
|
|
305
|
+
return out
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
__all__ = [
|
|
312
|
+
'create_webdriver',
|
|
313
|
+
'_ensure_driver',
|
|
314
|
+
'_ensure_driver_and_window',
|
|
315
|
+
'_ensure_singleton_window',
|
|
316
|
+
'close_singleton_window',
|
|
317
|
+
'_cleanup_own_blank_tabs',
|
|
318
|
+
'_close_extra_blank_windows_safe',
|
|
319
|
+
'get_chromedriver_capability_version',
|
|
320
|
+
'_validate_window_context',
|
|
321
|
+
'ensure_process_tag',
|
|
322
|
+
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Process and port management."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import socket
|
|
7
|
+
import tempfile
|
|
8
|
+
import psutil
|
|
9
|
+
from typing import Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from ..constants import RENDEZVOUS_TTL_SEC
|
|
12
|
+
from ..config.environment import profile_key
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_port_open(host: str, port: int, timeout: float = 0.25) -> bool:
|
|
16
|
+
"""Check if a port is open."""
|
|
17
|
+
try:
|
|
18
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
19
|
+
return True
|
|
20
|
+
except Exception:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_free_port() -> int:
|
|
25
|
+
"""Get a free port by binding to port 0."""
|
|
26
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
27
|
+
s.bind(("127.0.0.1", 0))
|
|
28
|
+
return s.getsockname()[1]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def make_process_tag() -> str:
|
|
32
|
+
"""Create a unique process tag."""
|
|
33
|
+
import uuid
|
|
34
|
+
return f"agent:{uuid.uuid4().hex}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ensure_process_tag() -> str:
|
|
38
|
+
"""
|
|
39
|
+
Get or create the process tag for this session.
|
|
40
|
+
|
|
41
|
+
Uses the context to store the tag persistently across the session.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The process tag string
|
|
45
|
+
"""
|
|
46
|
+
from ..context import get_context
|
|
47
|
+
|
|
48
|
+
ctx = get_context()
|
|
49
|
+
if ctx.process_tag is None:
|
|
50
|
+
ctx.process_tag = make_process_tag()
|
|
51
|
+
return ctx.process_tag
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_json(path: str) -> Optional[dict]:
|
|
55
|
+
"""Read JSON file, return None on error."""
|
|
56
|
+
try:
|
|
57
|
+
with open(path, "r") as f:
|
|
58
|
+
return json.load(f)
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def rendezvous_path(config: dict) -> str:
|
|
64
|
+
"""Get path to rendezvous file for this profile."""
|
|
65
|
+
return os.path.join(tempfile.gettempdir(), f"mcp_chrome_rendezvous_{profile_key(config)}.json")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def chromedriver_log_path(config: dict) -> str:
|
|
69
|
+
"""Get path to chromedriver log file for this profile and process."""
|
|
70
|
+
return os.path.join(tempfile.gettempdir(), f"chromedriver_shared_{profile_key(config)}_{os.getpid()}.log")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_rendezvous(config: dict) -> Tuple[Optional[int], Optional[int]]:
|
|
74
|
+
"""
|
|
75
|
+
Read rendezvous file to find existing Chrome debug port and PID.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Tuple of (port, pid) or (None, None) if not found/invalid
|
|
79
|
+
"""
|
|
80
|
+
from .devtools import is_debugger_listening
|
|
81
|
+
|
|
82
|
+
path = rendezvous_path(config)
|
|
83
|
+
try:
|
|
84
|
+
if not os.path.exists(path):
|
|
85
|
+
return None, None
|
|
86
|
+
if (time.time() - os.path.getmtime(path)) > RENDEZVOUS_TTL_SEC:
|
|
87
|
+
return None, None
|
|
88
|
+
data = _read_json(path) or {}
|
|
89
|
+
port = int(data.get("port", 0)) or None
|
|
90
|
+
pid = int(data.get("pid", 0)) or None
|
|
91
|
+
if not port or not pid:
|
|
92
|
+
return None, None
|
|
93
|
+
if not psutil.pid_exists(pid):
|
|
94
|
+
return None, None
|
|
95
|
+
if not is_debugger_listening("127.0.0.1", port):
|
|
96
|
+
return None, None
|
|
97
|
+
return port, pid
|
|
98
|
+
except Exception:
|
|
99
|
+
return None, None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_rendezvous(config: dict, port: int, pid: int) -> None:
|
|
103
|
+
"""Write rendezvous file with Chrome debug port and PID."""
|
|
104
|
+
path = rendezvous_path(config)
|
|
105
|
+
tmp = path + ".tmp"
|
|
106
|
+
data = {"port": port, "pid": pid, "ts": time.time()}
|
|
107
|
+
try:
|
|
108
|
+
with open(tmp, "w") as f:
|
|
109
|
+
json.dump(data, f)
|
|
110
|
+
os.replace(tmp, path)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def clear_rendezvous(config: dict) -> None:
|
|
116
|
+
"""Remove rendezvous file."""
|
|
117
|
+
try:
|
|
118
|
+
os.remove(rendezvous_path(config))
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
'_is_port_open',
|
|
125
|
+
'get_free_port',
|
|
126
|
+
'make_process_tag',
|
|
127
|
+
'_read_json',
|
|
128
|
+
'read_rendezvous',
|
|
129
|
+
'write_rendezvous',
|
|
130
|
+
'clear_rendezvous',
|
|
131
|
+
'rendezvous_path',
|
|
132
|
+
'chromedriver_log_path',
|
|
133
|
+
]
|