Skip to main content

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: Reloader class 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:

  1. Download the latest release from GitHub Releases
  2. Copy ida_reloader.py to your project directory
  3. 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 reload
  • skip (Sequence[str]): Module name prefixes to exclude from reloading
  • suppress_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 reload
  • priority_prefixes: Module prefixes to reload first (in order given)
  • suppress_errors: Whether to suppress ModuleNotFoundError

Methods:

  • scan(): Scan all modules and update dependency graph
  • reload_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

  1. Module Scanning: Recursively scans all modules in the package using pkgutil.walk_packages()

  2. 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_CHECKING guards
    • Tracks both forward and reverse dependencies
  3. Cycle Detection: Uses Kosaraju's algorithm to find strongly-connected components (import cycles)

    • Reports detected cycles as warnings
    • Handles cycles gracefully during reload
  4. Topological Sort: Produces a reload order that respects dependencies

    • Dependencies are reloaded before modules that import them
    • Priority modules are reloaded first (if using Reloader class)
    • Implicit parent package dependencies are added (pkg.sub depends on pkg)
  5. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ida_reloader-0.1.0.tar.gz (20.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ida_reloader-0.1.0-py3-none-any.whl (13.4 kB view details)

Uploaded Python 3

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

Hashes for ida_reloader-0.1.0.tar.gz
Algorithm Hash digest
SHA256 45bec988e66d94bdb8fc3d1d2dce7eff7b888245feb584571529e04bfbdd8a12
MD5 7e6766cae8a2a3c223e9cfb81bd554e4
BLAKE2b-256 41f8ccc24cfc67cf8a0a73a73d4521aacaee96da9bb08aee9dea218e573771f5

See more details on using hashes here.

Provenance

The following attestation bundles were made for ida_reloader-0.1.0.tar.gz:

Publisher: release.yml on mahmoudimus/ida-reloader

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for ida_reloader-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f4e0f20e63ab5a625d760beb110f869f7f7b92a8213145eba4a1ab1303471d33
MD5 8d1d71307b270709c83759d4c2b148df
BLAKE2b-256 26da562e05479065c6f53c948a2c82a45b1f5db9b8162e941bb11b900677f254

See more details on using hashes here.

Provenance

The following attestation bundles were made for ida_reloader-0.1.0-py3-none-any.whl:

Publisher: release.yml on mahmoudimus/ida-reloader

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page