# How the Patator Controller Base Class Manages Module Lifecycles

> Learn how the Patator Controller base class manages module lifecycles using a producer-consumer multiprocessing architecture for efficient argument parsing, parallel execution, and progress reporting.

- Repository: [lanjelot/patator](https://github.com/lanjelot/patator)
- Tags: internals
- Published: 2026-03-05

---

**The `Controller` class in [`src/patator/patator.py`](https://github.com/lanjelot/patator/blob/main/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`](https://github.com/lanjelot/patator/blob/main/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).

```python
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.

```python
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:

```python
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`](https://github.com/lanjelot/patator/blob/main/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.