webtap-tool 0.1.2__tar.gz → 0.1.4__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.
Potentially problematic release.
This version of webtap-tool might be problematic. Click here for more details.
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/CHANGELOG.md +46 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/PKG-INFO +1 -1
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/extension/popup.html +9 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/extension/popup.js +50 -11
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/pyproject.toml +1 -1
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/__init__.py +11 -3
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/api.py +106 -31
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/app.py +18 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/query.py +2 -1
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/launch.py +3 -17
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/body.py +1 -1
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/console.py +1 -1
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/fetch.py +1 -1
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/network.py +2 -2
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/setup.py +20 -10
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/.gitignore +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/ARCHITECTURE.md +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/README.md +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/data/filters.json +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/extension/manifest.json +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/llms.txt +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/VISION.md +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/README.md +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/__init__.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/schema/README.md +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/schema/cdp_protocol.json +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/schema/cdp_version.json +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/session.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/DEVELOPER_GUIDE.md +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/TIPS.md +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/__init__.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/_builders.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/_errors.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/_tips.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/_utils.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/body.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/connection.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/console.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/events.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/fetch.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/filters.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/inspect.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/javascript.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/navigation.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/network.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/setup.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/filters.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/README.md +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/__init__.py +0 -0
- {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/main.py +0 -0
|
@@ -15,6 +15,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
|
|
16
16
|
### Removed
|
|
17
17
|
|
|
18
|
+
## [0.1.4] - 2025-09-08
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Request timeout handling (3 seconds) in Chrome extension for better error detection
|
|
22
|
+
- Comprehensive `cleanup()` method for proper resource management on exit
|
|
23
|
+
- `atexit` registration to ensure cleanup happens even on unexpected termination
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **API Consolidation**: Merged `/pages` and `/instance` endpoints into single `/info` endpoint
|
|
27
|
+
- Enhanced `/status` endpoint to include fetch details inline (eliminates separate API call)
|
|
28
|
+
- Improved error messages in extension to distinguish between timeout, server not running, and other failures
|
|
29
|
+
- API server shutdown now uses `asyncio.run()` for proper task cleanup
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Graceful shutdown issue that left zombie tasks with "Task was destroyed but it is pending" error
|
|
33
|
+
- API server cleanup on application exit now properly cancels all async tasks
|
|
34
|
+
- Extension error handling now provides specific feedback for different failure modes
|
|
35
|
+
|
|
36
|
+
### Removed
|
|
37
|
+
- `/instance` endpoint (functionality merged into `/info`)
|
|
38
|
+
- `/fetch/paused` endpoint (data now included in `/status` response)
|
|
39
|
+
|
|
40
|
+
## [0.1.3] - 2025-09-05
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Multi-instance WebTap support with first-come-first-serve port management
|
|
44
|
+
- `/instance` endpoint to show WebTap instance information (PID, connected page, events)
|
|
45
|
+
- `/release` endpoint for graceful API port handoff between instances
|
|
46
|
+
- Chrome extension "Switch WebTap Instance" button for managing multiple instances
|
|
47
|
+
- Instance status display in extension popup showing PID and event count
|
|
48
|
+
- Automatic reconnection in extension after instance switching
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- API server now checks port availability before starting (port 8765)
|
|
52
|
+
- Chrome profile launch strategy uses bindfs mounting instead of symlinks
|
|
53
|
+
- Service docstrings simplified (removed redundant "Internal service for" prefixes)
|
|
54
|
+
- Added `api_thread` tracking to WebTapState for proper thread lifecycle management
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
- SQL numeric comparisons in query builder now use string comparison instead of CAST
|
|
58
|
+
- Type annotations improved with proper union types (`threading.Thread | None`)
|
|
59
|
+
- Network service HTTP status filtering uses string comparison to prevent SQL errors
|
|
60
|
+
- Extension connection handling during instance transitions
|
|
61
|
+
|
|
62
|
+
### Removed
|
|
63
|
+
|
|
18
64
|
## [0.1.2] - 2025-09-05
|
|
19
65
|
|
|
20
66
|
### Added
|
|
@@ -150,6 +150,15 @@
|
|
|
150
150
|
|
|
151
151
|
<div class="divider"></div>
|
|
152
152
|
|
|
153
|
+
<button
|
|
154
|
+
id="switchInstance"
|
|
155
|
+
style="width: 100%; font-size: 11px; margin-bottom: 8px"
|
|
156
|
+
>
|
|
157
|
+
Switch WebTap Instance
|
|
158
|
+
</button>
|
|
159
|
+
|
|
160
|
+
<div class="divider"></div>
|
|
161
|
+
|
|
153
162
|
<div class="filter-section">
|
|
154
163
|
<div
|
|
155
164
|
style="
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
// API helper - communicate with WebTap service
|
|
2
2
|
async function api(endpoint, method = "GET", body = null) {
|
|
3
3
|
try {
|
|
4
|
-
const opts = {
|
|
4
|
+
const opts = {
|
|
5
|
+
method,
|
|
6
|
+
// Add timeout to detect unresponsive server faster
|
|
7
|
+
signal: AbortSignal.timeout(3000)
|
|
8
|
+
};
|
|
5
9
|
if (body) {
|
|
6
10
|
opts.headers = { "Content-Type": "application/json" };
|
|
7
11
|
opts.body = JSON.stringify(body);
|
|
8
12
|
}
|
|
9
13
|
const resp = await fetch(`http://localhost:8765${endpoint}`, opts);
|
|
14
|
+
if (!resp.ok) {
|
|
15
|
+
return { error: `HTTP ${resp.status}: ${resp.statusText}` };
|
|
16
|
+
}
|
|
10
17
|
return await resp.json();
|
|
11
18
|
} catch (e) {
|
|
19
|
+
// Better error messages
|
|
20
|
+
if (e.name === 'AbortError') {
|
|
21
|
+
return { error: "WebTap not responding (timeout)" };
|
|
22
|
+
}
|
|
23
|
+
if (e.message.includes('Failed to fetch')) {
|
|
24
|
+
return { error: "WebTap not running" };
|
|
25
|
+
}
|
|
12
26
|
return { error: e.message };
|
|
13
27
|
}
|
|
14
28
|
}
|
|
@@ -17,15 +31,20 @@ async function api(endpoint, method = "GET", body = null) {
|
|
|
17
31
|
|
|
18
32
|
// Load available pages from WebTap
|
|
19
33
|
async function loadPages() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
// Use combined /info endpoint for single round trip
|
|
35
|
+
const info = await api("/info");
|
|
36
|
+
|
|
37
|
+
if (info.error) {
|
|
23
38
|
document.getElementById("pageList").innerHTML =
|
|
24
|
-
|
|
39
|
+
`<option disabled>${info.error === "WebTap not initialized" ? "WebTap not running" : "Error loading pages"}</option>`;
|
|
25
40
|
return;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
// Update instance info tooltip
|
|
44
|
+
document.getElementById("switchInstance").title =
|
|
45
|
+
`PID: ${info.pid} | Events: ${info.events}`;
|
|
46
|
+
|
|
47
|
+
const pages = info.pages || [];
|
|
29
48
|
const select = document.getElementById("pageList");
|
|
30
49
|
|
|
31
50
|
select.innerHTML = "";
|
|
@@ -247,6 +266,27 @@ document.getElementById("disableAllFilters").onclick = async () => {
|
|
|
247
266
|
setTimeout(updateFilters, 100);
|
|
248
267
|
};
|
|
249
268
|
|
|
269
|
+
// Switch to a different WebTap instance
|
|
270
|
+
document.getElementById("switchInstance").onclick = async () => {
|
|
271
|
+
const result = await api("/release", "POST");
|
|
272
|
+
if (!result.error) {
|
|
273
|
+
document.getElementById("status").innerHTML =
|
|
274
|
+
'<span style="color: #666">Port released. Start new WebTap.</span>';
|
|
275
|
+
// Disable controls until reconnected
|
|
276
|
+
document.getElementById("connect").disabled = true;
|
|
277
|
+
document.getElementById("disconnect").disabled = true;
|
|
278
|
+
document.getElementById("fetchToggle").disabled = true;
|
|
279
|
+
// Try to reconnect after delay
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
loadPages();
|
|
282
|
+
updateStatus();
|
|
283
|
+
}, 2000);
|
|
284
|
+
} else {
|
|
285
|
+
document.getElementById("status").innerHTML =
|
|
286
|
+
`<span class="error">Error: ${result.error}</span>`;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
250
290
|
// Update all status from server - single source of truth
|
|
251
291
|
async function updateStatus() {
|
|
252
292
|
const status = await api("/status");
|
|
@@ -268,13 +308,12 @@ async function updateStatus() {
|
|
|
268
308
|
document.getElementById("status").innerHTML =
|
|
269
309
|
`<span class="connected">Connected</span> - Events: ${status.events}`;
|
|
270
310
|
|
|
271
|
-
//
|
|
272
|
-
if (status.fetch_enabled) {
|
|
273
|
-
const fetchDetails = await api("/fetch/paused");
|
|
311
|
+
// Use fetch details from enhanced status (no extra API call needed)
|
|
312
|
+
if (status.fetch_enabled && status.fetch_details) {
|
|
274
313
|
updateFetchStatus(
|
|
275
314
|
true,
|
|
276
|
-
status.
|
|
277
|
-
|
|
315
|
+
status.fetch_details.paused_count || 0,
|
|
316
|
+
status.fetch_details.response_stage || false,
|
|
278
317
|
);
|
|
279
318
|
} else {
|
|
280
319
|
updateFetchStatus(false);
|
|
@@ -9,6 +9,7 @@ PUBLIC API:
|
|
|
9
9
|
- main: Entry point function for CLI
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import atexit
|
|
12
13
|
import sys
|
|
13
14
|
import logging
|
|
14
15
|
|
|
@@ -45,10 +46,17 @@ def main():
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def _start_api_server_safely():
|
|
48
|
-
"""Start API server with error handling."""
|
|
49
|
+
"""Start API server with error handling and cleanup registration."""
|
|
49
50
|
try:
|
|
50
|
-
start_api_server(app.state)
|
|
51
|
-
|
|
51
|
+
thread = start_api_server(app.state)
|
|
52
|
+
if thread and app.state:
|
|
53
|
+
app.state.api_thread = thread
|
|
54
|
+
logger.info("API server started on port 8765")
|
|
55
|
+
|
|
56
|
+
# Register cleanup to shut down API server on exit
|
|
57
|
+
atexit.register(lambda: app.state.cleanup() if app.state else None)
|
|
58
|
+
else:
|
|
59
|
+
logger.info("Port 8765 in use by another instance")
|
|
52
60
|
except Exception as e:
|
|
53
61
|
logger.warning(f"Failed to start API server: {e}")
|
|
54
62
|
|
|
@@ -5,6 +5,8 @@ PUBLIC API:
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
8
10
|
import threading
|
|
9
11
|
from typing import Any, Dict
|
|
10
12
|
|
|
@@ -48,22 +50,54 @@ api.add_middleware(
|
|
|
48
50
|
app_state = None
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
@api.get("/
|
|
52
|
-
async def
|
|
53
|
-
"""
|
|
53
|
+
@api.get("/health")
|
|
54
|
+
async def health_check() -> Dict[str, Any]:
|
|
55
|
+
"""Quick health check endpoint for extension."""
|
|
56
|
+
return {"status": "ok", "pid": os.getpid()}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@api.get("/info")
|
|
60
|
+
async def get_info() -> Dict[str, Any]:
|
|
61
|
+
"""Combined endpoint for pages and instance info - reduces round trips."""
|
|
54
62
|
if not app_state:
|
|
55
|
-
return {"error": "WebTap not initialized", "pages": []}
|
|
63
|
+
return {"error": "WebTap not initialized", "pages": [], "pid": os.getpid()}
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
# Get pages
|
|
66
|
+
pages_data = app_state.service.list_pages()
|
|
67
|
+
|
|
68
|
+
# Get instance info
|
|
69
|
+
connected_to = None
|
|
70
|
+
if app_state.cdp.is_connected and app_state.cdp.page_info:
|
|
71
|
+
connected_to = app_state.cdp.page_info.get("title", "Untitled")
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"pid": os.getpid(),
|
|
75
|
+
"connected_to": connected_to,
|
|
76
|
+
"events": app_state.service.event_count,
|
|
77
|
+
"pages": pages_data.get("pages", []),
|
|
78
|
+
"error": pages_data.get("error"),
|
|
79
|
+
}
|
|
58
80
|
|
|
59
81
|
|
|
60
82
|
@api.get("/status")
|
|
61
83
|
async def get_status() -> Dict[str, Any]:
|
|
62
|
-
"""Get
|
|
84
|
+
"""Get comprehensive status including connection, events, and fetch details."""
|
|
63
85
|
if not app_state:
|
|
64
86
|
return {"connected": False, "error": "WebTap not initialized", "events": 0}
|
|
65
87
|
|
|
66
|
-
|
|
88
|
+
status = app_state.service.get_status()
|
|
89
|
+
|
|
90
|
+
# Add fetch details if fetch is enabled
|
|
91
|
+
if status.get("fetch_enabled"):
|
|
92
|
+
fetch_service = app_state.service.fetch
|
|
93
|
+
paused_list = fetch_service.get_paused_list()
|
|
94
|
+
status["fetch_details"] = {
|
|
95
|
+
"paused_requests": paused_list,
|
|
96
|
+
"paused_count": len(paused_list),
|
|
97
|
+
"response_stage": fetch_service.enable_response_stage,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return status
|
|
67
101
|
|
|
68
102
|
|
|
69
103
|
@api.post("/connect")
|
|
@@ -106,25 +140,6 @@ async def set_fetch_interception(request: FetchRequest) -> Dict[str, Any]:
|
|
|
106
140
|
return result
|
|
107
141
|
|
|
108
142
|
|
|
109
|
-
@api.get("/fetch/paused")
|
|
110
|
-
async def get_paused_requests() -> Dict[str, Any]:
|
|
111
|
-
"""Get list of currently paused fetch requests."""
|
|
112
|
-
if not app_state:
|
|
113
|
-
return {"error": "WebTap not initialized", "requests": []}
|
|
114
|
-
|
|
115
|
-
fetch_service = app_state.service.fetch
|
|
116
|
-
if not fetch_service.enabled:
|
|
117
|
-
return {"enabled": False, "requests": []}
|
|
118
|
-
|
|
119
|
-
paused_list = fetch_service.get_paused_list()
|
|
120
|
-
return {
|
|
121
|
-
"enabled": True,
|
|
122
|
-
"requests": paused_list,
|
|
123
|
-
"count": len(paused_list),
|
|
124
|
-
"response_stage": fetch_service.enable_response_stage,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
143
|
@api.get("/filters/status")
|
|
129
144
|
async def get_filter_status() -> Dict[str, Any]:
|
|
130
145
|
"""Get current filter configuration and enabled categories."""
|
|
@@ -184,7 +199,26 @@ async def disable_all_filters() -> Dict[str, Any]:
|
|
|
184
199
|
return {"enabled": [], "total": 0}
|
|
185
200
|
|
|
186
201
|
|
|
187
|
-
|
|
202
|
+
@api.post("/release")
|
|
203
|
+
async def release_port() -> Dict[str, Any]:
|
|
204
|
+
"""Release API port for another WebTap instance."""
|
|
205
|
+
logger.info("Releasing API port for another instance")
|
|
206
|
+
|
|
207
|
+
# Schedule graceful shutdown after response
|
|
208
|
+
def shutdown():
|
|
209
|
+
# Just set the flag to stop uvicorn, don't kill the whole process
|
|
210
|
+
global _shutdown_requested
|
|
211
|
+
_shutdown_requested = True
|
|
212
|
+
|
|
213
|
+
threading.Timer(0.5, shutdown).start()
|
|
214
|
+
return {"message": "Releasing port 8765"}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Flag to signal shutdown
|
|
218
|
+
_shutdown_requested = False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def start_api_server(state, host: str = "127.0.0.1", port: int = 8765) -> threading.Thread | None:
|
|
188
222
|
"""Start the API server in a background thread.
|
|
189
223
|
|
|
190
224
|
Args:
|
|
@@ -193,10 +227,19 @@ def start_api_server(state, host: str = "127.0.0.1", port: int = 8765):
|
|
|
193
227
|
port: Port to bind to. Defaults to 8765.
|
|
194
228
|
|
|
195
229
|
Returns:
|
|
196
|
-
Thread instance running the server.
|
|
230
|
+
Thread instance running the server, or None if port is in use.
|
|
197
231
|
"""
|
|
198
|
-
|
|
232
|
+
# Check port availability first
|
|
233
|
+
try:
|
|
234
|
+
with socket.socket() as s:
|
|
235
|
+
s.bind((host, port))
|
|
236
|
+
except OSError:
|
|
237
|
+
logger.info(f"Port {port} already in use")
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
global app_state, _shutdown_requested
|
|
199
241
|
app_state = state
|
|
242
|
+
_shutdown_requested = False # Reset flag for new instance
|
|
200
243
|
|
|
201
244
|
thread = threading.Thread(target=run_server, args=(host, port), daemon=True)
|
|
202
245
|
thread.start()
|
|
@@ -207,14 +250,46 @@ def start_api_server(state, host: str = "127.0.0.1", port: int = 8765):
|
|
|
207
250
|
|
|
208
251
|
def run_server(host: str, port: int):
|
|
209
252
|
"""Run the FastAPI server in a thread."""
|
|
210
|
-
|
|
211
|
-
|
|
253
|
+
import asyncio
|
|
254
|
+
|
|
255
|
+
async def run():
|
|
256
|
+
"""Run server with proper shutdown handling."""
|
|
257
|
+
config = uvicorn.Config(
|
|
212
258
|
api,
|
|
213
259
|
host=host,
|
|
214
260
|
port=port,
|
|
215
261
|
log_level="error",
|
|
216
262
|
access_log=False,
|
|
217
263
|
)
|
|
264
|
+
server = uvicorn.Server(config)
|
|
265
|
+
|
|
266
|
+
# Start server in background task
|
|
267
|
+
serve_task = asyncio.create_task(server.serve())
|
|
268
|
+
|
|
269
|
+
# Wait for shutdown signal
|
|
270
|
+
while not _shutdown_requested:
|
|
271
|
+
await asyncio.sleep(0.1)
|
|
272
|
+
if serve_task.done():
|
|
273
|
+
break
|
|
274
|
+
|
|
275
|
+
# Trigger shutdown
|
|
276
|
+
if not serve_task.done():
|
|
277
|
+
logger.info("API server shutting down")
|
|
278
|
+
server.should_exit = True
|
|
279
|
+
# Wait a bit for graceful shutdown
|
|
280
|
+
try:
|
|
281
|
+
await asyncio.wait_for(serve_task, timeout=1.0)
|
|
282
|
+
except asyncio.TimeoutError:
|
|
283
|
+
logger.debug("Server task timeout - cancelling")
|
|
284
|
+
serve_task.cancel()
|
|
285
|
+
try:
|
|
286
|
+
await serve_task
|
|
287
|
+
except asyncio.CancelledError:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
# Use asyncio.run() which properly cleans up
|
|
292
|
+
asyncio.run(run())
|
|
218
293
|
except Exception as e:
|
|
219
294
|
logger.error(f"API server failed: {e}")
|
|
220
295
|
|
|
@@ -6,6 +6,7 @@ PUBLIC API:
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import sys
|
|
9
|
+
import threading
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
|
|
11
12
|
from replkit2 import App
|
|
@@ -24,15 +25,32 @@ class WebTapState:
|
|
|
24
25
|
Attributes:
|
|
25
26
|
cdp: Chrome DevTools Protocol session instance.
|
|
26
27
|
service: WebTapService orchestrating all domain services.
|
|
28
|
+
api_thread: Thread running the FastAPI server (if this instance owns the port).
|
|
27
29
|
"""
|
|
28
30
|
|
|
29
31
|
cdp: CDPSession = field(default_factory=CDPSession)
|
|
30
32
|
service: WebTapService = field(init=False)
|
|
33
|
+
api_thread: threading.Thread | None = None
|
|
31
34
|
|
|
32
35
|
def __post_init__(self):
|
|
33
36
|
"""Initialize service with self reference after dataclass init."""
|
|
34
37
|
self.service = WebTapService(self)
|
|
35
38
|
|
|
39
|
+
def cleanup(self):
|
|
40
|
+
"""Cleanup resources on exit."""
|
|
41
|
+
# Disconnect CDP if connected
|
|
42
|
+
if self.cdp.is_connected:
|
|
43
|
+
self.cdp.disconnect()
|
|
44
|
+
|
|
45
|
+
# Stop API server if we own it
|
|
46
|
+
if self.api_thread and self.api_thread.is_alive():
|
|
47
|
+
# Import here to avoid circular dependency
|
|
48
|
+
import webtap.api
|
|
49
|
+
|
|
50
|
+
webtap.api._shutdown_requested = True
|
|
51
|
+
# Wait up to 1 second for graceful shutdown
|
|
52
|
+
self.api_thread.join(timeout=1.0)
|
|
53
|
+
|
|
36
54
|
|
|
37
55
|
# Must be created before command imports for decorator registration
|
|
38
56
|
app = App(
|
|
@@ -74,7 +74,8 @@ def build_query(
|
|
|
74
74
|
pattern = value
|
|
75
75
|
path_conditions.append(f"json_extract_string(event, '{json_path}') LIKE '{pattern}'")
|
|
76
76
|
elif isinstance(value, (int, float)):
|
|
77
|
-
|
|
77
|
+
# Use string comparison for numeric values to avoid type conversion errors
|
|
78
|
+
path_conditions.append(f"json_extract_string(event, '{json_path}') = '{value}'")
|
|
78
79
|
elif isinstance(value, bool):
|
|
79
80
|
path_conditions.append(f"json_extract_string(event, '{json_path}') = '{str(value).lower()}'")
|
|
80
81
|
elif value is None:
|
|
@@ -46,23 +46,9 @@ def run_chrome(state, detach: bool = True, port: int = 9222) -> dict:
|
|
|
46
46
|
],
|
|
47
47
|
)
|
|
48
48
|
|
|
49
|
-
#
|
|
49
|
+
# Simple: use clean temp profile for debugging
|
|
50
50
|
temp_config = Path("/tmp/webtap-chrome-debug")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if not temp_config.exists():
|
|
54
|
-
temp_config.mkdir(parents=True)
|
|
55
|
-
|
|
56
|
-
# Symlink Default profile
|
|
57
|
-
default_profile = real_config / "Default"
|
|
58
|
-
if default_profile.exists():
|
|
59
|
-
(temp_config / "Default").symlink_to(default_profile)
|
|
60
|
-
|
|
61
|
-
# Copy essential files
|
|
62
|
-
for file in ["Local State", "First Run"]:
|
|
63
|
-
src = real_config / file
|
|
64
|
-
if src.exists():
|
|
65
|
-
(temp_config / file).write_text(src.read_text())
|
|
51
|
+
temp_config.mkdir(parents=True, exist_ok=True)
|
|
66
52
|
|
|
67
53
|
# Launch Chrome
|
|
68
54
|
cmd = [chrome_exe, f"--remote-debugging-port={port}", "--remote-allow-origins=*", f"--user-data-dir={temp_config}"]
|
|
@@ -74,7 +60,7 @@ def run_chrome(state, detach: bool = True, port: int = 9222) -> dict:
|
|
|
74
60
|
details={
|
|
75
61
|
"Port": str(port),
|
|
76
62
|
"Mode": "Background (detached)",
|
|
77
|
-
"Profile":
|
|
63
|
+
"Profile": "Temporary (clean)",
|
|
78
64
|
"Next step": "Run connect() to attach WebTap",
|
|
79
65
|
},
|
|
80
66
|
)
|
|
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class NetworkService:
|
|
13
|
-
"""
|
|
13
|
+
"""Network event queries and monitoring."""
|
|
14
14
|
|
|
15
15
|
def __init__(self):
|
|
16
16
|
"""Initialize network service."""
|
|
@@ -81,7 +81,7 @@ class NetworkService:
|
|
|
81
81
|
json_extract_string(event, '$.params.response.statusText') as StatusText
|
|
82
82
|
FROM events
|
|
83
83
|
WHERE json_extract_string(event, '$.method') = 'Network.responseReceived'
|
|
84
|
-
AND
|
|
84
|
+
AND json_extract_string(event, '$.params.response.status') >= '400'
|
|
85
85
|
ORDER BY rowid DESC LIMIT {limit}
|
|
86
86
|
"""
|
|
87
87
|
|
|
@@ -177,22 +177,32 @@ class SetupService:
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
wrapper_script = """#!/bin/bash
|
|
180
|
-
# Chrome wrapper
|
|
180
|
+
# Chrome wrapper using bindfs for perfect state sync with debug port
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
DEBUG_DIR="$HOME/.config/google-chrome-debug"
|
|
183
|
+
REAL_DIR="$HOME/.config/google-chrome"
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
cp "$REAL_CONFIG/First Run" "$DEBUG_CONFIG/" 2>/dev/null || true
|
|
185
|
+
# Check if bindfs is installed
|
|
186
|
+
if ! command -v bindfs &>/dev/null; then
|
|
187
|
+
echo "Error: bindfs not installed. Install with: yay -S bindfs" >&2
|
|
188
|
+
exit 1
|
|
190
189
|
fi
|
|
191
190
|
|
|
191
|
+
# Mount real profile via bindfs if not already mounted
|
|
192
|
+
if ! mountpoint -q "$DEBUG_DIR" 2>/dev/null; then
|
|
193
|
+
mkdir -p "$DEBUG_DIR"
|
|
194
|
+
if ! bindfs --no-allow-other "$REAL_DIR" "$DEBUG_DIR"; then
|
|
195
|
+
echo "Error: Failed to mount Chrome profile via bindfs" >&2
|
|
196
|
+
exit 1
|
|
197
|
+
fi
|
|
198
|
+
echo "Chrome debug profile mounted. To unmount: fusermount -u $DEBUG_DIR" >&2
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# Launch Chrome with debugging on bindfs mount
|
|
192
202
|
exec /usr/bin/google-chrome-stable \\
|
|
193
203
|
--remote-debugging-port=9222 \\
|
|
194
|
-
--remote-allow-origins
|
|
195
|
-
--user-data-dir="$
|
|
204
|
+
--remote-allow-origins='*' \\
|
|
205
|
+
--user-data-dir="$DEBUG_DIR" \\
|
|
196
206
|
"$@"
|
|
197
207
|
"""
|
|
198
208
|
|
|
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
|