mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 15:50:11 +02:00
test(ios): real Swift compile + XCTest fixture; device-path probe; loopback bind fix
Closes the gap from prior commits where E2E tests stubbed the Swift StateServer in TypeScript. Now there's a real SwiftPM fixture at test/fixtures/ios-qa/FixtureApp/ that compiles the production templates and runs an XCTest suite against the actual StateServer implementation. Three new test layers: - swift build invariants (periodic-tier): debug-config build succeeds, XCTest suite passes (validates real Swift impl over Foundation + Network), release-config build has zero DebugBridge symbols (structural #if DEBUG gate works end-to-end). - Real-device probe (periodic-tier, GSTACK_HAS_IOS_DEVICE=1): devicectl can list + pair the connected iPhone. Surfaces actionable instructions when the trust dialog hasn't been confirmed yet. - Fixture sources copied from ios-qa/templates/ — Package.swift splits the bridge into DebugBridgeCore (Foundation+Network, cross-platform) and DebugBridgeUI (UIKit/SwiftUI, iOS-only) so swift build can validate the bulk of the production code on macOS without an iPhone or simulator. Also fixes a real bug the XCTest unit suite caught: NWListener with requiredLocalEndpoint on params silently fails to bind for listening (it's an outbound-connection concept). Replaced with .requiredInterfaceType=.loopback + .acceptLocalOnly=true + a per-connection peer-address check. The fork's inherited code had this bug; we shipped it untouched in v1.41.0.0 and the new XCTest suite caught it immediately.
This commit is contained in:
@@ -134,20 +134,36 @@ public final class StateServer {
|
||||
|
||||
private func startListener(family: AddressFamily) {
|
||||
do {
|
||||
// Loopback-only binding: requiredInterfaceType=.loopback restricts the
|
||||
// listener to lo0. NWListener does NOT honor requiredLocalEndpoint on
|
||||
// its NWParameters (that's an outbound-connection concept) — so we
|
||||
// pair the loopback interface gate with an explicit per-connection
|
||||
// peer-address check below.
|
||||
let params = NWParameters.tcp
|
||||
params.requiredLocalEndpoint = .hostPort(host: family.host, port: NWEndpoint.Port(rawValue: port)!)
|
||||
params.allowLocalEndpointReuse = true
|
||||
params.requiredInterfaceType = .loopback
|
||||
params.acceptLocalOnly = true
|
||||
|
||||
let listener = try NWListener(using: params, on: NWEndpoint.Port(rawValue: port)!)
|
||||
listener.stateUpdateHandler = { [weak self] state in
|
||||
Task { @MainActor in
|
||||
if case .ready = state {
|
||||
self?.logger.notice("StateServer listening on \(String(describing: family))")
|
||||
} else if case .failed(let err) = state {
|
||||
self?.logger.error("StateServer listener failed: \(err.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
listener.newConnectionHandler = { [weak self] connection in
|
||||
Task { @MainActor in self?.handle(connection) }
|
||||
Task { @MainActor in
|
||||
// Defense-in-depth: even with .loopback interface gate, double-check
|
||||
// the peer is loopback. Reject otherwise.
|
||||
if let self, self.isLoopbackPeer(connection) {
|
||||
self.handle(connection)
|
||||
} else {
|
||||
connection.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
listener.start(queue: .global(qos: .userInitiated))
|
||||
|
||||
@@ -160,6 +176,19 @@ public final class StateServer {
|
||||
}
|
||||
}
|
||||
|
||||
private func isLoopbackPeer(_ connection: NWConnection) -> Bool {
|
||||
switch connection.endpoint {
|
||||
case .hostPort(let host, _):
|
||||
switch host {
|
||||
case .ipv4(let addr): return addr == .loopback
|
||||
case .ipv6(let addr): return addr.isLoopback
|
||||
case .name(let name, _): return name == "localhost"
|
||||
@unknown default: return false
|
||||
}
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Request handling
|
||||
|
||||
private func handle(_ connection: NWConnection) {
|
||||
|
||||
Reference in New Issue
Block a user