修复账号登录后应用直接退出

系统的登录窗口在后台队列上回调结果,而回调闭包写在主线程隔离的方法里,Swift 6 在闭包入口检查执行器,
不在主线程就中止进程:选完 GitHub / Google / Apple 账号应用立即闪退,令牌还没换到,重开也没有登录状态。
回调改为在非隔离的上下文里创建,收到结果后再回到主线程处理;补了在后台队列调用回调的回归测试。
This commit is contained in:
gentpan committed 2026-09-21 03:38:08 +05:00
1 parent 718d441fa9
commit 1022e4e541
5 files changed
+46 -4

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ let package = Package(
.testTarget(name: "HelperSharedTests", dependencies: ["HelperShared"]),
.testTarget(name: "UpdatesTests", dependencies: ["Updates"]),
.testTarget(name: "AccountSyncTests", dependencies: ["AccountSync"]),
.testTarget(name: "OpenStatsUITests", dependencies: ["OpenStatsUI", "Metrics"]),
.testTarget(name: "OpenStatsUITests", dependencies: ["OpenStatsUI", "Metrics", "AccountSync"]),
.testTarget(name: "LocalizationTests", dependencies: ["Localization"]),
],
swiftLanguageModes: [.v6]
@@ -116,9 +116,8 @@ public final class SyncController {
let verifier = PKCE.verifier()
let url = client.loginURL(provider: provider, challenge: PKCE.challenge(for: verifier))
let anchor = AnchorProvider(anchor: presentationAnchor)
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: SyncClient.callbackScheme) { [weak self] callback, error in
Task { @MainActor in self?.finishSignIn(callback: callback, error: error, verifier: verifier) }
}
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: SyncClient.callbackScheme,
completionHandler: Self.completionHandler(for: self, verifier: verifier))
session.presentationContextProvider = anchor
// 保留浏览器里已登录的第三方账号,用户不必再输一次密码
session.prefersEphemeralWebBrowserSession = false
@@ -132,6 +131,15 @@ public final class SyncController {
}
}
/// 登录结束时系统在它自己的 XPC 队列上回调,不在主线程。这个闭包必须在非隔离的上下文里创建:
/// 直接写在 signIn 里会继承 @MainActor 隔离,Swift 6 在闭包入口检查执行器,发现不在主线程就中止进程
/// (选完账号应用直接退出,令牌还没换到,重开也没有登录状态)
nonisolated static func completionHandler(for controller: SyncController, verifier: String) -> @Sendable (URL?, Error?) -> Void {
{ [weak controller] callback, error in
Task { @MainActor in controller?.finishSignIn(callback: callback, error: error, verifier: verifier) }
}
}
public func cancelSignIn() {
authSession?.cancel()
authSession = nil
@@ -1,3 +1,4 @@
import AccountSync
import Foundation
@testable import Metrics
import Testing
@@ -387,3 +388,28 @@ private func isolatedDefaults() -> UserDefaults {
#expect(ranking.ranked(limit: 2).map(\.id) == ["z", "x"])
}
}
@MainActor
@Suite struct SyncSignInTests {
/// 系统在后台的 XPC 队列上回调登录结果。回调闭包如果带着主线程隔离,Swift 6 会在入口处中止进程,
/// 表现为选完账号应用直接退出;这里在后台队列调用一次,能跑完并回到主线程处理就算通过
@Test func completionHandlerMayRunOffMainThread() async {
let controller = SyncController(settings: AppSettings(defaults: isolatedDefaults()), defaults: isolatedDefaults(),
tokens: MemoryTokenStore())
let handler = SyncController.completionHandler(for: controller, verifier: "verifier")
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
DispatchQueue.global().async {
handler(nil, nil)
continuation.resume()
}
}
// 没有回调地址按失败处理;处理发生在主线程的下一个任务里,稍等一下
for _ in 0..<100 where controller.phase == .idle {
try? await Task.sleep(for: .milliseconds(10))
}
guard case .failed = controller.phase else {
Issue.record("回调之后 phase 应为 failed,实际是 \(controller.phase)")
return
}
}
}