How Holehe Uses TrioProgress for Asynchronous Progress Tracking

TrioProgress is a custom Trio instrument in Holehe that monitors coroutine lifecycle events to drive a real-time tqdm progress bar, giving users live visual feedback during email OSINT scans.

Holehe is an asynchronous email reconnaissance tool that checks username and email availability across hundreds of platforms. Because it leverages the Trio async library to run concurrent checks, it needs a way to visualize progress without blocking the event loop. The solution is a custom instrument called TrioProgress, implemented in holehe/instruments.py, that hooks into Trio's task scheduling system to track completion status in real time.

What is TrioProgress?

TrioProgress is a specialized class that inherits from trio.abc.Instrument, allowing it to receive low-level callbacks whenever tasks are spawned or completed. According to the source code in holehe/instruments.py, this instrument bridges Trio's internal task management with the tqdm library to produce a command-line progress bar.

The class maintains three critical state attributes:

  • _total: The total number of modules (check coroutines) to execute
  • _bar: A tqdm.tqdm instance configured with the module count
  • _running: A counter tracking currently active tasks

How TrioProgress Tracks Async Tasks

The instrument works by implementing two key callback methods that Trio automatically invokes during the nursery lifecycle.

Task Spawn Monitoring

When the main scanning loop starts a new check coroutine, Trio calls task_spawned(). This method increments the internal _running counter and advances the progress bar by one unit:


# holehe/instruments.py

def task_spawned(self, task):
    self._running += 1
    self._bar.update(1)

This callback fires immediately when nursery.start_soon() is invoked in holehe/core.py, ensuring the progress bar updates the instant a new platform check begins.

Task Exit Handling

When a coroutine completes—whether successfully or with an exception—Trio invokes task_exited(). The instrument decrements the running counter and closes the progress bar when all tasks finish:


# holehe/instruments.py

def task_exited(self, task):
    self._running -= 1
    if self._running == 0:
        self._bar.close()

This ensures the visual indicator accurately reflects the actual state of the async worker pool, preventing the bar from stalling on failed or cancelled checks.

tqdm Integration

The constructor initializes the visual component using tqdm's standard API:


# holehe/instruments.py

def __init__(self, total):
    self._total = total
    self._bar = tqdm(total=total, desc="Scanning", unit="module")
    self._running = 0

By setting unit="module", the progress bar displays "module" as the unit of work, making it clear to users that each increment represents one platform check completing.

Integration in the Core Scanning Loop

In holehe/core.py, the main async runner orchestrates the instrument's lifecycle around the Trio nursery. Before spawning any check coroutines, the code registers the instrument using Trio's low-level API. After all tasks complete, it cleans up by removing the instrument.


# holehe/core.py (conceptual excerpt)

import trio
from .instruments import TrioProgress

async def maincore(module_coroutines):
    # Initialize progress tracking with total module count

    instrument = TrioProgress(total=len(module_coroutines))
    
    # Register the instrument to receive task callbacks

    trio.lowlevel.add_instrument(instrument)
    
    try:
        async with trio.open_nursery() as nursery:
            for coro in module_coroutines:
                nursery.start_soon(coro)
    finally:
        # Ensure cleanup happens even if errors occur

        trio.lowlevel.remove_instrument(instrument)

This pattern ensures the progress bar exists only during active scanning and does not interfere with other Trio operations or subsequent runs.

Practical Usage Examples

Command Line Execution

When running Holehe normally, the TrioProgress bar appears automatically as modules execute:

holehe -u user@example.com

Output shows real-time progress:


Scanning: 23/45 [===============>................]  51%

Custom Script Integration

You can reuse the same progress tracking in custom async scripts that leverage Holehe's checking modules:

import trio
from holehe.instruments import TrioProgress
from holehe.modules import my_custom_check

async def run_custom_scan(email):
    # Build list of coroutines

    coroutines = [
        my_custom_check(email),
        # ... additional checks

    ]
    
    # Setup progress tracking

    progress = TrioProgress(total=len(coroutines))
    trio.lowlevel.add_instrument(progress)
    
    try:
        async with trio.open_nursery() as nursery:
            for coro in coroutines:
                nursery.start_soon(coro)
    finally:
        trio.lowlevel.remove_instrument(progress)

if __name__ == "__main__":
    trio.run(run_custom_scan, "target@example.com")

Programmatic Progress Monitoring

If you need to extract progress data for a GUI or logging system instead of stdout, subclass TrioProgress to expose the counters:

from holehe.instruments import TrioProgress

class LoggingProgress(TrioProgress):
    def task_spawned(self, task):
        super().task_spawned(task)
        logger.info(f"Started {task.name}, {self._running} active")
    
    def task_exited(self, task):
        super().task_exited(task)
        logger.info(f"Finished {task.name}, {self._running} remaining")

Summary

  • TrioProgress is defined in holehe/instruments.py and inherits from trio.abc.Instrument to receive async task callbacks.
  • It tracks coroutine lifecycle through task_spawned() and task_exited() methods, maintaining an internal counter of active checks.
  • The instrument wraps a tqdm progress bar that updates in real-time as each platform check starts, providing "Scanning: X/total" visual feedback.
  • In holehe/core.py, the instrument is registered with trio.lowlevel.add_instrument() before opening the nursery and removed with remove_instrument() after completion.
  • This architecture separates progress visualization from business logic while leveraging Trio's native instrumentation API for zero-overhead monitoring.

Frequently Asked Questions

How does TrioProgress handle failed or crashing modules?

When a check coroutine raises an exception, Trio's nursery management still invokes task_exited() on the instrument. The method decrements the _running counter regardless of exit status, ensuring the progress bar advances even when modules crash. The exception propagation is handled separately by the nursery's error policy, but the visual indicator remains accurate.

Can I use TrioProgress with asyncio instead of Trio?

No. TrioProgress specifically implements trio.abc.Instrument, which is part of Trio's low-level instrumentation API. This protocol has no equivalent in asyncio's architecture. To use Holehe's progress tracking, you must run the checks within Trio's event loop using trio.run() or trio.lowlevel.start_guest_run().

Why does the progress bar show "module" as the unit instead of "site" or "check"?

The __init__ method in holehe/instruments.py explicitly sets unit="module" when constructing the tqdm instance. This terminology reflects the codebase architecture where each platform checker is implemented as a separate module. You can customize this by subclassing and overriding the constructor, but the default aligns with Holehe's internal naming conventions.

Is the progress bar thread-safe when running multiple scans concurrently?

Yes, because Trio instruments are bound to a specific Trio thread and event loop. Each call to trio.lowlevel.add_instrument() registers the instance only for that particular task tree. If you run multiple scans in parallel using separate Trio low-level calls or guest runs, each maintains its own instrument state, preventing race conditions on the tqdm output.

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 →