diff --git a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift index bb919d5..1c241b7 100644 --- a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift +++ b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift @@ -132,29 +132,8 @@ public final class ScaffoldingClient { private func joinRoom(port: UInt16) async throws { - let connection: NWConnection = NWConnection(to: .hostPort(host: "127.0.0.1", port: .init(integerLiteral: port)), using: .tcp) - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - @Sendable func finish(_ result: Result) { - connection.stateUpdateHandler = nil - continuation.resume(with: result) - } - - connection.stateUpdateHandler = { [weak self] state in - switch state { - case .ready: - self?.connection = connection - finish(.success(())) - case .failed(let error): - finish(.failure(error)) - case .cancelled: - finish(.failure(ConnectionError.cancelled)) - default: - break - } - } - Logger.info("Connecting to scaffolding server...") - connection.start(queue: Scaffolding.connectQueue) - } + Logger.info("Connecting to scaffolding server...") + self.connection = try await ConnectionUtil.makeConnection(host: "127.0.0.1", port: port) Logger.info("Connected to scaffolding server") room = Room(members: [], serverPort: 0) @@ -162,8 +141,12 @@ public final class ScaffoldingClient { let serverPort: UInt16 = ByteBuffer(data: try await sendRequest("c:server_port").data).readUInt16() let localPort: UInt16 = try ConnectionUtil.getPort(serverPort) try easyTier.addPortForward(bind: "127.0.0.1:\(localPort)", destination: "\(serverNodeIp!):\(serverPort)") + guard try await Scaffolding.checkMinecraftServer(on: localPort) else { + Logger.error("Minecraft server check failed") + throw ConnectionError.invalidPort + } room.serverPort = localPort - Logger.info("Minecraft server ready: 127.0.0.1:\(localPort)") + Logger.info("Minecraft server is ready: 127.0.0.1:\(localPort)") } private func assertReady() throws { diff --git a/Sources/SwiftScaffolding/Scaffolding.swift b/Sources/SwiftScaffolding/Scaffolding.swift index 4e896c6..aefa5a1 100644 --- a/Sources/SwiftScaffolding/Scaffolding.swift +++ b/Sources/SwiftScaffolding/Scaffolding.swift @@ -11,7 +11,7 @@ import CryptoKit import Network public final class Scaffolding { - internal static let connectQueue: DispatchQueue = DispatchQueue(label: "SwiftScaffolding.Connect") + internal static let networkQueue: DispatchQueue = DispatchQueue(label: "SwiftScaffolding.Network") /// 根据设备的主板唯一标识符生成设备标识符。 /// https://github.com/Scaffolding-MC/Scaffolding-MC/blob/main/README.md#machine_id @@ -49,7 +49,7 @@ public final class Scaffolding { block() } } - connectQueue.asyncAfter(deadline: .now() + 5) { + networkQueue.asyncAfter(deadline: .now() + 5) { safeResume { continuation.resume(throwing: ConnectionError.timeout) } @@ -95,6 +95,22 @@ public final class Scaffolding { } } + /// 检查本地指定端口是否存在一个 Minecraft 服务器。 + /// - Parameter port: 服务器端口。 + public static func checkMinecraftServer(on port: UInt16) async throws -> Bool { + let connection: NWConnection = try await ConnectionUtil.makeConnection(host: "127.0.0.1", port: port) + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: [0xFE], completion: .contentProcessed { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } + }) + } + return try await ConnectionUtil.receiveData(from: connection, length: 1)[0] == 0xFF + } + public final class Response { public let status: UInt8 public let data: Data diff --git a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift index 398ac1e..e76c5f8 100644 --- a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift +++ b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift @@ -84,7 +84,7 @@ public final class ScaffoldingServer { } cleanup(connection) } - connection.start(queue: Scaffolding.connectQueue) + connection.start(queue: Scaffolding.networkQueue) } try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in @Sendable func finish(_ result: Result) { @@ -104,7 +104,7 @@ public final class ScaffoldingServer { break } } - listener.start(queue: Scaffolding.connectQueue) + listener.start(queue: Scaffolding.networkQueue) } Logger.info("ScaffoldingServer listener started at 127.0.0.1:\(port)") } diff --git a/Sources/SwiftScaffolding/Util/ConnectionUtil.swift b/Sources/SwiftScaffolding/Util/ConnectionUtil.swift index ef77b8b..bab8671 100644 --- a/Sources/SwiftScaffolding/Util/ConnectionUtil.swift +++ b/Sources/SwiftScaffolding/Util/ConnectionUtil.swift @@ -9,37 +9,72 @@ import Foundation import Network internal enum ConnectionUtil { - /// 从连接异步接收指定长度的数据。 + /// 向目标地址创建一个 TCP 连接。 /// - Parameters: - /// - connection: 目标连接。 - /// - length: 数据长度。 - /// - Returns: 接收到的数据。 - public static func receiveData(from connection: NWConnection, length: Int) async throws -> Data { + /// - host: 目标地址。 + /// - port: 目标端口。 + /// - timeout: 超时时间。 + public static func makeConnection(host: String, port: UInt16, timeout: Double = 10) async throws -> NWConnection { + let connection: NWConnection = NWConnection(to: .hostPort(host: .init(stringLiteral: host), port: .init(integerLiteral: port)), using: .tcp) + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let once: Once = .init() + @Sendable func finish(with result: Result) { + Task { + await once.run { + connection.stateUpdateHandler = nil + continuation.resume(with: result) + } + } + } + connection.stateUpdateHandler = { state in + switch state { + case .ready: + finish(with: .success(connection)) + case .failed(let error): + finish(with: .failure(error)) + case .cancelled: + finish(with: .failure(ConnectionError.cancelled)) + default: + break + } + } + Scaffolding.networkQueue.asyncAfter(deadline: .now() + timeout) { + connection.cancel() + finish(with: .failure(ConnectionError.timeout)) + } + connection.start(queue: Scaffolding.networkQueue) + } + } + + public static func receiveData(from connection: NWConnection, length: Int, timeout: Double = 10) async throws -> Data { if length == 0 { return Data() } - return try await withThrowingTaskGroup(of: Data.self) { group in - group.addTask { - try await withCheckedThrowingContinuation { continuation in - connection.receive(minimumIncompleteLength: length, maximumLength: length) { data, _, _, error in - if let error = error { - continuation.resume(throwing: error) - return - } - guard let data = data, data.count == length else { - continuation.resume(throwing: ConnectionError.cancelled) - return - } - continuation.resume(returning: data) + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let once: Once = .init() + + @Sendable func finish(with result: Result) { + Task { + await once.run { + continuation.resume(with: result) } } } - group.addTask { - try await Task.sleep(nanoseconds: 10 * 1_000_000_000) + + connection.receive(minimumIncompleteLength: length, maximumLength: length) { (data: Data?, _: NWConnection.ContentContext?, _: Bool, error: NWError?) in + if let error: NWError = error { + finish(with: .failure(error)) + return + } + guard let data: Data = data, data.count == length else { + finish(with: .failure(ConnectionError.cancelled)) + return + } + finish(with: .success(data)) + } + + Scaffolding.networkQueue.asyncAfter(deadline: .now() + timeout) { connection.cancel() - throw ConnectionError.timeout + finish(with: .failure(ConnectionError.timeout)) } - let result = try await group.next()! - group.cancelAll() - return result } } diff --git a/Sources/SwiftScaffolding/Util/Once.swift b/Sources/SwiftScaffolding/Util/Once.swift new file mode 100644 index 0000000..b24447c --- /dev/null +++ b/Sources/SwiftScaffolding/Util/Once.swift @@ -0,0 +1,15 @@ +// +// Once.swift +// SwiftScaffolding +// +// Created by 温迪 on 2026/1/28. +// + +actor Once { + private var done: Bool = false + func run(_ body: () -> Void) { + guard !done else { return } + done = true + body() + } +}