How TrioProgress Integrates with Holehe's Task Lifecycle: Deep Dive into Async Instrumentation

TrioProgress hooks into Trio's low-level instrumentation API to track every spawned and exited task during Holehe's concurrent email-checking operations, providing real-time progress visibility without blocking the async event loop.

Holehe leverages Trio, a structured concurrency library for Python, to execute multiple OSINT module checks simultaneously. To monitor these concurrent operations, the tool implements a custom trio.abc.Instrument subclass called TrioProgress that intercepts task lifecycle events during the entire execution flow.

The Integration Architecture

Holehe's integration of TrioProgress follows a precise lifecycle: registration, execution monitoring, and cleanup. This pattern ensures that every async task spawned to check an email against a service is counted and reported without interfering with the actual workload.

Instrument Initialization and Registration

When maincore() begins execution in holehe/core.py, it instantiates TrioProgress with the user's verbosity preferences. The instrument is immediately registered with Trio's low-level system using trio.lowlevel.add_instrument().


# holehe/core.py

instrument = TrioProgress(verbose=verbose, debug=debug)
trio.lowlevel.add_instrument(instrument)

This registration occurs before any async work begins, ensuring the instrument captures the complete task lifecycle. The TrioProgress constructor initializes counters (started and finished) and optionally prints an initialization message when verbose mode is enabled.

Task Spawning in the Nursery

Holehe uses Trio's nursery pattern to manage concurrent execution. Inside the runner() async function, the code opens a nursery and spawns each module check as a separate task:


# holehe/core.py

async def runner():
    async with trio.open_nursery() as nursery:
        for module in modules:
            nursery.start_soon(module.run, email, verbose, debug, sleep, output, mode, json_out)

Each call to nursery.start_soon() triggers Trio's internal task spawning mechanism, which automatically invokes the task_spawned() callback on any registered instrument, including our TrioProgress instance.

Lifecycle Callbacks and Progress Tracking

The TrioProgress class in holehe/instruments.py implements three critical methods from trio.abc.Instrument:

  • task_spawned(task): Increments the started counter and optionally logs debug output showing the task object representation
  • task_exited(task): Increments the finished counter when a task completes, whether successfully or with an exception
  • after_run(): Prints a final completion summary showing the ratio of finished to started tasks

# holehe/instruments.py

class TrioProgress(trio.abc.Instrument):
    def task_spawned(self, task):
        self.started += 1
        if self.debug:
            print(f"[TrioProgress] Task spawned: {task!r}")
    
    def task_exited(self, task):
        self.finished += 1
        if self.debug:
            print(f"[TrioProgress] Task exited: {task!r}")
    
    def after_run(self):
        if self.verbose:
            print(f"[TrioProgress] Completed {self.finished}/{self.started} tasks")

These callbacks execute synchronously within Trio's event loop, allowing accurate counting without introducing async overhead or blocking delays.

Cleanup and Instrument Removal

After trio.run(runner) completes and the nursery closes, maincore() explicitly removes the instrument from Trio's system:


# holehe/core.py

trio.run(runner)
trio.lowlevel.remove_instrument(instrument)

This cleanup step is essential to prevent memory leaks and ensure that subsequent calls to maincore() (if any) start with fresh instrumentation rather than accumulating stale callbacks.

Code Implementation Details

The TrioProgress Class

Located in holehe/instruments.py, the TrioProgress class inherits from trio.abc.Instrument and tracks state across the entire run:


# holehe/instruments.py

import trio

class TrioProgress(trio.abc.Instrument):
    def __init__(self, verbose=False, debug=False):
        self.verbose = verbose
        self.debug = debug
        self.started = 0
        self.finished = 0
        if self.verbose:
            print("[TrioProgress] Initialized")

The class maintains simple integer counters that are incremented thread-safely by Trio's internal machinery, making it suitable for high-concurrency scenarios where dozens of modules may run simultaneously.

Core Orchestration Logic

The maincore() function serves as the synchronous entry point that bridges the CLI interface with the async Trio world. It handles the full instrument lifecycle:


# holehe/core.py

def maincore(email, verbose, debug, sleep, output, mode, json_out, modules):
    instrument = TrioProgress(verbose=verbose, debug=debug)
    trio.lowlevel.add_instrument(instrument)
    
    async def runner():
        async with trio.open_nursery() as nursery:
            for module in modules:
                nursery.start_soon(module.run, email, verbose, debug, sleep, output, mode, json_out)
    
    trio.run(runner)
    trio.lowlevel.remove_instrument(instrument)

This design separates the synchronous setup/teardown from the async execution logic, keeping the instrumentation plumbing isolated from the actual OSINT work performed by individual modules.

Practical Usage Example

To see TrioProgress in action during a Holehe scan, enable verbose output:

holehe user@example.com --verbose

With verbose mode enabled, you'll see output similar to:


[TrioProgress] Initialized
[TrioProgress] Completed 15/15 tasks

For debugging concurrent behavior, use the debug flag to see individual task spawn/exit events:

holehe user@example.com --debug

This outputs detailed task objects as they enter and exit the nursery:


[TrioProgress] Task spawned: <Task '__main__.runner.<locals>._run_wrapper' at 0x...>
[TrioProgress] Task exited: <Task '__main__.runner.<locals>._run_wrapper' at 0x...>

Summary

  • TrioProgress is a custom trio.abc.Instrument implementation that tracks task lifecycle events in Holehe's async execution engine.
  • The instrument is registered via trio.lowlevel.add_instrument() in holehe/core.py before the nursery opens and removed via trio.lowlevel.remove_instrument() after completion.
  • It counts tasks via task_spawned() and task_spawned() callbacks, reporting progress through after_run() when verbose mode is active.
  • This integration allows Holehe to monitor dozens of concurrent email verification checks without modifying the individual module code or blocking the event loop.

Frequently Asked Questions

What is the purpose of TrioProgress in Holehe?

TrioProgress provides transparent monitoring of concurrent task execution. It allows users to see how many OSINT checks have started and finished when running Holehe against an email address, giving visibility into the progress of potentially dozens of simultaneous network requests without interfering with the actual verification logic.

How does TrioProgress differ from a standard progress bar library?

Unlike traditional progress bars that require manual updates within business logic, TrioProgress leverages Trio's instrumentation API to intercept task events automatically. Standard libraries like tqdm require explicit update() calls in user code, whereas TrioProgress receives callbacks directly from the Trio runtime whenever any task spawns or exits, making it decoupled from the individual module implementations in holehe/modules/.

Can TrioProgress be used outside of Holehe?

Yes, the pattern is reusable for any Trio-based application. The TrioProgress class implements the standard trio.abc.Instrument interface, meaning you can instantiate it and add it to any Trio application using trio.lowlevel.add_instrument() to track task metrics. However, the specific output formatting and counter logic in Holehe's implementation are tailored for tracking module execution counts.

Why does Holehe remove the instrument after trio.run() completes?

Removal prevents state leakage and callback accumulation. According to the implementation in holehe/core.py, calling trio.lowlevel.remove_instrument(instrument) ensures that the instrument's references are released and its callbacks are unregistered. This is critical for long-running processes or repeated invocations where stale instruments could cause memory leaks or double-counting in subsequent runs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →