How the Patator Controller Base Class Manages Module Lifecycles

The Controller class in src/patator/patator.py orchestrates the complete runtime of Patator modules through a producer-consumer multiprocessing architecture, handling argument parsing, payload generation, parallel execution, and progress reporting via shared namespaces and process queues.

Patator is a modular brute-force testing framework that relies on a robust execution engine to coordinate diverse security testing modules. At its core, the Controller base class defined in src/patator/patator.py manages every phase of a module's lifecycle—from initialization and payload generation to parallel execution and final aggregation. Understanding this architecture reveals how Patator achieves high-performance concurrent testing while maintaining clean separation between orchestration logic and specific protocol implementations.

Construction and Initialization

The lifecycle begins in Controller.__init__, which prepares the execution environment before any worker processes spawn. The constructor initializes critical data structures including self.thread_report (queues for per-thread results), self.thread_progress (counters for each worker), self.payload (template dictionary for payload values), self.iter_keys (parsed iterator specifications), and self.iter_groups (grouping configurations for pitch-fork mode).

def __init__(self, module, argv):
    self.thread_report = []
    self.thread_progress = []
    self.payload = {}
    self.iter_keys = {}
    self.iter_groups = {}
    self.enc_keys = []
    self.module = module
    
    opts, args = self.parse_usage(argv)
    self.ns = manager.Namespace()

The method calls parse_usage to build an optparse.OptionParser instance that handles both global options and module-specific arguments. It then creates self.ns, a multiprocessing.Namespace object that serves as shared mutable state accessible across all processes. This namespace initialization occurs in lines 84-95 of the source file.

The Shared Namespace Architecture

self.ns functions as the central coordination mechanism for inter-process communication. All workers read from and write to this namespace to maintain global state consistency during execution.

Key namespace attributes include:

  • actions – Maps pending actions for each payload (skip, free, hit, etc.)
  • free_list and skip_list – Track values excluded from remaining iterations
  • paused – Boolean flag that pauses the consumer loop when set
  • quit_now – Global termination flag triggered by SIGINT or fatal errors
  • total_size and start_time – Store overall payload count and execution timing

These fields are accessed throughout the lifecycle in produce, consume, monitor_progress, and final reporting routines.

Spawning the Worker Pipeline

The start_threads method (lines 42-55) initializes the multiprocessing infrastructure by creating communication queues and spawning worker processes. It establishes one producer process and multiple consumer processes based on the num_threads configuration.

def start_threads(self):
    task_queues = [multiprocessing.Queue(maxsize=10000) for _ in range(self.num_threads)]
    
    for num in range(self.num_threads):
        report_queue = multiprocessing.Queue(maxsize=1000)
        t = multiprocessing.Process(name='Consumer-%d' % num,
                                    target=self.consume,
                                    args=(task_queues[num], report_queue, logger.queue))
        t.daemon = True
        t.start()
        self.thread_report.append(report_queue)
        self.thread_progress.append(Progress())
    
    t = multiprocessing.Process(name='Producer',
                                target=self.produce,
                                args=(task_queues, logger.queue))
    t.daemon = True
    t.start()

Each consumer receives a dedicated task_queue with a maximum size of 10,000 items and a report_queue capped at 1,000 entries. The producer distributes payloads across all consumer queues using a round-robin algorithm.

Payload Generation in the Producer

The produce method (lines 71-120) handles payload generation by computing the Cartesian product of all iterator keys (FILE, NET, MOD, etc.). It iterates over self.iter_keys, creates appropriate iterators (FileIter, IP, RangeIter, ProgIter), and groups them according to self.iter_groups for pitch-fork mode operation.

The producer applies start/stop offsets and resume points before enqueueing each payload into the appropriate consumer queue. It also calculates the total payload space and stores this value in self.ns.total_size (line 63), enabling accurate progress tracking throughout the execution.

Consumer Execution Loop

Each consumer process executes the consume method, which runs a continuous loop processing payloads from its assigned queue. The method performs several critical operations for each payload:

  1. Expands placeholders such as FILE#, NET#, and COMBO# within a copy of self.payload (lines 49-68)
  2. Applies encodings like _@@_:b64 through regex substitution (lines 69-71)
  3. Checks skip/free conditions via should_skip and should_free methods
  4. Executes the module through module.execute(**payload) with retry and timeout handling
  5. Maps responses to actions using lookup_actions to determine if results indicate hits, failures, or skips
  6. Reports results back to the main process through the report_queue

The consumer respects global control flags: it aborts immediately when self.ns.quit_now is set, and enters a sleep loop when self.ns.paused is true (lines 90-93).

Progress Monitoring and Reporting

The main thread coordinates progress tracking through two primary methods. report_progress pulls items from each consumer's report_queue, updates per-thread statistics in self.thread_progress, and aggregates actions across all workers. monitor_progress runs concurrently with workers, repeatedly invoking report_progress while any processes remain alive (lines 56-60).

Upon completion of all workers, the controller prints a final summary including hit counts, completed payloads, skips, failures, total size, and average execution rate.

Finalization Hooks for Subclasses

The Controller class provides two extension points for module-specific cleanup and aggregation:

def push_final(self, resp): pass
def show_final(self): pass

push_final receives each final response for modules requiring per-result data collection, while show_final executes after the run completes to display aggregated statistics. The Controller_Finger subclass demonstrates this pattern by implementing these methods to gather unique user lines (lines 52-60), allowing modules to customize result presentation without modifying the core lifecycle logic.

Summary

  • The Controller class in src/patator/patator.py serves as the central execution engine, managing module lifecycles from initialization through final reporting.
  • A producer-consumer architecture enables parallel execution, with one producer generating payloads and multiple consumers executing module logic across separate processes.
  • Shared state coordination occurs through multiprocessing.Namespace (self.ns), which tracks global flags, skip lists, and progress counters accessible to all workers.
  • Queue-based communication uses separate task queues (maxsize 10,000) for payload distribution and report queues (maxsize 1,000) for result aggregation.
  • Lifecycle hooks (push_final and show_final) allow subclasses to implement custom result aggregation without altering the core execution flow.
  • The architecture supports graceful interruption through the quit_now and paused flags, enabling clean shutdowns and pause/resume functionality.

Frequently Asked Questions

How does Patator handle graceful shutdown during a scan?

The Controller implements signal handling that sets self.ns.quit_now to True when receiving SIGINT or encountering fatal errors. Consumer processes check this flag during each iteration of their main loop and abort immediately when set, while the main thread waits for all workers to exit before printing final statistics. This design ensures no zombie processes remain and partial results are preserved.

What is the difference between the Producer and Consumer processes?

The Producer (single process) generates the complete payload space by iterating over all input sources (FILE, NET, RANGE, etc.) and distributes combinations to task queues. Consumers (multiple processes, typically matching CPU count) pull individual payloads from their dedicated queues, execute the module's execute method, handle retries/timeouts, and push results back through report queues. This separation allows CPU-intensive payload generation to proceed independently of network-bound module execution.

How can modules customize result aggregation?

Modules subclassing Controller can override the push_final and show_final methods. push_final receives each response object for real-time data extraction (e.g., collecting unique usernames), while show_final executes after completion to display aggregated statistics. The Controller_Finger implementation in the source code demonstrates gathering unique lines and presenting them in a final summary without modifying the core controller logic.

What controls the number of concurrent workers in Patator?

The num_threads parameter (typically set via command-line options parsed in __init__) determines consumer process count in start_threads. Each consumer operates as a separate multiprocessing.Process with dedicated communication queues, allowing true parallel execution across CPU cores. The producer remains a single process regardless of thread count, as payload generation is typically I/O bound rather than CPU intensive.

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 →