Swift - How to wait for something without making the app hanging - swift

I have a code like this:
print("Migration Execution: Successfully uninstalled MCAfee")
migrationInfoPicture.image = NSImage(named: "Unroll")
migrationInfoText.stringValue = NSLocalizedString("Unrolling from old server... Please wait!", comment: "Unrolling")
while(!readFile(path:logfilePath)!.contains("result: 2 OK")) {
searchLogForError(scriptPath: scriptOnePath)
}
print("Migration Execution: Successfully unrolled from old server")
migrationInfoText.stringValue = NSLocalizedString("Setting up MDM profile... Please wait!", comment: "Setting up MDM")
while(!readFile(path:logfilePath)!.contains("result: 3 OK")) {
searchLogForError(scriptPath: scriptOnePath)
}
It actually works in the background, reading from the file works and logging works but since the GUI will be hanging executing a while loop with a quickly completed task, the image and the text changes will not be visible.
Code for searchForLogError is:
func searchLogForError(scriptPath:String) {
if((readFile(path:logfilePath)!.filter { $0.contains("ERROR") }).contains("ERROR")) {
print("Migration abborted")
migrationInfoPicture.image = NSImage(named: "FatalError")
migrationInfoText.stringValue = NSLocalizedString("An error occured: \n", comment: "Error occurence") + readFile(path:logfilePath)!.filter { $0.contains("ERROR") }[0]
migrationWarningText.stringValue = NSLocalizedString("In order to get further help, please contact: mac.workplace#swisscom.com", comment: "Error support information")
self.view.window?.level = .normal
btnExitApplicationOutlet.isHidden = false
getScriptProcess(path:scriptPath).terminate()
return
}
}
How can I achieve a visible change of NSImage and NSLocalizedString while constantly looking for log file change without a hanging GUI (or even with a hanging GUI, but with enough time to change the visible elements between the while-loops)?

Polling file system resources is a horrible practice. Don't do that. There are dedicated APIs to observe file system resources for example DispatchSourceFileSystemObject
Create a property
var fileSystemObject : DispatchSourceFileSystemObject?
and two methods to start and stop the observer. In the closure of setEventHandler insert the code to read the file
func startObserver(at url: URL)
{
if fileSystemObject != nil { return }
let fileDescriptor : CInt = open(url.path, O_EVTONLY);
if fileDescriptor < 0 {
print("Could not open file descriptor"))
return
}
fileSystemObject = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: [.write, .rename], queue: .global())
if fileSystemObject == nil {
close(fileDescriptor)
print"Could not create Dispatch Source"))
return
}
fileSystemObject!.setEventHandler {
if self.fileSystemObject!.mask.contains(.write) {
// the file has been modified, do something
}
}
fileSystemObject!.setCancelHandler {
close(fileDescriptor)
}
fileSystemObject!.resume()
}
func stopObserver()
{
fileSystemObject?.cancel()
fileSystemObject = nil
}

Related

stopContinuousRecognition() blocks the app for 5-7 seconds

I am trying to implement speech recognition using the Azure Speech SDK in iOS project using Swift and I ran into the problem that the speech recognition completion function (stopContinuousRecognition()) blocks the app UI for a few seconds, but there is no memory or processor load or leak. I tried to move this function to DispatchQueue.main.async {}, but it gave no results. Maybe someone faced such a problem? Is it necessary to put this in a separate thread and why does the function take so long to finish?
Edit:
It is very hard to provide working example, but basically I am calling this function on button press:
private func startListenAzureRecognition(lang:String) {
let audioFormat = SPXAudioStreamFormat.init(usingPCMWithSampleRate: 8000, bitsPerSample: 16, channels: 1)
azurePushAudioStream = SPXPushAudioInputStream(audioFormat: audioFormat!)
let audioConfig = SPXAudioConfiguration(streamInput: azurePushAudioStream!)!
var speechConfig: SPXSpeechConfiguration?
do {
let sub = "enter your code here"
let region = "enter you region here"
try speechConfig = SPXSpeechConfiguration(subscription: sub, region: region)
speechConfig!.enableDictation();
speechConfig?.speechRecognitionLanguage = lang
} catch {
print("error \(error) happened")
speechConfig = nil
}
self.azureRecognition = try! SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig)
self.azureRecognition!.addRecognizingEventHandler() {reco, evt in
if (evt.result.text != nil && evt.result.text != "") {
print(evt.result.text ?? "no result")
}
}
self.azureRecognition!.addRecognizedEventHandler() {reco, evt in
if (evt.result.text != nil && evt.result.text != "") {
print(evt.result.text ?? "no result")
}
}
do {
try! self.azureRecognition?.startContinuousRecognition()
} catch {
print("error \(error) happened")
}
}
And when I press the button again to stop recognition, I am calling this function:
private func stopListenAzureRecognition(){
DispatchQueue.main.async {
print("start")
// app blocks here
try! self.azureRecognition?.stopContinuousRecognition()
self.azurePushAudioStream!.close()
self.azureRecognition = nil
self.azurePushAudioStream = nil
print("stop")
}
}
Also I am using raw audio data from mic (recognizeOnce works perfectly for first phrase, so everything is fine with audio data)
Try closing the stream first and then stopping the continuous recognition:
azurePushAudioStream!.close()
try! azureRecognition?.stopContinuousRecognition()
azureRecognition = nil
azurePushAudioStream = nil
You don't even need to do it asynchronously.
At least this worked for me.

Return from async NSOpenPanel

How can this function be re-written to return the variable "files"? I'm completely unfamiliar with async and completion handlers, so I wasn't able to use existing answers as a starting point. It's a simple function that returns the files from a user-selected directory.
func readFolder() {
let dialog = NSOpenPanel()
dialog.prompt = "Choose"
dialog.allowsMultipleSelection = false
dialog.canChooseDirectories = true
dialog.canCreateDirectories = true
dialog.canChooseFiles = false
dialog.showsResizeIndicator = true
dialog.showsHiddenFiles = false
dialog.begin {
(result) -> Void in if result == .OK {
let directory = dialog.url!
do {
var files = try FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: nil
)
} catch {
NSLog(error.localizedDescription)
}
}
}
}
You're on the right track with your question when you mentioned "completion handlers." With async functions, you aren't going to actually return the value directly, but rather through a function that you provide as a completion handler. This takes some getting-used-to to restructure the way you think of some of your code, but is a great concept to become familiar with.
Code first, then explanation:
func doSomethingThatRequiresFiles() {
readFolder { result in
switch result {
case .success(let files):
print(files)
case .failure(let error):
print(error)
}
}
}
func readFolder(completion: #escaping (Result<[URL],Error>) -> Void) {
let dialog = NSOpenPanel()
dialog.prompt = "Choose"
dialog.allowsMultipleSelection = false
dialog.canChooseDirectories = true
dialog.canCreateDirectories = true
dialog.canChooseFiles = false
dialog.showsResizeIndicator = true
dialog.showsHiddenFiles = false
dialog.begin { (result) -> Void in
if result == .OK {
guard let directory = dialog.url else {
assertionFailure("Not a directory")
return
}
do {
let files = try FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: nil
)
completion(.success(files))
} catch {
NSLog(error.localizedDescription)
completion(.failure(error))
}
} else {
//handle cancelled case
}
}
}
The first function (doSomethingThatRequiresFiles ) is an example of a place in your code where you want to deal with files. You'll see there's a switch statement that lets you handle either success or failure. You can see that the line print(files) is where you would put your code that needs to deal with the files somehow.
In the readFolder function, there's now a parameter (completion) gets a Result type -- it can either be an array of URLs or an Error. Read more about Result: https://www.hackingwithswift.com/articles/161/how-to-use-result-in-swift
And detail about why #escaping is used: https://www.donnywals.com/what-is-escaping-in-swift/
Inside the dialog.begin, you can see that completion gets called with either the list of files or the error.
Using async functions will be a familiar pattern when working with the filesystem (in particular if you have to deal with the iCloud APIs) and certainly with networking, where basically everything is asynchronous. It's also a good pattern to be familiar with in situations (like this one) where you're waiting for a UI interaction.

Not connecting to RPC server in release mode, but works fine in debug mode

I have a command line app that does the following:
downloads an RSS feed with torrent links
stores it in a sqlite database and tags them as "added" or "ignored"
connects to a transmission server (in my local network)
loads items from sqlite marked as "added" and adds to transmission server
The above works fine in debug mode. However, when I build for release and try to run directly or from launchd, it always times out. The most relevant code is in main.swift which goes below.
private func getTransmissionClient() -> Transmission? {
let client = Transmission(
baseURL: serverConfig.server,
username: serverConfig.username,
password: serverConfig.password)
var cancellables = Set<AnyCancellable>()
let group = DispatchGroup()
group.enter()
print("[INFO] Connecting to client")
client.request(.rpcVersion)
.sink(
receiveCompletion: { _ in group.leave() },
receiveValue: { rpcVersion in
print("[INFO]: Successfully Connected! RPC Version: \(rpcVersion)")
})
.store(in: &cancellables)
let wallTimeout = DispatchWallTime.now() +
DispatchTimeInterval.seconds(serverConfig.secondsTimeout ?? 15)
let res = group.wait(wallTimeout: wallTimeout)
if res == DispatchTimeoutResult.success {
return client
} else {
return nil
}
}
public func updateTransmission() throws {
print("[INFO] [\(Date())] Starting Transmission Update")
let clientOpt = getTransmissionClient()
guard let client = clientOpt else {
print("[ERROR] Failed to connect to transmission client")
exit(1)
}
var cancellables = Set<AnyCancellable>()
let items = try store.getPendingDownload()
print("[INFO] [\(Date())] Adding \(items.count) new items to transmission")
let group = DispatchGroup()
for item in items {
let linkComponents = "\(item.link)".components(separatedBy: "&")
assert(linkComponents.count > 0, "Link seems wrong")
group.enter()
client.request(.add(url: item.link))
.sink(receiveCompletion: { completion in
if case let .failure(error) = completion {
print("[Failure] \(item.title)")
print("[Failure] Details: \(error)")
}
group.leave()
}, receiveValue: { _ in
print("[Success] \(item.title)")
do {
try self.store.update(item: item, with: .downloaded)
} catch {
print("[Error] Couldn't save new status to DB")
}
})
.store(in: &cancellables)
}
let wallTimeout = DispatchWallTime.now() +
DispatchTimeInterval.seconds(serverConfig.secondsTimeout ?? 15)
let res = group.wait(wallTimeout: wallTimeout)
if res == DispatchTimeoutResult.success {
print("Tasks successfully submitted")
} else {
print("Timed out")
exit(1)
}
}
Oddly enough, the code seemed to work fine before I added the database. The DispatchGroup was already there, as well as the Transmission-Swift client. I guess something that I did is being "optimized away" by the compiler? This is just speculation though after seeing some other questions on StackOverflow, but I am still not clear on it.
I am using macOS 10.15 and Swift 5.2.2.
Full code available in github (link to specific commit that has the bug)
I asked for help on Swift Forums at https://forums.swift.org/t/not-connecting-to-rpc-server-in-release-mode-but-works-fine-in-debug-mode/36251 and here is the gist of it:
Debug vs Release bugs are common in the Apple ecosystem.
One common reason for the above: the compiler has much more aggressive retain and release patterns in release mode.
My problem was exactly that: a certain class was being disposed of earlier than it should and it was exactly the cancellable for the subscription, so my server requests were being cancelled in the middle.
This commit fixes it and it basically does the following:
diff --git a/Sources/TorrentRSS/TorrentRSS.swift b/Sources/TorrentRSS/TorrentRSS.swift
index 17e1a6b..0b80cd5 100644
--- a/Sources/TorrentRSS/TorrentRSS.swift
+++ b/Sources/TorrentRSS/TorrentRSS.swift
## -63,6 +63,10 ## public struct TorrentRSS {
DispatchTimeInterval.seconds(serverConfig.secondsTimeout ?? 15)
let res = group.wait(wallTimeout: wallTimeout)
+ for cancellable in cancellables {
+ cancellable.cancel()
+ }
+
if res == DispatchTimeoutResult.success {
return client
} else {
## -117,6 +121,11 ## public struct TorrentRSS {
let wallTimeout = DispatchWallTime.now() +
DispatchTimeInterval.seconds(serverConfig.secondsTimeout ?? 15)
let res = group.wait(wallTimeout: wallTimeout)
+
+ for cancellable in cancellables {
+ cancellable.cancel()
+ }
+
if res == DispatchTimeoutResult.success {
print("Tasks successfully submitted")
} else {
Calling cancellable explicitly avoids the object being disposed of before it should. That specific location is where I meant to dispose of the object, not any sooner.

open(FileManager.default.fileSystemRepresentation(withPath: path), O_EVTONLY) returns -1

I am using SKQueue to monitor some folders in the mac filesystem. As per the documentation, I have added the directory paths to the queues but I noticed that while adding the path, the following line of code in SKQueue is returning -1 and hence it is unable to monitor my folder.
This is the SKQueue Documentation.
The following is code from the documentation, written in the controller class.
import SKQueue
class SomeClass: SKQueueDelegate {
func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) {
print("\(notification.toStrings().map { $0.rawValue }) # \(path)")
}
}
let delegate = SomeClass()
let queue = SKQueue(delegate: delegate)!
queue.addPath("/Users/steve/Documents")
queue.addPath("/Users/steve/Documents/dog.jpg")
The following is code inside SKQueue dependency.
public func addPath(_ path: String, notifyingAbout notification: SKQueueNotification = SKQueueNotification.Default) {
var fileDescriptor: Int32! = watchedPaths[path]
if fileDescriptor == nil {
fileDescriptor = open(FileManager.default.fileSystemRepresentation(withPath: path), O_EVTONLY)
guard fileDescriptor >= 0 else { return }
watchedPaths[path] = fileDescriptor
}
fileDescriptor =
open(FileManager.default.fileSystemRepresentation(withPath: path),
O_EVTONLY)
The above code is returning -1 and hence it is failing.
I was getting the same -1 return code and couldn't understand why. Whilst looking for a solution I stumbled upon SwiftFolderMonitor at https://github.com/MartinJNash/SwiftFolderMonitor. This class worked so I knew it wasn't a permission problem.
SwiftFolderMonitor uses DispatchSource.makeFileSystemObjectSource rather than kevent, but it also takes a URL parameter rather than a String path. I amended SKQueue to take a URL instead of a String and it works.
Here's my amended addPath:
public func addPath(url: URL, notifyingAbout notification: SKQueueNotification = SKQueueNotification.Default) {
let path = url.absoluteString
var fileDescriptor: Int32! = watchedPaths[path]
if fileDescriptor == nil {
fileDescriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
guard fileDescriptor >= 0 else { return }
watchedPaths[path] = fileDescriptor
}
var edit = kevent(
ident: UInt(fileDescriptor),
filter: Int16(EVFILT_VNODE),
flags: UInt16(EV_ADD | EV_CLEAR),
fflags: notification.rawValue,
data: 0,
udata: nil
)
kevent(kqueueId, &edit, 1, nil, 0, nil)
if !keepWatcherThreadRunning {
keepWatcherThreadRunning = true
DispatchQueue.global().async(execute: watcherThread)
}
}
I don't know why this works, perhaps someone else can shed some light on this.
I'm still playing with both solutions but it looks like SwiftFolderMonitor does all I need (I just need to know when a specific file has changed) and it's code is clean and minimal so I think I'll use it over SKQueue.
I hope this helps.
The call to open() failed, likely due to insufficient permissions. Since macOS 10.15, apps can't access certain files and folders without permission (the user's home directory, for example). Read more here.

Is it iCloud or is it my code?

I am using a slightly updated code of this question: Method for downloading iCloud files? Very confusing?
Here is an excerpt of the code:
private func downloadUbiquitiousItem(atURL url: URL) -> Void {
do {
try FileManager.default.startDownloadingUbiquitousItem(at: url)
do {
let attributes = try url.resourceValues(forKeys: [URLResourceKey.ubiquitousItemDownloadingStatusKey])
if let status: URLUbiquitousItemDownloadingStatus = attributes.allValues[URLResourceKey.ubiquitousItemDownloadingStatusKey] as? URLUbiquitousItemDownloadingStatus {
if status == URLUbiquitousItemDownloadingStatus.current {
self.processDocument(withURL: url)
return
} else if status == URLUbiquitousItemDownloadingStatus.downloaded {
self.processDocument(withURL: url)
return
} else if status == URLUbiquitousItemDownloadingStatus.notDownloaded {
do {
//will go just fine, if it is unnecessary to download again
try FileManager.default.startDownloadingUbiquitousItem(at: url)
} catch {
return
}
}
}
} catch {
}
//only happens if the try does not fail
self.documentsQuery = NSMetadataQuery()
self.documentsQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
self.documentsQuery.valueListAttributes = [NSMetadataUbiquitousItemPercentDownloadedKey,NSMetadataUbiquitousItemDownloadingStatusKey]
self.documentsQuery.predicate = NSPredicate(format: "%K like 'backup.json'", argumentArray: [NSMetadataItemFSNameKey])
NotificationCenter.default.addObserver(self, selector: #selector(self.queryUpdate(notification:)), name: NSNotification.Name.NSMetadataQueryDidUpdate, object: self.documentsQuery)
DispatchQueue.main.async {
if self.documentsQuery.start() {
if self.restoreHUD != nil {
self.restoreHUD.show(animated: true)
//timeout for restoring
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(30), execute: {
if self.restoreHUD != nil {
self.restoreHUD.hide(animated: true)
}
})
}
}
}
} catch {
//file does not exist in icloud most likely
}
}
So this works sometimes, but it is really unstable, for example we tested the following cases:
Backup to iCloud
Check that we have a valid document in Settings -> iCloud -> Storage -> Manage Storage -> MyApp -> backup.json
Force a first launch, so that the app restores backup.json (aka executes the code above)
This sometimes works and sometimes doesn't. Sometimes the query won't update.
We also tested the following scenario:
Remove backup from iCloud manually via settings
Uninstall the app and reinstall it to provide a first launch
the startDownloadingUbiquitousItem function does not seem to throw, even though nothing is in iCloud because I think that iCloud still hasn't synced the local file or deleted the local data, but it also does not download properly... yet the status is notDownloaded.
Maybe users are not supposed to wipe the stuff via Settings? I'd like to know if my code is missing a case that could happen, or if this API is just really unhandy for developers...
Thanks!
Probably, we call to adding notification in "main thread".
NotificationCenter.default.addObserver(self, selector: #selector(self.queryUpdate(notification:)), name: NSNotification.Name.NSMetadataQueryDidUpdate, object: self.documentsQuery)