Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 7 additions & 24 deletions Sources/SwiftScaffolding/Client/ScaffoldingClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,38 +132,21 @@ 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<Void, Error>) in
@Sendable func finish(_ result: Result<Void, Error>) {
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)
try await heartbeat()
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 {
Expand Down
20 changes: 18 additions & 2 deletions Sources/SwiftScaffolding/Scaffolding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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<Void, Error>) 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
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftScaffolding/Server/ScaffoldingServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void, Error>) in
@Sendable func finish(_ result: Result<Void, Error>) {
Expand All @@ -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)")
}
Expand Down
83 changes: 59 additions & 24 deletions Sources/SwiftScaffolding/Util/ConnectionUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<NWConnection, Error>) in
let once: Once = .init()
@Sendable func finish(with result: Result<NWConnection, Error>) {
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<Data, Error>) in
let once: Once = .init()

@Sendable func finish(with result: Result<Data, Error>) {
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
}
}

Expand Down
15 changes: 15 additions & 0 deletions Sources/SwiftScaffolding/Util/Once.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}