pentool 0.2.0__tar.gz → 0.2.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.
- {pentool-0.2.0 → pentool-0.2.2}/PKG-INFO +5 -4
- {pentool-0.2.0 → pentool-0.2.2}/README.md +4 -3
- {pentool-0.2.0 → pentool-0.2.2}/pentool/__init__.py +2 -2
- {pentool-0.2.0 → pentool-0.2.2}/pentool/__main__.py +12 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/main.py +27 -0
- pentool-0.2.2/pentool/core/crash_reporter.py +137 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/license.py +86 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/services/proxy_service.py +19 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/app.py +98 -3
- pentool-0.2.2/pentool/tui/dialogs/__init__.py +9 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/target/screen.py +18 -7
- pentool-0.2.2/pentool/tui/widgets/activity_indicator.py +143 -0
- pentool-0.2.2/pentool/tui/widgets/activity_indicator.tcss +30 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/module_tabs.py +1 -1
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/statusbar.py +7 -1
- {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/PKG-INFO +5 -4
- {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/SOURCES.txt +2 -0
- {pentool-0.2.0 → pentool-0.2.2}/pyproject.toml +1 -1
- pentool-0.2.0/pentool/core/crash_reporter.py +0 -75
- pentool-0.2.0/pentool/tui/mixins/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/LICENSE +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/base_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/comparer_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/decoder_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/intruder_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/proxy_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/repeater_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/sequencer_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/spider_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/api/target_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/project.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/proxy.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/scan.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/config.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/database.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/event_bus.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/events.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/features.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/logging.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/plugin_manager.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/project.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/storage_interface.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/updater.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/core/utils.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/comparer.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/decoder.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/intruder.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/match_replace.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/proxy.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/repeater.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/sequencer.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/spider.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/target.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/websocket_handler.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/plugins/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/plugins/builtin/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/plugins/example_plugin.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/plugins/example_plugin.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/services/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/services/base_service.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/services/intruder_service.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/services/repeater_service.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/services/scan_service.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/storage/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/storage/http_storage.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/storage/large_body_handler.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/storage/lru_cache.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/t.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/constants.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/cert_dialog.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/file_selector.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/file_selector.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/load_from_proxy.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/match_replace_dialog.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/project_dialog.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/project_dialog.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/scope_dialog.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/messages.py +0 -0
- {pentool-0.2.0/pentool/tui/dialogs → pentool-0.2.2/pentool/tui/mixins}/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/mixins/app_mixin.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/mixins/dialog_cancel.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/mixins/request_context_menu.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/project_manager.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/base.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/comparer/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/comparer/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/comparer/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/dashboard/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/dashboard/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/dashboard/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/decoder/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/decoder/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/decoder/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/extensions/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/extensions/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/extensions/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/intruder/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/intruder/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/intruder/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/proxy/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/proxy/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/proxy/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/repeater/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/repeater/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/repeater/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/scanner_unavailable.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/sequencer/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/sequencer/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/sequencer/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/hotkeys.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/spider/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/spider/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/spider/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/target/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/target/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/terminal/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/terminal/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/terminal/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/context_menu.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/context_menu.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/filter_bar.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/filter_bar.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/inspector_panel.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/inspector_panel.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/menu.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/menu_bar.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/module_tabs.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/payload_drop_zone.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/request_editor.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/request_editor.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/resize_handle.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/resize_handle.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/search_bar.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/search_bar.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/statusbar.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/toolbar_button.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/toolbar_button.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/cert.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/coder.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/copy_as.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/diff.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/http_client.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/parser.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/terminal_check.py +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/dependency_links.txt +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/entry_points.txt +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/requires.txt +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/top_level.txt +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/setup.cfg +0 -0
- {pentool-0.2.0 → pentool-0.2.2}/tests/test_user_workflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pentool
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Modern Web Pentesting TUI - Professional security testing tool
|
|
5
5
|
Author-email: Pentool Team <dev@pentool.pro>
|
|
6
6
|
License: AGPL-3.0
|
|
@@ -70,15 +70,15 @@ Fast, transparent, and built for real-world testing.
|
|
|
70
70
|
|
|
71
71
|
| Dashboard | Scanner |
|
|
72
72
|
|:---------:|:-------:|
|
|
73
|
-
|  |  |
|
|
73
|
+
|  |  |
|
|
74
74
|
|
|
75
75
|
| Proxy | Repeater |
|
|
76
76
|
|:-----:|:--------:|
|
|
77
|
-
|  |  |
|
|
77
|
+
|  |  |
|
|
78
78
|
|
|
79
79
|
| Intruder | Settings |
|
|
80
80
|
|:--------:|:--------:|
|
|
81
|
-
|  |  |
|
|
81
|
+
|  |  |
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
@@ -213,6 +213,7 @@ PRO extensions are available under a commercial license.
|
|
|
213
213
|
## 📬 Contact
|
|
214
214
|
|
|
215
215
|
- **Website:** [pentool.pro](https://pentool.pro)
|
|
216
|
+
- **Telegram channel:** [t.me/pentool_pro](https://t.me/pentool_pro)
|
|
216
217
|
- **Telegram:** [@sudores](https://t.me/sudores)
|
|
217
218
|
- **Email:** support@pentool.pro
|
|
218
219
|
- **Author:** Anatoly Kashtanov (DoctorX)
|
|
@@ -24,15 +24,15 @@ Fast, transparent, and built for real-world testing.
|
|
|
24
24
|
|
|
25
25
|
| Dashboard | Scanner |
|
|
26
26
|
|:---------:|:-------:|
|
|
27
|
-
|  |  |
|
|
27
|
+
|  |  |
|
|
28
28
|
|
|
29
29
|
| Proxy | Repeater |
|
|
30
30
|
|:-----:|:--------:|
|
|
31
|
-
|  |  |
|
|
31
|
+
|  |  |
|
|
32
32
|
|
|
33
33
|
| Intruder | Settings |
|
|
34
34
|
|:--------:|:--------:|
|
|
35
|
-
|  |  |
|
|
35
|
+
|  |  |
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -167,6 +167,7 @@ PRO extensions are available under a commercial license.
|
|
|
167
167
|
## 📬 Contact
|
|
168
168
|
|
|
169
169
|
- **Website:** [pentool.pro](https://pentool.pro)
|
|
170
|
+
- **Telegram channel:** [t.me/pentool_pro](https://t.me/pentool_pro)
|
|
170
171
|
- **Telegram:** [@sudores](https://t.me/sudores)
|
|
171
172
|
- **Email:** support@pentool.pro
|
|
172
173
|
- **Author:** Anatoly Kashtanov (DoctorX)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Pentool — professional web security testing toolkit with Textual TUI."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.2"
|
|
4
4
|
__author__ = "pentool"
|
|
5
5
|
|
|
6
6
|
|
|
@@ -34,8 +34,8 @@ def _bootstrap_pro() -> None:
|
|
|
34
34
|
_repo_root = _pkg_dir.parent # .../pentool/
|
|
35
35
|
|
|
36
36
|
candidates: list[Path] = [
|
|
37
|
-
Path.home() / ".pentool" / "pro" / "pentool", # installed PRO package
|
|
38
37
|
_repo_root / "pro" / "pentool", # dev submodule
|
|
38
|
+
Path.home() / ".pentool" / "pro" / "pentool", # installed PRO package
|
|
39
39
|
]
|
|
40
40
|
|
|
41
41
|
import pentool as _self
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Entry point: no arguments — TUI, with arguments — CLI."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
import threading
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def main() -> None:
|
|
@@ -8,6 +9,17 @@ def main() -> None:
|
|
|
8
9
|
from pentool.cli.main import cli
|
|
9
10
|
cli()
|
|
10
11
|
else:
|
|
12
|
+
# Anonymous install-counter ping (fire-and-forget, one increment per
|
|
13
|
+
# machine — dedup happens server-side). Runs in a background thread —
|
|
14
|
+
# send_first_run_ping() opens a network connection with an 8s
|
|
15
|
+
# timeout, which must never delay the TUI actually starting when
|
|
16
|
+
# the network is slow/unreachable.
|
|
17
|
+
try:
|
|
18
|
+
from pentool.core.crash_reporter import send_first_run_ping
|
|
19
|
+
threading.Thread(target=send_first_run_ping, daemon=True).start()
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
|
|
11
23
|
try:
|
|
12
24
|
from pentool.tui.app import PentoolApp
|
|
13
25
|
PentoolApp().run()
|
|
@@ -126,6 +126,7 @@ def update_cmd(check_only: bool) -> None:
|
|
|
126
126
|
click.echo("Upgrading via pip...")
|
|
127
127
|
if do_pip_upgrade():
|
|
128
128
|
click.echo("Upgrade successful. Restart Pentool to use the new version.")
|
|
129
|
+
_sync_pro_package_after_upgrade()
|
|
129
130
|
else:
|
|
130
131
|
click.echo(
|
|
131
132
|
"pip upgrade failed. You can update manually:\n"
|
|
@@ -135,6 +136,32 @@ def update_cmd(check_only: bool) -> None:
|
|
|
135
136
|
raise SystemExit(1)
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
def _sync_pro_package_after_upgrade() -> None:
|
|
140
|
+
"""Re-download the PRO package too, if one is active on this machine.
|
|
141
|
+
|
|
142
|
+
`pip install --upgrade pentool` only touches the FREE package — the PRO
|
|
143
|
+
package lives separately in ~/.pentool/pro/ and is otherwise only ever
|
|
144
|
+
(re)downloaded by check_and_update_pro_package(), which normally runs in
|
|
145
|
+
the background on TUI startup. Doing it here too means a CLI-driven
|
|
146
|
+
upgrade doesn't leave a stale PRO build around until the next TUI launch.
|
|
147
|
+
Best-effort — never fails the overall upgrade if this part doesn't work.
|
|
148
|
+
"""
|
|
149
|
+
import asyncio
|
|
150
|
+
|
|
151
|
+
from pentool.core.license import PRO_PACKAGE_DIR, check_and_update_pro_package
|
|
152
|
+
|
|
153
|
+
if not PRO_PACKAGE_DIR.exists():
|
|
154
|
+
return # PRO was never activated on this machine — nothing to sync
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
click.echo("Checking for a newer PRO build...")
|
|
158
|
+
updated = asyncio.run(check_and_update_pro_package())
|
|
159
|
+
if updated:
|
|
160
|
+
click.echo("PRO package updated to the latest build.")
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
click.echo(f"Could not check for a PRO update: {exc}", err=True)
|
|
163
|
+
|
|
164
|
+
|
|
138
165
|
@cli.group()
|
|
139
166
|
def license() -> None:
|
|
140
167
|
"""License management: trial, activation, status."""
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Crash reporter — collects and sends anonymous crash reports.
|
|
2
|
+
|
|
3
|
+
Also sends a one-shot "first run" ping so the admin panel can show an
|
|
4
|
+
install counter — see send_first_run_ping(). Both go to the same
|
|
5
|
+
pentool-license Cloudflare Worker that already handles license
|
|
6
|
+
validation (pentool.core.license._LICENSE_API_BASE) — /api/crash and
|
|
7
|
+
/api/telemetry/first_run are public, unauthenticated ingestion
|
|
8
|
+
endpoints there, mirrored in pentool-backend's admin.html dashboard.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import sys
|
|
17
|
+
import traceback
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
# Same Worker as pentool.core.license — kept as a separate literal here
|
|
21
|
+
# (rather than importing _LICENSE_API_BASE) so crash reporting has no
|
|
22
|
+
# import-time dependency on the licensing module.
|
|
23
|
+
_API_BASE = "https://pentool-license.akashtanov2020.workers.dev"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_metadata() -> dict[str, Any]:
|
|
27
|
+
"""Collect anonymous metadata for crash report."""
|
|
28
|
+
try:
|
|
29
|
+
from pentool import __version__ as version
|
|
30
|
+
except Exception:
|
|
31
|
+
version = "unknown"
|
|
32
|
+
return {
|
|
33
|
+
"version": version,
|
|
34
|
+
"python": sys.version.split()[0],
|
|
35
|
+
"os": platform.system(),
|
|
36
|
+
"os_version": platform.release(),
|
|
37
|
+
"arch": platform.machine(),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _anonymize(text: str) -> str:
|
|
42
|
+
"""Remove paths and potential tokens from traceback text."""
|
|
43
|
+
import re
|
|
44
|
+
# Remove absolute paths — keep only filename
|
|
45
|
+
text = re.sub(r' File "([^"]+)"', lambda m: f' File "{os.path.basename(m.group(1))}"', text)
|
|
46
|
+
# Remove anything that looks like a token/key (long hex strings)
|
|
47
|
+
text = re.sub(r'\b[A-Fa-f0-9]{32,}\b', '<TOKEN>', text)
|
|
48
|
+
return text
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def send_crash_async(exc: BaseException, endpoint: str = f"{_API_BASE}/api/crash") -> None:
|
|
52
|
+
"""Send crash report asynchronously. Silently ignores any errors."""
|
|
53
|
+
try:
|
|
54
|
+
import aiohttp # type: ignore[import]
|
|
55
|
+
except ImportError:
|
|
56
|
+
return # aiohttp not available — skip silently
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
60
|
+
tb_text = _anonymize("".join(tb_lines))
|
|
61
|
+
payload = {
|
|
62
|
+
"traceback": tb_text,
|
|
63
|
+
"exception": type(exc).__name__,
|
|
64
|
+
**_get_metadata(),
|
|
65
|
+
}
|
|
66
|
+
timeout = aiohttp.ClientTimeout(total=8)
|
|
67
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
68
|
+
await session.post(endpoint, json=payload, ssl=False)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass # never propagate errors from crash reporter
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def send_crash(exc: BaseException) -> None:
|
|
74
|
+
"""Fire-and-forget crash report from sync context."""
|
|
75
|
+
try:
|
|
76
|
+
from pentool.core.config import get_config
|
|
77
|
+
cfg = get_config()
|
|
78
|
+
if not getattr(cfg, "send_crash_reports", True):
|
|
79
|
+
return
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
loop = asyncio.new_event_loop()
|
|
85
|
+
loop.run_until_complete(send_crash_async(exc))
|
|
86
|
+
loop.close()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def send_first_run_ping_async(endpoint: str = f"{_API_BASE}/api/telemetry/first_run") -> None:
|
|
92
|
+
"""Ping the install counter once per machine. Silently ignores errors.
|
|
93
|
+
|
|
94
|
+
Dedup happens server-side (keyed by machine_id) — safe to call on every
|
|
95
|
+
startup; only the very first call from a given machine increments the
|
|
96
|
+
counter, so no local "have I already sent this" state is needed here.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
import aiohttp # type: ignore[import]
|
|
100
|
+
except ImportError:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
from pentool.core.license import get_machine_id
|
|
105
|
+
machine_id = get_machine_id()
|
|
106
|
+
except Exception:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
timeout = aiohttp.ClientTimeout(total=8)
|
|
111
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
112
|
+
await session.post(endpoint, json={"machine_id": machine_id}, ssl=False)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def send_first_run_ping() -> None:
|
|
118
|
+
"""Fire-and-forget install-counter ping from sync context.
|
|
119
|
+
|
|
120
|
+
Respects the same send_crash_reports opt-out as send_crash() — both are
|
|
121
|
+
"anonymous telemetry to Anthropic-free, self-hosted infra" in spirit,
|
|
122
|
+
so one setting covers both rather than adding a second toggle.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
from pentool.core.config import get_config
|
|
126
|
+
cfg = get_config()
|
|
127
|
+
if not getattr(cfg, "send_crash_reports", True):
|
|
128
|
+
return
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
loop = asyncio.new_event_loop()
|
|
134
|
+
loop.run_until_complete(send_first_run_ping_async())
|
|
135
|
+
loop.close()
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
@@ -503,6 +503,13 @@ def _current_platform() -> str:
|
|
|
503
503
|
return "linux"
|
|
504
504
|
|
|
505
505
|
|
|
506
|
+
# File recording which PRO build is currently unpacked into PRO_PACKAGE_DIR —
|
|
507
|
+
# lets check_and_update_pro_package() tell "same build as last time" apart
|
|
508
|
+
# from "a new build was published" without re-downloading the archive to
|
|
509
|
+
# compare it. See _fetch_pro_build_id() / /api/pro/version on the Worker.
|
|
510
|
+
_PRO_BUILD_MARKER = PRO_PACKAGE_DIR / ".build_id"
|
|
511
|
+
|
|
512
|
+
|
|
506
513
|
async def download_pro_package(key: str, machine_id: str) -> bool:
|
|
507
514
|
"""Download, verify, and install the obfuscated PRO package.
|
|
508
515
|
|
|
@@ -555,6 +562,85 @@ async def download_pro_package(key: str, machine_id: str) -> bool:
|
|
|
555
562
|
finally:
|
|
556
563
|
tmp_archive.unlink(missing_ok=True)
|
|
557
564
|
|
|
565
|
+
# Record which build this is, so a later check_and_update_pro_package()
|
|
566
|
+
# can tell "nothing changed" from "a new build was published" without
|
|
567
|
+
# re-downloading the archive just to compare it.
|
|
568
|
+
try:
|
|
569
|
+
build_id = await _fetch_pro_build_id(plat)
|
|
570
|
+
if build_id:
|
|
571
|
+
_PRO_BUILD_MARKER.write_text(build_id, encoding="utf-8")
|
|
572
|
+
except Exception:
|
|
573
|
+
pass # non-fatal — worst case, next check just re-downloads once more
|
|
574
|
+
|
|
558
575
|
return True
|
|
559
576
|
except Exception:
|
|
560
577
|
return False
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
async def _fetch_pro_build_id(plat: str) -> str | None:
|
|
581
|
+
"""Query /api/pro/version for the current PRO release's build_id.
|
|
582
|
+
|
|
583
|
+
No license key needed — this only identifies which build is published,
|
|
584
|
+
not whether the caller is entitled to it (that's still enforced by
|
|
585
|
+
/api/download re-validating the key server-side). Returns None on any
|
|
586
|
+
failure (network, missing asset, etc.) — callers should treat that as
|
|
587
|
+
"couldn't check, don't act on it" rather than "no update available".
|
|
588
|
+
"""
|
|
589
|
+
try:
|
|
590
|
+
import aiohttp
|
|
591
|
+
async with aiohttp.ClientSession(
|
|
592
|
+
timeout=aiohttp.ClientTimeout(total=8)
|
|
593
|
+
) as session:
|
|
594
|
+
async with session.get(
|
|
595
|
+
f"{_LICENSE_API_BASE}/api/pro/version",
|
|
596
|
+
params={"platform": plat},
|
|
597
|
+
ssl=False,
|
|
598
|
+
) as resp:
|
|
599
|
+
if resp.status != 200:
|
|
600
|
+
return None
|
|
601
|
+
data = await resp.json()
|
|
602
|
+
return data.get("build_id")
|
|
603
|
+
except Exception:
|
|
604
|
+
return None
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
async def check_and_update_pro_package() -> bool:
|
|
608
|
+
"""Re-download the PRO package if a newer build has been published.
|
|
609
|
+
|
|
610
|
+
Called on TUI startup (background worker, same pattern as
|
|
611
|
+
_check_for_updates for the pip package) and after `pentool update` — this
|
|
612
|
+
is what closes the gap where pentool-pro ships a fix but a machine that
|
|
613
|
+
already activated its license keeps running the stale code from
|
|
614
|
+
~/.pentool/pro/ forever, because download_pro_package() otherwise only
|
|
615
|
+
ever runs once, at initial activation/trial-start.
|
|
616
|
+
|
|
617
|
+
Silently does nothing (returns False) if:
|
|
618
|
+
- there's no active PRO license (nothing to update)
|
|
619
|
+
- the PRO package was never downloaded in the first place (that's
|
|
620
|
+
activate_license's job, not this one)
|
|
621
|
+
- the version check or download fails for any reason
|
|
622
|
+
|
|
623
|
+
Returns True if a new build was found and successfully installed.
|
|
624
|
+
"""
|
|
625
|
+
info = get_license()
|
|
626
|
+
if not info.valid or not info.features:
|
|
627
|
+
return False
|
|
628
|
+
if not PRO_PACKAGE_DIR.exists():
|
|
629
|
+
return False # never activated on this machine — nothing to refresh
|
|
630
|
+
|
|
631
|
+
plat = _current_platform()
|
|
632
|
+
remote_build_id = await _fetch_pro_build_id(plat)
|
|
633
|
+
if not remote_build_id:
|
|
634
|
+
return False # couldn't reach the server — don't touch anything
|
|
635
|
+
|
|
636
|
+
local_build_id = None
|
|
637
|
+
try:
|
|
638
|
+
if _PRO_BUILD_MARKER.exists():
|
|
639
|
+
local_build_id = _PRO_BUILD_MARKER.read_text(encoding="utf-8").strip()
|
|
640
|
+
except Exception:
|
|
641
|
+
pass
|
|
642
|
+
|
|
643
|
+
if local_build_id == remote_build_id:
|
|
644
|
+
return False # already on the latest build
|
|
645
|
+
|
|
646
|
+
return await download_pro_package(info.license_key, info.machine_id)
|
|
@@ -23,6 +23,7 @@ class ProxyService(BaseService):
|
|
|
23
23
|
event_bus: EventBus | None = None,
|
|
24
24
|
tui_loop: asyncio.AbstractEventLoop | None = None,
|
|
25
25
|
on_log: Callable[[str], None] | None = None,
|
|
26
|
+
on_storage_error: Callable[[str], None] | None = None,
|
|
26
27
|
) -> None:
|
|
27
28
|
super().__init__(event_bus=event_bus, tui_loop=tui_loop, on_log=on_log)
|
|
28
29
|
self._proxy_api = proxy_api
|
|
@@ -31,6 +32,20 @@ class ProxyService(BaseService):
|
|
|
31
32
|
self._storage_ready = False
|
|
32
33
|
self._pre_storage_queue: list[InterceptedRequest] = []
|
|
33
34
|
self._pre_storage_queue_max = 2000
|
|
35
|
+
# Optional callback(message: str) — fired when the DB file can't be
|
|
36
|
+
# opened (e.g. "database is locked" because another Pentool
|
|
37
|
+
# instance/process already has it open, or the path is unwritable).
|
|
38
|
+
# Without this, such failures were logged only — requests silently
|
|
39
|
+
# stopped being saved with zero indication in the UI of why.
|
|
40
|
+
self._on_storage_error = on_storage_error
|
|
41
|
+
|
|
42
|
+
def _report_storage_error(self, context: str, exc: Exception) -> None:
|
|
43
|
+
msg = str(exc) or type(exc).__name__
|
|
44
|
+
if self._on_storage_error:
|
|
45
|
+
try:
|
|
46
|
+
self._on_storage_error(f"{context}: {msg}")
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
34
49
|
|
|
35
50
|
async def init_storage(self) -> None:
|
|
36
51
|
try:
|
|
@@ -49,6 +64,7 @@ class ProxyService(BaseService):
|
|
|
49
64
|
self._pre_storage_queue.clear()
|
|
50
65
|
except Exception as exc:
|
|
51
66
|
logger.error("ProxyService: storage init failed: %s", exc)
|
|
67
|
+
self._report_storage_error("Failed to open project database", exc)
|
|
52
68
|
|
|
53
69
|
async def store_request(self, req: InterceptedRequest) -> int | None:
|
|
54
70
|
if not self._storage_ready:
|
|
@@ -193,6 +209,9 @@ class ProxyService(BaseService):
|
|
|
193
209
|
except Exception as exc:
|
|
194
210
|
logger.error("ProxyService: switch_db failed: %s", exc)
|
|
195
211
|
self._storage_ready = False
|
|
212
|
+
self._report_storage_error(
|
|
213
|
+
f"Failed to open project database ({new_db_path})", exc
|
|
214
|
+
)
|
|
196
215
|
|
|
197
216
|
async def clear_history(self) -> None:
|
|
198
217
|
if not self._storage_ready:
|
|
@@ -10,6 +10,7 @@ import threading
|
|
|
10
10
|
from textual import on
|
|
11
11
|
from textual.app import App, ComposeResult
|
|
12
12
|
from textual.binding import Binding
|
|
13
|
+
from textual.containers import Vertical
|
|
13
14
|
from textual.widgets import Checkbox, ContentSwitcher, Footer
|
|
14
15
|
|
|
15
16
|
# Nicer checkbox glyph applied project-wide — Checkbox is a subclass of
|
|
@@ -135,6 +136,31 @@ class PentoolApp(App):
|
|
|
135
136
|
dock: bottom;
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/* Wrapper around Footer + StatusBar — see compose()'s comment for why
|
|
140
|
+
this exists: two independently-docked bottom widgets overlap rather
|
|
141
|
+
than stack, so both are laid out normally inside one docked container
|
|
142
|
+
instead. Height 2 = Footer's 1 + StatusBar's 1.
|
|
143
|
+
|
|
144
|
+
Both Footer (in its own DEFAULT_CSS) and StatusBar (in statusbar.tcss)
|
|
145
|
+
hard-code `dock: bottom` themselves — just nesting them inside this
|
|
146
|
+
wrapper is not enough, they'd still each dock to the bottom of the
|
|
147
|
+
wrapper and overlap each other exactly as before. Both docks must be
|
|
148
|
+
explicitly cancelled here so they lay out normally (stacked, one
|
|
149
|
+
above the other) inside #bottom-dock instead. */
|
|
150
|
+
#bottom-dock {
|
|
151
|
+
dock: bottom;
|
|
152
|
+
height: 2;
|
|
153
|
+
layout: vertical;
|
|
154
|
+
}
|
|
155
|
+
#bottom-dock Footer {
|
|
156
|
+
dock: none;
|
|
157
|
+
height: 1;
|
|
158
|
+
}
|
|
159
|
+
#bottom-dock StatusBar {
|
|
160
|
+
dock: none;
|
|
161
|
+
height: 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
138
164
|
/* Global checkbox style — compact, height 1, no border.
|
|
139
165
|
Glyph itself ("[ ]" / "[✓]") is set once in Checkbox.BUTTON_LEFT/
|
|
140
166
|
INNER/RIGHT above; this just controls color/spacing/hover so every
|
|
@@ -268,8 +294,17 @@ class PentoolApp(App):
|
|
|
268
294
|
yield ExtensionsScreen(id="screen-extensions")
|
|
269
295
|
yield TerminalScreen(id="screen-terminal")
|
|
270
296
|
yield SettingsScreen(id="screen-settings")
|
|
271
|
-
|
|
272
|
-
|
|
297
|
+
# Footer and StatusBar both used to `dock: bottom` independently —
|
|
298
|
+
# in Textual, multiple independently-docked widgets at the same
|
|
299
|
+
# edge don't stack, they all pin to the same row and overlap each
|
|
300
|
+
# other (whichever was composed last wins the pixels). That's why
|
|
301
|
+
# StatusBar (and, inside it, the new ActivityIndicator) never
|
|
302
|
+
# rendered — Footer, composed after it, was drawn on top and
|
|
303
|
+
# covered it completely. Wrapping both in one docked container
|
|
304
|
+
# lets them lay out normally (one above the other) inside it.
|
|
305
|
+
with Vertical(id="bottom-dock"):
|
|
306
|
+
yield Footer()
|
|
307
|
+
yield StatusBar(id="statusbar")
|
|
273
308
|
|
|
274
309
|
async def on_mount(self) -> None:
|
|
275
310
|
setup_logging(self._cfg.log_file, self._cfg.log_level)
|
|
@@ -298,6 +333,7 @@ class PentoolApp(App):
|
|
|
298
333
|
proxy_api=self._proxy_api,
|
|
299
334
|
db_path=self._cfg.db_path,
|
|
300
335
|
event_bus=get_event_bus(),
|
|
336
|
+
on_storage_error=self._on_storage_error,
|
|
301
337
|
)
|
|
302
338
|
try:
|
|
303
339
|
from pentool.tui.screens.proxy.screen import ProxyScreen
|
|
@@ -372,6 +408,12 @@ class PentoolApp(App):
|
|
|
372
408
|
if getattr(self._cfg, "check_updates", True):
|
|
373
409
|
self.run_worker(self._check_for_updates(), exclusive=False, thread=False)
|
|
374
410
|
|
|
411
|
+
# Check for a newer PRO build in background (if a PRO license is
|
|
412
|
+
# active) — closes the gap where pentool-pro ships a fix but an
|
|
413
|
+
# already-activated machine keeps running the stale code forever,
|
|
414
|
+
# since the PRO package otherwise only downloads once, at activation.
|
|
415
|
+
self.run_worker(self._check_for_pro_update(), exclusive=False, thread=False)
|
|
416
|
+
|
|
375
417
|
async def _auto_open_last_project(self) -> None:
|
|
376
418
|
"""Open the last project from recent_projects at startup."""
|
|
377
419
|
await asyncio.sleep(0.2) # give Textual time to render the UI
|
|
@@ -403,6 +445,30 @@ class PentoolApp(App):
|
|
|
403
445
|
except Exception as exc:
|
|
404
446
|
logger.debug("APP: update check failed: %s", exc)
|
|
405
447
|
|
|
448
|
+
async def _check_for_pro_update(self) -> None:
|
|
449
|
+
"""Re-download the PRO package in the background if a newer build
|
|
450
|
+
has been published, then notify so the user knows to restart.
|
|
451
|
+
|
|
452
|
+
No-op (silently) if there's no active PRO license or the package was
|
|
453
|
+
never downloaded in the first place — see
|
|
454
|
+
pentool.core.license.check_and_update_pro_package for the actual
|
|
455
|
+
build_id comparison logic.
|
|
456
|
+
"""
|
|
457
|
+
import asyncio as _asyncio
|
|
458
|
+
await _asyncio.sleep(4.0) # after the pip-update check, UI settled
|
|
459
|
+
try:
|
|
460
|
+
from pentool.core.license import check_and_update_pro_package
|
|
461
|
+
updated = await check_and_update_pro_package()
|
|
462
|
+
if updated:
|
|
463
|
+
self.notify(
|
|
464
|
+
"PRO package updated to the latest build — restart Pentool to use it.",
|
|
465
|
+
severity="information",
|
|
466
|
+
timeout=10,
|
|
467
|
+
)
|
|
468
|
+
logger.info("APP: PRO package updated to a newer build")
|
|
469
|
+
except Exception as exc:
|
|
470
|
+
logger.debug("APP: PRO update check failed: %s", exc)
|
|
471
|
+
|
|
406
472
|
def _setup_auto_save(self) -> None:
|
|
407
473
|
"""Configure / restart the auto-save timer from the current config.
|
|
408
474
|
|
|
@@ -649,6 +715,24 @@ class PentoolApp(App):
|
|
|
649
715
|
except Exception:
|
|
650
716
|
pass
|
|
651
717
|
|
|
718
|
+
def _on_storage_error(self, message: str) -> None:
|
|
719
|
+
"""ProxyService.init_storage()/switch_db() failed to open the DB.
|
|
720
|
+
|
|
721
|
+
Most common cause: the project's .db file is locked by another
|
|
722
|
+
process (e.g. a second Pentool instance already has it open) or the
|
|
723
|
+
path is unwritable. Previously this was logged only — requests
|
|
724
|
+
silently stopped being saved to history with no visible indication
|
|
725
|
+
why. init_storage()/switch_db() both run on the app's own event
|
|
726
|
+
loop (awaited directly, never from a worker thread), so a plain
|
|
727
|
+
notify() here is safe without call_from_thread.
|
|
728
|
+
"""
|
|
729
|
+
self.notify(
|
|
730
|
+
f"{message}\n\nRequests will not be saved until this is fixed — "
|
|
731
|
+
f"check that no other Pentool instance has this project open.",
|
|
732
|
+
severity="error",
|
|
733
|
+
timeout=8,
|
|
734
|
+
)
|
|
735
|
+
|
|
652
736
|
def show_context_menu(
|
|
653
737
|
self,
|
|
654
738
|
items: list[tuple[str, str]],
|
|
@@ -769,11 +853,22 @@ class PentoolApp(App):
|
|
|
769
853
|
await self._proxy._server.serve_forever()
|
|
770
854
|
except Exception as exc:
|
|
771
855
|
logger.error("Proxy error: %s", exc)
|
|
856
|
+
# Surface this to the user — previously only logged, so a
|
|
857
|
+
# "port already in use" / "another process holds this file"
|
|
858
|
+
# failure looked like the proxy silently did nothing when the
|
|
859
|
+
# toolbar button was pressed, with no clue why.
|
|
860
|
+
msg = str(exc) or type(exc).__name__
|
|
861
|
+
self.call_from_thread(
|
|
862
|
+
self.notify,
|
|
863
|
+
f"Proxy failed to start: {msg}",
|
|
864
|
+
severity="error",
|
|
865
|
+
timeout=6,
|
|
866
|
+
)
|
|
772
867
|
finally:
|
|
773
868
|
self.call_from_thread(self._update_status)
|
|
774
869
|
self.call_from_thread(self._update_proxy_screen_labels)
|
|
775
870
|
self.call_from_thread(self._update_dashboard_proxy_status, False)
|
|
776
|
-
self.call_from_thread(self.flash, "○ Proxy
|
|
871
|
+
self.call_from_thread(self.flash, "○ Proxy stopped", "warning")
|
|
777
872
|
|
|
778
873
|
def _stop_proxy(self) -> None:
|
|
779
874
|
logger.info("APP: _stop_proxy called")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""tui.dialogs package: modal dialogs shared across screens.
|
|
2
|
+
|
|
3
|
+
extend_path lets the PRO package (pro/pentool/tui/dialogs/, e.g.
|
|
4
|
+
checks_dialog.py) merge into this namespace the same way
|
|
5
|
+
pentool.tui/pentool.tui.screens already do — see pentool/tui/__init__.py
|
|
6
|
+
and pentool/tui/screens/__init__.py for the same pattern.
|
|
7
|
+
"""
|
|
8
|
+
import pkgutil
|
|
9
|
+
__path__ = pkgutil.extend_path(__path__, __name__)
|
|
@@ -50,8 +50,6 @@ class TargetScreen(Widget):
|
|
|
50
50
|
tooltip="Crawl the selected host in the tree with the Spider"
|
|
51
51
|
)
|
|
52
52
|
yield Static(" │ ", classes="toolbar-sep")
|
|
53
|
-
yield ToolbarButton("↺ Reload from DB", "btn-refresh")
|
|
54
|
-
yield Static(" │ ", classes="toolbar-sep")
|
|
55
53
|
yield ToolbarButton("🗑 Clear", "btn-clear")
|
|
56
54
|
yield Static(" │ ", classes="toolbar-sep")
|
|
57
55
|
yield ToolbarButton("📄 Export JSON", "btn-export")
|
|
@@ -64,7 +62,7 @@ class TargetScreen(Widget):
|
|
|
64
62
|
yield RichLog(id="detail-log", markup=True, highlight=False)
|
|
65
63
|
|
|
66
64
|
yield Static(
|
|
67
|
-
"
|
|
65
|
+
"Scope: Add/Remove Scope │ M: Context menu",
|
|
68
66
|
id="status-bar",
|
|
69
67
|
)
|
|
70
68
|
|
|
@@ -79,6 +77,17 @@ class TargetScreen(Widget):
|
|
|
79
77
|
return self._target_api
|
|
80
78
|
|
|
81
79
|
def _load_sitemap(self) -> None:
|
|
80
|
+
# Reset selection/details BEFORE the async reload — otherwise a stale
|
|
81
|
+
# node from the previous project stays shown in the Details panel
|
|
82
|
+
# until the user clicks something in the new tree (looks like an
|
|
83
|
+
# artifact "leaking" between projects).
|
|
84
|
+
self._selected_host = None
|
|
85
|
+
self._selected_node_data = None
|
|
86
|
+
try:
|
|
87
|
+
self.query_one("#detail-log", RichLog).clear()
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
82
91
|
# Save current scope hosts before reload
|
|
83
92
|
scope_hosts: set[str] = set()
|
|
84
93
|
try:
|
|
@@ -243,10 +252,6 @@ class TargetScreen(Widget):
|
|
|
243
252
|
def on_btn_crawl_host(self, _: ToolbarButton.Pressed) -> None:
|
|
244
253
|
self.action_crawl_selected_host()
|
|
245
254
|
|
|
246
|
-
@on(ToolbarButton.Pressed, "#btn-refresh")
|
|
247
|
-
def on_btn_refresh(self, _: ToolbarButton.Pressed) -> None:
|
|
248
|
-
self._load_sitemap()
|
|
249
|
-
|
|
250
255
|
@on(ToolbarButton.Pressed, "#btn-clear")
|
|
251
256
|
def on_btn_clear(self, _: ToolbarButton.Pressed) -> None:
|
|
252
257
|
self.action_clear()
|
|
@@ -314,6 +319,12 @@ class TargetScreen(Widget):
|
|
|
314
319
|
api = self._get_api()
|
|
315
320
|
api.clear()
|
|
316
321
|
self._build_tree({})
|
|
322
|
+
self._selected_host = None
|
|
323
|
+
self._selected_node_data = None
|
|
324
|
+
try:
|
|
325
|
+
self.query_one("#detail-log", RichLog).clear()
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
317
328
|
self.app.notify("Site map cleared", severity="information")
|
|
318
329
|
except Exception as exc:
|
|
319
330
|
logger.warning("_clear_worker: %s", exc)
|