Fix NWSocket deadlock and read/write

This commit is contained in:
世界
2026-02-04 22:26:28 +08:00
parent ef6d8d0fb8
commit 20e22b691a
3 changed files with 154 additions and 52 deletions
+130 -32
View File
@@ -2,52 +2,83 @@ import Foundation
import Libbox import Libbox
import Network import Network
public class NWSocket { public enum NWSocketError: Error {
case connectionClosed
case invalidLength(Int)
case messageTooLarge(Int)
case timeout(String)
}
extension NWSocketError: LocalizedError {
public var errorDescription: String? {
switch self {
case .connectionClosed:
"Connection closed"
case let .invalidLength(length):
"Invalid message length: \(length)"
case let .messageTooLarge(length):
"Message too large: \(length)"
case let .timeout(phase):
"Timed out: \(phase)"
}
}
}
private final class OneShot<T>: @unchecked Sendable {
private let lock = NSLock()
private var continuation: CheckedContinuation<T, Error>?
init(_ continuation: CheckedContinuation<T, Error>) {
self.continuation = continuation
}
@discardableResult
func resume(_ result: Result<T, Error>) -> Bool {
lock.lock()
guard let continuation else {
lock.unlock()
return false
}
self.continuation = nil
lock.unlock()
continuation.resume(with: result)
return true
}
}
public final class NWSocket {
private let connection: NWConnection private let connection: NWConnection
public init(_ connection: NWConnection) { public init(_ connection: NWConnection) {
self.connection = connection self.connection = connection
} }
public func read() throws -> Data { public func read(
let semaphore = DispatchSemaphore(value: 0) headerTimeout: TimeInterval = 0,
var result: Result<Data, Error>! bodyTimeout: TimeInterval = 60,
connection.receive(minimumIncompleteLength: 2, maximumLength: 2) { content, _, _, error in maxMessageSize: Int = 32 * 1024 * 1024
if let error { ) async throws -> Data {
result = .failure(error) let lengthChunk = try await receiveExactly(count: 2, timeout: headerTimeout, phase: "read header")
} else {
result = .success(content!)
}
semaphore.signal()
}
semaphore.wait()
let lengthChunk = try result.get()
let length = Int(LibboxDecodeLengthChunk(lengthChunk)) let length = Int(LibboxDecodeLengthChunk(lengthChunk))
connection.receive(minimumIncompleteLength: length, maximumLength: length) { content, _, _, error in guard length >= 0 else {
if let error { connection.cancel()
result = .failure(error) throw NWSocketError.invalidLength(length)
} else {
result = .success(content!)
} }
semaphore.signal() guard length <= maxMessageSize else {
connection.cancel()
throw NWSocketError.messageTooLarge(length)
} }
semaphore.wait() guard length > 0 else {
return try result.get() return Data()
}
return try await receiveExactly(count: length, timeout: bodyTimeout, phase: "read body")
} }
public func write(_ data: Data?) throws { public func write(_ data: Data?, timeout: TimeInterval = 30) async throws {
guard let data else { guard let data else {
return return
} }
let semaphore = DispatchSemaphore(value: 0) try await sendAndAwait(content: LibboxEncodeChunkedMessage(data), timeout: timeout, phase: "write")
var result: Error?
connection.send(content: LibboxEncodeChunkedMessage(data), isComplete: false, completion: .contentProcessed { error in
result = error
semaphore.wait()
})
if let result {
throw result
}
} }
public func send(_ data: Data?) { public func send(_ data: Data?) {
@@ -60,4 +91,71 @@ public class NWSocket {
public func cancel() { public func cancel() {
connection.cancel() connection.cancel()
} }
private func receiveExactly(count: Int, timeout: TimeInterval, phase: String) async throws -> Data {
guard count > 0 else {
return Data()
}
return try await withCheckedThrowingContinuation { continuation in
let oneShot = OneShot<Data>(continuation)
let timeoutItem: DispatchWorkItem?
if timeout > 0 {
timeoutItem = DispatchWorkItem { [connection] in
if oneShot.resume(.failure(NWSocketError.timeout(phase))) {
connection.cancel()
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: timeoutItem!)
} else {
timeoutItem = nil
}
connection.receive(minimumIncompleteLength: count, maximumLength: count) { content, _, isComplete, error in
timeoutItem?.cancel()
if let error {
oneShot.resume(.failure(error))
return
}
guard let content else {
oneShot.resume(.failure(NWSocketError.connectionClosed))
return
}
guard content.count == count else {
if isComplete {
oneShot.resume(.failure(NWSocketError.connectionClosed))
} else {
oneShot.resume(.failure(NWSocketError.invalidLength(content.count)))
}
return
}
oneShot.resume(.success(content))
}
}
}
private func sendAndAwait(content: Data?, timeout: TimeInterval, phase: String) async throws {
try await withCheckedThrowingContinuation { continuation in
let oneShot = OneShot<Void>(continuation)
let timeoutItem: DispatchWorkItem?
if timeout > 0 {
timeoutItem = DispatchWorkItem { [connection] in
if oneShot.resume(.failure(NWSocketError.timeout(phase))) {
connection.cancel()
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: timeoutItem!)
} else {
timeoutItem = nil
}
connection.send(content: content, isComplete: false, completion: .contentProcessed { error in
timeoutItem?.cancel()
if let error {
oneShot.resume(.failure(error))
} else {
oneShot.resume(.success(()))
}
})
}
}
} }
+9 -11
View File
@@ -55,17 +55,17 @@ public class ProfileServer {
try await writeProfilePreviewList() try await writeProfilePreviewList()
} catch { } catch {
NSLog("profile server: write profile list: \(error.localizedDescription)") NSLog("profile server: write profile list: \(error.localizedDescription)")
writeError(error.localizedDescription) await writeError(error.localizedDescription)
return return
} }
do { do {
while true { while true {
let message = try connection.read() let message = try await connection.read()
try processMessage(message) try await processMessage(message)
} }
} catch { } catch {
NSLog("profile server: process connection: \(error.localizedDescription)") NSLog("profile server: process connection: \(error.localizedDescription)")
writeError(error.localizedDescription) await writeError(error.localizedDescription)
} }
} }
@@ -89,16 +89,14 @@ public class ProfileServer {
} }
#endif #endif
private func processMessage(_ data: Data) throws { private func processMessage(_ data: Data) async throws {
if data.count == 0 { if data.count == 0 {
return return
} }
let messageType = Int64(data[0]) let messageType = Int64(data[0])
switch messageType { switch messageType {
case LibboxMessageTypeProfileContentRequest: case LibboxMessageTypeProfileContentRequest:
Task {
try await processProfileContentRequest(data) try await processProfileContentRequest(data)
}
default: default:
throw NSError(domain: "ProfileServer", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Unexpected message type \(messageType)")]) throw NSError(domain: "ProfileServer", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Unexpected message type \(messageType)")])
} }
@@ -136,7 +134,7 @@ public class ProfileServer {
content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970) content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970)
} }
} }
try connection.write(content.encode()) try await connection.write(content.encode())
} }
private func writeProfilePreviewList() async throws { private func writeProfilePreviewList() async throws {
@@ -156,13 +154,13 @@ public class ProfileServer {
} }
encoder.append(preview) encoder.append(preview)
} }
try connection.write(encoder.encode()) try await connection.write(encoder.encode())
} }
private func writeError(_ message: String) { private func writeError(_ message: String) async {
let errorMessage = LibboxErrorMessage() let errorMessage = LibboxErrorMessage()
errorMessage.message = message errorMessage.message = message
try? connection.write(errorMessage.encode()) try? await connection.write(errorMessage.encode())
} }
} }
} }
@@ -59,10 +59,13 @@
var message: Data var message: Data
while true { while true {
do { do {
message = try socket.read() message = try await socket.read()
} catch { } catch {
throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Read from connection: \(error.localizedDescription)")]) throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Read from connection: \(error.localizedDescription)")])
} }
if message.isEmpty {
continue
}
var error: NSError? var error: NSError?
switch Int64(message[0]) { switch Int64(message[0]) {
case LibboxMessageTypeError: case LibboxMessageTypeError:
@@ -112,14 +115,17 @@
connection.stateUpdateHandler = nil connection.stateUpdateHandler = nil
let request = LibboxProfileContentRequest() let request = LibboxProfileContentRequest()
request.profileID = profileID request.profileID = profileID
do {
try socket.write(request.encode())
isImporting = true isImporting = true
Task {
do {
try await socket.write(request.encode())
} catch { } catch {
isImporting = false
alert = AlertState(error: error) alert = AlertState(error: error)
reset() reset()
} }
} }
}
private nonisolated func importProfile(_ content: LibboxProfileContent, environments: ExtensionEnvironments) async throws { private nonisolated func importProfile(_ content: LibboxProfileContent, environments: ExtensionEnvironments) async throws {
var type: ProfileType = .local var type: ProfileType = .local