PyMemoryEditor 3.3.2__tar.gz → 3.4.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.3.2 → pymemoryeditor-3.4.0}/PKG-INFO +1 -1
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/__init__.py +1 -1
- pymemoryeditor-3.4.0/PyMemoryEditor/app/_gc.py +71 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/application.py +13 -4
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/linux/process.py +3 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/macos/process.py +4 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/mcp/policy.py +7 -3
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/mcp/toolset.py +4 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/abstract.py +105 -13
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/util/pattern.py +10 -1
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/functions.py +7 -4
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/process.py +4 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/conf.py +1 -1
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/pointer-scan.md +8 -2
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/pyproject.toml +1 -0
- pymemoryeditor-3.4.0/tests/app/conftest.py +31 -0
- pymemoryeditor-3.4.0/tests/app/test_app_gc.py +58 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_app_smoke.py +30 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/core/test_abstract_process.py +18 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/conftest.py +45 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_parsing.py +21 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_policy.py +6 -2
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_toolset.py +3 -2
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_write_safety.py +62 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_bitness_fallback_warns.py +23 -4
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/pointer/test_pointer_scan.py +43 -2
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/test_pattern_compile.py +7 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/.flake8 +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/.gitignore +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/.readthedocs.yaml +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/CODE_OF_CONDUCT.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/CONTRIBUTING.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/LICENSE +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/Makefile +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/__main__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/_icon.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/_widgets.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/assets/icon.svg +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/cheat_entry.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/cheat_poll_worker.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/cheat_table.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/log_console_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/main_window.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/memory_map_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/memory_viewer_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/modules_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/open_process_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/pointer_chain_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/pointer_scan_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/results_view.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/scan_types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/scan_worker.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/scanner_panel.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/threads_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/app/value_types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/enums.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/linux/functions.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/linux/libc.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/linux/types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/macos/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/macos/functions.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/macos/libsystem.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/macos/types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/mcp/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/mcp/__main__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/mcp/config.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/mcp/procinfo.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/mcp/server.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/mcp/session.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/_guard.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/errors.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/info.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/module_info.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/pointer_scan.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/region.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/remote_pointer.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/scanning.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/thread_info.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/process/util.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/py.typed +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/util/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/util/convert.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/util/scan.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/util/scan_numpy.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/enums/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/PyMemoryEditor/win32/types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/README.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/SECURITY.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/assets/screenshots/app.png +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/assets/social-preview.html +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/assets/social-preview.png +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/_static/.gitkeep +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/_static/custom.css +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/_templates/layout.html +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/enums.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/errors.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/index.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/memory-region.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/module-info.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/openprocess.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/pointer-path.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/remote-pointer.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/thread-info.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/api/utilities.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/app.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/contributing.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/funding.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/glossary.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/allocate-free.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/logging.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/memory-regions.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/modules-threads.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/opening-process.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/pattern-scan.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/pointers.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/read-write.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/guide/searching.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/index.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/installation.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/license.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/mcp.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/platform-notes.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/quickstart.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/requirements.txt +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/troubleshooting.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/docs/why.md +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_app_cheat_entry.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_app_pointer_parsing.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_app_scan_request.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_app_value_types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_app_widgets.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_cheat_poll_worker.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_main_window_lifecycle.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_open_process_dialog.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_refine_scan_worker.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/app/test_results_model.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/conftest.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/core/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/core/test_errors.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/core/test_logger.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_argument_fuzz.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_fake_fidelity.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_live.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_registry.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_server.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/mcp_server/test_session.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_allocate_free.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_bufflength_inference.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_editor.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_narrow_width_consistency.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_partial_io.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_read_into.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_search_argument_errors.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_str_boundary.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_str_decode_consistency.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_typed_accessors.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_unaligned_scan.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_write_span.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/memory/test_write_str_bytes_width.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_bitness.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_closed_generators.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_linux_error_hints.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_linux_pid_reuse.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_linux_proc_decoding.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_linux_snapshot_passthrough.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_linux_types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_macos_close.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_macos_load_commands.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_macos_protect.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_macos_region_walk.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_macos_snapshot.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_macos_threads.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_macos_transient_reads.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_macos_types.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_win32_backend_guards.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_win32_permissions.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_win32_region_walk.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_win32_toolhelp.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/platforms/test_win32_wow64.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/pointer/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/pointer/test_pointer_chain.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/pointer/test_remote_pointer.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/process/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/process/test_foreign_process.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/process/test_memory_region.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/process/test_module_enumeration.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/process/test_partial_name_match.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/process/test_process_enumeration.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/process/test_process_lookup.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/process/test_thread_enumeration.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/__init__.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/_fakes.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/test_chunking_integration.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/test_pattern_scan.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/test_region_snapshot.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/test_scan.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/test_scan_numpy.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/test_scan_properties.py +0 -0
- {pymemoryeditor-3.3.2 → pymemoryeditor-3.4.0}/tests/scan/test_scanning_helper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: PyMemoryEditor
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.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
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Run Python's cyclic garbage collector on the GUI thread only.
|
|
4
|
+
|
|
5
|
+
The automatic collector runs in whichever thread happens to allocate when a
|
|
6
|
+
threshold is crossed -- in this app, often a ``QThread`` worker in the middle
|
|
7
|
+
of a scan. If that collection frees a reference cycle holding Qt objects that
|
|
8
|
+
belong to the GUI thread, they are destroyed from the worker, which crashes
|
|
9
|
+
the process (a segfault on macOS, an access violation on Windows; seen on CI
|
|
10
|
+
in the pointer-scan worker's ``_hand_over``).
|
|
11
|
+
|
|
12
|
+
So while the app runs, automatic collection is switched off and a timer on the
|
|
13
|
+
GUI thread collects instead, generation by generation, as the automatic
|
|
14
|
+
collector would have. Plain reference counting still frees everything that is
|
|
15
|
+
not in a cycle immediately, in any thread.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import contextlib
|
|
19
|
+
import gc
|
|
20
|
+
from typing import Iterator, Optional
|
|
21
|
+
|
|
22
|
+
from PySide6.QtCore import QObject, QTimer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#: How often the GUI thread checks whether a collection is due.
|
|
26
|
+
COLLECT_INTERVAL_MS = 500
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MainThreadGarbageCollector(QObject):
|
|
30
|
+
"""Disable automatic collection and collect from a GUI-thread timer.
|
|
31
|
+
|
|
32
|
+
Create it on the GUI thread; :meth:`stop` restores the collector's previous
|
|
33
|
+
state, so an embedder's own setting survives the app.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, parent: Optional[QObject] = None, interval_ms: int = COLLECT_INTERVAL_MS
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
self._was_enabled = gc.isenabled()
|
|
41
|
+
self._threshold = gc.get_threshold()
|
|
42
|
+
gc.disable()
|
|
43
|
+
self._timer = QTimer(self)
|
|
44
|
+
self._timer.timeout.connect(self.collect_if_due)
|
|
45
|
+
self._timer.start(interval_ms)
|
|
46
|
+
|
|
47
|
+
def collect_if_due(self) -> None:
|
|
48
|
+
"""Collect each generation whose allocation count passed its threshold."""
|
|
49
|
+
count0, count1, count2 = gc.get_count()
|
|
50
|
+
if count0 > self._threshold[0]:
|
|
51
|
+
gc.collect(0)
|
|
52
|
+
if count1 > self._threshold[1]:
|
|
53
|
+
gc.collect(1)
|
|
54
|
+
if count2 > self._threshold[2]:
|
|
55
|
+
gc.collect(2)
|
|
56
|
+
|
|
57
|
+
def stop(self) -> None:
|
|
58
|
+
"""Stop the timer and give automatic collection back."""
|
|
59
|
+
self._timer.stop()
|
|
60
|
+
if self._was_enabled:
|
|
61
|
+
gc.enable()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@contextlib.contextmanager
|
|
65
|
+
def garbage_collected_on_gui_thread(parent: Optional[QObject] = None) -> Iterator[None]:
|
|
66
|
+
"""Run the body with :class:`MainThreadGarbageCollector` in charge."""
|
|
67
|
+
collector = MainThreadGarbageCollector(parent)
|
|
68
|
+
try:
|
|
69
|
+
yield
|
|
70
|
+
finally:
|
|
71
|
+
collector.stop()
|
|
@@ -560,10 +560,7 @@ def main(argv=None):
|
|
|
560
560
|
from PySide6.QtCore import QSettings
|
|
561
561
|
from PySide6.QtWidgets import QApplication
|
|
562
562
|
|
|
563
|
-
from .
|
|
564
|
-
from .main_window import MainWindow
|
|
565
|
-
from .open_process_dialog import OpenProcessDialog
|
|
566
|
-
|
|
563
|
+
from ._gc import garbage_collected_on_gui_thread
|
|
567
564
|
from ._icon import app_icon
|
|
568
565
|
|
|
569
566
|
app = QApplication.instance() or QApplication(argv)
|
|
@@ -577,6 +574,18 @@ def main(argv=None):
|
|
|
577
574
|
saved_theme = str(QSettings().value("theme", DEFAULT_THEME_ID))
|
|
578
575
|
apply_theme(app, saved_theme)
|
|
579
576
|
|
|
577
|
+
# Every dialog from here on runs QThread workers; see _gc for why the
|
|
578
|
+
# cyclic collector must not run inside them.
|
|
579
|
+
with garbage_collected_on_gui_thread(app):
|
|
580
|
+
_pick_process_and_run(app)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _pick_process_and_run(app) -> None:
|
|
584
|
+
"""The picker, then the main window until the event loop exits."""
|
|
585
|
+
from ._widgets import detached_workers_running
|
|
586
|
+
from .main_window import MainWindow
|
|
587
|
+
from .open_process_dialog import OpenProcessDialog
|
|
588
|
+
|
|
580
589
|
picker = OpenProcessDialog()
|
|
581
590
|
if picker.exec() != picker.DialogCode.Accepted:
|
|
582
591
|
return
|
|
@@ -196,12 +196,14 @@ class LinuxProcess(AbstractProcess):
|
|
|
196
196
|
bufflength: Optional[int] = None,
|
|
197
197
|
) -> T:
|
|
198
198
|
self.__require_target(hot_path=True)
|
|
199
|
+
self._require_address(address)
|
|
199
200
|
return read_process_memory(
|
|
200
201
|
self.pid, address, pytype, resolve_bufflength(pytype, bufflength)
|
|
201
202
|
)
|
|
202
203
|
|
|
203
204
|
def read_process_memory_into(self, address: int, buffer: object) -> int:
|
|
204
205
|
self.__require_target(hot_path=True)
|
|
206
|
+
self._require_address(address)
|
|
205
207
|
return read_process_memory_into(self.pid, address, buffer)
|
|
206
208
|
|
|
207
209
|
def _scan_addresses(
|
|
@@ -270,6 +272,7 @@ class LinuxProcess(AbstractProcess):
|
|
|
270
272
|
value: Union[bool, int, float, str, bytes] = UNSET,
|
|
271
273
|
) -> Union[bool, int, float, str, bytes]:
|
|
272
274
|
self.__require_target()
|
|
275
|
+
self._require_address(address)
|
|
273
276
|
w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value)
|
|
274
277
|
write_process_memory(self.pid, address, w_pytype, w_length, w_value)
|
|
275
278
|
return value
|
|
@@ -255,12 +255,14 @@ class MacProcess(AbstractProcess):
|
|
|
255
255
|
bufflength: Optional[int] = None,
|
|
256
256
|
) -> T:
|
|
257
257
|
self._require_open()
|
|
258
|
+
self._require_address(address)
|
|
258
259
|
return read_process_memory(
|
|
259
260
|
self.__task, address, pytype, resolve_bufflength(pytype, bufflength)
|
|
260
261
|
)
|
|
261
262
|
|
|
262
263
|
def read_process_memory_into(self, address: int, buffer: object) -> int:
|
|
263
264
|
self._require_open()
|
|
265
|
+
self._require_address(address)
|
|
264
266
|
return read_process_memory_into(self.__task, address, buffer)
|
|
265
267
|
|
|
266
268
|
def write_process_memory(
|
|
@@ -296,6 +298,7 @@ class MacProcess(AbstractProcess):
|
|
|
296
298
|
:param value: value to be written.
|
|
297
299
|
"""
|
|
298
300
|
self._require_open()
|
|
301
|
+
self._require_address(address)
|
|
299
302
|
w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value)
|
|
300
303
|
write_process_memory(self.__task, address, w_pytype, w_length, w_value)
|
|
301
304
|
return value
|
|
@@ -308,6 +311,7 @@ class MacProcess(AbstractProcess):
|
|
|
308
311
|
|
|
309
312
|
def free_memory(self, address: int, size: int = 0) -> bool:
|
|
310
313
|
self._require_open()
|
|
314
|
+
self._require_address(address)
|
|
311
315
|
# mach_vm_deallocate needs the exact size. Reuse the tracked size when
|
|
312
316
|
# the caller doesn't supply one.
|
|
313
317
|
actual_size = size or self.__allocations.get(address, 0)
|
|
@@ -218,6 +218,12 @@ class AccessDecision:
|
|
|
218
218
|
needs_approval: bool = False
|
|
219
219
|
|
|
220
220
|
|
|
221
|
+
def _self_and_ancestor_pids() -> FrozenSet[int]:
|
|
222
|
+
"""This process, its parent and every ancestor: what ``protect_self``
|
|
223
|
+
refuses. A function of its own so tests can swap the real chain out."""
|
|
224
|
+
return frozenset({os.getpid(), os.getppid()}) | ancestor_pids()
|
|
225
|
+
|
|
226
|
+
|
|
221
227
|
class ProcessPolicy:
|
|
222
228
|
"""Decides whether a given ``(pid, name)`` may be attached to.
|
|
223
229
|
|
|
@@ -282,9 +288,7 @@ class ProcessPolicy:
|
|
|
282
288
|
# further up. Taken once: the chain that launched the server does not
|
|
283
289
|
# change while it runs.
|
|
284
290
|
self._protected: FrozenSet[int] = (
|
|
285
|
-
|
|
286
|
-
if protect_self
|
|
287
|
-
else frozenset()
|
|
291
|
+
_self_and_ancestor_pids() if protect_self else frozenset()
|
|
288
292
|
)
|
|
289
293
|
self._identify = identify
|
|
290
294
|
# Only Linux truncates names; applying the rule elsewhere would let a
|
|
@@ -424,6 +424,10 @@ def _parse_signed_int(raw: Any, field: str) -> int:
|
|
|
424
424
|
negative = text.startswith("-")
|
|
425
425
|
body = text[1:] if negative else text
|
|
426
426
|
try:
|
|
427
|
+
# int() accepts a sign of its own, so "--5" would strip to "-5" and
|
|
428
|
+
# the two minuses cancel: a write of "--5" stored +5. One sign only.
|
|
429
|
+
if negative and body.lstrip()[:1] in ("-", "+"):
|
|
430
|
+
raise ValueError(body)
|
|
427
431
|
value = int(body, 16) if body[:2].lower() == "0x" else int(body, 10)
|
|
428
432
|
except (ValueError, IndexError):
|
|
429
433
|
raise ToolError(
|
|
@@ -53,15 +53,22 @@ _POINTER_SCAN_FORMAT = "pymemoryeditor-pointerscan"
|
|
|
53
53
|
_POINTER_SCAN_VERSION = 1
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
class
|
|
57
|
-
"""
|
|
56
|
+
class _AddressOutOfRangeError(ValueError, OSError):
|
|
57
|
+
"""An address (or a pointer chain step) lies outside the address space.
|
|
58
58
|
|
|
59
59
|
A ``ValueError`` as documented, and also an ``OSError`` because the failed
|
|
60
60
|
read of the wrapped address it replaces raised one: callers that guard a
|
|
61
|
-
stale chain with ``except OSError`` keep working.
|
|
61
|
+
stale chain or a bad address with ``except OSError`` keep working.
|
|
62
62
|
"""
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
# One past the highest address this host can pass to the OS. ctypes masks an
|
|
66
|
+
# address argument to the width of the C type it is converted to instead of
|
|
67
|
+
# rejecting it, so a 32-bit Python would turn 0x1_0000_1000 into 0x1000 just as
|
|
68
|
+
# a 64-bit one turns 2**64 + 0x1000 into 0x1000.
|
|
69
|
+
_HOST_ADDRESS_END = min(ADDRESS_SPACE_END, 1 << (8 * ctypes.sizeof(ctypes.c_void_p)))
|
|
70
|
+
|
|
71
|
+
|
|
65
72
|
class AbstractProcess(ABC):
|
|
66
73
|
"""
|
|
67
74
|
Abstract class to represent a process.
|
|
@@ -205,6 +212,23 @@ class AbstractProcess(ABC):
|
|
|
205
212
|
if not self._target_alive():
|
|
206
213
|
raise ProcessIDNotExistsError(self.pid)
|
|
207
214
|
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _require_address(address: int) -> None:
|
|
217
|
+
"""
|
|
218
|
+
Refuse an address outside ``[0, host address space)`` before it
|
|
219
|
+
reaches ctypes, which would mask it to the C type's width and read,
|
|
220
|
+
write or free a *different*, wrapped address instead of failing.
|
|
221
|
+
"""
|
|
222
|
+
if not 0 <= address < _HOST_ADDRESS_END:
|
|
223
|
+
raise _AddressOutOfRangeError(
|
|
224
|
+
"address %s is outside the %d-bit address space; it would "
|
|
225
|
+
"wrap around to a different address."
|
|
226
|
+
% (
|
|
227
|
+
("-0x%X" % -address) if address < 0 else ("0x%X" % address),
|
|
228
|
+
_HOST_ADDRESS_END.bit_length() - 1,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
208
232
|
@abstractmethod
|
|
209
233
|
def _detect_is_64bit(self) -> Optional[bool]:
|
|
210
234
|
"""
|
|
@@ -736,7 +760,9 @@ class AbstractProcess(ABC):
|
|
|
736
760
|
and ``convert_from_byte_array``. Callers that need the original
|
|
737
761
|
bytes verbatim (no decoding) should pass ``pytype=bytes``.
|
|
738
762
|
|
|
739
|
-
:raises ValueError: if ``pytype`` is unsupported
|
|
763
|
+
:raises ValueError: if ``pytype`` is unsupported, or ``address`` is
|
|
764
|
+
negative or past the end of the address space (also an
|
|
765
|
+
``OSError``, so ``except OSError`` still catches it).
|
|
740
766
|
:raises OSError: if the read fails, or returns fewer bytes than
|
|
741
767
|
requested (e.g. the range crosses an unreadable/freed page).
|
|
742
768
|
"""
|
|
@@ -770,7 +796,9 @@ class AbstractProcess(ABC):
|
|
|
770
796
|
|
|
771
797
|
:raises TypeError: if ``buffer`` is not a writable buffer (e.g. an
|
|
772
798
|
immutable ``bytes`` object).
|
|
773
|
-
:raises ValueError: if ``buffer`` is empty or not contiguous
|
|
799
|
+
:raises ValueError: if ``buffer`` is empty or not contiguous, or
|
|
800
|
+
``address`` is outside the address space (as in
|
|
801
|
+
:meth:`read_process_memory`).
|
|
774
802
|
:raises OSError: if the read fails, or returns fewer bytes than
|
|
775
803
|
requested (e.g. the range crosses an unreadable/freed page).
|
|
776
804
|
|
|
@@ -821,7 +849,8 @@ class AbstractProcess(ABC):
|
|
|
821
849
|
(``write_process_memory(address, int, 4, 99)``).
|
|
822
850
|
:raises ValueError: if ``pytype`` is unsupported, or if an ``int``
|
|
823
851
|
``value`` does not fit in ``bufflength`` bytes (rejected up front
|
|
824
|
-
rather than silently truncated)
|
|
852
|
+
rather than silently truncated), or if ``address`` is outside the
|
|
853
|
+
address space (as in :meth:`read_process_memory`).
|
|
825
854
|
:raises OSError: if the write fails, or fewer bytes than requested are
|
|
826
855
|
written (e.g. the range crosses an unwritable/freed page).
|
|
827
856
|
:return: the original ``value`` passed in.
|
|
@@ -1080,7 +1109,8 @@ class AbstractProcess(ABC):
|
|
|
1080
1109
|
:return: ``True`` on success.
|
|
1081
1110
|
|
|
1082
1111
|
:raises ValueError: on macOS and Windows, for a region this object did
|
|
1083
|
-
not allocate when no ``size`` is given
|
|
1112
|
+
not allocate when no ``size`` is given, or for an ``address``
|
|
1113
|
+
outside the address space.
|
|
1084
1114
|
:raises NotImplementedError: on Linux (see :meth:`allocate_memory`).
|
|
1085
1115
|
"""
|
|
1086
1116
|
raise NotImplementedError()
|
|
@@ -1190,7 +1220,7 @@ class AbstractProcess(ABC):
|
|
|
1190
1220
|
# it, so `ptr + offset` below zero or past 2**64 would silently
|
|
1191
1221
|
# read a *different*, wrapped address. Refuse it by name instead.
|
|
1192
1222
|
if not 0 <= addr < ADDRESS_SPACE_END:
|
|
1193
|
-
raise
|
|
1223
|
+
raise _AddressOutOfRangeError(
|
|
1194
1224
|
"pointer chain step produced address %s, outside the "
|
|
1195
1225
|
"64-bit address space; the chain is stale (a hop read "
|
|
1196
1226
|
"NULL or garbage) or an offset is wrong."
|
|
@@ -1303,6 +1333,15 @@ class AbstractProcess(ABC):
|
|
|
1303
1333
|
bounds it. (To abort the map-building phase, raise from
|
|
1304
1334
|
``progress_callback``.)
|
|
1305
1335
|
|
|
1336
|
+
:raises ValueError: ``ptr_size`` other than 4 or 8, ``max_depth`` or
|
|
1337
|
+
``max_results`` below 1, or a negative ``max_offset``.
|
|
1338
|
+
:raises ClosedProcess: the process was closed (also raised by the
|
|
1339
|
+
returned generator once it is).
|
|
1340
|
+
|
|
1341
|
+
Arguments and access are checked at the call, like every ``search_*``
|
|
1342
|
+
method; the pointer map is built and walked lazily, as the generator
|
|
1343
|
+
is consumed.
|
|
1344
|
+
|
|
1306
1345
|
Example
|
|
1307
1346
|
-------
|
|
1308
1347
|
::
|
|
@@ -1316,11 +1355,26 @@ class AbstractProcess(ABC):
|
|
|
1316
1355
|
live = path.rebase(process).to_pointer(process, pytype=int, bufflength=4)
|
|
1317
1356
|
live.value = 9999
|
|
1318
1357
|
"""
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1358
|
+
# Validate eagerly, like every search_* method: a bad argument or a
|
|
1359
|
+
# closed process raises here, at the call, not on the first next().
|
|
1360
|
+
# The map build and path search stay lazy in _iter_pointer_paths.
|
|
1361
|
+
#
|
|
1362
|
+
# A subclass written against the 3.3 contract implements the listings
|
|
1363
|
+
# itself and need not define these hooks (see __init_subclass__): each
|
|
1364
|
+
# check runs only where the subclass provides it, instead of hitting
|
|
1365
|
+
# the NotImplementedError of the base _require_open.
|
|
1366
|
+
cls = type(self)
|
|
1367
|
+
guarded = cls._require_open is not AbstractProcess._require_open
|
|
1368
|
+
if guarded or cls._require_scanning is not AbstractProcess._require_scanning:
|
|
1369
|
+
self._require_scanning()
|
|
1370
|
+
|
|
1371
|
+
if max_depth < 1:
|
|
1372
|
+
raise ValueError("max_depth must be at least 1.")
|
|
1373
|
+
if max_offset < 0:
|
|
1374
|
+
raise ValueError("max_offset must not be negative.")
|
|
1375
|
+
if max_results is not None and max_results < 1:
|
|
1376
|
+
# 0 would build the whole pointer map only to yield nothing.
|
|
1377
|
+
raise ValueError("max_results must be at least 1 (or None for no cap).")
|
|
1324
1378
|
|
|
1325
1379
|
if ptr_size is None:
|
|
1326
1380
|
ptr_size = self.pointer_size
|
|
@@ -1330,6 +1384,44 @@ class AbstractProcess(ABC):
|
|
|
1330
1384
|
"ptr_size must be 4 (32-bit target) or 8 (64-bit target)."
|
|
1331
1385
|
)
|
|
1332
1386
|
|
|
1387
|
+
scan = self._iter_pointer_paths(
|
|
1388
|
+
target_address,
|
|
1389
|
+
max_depth=max_depth,
|
|
1390
|
+
max_offset=max_offset,
|
|
1391
|
+
ptr_size=ptr_size,
|
|
1392
|
+
aligned=aligned,
|
|
1393
|
+
writable_only=writable_only,
|
|
1394
|
+
static_ranges=static_ranges,
|
|
1395
|
+
max_results=max_results,
|
|
1396
|
+
memory_regions=memory_regions,
|
|
1397
|
+
progress_callback=progress_callback,
|
|
1398
|
+
should_stop=should_stop,
|
|
1399
|
+
)
|
|
1400
|
+
return guard_generator(scan, self._require_open) if guarded else scan
|
|
1401
|
+
|
|
1402
|
+
def _iter_pointer_paths(
|
|
1403
|
+
self,
|
|
1404
|
+
target_address: int,
|
|
1405
|
+
*,
|
|
1406
|
+
max_depth: int,
|
|
1407
|
+
max_offset: int,
|
|
1408
|
+
ptr_size: int,
|
|
1409
|
+
aligned: bool,
|
|
1410
|
+
writable_only: bool,
|
|
1411
|
+
static_ranges: Optional[Sequence[Tuple[int, int]]],
|
|
1412
|
+
max_results: Optional[int],
|
|
1413
|
+
memory_regions: Optional[Sequence[MemoryRegion]],
|
|
1414
|
+
progress_callback: Optional["Callable[[float], None]"],
|
|
1415
|
+
should_stop: Optional["Callable[[], bool]"],
|
|
1416
|
+
) -> Generator["PointerPath", None, None]:
|
|
1417
|
+
"""The lazy body of :meth:`scan_pointer_paths`, run once arguments are
|
|
1418
|
+
validated."""
|
|
1419
|
+
from .pointer_scan import (
|
|
1420
|
+
AddressRanges,
|
|
1421
|
+
build_pointer_map,
|
|
1422
|
+
find_pointer_paths,
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1333
1425
|
if memory_regions is None:
|
|
1334
1426
|
memory_regions = list(self.get_memory_regions())
|
|
1335
1427
|
|
|
@@ -71,9 +71,18 @@ def compile_pattern(
|
|
|
71
71
|
:raises ValueError: malformed IDA-style token, or ``byte_length`` omitted
|
|
72
72
|
for a regex / pre-compiled pattern.
|
|
73
73
|
:raises TypeError: if ``pattern`` is not a ``str``, ``bytes`` or
|
|
74
|
-
``re.Pattern[bytes]
|
|
74
|
+
``re.Pattern[bytes]`` — a compiled ``str`` regex included, as it can
|
|
75
|
+
never match process memory.
|
|
75
76
|
"""
|
|
76
77
|
if isinstance(pattern, re.Pattern):
|
|
78
|
+
# A str pattern can't match process memory: it would only fail later,
|
|
79
|
+
# deep inside the scan, with "cannot use a string pattern on a
|
|
80
|
+
# bytes-like object".
|
|
81
|
+
if not isinstance(pattern.pattern, bytes):
|
|
82
|
+
raise TypeError(
|
|
83
|
+
"Pattern must be a compiled bytes regex (re.compile(rb'...')), "
|
|
84
|
+
"not a str one."
|
|
85
|
+
)
|
|
77
86
|
if byte_length <= 0:
|
|
78
87
|
raise ValueError(
|
|
79
88
|
"byte_length= is required when passing a pre-compiled regex "
|
|
@@ -251,6 +251,8 @@ _PROCESSOR_ARCHITECTURE_64BIT = (6, 9, 12)
|
|
|
251
251
|
_OS_IS_64BIT = (
|
|
252
252
|
_native_system_information.wProcessorArchitecture in _PROCESSOR_ARCHITECTURE_64BIT
|
|
253
253
|
)
|
|
254
|
+
# The running Python's bitness: the fallback guess when a target's can't be read.
|
|
255
|
+
_HOST_IS_64BIT = ctypes.sizeof(ctypes.c_void_p) == 8
|
|
254
256
|
|
|
255
257
|
|
|
256
258
|
def mbi_class_for_handle(process_handle: int):
|
|
@@ -318,12 +320,13 @@ def IsProcess64Bit(process_handle: int) -> bool:
|
|
|
318
320
|
return detected
|
|
319
321
|
|
|
320
322
|
_logger.warning(
|
|
321
|
-
"IsWow64Process failed (err=%d); assuming the target
|
|
322
|
-
"
|
|
323
|
-
"
|
|
323
|
+
"IsWow64Process failed (err=%d); assuming the target matches the "
|
|
324
|
+
"host bitness (%d-bit). Pointer-width detection may be wrong if it "
|
|
325
|
+
"does not.",
|
|
324
326
|
ctypes.get_last_error(),
|
|
327
|
+
64 if _HOST_IS_64BIT else 32,
|
|
325
328
|
)
|
|
326
|
-
return
|
|
329
|
+
return _HOST_IS_64BIT
|
|
327
330
|
|
|
328
331
|
|
|
329
332
|
T = TypeVar("T")
|
|
@@ -303,6 +303,7 @@ class WindowsProcess(AbstractProcess):
|
|
|
303
303
|
) -> T:
|
|
304
304
|
self._require_open()
|
|
305
305
|
self.__require_read()
|
|
306
|
+
self._require_address(address)
|
|
306
307
|
return ReadProcessMemory(
|
|
307
308
|
self.__process_handle,
|
|
308
309
|
address,
|
|
@@ -313,6 +314,7 @@ class WindowsProcess(AbstractProcess):
|
|
|
313
314
|
def read_process_memory_into(self, address: int, buffer: object) -> int:
|
|
314
315
|
self._require_open()
|
|
315
316
|
self.__require_read()
|
|
317
|
+
self._require_address(address)
|
|
316
318
|
return ReadProcessMemoryInto(self.__process_handle, address, buffer)
|
|
317
319
|
|
|
318
320
|
def write_process_memory(
|
|
@@ -324,6 +326,7 @@ class WindowsProcess(AbstractProcess):
|
|
|
324
326
|
) -> Union[bool, int, float, str, bytes]:
|
|
325
327
|
self._require_open()
|
|
326
328
|
self.__require_write()
|
|
329
|
+
self._require_address(address)
|
|
327
330
|
w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value)
|
|
328
331
|
WriteProcessMemory(
|
|
329
332
|
self.__process_handle,
|
|
@@ -344,6 +347,7 @@ class WindowsProcess(AbstractProcess):
|
|
|
344
347
|
def free_memory(self, address: int, size: int = 0) -> bool:
|
|
345
348
|
self._require_open()
|
|
346
349
|
self.__require_operation()
|
|
350
|
+
self._require_address(address)
|
|
347
351
|
# MEM_RELEASE tears down whatever VirtualAlloc'd block starts at
|
|
348
352
|
# `address` -- a heap segment or a thread stack just as readily as one
|
|
349
353
|
# of ours -- so an address this object did not allocate is refused
|
|
@@ -23,7 +23,7 @@ copyright = f"{datetime.now().year}, {author}"
|
|
|
23
23
|
try:
|
|
24
24
|
from PyMemoryEditor import __version__ as release
|
|
25
25
|
except Exception: # pragma: no cover - docs can build without the package installed
|
|
26
|
-
release = "3.
|
|
26
|
+
release = "3.4.0"
|
|
27
27
|
|
|
28
28
|
version = ".".join(release.split(".")[:2])
|
|
29
29
|
|
|
@@ -43,8 +43,8 @@ It carries everything you need to reconstruct the chain in another run.
|
|
|
43
43
|
(default — faster and usually correct).
|
|
44
44
|
:param static_ranges: explicit ``(start, size)`` ranges to treat as valid
|
|
45
45
|
chain bases. Defaults to the image range of every loaded module.
|
|
46
|
-
:param int max_results: stop after yielding this many paths
|
|
47
|
-
Recommended for shallow exploration.
|
|
46
|
+
:param int max_results: stop after yielding this many paths (at least 1;
|
|
47
|
+
``None`` for no cap). Recommended for shallow exploration.
|
|
48
48
|
:param memory_regions: optional snapshot from
|
|
49
49
|
:py:meth:`snapshot_memory_regions`.
|
|
50
50
|
:param progress_callback: a callable ``callback(fraction)`` invoked as the
|
|
@@ -55,6 +55,12 @@ It carries everything you need to reconstruct the chain in another run.
|
|
|
55
55
|
yielded. The search can run a long time between two paths, so this is
|
|
56
56
|
how a deadline or a cancel button bounds it.
|
|
57
57
|
:returns: a generator of :py:class:`PointerPath`.
|
|
58
|
+
:raises ValueError: ``ptr_size`` other than 4 or 8, ``max_depth`` or
|
|
59
|
+
``max_results`` below 1, or a negative ``max_offset``.
|
|
60
|
+
:raises ClosedProcess: the process was closed.
|
|
61
|
+
|
|
62
|
+
Arguments are checked when you call the method; the pointer map is built
|
|
63
|
+
and walked lazily, as you iterate.
|
|
58
64
|
```
|
|
59
65
|
|
|
60
66
|
## Tuning the scan
|
|
@@ -241,6 +241,7 @@ skip_empty = true
|
|
|
241
241
|
[tool.pytest.ini_options]
|
|
242
242
|
addopts = "-n auto --dist=loadfile"
|
|
243
243
|
markers = [
|
|
244
|
+
"real_self_protection: MCP tests that check the real self/ancestor protection; the others see stand-in pids (tests/mcp_server/conftest.py)",
|
|
244
245
|
"slow: integration tests that read/scan the current process's whole address space (minutes, not seconds)",
|
|
245
246
|
"scan_heavy: tests that scan a *live* process's entire address space. Separated from `slow` because the cost is wildly platform-dependent: a macOS process exposes ~390 GiB across ~145 regions and a Windows one is not much better, so on a CI runner each scan is minutes rather than the ~1s it takes on a developer machine. CI runs these on Linux only (see the build-mcp job); the library's own live-scan coverage on the other two platforms comes from the build/build-macos jobs.",
|
|
246
247
|
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Shared setup for the GUI app tests."""
|
|
4
|
+
|
|
5
|
+
import gc
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(autouse=True)
|
|
11
|
+
def _collect_garbage_on_the_test_thread():
|
|
12
|
+
"""Keep the cyclic collector out of the tests' QThread workers.
|
|
13
|
+
|
|
14
|
+
The app does this with ``MainThreadGarbageCollector`` (``app/_gc.py``), but
|
|
15
|
+
the tests build widgets and workers without going through ``main()``. An
|
|
16
|
+
automatic collection inside a worker can free reference cycles that hold
|
|
17
|
+
the GUI thread's Qt objects -- left over from this or an earlier test --
|
|
18
|
+
and destroy them from the wrong thread, which crashed the pytest worker on
|
|
19
|
+
CI (segfault on macOS, access violation on Windows). So automatic
|
|
20
|
+
collection is off during each test, and the garbage is collected here, on
|
|
21
|
+
the test's own thread, at teardown.
|
|
22
|
+
"""
|
|
23
|
+
was_enabled = gc.isenabled()
|
|
24
|
+
gc.collect()
|
|
25
|
+
gc.disable()
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
finally:
|
|
29
|
+
gc.collect()
|
|
30
|
+
if was_enabled:
|
|
31
|
+
gc.enable()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""``MainThreadGarbageCollector``: the cyclic collector runs on the GUI thread."""
|
|
4
|
+
|
|
5
|
+
import gc
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
pytest.importorskip("PySide6")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Cycle:
|
|
14
|
+
def __init__(self, sink):
|
|
15
|
+
self.self = self
|
|
16
|
+
self.sink = sink
|
|
17
|
+
|
|
18
|
+
def __del__(self):
|
|
19
|
+
self.sink.append(threading.current_thread() is threading.main_thread())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_it_turns_automatic_collection_off_and_back_on(qtbot):
|
|
23
|
+
from PyMemoryEditor.app._gc import MainThreadGarbageCollector
|
|
24
|
+
|
|
25
|
+
gc.enable()
|
|
26
|
+
collector = MainThreadGarbageCollector()
|
|
27
|
+
try:
|
|
28
|
+
assert not gc.isenabled()
|
|
29
|
+
finally:
|
|
30
|
+
collector.stop()
|
|
31
|
+
assert gc.isenabled()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_it_keeps_a_caller_disabled_collector_disabled(qtbot):
|
|
35
|
+
from PyMemoryEditor.app._gc import MainThreadGarbageCollector
|
|
36
|
+
|
|
37
|
+
gc.disable()
|
|
38
|
+
try:
|
|
39
|
+
MainThreadGarbageCollector().stop()
|
|
40
|
+
assert not gc.isenabled()
|
|
41
|
+
finally:
|
|
42
|
+
gc.enable()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_its_timer_collects_cycles_on_the_gui_thread(qtbot, monkeypatch):
|
|
46
|
+
from PyMemoryEditor.app._gc import MainThreadGarbageCollector
|
|
47
|
+
|
|
48
|
+
gc.collect()
|
|
49
|
+
# Any allocation makes a collection due, so the first tick runs one.
|
|
50
|
+
monkeypatch.setattr(gc, "get_threshold", lambda: (0, 0, 0))
|
|
51
|
+
collector = MainThreadGarbageCollector(interval_ms=10)
|
|
52
|
+
try:
|
|
53
|
+
freed = []
|
|
54
|
+
_Cycle(freed)
|
|
55
|
+
qtbot.waitUntil(lambda: bool(freed), timeout=2000)
|
|
56
|
+
assert freed == [True]
|
|
57
|
+
finally:
|
|
58
|
+
collector.stop()
|
|
@@ -131,6 +131,36 @@ def test_main_cli_scopes_sig_dfl_to_the_run(monkeypatch):
|
|
|
131
131
|
signal.signal(signal.SIGINT, original)
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
def test_main_runs_the_cyclic_collector_on_the_gui_thread_only(monkeypatch):
|
|
135
|
+
"""
|
|
136
|
+
While the app runs, automatic collection is off (a timer on the GUI thread
|
|
137
|
+
collects instead), so a collection never runs inside a QThread worker and
|
|
138
|
+
destroys GUI-thread Qt objects from there. It is back on afterwards.
|
|
139
|
+
"""
|
|
140
|
+
import gc
|
|
141
|
+
|
|
142
|
+
from PyMemoryEditor.app import application, open_process_dialog
|
|
143
|
+
|
|
144
|
+
seen = {}
|
|
145
|
+
|
|
146
|
+
class _ProbingDialog:
|
|
147
|
+
class DialogCode:
|
|
148
|
+
Accepted = 1
|
|
149
|
+
|
|
150
|
+
def exec(self):
|
|
151
|
+
seen["inside"] = gc.isenabled()
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
process = None
|
|
155
|
+
|
|
156
|
+
monkeypatch.setattr(open_process_dialog, "OpenProcessDialog", _ProbingDialog)
|
|
157
|
+
|
|
158
|
+
gc.enable()
|
|
159
|
+
assert application.main(["pymemoryeditor"]) is None
|
|
160
|
+
assert seen["inside"] is False
|
|
161
|
+
assert gc.isenabled()
|
|
162
|
+
|
|
163
|
+
|
|
134
164
|
def test_scoped_signal_handler_is_a_noop_off_the_main_thread():
|
|
135
165
|
"""
|
|
136
166
|
``signal.signal`` only works on the main thread. The helper must degrade to
|