diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj index 223b83f..e5ae28b 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj @@ -660,7 +660,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -690,7 +690,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Release; }; @@ -835,7 +835,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.ProtoTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop"; }; name = Debug; @@ -853,7 +853,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.ProtoTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop"; }; name = Release; diff --git a/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/ProtoTests.xcscheme b/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/ProtoTests.xcscheme index 3366121..556492f 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/ProtoTests.xcscheme +++ b/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/ProtoTests.xcscheme @@ -54,6 +54,15 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> + + + + diff --git a/Coder Desktop/Proto/Receiver.swift b/Coder Desktop/Proto/Receiver.swift index 74a85cb..6a279e6 100644 --- a/Coder Desktop/Proto/Receiver.swift +++ b/Coder Desktop/Proto/Receiver.swift @@ -22,7 +22,7 @@ actor Receiver { dispatch.read(offset: 0, length: 4, queue: queue) { done, data, error in guard error == 0 else { let errStrPtr = strerror(error) - let errStr = String(validatingUTF8: errStrPtr!)! + let errStr = String(validatingCString: errStrPtr!)! continuation.resume(throwing: ReceiveError.readError(errStr)) return } @@ -42,7 +42,7 @@ actor Receiver { dispatch.read(offset: 0, length: Int(length), queue: queue) { done, data, error in guard error == 0 else { let errStrPtr = strerror(error) - let errStr = String(validatingUTF8: errStrPtr!)! + let errStr = String(validatingCString: errStrPtr!)! continuation.resume(throwing: ReceiveError.readError(errStr)) return } diff --git a/Coder Desktop/Proto/Speaker.swift b/Coder Desktop/Proto/Speaker.swift index fdb334b..ca0740d 100644 --- a/Coder Desktop/Proto/Speaker.swift +++ b/Coder Desktop/Proto/Speaker.swift @@ -133,20 +133,20 @@ class Speaker { /// Send a unary RPC message and handle the response func unaryRPC(_ req: SendMsg) async throws -> RecvMsg { return try await withCheckedThrowingContinuation { continuation in - Task { - let msgID = await self.secretary.record(continuation: continuation) + Task { [sender, secretary, logger] in + let msgID = await secretary.record(continuation: continuation) var req = req req.rpc = Vpn_RPC() req.rpc.msgID = msgID do { - self.logger.debug("sending RPC with msgID: \(msgID)") - try await self.sender.send(req) + logger.debug("sending RPC with msgID: \(msgID)") + try await sender.send(req) } catch { - self.logger.warning("failed to send RPC with msgID: \(msgID): \(error)") - await self.secretary.erase(id: req.rpc.msgID) + logger.warning("failed to send RPC with msgID: \(msgID): \(error)") + await secretary.erase(id: req.rpc.msgID) continuation.resume(throwing: error) } - self.logger.debug("sent RPC with msgID: \(msgID)") + logger.debug("sent RPC with msgID: \(msgID)") } } } @@ -169,7 +169,7 @@ class Speaker { } /// A class that performs the initial VPN protocol handshake and version negotiation. -class Handshaker { +class Handshaker: @unchecked Sendable { private let writeFD: FileHandle private let dispatch: DispatchIO private var theirData: Data = .init() @@ -219,7 +219,7 @@ class Handshaker { private func handleRead(_: Bool, _ data: DispatchData?, _ error: Int32) { guard error == 0 else { let errStrPtr = strerror(error) - let errStr = String(validatingUTF8: errStrPtr!)! + let errStr = String(validatingCString: errStrPtr!)! continuation?.resume(throwing: HandshakeError.readError(errStr)) return } @@ -277,7 +277,7 @@ enum HandshakeError: Error { case unsupportedVersion([ProtoVersion]) } -struct RPCRequest { +struct RPCRequest: Sendable { let msg: RecvMsg private let sender: Sender @@ -302,7 +302,7 @@ enum RPCError: Error { } /// An actor to record outgoing RPCs and route their replies to the original sender -actor RPCSecretary { +actor RPCSecretary { private var continuations: [UInt64: CheckedContinuation] = [:] private var nextMsgID: UInt64 = 1 diff --git a/Coder Desktop/ProtoTests/SpeakerTests.swift b/Coder Desktop/ProtoTests/SpeakerTests.swift index 08829ff..82651cb 100644 --- a/Coder Desktop/ProtoTests/SpeakerTests.swift +++ b/Coder Desktop/ProtoTests/SpeakerTests.swift @@ -4,20 +4,45 @@ import Testing /// A concrete, test class for the abstract Speaker, which overrides the handlers to send things to /// continuations we set in the test. -class TestTunnel: Speaker { - var msgHandler: CheckedContinuation? +class TestTunnel: Speaker, @unchecked Sendable { + private var msgHandler: CheckedContinuation? override func handleMessage(_ msg: Vpn_ManagerMessage) { msgHandler?.resume(returning: msg) } - var rpcHandler: CheckedContinuation, Error>? + /// Runs the given closure asynchronously and returns the next non-RPC message received. + func expectMessage(with closure: + @escaping @Sendable () async -> Void) async throws -> Vpn_ManagerMessage + { + return try await withCheckedThrowingContinuation { continuation in + msgHandler = continuation + Task { + await closure() + } + } + } + + private var rpcHandler: CheckedContinuation, Error>? override func handleRPC(_ req: RPCRequest) { rpcHandler?.resume(returning: req) } + + /// Runs the given closure asynchronously and return the next non-RPC message received + func expectRPC(with closure: + @escaping @Sendable () async -> Void) async throws -> + RPCRequest + { + return try await withCheckedThrowingContinuation { continuation in + rpcHandler = continuation + Task { + await closure() + } + } + } } @Suite(.timeLimit(.minutes(1))) -struct SpeakerTests { +struct SpeakerTests: Sendable { let pipeMT = Pipe() let pipeTM = Pipe() let uut: TestTunnel @@ -56,14 +81,11 @@ struct SpeakerTests { @Test func handleSingleMessage() async throws { async let readDone: () = try uut.readLoop() - let got = try await withCheckedThrowingContinuation { continuation in - uut.msgHandler = continuation - Task { - var s = Vpn_ManagerMessage() - s.start = Vpn_StartRequest() - await #expect(throws: Never.self) { - try await sender.send(s) - } + let got = try await uut.expectMessage { + var s = Vpn_ManagerMessage() + s.start = Vpn_StartRequest() + await #expect(throws: Never.self) { + try await sender.send(s) } } #expect(got.msg == .start(Vpn_StartRequest())) @@ -74,16 +96,13 @@ struct SpeakerTests { @Test func handleRPC() async throws { async let readDone: () = try uut.readLoop() - let got = try await withCheckedThrowingContinuation { continuation in - uut.rpcHandler = continuation - Task { - var s = Vpn_ManagerMessage() - s.start = Vpn_StartRequest() - s.rpc = Vpn_RPC() - s.rpc.msgID = 33 - await #expect(throws: Never.self) { - try await sender.send(s) - } + let got = try await uut.expectRPC { + var s = Vpn_ManagerMessage() + s.start = Vpn_StartRequest() + s.rpc = Vpn_RPC() + s.rpc.msgID = 33 + await #expect(throws: Never.self) { + try await sender.send(s) } } #expect(got.msg.msg == .start(Vpn_StartRequest()))