A hot-reload infrastructure for IDA plugins with dependency graph analysis and cycle detection. Automatically tracks module dependencies and reloads in correct order.
Project description
IDA Reloader
A hot-reload infrastructure for IDA plugins with dependency graph analysis and cycle detection.
Special thanks to @w00tzenheimer d810-ng project that showed this very elegant pattern.
Features
- Dependency graph analysis: Automatically tracks module dependencies and reloads in correct order
- Cycle detection: Detects and reports circular import dependencies
- Simple API: Easy-to-use
reload_package()function for basic hot-reloading - Advanced control:
Reloaderclass for priority-based reloading and custom workflows - Reloadable infrastructure: The reloader itself can be reloaded without losing state
Installation
Option 1: Install from PyPI (Recommended)
pip install ida-reloader
Option 2: Install from Source
git clone https://github.com/mahmoudimus/ida-reloader.git
cd ida-reloader
pip install -e .
Option 3: Standalone File (No Installation)
For quick integration without installing a package, you can copy the standalone ida_reloader.py file directly into your project:
- Download the latest release from GitHub Releases
- Copy
ida_reloader.pyto your project directory - Import and use directly:
import ida_reloader
ida_reloader.reload_package(my_package)
This option is useful for:
- IDA Pro plugin development where you want a single file
- Projects that can't install external dependencies
- Quick prototyping and testing
Quick Start
Simple Package Reloading
For basic hot-reloading of a package, use the reload_package() function:
from ida_reloader import reload_package
# Reload by module object
import mypackage
reload_package(mypackage)
# Or by package name
reload_package("mypackage")
# Skip certain submodules
reload_package("mypackage", skip=["mypackage.vendor", "mypackage.legacy"])
# Suppress ModuleNotFoundError during reload
reload_package(mypackage, suppress_errors=True)
This function:
- Automatically scans all modules in the package
- Builds a dependency graph
- Detects import cycles
- Reloads modules in topological order (dependencies first)
Advanced: IDA Plugin Integration
For IDA plugins with advanced reload requirements (priority modules, custom hooks):
import ida_hexrays
import ida_kernwin
import idaapi
import ida_reloader
class _UIHooks(idaapi.UI_Hooks):
def ready_to_run(self):
pass
class ReloadablePlugin(ida_reloader.ReloadablePluginBase, idaapi.plugin_t, idaapi.action_handler_t):
#
# Plugin flags:
# - PLUGIN_MOD: plugin may modify the database
# - PLUGIN_PROC: Load/unload plugin when an IDB opens / closes
# - PLUGIN_HIDE: Hide plugin from the IDA plugin menu (if this is set, wanted_hotkey is ignored!)
# - PLUGIN_FIX: Keep plugin alive after IDB is closed
#
#
flags = idaapi.PLUGIN_PROC | idaapi.PLUGIN_MOD
wanted_name = "PLUGIN_NAME_HERE"
wanted_hotkey = "Ctrl-Shift-Q"
comment = "Interface to the PLUGIN_NAME_HERE plugin"
help = ""
def __init__(
self,
*,
global_name: str,
base_package_name: str,
plugin_class: str,
):
super().__init__(global_name, base_package_name, plugin_class, _UIHooks, idaapi.PLUGIN_SKIP, idaapi.PLUGIN_OK)
self.suppress_reload_errors = False
@override
def update(self, ctx: ida_kernwin.action_ctx_base_t) -> int:
return idaapi.AST_ENABLE_ALWAYS
@_compat.override
def activate(self, ctx: ida_kernwin.action_ctx_base_t):
with self.plugin_setup_reload():
self.reload()
return 1
def register_reload_action(self):
idaapi.register_action(
idaapi.action_desc_t(
f"{self.global_name}:reload_plugin",
f"Reload plugin: {self.global_name}",
self,
)
)
def unregister_reload_action(self):
idaapi.unregister_action(f"{self.global_name}:reload_plugin")
@override
def init(self):
if not init_hexrays():
print(f"{self.wanted_name} need Hex-Rays decompiler. Skipping")
return idaapi.PLUGIN_SKIP
kv = ida_kernwin.get_kernel_version().split(".")
if (int(kv[0]) < 7) or ((int(kv[0]) == 7) and (int(kv[1]) < 5)):
print(f"{self.wanted_name} need IDA version >= 7.5. Skipping")
return idaapi.PLUGIN_SKIP
return super().init()
@override
def late_init(self):
super().late_init()
if not ida_hexrays.init_hexrays_plugin():
print(f"{self.wanted_name} need Hex-Rays decompiler. Unloading...")
self.term()
print(f"{self.wanted_name} initialized (version {PLUGIN_VERSION})")
@override
def run(self, args):
with self.plugin_setup_reload():
self.reload()
def reload(self):
"""Hot-reload the *entire* package with priority-based reloading.
This method creates a fresh Reloader instance.
The reloader:
1. Scans all modules in the package and builds a dependency graph
2. Detects strongly-connected components (import cycles)
3. Produces a topological order respecting dependencies
4. Reloads priority modules first (reloadable, then registry)
5. Reloads remaining modules in dependency order
"""
# Create a NEW Reloader instance to pick up any changes to the class
reloader = ida_reloader.Reloader(
base_package=self.base_package_name,
pkg_path=PLUGIN.__path__,
skip_prefixes=(f"{self.base_package_name}.registry",),
priority_prefixes=(
f"{self.base_package_name}.registry", # Then registry (if not skipped)
),
suppress_errors=self.suppress_reload_errors,
)
# Perform the reload
reloader.reload_all()
def PLUGIN_ENTRY():
return ReloadablePlugin()
API Reference
reload_package(target, *, skip=(), suppress_errors=False)
Recursively reload a package and its submodules in dependency order.
Parameters:
target(str | types.ModuleType): The package name or module object to reloadskip(Sequence[str]): Module name prefixes to exclude from reloadingsuppress_errors(bool): If True, ignore ModuleNotFoundError during reload
Example:
from ida_reloader import reload_package
import mypackage
# Simple reload
reload_package(mypackage)
# With options
reload_package("mypackage", skip=["mypackage.vendor"], suppress_errors=True)
Reloader Class
Advanced hot-reload manager with priority-based reload ordering.
Constructor:
Reloader(
base_package: str,
pkg_path: Iterable[str],
*,
skip_prefixes: Sequence[str] = (),
priority_prefixes: Sequence[str] = (),
suppress_errors: bool = False
)
Parameters:
base_package: Base package name (e.g., "mypackage")pkg_path: Package search paths (e.g.,mypackage.__path__)skip_prefixes: Module prefixes to skip during reloadpriority_prefixes: Module prefixes to reload first (in order given)suppress_errors: Whether to suppress ModuleNotFoundError
Methods:
scan(): Scan all modules and update dependency graphreload_all(): Reload all modules in dependency order with priority handling
Example:
from ida_reloader import Reloader
reloader = Reloader(
base_package="mypackage",
pkg_path=mypackage.__path__,
priority_prefixes=("mypackage.core",),
skip_prefixes=("mypackage.vendor",)
)
reloader.reload_all()
How It Works
-
Module Scanning: Recursively scans all modules in the package using
pkgutil.walk_packages() -
Dependency Analysis: Parses each module's AST to extract import statements and build a dependency graph
- Handles relative imports (
. import foo,from .. import bar) - Ignores imports inside
TYPE_CHECKINGguards - Tracks both forward and reverse dependencies
- Handles relative imports (
-
Cycle Detection: Uses Kosaraju's algorithm to find strongly-connected components (import cycles)
- Reports detected cycles as warnings
- Handles cycles gracefully during reload
-
Topological Sort: Produces a reload order that respects dependencies
- Dependencies are reloaded before modules that import them
- Priority modules are reloaded first (if using
Reloaderclass) - Implicit parent package dependencies are added (
pkg.subdepends onpkg)
-
Hot Reload: Reloads modules in the computed order using
importlib.reload()
Use Cases
- IDA Plugin Development: Reload your entire plugin without restarting IDA
- Interactive Development: Test code changes immediately in long-running processes
- Debugging: Quickly iterate on fixes without restarting your application
- Dynamic Code Updates: Update running systems without downtime
Requirements
- Python 3.10+ (uses modern type annotations and match/case)
- For IDA plugins: IDA Pro 7.5+
License
See LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ida_reloader-0.1.0.tar.gz.
File metadata
- Download URL: ida_reloader-0.1.0.tar.gz
- Upload date:
- Size: 20.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
45bec988e66d94bdb8fc3d1d2dce7eff7b888245feb584571529e04bfbdd8a12
|
|
| MD5 |
7e6766cae8a2a3c223e9cfb81bd554e4
|
|
| BLAKE2b-256 |
41f8ccc24cfc67cf8a0a73a73d4521aacaee96da9bb08aee9dea218e573771f5
|
Provenance
The following attestation bundles were made for ida_reloader-0.1.0.tar.gz:
Publisher:
release.yml on mahmoudimus/ida-reloader
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ida_reloader-0.1.0.tar.gz -
Subject digest:
45bec988e66d94bdb8fc3d1d2dce7eff7b888245feb584571529e04bfbdd8a12 - Sigstore transparency entry: 721350590
- Sigstore integration time:
-
Permalink:
mahmoudimus/ida-reloader@67e066c4b1cae4b5d814262dfdbdede7cd4baf08 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mahmoudimus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@67e066c4b1cae4b5d814262dfdbdede7cd4baf08 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ida_reloader-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ida_reloader-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4e0f20e63ab5a625d760beb110f869f7f7b92a8213145eba4a1ab1303471d33
|
|
| MD5 |
8d1d71307b270709c83759d4c2b148df
|
|
| BLAKE2b-256 |
26da562e05479065c6f53c948a2c82a45b1f5db9b8162e941bb11b900677f254
|
Provenance
The following attestation bundles were made for ida_reloader-0.1.0-py3-none-any.whl:
Publisher:
release.yml on mahmoudimus/ida-reloader
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ida_reloader-0.1.0-py3-none-any.whl -
Subject digest:
f4e0f20e63ab5a625d760beb110f869f7f7b92a8213145eba4a1ab1303471d33 - Sigstore transparency entry: 721350610
- Sigstore integration time:
-
Permalink:
mahmoudimus/ida-reloader@67e066c4b1cae4b5d814262dfdbdede7cd4baf08 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mahmoudimus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@67e066c4b1cae4b5d814262dfdbdede7cd4baf08 -
Trigger Event:
push
-
Statement type: