I am trying to create a Robotic Process Automation tool for Macos using Swift. Users create an Automation that is an array of Step objects and then play it. One of the subclasses of Step is Pause which is supposed to pause the execution for a given number of seconds.
For some reason, execution hangs when I use the DispatchQueue.main.asyncAfter()
method in the Pause class. Usually the first run through the automation is fine, but when it goes to repeat, it eventually hangs for much longer. The error goes away when I use sleep()
instead.
The other weird thing about this bug is when I open Xcode to try and see what is happening, the hang resolves and execution continues. I am wondering if the process enters background somehow and then the DispatchQueue.main.asyncAfter()
doesn't work. I have tried to change the Info.plist "Application does not run in background" to YES, but this doesn't have any effect.
The problem with using sleep()
is it blocks the UI thread so users can't stop the automation if they need to. I have tried lots of different variations of threading with DispatchQueue, but it always seems to hang somewhere on repeat execution. I have also tried using a Timer.scheduledTimer()
instead of DispatchQueue but that hangs as well. I'm sure I'm missing something simple, but I can't figure it out.
Creating the Step Array and Starting Automation
class AutomationPlayer {
static let shared = AutomationPlayer()
var automation: Automation?
var stepArray: [Step] = []
func play() {
// Create array of steps
guard let steps = automation?.steps, let array = Array(steps) as? [Step] else {
return
}
// If the automation repeats, add more steps to array.
for _ in 0..<(automation?.numberOfRepeats ?? 1) {
for (index, step) in array.enumerated() {
stepArray.append(step)
}
}
// Add small delay to allow window to close before execution.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
self?.execute(index: 0)
}
}
private func execute(index: Int) {
let step = stepArray[index]
executeStep(step: step) { [weak self] success, error in
guard error == nil else { return }
let newIndex = index + 1
if newIndex < self?.stepArray.count ?? 0 {
//Need a small delay between steps otherwise execution is getting messed up.
usleep(400000)
self?.execute(index: newIndex)
} else {
self?.stepArray = []
}
}
}
private func executeStep(step: Step?, completionHandler: @escaping (Bool, Error?) -> Void) -> Void {
step?.execute(completionHandler: { [weak self] success, error in
guard error == nil else {
completionHandler(false, error)
return
}
completionHandler(true, nil)
})
}
Pause Class
@objc(Pause)
public class Pause: Step {
override func execute(completionHandler: @escaping (Bool, Error?) -> Void) {
print("Pause for: \(self.time) seconds")
// This will eventually hang when the automation repeats itself
DispatchQueue.main.asyncAfter(deadline: .now() + Double(self.time)) {
completionHandler(true, nil)
})
// This will also hang
Timer.scheduledTimer(withTimeInterval: self.time, repeats: false) { timer in
completionHandler(true, nil)
}
// If I use this instead, the automation repeats just fine
sleep(UInt32(self.time))
completionHandler(true, nil)
}
}