webpilot-engine 2.0.0__tar.gz → 2.0.2__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.
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/PKG-INFO +1 -1
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/adapters/browser_adapter.py +48 -5
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/adapters/live_session_manager.py +17 -13
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/cli.py +41 -8
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/pyproject.toml +1 -1
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/supervisor/client.py +11 -6
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/supervisor/master_daemon.py +17 -10
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_live_session.py +2 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/webpilot_engine.egg-info/PKG-INFO +1 -1
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/README.md +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/adapters/__init__.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/adapters/base.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/adapters/dom_scripts.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/adapters/envelope_builder.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/adapters/upload_adapter.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/builder.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/config/__init__.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/config/settings.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/constants/__init__.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/constants/contracts.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/constants/selectors.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/constants/timeouts.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/core/__init__.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/core/exceptions.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/core/models.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/engine.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/exceptions.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/mcp_server.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/models.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/orchestration/__init__.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/orchestration/context.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/orchestration/flow_orchestrator.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/orchestration/session_coordinator.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/services/__init__.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/services/auth_navigation_service.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/services/field_interaction_service.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/services/form_filler_service.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/services/inspection_formatter_service.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/services/inspection_service.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/services/reactive_interaction_service.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/setup.cfg +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/supervisor/__init__.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/supervisor/contracts.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/supervisor/shell.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/supervisor/worker_process.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_config_and_builder.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_contracts.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_engine_facade.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_mcp_server.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_orchestration.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_redaction.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_services.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/tests/test_supervisor.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/validators/__init__.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/validators/form_validator.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/validators/payload_validator.py +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/webpilot_engine.egg-info/SOURCES.txt +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/webpilot_engine.egg-info/dependency_links.txt +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/webpilot_engine.egg-info/entry_points.txt +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/webpilot_engine.egg-info/requires.txt +0 -0
- {webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/webpilot_engine.egg-info/top_level.txt +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
from typing import Any, Dict, List, Optional
|
|
8
9
|
from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page, Playwright
|
|
@@ -135,6 +136,43 @@ class BrowserAdapter(BrowserAdapterProtocol):
|
|
|
135
136
|
else:
|
|
136
137
|
raise IndexError(f"Tab index #{index} out of range (Total open tabs: {len(pages)})")
|
|
137
138
|
|
|
139
|
+
def _ensure_browser_binaries(self) -> None:
|
|
140
|
+
"""Verifies Chromium is installed; if not, automatically downloads it or shows a clear error."""
|
|
141
|
+
needs_install = False
|
|
142
|
+
try:
|
|
143
|
+
exec_path = self._pw.chromium.executable_path
|
|
144
|
+
if not exec_path or not os.path.exists(exec_path):
|
|
145
|
+
needs_install = True
|
|
146
|
+
except Exception:
|
|
147
|
+
needs_install = True
|
|
148
|
+
|
|
149
|
+
if needs_install:
|
|
150
|
+
print("\n" + "═" * 70)
|
|
151
|
+
print(" 🚀 WEBPILOT | INITIAL BROWSER SETUP")
|
|
152
|
+
print(" [*] Playwright Chromium binaries not found on this system.")
|
|
153
|
+
print(" [*] Automatically downloading Chromium browser (one-time setup)...")
|
|
154
|
+
print("═" * 70)
|
|
155
|
+
try:
|
|
156
|
+
cmd = [sys.executable, "-m", "playwright", "install", "chromium"]
|
|
157
|
+
subprocess.run(cmd, check=True)
|
|
158
|
+
print("[✓] Chromium installed successfully!")
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
err_banner = (
|
|
161
|
+
"\n" + "═" * 70 + "\n"
|
|
162
|
+
" ❌ WEBPILOT ERROR: CHROMIUM BROWSER NOT INSTALLED\n"
|
|
163
|
+
" " + "═" * 35 + "\n"
|
|
164
|
+
" WebPilot requires Chromium to operate websites.\n"
|
|
165
|
+
" Automatic installation failed (offline or network restriction).\n\n"
|
|
166
|
+
" Please run the following command manually to install the browser:\n\n"
|
|
167
|
+
" playwright install chromium\n"
|
|
168
|
+
" or\n"
|
|
169
|
+
" wp install\n\n"
|
|
170
|
+
f" Details: {exc}\n"
|
|
171
|
+
"═" * 70 + "\n"
|
|
172
|
+
)
|
|
173
|
+
print(err_banner, file=sys.stderr)
|
|
174
|
+
raise RuntimeError("Chromium browser is not installed. Please run: wp install") from None
|
|
175
|
+
|
|
138
176
|
def start(self, cookies_path: Optional[str] = None) -> None:
|
|
139
177
|
"""Starts browser context or connects to existing persistent live session."""
|
|
140
178
|
if self.close_session:
|
|
@@ -146,6 +184,7 @@ class BrowserAdapter(BrowserAdapterProtocol):
|
|
|
146
184
|
return
|
|
147
185
|
|
|
148
186
|
self._pw = sync_playwright().start()
|
|
187
|
+
self._ensure_browser_binaries()
|
|
149
188
|
|
|
150
189
|
existing_session = LiveSessionManager.load_session_state()
|
|
151
190
|
should_use_persistent = self.keep_alive or (existing_session is not None)
|
|
@@ -214,11 +253,15 @@ class BrowserAdapter(BrowserAdapterProtocol):
|
|
|
214
253
|
args=["--disable-blink-features=AutomationControlled"],
|
|
215
254
|
)
|
|
216
255
|
try:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
256
|
+
if sys.platform == "win32":
|
|
257
|
+
ps_c = f'(Get-CimInstance Win32_Process | Where-Object {{ $_.ParentProcessId -eq {os.getpid()} }}).ProcessId | Select-Object -First 1'
|
|
258
|
+
out = subprocess.check_output(["powershell", "-NoProfile", "-Command", ps_c], text=True).strip()
|
|
259
|
+
if out.isdigit():
|
|
260
|
+
self.browser_pid = int(out)
|
|
261
|
+
else:
|
|
262
|
+
out = subprocess.check_output(["pgrep", "-P", str(os.getpid())], text=True).strip().split()
|
|
263
|
+
if out and out[0].isdigit():
|
|
264
|
+
self.browser_pid = int(out[0])
|
|
222
265
|
except Exception:
|
|
223
266
|
pass
|
|
224
267
|
|
|
@@ -188,15 +188,16 @@ class LiveSessionManager:
|
|
|
188
188
|
proc_pid = cls._launch_wmi_process(cmd_str)
|
|
189
189
|
|
|
190
190
|
if proc_pid is None:
|
|
191
|
-
|
|
191
|
+
popen_kwargs: Dict[str, Any] = {
|
|
192
|
+
"stdout": subprocess.DEVNULL,
|
|
193
|
+
"stderr": subprocess.DEVNULL,
|
|
194
|
+
}
|
|
192
195
|
if sys.platform == "win32":
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
creationflags=flags,
|
|
199
|
-
)
|
|
196
|
+
popen_kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
197
|
+
else:
|
|
198
|
+
popen_kwargs["start_new_session"] = True
|
|
199
|
+
|
|
200
|
+
proc = subprocess.Popen(args, **popen_kwargs)
|
|
200
201
|
proc_pid = proc.pid
|
|
201
202
|
|
|
202
203
|
# Wait up to 5 seconds for CDP port to become active
|
|
@@ -303,16 +304,19 @@ if ($proc.ReturnValue -eq 0) {{
|
|
|
303
304
|
if wmi_pid is not None:
|
|
304
305
|
return
|
|
305
306
|
|
|
306
|
-
|
|
307
|
+
popen_kwargs: Dict[str, Any] = {
|
|
308
|
+
"stdout": subprocess.DEVNULL,
|
|
309
|
+
"stderr": subprocess.DEVNULL,
|
|
310
|
+
}
|
|
307
311
|
if sys.platform == "win32":
|
|
308
|
-
|
|
312
|
+
popen_kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
313
|
+
else:
|
|
314
|
+
popen_kwargs["start_new_session"] = True
|
|
309
315
|
|
|
310
316
|
try:
|
|
311
317
|
subprocess.Popen(
|
|
312
318
|
[sys.executable, "-m", "adapters.live_session_manager", str(browser_pid), str(port), str(timeout_seconds)],
|
|
313
|
-
|
|
314
|
-
stderr=subprocess.DEVNULL,
|
|
315
|
-
creationflags=flags,
|
|
319
|
+
**popen_kwargs,
|
|
316
320
|
)
|
|
317
321
|
except Exception:
|
|
318
322
|
pass
|
|
@@ -326,15 +326,48 @@ def main():
|
|
|
326
326
|
p_service.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
|
|
327
327
|
p_service.add_argument("--port", type=int, default=9333, help="Supervisor port")
|
|
328
328
|
|
|
329
|
+
# 5. Browser Binaries Installation Subcommand
|
|
330
|
+
p_install = subparsers.add_parser("install", help="Download and configure required Playwright Chromium browser")
|
|
331
|
+
p_install.add_argument("--all", action="store_true", help="Download all browsers (Chromium, Firefox, WebKit)")
|
|
332
|
+
|
|
329
333
|
args = parser.parse_args()
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
334
|
+
try:
|
|
335
|
+
if args.command == "inspect":
|
|
336
|
+
inspect_cmd(args)
|
|
337
|
+
elif args.command == "apply":
|
|
338
|
+
apply_cmd(args)
|
|
339
|
+
elif args.command == "shell":
|
|
340
|
+
shell_cmd(args)
|
|
341
|
+
elif args.command == "service":
|
|
342
|
+
service_cmd(args)
|
|
343
|
+
elif args.command == "install":
|
|
344
|
+
install_cmd(args)
|
|
345
|
+
except KeyboardInterrupt:
|
|
346
|
+
print("\n[!] Operation cancelled by user.")
|
|
347
|
+
sys.exit(130)
|
|
348
|
+
except Exception as exc:
|
|
349
|
+
print(f"\n[!] Error: {exc}", file=sys.stderr)
|
|
350
|
+
sys.exit(1)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def install_cmd(args: argparse.Namespace) -> None:
|
|
354
|
+
"""Installs required Playwright browser binaries."""
|
|
355
|
+
import subprocess
|
|
356
|
+
print("\n" + "═" * 70)
|
|
357
|
+
print(" 🚀 WEBPILOT | BROWSER BINARY INSTALLER")
|
|
358
|
+
print("═" * 70)
|
|
359
|
+
target = "all" if getattr(args, "all", False) else "chromium"
|
|
360
|
+
cmd = [sys.executable, "-m", "playwright", "install"]
|
|
361
|
+
if target != "all":
|
|
362
|
+
cmd.append("chromium")
|
|
363
|
+
print(f"[*] Downloading and configuring Playwright {target} browser binaries...")
|
|
364
|
+
try:
|
|
365
|
+
subprocess.run(cmd, check=True)
|
|
366
|
+
print("\n[✓] Browser binaries downloaded and configured successfully!")
|
|
367
|
+
except Exception as exc:
|
|
368
|
+
print(f"\n[!] Failed to install browser binaries: {exc}", file=sys.stderr)
|
|
369
|
+
print("Please check your internet connection or run 'playwright install chromium' manually.", file=sys.stderr)
|
|
370
|
+
sys.exit(1)
|
|
338
371
|
|
|
339
372
|
|
|
340
373
|
if __name__ == "__main__":
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webpilot-engine"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.2"
|
|
8
8
|
description = "WebPilot: Enterprise Autonomous Browser Engine, Universal Form Inspector & AI Agent Web Runtime"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -49,15 +49,12 @@ class SupervisorClient:
|
|
|
49
49
|
script_path = os.path.join(PROJECT_ROOT, "supervisor", "master_daemon.py")
|
|
50
50
|
py_exe = sys.executable
|
|
51
51
|
|
|
52
|
-
# Launch detached via WMI outside any parent terminal Job Object
|
|
52
|
+
# Launch detached via WMI outside any parent terminal Job Object on Windows
|
|
53
53
|
if sys.platform == "win32":
|
|
54
54
|
if getattr(sys, "frozen", False):
|
|
55
|
-
py_exe = sys.executable
|
|
56
55
|
cwd = os.path.dirname(py_exe)
|
|
57
56
|
cmd_line = f'`"{py_exe}`" --run-daemon'
|
|
58
57
|
else:
|
|
59
|
-
script_path = os.path.join(PROJECT_ROOT, "supervisor", "master_daemon.py")
|
|
60
|
-
py_exe = sys.executable
|
|
61
58
|
cwd = PROJECT_ROOT
|
|
62
59
|
cmd_line = f'`"{py_exe}`" `"{script_path}`"'
|
|
63
60
|
|
|
@@ -79,12 +76,20 @@ if ($proc.ReturnValue -eq 0) {{
|
|
|
79
76
|
timeout=5,
|
|
80
77
|
)
|
|
81
78
|
else:
|
|
79
|
+
# POSIX / Linux / macOS
|
|
80
|
+
if getattr(sys, "frozen", False):
|
|
81
|
+
cmd = [py_exe, "--run-daemon"]
|
|
82
|
+
cwd = os.path.dirname(py_exe)
|
|
83
|
+
else:
|
|
84
|
+
cmd = [py_exe, script_path]
|
|
85
|
+
cwd = PROJECT_ROOT
|
|
86
|
+
|
|
82
87
|
subprocess.Popen(
|
|
83
|
-
|
|
88
|
+
cmd,
|
|
84
89
|
stdout=subprocess.DEVNULL,
|
|
85
90
|
stderr=subprocess.DEVNULL,
|
|
86
91
|
start_new_session=True,
|
|
87
|
-
cwd=
|
|
92
|
+
cwd=cwd,
|
|
88
93
|
)
|
|
89
94
|
|
|
90
95
|
# Wait up to 4 seconds for supervisor to become active
|
|
@@ -87,15 +87,18 @@ class MasterSupervisor:
|
|
|
87
87
|
worker_cmd = [sys.executable, "-m", "supervisor.worker_process"]
|
|
88
88
|
worker_cwd = PROJECT_ROOT
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
popen_kwargs: Dict[str, Any] = {
|
|
91
|
+
"stdin": subprocess.PIPE,
|
|
92
|
+
"stdout": subprocess.PIPE,
|
|
93
|
+
"stderr": subprocess.PIPE,
|
|
94
|
+
"text": True,
|
|
95
|
+
"bufsize": 1,
|
|
96
|
+
"cwd": worker_cwd,
|
|
97
|
+
}
|
|
98
|
+
if sys.platform != "win32":
|
|
99
|
+
popen_kwargs["start_new_session"] = True
|
|
100
|
+
|
|
101
|
+
self.worker_process = subprocess.Popen(worker_cmd, **popen_kwargs)
|
|
99
102
|
return self.worker_process
|
|
100
103
|
|
|
101
104
|
def execute_action(self, req: SupervisorActionRequest) -> SupervisorActionResponse:
|
|
@@ -193,7 +196,11 @@ class MasterSupervisor:
|
|
|
193
196
|
stderr=subprocess.DEVNULL,
|
|
194
197
|
)
|
|
195
198
|
else:
|
|
196
|
-
|
|
199
|
+
try:
|
|
200
|
+
import signal
|
|
201
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
202
|
+
except Exception:
|
|
203
|
+
proc.kill()
|
|
197
204
|
except Exception:
|
|
198
205
|
pass
|
|
199
206
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import subprocess
|
|
5
|
+
import sys
|
|
5
6
|
import time
|
|
6
7
|
import urllib.request
|
|
7
8
|
import pytest
|
|
@@ -10,6 +11,7 @@ from playwright.sync_api import sync_playwright
|
|
|
10
11
|
from adapters.live_session_manager import LiveSessionManager, DEFAULT_CDP_PORT
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
@pytest.mark.skipif(sys.platform != "win32", reason="WMI is only available on Windows")
|
|
13
15
|
def test_wmi_launch():
|
|
14
16
|
"""Verify _launch_wmi_process returns a real PID."""
|
|
15
17
|
p = sync_playwright().start()
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webpilot_engine-2.0.0 → webpilot_engine-2.0.2}/webpilot_engine.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|