This document explains the role and functionality of the Worker class (grazr/core/worker.py) and its associated QThread in the Grazr application. The worker system is fundamental to keeping the UI responsive by offloading potentially long-running or blocking operations to a separate thread.
- Overview of the Worker System
- The
WorkerClass - Task Dispatching
- Task Execution Flow in
doWork - Returning Results (
resultReadySignal Emission) - Interaction with
MainWindow.handleWorkerResult - Adding New Tasks to the Worker
- Troubleshooting Worker Tasks
- Contributing to
worker.py
Many operations in Grazr, such as starting/stopping services, installing Nginx site configurations, generating SSL certificates, or managing PHP extensions, can take a noticeable amount of time. Performing these directly on the main UI thread would cause the application to freeze and become unresponsive.
The worker system offloads these operations to a separate background thread (QThread), ensuring the UI remains smooth and responsive.
- In
grazr/main.py(orgrazr/ui/main_window.pyduringMainWindowinitialization):- A
QThreadinstance is created:self.thread = QThread(self)(parented toMainWindow). - An instance of the
Workerclass is created:self.worker = Worker(). - The worker object is moved to the thread:
self.worker.moveToThread(self.thread). This means the worker's slots will execute in the context ofself.thread, not the main UI thread. - The thread is started:
self.thread.start().
- A
- Signals and Slots for Communication:
MainWindowhas a signaltriggerWorker = Signal(str, dict)which is connected toself.worker.doWork.Workerhas a signalresultReady = Signal(str, dict, bool, str)which is connected toself.main_window.handleWorkerResult.
- Cleanup: When the application quits,
main.py'sapplication_cleanupfunction callsself.thread.quit()andself.thread.wait()to ensure the worker thread terminates cleanly. Thethread.finishedsignal is connected toworker.deleteLaterandthread.deleteLaterfor Qt's memory management.
Defined in grazr/core/worker.py.
class Worker(QObject):
resultReady = Signal(str, dict, bool, str) # task_name, context_data, success, messageresultReady: This signal is emitted by the worker when a task is completed (either successfully or with an error).task_name (str): The name of the task that finished.context_data (dict): The original data dictionary that was passed todoWorkfor this task, potentially with additional context added by the worker (e.g.,instance_idfor PostgreSQL tasks). This helpsMainWindow.handleWorkerResultidentify what the result pertains to.success (bool):Trueif the task was successful,Falseotherwise.message (str): A human-readable message describing the outcome or any error.
@Slot(str, dict)
def doWork(self, task_name: str, data: dict):
# ... implementation ...This is the main entry point for all background tasks. When MainWindow emits triggerWorker, this slot is executed in the worker's thread.
When a user action in the UI requires a background operation (e.g., clicking "Start" on a service):
- The UI page (e.g.,
ServicesPage) emits a signal. - A slot in
MainWindow(e.g.,on_service_action_triggered) receives this signal. MainWindowprepares atask_name(string identifying the operation) and adatadictionary (containing necessary parameters like service ID, version, path, etc.).MainWindowemitsself.triggerWorker.emit(task_name, data).
Task names are strings, typically like "start_internal_nginx", "install_nginx", "start_php_fpm", "enable_ssl", "start_postgres". These directly correspond to if/elif blocks within the doWork method.
The data dictionary carries all necessary information for the worker to perform the task. Examples:
- For starting PHP-FPM:
{"version": "8.3"} - For installing an Nginx site:
{"path": "/path/to/site/docroot"} - For starting a PostgreSQL instance:
{"instance_id": "unique_uuid_for_pg_instance"} - For enabling SSL:
{"site_info": {"domain": "mysite.test", "path": "..."}}
At the beginning of doWork, context_data = data.copy() is created so that the original input data can be passed back with the resultReady signal for UI context.
At the start of doWork:
local_success: bool = False
local_message: str = f"Unknown task '{task_name}'." # Default message
action: str = "" # Used by some task blocksThe entire task-handling logic is wrapped in a try...except Exception as e:...finally:... block:
try: Contains theif/elifchain to dispatch to the correct task logic.except Exception as e: Catches any unexpected Python exceptions during task execution. It logs the error with a full traceback (exc_info=True) and setslocal_success = Falseandlocal_messageto an error string.finally: This block always executes, whether the task succeeded, failed with a known error (handled within anif/elifblock), or failed with an unexpected exception. Its primary role is to emit theresultReadysignal:finally: # Add instance_id to context_data for PostgreSQL tasks for UI refresh if task_name in ["start_postgres", "stop_postgres"] and "instance_id" in data: context_data["instance_id"] = data["instance_id"] logger.info(f"WORKER: Emitting resultReady signal for task '{task_name}' (Success={local_success}) with context {context_data}") self.resultReady.emit(task_name, context_data, local_success, local_message)
The doWork method uses a long if/elif/else chain based on task_name to execute the appropriate logic.
Example for install_nginx:
elif task_name == "install_nginx":
results_log = []
overall_success = False
path = data.get("path")
if path:
# ... call install_nginx_site from nginx_manager.py ...
# ... call run_root_helper_action to update /etc/hosts ...
# ... append to results_log ...
# ... set overall_success ...
local_success = overall_success
local_message = f"Install Site: {' | '.join(results_log)}"
else:
local_success = False
local_message = "Missing 'path' for install_nginx..."
# ... other tasks ...Each task block typically:
- Extracts necessary parameters from the
datadictionary. - Calls functions from the relevant service manager modules (e.g.,
nginx_manager.install_nginx_site(),php_manager.start_php_fpm(),postgres_manager.start_postgres(service_instance_config)). - Service manager functions return a success status (boolean) and often a message string.
For tasks involving multiple steps (like installing an Nginx site which also involves updating /etc/hosts), a local results_log list and an overall_success boolean are often used to track the outcome of each sub-step and determine the final local_success and local_message.
- Specific error conditions within a task block (e.g., missing parameters in
data) setlocal_success = Falseand a specificlocal_message. - The main
try...except Exceptionblock catches any unhandled Python exceptions from the manager calls or worker logic.
As seen in the finally block, after a task is processed, self.resultReady.emit(...) is called.
This is a copy of the original data dictionary passed to doWork. For tasks that operate on specific instances (like PostgreSQL), the instance_id is explicitly ensured to be in context_data before emitting. This allows MainWindow.handleWorkerResult to know which UI element or data item the result pertains to.
These reflect the outcome of the task and are passed to MainWindow for logging and potentially displaying to the user (e.g., in a status bar or notification).
In grazr/ui/main_window.py, the handleWorkerResult slot is connected to the worker's resultReady signal. This slot:
- Logs the task completion and its result.
- Determines which UI page (
target_page) and specific UI element might need updating based on thetask_nameandcontext_data. - Triggers UI refresh methods (e.g.,
self.sites_page.refresh_data(),self._refresh_specific_service_on_page(service_id_for_ui_refresh)), often with a shortQTimer.singleShotdelay to allow the event loop to process. - Re-enables controls on the relevant page that might have been disabled while the task was running.
- Define a new unique
task_namestring. - Ensure
MainWindow(or another UI component) can emittriggerWorkerwith thistask_nameand the requireddatadictionary. - Add a new
elif task_name == "your_new_task_name":block indoWork:- Import any new manager functions needed at the top of
worker.py. - Extract parameters from
data. - Call the appropriate manager function(s).
- Set
local_successandlocal_messagebased on the outcome. - If the task operates on an item that needs specific identification for UI updates (like a PostgreSQL
instance_id), ensure this identifier is present in or added tocontext_datain thefinallyblock.
- Import any new manager functions needed at the top of
- Update
MainWindow.handleWorkerResult:- Add logic to recognize the new
task_name. - Determine the
display_namefor logging. - Determine
service_id_for_ui_refreshif a specific service item needs updating onServicesPage. - Call appropriate UI refresh methods.
- Add logic to recognize the new
- Task Not Starting:
- Verify
triggerWorker.emit(task_name, data)is being called inMainWindowwith the correcttask_name. - Check for typos in
task_namein theif/elifchain indoWork.
- Verify
UnboundLocalErrorforlocal_successorlocal_message: Ensure these are assigned in all possible execution paths within every task block, or that a task block doesn't fall through without setting them. The initial defaults should prevent this for unknown tasks, but each known task block is responsible for its outcome.- Task Seems to Freeze UI (if it wasn't supposed to): The operation being performed by the manager function might still be blocking in an unexpected way, or the worker thread itself might be having issues (though less common).
- Incorrect Results/Messages: Debug the logic within the specific task block in
doWorkand the manager function(s) it calls. Check the logs produced by the worker and the managers.
- Maintain the clear separation of concerns:
worker.pydispatches tasks and reports results; actual business logic resides in the service managers. - Ensure robust error handling for each task.
- Provide clear and informative
local_messagestrings for both success and failure cases. - When adding tasks for instance-based services (like PostgreSQL), ensure the
instance_idor equivalent context is correctly handled and passed back incontext_data.