PyMemoryEditor 3.0.1__tar.gz → 3.1.0__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.
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/CONTRIBUTING.md +21 -6
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/Makefile +39 -6
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PKG-INFO +3 -1
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/__init__.py +1 -1
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/_widgets.py +54 -26
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/application.py +72 -6
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/cheat_table.py +95 -23
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/log_console_dialog.py +2 -2
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/main_window.py +66 -16
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/memory_map_dialog.py +4 -14
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/memory_viewer_dialog.py +14 -15
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/modules_dialog.py +2 -3
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/open_process_dialog.py +2 -14
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/pointer_chain_dialog.py +18 -16
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/results_view.py +89 -15
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/scan_worker.py +2 -1
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/linux/functions.py +68 -8
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/linux/process.py +4 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/macos/functions.py +51 -3
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/macos/process.py +8 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/macos/types.py +5 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/mcp/config.py +41 -24
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/mcp/policy.py +108 -16
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/mcp/server.py +8 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/mcp/session.py +4 -17
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/mcp/toolset.py +223 -172
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/abstract.py +113 -14
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/pointer_scan.py +43 -11
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/region.py +1 -4
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/remote_pointer.py +11 -3
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/scanning.py +38 -33
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/util/convert.py +51 -50
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/util/scan.py +202 -78
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/functions.py +160 -14
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/process.py +58 -10
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/README.md +2 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/openprocess.md +7 -2
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/pointer-path.md +4 -1
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/utilities.md +9 -5
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/app.md +3 -2
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/conf.py +1 -1
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/searching.md +26 -2
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/mcp.md +27 -7
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/platform-notes.md +20 -3
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/troubleshooting.md +21 -2
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/pyproject.toml +13 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_app_cheat_entry.py +98 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_app_pointer_parsing.py +10 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_app_smoke.py +78 -1
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_app_widgets.py +68 -0
- pymemoryeditor-3.1.0/tests/app/test_main_window_lifecycle.py +200 -0
- pymemoryeditor-3.1.0/tests/app/test_results_model.py +164 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/conftest.py +12 -2
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/test_argument_fuzz.py +3 -2
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/test_fake_fidelity.py +29 -1
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/test_live.py +14 -7
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/test_parsing.py +142 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/test_policy.py +108 -0
- pymemoryeditor-3.1.0/tests/mcp_server/test_registry.py +88 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/test_toolset.py +128 -0
- pymemoryeditor-3.1.0/tests/memory/test_search_argument_errors.py +69 -0
- pymemoryeditor-3.1.0/tests/memory/test_unaligned_scan.py +81 -0
- pymemoryeditor-3.1.0/tests/platforms/test_linux_error_hints.py +104 -0
- pymemoryeditor-3.1.0/tests/platforms/test_macos_region_walk.py +118 -0
- pymemoryeditor-3.1.0/tests/platforms/test_win32_region_walk.py +159 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/pointer/test_pointer_scan.py +148 -0
- pymemoryeditor-3.1.0/tests/process/test_foreign_process.py +95 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/test_chunking_integration.py +53 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/test_scan.py +210 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/test_scan_properties.py +99 -11
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/.flake8 +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/.gitignore +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/.readthedocs.yaml +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/CODE_OF_CONDUCT.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/LICENSE +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/__main__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/_icon.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/assets/icon.svg +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/cheat_entry.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/cheat_poll_worker.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/pointer_scan_dialog.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/scan_types.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/scanner_panel.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/threads_dialog.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/app/value_types.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/enums.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/linux/libc.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/linux/types.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/macos/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/macos/libsystem.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/mcp/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/mcp/__main__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/errors.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/info.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/module_info.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/thread_info.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/process/util.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/py.typed +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/util/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/util/pattern.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/util/scan_numpy.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/enums/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/PyMemoryEditor/win32/types.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/SECURITY.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/assets/screenshots/app.png +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/assets/social-preview.html +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/assets/social-preview.png +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/_static/.gitkeep +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/_static/custom.css +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/_templates/layout.html +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/enums.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/errors.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/index.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/memory-region.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/module-info.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/remote-pointer.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/api/thread-info.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/contributing.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/funding.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/glossary.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/allocate-free.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/logging.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/memory-regions.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/modules-threads.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/opening-process.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/pattern-scan.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/pointer-scan.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/pointers.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/guide/read-write.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/index.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/installation.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/license.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/quickstart.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/requirements.txt +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/docs/why.md +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_app_scan_request.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_app_value_types.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_cheat_poll_worker.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_open_process_dialog.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/app/test_refine_scan_worker.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/conftest.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/core/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/core/test_errors.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/core/test_logger.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/test_server.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/mcp_server/test_session.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_allocate_free.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_bufflength_inference.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_editor.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_narrow_width_consistency.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_partial_io.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_read_into.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_str_boundary.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_str_decode_consistency.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_typed_accessors.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_write_safety.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_write_span.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/memory/test_write_str_bytes_width.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/platforms/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/platforms/test_bitness.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/platforms/test_bitness_fallback_warns.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/platforms/test_linux_types.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/platforms/test_macos_protect.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/platforms/test_macos_transient_reads.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/platforms/test_win32_permissions.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/pointer/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/pointer/test_pointer_chain.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/pointer/test_remote_pointer.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/process/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/process/test_memory_region.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/process/test_module_enumeration.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/process/test_partial_name_match.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/process/test_process_enumeration.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/process/test_process_lookup.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/process/test_thread_enumeration.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/__init__.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/test_pattern_compile.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/test_pattern_scan.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/test_region_snapshot.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/test_scan_numpy.py +0 -0
- {pymemoryeditor-3.0.1 → pymemoryeditor-3.1.0}/tests/scan/test_scanning_helper.py +0 -0
|
@@ -22,9 +22,15 @@ The tests read and write the memory of the test process itself; they should run
|
|
|
22
22
|
on any supported platform without elevated privileges.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
make test
|
|
25
|
+
make test-fast # pytest tests -m "not slow" (~20 s: unit, property and GUI tests)
|
|
26
|
+
make test # pytest tests -v (everything, incl. live whole-process scans)
|
|
27
|
+
make test-mcp # pytest tests/mcp_server (the MCP server suite, ~10 s)
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
`test-fast` is the loop to iterate on; the `slow` and `scan_heavy` markers
|
|
31
|
+
(defined in `pyproject.toml`) hold the tests that scan the test process's whole
|
|
32
|
+
address space, which takes minutes on CI runners.
|
|
33
|
+
|
|
28
34
|
## Linting
|
|
29
35
|
|
|
30
36
|
```bash
|
|
@@ -46,11 +52,13 @@ make pre-commit # lint + type-check + test
|
|
|
46
52
|
```
|
|
47
53
|
|
|
48
54
|
The CI pipeline runs the same lint, mypy and tests, and blocks merges on failure.
|
|
49
|
-
The
|
|
50
|
-
all three platform backends are exercised on every
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
The library matrix runs Python 3.10–3.13 on Ubuntu and Windows plus a single
|
|
56
|
+
Python 3.12 cell on macOS, so all three platform backends are exercised on every
|
|
57
|
+
push. Dedicated jobs also run the suite with the `speed` extra (NumPy) to keep
|
|
58
|
+
the vectorized scan path covered, and the MCP server suite once per OS. Even so,
|
|
59
|
+
contributors touching a specific backend are encouraged to run `pytest tests`
|
|
60
|
+
locally on that platform before submitting. `make ci` runs the same lint,
|
|
61
|
+
type-check and coverage-gated test invocations the workflow does.
|
|
54
62
|
|
|
55
63
|
## Project layout
|
|
56
64
|
|
|
@@ -63,6 +71,7 @@ PyMemoryEditor/
|
|
|
63
71
|
├── win32/ # Windows implementation (kernel32, user32)
|
|
64
72
|
├── linux/ # Linux implementation (process_vm_readv/writev, /proc/<pid>/maps)
|
|
65
73
|
├── macos/ # macOS implementation (task_for_pid, mach_vm_*)
|
|
74
|
+
├── mcp/ # Model Context Protocol server exposed as `pymemoryeditor-mcp`
|
|
66
75
|
└── app/ # PySide6 (Qt) demo app exposed as `pymemoryeditor` CLI
|
|
67
76
|
```
|
|
68
77
|
|
|
@@ -84,6 +93,12 @@ The public alias `OpenProcess` is chosen at import time in `__init__.py` based o
|
|
|
84
93
|
2. Branch from `main`. Keep commits focused.
|
|
85
94
|
3. Run `make pre-commit` (lint + type-check + tests) locally before pushing.
|
|
86
95
|
4. Open a PR describing the change and how it was tested.
|
|
96
|
+
5. Give the PR a [Conventional Commits](https://www.conventionalcommits.org/) title,
|
|
97
|
+
e.g. `fix: reject empty search targets` or `docs(mcp): document --allow-write`.
|
|
98
|
+
A required check lints it: the type must be one of `feat`, `fix`, `perf`,
|
|
99
|
+
`refactor`, `revert`, `docs`, `ci`, `build`, `chore`, `test`, `style`, and
|
|
100
|
+
the subject must start lowercase and not end with a period. PRs are
|
|
101
|
+
squash-merged, so the title becomes the commit message on `main`.
|
|
87
102
|
|
|
88
103
|
## Reporting bugs
|
|
89
104
|
|
|
@@ -30,7 +30,9 @@ help:
|
|
|
30
30
|
@echo " $(YELLOW)install-app$(NC) - Install dependencies to run the PyMemoryEditor App"
|
|
31
31
|
@echo " $(YELLOW)run-app$(NC) - Run the PyMemoryEditor App"
|
|
32
32
|
@echo " $(YELLOW)install-dev$(NC) - Install development dependencies"
|
|
33
|
-
@echo " $(YELLOW)test$(NC) - Run
|
|
33
|
+
@echo " $(YELLOW)test$(NC) - Run the full test suite (incl. live whole-process scans)"
|
|
34
|
+
@echo " $(YELLOW)test-fast$(NC) - Run the fast lane only (-m \"not slow\", ~20 s)"
|
|
35
|
+
@echo " $(YELLOW)test-mcp$(NC) - Run the MCP server suite"
|
|
34
36
|
@echo " $(YELLOW)test-verbose$(NC) - Run tests with verbose output"
|
|
35
37
|
@echo " $(YELLOW)test-coverage$(NC) - Run tests with coverage report"
|
|
36
38
|
@echo " $(YELLOW)lint$(NC) - Run linter (flake8)"
|
|
@@ -40,8 +42,9 @@ help:
|
|
|
40
42
|
@echo " $(YELLOW)build-wheel$(NC) - Build wheel package"
|
|
41
43
|
@echo " $(YELLOW)build-sdist$(NC) - Build source distribution"
|
|
42
44
|
@echo " $(YELLOW)validate$(NC) - Validate package"
|
|
43
|
-
@echo " $(YELLOW)publish$(NC) - Publish to PyPI"
|
|
45
|
+
@echo " $(YELLOW)publish$(NC) - Publish to PyPI by token (emergency path; releases go through GitHub Releases)"
|
|
44
46
|
@echo " $(YELLOW)publish-test$(NC) - Publish to Test PyPI"
|
|
47
|
+
@echo " $(YELLOW)ci$(NC) - Mirror the CI workflow: lint, type-check, both coverage-gated test runs"
|
|
45
48
|
@echo " $(YELLOW)version$(NC) - Show current version"
|
|
46
49
|
@echo " $(YELLOW)check-deps$(NC) - Check for outdated dependencies"
|
|
47
50
|
@echo " $(YELLOW)update-deps$(NC) - Update dependencies"
|
|
@@ -103,13 +106,30 @@ install:
|
|
|
103
106
|
$(PIP) install -e .
|
|
104
107
|
@echo "$(GREEN)Package installed successfully!$(NC)"
|
|
105
108
|
|
|
106
|
-
# Run tests
|
|
109
|
+
# Run tests (everything, including the live scans marked `slow`/`scan_heavy`)
|
|
107
110
|
.PHONY: test
|
|
108
111
|
test:
|
|
109
112
|
@echo "$(GREEN)Running tests...$(NC)"
|
|
110
113
|
$(PYTHON) -m pytest $(TEST_DIR) -v
|
|
111
114
|
@echo "$(GREEN)Tests completed!$(NC)"
|
|
112
115
|
|
|
116
|
+
# Fast lane: unit, property and GUI tests only (~20 s). The markers are
|
|
117
|
+
# defined in pyproject.toml.
|
|
118
|
+
.PHONY: test-fast
|
|
119
|
+
test-fast:
|
|
120
|
+
@echo "$(GREEN)Running the fast test lane (-m \"not slow\")...$(NC)"
|
|
121
|
+
$(PYTHON) -m pytest $(TEST_DIR) -m "not slow"
|
|
122
|
+
@echo "$(GREEN)Fast tests completed!$(NC)"
|
|
123
|
+
|
|
124
|
+
# The MCP server suite. CI runs it as its own job (`build-mcp`), with the
|
|
125
|
+
# pytest-qt plugin disabled because it imports Qt at startup and no test here
|
|
126
|
+
# needs it.
|
|
127
|
+
.PHONY: test-mcp
|
|
128
|
+
test-mcp:
|
|
129
|
+
@echo "$(GREEN)Running the MCP server tests...$(NC)"
|
|
130
|
+
$(PYTHON) -m pytest $(TEST_DIR)/mcp_server -p no:pytest-qt
|
|
131
|
+
@echo "$(GREEN)MCP tests completed!$(NC)"
|
|
132
|
+
|
|
113
133
|
# Run tests with verbose output
|
|
114
134
|
.PHONY: test-verbose
|
|
115
135
|
test-verbose:
|
|
@@ -185,7 +205,10 @@ validate: build
|
|
|
185
205
|
$(PYTHON) -m twine check $(DIST_DIR)/*
|
|
186
206
|
@echo "$(GREEN)Package validation completed!$(NC)"
|
|
187
207
|
|
|
188
|
-
# Publish to PyPI
|
|
208
|
+
# Publish to PyPI by API token. This is the emergency path only: releases are
|
|
209
|
+
# cut by publishing a GitHub Release, which .github/workflows/publish.yml
|
|
210
|
+
# uploads to PyPI via trusted publishing (OIDC, no token) and then lists on
|
|
211
|
+
# the MCP Registry. Use this target only if that workflow is unavailable.
|
|
189
212
|
.PHONY: publish
|
|
190
213
|
publish: validate
|
|
191
214
|
@echo "$(YELLOW)Are you sure you want to publish to PyPI? [y/N]$(NC)" && read ans && [ $${ans:-N} = y ]
|
|
@@ -281,9 +304,19 @@ pre-commit: lint type-check test
|
|
|
281
304
|
pre-publish: all security
|
|
282
305
|
@echo "$(GREEN)Pre-publish checks completed!$(NC)"
|
|
283
306
|
|
|
284
|
-
# CI/CD targets
|
|
307
|
+
# CI/CD targets. Mirrors .github/workflows/python-package.yml: the library
|
|
308
|
+
# and app tests are gated on the library's coverage (.coveragerc-lib), the
|
|
309
|
+
# MCP suite on its own (.coveragerc-mcp), with the same floors as the
|
|
310
|
+
# workflow. Keep the two in sync when either changes.
|
|
285
311
|
.PHONY: ci
|
|
286
|
-
ci: install-dev install lint type-check
|
|
312
|
+
ci: install-dev install lint type-check
|
|
313
|
+
@echo "$(GREEN)Running the library/app suite with the CI coverage gate...$(NC)"
|
|
314
|
+
QT_QPA_PLATFORM=offscreen $(PYTHON) -m pytest $(TEST_DIR) --ignore=$(TEST_DIR)/mcp_server --maxfail=10 \
|
|
315
|
+
--cov=$(PACKAGE_NAME) --cov-config=.coveragerc-lib --cov-report=term --cov-fail-under=62
|
|
316
|
+
@echo "$(GREEN)Running the MCP suite with the CI coverage gate...$(NC)"
|
|
317
|
+
$(PYTHON) -m pytest $(TEST_DIR)/mcp_server -p no:pytest-qt --maxfail=10 \
|
|
318
|
+
--cov=$(PACKAGE_NAME)/mcp --cov-config=.coveragerc-mcp --cov-report=term --cov-fail-under=85
|
|
319
|
+
$(MAKE) build validate
|
|
287
320
|
@echo "$(GREEN)CI pipeline completed!$(NC)"
|
|
288
321
|
|
|
289
322
|
# Show package info
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: PyMemoryEditor
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Read, write and scan process memory in a few lines of Python — Cheat Engine-style scans, pointer chains and AOB search on Windows, Linux and macOS.
|
|
5
5
|
Project-URL: Homepage, https://github.com/JeanExtreme002/PyMemoryEditor
|
|
6
6
|
Project-URL: Documentation, https://pymemoryeditor.readthedocs.io
|
|
@@ -61,6 +61,8 @@ Description-Content-Type: text/markdown
|
|
|
61
61
|
|
|
62
62
|
# PyMemoryEditor
|
|
63
63
|
|
|
64
|
+
<!-- mcp-name: io.github.JeanExtreme002/PyMemoryEditor -->
|
|
65
|
+
|
|
64
66
|
A pure-Python library (built on [ctypes](https://docs.python.org/3/library/ctypes.html)) that lets you **inspect, modify and search the memory of any running process in a few lines of Python** — Cheat Engine workflows on Windows, Linux and macOS!
|
|
65
67
|
|
|
66
68
|
---
|
|
@@ -166,45 +166,76 @@ class NumericItem(QStandardItem):
|
|
|
166
166
|
return self.text() < other.text()
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
_HEX_DIGITS = frozenset("0123456789abcdefABCDEF")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _parse_hex_digits(text: str) -> Optional[int]:
|
|
173
|
+
"""``text`` as an unsigned hex number, or None. Accepts a ``0x`` prefix."""
|
|
174
|
+
cleaned = text.strip()
|
|
175
|
+
if cleaned[:2].lower() == "0x":
|
|
176
|
+
cleaned = cleaned[2:]
|
|
177
|
+
# Explicit digit check rather than relying on int(): int(x, 16) also takes
|
|
178
|
+
# "1_0" (underscore separators) and leading sign/whitespace, none of which
|
|
179
|
+
# is a hex address a user meant to type.
|
|
180
|
+
if not cleaned or not _HEX_DIGITS.issuperset(cleaned):
|
|
181
|
+
return None
|
|
182
|
+
return int(cleaned, 16)
|
|
183
|
+
|
|
184
|
+
|
|
169
185
|
def parse_hex_address(text: str) -> Optional[int]:
|
|
170
186
|
"""Parse a hex address string (with or without 0x prefix) into an int.
|
|
171
187
|
|
|
172
|
-
|
|
188
|
+
Addresses are unsigned: a leading sign makes the string invalid. Returns
|
|
189
|
+
None on any parse error. Surrounding whitespace is tolerated.
|
|
173
190
|
"""
|
|
174
191
|
if not text:
|
|
175
192
|
return None
|
|
176
|
-
|
|
177
|
-
|
|
193
|
+
return _parse_hex_digits(text)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def parse_hex_offset(text: str) -> Optional[int]:
|
|
197
|
+
"""Parse a hex pointer-chain offset, which unlike an address may be negative.
|
|
198
|
+
|
|
199
|
+
``"-0x10"`` → -16, ``"10"`` → 16. Returns None on any parse error.
|
|
200
|
+
"""
|
|
201
|
+
if not text:
|
|
178
202
|
return None
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
cleaned = text.strip()
|
|
204
|
+
negative = cleaned.startswith("-")
|
|
205
|
+
if negative or cleaned.startswith("+"):
|
|
206
|
+
cleaned = cleaned[1:]
|
|
207
|
+
parsed = _parse_hex_digits(cleaned)
|
|
208
|
+
if parsed is None:
|
|
184
209
|
return None
|
|
210
|
+
return -parsed if negative else parsed
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def format_size(size: int) -> str:
|
|
214
|
+
"""Human-readable byte count (``1536`` → ``"1.5 KB"``), binary units."""
|
|
215
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
216
|
+
s = float(size)
|
|
217
|
+
for unit in units:
|
|
218
|
+
if s < 1024 or unit == units[-1]:
|
|
219
|
+
return f"{s:,.1f} {unit}" if unit != "B" else f"{int(s):,} B"
|
|
220
|
+
s /= 1024
|
|
221
|
+
return f"{size:,} B"
|
|
185
222
|
|
|
186
223
|
|
|
187
224
|
def parse_offsets(texts: Iterable[str]) -> Optional[List[int]]:
|
|
188
225
|
"""Parse pointer-chain offset tokens (in order) from raw field strings.
|
|
189
226
|
|
|
190
227
|
Empty tokens are skipped; every remaining token is read as hex (with or
|
|
191
|
-
without ``0x
|
|
192
|
-
the caller treats that as "invalid chain, do
|
|
193
|
-
can be unit-tested directly.
|
|
228
|
+
without ``0x``, optionally negative). Returns ``None`` if any non-empty
|
|
229
|
+
token can't be parsed — the caller treats that as "invalid chain, do
|
|
230
|
+
nothing". Pure (no Qt) so it can be unit-tested directly.
|
|
194
231
|
"""
|
|
195
232
|
offsets: List[int] = []
|
|
196
233
|
for text in texts:
|
|
197
234
|
if not text:
|
|
198
235
|
continue
|
|
199
|
-
parsed =
|
|
236
|
+
parsed = parse_hex_offset(text)
|
|
200
237
|
if parsed is None:
|
|
201
|
-
|
|
202
|
-
# int for ambiguous tokens like "10" (the dialog treats offsets as
|
|
203
|
-
# hex throughout).
|
|
204
|
-
try:
|
|
205
|
-
parsed = int(text, 16)
|
|
206
|
-
except ValueError:
|
|
207
|
-
return None
|
|
238
|
+
return None
|
|
208
239
|
offsets.append(parsed)
|
|
209
240
|
return offsets
|
|
210
241
|
|
|
@@ -230,13 +261,10 @@ def resolve_base_address(
|
|
|
230
261
|
module_name = name_part.strip().strip('"').strip("'").strip()
|
|
231
262
|
offset = parse_hex_address(offset_part)
|
|
232
263
|
if offset is None:
|
|
233
|
-
|
|
234
|
-
offset
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"The offset after '+' must be hex "
|
|
238
|
-
'(e.g. "game.exe"+0x10F4F4).'
|
|
239
|
-
)
|
|
264
|
+
return None, (
|
|
265
|
+
"The offset after '+' must be hex "
|
|
266
|
+
'(e.g. "game.exe"+0x10F4F4).'
|
|
267
|
+
)
|
|
240
268
|
module_base = module_lookup(module_name)
|
|
241
269
|
if module_base is None:
|
|
242
270
|
return None, (
|
|
@@ -5,6 +5,7 @@ Entry point for the PyMemoryEditor App.
|
|
|
5
5
|
A Cheat-Engine-inspired memory scanner built on PySide6 (Qt for Python),
|
|
6
6
|
working on Windows, Linux and macOS.
|
|
7
7
|
"""
|
|
8
|
+
import logging
|
|
8
9
|
import sys
|
|
9
10
|
import signal
|
|
10
11
|
import contextlib
|
|
@@ -266,11 +267,6 @@ def apply_theme(app, theme_id: str = DEFAULT_THEME_ID) -> None:
|
|
|
266
267
|
)
|
|
267
268
|
|
|
268
269
|
|
|
269
|
-
def apply_dark_theme(app) -> None:
|
|
270
|
-
"""Backward-compatible shim that applies the default dark theme."""
|
|
271
|
-
apply_theme(app, DEFAULT_THEME_ID)
|
|
272
|
-
|
|
273
|
-
|
|
274
270
|
STYLE_SHEET = """
|
|
275
271
|
QToolTip {
|
|
276
272
|
color: %(text)s;
|
|
@@ -484,10 +480,66 @@ def _scoped_signal_handler(signalnum, handler):
|
|
|
484
480
|
signal.signal(signalnum, previous_handler)
|
|
485
481
|
|
|
486
482
|
|
|
483
|
+
_LOG = logging.getLogger(__name__)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _qt_excepthook(exc_type, exc_value, exc_tb) -> None:
|
|
487
|
+
"""``sys.excepthook`` for the duration of the Qt event loop.
|
|
488
|
+
|
|
489
|
+
PySide6 catches an exception raised inside a slot, hands it to
|
|
490
|
+
``sys.excepthook`` and carries on — with the default hook that is a
|
|
491
|
+
traceback on stderr the user never sees, and the action they clicked
|
|
492
|
+
silently does nothing. Log it (so the Log Console gets it) and show it.
|
|
493
|
+
KeyboardInterrupt is left to the interpreter's hook: it is the host's
|
|
494
|
+
signal, not an app bug.
|
|
495
|
+
"""
|
|
496
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
497
|
+
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
498
|
+
return
|
|
499
|
+
_LOG.error(
|
|
500
|
+
"Unhandled exception in the UI", exc_info=(exc_type, exc_value, exc_tb)
|
|
501
|
+
)
|
|
502
|
+
try:
|
|
503
|
+
from PySide6.QtCore import QThread
|
|
504
|
+
from PySide6.QtWidgets import QApplication, QMessageBox
|
|
505
|
+
|
|
506
|
+
app = QApplication.instance()
|
|
507
|
+
# Widgets may only be created on the GUI thread. PySide6 routes an
|
|
508
|
+
# exception escaping a QThread.run() through this same hook, from
|
|
509
|
+
# that worker thread, so a dialog there would be undefined behaviour.
|
|
510
|
+
# The log line above is the whole report in that case.
|
|
511
|
+
if app is None or QThread.currentThread() != app.thread():
|
|
512
|
+
return
|
|
513
|
+
QMessageBox.critical(
|
|
514
|
+
None,
|
|
515
|
+
"Unexpected error",
|
|
516
|
+
"%s: %s\n\nThe full traceback is in Tools → Log Console."
|
|
517
|
+
% (exc_type.__name__, exc_value),
|
|
518
|
+
)
|
|
519
|
+
except Exception: # noqa: BLE001 — a hook must never raise
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@contextlib.contextmanager
|
|
524
|
+
def _scoped_excepthook(hook):
|
|
525
|
+
"""Install ``hook`` as ``sys.excepthook`` for the block, then restore."""
|
|
526
|
+
previous = sys.excepthook
|
|
527
|
+
sys.excepthook = hook
|
|
528
|
+
try:
|
|
529
|
+
yield
|
|
530
|
+
finally:
|
|
531
|
+
sys.excepthook = previous
|
|
532
|
+
|
|
533
|
+
|
|
487
534
|
def main(argv=None):
|
|
488
535
|
"""
|
|
489
536
|
Run the app. Safe to call in-process: it installs no signal handler.
|
|
490
537
|
|
|
538
|
+
While the event loop runs, ``sys.excepthook`` is swapped for one that logs
|
|
539
|
+
and shows slot exceptions (PySide6 routes them there and continues), and
|
|
540
|
+
restored on the way out — the loop owns the thread for that time, so an
|
|
541
|
+
embedder's own hook is not observable from inside it anyway.
|
|
542
|
+
|
|
491
543
|
``argv`` defaults to ``sys.argv`` so packaging tools (which call
|
|
492
544
|
``main()`` with no arguments) keep working. Tests and embedders can pass
|
|
493
545
|
an explicit list — previously a positional ``*args`` was accepted but
|
|
@@ -536,7 +588,8 @@ def main(argv=None):
|
|
|
536
588
|
window = MainWindow(process)
|
|
537
589
|
window.show()
|
|
538
590
|
try:
|
|
539
|
-
|
|
591
|
+
with _scoped_excepthook(_qt_excepthook):
|
|
592
|
+
app.exec()
|
|
540
593
|
finally:
|
|
541
594
|
try:
|
|
542
595
|
# `process` is only the handle the picker opened; File → Change
|
|
@@ -559,7 +612,20 @@ def main_cli(argv=None):
|
|
|
559
612
|
|
|
560
613
|
Kept separate from :func:`main` so that process-wide change only happens
|
|
561
614
|
when the app *is* the process, never when it is embedded in someone else's.
|
|
615
|
+
|
|
616
|
+
``pymemoryeditor mcp [flags]`` runs the MCP server instead, the same as
|
|
617
|
+
``pymemoryeditor-mcp [flags]``. MCP registries launch a PyPI package by its
|
|
618
|
+
project name (``uvx --from "PyMemoryEditor[mcp]" pymemoryeditor mcp``), so
|
|
619
|
+
this is the only command they can reach. It never touches Qt, and the
|
|
620
|
+
server keeps its own signal handling.
|
|
562
621
|
"""
|
|
622
|
+
if argv is None:
|
|
623
|
+
argv = sys.argv
|
|
624
|
+
if len(argv) > 1 and argv[1].strip() == "mcp":
|
|
625
|
+
from PyMemoryEditor.mcp.server import main as mcp_main
|
|
626
|
+
|
|
627
|
+
return mcp_main(argv[2:])
|
|
628
|
+
|
|
563
629
|
with _scoped_signal_handler(signal.SIGINT, signal.SIG_DFL):
|
|
564
630
|
return main(argv)
|
|
565
631
|
|
|
@@ -56,11 +56,6 @@ from .value_types import (
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
# Re-exported for backward compatibility with callers that imported the
|
|
60
|
-
# poll-interval constant from this module before the split.
|
|
61
|
-
_TICK_INTERVAL_MS = TICK_INTERVAL_MS
|
|
62
|
-
|
|
63
|
-
|
|
64
59
|
class CheatTable(QWidget):
|
|
65
60
|
"""Bottom pane: saved addresses, freezing, manual edits."""
|
|
66
61
|
|
|
@@ -80,6 +75,14 @@ class CheatTable(QWidget):
|
|
|
80
75
|
self._suspend_signals = False
|
|
81
76
|
# Before _build_ui(), which connects cellChanged — a path that reads it.
|
|
82
77
|
self._poller_stopped = False
|
|
78
|
+
# Set while the owner knows the target is gone (see set_polling_paused):
|
|
79
|
+
# the poller idles on an empty snapshot and edits are refused instead
|
|
80
|
+
# of being queued against a dead handle.
|
|
81
|
+
self._paused = False
|
|
82
|
+
# Write failures reported by the poller since the last flush. They are
|
|
83
|
+
# shown in one dialog per event-loop burst, so a bulk edit whose every
|
|
84
|
+
# row fails (target exited) produces one message, not one per row.
|
|
85
|
+
self._pending_write_failures: List[Tuple[int, str]] = []
|
|
83
86
|
|
|
84
87
|
self._build_ui()
|
|
85
88
|
|
|
@@ -130,6 +133,31 @@ class CheatTable(QWidget):
|
|
|
130
133
|
self.shutdown()
|
|
131
134
|
super().closeEvent(event)
|
|
132
135
|
|
|
136
|
+
def set_polling_paused(self, paused: bool) -> None:
|
|
137
|
+
"""Idle the poll loop while the target is known to be gone.
|
|
138
|
+
|
|
139
|
+
The owning window's heartbeat calls this when the process exits. The
|
|
140
|
+
publish timer stops and the worker is handed an empty snapshot, so it
|
|
141
|
+
sleeps through its ticks instead of hitting a dead handle ten times a
|
|
142
|
+
second (and, on a recycled PID, a stranger's memory). Entries are kept:
|
|
143
|
+
the table still shows the last values read. Resuming re-publishes.
|
|
144
|
+
"""
|
|
145
|
+
if self._poller_stopped or paused == self._paused:
|
|
146
|
+
return
|
|
147
|
+
self._paused = paused
|
|
148
|
+
if paused:
|
|
149
|
+
self._publish_timer.stop()
|
|
150
|
+
self._poller.update_snapshot([])
|
|
151
|
+
else:
|
|
152
|
+
self._publish_snapshot_to_worker()
|
|
153
|
+
self._publish_timer.start()
|
|
154
|
+
|
|
155
|
+
def is_polling_paused(self) -> bool:
|
|
156
|
+
return self._paused
|
|
157
|
+
|
|
158
|
+
def entry_count(self) -> int:
|
|
159
|
+
return len(self._entries)
|
|
160
|
+
|
|
133
161
|
def _build_ui(self) -> None:
|
|
134
162
|
layout = QVBoxLayout(self)
|
|
135
163
|
# Small top inset so the toolbar buttons don't sit flush against the
|
|
@@ -208,8 +236,17 @@ class CheatTable(QWidget):
|
|
|
208
236
|
# spotted from the table, so the floor is enforced once, at the door,
|
|
209
237
|
# rather than at each of those call sites. (The AOB pattern spec is the
|
|
210
238
|
# one whose declared length is 0 — the scanner derives its real width
|
|
211
|
-
# from the pattern.)
|
|
212
|
-
|
|
239
|
+
# from the pattern.) The ceiling is the same one every other width
|
|
240
|
+
# field enforces (MAX_ENTRY_LENGTH); only a JSON import can carry more,
|
|
241
|
+
# and silently resizing its entry would alter what the user exports
|
|
242
|
+
# next, so that is refused -- the import loop reports and skips it.
|
|
243
|
+
length = int(entry.length)
|
|
244
|
+
if length > MAX_ENTRY_LENGTH:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"entry at 0x%X has length %d, over the %d-byte maximum"
|
|
247
|
+
% (entry.address, length, MAX_ENTRY_LENGTH)
|
|
248
|
+
)
|
|
249
|
+
entry.length = max(1, length)
|
|
213
250
|
|
|
214
251
|
# If the address already exists, just refresh its description/type.
|
|
215
252
|
for existing in self._entries:
|
|
@@ -236,7 +273,16 @@ class CheatTable(QWidget):
|
|
|
236
273
|
length: int,
|
|
237
274
|
description: str = "",
|
|
238
275
|
) -> None:
|
|
239
|
-
"""Convenience used by the scanner panel to bulk-promote rows.
|
|
276
|
+
"""Convenience used by the scanner panel to bulk-promote rows.
|
|
277
|
+
|
|
278
|
+
:raises ValueError: if ``length`` is over ``MAX_ENTRY_LENGTH`` -- checked
|
|
279
|
+
once up front so a refusal adds none of the rows rather than some.
|
|
280
|
+
"""
|
|
281
|
+
if int(length) > MAX_ENTRY_LENGTH:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
"length %d is over the %d-byte maximum for a cheat-table entry"
|
|
284
|
+
% (int(length), MAX_ENTRY_LENGTH)
|
|
285
|
+
)
|
|
240
286
|
for addr in addresses:
|
|
241
287
|
self.add_entry(
|
|
242
288
|
CheatEntry(
|
|
@@ -356,8 +402,9 @@ class CheatTable(QWidget):
|
|
|
356
402
|
# next poll tick reads the value back and corrects the cell if the
|
|
357
403
|
# write didn't take, and an outright failure returns via
|
|
358
404
|
# write_failed → _on_write_failed.
|
|
359
|
-
# Nothing to route it to once shutdown() handed the worker over
|
|
360
|
-
|
|
405
|
+
# Nothing to route it to once shutdown() handed the worker over,
|
|
406
|
+
# and nothing to write to while the target is known to be gone.
|
|
407
|
+
if self._poller_stopped or self._paused:
|
|
361
408
|
return
|
|
362
409
|
self._poller.request_write(
|
|
363
410
|
entry.address, entry.spec.pytype, entry.length, value
|
|
@@ -469,13 +516,33 @@ class CheatTable(QWidget):
|
|
|
469
516
|
def _on_write_failed(self, failure) -> None:
|
|
470
517
|
"""Surface a manual value write that failed on the worker thread.
|
|
471
518
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
519
|
+
Writes are async (queued onto the poll worker), so a failure arrives
|
|
520
|
+
here via the ``write_failed`` signal rather than inline. The optimistic
|
|
521
|
+
cell update made when the edit was queued is corrected by the next poll
|
|
522
|
+
tick, which reads back the unchanged value.
|
|
523
|
+
|
|
524
|
+
Failures are collected and shown by ``_flush_write_failures`` on the
|
|
525
|
+
next event-loop pass: the worker drains a bulk edit's queue in one
|
|
526
|
+
tick, so its failures land here back to back and read as one report.
|
|
476
527
|
"""
|
|
477
|
-
|
|
478
|
-
|
|
528
|
+
address, _pytype, _length, message = failure
|
|
529
|
+
if not self._pending_write_failures:
|
|
530
|
+
QTimer.singleShot(0, self._flush_write_failures)
|
|
531
|
+
self._pending_write_failures.append((address, message))
|
|
532
|
+
|
|
533
|
+
def _flush_write_failures(self) -> None:
|
|
534
|
+
failures = self._pending_write_failures
|
|
535
|
+
self._pending_write_failures = []
|
|
536
|
+
if not failures:
|
|
537
|
+
return
|
|
538
|
+
if len(failures) == 1:
|
|
539
|
+
QMessageBox.critical(self, "Write Failed", failures[0][1])
|
|
540
|
+
return
|
|
541
|
+
preview = "\n".join(f"0x{addr:X}: {msg}" for addr, msg in failures[:10])
|
|
542
|
+
extra = "" if len(failures) <= 10 else f"\n…and {len(failures) - 10} more."
|
|
543
|
+
QMessageBox.critical(
|
|
544
|
+
self, "Write Failed", f"{len(failures)} write(s) failed:\n\n{preview}{extra}"
|
|
545
|
+
)
|
|
479
546
|
|
|
480
547
|
def _editing_row(self) -> int:
|
|
481
548
|
"""Return the row currently being edited, or -1 if none."""
|
|
@@ -603,15 +670,20 @@ class CheatTable(QWidget):
|
|
|
603
670
|
if spec.accepts_length_override:
|
|
604
671
|
entry.length = effective_length
|
|
605
672
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
673
|
+
# Same path as an inline edit: the syscall runs on the poll
|
|
674
|
+
# worker, never on the UI thread (a slow target or a page
|
|
675
|
+
# fault would freeze the interface for every row). A write
|
|
676
|
+
# that fails comes back via write_failed → one coalesced
|
|
677
|
+
# dialog; the optimistic update below is corrected by the
|
|
678
|
+
# next tick's read-back.
|
|
679
|
+
if self._poller_stopped or self._paused:
|
|
611
680
|
failures.append(
|
|
612
|
-
(entry.address,
|
|
681
|
+
(entry.address, "the target process is no longer available")
|
|
613
682
|
)
|
|
614
683
|
continue
|
|
684
|
+
self._poller.request_write(
|
|
685
|
+
entry.address, spec.pytype, entry.length, value
|
|
686
|
+
)
|
|
615
687
|
|
|
616
688
|
entry.last_value = value
|
|
617
689
|
if entry.frozen:
|
|
@@ -873,7 +945,7 @@ def prompt_for_manual_entry(parent) -> Optional[CheatEntry]:
|
|
|
873
945
|
"Buffer length (bytes):",
|
|
874
946
|
value=spec.length or 4,
|
|
875
947
|
minValue=1,
|
|
876
|
-
maxValue=
|
|
948
|
+
maxValue=MAX_ENTRY_LENGTH,
|
|
877
949
|
)
|
|
878
950
|
if not ok:
|
|
879
951
|
return None
|
|
@@ -31,7 +31,7 @@ from PySide6.QtWidgets import (
|
|
|
31
31
|
QVBoxLayout,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
from ._widgets import TearsDownOnClose
|
|
34
|
+
from ._widgets import MONOSPACE_FAMILY, TearsDownOnClose
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
_LOGGER_NAME = "PyMemoryEditor"
|
|
@@ -146,7 +146,7 @@ class LogConsoleDialog(TearsDownOnClose, QDialog):
|
|
|
146
146
|
|
|
147
147
|
self._console = QPlainTextEdit()
|
|
148
148
|
self._console.setReadOnly(True)
|
|
149
|
-
self._console.setFont(QFont(
|
|
149
|
+
self._console.setFont(QFont(MONOSPACE_FAMILY, 10))
|
|
150
150
|
self._console.setLineWrapMode(QPlainTextEdit.NoWrap)
|
|
151
151
|
# Cap the buffer so a long-running auto-scan doesn't grow the dialog
|
|
152
152
|
# memory unboundedly. ~5000 lines is plenty for live debugging.
|