Update ActivityIndicator for async/await, migrate to Swift Testing#210
Update ActivityIndicator for async/await, migrate to Swift Testing#210fpseverino merged 16 commits intovapor:consolekit-5from fpseverino:swift-testing
ActivityIndicator for async/await, migrate to Swift Testing#210Conversation
| import Musl | ||
| #endif | ||
| import Dispatch | ||
| //import Dispatch |
There was a problem hiding this comment.
I commented it because I don't know if there is now a solution for this
There was a problem hiding this comment.
Maybe marking linux_readpassphrase with @MainActor? Since there is no more Dispatch code
There was a problem hiding this comment.
As mentioned in the PR where I removed the original check, invoking this function off the main thread/actor is a violation of API contract. We only need to be as stringent as the original BSD implementation, which makes no such check, nor forces a change of execution context, so having no check or annotation at all just matches the behavior of the "real" function this implementation emulates. In short, it's the same as calling any other libc API which interacts with the controlling TTY. Honestly, I'd like to find an alternative to having this implementation in the first place; I kinda hacked it together rather than try to figure out how to get easy access to libbsd on Linux; quite possibly Glibc even has some API or another with similar functionality but a different name, which would be just as good. We don't need the precise behavioral quirks of the BSD readpassphrase(), we just need something which performs the same function.
| try await withThrowingTaskGroup { group in | ||
| group.addTask { | ||
| await self.start(refreshRate: refreshRate) | ||
| } | ||
|
|
||
| do { | ||
| let result = try await body() | ||
| group.cancelAll() | ||
| try await group.waitForAll() | ||
| if result { | ||
| self.succeed() | ||
| } else { | ||
| self.fail() | ||
| } | ||
| } catch { | ||
| group.cancelAll() | ||
| try? await group.waitForAll() | ||
| self.fail() | ||
| throw error | ||
| } | ||
| } |
There was a problem hiding this comment.
I think a single Task is preferable here
| try await withThrowingTaskGroup { group in | |
| group.addTask { | |
| await self.start(refreshRate: refreshRate) | |
| } | |
| do { | |
| let result = try await body() | |
| group.cancelAll() | |
| try await group.waitForAll() | |
| if result { | |
| self.succeed() | |
| } else { | |
| self.fail() | |
| } | |
| } catch { | |
| group.cancelAll() | |
| try? await group.waitForAll() | |
| self.fail() | |
| throw error | |
| } | |
| } | |
| let task = Task { | |
| await self.start(refreshRate: refreshRate) | |
| } | |
| defer { | |
| task.cancel() | |
| } | |
| let result: Bool | |
| do { | |
| result = try await body() | |
| await task.value | |
| result ? self.succeed() : self.fail() | |
| } catch { | |
| await task.value | |
| self.fail() | |
| throw error | |
| } |
There was a problem hiding this comment.
I'm new to such low level concurrency, so I wanted to avoid unstructured concurrency, but if you say this is fine it's ok with me
There was a problem hiding this comment.
I think you could call this structured in the sense that we know that the lifetime of the task is bounded by the method because we do wait for it to complete and also cancel it when exiting the function. But if you don't want to use Task there's also async let, something like
async let task = self.start(refreshRate: refreshRate)
let result: Bool
do {
result = try await body()
await task
result ? self.succeed() : self.fail()
} catch {
await task
self.fail()
throw error
}I just think it's somewhat weird to create a task group for one task
There was a problem hiding this comment.
Actually that might not work because start never returns
There was a problem hiding this comment.
Actually that might not work because
startnever returns
Yeah it doesn't, neither with Task nor async let, but if I cancel the Task instead of awaiting for its value it seems to work. Would you consider this correct?
let task = Task {
await self.start(refreshRate: refreshRate)
}
do {
let result = try await body()
task.cancel()
result ? self.succeed() : self.fail()
} catch {
task.cancel()
self.fail()
throw error
}There was a problem hiding this comment.
Yep that seems correct. I believe in your code waitForAll() is not actually doing anything because you're canceling beforehand anyway
There was a problem hiding this comment.
Actually it probably works like this too
let task = Task {
await self.start(refreshRate: refreshRate)
}
defer { task.cancel() }
do {
let result = try await body()
result ? self.succeed() : self.fail()
} catch {
self.fail()
throw error
}(so just removing the wait from my original example)
There was a problem hiding this comment.
No, I had already tried it with the TaskGroup. It doesn't work because the Task has to finish before succeed() or fail() are called, because of the way the functions are written
ActivityIndicatorfor async/awaitDispatchSourceTimerfrom GCD withAsyncTimerSequencefromswift-async-algorithmsstart()andsucceed()/fail()to control theActivityIndicator, use instead awith-style function that stops the indicator once the closure ends.swift-format