Skip to content

Update ActivityIndicator for async/await, migrate to Swift Testing#210

Merged
fpseverino merged 16 commits intovapor:consolekit-5from
fpseverino:swift-testing
May 30, 2025
Merged

Update ActivityIndicator for async/await, migrate to Swift Testing#210
fpseverino merged 16 commits intovapor:consolekit-5from
fpseverino:swift-testing

Conversation

@fpseverino
Copy link
Copy Markdown
Member

@fpseverino fpseverino commented May 28, 2025

  • Update ActivityIndicator for async/await
    • Substitute DispatchSourceTimer from GCD with AsyncTimerSequence from swift-async-algorithms
    • No longer use start() and succeed()/fail() to control the ActivityIndicator, use instead a with-style function that stops the indicator once the closure ends.
  • Start migration to Swift Testing
  • Remove deprecated stuff
  • Remove integration tests
  • Improve DocC
  • Adopt swift-format

@fpseverino fpseverino requested review from 0xTim and gwynne as code owners May 28, 2025 21:48
@fpseverino fpseverino requested review from MahdiBM and ptoffy May 28, 2025 21:55
Copy link
Copy Markdown
Member

@0xTim 0xTim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No objections on my end

import Musl
#endif
import Dispatch
//import Dispatch
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I commented it because I don't know if there is now a solution for this

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @gwynne

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe marking linux_readpassphrase with @MainActor? Since there is no more Dispatch code

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +113 to +133
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
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a single Task is preferable here

Suggested change
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
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually that might not work because start never returns

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually that might not work because start never 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
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that seems correct. I believe in your code waitForAll() is not actually doing anything because you're canceling beforehand anyway

Copy link
Copy Markdown
Member

@ptoffy ptoffy May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@fpseverino fpseverino merged commit e190904 into vapor:consolekit-5 May 30, 2025
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants