Threaded Task Creation
This example demonstrates how to create a threaded task runs in its own thread. It demonstrates how to handle 'Interrupted Pauses' (Safety Stops) and Aborts correctly without crashing.
Key Concepts
Managing background threads in a desktop automation environment requires careful handling to ensure that a "Pause" or "Stop" command actually halts execution immediately without leaving orphan processes or corrupted resources.
1. The Thread Controller vs. Basic Controller
While standard tasks use yield to give control back to the engine, threaded tasks run on a separate OS thread. Because of this, they use a specialized ThreadController which provides blocking methods like controller.sleep() and controller.waitForResume() to keep the thread synchronized with the main engine state.
2. Handling "Hard" Pauses (Interrupted Pauses)
When a user triggers a pause, the engine can perform an "Interrupted Pause" (Hard Pause).
- The Mechanism: The engine uses
.throw() to inject a TaskInterruptedException directly into the thread.
- The Benefit: This immediately breaks the thread out of a long
controller.sleep() call.
- Safety: By wrapping your logic in a
try/except/finally block, you can catch this interruption to safely release resources (like file handles or network sockets) before the thread is fully suspended.
3. Graceful Aborts
If a macro is stopped entirely, the engine throws a TaskAbortException.
- Immediate Exit: Unlike a pause, an abort signifies the end of the task's lifecycle.
- Best Practice: You should always catch this at the top level of your
ThreadFunc and return immediately to ensure the thread terminates and doesn't "hang" in the background.
4. Deterministic Resumption
After a hard pause, a thread may be in an inconsistent state. The controller.waitForResume() method acts as a synchronization barrier. It ensures that the thread remains stationary until the user explicitly hits "Play" again, preventing the macro from "jumping ahead" while the engine is still paused.
5. Managing Real-Time Drift
Because threaded tasks can be interrupted, calculating durations based on wall-clock time (time.time()) can be misleading. In the provided example, we calculate the target_duration to demonstrate how the total elapsed real time might exceed the actual "active" time if the task was paused mid-sleep.
The Implementation
| examples/threaded_macro.py |
|---|
| def taskInThread(controller: ThreadController):
"""
A simple example of a task running in its own thread.
It demonstrates how to handle 'Interrupted Pauses' (Safety Stops) and Aborts correctly without crashing.
"""
try:
# We want to sleep for exactly 5 seconds
# We calculate the target end time immediately
target_duration = 5.0
end_time = time.time() + target_duration
controller.log(f"[Thread] Attempting to sleep for {target_duration} seconds...")
try:
# Access resources here if needed
controller.log(f"[Thread] Accessing resources...")
# Sleep the thread for the target duration
controller.sleep(target_duration)
except TaskInterruptedException:
controller.log("[Thread] INTERRUPTION CAUGHT!")
finally:
# Always clean up resources here if needed
controller.log("[Thread] Cleaning up resources...")
# We could be paused after breaking out of the previous block, so wait until we're resumed
controller.waitForResume()
controller.log(
f"[Thread] Sleep Complete! Total elapsed real time: {time.time() - (end_time - target_duration):.2f}s")
except TaskAbortException:
# This handles any aborts from controller.sleep and controller.waitForResume
# We generally don't want to use this, but if you do make sure to return immediately after to not have hanging threads
controller.log("[Thread] STOPPED! Exiting task immediately.")
class ThreadMacro:
def __init__(self, studio: MacroStudio):
# Add run tasks to the studio
self.thread_task_controller = studio.addThreadTask(taskInThread)
self.pauser_controller = studio.addBasicTask(self.threadHardPauser)
def threadHardPauser(self, controller: Controller):
# Let's attempt to interrupt the threaded task!
yield from taskSleep(1)
# After a second of running, interrupt the threaded task
controller.log("Interrupting the thread controller!")
self.thread_task_controller.pause(True)
yield from taskSleep(2)
# After two seconds, unpause the threaded task so it can finish
# Since we were hard paused, the remaining time from the thread's sleep was discarded and the task ends early
self.thread_task_controller.resume()
|
View the complete script
| examples/threaded_macro.py |
|---|
| import time
from macro_studio import MacroStudio, Controller, ThreadController, TaskAbortException, TaskInterruptedException, taskSleep
def taskInThread(controller: ThreadController):
"""
A simple example of a task running in its own thread.
It demonstrates how to handle 'Interrupted Pauses' (Safety Stops) and Aborts correctly without crashing.
"""
try:
# We want to sleep for exactly 5 seconds
# We calculate the target end time immediately
target_duration = 5.0
end_time = time.time() + target_duration
controller.log(f"[Thread] Attempting to sleep for {target_duration} seconds...")
try:
# Access resources here if needed
controller.log(f"[Thread] Accessing resources...")
# Sleep the thread for the target duration
controller.sleep(target_duration)
except TaskInterruptedException:
controller.log("[Thread] INTERRUPTION CAUGHT!")
finally:
# Always clean up resources here if needed
controller.log("[Thread] Cleaning up resources...")
# We could be paused after breaking out of the previous block, so wait until we're resumed
controller.waitForResume()
controller.log(
f"[Thread] Sleep Complete! Total elapsed real time: {time.time() - (end_time - target_duration):.2f}s")
except TaskAbortException:
# This handles any aborts from controller.sleep and controller.waitForResume
# We generally don't want to use this, but if you do make sure to return immediately after to not have hanging threads
controller.log("[Thread] STOPPED! Exiting task immediately.")
class ThreadMacro:
def __init__(self, studio: MacroStudio):
# Add run tasks to the studio
self.thread_task_controller = studio.addThreadTask(taskInThread)
self.pauser_controller = studio.addBasicTask(self.threadHardPauser)
def threadHardPauser(self, controller: Controller):
# Let's attempt to interrupt the threaded task!
yield from taskSleep(1)
# After a second of running, interrupt the threaded task
controller.log("Interrupting the thread controller!")
self.thread_task_controller.pause(True)
yield from taskSleep(2)
# After two seconds, unpause the threaded task so it can finish
# Since we were hard paused, the remaining time from the thread's sleep was discarded and the task ends early
self.thread_task_controller.resume()
if __name__ == "__main__":
studio = MacroStudio("Thread Macro")
ThreadMacro(studio)
studio.launch()
|