diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ddf9665 Binary files /dev/null and b/.DS_Store differ diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..af15e61 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,12 @@ +[target.aarch64-linux-android] +linker = "/opt/homebrew/share/android-commandlinetools/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android30-clang" +ar = "/opt/homebrew/share/android-commandlinetools/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" + +[target.x86_64-pc-windows-gnu] +linker = "x86_64-w64-mingw32-gcc" +ar = "x86_64-w64-mingw32-ar" + +[env] +# Set Android SDK/NDK paths +ANDROID_NDK_HOME = "/opt/homebrew/share/android-commandlinetools/ndk/28.2.13676358" +ANDROID_NDK_ROOT = "/opt/homebrew/share/android-commandlinetools/ndk/28.2.13676358" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a618c9d..6b9553f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,13 +24,36 @@ env_logger = "0.11.8" once_cell = "1.21.3" # Optional dependencies for specific transports -quinn = { version = "0.11.8", optional = true } +quinn = { version = "0.11.8", optional = true, default-features = false, features = ["rustls-ring"] } tokio-rustls = { version = "0.26.2", optional = true } webrtc = { version = "0.13.0", optional = true } +libc = "0.2" + +# Platform-specific dependencies for path monitoring +[target.'cfg(target_vendor = "apple")'.dependencies] +libc = "0.2" + +[target.'cfg(target_os = "linux")'.dependencies] +rtnetlink = "0.14" +netlink-packet-route = "0.19" +futures = "0.3" + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.58", features = [ + "Win32_NetworkManagement_IpHelper", + "Win32_NetworkManagement_Ndis", + "Win32_Foundation", + "Win32_Networking_WinSock", +] } + +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.21" [dev-dependencies] tokio-test = "0.4.4" env_logger = "0.11.8" +ctrlc = "3.4" +chrono = "0.4" [build-dependencies] cbindgen = { version = "0.29.0", optional = true } @@ -48,3 +71,15 @@ cbindgen = ["dep:cbindgen"] lto = true codegen-units = 1 opt-level = 3 + +[[example]] +name = "path_monitor" +required-features = [] + +[[example]] +name = "path_monitor_detailed" +required-features = [] + +[[example]] +name = "path_monitor_watch" +required-features = [] diff --git a/Dockerfile.build b/Dockerfile.build index 8b5bbbc..58b8a59 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -32,18 +32,12 @@ RUN mkdir -p /opt && \ mv android-ndk-${ANDROID_NDK_VERSION} ${ANDROID_NDK_HOME} && \ rm android-ndk.zip -# Install Rust targets +# Install Rust targets (only those available in stable Rust) RUN rustup target add \ - aarch64-apple-ios \ - aarch64-apple-tvos \ - aarch64-apple-darwin \ - x86_64-apple-darwin \ - aarch64-apple-watchos \ aarch64-linux-android \ x86_64-unknown-linux-gnu \ aarch64-unknown-linux-gnu \ - x86_64-pc-windows-gnu \ - aarch64-pc-windows-gnullvm + x86_64-pc-windows-gnu # Install cbindgen RUN cargo install cbindgen diff --git a/Package.swift b/Package.swift index b755b01..8c40c99 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.2 +// swift-tools-version:6.1 import PackageDescription #if canImport(FoundationEssentials) import FoundationEssentials // Needed for binary target support diff --git a/bindings/swift/README.md b/bindings/swift/README.md index aa6b7e2..87c445f 100644 --- a/bindings/swift/README.md +++ b/bindings/swift/README.md @@ -1,10 +1,10 @@ # Transport Services Swift Bindings -Swift bindings for the Transport Services (RFC 9622) implementation. +Modern Swift 6 bindings for the Transport Services (RFC 9622) implementation with full concurrency support. ## Overview -This package provides a Swift-friendly API for Transport Services, wrapping the underlying Rust FFI implementation. +This package provides a Swift-friendly API for Transport Services, wrapping the underlying Rust FFI implementation with modern Swift concurrency features including async/await, AsyncSequence, and Sendable conformance. ## Building @@ -26,6 +26,8 @@ USE_LOCAL_ARTIFACT=1 swift test ## Usage +### Basic Connection Example + ```swift import TransportServices @@ -55,6 +57,35 @@ try await connection.close() TransportServices.cleanup() ``` +### Path Monitoring Example + +```swift +import TransportServices + +// Create a path monitor +let monitor = try PathMonitor() + +// List current interfaces +let interfaces = try await monitor.interfaces() +for interface in interfaces { + print("\(interface.name): \(interface.status) - \(interface.interfaceType)") +} + +// Monitor network changes +for await event in monitor.changes() { + switch event { + case .added(let interface): + print("Interface added: \(interface.name)") + case .removed(let interface): + print("Interface removed: \(interface.name)") + case .modified(let old, let new): + print("Interface changed: \(new.name)") + case .pathChanged(let description): + print("Path changed: \(description)") + } +} +``` + ## Requirements - Swift 6.2 or later @@ -65,9 +96,12 @@ TransportServices.cleanup() - [x] Basic package structure - [x] FFI binary target integration - [x] Swift Testing setup -- [ ] Complete async/await wrappers +- [x] Path monitoring with async/await +- [x] NetworkInterface type with Sendable conformance +- [x] AsyncSequence for network changes +- [x] Thread-safe actor-based implementation +- [ ] Complete connection async/await wrappers - [ ] Endpoint implementations - [ ] Transport properties - [ ] Security parameters -- [ ] Event handling -- [ ] Error handling \ No newline at end of file +- [ ] Connection event handling \ No newline at end of file diff --git a/bindings/swift/Sources/TransportServices/Connection.swift b/bindings/swift/Sources/TransportServices/Connection.swift new file mode 100644 index 0000000..3e02a40 --- /dev/null +++ b/bindings/swift/Sources/TransportServices/Connection.swift @@ -0,0 +1,268 @@ +#if !hasFeature(Embedded) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) +import Foundation +#endif +#endif +import TransportServicesFFI + +// MARK: - Connection State + +/// Connection state enumeration +public enum ConnectionState: Sendable { + case establishing + case ready + case closing + case closed + case failed(Error) + + /// Create from FFI state + init(ffi: TransportServicesConnectionState) { + switch ffi { + case TRANSPORT_SERVICES_CONNECTION_STATE_ESTABLISHING: + self = .establishing + case TRANSPORT_SERVICES_CONNECTION_STATE_READY: + self = .ready + case TRANSPORT_SERVICES_CONNECTION_STATE_CLOSING: + self = .closing + case TRANSPORT_SERVICES_CONNECTION_STATE_CLOSED: + self = .closed + case TRANSPORT_SERVICES_CONNECTION_STATE_FAILED: + self = .failed(TransportServicesError.connectionFailed(message: "Connection failed")) + default: + self = .closed + } + } +} + +// MARK: - Connection Events + +/// Events that can occur on a connection +public enum ConnectionEvent: Sendable { + case stateChanged(ConnectionState) + case received(Data) + case receivedPartial(Data, isEnd: Bool) + case sent + case sendError(Error) + case pathChanged + case softError(Error) +} + +// MARK: - Message + +/// Message for sending data with metadata +public struct Message: Sendable { + public let data: Data + public let context: MessageContext? + + public init(data: Data, context: MessageContext? = nil) { + self.data = data + self.context = context + } + + /// Create a message from a string + public static func from(_ string: String, encoding: String.Encoding = .utf8) -> Message? { + guard let data = string.data(using: encoding) else { return nil } + return Message(data: data) + } +} + +/// Message context for additional metadata +public struct MessageContext: Sendable { + public let messageLifetime: TimeInterval? + public let priority: Int? + public let isEndOfMessage: Bool + + public init(messageLifetime: TimeInterval? = nil, priority: Int? = nil, isEndOfMessage: Bool = true) { + self.messageLifetime = messageLifetime + self.priority = priority + self.isEndOfMessage = isEndOfMessage + } +} + +// MARK: - Connection Actor + +/// Thread-safe connection manager using actor +public actor Connection { + private let handle: OpaquePointer + private var eventContinuation: AsyncStream.Continuation? + private var isClosed = false + + /// Current connection state + public private(set) var state: ConnectionState = .establishing + + /// Create a connection from an FFI handle + init(handle: OpaquePointer) { + self.handle = handle + setupEventHandling() + } + + deinit { + if !isClosed { + transport_services_connection_close(handle) + } + transport_services_connection_free(handle) + } + + // MARK: - Public Methods + + /// Get the current connection state + public func getState() -> ConnectionState { + guard !isClosed else { return .closed } + + let ffiState = transport_services_connection_get_state(handle) + let newState = ConnectionState(ffi: ffiState) + + // Update our cached state + state = newState + return newState + } + + /// Send data on the connection + public func send(_ message: Message) async throws { + guard !isClosed else { + throw TransportServicesError.connectionClosed + } + + guard case .ready = state else { + throw TransportServicesError.sendFailed(message: "Connection not ready") + } + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + message.data.withUnsafeBytes { dataBufferPointer in + var ffiMessage = TransportServicesMessage() + ffiMessage.data = dataBufferPointer.baseAddress + ffiMessage.length = message.data.count + + if let context = message.context { + if let lifetime = context.messageLifetime { + ffiMessage.lifetime_ms = UInt64(lifetime * 1000) + } + if let priority = context.priority { + ffiMessage.priority = Int32(priority) + } + ffiMessage.is_end_of_message = context.isEndOfMessage + } else { + ffiMessage.is_end_of_message = true + } + + let sendContext = Unmanaged.passRetained(SendContinuationContext(continuation: continuation)) + + let result = transport_services_connection_send( + handle, + &ffiMessage, + { error, _, userData in // message pointer is ignored here + guard let userData = userData else { return } + let context = Unmanaged.fromOpaque(userData).takeRetainedValue() + if error == TRANSPORT_SERVICES_ERROR_NONE { + context.continuation.resume() + } else { + let errorMessage = TransportServices.getLastError() ?? "Send failed with code \(error)" + context.continuation.resume(throwing: TransportServicesError.sendFailed(message: errorMessage)) + } + }, + sendContext.toOpaque() + ) + + if result != TRANSPORT_SERVICES_ERROR_NONE { + sendContext.release() + let errorMessage = TransportServices.getLastError() ?? "Send failed" + continuation.resume(throwing: TransportServicesError.sendFailed(message: errorMessage)) + } + } + } + } + + /// Send data convenience method + public func send(_ data: Data) async throws { + try await send(Message(data: data)) + } + + /// Send string convenience method + public func send(_ string: String, encoding: String.Encoding = .utf8) async throws { + guard let message = Message.from(string, encoding: encoding) else { + throw TransportServicesError.invalidParameter + } + try await send(message) + } + + /// Receive data from the connection + public func receive() async throws -> Data { + guard !isClosed else { + throw TransportServicesError.connectionClosed + } + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let receiveContext = Unmanaged.passRetained(ReceiveContinuationContext(continuation: continuation)) + + transport_services_connection_receive( + handle, + { messagePtr, error, userData in + guard let userData = userData else { return } + let context = Unmanaged.fromOpaque(userData).takeRetainedValue() + + if let messagePtr = messagePtr { + let message = messagePtr.pointee + if let dataPtr = message.data, message.length > 0 { + let data = Data(bytes: dataPtr, count: message.length) + context.continuation.resume(returning: data) + } else { + context.continuation.resume(throwing: TransportServicesError.receiveFailed(message: "Empty message received")) + } + } else { + let errorMessage = TransportServices.getLastError() ?? "Receive failed with code \(error)" + context.continuation.resume(throwing: TransportServicesError.receiveFailed(message: errorMessage)) + } + }, + receiveContext.toOpaque() + ) + } + } + + /// Close the connection gracefully + public func close() async throws { + guard !isClosed else { return } + + isClosed = true + state = .closing + + // Close the connection + transport_services_connection_close(handle) + state = .closed + + // Notify event stream + eventContinuation?.yield(.stateChanged(.closed)) + eventContinuation?.finish() + } + + /// Get an async sequence of connection events + public func events() -> AsyncStream { + AsyncStream { continuation in + self.eventContinuation = continuation + + // Yield current state + continuation.yield(.stateChanged(state)) + } + } + + // MARK: - Private Methods + + private func setupEventHandling() { + // TODO: Set up FFI event callbacks + } +} + +// MARK: - Callback Contexts + +/// Context for send continuations +private final class SendContinuationContext { + let continuation: CheckedContinuation + init(continuation: CheckedContinuation) { self.continuation = continuation } +} + +/// Context for receive continuations +private final class ReceiveContinuationContext { + let continuation: CheckedContinuation + init(continuation: CheckedContinuation) { self.continuation = continuation } +} diff --git a/bindings/swift/Sources/TransportServices/Endpoints.swift b/bindings/swift/Sources/TransportServices/Endpoints.swift new file mode 100644 index 0000000..d112291 --- /dev/null +++ b/bindings/swift/Sources/TransportServices/Endpoints.swift @@ -0,0 +1,147 @@ +#if !hasFeature(Embedded) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) +import Foundation +#endif +#endif +import TransportServicesFFI + +// MARK: - Endpoint Protocol + +/// Common protocol for all endpoint types +public protocol Endpoint: Sendable { + /// Convert to FFI representation + func toFFI() -> TransportServicesEndpoint +} + +// MARK: - Local Endpoint + +/// Local endpoint for connections +public struct LocalEndpoint: Endpoint, Hashable { + /// IP address (optional) + public let ipAddress: String? + + /// Port number (0 for any available port) + public let port: UInt16 + + /// Network interface name (optional) + public let interface: String? + + /// Create a local endpoint + public init(ipAddress: String? = nil, port: UInt16 = 0, interface: String? = nil) { + self.ipAddress = ipAddress + self.port = port + self.interface = interface + } + + /// Create a local endpoint listening on any address + public static func any(port: UInt16 = 0) -> LocalEndpoint { + LocalEndpoint(ipAddress: nil, port: port) + } + + /// Create a local endpoint for localhost + public static func localhost(port: UInt16 = 0) -> LocalEndpoint { + LocalEndpoint(ipAddress: "127.0.0.1", port: port) + } + + public func toFFI() -> TransportServicesEndpoint { + TransportServicesEndpoint( + hostname: ipAddress?.withCString { strdup($0) }, + port: port, + service: nil, + interface: interface?.withCString { strdup($0) } + ) + } +} + +// MARK: - Remote Endpoint + +/// Remote endpoint for connections +public struct RemoteEndpoint: Endpoint, Hashable { + /// Hostname or IP address + public let hostname: String + + /// Port number or service name + public let portOrService: PortOrService + + /// Network interface to use (optional) + public let interface: String? + + /// Port or service identifier + public enum PortOrService: Hashable, Sendable { + case port(UInt16) + case service(String) + } + + /// Create a remote endpoint with hostname and port + public init(hostname: String, port: UInt16, interface: String? = nil) { + self.hostname = hostname + self.portOrService = .port(port) + self.interface = interface + } + + /// Create a remote endpoint with hostname and service name + public init(hostname: String, service: String, interface: String? = nil) { + self.hostname = hostname + self.portOrService = .service(service) + self.interface = interface + } + + public func toFFI() -> TransportServicesEndpoint { + let service: UnsafeMutablePointer? + let port: UInt16 + + switch portOrService { + case .port(let p): + port = p + service = nil + case .service(let s): + port = 0 + service = s.withCString { strdup($0) } + } + + return TransportServicesEndpoint( + hostname: hostname.withCString { strdup($0) }, + port: port, + service: service, + interface: interface?.withCString { strdup($0) } + ) + } +} + +// MARK: - Endpoint Utilities + +extension Array where Element == any Endpoint { + /// Convert array of endpoints to FFI representation + func toFFIArray() -> (UnsafeMutablePointer?, Int) { + guard !isEmpty else { return (nil, 0) } + + let buffer = UnsafeMutablePointer.allocate(capacity: count) + for (index, endpoint) in enumerated() { + buffer.advanced(by: index).pointee = endpoint.toFFI() + } + + return (buffer, count) + } +} + +/// Free FFI endpoint array +func freeFFIEndpoints(_ endpoints: UnsafeMutablePointer?, count: Int) { + guard let endpoints = endpoints, count > 0 else { return } + + for i in 0...Continuation? + private var acceptContinuations: [CheckedContinuation] = [] + private var isStopped = false + + /// Maximum number of pending connections + public var connectionLimit: Int = 100 { + didSet { + guard !isStopped else { return } + transport_services_listener_set_new_connection_limit(handle, Int32(connectionLimit)) + } + } + + /// Create a listener from an FFI handle + init(handle: OpaquePointer) { + self.handle = handle + setupEventHandling() + } + + deinit { + if !isStopped { + transport_services_listener_stop(handle) + } + transport_services_listener_free(handle) + } + + // MARK: - Public Methods + + /// Get the local address the listener is bound to + public func getLocalAddress() async throws -> (address: String, port: UInt16) { + guard !isStopped else { + throw TransportServicesError.listenerFailed(message: "Listener is stopped") + } + + var address: UnsafeMutablePointer? + var port: UInt16 = 0 + + let result = transport_services_listener_get_local_endpoint(handle, &address, &port) + + guard result == 0, let addressPtr = address else { + throw TransportServicesError.listenerFailed(message: "Failed to get local address") + } + + defer { transport_services_free_string(addressPtr) } + + let addressString = String(cString: addressPtr) + return (addressString, port) + } + + /// Accept a new connection + public func accept() async throws -> Connection { + guard !isStopped else { + throw TransportServicesError.listenerFailed(message: "Listener is stopped") + } + + return try await withCheckedThrowingContinuation { continuation in + acceptContinuations.append(continuation) + + // Trigger accept if not already waiting + if acceptContinuations.count == 1 { + startAccepting() + } + } + } + + /// Get an async sequence of incoming connections + public func connections() -> ListenerConnectionSequence { + ListenerConnectionSequence(listener: self) + } + + /// Get an async sequence of listener events + public func events() -> AsyncStream { + AsyncStream { continuation in + self.eventContinuation = continuation + + // Get and yield initial ready event + Task { + do { + let (address, port) = try await getLocalAddress() + continuation.yield(.ready(localAddress: address, port: port)) + } catch { + // Ignore if we can't get the address immediately + } + } + } + } + + /// Stop the listener + public func stop() async { + guard !isStopped else { return } + + isStopped = true + + // Cancel all pending accepts + for continuation in acceptContinuations { + continuation.resume(throwing: TransportServicesError.cancelled) + } + acceptContinuations.removeAll() + + // Stop the listener + transport_services_listener_stop(handle) + + // Notify event stream + eventContinuation?.yield(.stopped(nil)) + eventContinuation?.finish() + } + + // MARK: - Private Methods + + private func setupEventHandling() { + // Set up connection received callback + let context = Unmanaged.passRetained(ListenerContext { [weak self] connectionHandle in + Task { [weak self] in + await self?.handleConnectionReceived(connectionHandle) + } + }) + + transport_services_listener_set_new_connection_handler( + handle, + { connectionHandle, userData in + guard let userData = userData, let connectionHandle = connectionHandle else { return } + let context = Unmanaged.fromOpaque(userData).takeUnretainedValue() + context.callback(connectionHandle) + }, + context.toOpaque() + ) + } + + private func startAccepting() { + guard !isStopped, !acceptContinuations.isEmpty else { return } + + // The FFI layer will call our callback when a connection is received + // No explicit accept call needed - it's event-driven + } + + private func handleConnectionReceived(_ connectionHandle: OpaquePointer) { + let connection = Connection(handle: connectionHandle) + + // Fulfill waiting accept if any + if let continuation = acceptContinuations.first { + acceptContinuations.removeFirst() + continuation.resume(returning: connection) + } + + // Also yield to event stream + eventContinuation?.yield(.connectionReceived(connection)) + } +} + +// MARK: - AsyncSequence for Connections + +/// AsyncSequence that yields incoming connections +public struct ListenerConnectionSequence: AsyncSequence { + public typealias Element = Connection + + private let listener: Listener + + init(listener: Listener) { + self.listener = listener + } + + public func makeAsyncIterator() -> ListenerConnectionIterator { + ListenerConnectionIterator(listener: listener) + } +} + +/// AsyncIterator for incoming connections +public struct ListenerConnectionIterator: AsyncIteratorProtocol { + public typealias Element = Connection + + private let listener: Listener + + init(listener: Listener) { + self.listener = listener + } + + public mutating func next() async -> Connection? { + do { + return try await listener.accept() + } catch { + // Return nil on error to end iteration + return nil + } + } +} + +// MARK: - Listener Context + +/// Context for listener callbacks +private final class ListenerContext { + let callback: (OpaquePointer) -> Void + + init(callback: @escaping (OpaquePointer) -> Void) { + self.callback = callback + } +} + +// MARK: - Convenience Extensions + +public extension Listener { + /// Accept connections with a handler closure + func acceptLoop(handler: @escaping (Connection) async throws -> Void) async { + for await connection in connections() { + // Handle each connection concurrently + Task { + do { + try await handler(connection) + } catch { + // Error is silently ignored to prevent crashes + // Users should handle errors in their handler if needed + } + } + } + } + + /// Accept connections with a handler closure and error handler + func acceptLoop( + handler: @escaping (Connection) async throws -> Void, + errorHandler: @escaping (Error) -> Void + ) async { + for await connection in connections() { + // Handle each connection concurrently + Task { + do { + try await handler(connection) + } catch { + errorHandler(error) + } + } + } + } + + /// Accept a limited number of connections + func accept(count: Int) async throws -> [Connection] { + var connections: [Connection] = [] + + for _ in 0.. [NetworkInterface] { + try await withCheckedThrowingContinuation { continuation in + lock.lock() + defer { lock.unlock() } + + var interfacePointers: UnsafeMutablePointer?>? + var count: Int = 0 + + let result = transport_services_path_monitor_list_interfaces( + handle, + &interfacePointers, + &count + ) + + guard result == 0, let interfaces = interfacePointers else { + let error = Self.getLastError() ?? "Failed to list interfaces" + continuation.resume(throwing: PathMonitorError.listInterfacesFailed(message: error)) + return + } + + defer { + transport_services_path_monitor_free_interfaces(interfaces, count) + } + + var swiftInterfaces: [NetworkInterface] = [] + + for i in 0.. NetworkChangeSequence { + NetworkChangeSequence(monitor: self) + } + + // MARK: - Private Helpers + + fileprivate func startWatching(callback: @escaping (NetworkChangeEvent) -> Void) -> OpaquePointer? { + let context = Unmanaged.passRetained(NetworkChangeContext(callback: callback)) + + let watcherHandle = transport_services_path_monitor_start_watching( + handle, + { eventPtr, userDataPtr in + guard let eventPtr = eventPtr, + let userDataPtr = userDataPtr else { return } + + let context = Unmanaged.fromOpaque(userDataPtr).takeUnretainedValue() + let event = eventPtr.pointee + + let swiftEvent = NetworkChangeEvent(from: event) + context.callback(swiftEvent) + }, + context.toOpaque() + ) + + if watcherHandle == nil { + context.release() + } + + return watcherHandle + } + + private static func getLastError() -> String? { + guard let errorCString = transport_services_get_last_error() else { return nil } + return String(cString: errorCString) + } +} + +// MARK: - Network Interface + +/// Represents a network interface + +public struct NetworkInterface: Sendable, Identifiable { + public let id: String + public let name: String + public let index: UInt32 + public let ipAddresses: [String] + public let status: Status + public let interfaceType: String + public let isExpensive: Bool + + public enum Status: Sendable { + case up + case down + case unknown + } + + init(from ffi: TransportServicesInterface) { + self.id = "\(ffi.name ?? "unknown")_\(ffi.index)" + self.name = String(cString: ffi.name ?? "unknown") + self.index = ffi.index + + // Convert IP addresses + var addresses: [String] = [] + if let ips = ffi.ips, ffi.ip_count > 0 { + for i in 0.. NetworkChangeIterator { + NetworkChangeIterator(monitor: monitor) + } +} + +/// AsyncIterator for network change events + +public actor NetworkChangeIterator: AsyncIteratorProtocol { + public typealias Element = NetworkChangeEvent + + private let monitor: PathMonitor + private var watcherHandle: OpaquePointer? + private var continuation: AsyncStream.Continuation? + private var stream: AsyncStream? + private var iterator: AsyncStream.Iterator? + + init(monitor: PathMonitor) { + self.monitor = monitor + setupStream() + } + + deinit { + Task { [watcherHandle] in + if let handle = watcherHandle { + transport_services_path_monitor_stop_watching(handle) + } + } + } + + private func setupStream() { + let (stream, continuation) = AsyncStream.makeStream() + self.stream = stream + self.continuation = continuation + self.iterator = stream.makeAsyncIterator() + + // Start watching + Task { + await startWatching() + } + } + + private func startWatching() { + guard let continuation = continuation else { return } + + watcherHandle = monitor.startWatching { event in + continuation.yield(event) + } + + if watcherHandle == nil { + continuation.finish() + } + } + + public func next() async -> NetworkChangeEvent? { + await iterator?.next() + } +} + +// MARK: - Supporting Types + +/// Context for network change callbacks +private final class NetworkChangeContext { + let callback: (NetworkChangeEvent) -> Void + + init(callback: @escaping (NetworkChangeEvent) -> Void) { + self.callback = callback + } +} + +// MARK: - Errors + +/// Errors that can occur with path monitoring + +public enum PathMonitorError: Error, LocalizedError { + case creationFailed(message: String) + case listInterfacesFailed(message: String) + case watchingFailed(message: String) + + public var errorDescription: String? { + switch self { + case .creationFailed(let message): + return "Failed to create path monitor: \(message)" + case .listInterfacesFailed(let message): + return "Failed to list interfaces: \(message)" + case .watchingFailed(let message): + return "Failed to start watching: \(message)" + } + } +} + +// MARK: - Convenience Extensions + + +public extension NetworkInterface { + /// Check if this interface has IPv4 connectivity + var hasIPv4: Bool { + ipAddresses.contains { address in + address.contains(".") && !address.contains(":") + } + } + + /// Check if this interface has IPv6 connectivity + var hasIPv6: Bool { + ipAddresses.contains { address in + address.contains(":") + } + } + + /// Check if this is a loopback interface + var isLoopback: Bool { + name.lowercased().contains("lo") || interfaceType.lowercased() == "loopback" + } + + /// Check if this is a WiFi interface + var isWiFi: Bool { + interfaceType.lowercased() == "wifi" || name.lowercased().contains("en0") + } + + /// Check if this is a cellular interface + var isCellular: Bool { + interfaceType.lowercased() == "cellular" || name.lowercased().contains("pdp") + } +} \ No newline at end of file diff --git a/bindings/swift/Sources/TransportServices/Preconnection.swift b/bindings/swift/Sources/TransportServices/Preconnection.swift new file mode 100644 index 0000000..8f2a724 --- /dev/null +++ b/bindings/swift/Sources/TransportServices/Preconnection.swift @@ -0,0 +1,202 @@ +#if !hasFeature(Embedded) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) +import Foundation +#endif +#endif +import TransportServicesFFI + +/// Preconnection represents a set of parameters for establishing connections +public struct Preconnection: Sendable { + private let handle: OpaquePointer + + /// Local endpoints + public let localEndpoints: [LocalEndpoint] + + /// Remote endpoints + public let remoteEndpoints: [RemoteEndpoint] + + /// Transport properties + public let transportProperties: TransportProperties + + /// Security parameters + public let securityParameters: SecurityParameters + + /// Create a new preconnection + public init( + localEndpoints: [LocalEndpoint] = [], + remoteEndpoints: [RemoteEndpoint] = [], + transportProperties: TransportProperties = TransportProperties(), + securityParameters: SecurityParameters = SecurityParameters() + ) throws { + // Ensure runtime is initialized + try Runtime.shared.initialize() + + self.localEndpoints = localEndpoints + self.remoteEndpoints = remoteEndpoints + self.transportProperties = transportProperties + self.securityParameters = securityParameters + + // Convert endpoints to FFI + let (localFFI, localCount) = localEndpoints.map { $0 as any Endpoint }.toFFIArray() + defer { freeFFIEndpoints(localFFI, count: localCount) } + + let (remoteFFI, remoteCount) = remoteEndpoints.map { $0 as any Endpoint }.toFFIArray() + defer { freeFFIEndpoints(remoteFFI, count: remoteCount) } + + // Get property handles + guard let propertiesHandle = transportProperties.toFFIHandle() else { + throw TransportServicesError.invalidParameter + } + defer { transport_services_transport_properties_free(propertiesHandle) } + + guard let securityHandle = securityParameters.toFFIHandle() else { + throw TransportServicesError.invalidParameter + } + defer { transport_services_security_parameters_free(securityHandle) } + + // Create preconnection + guard let handle = transport_services_preconnection_new( + localFFI, + localCount, + remoteFFI, + remoteCount, + propertiesHandle, + securityHandle + ) else { + let error = TransportServices.getLastError() ?? "Failed to create preconnection" + throw TransportServicesError.connectionFailed(message: error) + } + + self.handle = handle + } + + /// Initiate a connection + public func initiate() async throws -> Connection { + try await withCheckedThrowingContinuation { continuation in + let context = PreconnectionContext(continuation: continuation) + let contextPtr = Unmanaged.passRetained(context) + + transport_services_preconnection_initiate( + handle, + { connectionHandle, error, userData in + guard let userData = userData else { return } + let context = Unmanaged.fromOpaque(userData).takeRetainedValue() + + if let connectionHandle = connectionHandle { + let connection = Connection(handle: connectionHandle) + context.continuation.resume(returning: connection) + } else { + let errorMessage = TransportServices.getLastError() ?? "Connection initiation failed" + context.continuation.resume(throwing: TransportServicesError.connectionFailed(message: errorMessage)) + } + }, + contextPtr.toOpaque() + ) + } + } + + /// Listen for incoming connections + public func listen() async throws -> Listener { + let listenerHandle = transport_services_preconnection_listen(handle) + guard let handle = listenerHandle else { + let error = TransportServices.getLastError() ?? "Failed to create listener" + throw TransportServicesError.listenerFailed(message: error) + } + + return Listener(handle: handle) + } + + /// Start a rendezvous (simultaneous connect/listen) + public func rendezvous() async throws -> (Connection, Listener) { + // Create a listener first + let listener = try await listen() + + // Then initiate a connection + let connection = try await initiate() + + return (connection, listener) + } +} + +// MARK: - Preconnection Context + +/// Context for preconnection callbacks +private final class PreconnectionContext { + let continuation: CheckedContinuation + + init(continuation: CheckedContinuation) { + self.continuation = continuation + } +} + +// MARK: - Preconnection Builder + +/// Builder pattern for creating preconnections +public struct PreconnectionBuilder: Sendable { + private var localEndpoints: [LocalEndpoint] = [] + private var remoteEndpoints: [RemoteEndpoint] = [] + private var transportProperties = TransportProperties() + private var securityParameters = SecurityParameters() + + public init() {} + + /// Add a local endpoint + public func withLocalEndpoint(_ endpoint: LocalEndpoint) -> PreconnectionBuilder { + var builder = self + builder.localEndpoints.append(endpoint) + return builder + } + + /// Add a remote endpoint + public func withRemoteEndpoint(_ endpoint: RemoteEndpoint) -> PreconnectionBuilder { + var builder = self + builder.remoteEndpoints.append(endpoint) + return builder + } + + /// Add a remote endpoint with hostname and port + public func withRemote(hostname: String, port: UInt16) -> PreconnectionBuilder { + withRemoteEndpoint(RemoteEndpoint(hostname: hostname, port: port)) + } + + /// Set transport properties + public func withTransportProperties(_ properties: TransportProperties) -> PreconnectionBuilder { + var builder = self + builder.transportProperties = properties + return builder + } + + /// Use reliable stream transport (TCP-like) + public func withReliableStream() -> PreconnectionBuilder { + withTransportProperties(.reliableStream()) + } + + /// Use unreliable datagram transport (UDP-like) + public func withUnreliableDatagram() -> PreconnectionBuilder { + withTransportProperties(.unreliableDatagram()) + } + + /// Set security parameters + public func withSecurityParameters(_ parameters: SecurityParameters) -> PreconnectionBuilder { + var builder = self + builder.securityParameters = parameters + return builder + } + + /// Enable TLS + public func withTLS(serverName: String? = nil) -> PreconnectionBuilder { + withSecurityParameters(.tls(serverName: serverName)) + } + + /// Build the preconnection + public func build() throws -> Preconnection { + try Preconnection( + localEndpoints: localEndpoints, + remoteEndpoints: remoteEndpoints, + transportProperties: transportProperties, + securityParameters: securityParameters + ) + } +} \ No newline at end of file diff --git a/bindings/swift/Sources/TransportServices/Properties.swift b/bindings/swift/Sources/TransportServices/Properties.swift new file mode 100644 index 0000000..5ff6968 --- /dev/null +++ b/bindings/swift/Sources/TransportServices/Properties.swift @@ -0,0 +1,249 @@ +#if !hasFeature(Embedded) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) +import Foundation +#endif +#endif +import TransportServicesFFI + +// MARK: - Preference + +/// Preference level for transport properties +public enum Preference: Int32, CaseIterable, Sendable { + case require = 0 + case prefer = 1 + case noPreference = 2 + case avoid = 3 + case prohibit = 4 + + /// Convert to FFI representation + var toFFI: TransportServicesPreference { + TransportServicesPreference(rawValue: self.rawValue)! + } + + /// Create from FFI representation + init(ffi: TransportServicesPreference) { + self = Preference(rawValue: ffi.rawValue)! + } +} + +// MARK: - Multipath Configuration + +/// Multipath configuration options +public enum MultipathConfig: Int32, CaseIterable, Sendable { + case disabled = 0 + case active = 1 + case passive = 2 + + /// Convert to FFI representation + var toFFI: TransportServicesMultipathConfig { + TransportServicesMultipathConfig(rawValue: self.rawValue)! + } + + /// Create from FFI representation + init(ffi: TransportServicesMultipathConfig) { + self = MultipathConfig(rawValue: ffi.rawValue)! + } +} + +// MARK: - Communication Direction + +/// Communication direction for connections +public enum CommunicationDirection: Int32, CaseIterable, Sendable { + case bidirectional = 0 + case unidirectionalSend = 1 + case unidirectionalReceive = 2 + + /// Convert to FFI representation + var toFFI: TransportServicesCommunicationDirection { + TransportServicesCommunicationDirection(rawValue: self.rawValue)! + } + + /// Create from FFI representation + init(ffi: TransportServicesCommunicationDirection) { + self = CommunicationDirection(rawValue: ffi.rawValue)! + } +} + +// MARK: - Transport Properties + +/// Transport properties configuration +public struct TransportProperties: Sendable { + // Protocol preferences + public var reliability: Preference + public var preserveOrder: Preference + public var preserveMsgBoundaries: Preference + public var perMessageReliability: Preference + public var zeroRttMsg: Preference + public var multistreaming: Preference + public var fullchecksum: Preference + public var congestionControl: Preference + public var keepAlive: Preference + + // Interface preferences + public var useTemporaryLocalAddress: Preference + public var multipath: MultipathConfig + public var direction: CommunicationDirection + public var retransmitNotify: Preference + public var softErrorNotify: Preference + + // Connection preferences + public var pvd: String? + public var expiredDnsAllowed: Bool + + /// Create transport properties with default values + public init() { + // Defaults for reliable, ordered delivery (TCP-like) + self.reliability = .require + self.preserveOrder = .require + self.preserveMsgBoundaries = .noPreference + self.perMessageReliability = .noPreference + self.zeroRttMsg = .noPreference + self.multistreaming = .noPreference + self.fullchecksum = .require + self.congestionControl = .require + self.keepAlive = .noPreference + + self.useTemporaryLocalAddress = .noPreference + self.multipath = .disabled + self.direction = .bidirectional + self.retransmitNotify = .noPreference + self.softErrorNotify = .noPreference + + self.pvd = nil + self.expiredDnsAllowed = false + } + + /// Create properties for reliable, ordered stream (TCP-like) + public static func reliableStream() -> TransportProperties { + TransportProperties() + } + + /// Create properties for unreliable datagram (UDP-like) + public static func unreliableDatagram() -> TransportProperties { + var props = TransportProperties() + props.reliability = .avoid + props.preserveOrder = .avoid + props.preserveMsgBoundaries = .require + props.congestionControl = .avoid + return props + } + + /// Create properties for reliable datagram (SCTP-like) + public static func reliableDatagram() -> TransportProperties { + var props = TransportProperties() + props.reliability = .require + props.preserveOrder = .noPreference + props.preserveMsgBoundaries = .require + return props + } + + /// Convert to FFI handle + func toFFIHandle() -> OpaquePointer? { + let handle = transport_services_transport_properties_new() + + // Set all properties + transport_services_transport_properties_set_reliability(handle, reliability.toFFI) + transport_services_transport_properties_set_preserve_order(handle, preserveOrder.toFFI) + transport_services_transport_properties_set_preserve_msg_boundaries(handle, preserveMsgBoundaries.toFFI) + transport_services_transport_properties_set_per_msg_reliability(handle, perMessageReliability.toFFI) + transport_services_transport_properties_set_zero_rtt_msg(handle, zeroRttMsg.toFFI) + transport_services_transport_properties_set_multistreaming(handle, multistreaming.toFFI) + transport_services_transport_properties_set_fullchecksum(handle, fullchecksum.toFFI) + transport_services_transport_properties_set_congestion_control(handle, congestionControl.toFFI) + transport_services_transport_properties_set_keep_alive(handle, keepAlive.toFFI) + + transport_services_transport_properties_set_temporary_local_address(handle, useTemporaryLocalAddress.toFFI) + transport_services_transport_properties_set_multipath(handle, multipath.toFFI) + transport_services_transport_properties_set_direction(handle, direction.toFFI) + transport_services_transport_properties_set_retransmit_notify(handle, retransmitNotify.toFFI) + transport_services_transport_properties_set_soft_error_notify(handle, softErrorNotify.toFFI) + + if let pvd = pvd { + pvd.withCString { cString in + transport_services_transport_properties_set_pvd(handle, cString) + } + } + + transport_services_transport_properties_set_expired_dns_allowed(handle, expiredDnsAllowed) + + return handle + } +} + +// MARK: - Security Parameters + +/// Security parameters configuration +public struct SecurityParameters: Sendable { + /// Whether to use TLS + public var useTLS: Bool + + /// Minimum TLS version + public var minimumTLSVersion: TLSVersion? + + /// Server name for SNI + public var serverName: String? + + /// Certificate verification mode + public var verifyMode: CertificateVerificationMode + + /// TLS version enumeration + public enum TLSVersion: String, CaseIterable, Sendable { + case tls10 = "1.0" + case tls11 = "1.1" + case tls12 = "1.2" + case tls13 = "1.3" + } + + /// Certificate verification modes + public enum CertificateVerificationMode: Sendable { + case system // Use system default verification + case disabled // No verification (dangerous!) + case custom(verify: @Sendable (Data) -> Bool) // Custom verification + } + + /// Create default security parameters (no TLS) + public init() { + self.useTLS = false + self.minimumTLSVersion = nil + self.serverName = nil + self.verifyMode = .system + } + + /// Create TLS-enabled security parameters + public static func tls(serverName: String? = nil, minimumVersion: TLSVersion = .tls12) -> SecurityParameters { + var params = SecurityParameters() + params.useTLS = true + params.minimumTLSVersion = minimumVersion + params.serverName = serverName + params.verifyMode = .system + return params + } + + /// Create security parameters with disabled certificate verification (for testing only!) + public static func insecureTLS() -> SecurityParameters { + var params = SecurityParameters() + params.useTLS = true + params.minimumTLSVersion = .tls12 + params.verifyMode = .disabled + return params + } + + /// Convert to FFI handle + func toFFIHandle() -> OpaquePointer? { + let handle = transport_services_security_parameters_new() + + transport_services_security_parameters_set_use_tls(handle, useTLS) + + if let serverName = serverName { + serverName.withCString { cString in + transport_services_security_parameters_set_server_name(handle, cString) + } + } + + // TODO: Implement certificate verification modes and TLS version setting + + return handle + } +} \ No newline at end of file diff --git a/bindings/swift/Sources/TransportServices/Runtime.swift b/bindings/swift/Sources/TransportServices/Runtime.swift new file mode 100644 index 0000000..1b48003 --- /dev/null +++ b/bindings/swift/Sources/TransportServices/Runtime.swift @@ -0,0 +1,59 @@ +#if !hasFeature(Embedded) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) +import Foundation +#endif +#endif +import TransportServicesFFI + +/// Thread-safe runtime manager for Transport Services +/// +/// This actor ensures that runtime initialization and cleanup are handled safely +/// across concurrent contexts. + +actor Runtime { + /// Shared runtime instance + static let shared = Runtime() + + private var isInitialized = false + private var initializationCount = 0 + + private init() {} + + /// Initialize the runtime (can be called multiple times safely) + func initialize() throws { + if !isInitialized { + let result = transport_services_init_runtime() + guard result == 0 else { + throw TransportServicesError.initializationFailed(code: result) + } + isInitialized = true + } + initializationCount += 1 + } + + /// Cleanup the runtime (only actually cleans up when all references are released) + func cleanup() { + guard isInitialized else { return } + + initializationCount -= 1 + if initializationCount <= 0 { + transport_services_shutdown_runtime() + isInitialized = false + initializationCount = 0 + } + } + + /// Check if the runtime is initialized + var initialized: Bool { + isInitialized + } + + /// Ensure runtime is initialized for an operation + func ensureInitialized() throws { + guard isInitialized else { + throw TransportServicesError.runtimeNotInitialized + } + } +} \ No newline at end of file diff --git a/bindings/swift/Sources/TransportServices/TransportServices.swift b/bindings/swift/Sources/TransportServices/TransportServices.swift index 063400e..a152dcb 100644 --- a/bindings/swift/Sources/TransportServices/TransportServices.swift +++ b/bindings/swift/Sources/TransportServices/TransportServices.swift @@ -1,19 +1,25 @@ +#if !hasFeature(Embedded) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) import Foundation +#endif +#endif import TransportServicesFFI /// Swift wrapper for Transport Services (RFC 9622) -public class TransportServices { +/// +/// This enum provides namespace and static methods for Transport Services operations + +public enum TransportServices { /// Initialize the Transport Services runtime public static func initialize() throws { - let result = transport_services_init() - guard result == 0 else { - throw TransportServicesError.initializationFailed(code: result) - } + try Runtime.shared.initialize() } /// Cleanup the Transport Services runtime public static func cleanup() { - transport_services_cleanup() + Runtime.shared.cleanup() } /// Get the version string of the Transport Services library @@ -21,105 +27,13 @@ public class TransportServices { guard let cString = transport_services_version() else { return "Unknown" } - defer { transport_services_free_string(cString) } + defer { transport_services_free_string(UnsafeMutablePointer(mutating: cString)) } return String(cString: cString) } -} - -/// Errors that can occur in Transport Services -public enum TransportServicesError: Error, LocalizedError { - case initializationFailed(code: Int32) - case invalidParameter - case connectionFailed(message: String) - - public var errorDescription: String? { - switch self { - case .initializationFailed(let code): - return "Failed to initialize Transport Services (error code: \(code))" - case .invalidParameter: - return "Invalid parameter provided" - case .connectionFailed(let message): - return "Connection failed: \(message)" - } - } -} - -/// Preconnection represents a set of parameters for establishing a connection -public class Preconnection { - private let handle: OpaquePointer - - /// Create a new preconnection - public init(localEndpoints: [LocalEndpoint] = [], - remoteEndpoints: [RemoteEndpoint] = [], - transportProperties: TransportProperties = TransportProperties(), - securityParameters: SecurityParameters = SecurityParameters()) throws { - - // TODO: Convert Swift endpoints to FFI endpoints - // For now, create a basic preconnection - guard let handle = transport_services_preconnection_new(nil, 0, nil, 0, nil, nil) else { - throw TransportServicesError.invalidParameter - } - self.handle = handle - } - deinit { - transport_services_preconnection_free(handle) + /// Get the last error message from the FFI layer + static func getLastError() -> String? { + guard let errorCString = transport_services_get_last_error() else { return nil } + return String(cString: errorCString) } - - /// Initiate a connection - public func initiate() async throws -> Connection { - // TODO: Implement async wrapper around FFI callback-based initiate - fatalError("Not implemented yet") - } -} - -/// Connection represents an established transport connection -public class Connection { - private let handle: OpaquePointer - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - transport_services_connection_free(handle) - } - - /// Send data on the connection - public func send(_ data: Data) async throws { - // TODO: Implement async wrapper around FFI send - fatalError("Not implemented yet") - } - - /// Receive data from the connection - public func receive() async throws -> Data { - // TODO: Implement async wrapper around FFI receive - fatalError("Not implemented yet") - } - - /// Close the connection gracefully - public func close() async throws { - // TODO: Implement async wrapper around FFI close - fatalError("Not implemented yet") - } -} - -/// Local endpoint for connections -public struct LocalEndpoint { - // TODO: Implement -} - -/// Remote endpoint for connections -public struct RemoteEndpoint { - // TODO: Implement -} - -/// Transport properties configuration -public struct TransportProperties { - // TODO: Implement -} - -/// Security parameters configuration -public struct SecurityParameters { - // TODO: Implement } \ No newline at end of file diff --git a/bindings/swift/Tests/TransportServicesTests/TransportServicesTests.swift b/bindings/swift/Tests/TransportServicesTests/TransportServicesTests.swift index 5d5c528..9367b28 100644 --- a/bindings/swift/Tests/TransportServicesTests/TransportServicesTests.swift +++ b/bindings/swift/Tests/TransportServicesTests/TransportServicesTests.swift @@ -1,5 +1,11 @@ import Testing +#if !hasFeature(Embedded) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) import Foundation +#endif +#endif @testable import TransportServices @Suite("Transport Services Tests") diff --git a/examples/ffi/path_monitor_example.c b/examples/ffi/path_monitor_example.c new file mode 100644 index 0000000..348e144 --- /dev/null +++ b/examples/ffi/path_monitor_example.c @@ -0,0 +1,227 @@ +/** + * Example C program demonstrating the Path Monitor FFI + * + * This example shows how to: + * 1. Create a network path monitor + * 2. List current network interfaces + * 3. Watch for network changes + * 4. Clean up resources + */ + +#include +#include +#include +#include + +// Include the Transport Services header +// In a real application, this would be: #include +// For this example, we'll define the necessary structures and functions + +typedef void* TransportServicesHandle; + +// Interface status enum +typedef enum { + TRANSPORT_SERVICES_INTERFACE_STATUS_UP = 0, + TRANSPORT_SERVICES_INTERFACE_STATUS_DOWN = 1, + TRANSPORT_SERVICES_INTERFACE_STATUS_UNKNOWN = 2 +} TransportServicesInterfaceStatus; + +// Interface structure +typedef struct { + char* name; + uint32_t index; + char** ips; + size_t ip_count; + TransportServicesInterfaceStatus status; + char* interface_type; + int is_expensive; +} TransportServicesInterface; + +// Change event type enum +typedef enum { + TRANSPORT_SERVICES_CHANGE_EVENT_ADDED = 0, + TRANSPORT_SERVICES_CHANGE_EVENT_REMOVED = 1, + TRANSPORT_SERVICES_CHANGE_EVENT_MODIFIED = 2, + TRANSPORT_SERVICES_CHANGE_EVENT_PATH_CHANGED = 3 +} TransportServicesChangeEventType; + +// Change event structure +typedef struct { + TransportServicesChangeEventType event_type; + TransportServicesInterface* interface; + TransportServicesInterface* old_interface; // For Modified events + char* description; // For PathChanged events +} TransportServicesChangeEvent; + +// Function declarations +extern int transport_services_init(void); +extern void transport_services_cleanup(void); +extern const char* transport_services_get_last_error(void); + +extern TransportServicesHandle* transport_services_path_monitor_create(void); +extern void transport_services_path_monitor_destroy(TransportServicesHandle* handle); + +extern int transport_services_path_monitor_list_interfaces( + TransportServicesHandle* handle, + TransportServicesInterface*** interfaces, + size_t* count +); + +extern void transport_services_path_monitor_free_interfaces( + TransportServicesInterface** interfaces, + size_t count +); + +typedef void (*TransportServicesPathMonitorCallback)( + const TransportServicesChangeEvent* event, + void* user_data +); + +extern TransportServicesHandle* transport_services_path_monitor_start_watching( + TransportServicesHandle* handle, + TransportServicesPathMonitorCallback callback, + void* user_data +); + +extern void transport_services_path_monitor_stop_watching( + TransportServicesHandle* handle +); + +// Helper function to print interface information +void print_interface(const TransportServicesInterface* iface) { + printf("Interface: %s (index: %u)\n", iface->name, iface->index); + printf(" Status: %s\n", + iface->status == TRANSPORT_SERVICES_INTERFACE_STATUS_UP ? "UP" : + iface->status == TRANSPORT_SERVICES_INTERFACE_STATUS_DOWN ? "DOWN" : "UNKNOWN"); + printf(" Type: %s\n", iface->interface_type); + printf(" Expensive: %s\n", iface->is_expensive ? "Yes" : "No"); + + if (iface->ip_count > 0) { + printf(" IP Addresses:\n"); + for (size_t i = 0; i < iface->ip_count; i++) { + printf(" - %s\n", iface->ips[i]); + } + } + printf("\n"); +} + +// Callback function for network changes +void network_change_callback(const TransportServicesChangeEvent* event, void* user_data) { + const char* event_name = ""; + + switch (event->event_type) { + case TRANSPORT_SERVICES_CHANGE_EVENT_ADDED: + event_name = "ADDED"; + break; + case TRANSPORT_SERVICES_CHANGE_EVENT_REMOVED: + event_name = "REMOVED"; + break; + case TRANSPORT_SERVICES_CHANGE_EVENT_MODIFIED: + event_name = "MODIFIED"; + break; + case TRANSPORT_SERVICES_CHANGE_EVENT_PATH_CHANGED: + event_name = "PATH_CHANGED"; + break; + } + + printf("=== Network Change Event: %s ===\n", event_name); + + switch (event->event_type) { + case TRANSPORT_SERVICES_CHANGE_EVENT_ADDED: + case TRANSPORT_SERVICES_CHANGE_EVENT_REMOVED: + if (event->interface) { + print_interface(event->interface); + } + break; + + case TRANSPORT_SERVICES_CHANGE_EVENT_MODIFIED: + if (event->old_interface) { + printf("Old interface state:\n"); + print_interface(event->old_interface); + } + if (event->interface) { + printf("New interface state:\n"); + print_interface(event->interface); + } + break; + + case TRANSPORT_SERVICES_CHANGE_EVENT_PATH_CHANGED: + if (event->description) { + printf("Path change: %s\n", event->description); + } + break; + } + + printf("================================\n\n"); +} + +int main(int argc, char* argv[]) { + // Initialize Transport Services + if (transport_services_init() != 0) { + fprintf(stderr, "Failed to initialize Transport Services\n"); + return 1; + } + + // Create a path monitor + TransportServicesHandle* monitor = transport_services_path_monitor_create(); + if (!monitor) { + fprintf(stderr, "Failed to create path monitor: %s\n", + transport_services_get_last_error()); + transport_services_cleanup(); + return 1; + } + + printf("Network Path Monitor Example\n"); + printf("============================\n\n"); + + // List current interfaces + TransportServicesInterface** interfaces = NULL; + size_t interface_count = 0; + + if (transport_services_path_monitor_list_interfaces(monitor, &interfaces, &interface_count) == 0) { + printf("Current network interfaces (%zu found):\n\n", interface_count); + + for (size_t i = 0; i < interface_count; i++) { + print_interface(interfaces[i]); + } + + // Free the interfaces + transport_services_path_monitor_free_interfaces(interfaces, interface_count); + } else { + fprintf(stderr, "Failed to list interfaces: %s\n", + transport_services_get_last_error()); + } + + // Start watching for changes + printf("Starting network change monitoring...\n"); + printf("Try connecting/disconnecting WiFi or changing networks\n"); + printf("Press Ctrl+C to stop\n\n"); + + TransportServicesHandle* watcher = transport_services_path_monitor_start_watching( + monitor, + network_change_callback, + NULL + ); + + if (!watcher) { + fprintf(stderr, "Failed to start watching: %s\n", + transport_services_get_last_error()); + transport_services_path_monitor_destroy(monitor); + transport_services_cleanup(); + return 1; + } + + // Run for 30 seconds + sleep(30); + + // Stop watching + transport_services_path_monitor_stop_watching(watcher); + + // Cleanup + transport_services_path_monitor_destroy(monitor); + transport_services_cleanup(); + + printf("\nMonitoring complete.\n"); + + return 0; +} \ No newline at end of file diff --git a/examples/path_monitor.rs b/examples/path_monitor.rs new file mode 100644 index 0000000..81b2752 --- /dev/null +++ b/examples/path_monitor.rs @@ -0,0 +1,88 @@ +//! Example demonstrating network path monitoring +//! +//! This example shows how to use the NetworkMonitor to: +//! 1. List current network interfaces +//! 2. Monitor for network changes + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use transport_services::path_monitor::{ChangeEvent, NetworkMonitor}; + +fn main() -> Result<(), Box> { + env_logger::init(); + + // Create a network monitor + let monitor = NetworkMonitor::new()?; + + // List current interfaces + println!("Current network interfaces:"); + println!("=========================="); + + let interfaces = monitor.list_interfaces()?; + for interface in interfaces { + println!("\nInterface: {}", interface.name); + println!(" Index: {}", interface.index); + println!(" Type: {}", interface.interface_type); + println!(" Status: {:?}", interface.status); + println!(" Expensive: {}", interface.is_expensive); + println!(" IP Addresses:"); + for ip in &interface.ips { + println!(" - {}", ip); + } + } + + println!("\n\nMonitoring for network changes (press Ctrl+C to stop)..."); + println!("========================================================="); + + // Set up monitoring + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + // Handle Ctrl+C + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + })?; + + // Start monitoring for changes + let _handle = monitor.watch_changes(|event| match event { + ChangeEvent::Added(interface) => { + println!( + "\n[ADDED] Interface: {} ({})", + interface.name, interface.interface_type + ); + for ip in &interface.ips { + println!(" - IP: {}", ip); + } + } + ChangeEvent::Removed(interface) => { + println!( + "\n[REMOVED] Interface: {} ({})", + interface.name, interface.interface_type + ); + } + ChangeEvent::Modified { old, new } => { + println!("\n[MODIFIED] Interface: {}", new.name); + if old.status != new.status { + println!(" - Status: {:?} -> {:?}", old.status, new.status); + } + if old.ips != new.ips { + println!(" - IPs changed"); + println!(" Old: {:?}", old.ips); + println!(" New: {:?}", new.ips); + } + } + ChangeEvent::PathChanged { description } => { + println!("\n[PATH CHANGE] {}", description); + } + }); + + // Keep running until interrupted + while running.load(Ordering::SeqCst) { + thread::sleep(Duration::from_millis(100)); + } + + println!("\nStopping monitor..."); + Ok(()) +} diff --git a/examples/path_monitor_detailed.rs b/examples/path_monitor_detailed.rs new file mode 100644 index 0000000..b7ac023 --- /dev/null +++ b/examples/path_monitor_detailed.rs @@ -0,0 +1,105 @@ +//! Detailed example demonstrating network path monitoring +//! +//! This example shows: +//! - Detecting expensive (metered) interfaces +//! - Interface indices +//! - Detailed interface information + +use std::net::IpAddr; +use transport_services::path_monitor::{NetworkMonitor, Status}; + +fn main() -> Result<(), Box> { + env_logger::init(); + + // Create a network monitor + let monitor = NetworkMonitor::new()?; + + println!("Network Interface Details"); + println!("========================\n"); + + // List current interfaces with detailed information + let interfaces = monitor.list_interfaces()?; + + // Group interfaces by type + let mut by_type = std::collections::HashMap::>::new(); + for interface in &interfaces { + by_type + .entry(interface.interface_type.clone()) + .or_default() + .push(interface); + } + + // Display interfaces grouped by type + for (iface_type, ifaces) in by_type { + println!("## {} Interfaces", iface_type.to_uppercase()); + println!(); + + for interface in ifaces { + println!("Interface: {} (index: {})", interface.name, interface.index); + println!(" Status: {:?}", interface.status); + println!( + " Expensive: {}", + if interface.is_expensive { + "Yes ⚠️" + } else { + "No ✓" + } + ); + + if !interface.ips.is_empty() { + println!(" IP Addresses:"); + for ip in &interface.ips { + match ip { + IpAddr::V4(v4) => println!(" - IPv4: {}", v4), + IpAddr::V6(v6) => { + // Skip link-local IPv6 for brevity + if !v6.segments()[0] == 0xfe80 { + println!(" - IPv6: {}", v6); + } + } + } + } + } + println!(); + } + } + + // Summary statistics + println!("## Summary"); + println!(); + + let active_count = interfaces.iter().filter(|i| i.status == Status::Up).count(); + + let expensive_count = interfaces + .iter() + .filter(|i| i.is_expensive && i.status == Status::Up) + .count(); + + println!("Total interfaces: {}", interfaces.len()); + println!("Active interfaces: {}", active_count); + println!( + "Expensive interfaces: {} {}", + expensive_count, + if expensive_count > 0 { "⚠️" } else { "" } + ); + + // Find preferred interface for internet connectivity + let preferred = interfaces + .iter() + .filter(|i| { + i.status == Status::Up + && !i.is_expensive + && !i.ips.is_empty() + && (i.interface_type == "wifi" || i.interface_type == "ethernet") + }) + .next(); + + if let Some(pref) = preferred { + println!( + "\nPreferred interface for internet: {} ({})", + pref.name, pref.interface_type + ); + } + + Ok(()) +} diff --git a/examples/path_monitor_watch.rs b/examples/path_monitor_watch.rs new file mode 100644 index 0000000..8165fbc --- /dev/null +++ b/examples/path_monitor_watch.rs @@ -0,0 +1,60 @@ +//! Example that demonstrates watching for network path changes +//! +//! This example starts monitoring network changes and reports them in real-time + +use std::thread; +use std::time::Duration; +use transport_services::path_monitor::{ChangeEvent, NetworkMonitor}; + +fn main() -> Result<(), Box> { + env_logger::init(); + + println!("Starting network path monitor..."); + println!("Try disconnecting/connecting WiFi or changing networks to see events"); + println!("Running for 30 seconds...\n"); + + // Create a network monitor + let monitor = NetworkMonitor::new()?; + + // Start watching for changes + let _handle = monitor.watch_changes(|event| { + println!( + "[{}] Network event: {:?}", + chrono::Local::now().format("%H:%M:%S"), + event + ); + + match event { + ChangeEvent::PathChanged { description } => { + println!(" → {}", description); + } + ChangeEvent::Added(interface) => { + println!( + " → Interface {} added (type: {}, expensive: {})", + interface.name, + interface.interface_type, + if interface.is_expensive { "yes" } else { "no" } + ); + } + ChangeEvent::Removed(interface) => { + println!(" → Interface {} removed", interface.name); + } + ChangeEvent::Modified { old, new } => { + println!(" → Interface {} modified:", new.name); + if old.status != new.status { + println!(" Status: {:?} → {:?}", old.status, new.status); + } + if old.ips != new.ips { + println!(" IPs changed"); + } + } + } + println!(); + }); + + // Keep the program running for 30 seconds + thread::sleep(Duration::from_secs(30)); + + println!("Monitoring complete."); + Ok(()) +} diff --git a/examples/test_windows_path_monitor.rs b/examples/test_windows_path_monitor.rs new file mode 100644 index 0000000..dccaad1 --- /dev/null +++ b/examples/test_windows_path_monitor.rs @@ -0,0 +1,57 @@ +//! Test example for Windows path monitoring +//! +//! This example tests the Windows path monitor implementation + +use transport_services::path_monitor::{NetworkMonitor, ChangeEvent}; +use std::time::Duration; +use std::thread; + +fn main() -> Result<(), Box> { + // Initialize logging + env_logger::init(); + + println!("Creating network monitor..."); + let monitor = NetworkMonitor::new()?; + + // List current interfaces + println!("\nCurrent network interfaces:"); + let interfaces = monitor.list_interfaces()?; + for interface in &interfaces { + println!("- {} (index: {})", interface.name, interface.index); + println!(" Type: {}", interface.interface_type); + println!(" Status: {:?}", interface.status); + println!(" IPs: {:?}", interface.ips); + println!(" Expensive: {}", interface.is_expensive); + } + + // Start monitoring for changes + println!("\nStarting network change monitoring..."); + let _handle = monitor.watch_changes(|event| { + match event { + ChangeEvent::Added(interface) => { + println!("Interface added: {} ({})", interface.name, interface.interface_type); + } + ChangeEvent::Removed(interface) => { + println!("Interface removed: {} ({})", interface.name, interface.interface_type); + } + ChangeEvent::Modified { old, new } => { + println!("Interface modified: {}", new.name); + if old.status != new.status { + println!(" Status changed: {:?} -> {:?}", old.status, new.status); + } + if old.ips != new.ips { + println!(" IPs changed: {:?} -> {:?}", old.ips, new.ips); + } + } + ChangeEvent::PathChanged { description } => { + println!("Path changed: {}", description); + } + } + }); + + println!("Monitoring for 30 seconds... (disable/enable network adapters to see changes)"); + thread::sleep(Duration::from_secs(30)); + + println!("\nStopping monitor..."); + Ok(()) +} \ No newline at end of file diff --git a/run-linux-tests.sh b/run-linux-tests.sh deleted file mode 100644 index da821b0..0000000 --- a/run-linux-tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# Run tests in Linux Docker container -docker run --rm -v "$(pwd):/project" -w /project rust:latest cargo test \ No newline at end of file diff --git a/scripts/build-artifact-bundle.ps1 b/scripts/build-artifact-bundle.ps1 deleted file mode 100644 index 3dcbf3b..0000000 --- a/scripts/build-artifact-bundle.ps1 +++ /dev/null @@ -1,299 +0,0 @@ -# Build script for creating Transport Services artifact bundle on Windows -# Supports cross-compilation for multiple platforms - -param( - [string]$Target = "all" -) - -$ErrorActionPreference = "Stop" - -# Configuration -$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path -$PROJECT_ROOT = Split-Path -Parent $SCRIPT_DIR -$BUILD_DIR = Join-Path $PROJECT_ROOT "build" -$ARTIFACT_BUNDLE_DIR = Join-Path $BUILD_DIR "transport_services.artifactbundle" -$ARTIFACT_NAME = "transport_services" -$VERSION = "0.1.0" - -# Target platforms and architectures -$TARGETS = @{ - "ios-arm64" = "aarch64-apple-ios" - "macos-arm64" = "aarch64-apple-darwin" - "macos-x86_64" = "x86_64-apple-darwin" - "android-arm64" = "aarch64-linux-android" - "linux-x86_64" = "x86_64-unknown-linux-gnu" - "linux-arm64" = "aarch64-unknown-linux-gnu" - "windows-x86_64" = "x86_64-pc-windows-msvc" - "windows-arm64" = "aarch64-pc-windows-msvc" -} - -# Initialize build environment -function Init-Build { - Write-Host "Initializing build environment..." - if (Test-Path $BUILD_DIR) { - Remove-Item -Recurse -Force $BUILD_DIR - } - New-Item -ItemType Directory -Path $BUILD_DIR | Out-Null - New-Item -ItemType Directory -Path $ARTIFACT_BUNDLE_DIR | Out-Null -} - -# Install cbindgen if needed -function Install-Cbindgen { - Write-Host "Checking cbindgen installation..." - $cbindgen = Get-Command cbindgen -ErrorAction SilentlyContinue - if (-not $cbindgen) { - Write-Host "Installing cbindgen..." - cargo install cbindgen - } -} - -# Generate C headers using cbindgen -function Generate-Headers { - Write-Host "Generating C headers..." - - Push-Location $PROJECT_ROOT - try { - cbindgen --config cbindgen.toml --crate transport_services --output "$BUILD_DIR/transport_services.h" - - # Generate module map - $moduleMap = @" -module TransportServices { - header "transport_services.h" - export * -} -"@ - $moduleMap | Out-File -FilePath "$BUILD_DIR/module.modulemap" -Encoding UTF8 - } - finally { - Pop-Location - } -} - -# Build static library for a specific target -function Build-Target { - param( - [string]$Platform, - [string]$RustTarget - ) - - $variantDir = Join-Path $ARTIFACT_BUNDLE_DIR "$ARTIFACT_NAME/$Platform" - - Write-Host "Building for $Platform ($RustTarget)..." - - New-Item -ItemType Directory -Path "$variantDir/lib" -Force | Out-Null - New-Item -ItemType Directory -Path "$variantDir/include" -Force | Out-Null - - Push-Location $PROJECT_ROOT - try { - # Build the static library - cargo build --release --target $RustTarget --features ffi --no-default-features - - # Copy the built library - $libName = switch ($Platform) { - {$_ -match "windows-"} { "transport_services.lib" } - default { "libtransport_services.a" } - } - - $sourcePath = Join-Path "target/$RustTarget/release" $libName - $destPath = Join-Path "$variantDir/lib" $libName - - if (Test-Path $sourcePath) { - Copy-Item $sourcePath $destPath - } else { - # Try alternative name for Windows - $altSourcePath = Join-Path "target/$RustTarget/release" "libtransport_services.a" - if (Test-Path $altSourcePath) { - Copy-Item $altSourcePath $destPath - } else { - Write-Warning "Library not found for $Platform" - return - } - } - - # Copy headers - Copy-Item "$BUILD_DIR/transport_services.h" "$variantDir/include/" - Copy-Item "$BUILD_DIR/module.modulemap" "$variantDir/include/" - } - finally { - Pop-Location - } -} - -# Create artifact bundle manifest -function Create-Manifest { - Write-Host "Creating artifact bundle manifest..." - - $variants = @() - - foreach ($platform in $TARGETS.Keys) { - $rustTarget = $TARGETS[$platform] - $libPath = if ($platform -match "windows-") { - "$ARTIFACT_NAME/$platform/lib/transport_services.lib" - } else { - "$ARTIFACT_NAME/$platform/lib/libtransport_services.a" - } - - $variantDir = Join-Path $ARTIFACT_BUNDLE_DIR "$ARTIFACT_NAME/$platform" - if (Test-Path "$variantDir/lib") { - $variants += @{ - path = $libPath - supportedTriples = @($rustTarget) - staticLibraryMetadata = @{ - headerPaths = @("$ARTIFACT_NAME/$platform/include") - moduleMapPath = "$ARTIFACT_NAME/$platform/include/module.modulemap" - } - } - } - } - - $manifest = @{ - schemaVersion = "1.0" - artifacts = @{ - $ARTIFACT_NAME = @{ - version = $VERSION - type = "staticLibrary" - variants = $variants - } - } - } - - $manifest | ConvertTo-Json -Depth 10 | Out-File -FilePath "$ARTIFACT_BUNDLE_DIR/info.json" -Encoding UTF8 -} - -# Create bundle groups -function Create-BundleGroups { - Write-Host "Creating bundle groups..." - - $bundleGroups = @{ - "apple" = @("ios-arm64", "macos-arm64", "macos-x86_64") - "android" = @("android-arm64") - "linux" = @("linux-x86_64", "linux-arm64") - "windows" = @("windows-x86_64", "windows-arm64") - } - - $bundles = @() - - foreach ($groupName in $bundleGroups.Keys) { - $platforms = $bundleGroups[$groupName] - $zipName = "transport_services-$groupName.zip" - $groupBundleDir = Join-Path $BUILD_DIR "transport_services-$groupName.artifactbundle" - - # Create group bundle directory - New-Item -ItemType Directory -Path $groupBundleDir -Force | Out-Null - - $groupVariants = @() - $supportedTriples = @() - - foreach ($platform in $platforms) { - $variantSrcDir = Join-Path $ARTIFACT_BUNDLE_DIR "$ARTIFACT_NAME/$platform" - if (Test-Path $variantSrcDir) { - $variantDestDir = Join-Path $groupBundleDir "$ARTIFACT_NAME/$platform" - New-Item -ItemType Directory -Path (Split-Path $variantDestDir -Parent) -Force | Out-Null - Copy-Item -Path $variantSrcDir -Destination $variantDestDir -Recurse -Force - - $rustTarget = $TARGETS[$platform] - $supportedTriples += $rustTarget - - $libPath = if ($platform -match "windows-") { - "$ARTIFACT_NAME/$platform/lib/transport_services.lib" - } else { - "$ARTIFACT_NAME/$platform/lib/libtransport_services.a" - } - - $groupVariants += @{ - path = $libPath - supportedTriples = @($rustTarget) - staticLibraryMetadata = @{ - headerPaths = @("$ARTIFACT_NAME/$platform/include") - moduleMapPath = "$ARTIFACT_NAME/$platform/include/module.modulemap" - } - } - } - } - - if ($groupVariants.Count -gt 0) { - # Create manifest for this group - $groupManifest = @{ - schemaVersion = "1.0" - artifacts = @{ - $ARTIFACT_NAME = @{ - version = $VERSION - type = "staticLibrary" - variants = $groupVariants - } - } - } - - $groupManifest | ConvertTo-Json -Depth 10 | Out-File -FilePath "$groupBundleDir/info.json" -Encoding UTF8 - - # Create zip file - Push-Location $BUILD_DIR - try { - Compress-Archive -Path (Split-Path $groupBundleDir -Leaf) -DestinationPath $zipName -Force - - # Calculate checksum - $hash = Get-FileHash -Path $zipName -Algorithm SHA256 - $checksum = $hash.Hash.ToLower() - - $bundles += @{ - fileName = $zipName - checksum = $checksum - supportedTriples = $supportedTriples - } - } - finally { - Pop-Location - } - } - } - - # Create bundle index - $bundleIndex = @{ - schemaVersion = "1.0" - bundles = $bundles - } - - $bundleIndex | ConvertTo-Json -Depth 10 | Out-File -FilePath "$BUILD_DIR/transport_services.artifactbundleindex" -Encoding UTF8 -} - -# Main build process -function Main { - Write-Host "Building Transport Services artifact bundle..." - - Init-Build - Install-Cbindgen - Generate-Headers - - # Build targets based on parameter - if ($Target -eq "all") { - foreach ($platform in $TARGETS.Keys) { - Build-Target -Platform $platform -RustTarget $TARGETS[$platform] - } - } else { - if ($TARGETS.ContainsKey($Target)) { - Build-Target -Platform $Target -RustTarget $TARGETS[$Target] - } else { - Write-Error "Unknown target: $Target" - return - } - } - - Create-Manifest - Create-BundleGroups - - # Create final zip of complete bundle - Push-Location $BUILD_DIR - try { - Compress-Archive -Path "transport_services.artifactbundle" -DestinationPath "transport_services-all.zip" -Force - } - finally { - Pop-Location - } - - Write-Host "Build complete! Artifacts available in $BUILD_DIR" - Write-Host "- Complete bundle: transport_services-all.zip" - Write-Host "- Split bundles with index: transport_services.artifactbundleindex" -} - -# Run main -Main \ No newline at end of file diff --git a/scripts/build-artifact-bundle.sh b/scripts/build-artifact-bundle.sh old mode 100644 new mode 100755 index b0c8eff..9e6c131 --- a/scripts/build-artifact-bundle.sh +++ b/scripts/build-artifact-bundle.sh @@ -1,7 +1,8 @@ -#!/bin/bash +#!/usr/bin/env bash # Build script for creating Transport Services artifact bundle -# Supports all target platforms: iOS, tvOS, macOS, watchOS, Android ARM64 Only, Linux x86_64 and ARM64, Windows x86_64 and ARM64 +# Supports all target platforms: iOS, tvOS, macOS, watchOS, visionOS (devices and simulators for Apple Silicon), +# Android ARM64, Linux x86_64 and ARM64, Windows x86_64 and ARM64 set -euo pipefail @@ -10,23 +11,66 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" BUILD_DIR="$PROJECT_ROOT/build" ARTIFACT_BUNDLE_DIR="$BUILD_DIR/transport_services.artifactbundle" -ARTIFACT_NAME="transport_services" +ARTIFACT_NAME="TransportServicesFFI" VERSION="0.1.0" # Target platforms and architectures -declare -A TARGETS=( - ["ios-arm64"]="aarch64-apple-ios" - ["tvos-arm64"]="aarch64-apple-tvos" - ["macos-arm64"]="aarch64-apple-darwin" - ["macos-x86_64"]="x86_64-apple-darwin" - ["watchos-arm64"]="aarch64-apple-watchos" - ["android-arm64"]="aarch64-linux-android" - ["linux-x86_64"]="x86_64-unknown-linux-gnu" - ["linux-arm64"]="aarch64-unknown-linux-gnu" - ["windows-x86_64"]="x86_64-pc-windows-msvc" - ["windows-arm64"]="aarch64-pc-windows-msvc" +# Note: tvOS, watchOS, and visionOS targets are not available in stable Rust +PLATFORMS=( + # Apple device targets + "ios-arm64" # iPhone/iPad + "macos-arm64" # Apple Silicon Mac + # "tvos-arm64" # Apple TV - NOT AVAILABLE IN RUST + # "watchos-arm64" # Apple Watch - NOT AVAILABLE IN RUST + # "visionos-arm64" # Vision Pro - NOT AVAILABLE IN RUST + + # Apple simulator targets + "ios-sim-arm64" # iOS Simulator on Apple Silicon + + # Android targets + "android-arm64" # Android ARM64 + + # Linux targets + "linux-x86_64" # Linux x86_64 + "linux-arm64" # Linux ARM64 + + # Windows targets + "windows-x86_64" # Windows 11 x86_64 + # "windows-arm64" # Windows 11 ARM64 - NOT SUPPORTED WITH MINGW ) +RUST_TARGETS=( + # Apple device targets + "aarch64-apple-ios" + "aarch64-apple-darwin" + + # Apple simulator targets + "aarch64-apple-ios-sim" + + # Android targets + "aarch64-linux-android" + + # Linux targets + "x86_64-unknown-linux-gnu" + "aarch64-unknown-linux-gnu" + + # Windows targets + "x86_64-pc-windows-gnu" + # "aarch64-pc-windows-gnu" # NOT SUPPORTED +) + +# Function to get rust target for a platform +get_rust_target() { + local platform=$1 + for i in "${!PLATFORMS[@]}"; do + if [[ "${PLATFORMS[$i]}" == "$platform" ]]; then + echo "${RUST_TARGETS[$i]}" + return + fi + done + echo "" +} + # Initialize build environment init_build() { echo "Initializing build environment..." @@ -38,7 +82,7 @@ init_build() { # Install required Rust targets install_rust_targets() { echo "Installing Rust targets..." - for target in "${TARGETS[@]}"; do + for target in "${RUST_TARGETS[@]}"; do rustup target add "$target" || true done } @@ -58,7 +102,7 @@ generate_headers() { # Generate module map cat > "$BUILD_DIR/module.modulemap" << EOF -module TransportServices { +module TransportServicesFFI { header "transport_services.h" export * } @@ -69,6 +113,7 @@ EOF build_target() { local platform=$1 local rust_target=$2 + local original_rust_target=$rust_target local variant_dir="$ARTIFACT_BUNDLE_DIR/$ARTIFACT_NAME/$platform" echo "Building for $platform ($rust_target)..." @@ -76,15 +121,39 @@ build_target() { mkdir -p "$variant_dir/lib" mkdir -p "$variant_dir/include" + # Clear potentially conflicting environment variables + unset CC + unset CXX + unset AR + unset ANDROID_NDK_ROOT + unset ANDROID_NDK + # Set up cross-compilation environment case "$platform" in - ios-*|tvos-*|macos-*|watchos-*) + ios-*|tvos-*|macos-*|watchos-*|visionos-*) # Apple platforms - use default toolchain ;; android-*) - # Android requires NDK - export CC="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang" - export AR="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" + # Android requires NDK - skip if not available + if [ -z "${ANDROID_NDK_HOME:-}" ]; then + echo "Skipping Android build - ANDROID_NDK_HOME not set" + rm -rf "$variant_dir" + return + fi + # Set NDK environment variables that aws-lc-sys expects + export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME" + export ANDROID_NDK="$ANDROID_NDK_HOME" # aws-lc-sys needs this + export CC="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android30-clang" + export AR="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" + export CXX="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android30-clang++" + # Point CMAKE to the actual executable as per issue #819 + export CMAKE="/opt/homebrew/bin/cmake" + # Set CMake toolchain file for Android + export CMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" + export CMAKE_TOOLCHAIN_FILE_aarch64_linux_android="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" + # Set Android-specific CMake variables + export ANDROID_ABI="arm64-v8a" + export ANDROID_PLATFORM="android-30" ;; linux-*) # Linux cross-compilation @@ -94,15 +163,39 @@ build_target() { fi ;; windows-*) - # Windows cross-compilation from Linux - export CC="x86_64-w64-mingw32-gcc" - export AR="x86_64-w64-mingw32-ar" + # Windows cross-compilation from macOS using MinGW + if command -v x86_64-w64-mingw32-gcc &> /dev/null; then + export CC="x86_64-w64-mingw32-gcc" + export AR="x86_64-w64-mingw32-ar" + export CXX="x86_64-w64-mingw32-g++" + else + echo "MinGW x86_64 compiler not found - skipping Windows build" + rm -rf "$variant_dir" + return + fi ;; esac - # Build the static library + # Build the static library in a subshell to isolate environment cd "$PROJECT_ROOT" - cargo build --release --target "$rust_target" --features ffi + if [[ "$platform" == android-* ]]; then + # Direct Android build without cargo-ndk to avoid CMake issues + if ! ( + cargo build --release --target "$rust_target" --features ffi + ); then + echo "Failed to build for $platform - skipping" + rm -rf "$variant_dir" + return + fi + else + if ! ( + cargo build --release --target "$rust_target" --features ffi + ); then + echo "Failed to build for $platform - skipping" + rm -rf "$variant_dir" + return + fi + fi # Copy the built library local lib_name @@ -129,8 +222,16 @@ create_manifest() { local variants_json="" - for platform in "${!TARGETS[@]}"; do - local rust_target="${TARGETS[$platform]}" + for i in "${!PLATFORMS[@]}"; do + local platform="${PLATFORMS[$i]}" + local rust_target="$(get_rust_target "$platform")" + local variant_dir="$ARTIFACT_BUNDLE_DIR/$ARTIFACT_NAME/$platform" + + # Skip if the variant wasn't built + if [ ! -d "$variant_dir" ]; then + continue + fi + local lib_path case "$platform" in @@ -178,7 +279,7 @@ create_bundle_index() { local bundles_json="" local bundle_groups=( - "apple:ios-arm64,tvos-arm64,macos-arm64,macos-x86_64,watchos-arm64" + "apple:ios-arm64,tvos-arm64,macos-arm64,watchos-arm64,visionos-arm64,ios-sim-arm64,tvos-sim-arm64,watchos-sim-arm64,visionos-sim-arm64" "android:android-arm64" "linux:linux-x86_64,linux-arm64" "windows:windows-x86_64,windows-arm64" @@ -202,7 +303,7 @@ create_bundle_index() { mkdir -p "$group_bundle_dir/$ARTIFACT_NAME" cp -r "$ARTIFACT_BUNDLE_DIR/$ARTIFACT_NAME/$platform" "$group_bundle_dir/$ARTIFACT_NAME/" - local rust_target="${TARGETS[$platform]}" + local rust_target="$(get_rust_target "$platform")" local lib_path case "$platform" in @@ -258,7 +359,7 @@ EOF if [ -n "$supported_triples" ]; then supported_triples+=", " fi - supported_triples+="\"${TARGETS[$platform]}\"" + supported_triples+="\"$(get_rust_target "$platform")\"" done if [ -n "$bundles_json" ]; then @@ -283,19 +384,74 @@ EOF EOF } +# Parse command line arguments +parse_args() { + SELECTED_PLATFORMS=() + while [[ $# -gt 0 ]]; do + case $1 in + -p|--platform) + SELECTED_PLATFORMS+=("$2") + shift 2 + ;; + -h|--help) + echo "Usage: $0 [-p|--platform PLATFORM] ..." + echo "Available platforms:" + for platform in "${PLATFORMS[@]}"; do + echo " $platform" + done + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use -h or --help for usage information" + exit 1 + ;; + esac + done + + # If no platforms specified, build all + if [ ${#SELECTED_PLATFORMS[@]} -eq 0 ]; then + SELECTED_PLATFORMS=("${PLATFORMS[@]}") + fi +} + # Main build process main() { + parse_args "$@" + echo "Building Transport Services artifact bundle..." + if [ ${#SELECTED_PLATFORMS[@]} -ne ${#PLATFORMS[@]} ]; then + echo "Building selected platforms: ${SELECTED_PLATFORMS[*]}" + fi init_build install_rust_targets generate_headers - # Build all targets - for platform in "${!TARGETS[@]}"; do - build_target "$platform" "${TARGETS[$platform]}" + # Track successful builds + local successful_builds=0 + + # Build selected targets + for platform in "${SELECTED_PLATFORMS[@]}"; do + local rust_target="$(get_rust_target "$platform")" + if [ -z "$rust_target" ]; then + echo "Error: Unknown platform '$platform'" + continue + fi + build_target "$platform" "$rust_target" + if [ -d "$ARTIFACT_BUNDLE_DIR/$ARTIFACT_NAME/$platform" ]; then + ((successful_builds++)) + fi done + if [ $successful_builds -eq 0 ]; then + echo "ERROR: No targets were successfully built!" + exit 1 + fi + + echo "" + echo "Successfully built $successful_builds out of ${#SELECTED_PLATFORMS[@]} selected targets" + create_manifest create_bundle_index diff --git a/scripts/build-multi-platform.ps1 b/scripts/build-multi-platform.ps1 deleted file mode 100644 index 314df02..0000000 --- a/scripts/build-multi-platform.ps1 +++ /dev/null @@ -1,235 +0,0 @@ -# PowerShell script to build Transport Services for multiple platforms -# Builds Windows targets locally and Linux/Android targets in Docker - -param( - [string]$BuildDir = "build", - [switch]$SkipDocker, - [switch]$AppleTargets -) - -$ErrorActionPreference = "Stop" - -# Configuration -$ProjectRoot = Split-Path -Parent $PSScriptRoot -$ArtifactBundleDir = Join-Path $BuildDir "transport_services.artifactbundle" -$ArtifactName = "transport_services" -$Version = "0.1.0" - -# Target configurations -$WindowsTargets = @{ - "windows-x86_64" = "x86_64-pc-windows-msvc" - "windows-arm64" = "aarch64-pc-windows-msvc" -} - -$LinuxTargets = @{ - "linux-x86_64" = "x86_64-unknown-linux-gnu" - "linux-arm64" = "aarch64-unknown-linux-gnu" - "android-arm64" = "aarch64-linux-android" -} - -$AppleTargetsMap = @{ - "ios-arm64" = "aarch64-apple-ios" - "tvos-arm64" = "aarch64-apple-tvos" - "macos-arm64" = "aarch64-apple-darwin" - "macos-x86_64" = "x86_64-apple-darwin" - "watchos-arm64" = "aarch64-apple-watchos" -} - -function Initialize-Build { - Write-Host "Initializing build environment..." -ForegroundColor Green - - if (Test-Path $BuildDir) { - Remove-Item -Path $BuildDir -Recurse -Force - } - - New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null - New-Item -ItemType Directory -Path $ArtifactBundleDir -Force | Out-Null -} - -function Install-Dependencies { - Write-Host "Checking dependencies..." -ForegroundColor Green - - # Check if Rust is installed - if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { - Write-Error "Rust is not installed. Please install from https://rustup.rs/" - exit 1 - } - - # Install cbindgen if not present - if (-not (Get-Command cbindgen -ErrorAction SilentlyContinue)) { - Write-Host "Installing cbindgen..." - cargo install cbindgen - } - - # Add Windows targets - Write-Host "Adding Rust targets..." - foreach ($target in $WindowsTargets.Values) { - rustup target add $target - } -} - -function Generate-Headers { - Write-Host "Generating C headers..." -ForegroundColor Green - - Push-Location $ProjectRoot - try { - # Generate header file - cbindgen --config cbindgen.toml --crate transport_services --output "$BuildDir\transport_services.h" - - # Generate module map - @" -module TransportServices { - header "transport_services.h" - export * -} -"@ | Out-File -FilePath "$BuildDir\module.modulemap" -Encoding UTF8 - } - finally { - Pop-Location - } -} - -function Build-WindowsTarget { - param( - [string]$Platform, - [string]$RustTarget - ) - - Write-Host "Building for $Platform ($RustTarget)..." -ForegroundColor Yellow - - $variantDir = Join-Path $ArtifactBundleDir "$ArtifactName\$Platform" - New-Item -ItemType Directory -Path "$variantDir\lib" -Force | Out-Null - New-Item -ItemType Directory -Path "$variantDir\include" -Force | Out-Null - - Push-Location $ProjectRoot - try { - # Build the static library - cargo build --release --target $RustTarget --features ffi --no-default-features - - # Copy the built library - $sourcePath = "target\$RustTarget\release\transport_services.lib" - if (-not (Test-Path $sourcePath)) { - $sourcePath = "target\$RustTarget\release\libtransport_services.a" - } - - Copy-Item -Path $sourcePath -Destination "$variantDir\lib\transport_services.lib" - - # Copy headers - Copy-Item -Path "$BuildDir\transport_services.h" -Destination "$variantDir\include\" - Copy-Item -Path "$BuildDir\module.modulemap" -Destination "$variantDir\include\" - } - finally { - Pop-Location - } -} - -function Build-DockerTargets { - Write-Host "Building Linux/Android targets in Docker..." -ForegroundColor Green - - Push-Location $ProjectRoot - try { - # Build Docker image - docker build -f Dockerfile.build -t transport-services-builder . - - # Run build in Docker - docker run --rm ` - -v "${ProjectRoot}:/workspace" ` - -e BUILD_TARGETS="linux-x86_64,linux-arm64,android-arm64" ` - transport-services-builder - } - finally { - Pop-Location - } -} - -function Create-Manifest { - param( - [hashtable]$Targets - ) - - Write-Host "Creating artifact bundle manifest..." -ForegroundColor Green - - $variants = @() - - foreach ($platform in $Targets.Keys) { - $rustTarget = $Targets[$platform] - $libPath = if ($platform -like "windows-*") { - "$ArtifactName/$platform/lib/transport_services.lib" - } else { - "$ArtifactName/$platform/lib/libtransport_services.a" - } - - $variant = @{ - path = $libPath - supportedTriples = @($rustTarget) - staticLibraryMetadata = @{ - headerPaths = @("$ArtifactName/$platform/include") - moduleMapPath = "$ArtifactName/$platform/include/module.modulemap" - } - } - - $variants += $variant - } - - $manifest = @{ - schemaVersion = "1.0" - artifacts = @{ - $ArtifactName = @{ - version = $Version - type = "staticLibrary" - variants = $variants - } - } - } - - $manifest | ConvertTo-Json -Depth 10 | Out-File -FilePath "$ArtifactBundleDir\info.json" -Encoding UTF8 -} - -function Create-Bundle { - Write-Host "Creating artifact bundle..." -ForegroundColor Green - - Push-Location $BuildDir - try { - # Create zip file - Compress-Archive -Path "transport_services.artifactbundle" -DestinationPath "transport_services-all.zip" - - Write-Host "Build complete! Artifact bundle created at: $BuildDir\transport_services-all.zip" -ForegroundColor Green - } - finally { - Pop-Location - } -} - -# Main execution -Initialize-Build -Install-Dependencies -Generate-Headers - -# Build Windows targets locally -foreach ($platform in $WindowsTargets.Keys) { - Build-WindowsTarget -Platform $platform -RustTarget $WindowsTargets[$platform] -} - -# Build Linux/Android targets in Docker (if not skipped) -if (-not $SkipDocker) { - Build-DockerTargets -} - -# Include Apple targets if requested (requires macOS) -$allTargets = $WindowsTargets.Clone() -if (-not $SkipDocker) { - foreach ($key in $LinuxTargets.Keys) { - $allTargets[$key] = $LinuxTargets[$key] - } -} -if ($AppleTargets) { - foreach ($key in $AppleTargetsMap.Keys) { - $allTargets[$key] = $AppleTargetsMap[$key] - } -} - -Create-Manifest -Targets $allTargets -Create-Bundle - -Write-Host "`nArtifact bundle created successfully!" -ForegroundColor Green -Write-Host "Location: $BuildDir\transport_services-all.zip" -ForegroundColor Cyan \ No newline at end of file diff --git a/scripts/install-cross-tools.sh b/scripts/install-cross-tools.sh new file mode 100755 index 0000000..97179da --- /dev/null +++ b/scripts/install-cross-tools.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash + +# Script to install cross-compilation tools for building Transport Services on all platforms +# Supports macOS host only (for now) + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Helper functions +print_success() { + echo -e "${GREEN}✓${NC} $1" +} + +print_error() { + echo -e "${RED}✗${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}!${NC} $1" +} + +print_info() { + echo -e "ℹ️ $1" +} + +# Check if running on macOS +check_macos() { + if [[ "$OSTYPE" != "darwin"* ]]; then + print_error "This script currently only supports macOS as the host system" + exit 1 + fi +} + +# Check if Homebrew is installed +check_homebrew() { + if ! command -v brew &> /dev/null; then + print_error "Homebrew is not installed" + print_info "Install Homebrew from https://brew.sh" + exit 1 + fi + print_success "Homebrew is installed" +} + +# Install Rust targets +install_rust_targets() { + print_info "Installing Rust targets..." + + local targets=( + # Apple targets + "aarch64-apple-ios" + "aarch64-apple-ios-sim" + "aarch64-apple-darwin" + + # Linux targets + "x86_64-unknown-linux-gnu" + "aarch64-unknown-linux-gnu" + + # Windows targets + "x86_64-pc-windows-msvc" + "aarch64-pc-windows-msvc" + + # Android target + "aarch64-linux-android" + ) + + for target in "${targets[@]}"; do + if rustup target list --installed | grep -q "^$target"; then + print_success "Rust target $target already installed" + else + print_info "Installing Rust target $target..." + if rustup target add "$target"; then + print_success "Installed Rust target $target" + else + print_warning "Failed to install Rust target $target" + fi + fi + done +} + +# Install Linux cross-compilation tools +install_linux_cross_tools() { + print_info "Installing Linux cross-compilation tools..." + + # Check if musl-cross is already installed + if brew list --formula | grep -q "musl-cross"; then + print_success "musl-cross already installed" + else + print_info "Installing musl-cross (this may take a while)..." + if brew install messense/macos-cross-toolchains/x86_64-unknown-linux-gnu; then + print_success "Installed x86_64-unknown-linux-gnu toolchain" + else + print_warning "Failed to install x86_64-unknown-linux-gnu toolchain" + fi + + if brew install messense/macos-cross-toolchains/aarch64-unknown-linux-gnu; then + print_success "Installed aarch64-unknown-linux-gnu toolchain" + else + print_warning "Failed to install aarch64-unknown-linux-gnu toolchain" + fi + fi + + # Set up cargo config for Linux cross-compilation + mkdir -p ~/.cargo + local cargo_config=~/.cargo/config.toml + + print_info "Updating cargo configuration for Linux cross-compilation..." + + # Check if config already exists + if [ -f "$cargo_config" ]; then + # Backup existing config + cp "$cargo_config" "$cargo_config.backup" + print_info "Backed up existing cargo config to $cargo_config.backup" + fi + + # Add Linux target configurations + cat >> "$cargo_config" << 'EOF' + +# Linux cross-compilation targets +[target.x86_64-unknown-linux-gnu] +linker = "x86_64-unknown-linux-gnu-gcc" +ar = "x86_64-unknown-linux-gnu-ar" + +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-unknown-linux-gnu-gcc" +ar = "aarch64-unknown-linux-gnu-ar" +EOF + + print_success "Updated cargo configuration for Linux targets" +} + +# Install Windows cross-compilation tools +install_windows_cross_tools() { + print_info "Installing Windows cross-compilation tools..." + + # For Windows, we'll use the built-in MSVC target support + # which works on macOS without additional tools for library compilation + print_info "Windows MSVC targets use Rust's built-in support" + print_info "No additional tools needed for static library compilation" + + # Note: Full Windows executable compilation would require Wine and MSVC tools + print_warning "Note: This setup is sufficient for static libraries only" +} + +# Install Android NDK +install_android_ndk() { + print_info "Checking Android NDK..." + + if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -d "$ANDROID_NDK_HOME" ]; then + print_success "Android NDK already configured at $ANDROID_NDK_HOME" + return + fi + + print_info "Android NDK not found. You have two options:" + print_info "1. Install Android Studio and configure NDK through SDK Manager" + print_info "2. Download standalone NDK from https://developer.android.com/ndk/downloads" + print_info "" + print_info "After installation, set ANDROID_NDK_HOME environment variable:" + print_info " export ANDROID_NDK_HOME=/path/to/android-ndk" + print_info "" + print_warning "Android build will be skipped until NDK is configured" +} + +# Update build script for cross-compilation +update_build_script() { + local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local build_script="$script_dir/build-artifact-bundle.sh" + + if [ -f "$build_script" ]; then + print_info "Build script found at $build_script" + print_info "The script is already configured to use these tools" + else + print_warning "Build script not found at expected location" + fi +} + +# Main installation process +main() { + echo "Transport Services Cross-Compilation Tools Installer" + echo "====================================================" + echo "" + + check_macos + check_homebrew + + print_info "This script will install tools for cross-compiling to:" + print_info " • Linux x86_64" + print_info " • Linux ARM64" + print_info " • Windows x86_64 (static libraries only)" + print_info " • Windows ARM64 (static libraries only)" + print_info " • Android ARM64 (requires separate NDK installation)" + echo "" + + read -p "Continue? (y/N) " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + print_info "Installation cancelled" + exit 0 + fi + + install_rust_targets + install_linux_cross_tools + install_windows_cross_tools + install_android_ndk + update_build_script + + echo "" + echo "Installation Summary" + echo "====================" + print_success "Rust targets installed" + print_success "Linux cross-compilation tools installed" + print_info "Windows builds use Rust's built-in MSVC target support" + print_warning "Android NDK must be installed separately" + + echo "" + print_info "You can now run ./scripts/build-artifact-bundle.sh to build for all platforms" + print_info "Platforms without proper tools will be automatically skipped" +} + +# Run main if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/scripts/test-artifact-bundle-docker.sh b/scripts/test-artifact-bundle-docker.sh deleted file mode 100644 index 5963bc7..0000000 --- a/scripts/test-artifact-bundle-docker.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/bin/bash - -# Test script for artifact bundle creation in Docker -# This tests Linux and Android builds - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -echo "Testing artifact bundle creation in Docker..." - -# Build a simplified Docker image for testing -cat > "$PROJECT_ROOT/Dockerfile.test-build" << 'EOF' -FROM rust:1.83-slim - -# Install build essentials -RUN apt-get update && apt-get install -y \ - build-essential \ - curl \ - git \ - pkg-config \ - libssl-dev \ - zip \ - gcc-aarch64-linux-gnu \ - g++-aarch64-linux-gnu \ - && rm -rf /var/lib/apt/lists/* - -# Install Rust targets for Linux -RUN rustup target add \ - x86_64-unknown-linux-gnu \ - aarch64-unknown-linux-gnu - -# Install cbindgen -RUN cargo install cbindgen - -# Set up cargo config -RUN mkdir -p /root/.cargo && \ - echo '[target.aarch64-unknown-linux-gnu]' >> /root/.cargo/config.toml && \ - echo 'linker = "aarch64-linux-gnu-gcc"' >> /root/.cargo/config.toml - -WORKDIR /workspace -EOF - -# Build the test Docker image -echo "Building Docker image..." -docker build -f "$PROJECT_ROOT/Dockerfile.test-build" -t transport-services-test-builder "$PROJECT_ROOT" - -# Run the build in Docker -echo "Running build in Docker..." -docker run --rm \ - -v "$PROJECT_ROOT:/workspace" \ - -w /workspace \ - transport-services-test-builder \ - bash -c " - set -euo pipefail - - # Create build directory - mkdir -p build/transport_services.artifactbundle - - # Generate headers - echo 'Generating headers...' - cbindgen --config cbindgen.toml --crate transport_services --output build/transport_services.h - - # Create module map - cat > build/module.modulemap << 'MODULEMAP' -module TransportServices { - header \"transport_services.h\" - export * -} -MODULEMAP - - # Build for Linux x86_64 - echo 'Building for Linux x86_64...' - cargo build --release --target x86_64-unknown-linux-gnu --features ffi - - # Create variant directory - mkdir -p build/transport_services.artifactbundle/transport_services/linux-x86_64/{lib,include} - cp target/x86_64-unknown-linux-gnu/release/libtransport_services.a \ - build/transport_services.artifactbundle/transport_services/linux-x86_64/lib/ - cp build/transport_services.h \ - build/transport_services.artifactbundle/transport_services/linux-x86_64/include/ - cp build/module.modulemap \ - build/transport_services.artifactbundle/transport_services/linux-x86_64/include/ - - # Build for Linux ARM64 - echo 'Building for Linux ARM64...' - cargo build --release --target aarch64-unknown-linux-gnu --features ffi - - # Create variant directory - mkdir -p build/transport_services.artifactbundle/transport_services/linux-arm64/{lib,include} - cp target/aarch64-unknown-linux-gnu/release/libtransport_services.a \ - build/transport_services.artifactbundle/transport_services/linux-arm64/lib/ - cp build/transport_services.h \ - build/transport_services.artifactbundle/transport_services/linux-arm64/include/ - cp build/module.modulemap \ - build/transport_services.artifactbundle/transport_services/linux-arm64/include/ - - # Create manifest - cat > build/transport_services.artifactbundle/info.json << 'MANIFEST' -{ - \"schemaVersion\": \"1.0\", - \"artifacts\": { - \"transport_services\": { - \"version\": \"0.1.0\", - \"type\": \"staticLibrary\", - \"variants\": [ - { - \"path\": \"transport_services/linux-x86_64/lib/libtransport_services.a\", - \"supportedTriples\": [\"x86_64-unknown-linux-gnu\"], - \"staticLibraryMetadata\": { - \"headerPaths\": [\"transport_services/linux-x86_64/include\"], - \"moduleMapPath\": \"transport_services/linux-x86_64/include/module.modulemap\" - } - }, - { - \"path\": \"transport_services/linux-arm64/lib/libtransport_services.a\", - \"supportedTriples\": [\"aarch64-unknown-linux-gnu\"], - \"staticLibraryMetadata\": { - \"headerPaths\": [\"transport_services/linux-arm64/include\"], - \"moduleMapPath\": \"transport_services/linux-arm64/include/module.modulemap\" - } - } - ] - } - } -} -MANIFEST - - # Create zip file - cd build - zip -r transport_services-linux.zip transport_services.artifactbundle - - echo 'Build complete!' - echo 'Contents of artifact bundle:' - find transport_services.artifactbundle -type f | sort - " - -# Clean up -rm -f "$PROJECT_ROOT/Dockerfile.test-build" - -echo "Test complete! Check build/transport_services-linux.zip" \ No newline at end of file diff --git a/src/ffi/connection.rs b/src/ffi/connection.rs index d8e0b50..aa60d4d 100644 --- a/src/ffi/connection.rs +++ b/src/ffi/connection.rs @@ -138,15 +138,15 @@ pub unsafe extern "C" fn transport_services_connection_receive( // Start receiving messages in a loop loop { match conn_clone.next_event().await { - Some(ConnectionEvent::Received { message, context }) => { + Some(ConnectionEvent::Received { message_data, message_context: _ }) => { // Convert message to FFI format let ffi_message = types::TransportServicesMessage { - data: message.data().as_ptr(), - length: message.data().len(), - lifetime_ms: message.lifetime().as_millis() as u64, - priority: message.priority(), - idempotent: message.is_safely_replayable(), - final_message: message.is_final(), + data: message_data.as_ptr(), + length: message_data.len(), + lifetime_ms: 0, // Not available in received message context + priority: 0, // Not available in received message context + idempotent: false, // Not available in received message context + final_message: true, // Not available in received message context }; // Call the message callback @@ -156,16 +156,16 @@ pub unsafe extern "C" fn transport_services_connection_receive( callback_data.user_data as *mut c_void, ); } - Some(ConnectionEvent::ReceivedPartial { data, is_end, .. }) => { + Some(ConnectionEvent::ReceivedPartial { message_data, end_of_message, .. }) => { // For partial messages, accumulate or handle differently // For now, just treat as complete message let ffi_message = types::TransportServicesMessage { - data: data.as_ptr(), - length: data.len(), + data: message_data.as_ptr(), + length: message_data.len(), lifetime_ms: 0, priority: 0, idempotent: false, - final_message: is_end, + final_message: end_of_message, }; (callback_data.message_callback)( @@ -175,7 +175,7 @@ pub unsafe extern "C" fn transport_services_connection_receive( ); } Some(ConnectionEvent::ReceiveError { error }) => { - error::set_last_error(&error); + error::set_last_error_string(&error); (callback_data.error_callback)( types::TransportServicesError::ReceiveFailed, error::transport_services_get_last_error(), diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index c2e679d..5777b78 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -5,6 +5,7 @@ pub mod connection; pub mod error; pub mod listener; pub mod message; +pub mod path_monitor; pub mod preconnection; pub mod runtime; pub mod security_parameters; @@ -91,3 +92,6 @@ pub unsafe fn handle_ref<'a, T>(handle: *const TransportServicesHandle) -> &'a T pub unsafe fn handle_mut<'a, T>(handle: *mut TransportServicesHandle) -> &'a mut T { &mut *(handle as *mut T) } + +// Android-specific FFI functions are defined in path_monitor/android.rs +// and exported with #[no_mangle] so they don't need to be re-exported here diff --git a/src/ffi/path_monitor.rs b/src/ffi/path_monitor.rs new file mode 100644 index 0000000..db36d98 --- /dev/null +++ b/src/ffi/path_monitor.rs @@ -0,0 +1,323 @@ +//! FFI bindings for Network Path Monitoring +//! +//! Provides C-compatible bindings for cross-platform network interface monitoring + +use super::*; +use crate::path_monitor::{ChangeEvent, Interface, NetworkMonitor, Status}; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; +use std::sync::{Arc, Mutex}; + +/// FFI representation of network interface status +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub enum TransportServicesInterfaceStatus { + Up = 0, + Down = 1, + Unknown = 2, +} + +impl From for TransportServicesInterfaceStatus { + fn from(status: Status) -> Self { + match status { + Status::Up => TransportServicesInterfaceStatus::Up, + Status::Down => TransportServicesInterfaceStatus::Down, + Status::Unknown => TransportServicesInterfaceStatus::Unknown, + } + } +} + +/// FFI representation of a network interface +#[repr(C)] +pub struct TransportServicesInterface { + pub name: *mut c_char, + pub index: u32, + pub ips: *mut *mut c_char, + pub ip_count: usize, + pub status: TransportServicesInterfaceStatus, + pub interface_type: *mut c_char, + pub is_expensive: bool, +} + +/// FFI representation of change event type +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub enum TransportServicesChangeEventType { + Added = 0, + Removed = 1, + Modified = 2, + PathChanged = 3, +} + +/// FFI representation of a change event +#[repr(C)] +pub struct TransportServicesChangeEvent { + pub event_type: TransportServicesChangeEventType, + pub interface: *mut TransportServicesInterface, + pub old_interface: *mut TransportServicesInterface, // For Modified events + pub description: *mut c_char, // For PathChanged events +} + +/// Callback type for network change events +pub type TransportServicesPathMonitorCallback = + extern "C" fn(*const TransportServicesChangeEvent, *mut c_void); + +/// Opaque handle for the monitor watcher +pub struct PathMonitorHandle { + _handle: crate::path_monitor::MonitorHandle, + _callback_data: Arc>, +} + +struct CallbackData { + callback: TransportServicesPathMonitorCallback, + user_data: *mut c_void, +} + +unsafe impl Send for CallbackData {} +unsafe impl Sync for CallbackData {} + +/// Create a new network path monitor +#[no_mangle] +pub extern "C" fn transport_services_path_monitor_create() -> *mut TransportServicesHandle { + match NetworkMonitor::new() { + Ok(monitor) => to_handle(Box::new(monitor)), + Err(e) => { + error::set_last_error_string(&format!("Failed to create network monitor: {}", e)); + std::ptr::null_mut() + } + } +} + +/// Destroy a network path monitor +#[no_mangle] +pub unsafe extern "C" fn transport_services_path_monitor_destroy( + handle: *mut TransportServicesHandle, +) { + if !handle.is_null() { + let _ = from_handle::(handle); + } +} + +/// List all network interfaces +#[no_mangle] +pub unsafe extern "C" fn transport_services_path_monitor_list_interfaces( + handle: *mut TransportServicesHandle, + interfaces: *mut *mut TransportServicesInterface, + count: *mut usize, +) -> c_int { + if handle.is_null() || interfaces.is_null() || count.is_null() { + return -1; + } + + let monitor = handle_ref::(handle); + + match monitor.list_interfaces() { + Ok(ifaces) => { + let iface_count = ifaces.len(); + *count = iface_count; + + if iface_count == 0 { + *interfaces = std::ptr::null_mut(); + return 0; + } + + // Allocate array of interface pointers + let iface_array = libc::calloc( + iface_count, + std::mem::size_of::<*mut TransportServicesInterface>(), + ) as *mut *mut TransportServicesInterface; + + if iface_array.is_null() { + return -1; + } + + // Convert each interface + for (i, iface) in ifaces.into_iter().enumerate() { + let ffi_iface = interface_to_ffi(iface); + *iface_array.add(i) = ffi_iface; + } + + *interfaces = iface_array as *mut TransportServicesInterface; + 0 + } + Err(e) => { + error::set_last_error_string(&format!("Failed to list interfaces: {}", e)); + -1 + } + } +} + +/// Free an array of interfaces returned by list_interfaces +#[no_mangle] +pub unsafe extern "C" fn transport_services_path_monitor_free_interfaces( + interfaces: *mut *mut TransportServicesInterface, + count: usize, +) { + if interfaces.is_null() || count == 0 { + return; + } + + // Free each interface + for i in 0..count { + let iface_ptr = *interfaces.add(i); + if !iface_ptr.is_null() { + free_ffi_interface(iface_ptr); + } + } + + // Free the array itself + libc::free(interfaces as *mut c_void); +} + +/// Start watching for network changes +#[no_mangle] +pub unsafe extern "C" fn transport_services_path_monitor_start_watching( + handle: *mut TransportServicesHandle, + callback: TransportServicesPathMonitorCallback, + user_data: *mut c_void, +) -> *mut TransportServicesHandle { + if handle.is_null() { + return std::ptr::null_mut(); + } + + let monitor = handle_ref::(handle); + + let callback_data = Arc::new(Mutex::new(CallbackData { + callback, + user_data, + })); + + let callback_data_clone = callback_data.clone(); + + let monitor_handle = monitor.watch_changes(move |event| { + let data = callback_data_clone.lock().unwrap(); + let ffi_event = change_event_to_ffi(event); + (data.callback)(&ffi_event, data.user_data); + free_ffi_change_event(ffi_event); + }); + + to_handle(Box::new(PathMonitorHandle { + _handle: monitor_handle, + _callback_data: callback_data, + })) +} + +/// Stop watching for network changes +#[no_mangle] +pub unsafe extern "C" fn transport_services_path_monitor_stop_watching( + handle: *mut TransportServicesHandle, +) { + if !handle.is_null() { + let _ = from_handle::(handle); + } +} + +// Helper functions + +unsafe fn interface_to_ffi(iface: Interface) -> *mut TransportServicesInterface { + let ffi_iface = Box::new(TransportServicesInterface { + name: CString::new(iface.name).unwrap().into_raw(), + index: iface.index, + ips: std::ptr::null_mut(), + ip_count: 0, + status: iface.status.into(), + interface_type: CString::new(iface.interface_type).unwrap().into_raw(), + is_expensive: iface.is_expensive, + }); + + // Convert IP addresses + if !iface.ips.is_empty() { + let ip_count = iface.ips.len(); + let ip_array = + libc::calloc(ip_count, std::mem::size_of::<*mut c_char>()) as *mut *mut c_char; + + if !ip_array.is_null() { + for (i, ip) in iface.ips.iter().enumerate() { + let ip_str = CString::new(ip.to_string()).unwrap(); + *ip_array.add(i) = ip_str.into_raw(); + } + + let mut boxed_iface = ffi_iface; + boxed_iface.ips = ip_array; + boxed_iface.ip_count = ip_count; + return Box::into_raw(boxed_iface); + } + } + + Box::into_raw(ffi_iface) +} + +unsafe fn free_ffi_interface(iface: *mut TransportServicesInterface) { + if iface.is_null() { + return; + } + + let iface_ref = &*iface; + + // Free name + if !iface_ref.name.is_null() { + let _ = CString::from_raw(iface_ref.name); + } + + // Free interface type + if !iface_ref.interface_type.is_null() { + let _ = CString::from_raw(iface_ref.interface_type); + } + + // Free IP addresses + if !iface_ref.ips.is_null() && iface_ref.ip_count > 0 { + for i in 0..iface_ref.ip_count { + let ip_ptr = *iface_ref.ips.add(i); + if !ip_ptr.is_null() { + let _ = CString::from_raw(ip_ptr); + } + } + libc::free(iface_ref.ips as *mut c_void); + } + + // Free the interface struct itself + let _ = Box::from_raw(iface); +} + +fn change_event_to_ffi(event: ChangeEvent) -> TransportServicesChangeEvent { + unsafe { + match event { + ChangeEvent::Added(iface) => TransportServicesChangeEvent { + event_type: TransportServicesChangeEventType::Added, + interface: interface_to_ffi(iface), + old_interface: std::ptr::null_mut(), + description: std::ptr::null_mut(), + }, + ChangeEvent::Removed(iface) => TransportServicesChangeEvent { + event_type: TransportServicesChangeEventType::Removed, + interface: interface_to_ffi(iface), + old_interface: std::ptr::null_mut(), + description: std::ptr::null_mut(), + }, + ChangeEvent::Modified { old, new } => TransportServicesChangeEvent { + event_type: TransportServicesChangeEventType::Modified, + interface: interface_to_ffi(new), + old_interface: interface_to_ffi(old), + description: std::ptr::null_mut(), + }, + ChangeEvent::PathChanged { description } => TransportServicesChangeEvent { + event_type: TransportServicesChangeEventType::PathChanged, + interface: std::ptr::null_mut(), + old_interface: std::ptr::null_mut(), + description: CString::new(description).unwrap().into_raw(), + }, + } + } +} + +unsafe fn free_ffi_change_event(event: TransportServicesChangeEvent) { + if !event.interface.is_null() { + free_ffi_interface(event.interface); + } + if !event.old_interface.is_null() { + free_ffi_interface(event.old_interface); + } + if !event.description.is_null() { + let _ = CString::from_raw(event.description); + } +} diff --git a/src/lib.rs b/src/lib.rs index f126d90..3ab629f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod error; pub mod framer; pub mod listener; pub mod message; +pub mod path_monitor; pub mod preconnection; pub mod types; @@ -24,8 +25,9 @@ pub use connection_properties::{ }; pub use error::{Result, TransportServicesError}; pub use framer::{Framer, FramerStack, LengthPrefixFramer}; -pub use listener::Listener; +pub use listener::{Listener, ListenerEvent}; pub use message::{Message, MessageContext}; +pub use path_monitor::{ChangeEvent, Interface, MonitorHandle, NetworkMonitor, Status}; pub use preconnection::Preconnection; pub use types::*; diff --git a/src/path_monitor/android.rs b/src/path_monitor/android.rs new file mode 100644 index 0000000..c4232da --- /dev/null +++ b/src/path_monitor/android.rs @@ -0,0 +1,323 @@ +//! Android platform implementation using JNI +//! +//! Uses ConnectivityManager for monitoring network changes. + +use super::*; +use jni::{objects::{GlobalRef, JObject, JValue}, JNIEnv, JavaVM}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; + +static STATE: OnceLock>> = OnceLock::new(); + +struct State { + watchers: HashMap>, + current_interfaces: Vec, + next_watcher_id: usize, + java_support: Option, +} + +struct JavaSupport { + jvm: JavaVM, + support_object: GlobalRef, +} + +type WatcherId = usize; + +pub struct AndroidMonitor { + _phantom: std::marker::PhantomData<()>, +} + +pub struct AndroidWatchHandle { + id: WatcherId, +} + +impl Drop for AndroidWatchHandle { + fn drop(&mut self) { + if let Some(state_ref) = STATE.get() { + let mut state = state_ref.lock().unwrap(); + state.watchers.remove(&self.id); + + if state.watchers.is_empty() { + if let Some(ref support) = state.java_support { + let _ = stop_java_watching(support); + } + state.java_support = None; + } + } + } +} + +impl PlatformMonitor for AndroidMonitor { + fn list_interfaces(&self) -> Result, Error> { + // Get JVM and context from android_context + let (vm_ptr, _context_ptr) = android_context() + .ok_or_else(|| Error::PlatformError("Android context not set".into()))?; + + let jvm = unsafe { JavaVM::from_raw(vm_ptr as *mut jni::sys::JavaVM) } + .map_err(|e| Error::PlatformError(format!("Failed to get JavaVM: {:?}", e)))?; + + let mut env = jvm.attach_current_thread() + .map_err(|e| Error::PlatformError(format!("Failed to attach thread: {:?}", e)))?; + + // Call into Java to get network interfaces + list_interfaces_jni(&mut env) + } + + fn start_watching( + &mut self, + callback: Box, + ) -> PlatformHandle { + let state_ref = STATE.get_or_init(|| { + Arc::new(Mutex::new(State { + watchers: HashMap::new(), + current_interfaces: Vec::new(), + next_watcher_id: 1, + java_support: None, + })) + }); + + // Get current interfaces + let current_list = match self.list_interfaces() { + Ok(list) => list, + Err(_) => Vec::new(), + }; + + // Send initial events for all current interfaces + for interface in ¤t_list { + callback(ChangeEvent::Added(interface.clone())); + } + + let mut state = state_ref.lock().unwrap(); + let id = state.next_watcher_id; + state.next_watcher_id += 1; + state.current_interfaces = current_list; + let is_first_watcher = state.watchers.is_empty(); + state.watchers.insert(id, callback); + + if is_first_watcher { + let _ = start_java_watching(&mut state); + } + + Box::new(AndroidWatchHandle { id }) + } +} + +fn list_interfaces_jni(_env: &mut JNIEnv) -> Result, Error> { + // This would call into Java code to get the list of network interfaces + // For now, return an empty list as this requires Java-side implementation + Ok(Vec::new()) +} + +fn start_java_watching(state: &mut State) -> Result<(), Error> { + let (vm_ptr, context_ptr) = android_context() + .ok_or_else(|| Error::PlatformError("No Android context".into()))?; + + let jvm = unsafe { JavaVM::from_raw(vm_ptr as *mut jni::sys::JavaVM) } + .map_err(|e| Error::PlatformError(format!("Failed to get JavaVM: {:?}", e)))?; + + let support_object = { + let mut env = jvm.attach_current_thread() + .map_err(|e| Error::PlatformError(format!("Failed to attach thread: {:?}", e)))?; + + // Create the Java support object + let class_name = "com/transport_services/android/NetworkMonitorSupport"; + let support_class = env.find_class(class_name) + .map_err(|e| Error::PlatformError(format!("Failed to find class: {:?}", e)))?; + + let constructor_sig = "(Landroid/content/Context;)V"; + let context_obj = unsafe { JObject::from_raw(context_ptr as jni::sys::jobject) }; + + let support_object = env.new_object(&support_class, constructor_sig, &[(&context_obj).into()]) + .map_err(|e| Error::PlatformError(format!("Failed to create object: {:?}", e)))?; + + let global_ref = env.new_global_ref(support_object) + .map_err(|e| Error::PlatformError(format!("Failed to create global ref: {:?}", e)))?; + + // Start watching with callback pointer + let callback_ptr = transport_services_network_changed as *const () as jni::sys::jlong; + env.call_method( + &global_ref, + "startNetworkWatch", + "(J)V", + &[JValue::Long(callback_ptr)], + ).map_err(|e| Error::PlatformError(format!("Failed to start watch: {:?}", e)))?; + + global_ref + }; + + let java_support = JavaSupport { + jvm, + support_object, + }; + state.java_support = Some(java_support); + Ok(()) +} + +fn stop_java_watching(java_support: &JavaSupport) -> Result<(), Error> { + let mut env = java_support.jvm.attach_current_thread() + .map_err(|e| Error::PlatformError(format!("Failed to attach thread: {:?}", e)))?; + + env.call_method( + &java_support.support_object, + "stopNetworkWatch", + "()V", + &[], + ).map_err(|e| Error::PlatformError(format!("Failed to stop watch: {:?}", e)))?; + + Ok(()) +} + +/// Called from Java when network interfaces change +#[no_mangle] +pub extern "C" fn transport_services_network_changed() { + let Some(state_ref) = STATE.get() else { + return; + }; + + // Get new interface list + let new_list = match get_current_interfaces() { + Ok(list) => list, + Err(_) => return, + }; + + let mut state = state_ref.lock().unwrap(); + + // Calculate diff + let old_map: HashMap = state.current_interfaces + .iter() + .map(|i| (i.name.clone(), i)) + .collect(); + + let new_map: HashMap = new_list + .iter() + .map(|i| (i.name.clone(), i)) + .collect(); + + // Generate events + let mut events = Vec::new(); + + // Check for removed interfaces + for (name, old_iface) in &old_map { + if !new_map.contains_key(name) { + events.push(ChangeEvent::Removed((*old_iface).clone())); + } + } + + // Check for added or modified interfaces + for (name, new_iface) in &new_map { + match old_map.get(name) { + None => events.push(ChangeEvent::Added((*new_iface).clone())), + Some(old_iface) => { + if !interfaces_equal(old_iface, new_iface) { + events.push(ChangeEvent::Modified { + old: (*old_iface).clone(), + new: (*new_iface).clone(), + }); + } + } + } + } + + // Update state and notify watchers + state.current_interfaces = new_list; + for event in events { + for callback in state.watchers.values() { + callback(event.clone()); + } + } +} + +fn get_current_interfaces() -> Result, Error> { + let (vm_ptr, _) = android_context() + .ok_or_else(|| Error::PlatformError("No Android context".into()))?; + + let jvm = unsafe { JavaVM::from_raw(vm_ptr as *mut jni::sys::JavaVM) } + .map_err(|e| Error::PlatformError(format!("Failed to get JavaVM: {:?}", e)))?; + + let mut env = jvm.attach_current_thread() + .map_err(|e| Error::PlatformError(format!("Failed to attach thread: {:?}", e)))?; + + list_interfaces_jni(&mut env) +} + +fn interfaces_equal(a: &Interface, b: &Interface) -> bool { + a.name == b.name && + a.index == b.index && + a.ips == b.ips && + a.status == b.status && + a.interface_type == b.interface_type && + a.is_expensive == b.is_expensive +} + +// Android context management +struct AndroidContext { + vm: JavaVM, + context: GlobalRef, +} + +unsafe impl Send for AndroidContext {} +unsafe impl Sync for AndroidContext {} + +static ANDROID_CONTEXT: OnceLock>> = OnceLock::new(); + +/// Sets the Android context for the transport services library. +/// +/// # Safety +/// +/// This function is unsafe because it accepts raw pointers from the JNI layer. +/// The caller must ensure that: +/// - `env` is a valid JNIEnv pointer from the current JNI call +/// - `context` is a valid jobject representing an Android Context +/// - The pointers remain valid for the duration of this function call +#[no_mangle] +pub unsafe extern "C" fn transport_services_set_android_context( + env: *mut jni::sys::JNIEnv, + context: jni::sys::jobject, +) -> i32 { + match set_android_context_internal(env, context) { + Ok(()) => 0, + Err(_) => -1, + } +} + +unsafe fn set_android_context_internal( + env: *mut jni::sys::JNIEnv, + context: jni::sys::jobject, +) -> Result<(), Error> { + let env = JNIEnv::from_raw(env) + .map_err(|e| Error::PlatformError(format!("Invalid JNIEnv: {:?}", e)))?; + let context_obj = JObject::from_raw(context); + + let jvm = env.get_java_vm() + .map_err(|e| Error::PlatformError(format!("Failed to get JavaVM: {:?}", e)))?; + let global_context = env.new_global_ref(context_obj) + .map_err(|e| Error::PlatformError(format!("Failed to create global ref: {:?}", e)))?; + + let android_ctx = AndroidContext { + vm: jvm, + context: global_context, + }; + + let context_storage = ANDROID_CONTEXT.get_or_init(|| Mutex::new(None)); + *context_storage.lock().unwrap() = Some(android_ctx); + + Ok(()) +} + +fn android_context() -> Option<(*mut std::ffi::c_void, *mut std::ffi::c_void)> { + if let Some(context_storage) = ANDROID_CONTEXT.get() { + let ctx = context_storage.lock().unwrap(); + if let Some(ref android_ctx) = *ctx { + let vm_ptr = android_ctx.vm.get_java_vm_pointer() as *mut std::ffi::c_void; + let context_ptr = android_ctx.context.as_obj().as_raw() as *mut std::ffi::c_void; + return Some((vm_ptr, context_ptr)); + } + } + None +} + +pub fn create_platform_impl() -> Result, Error> { + Ok(Box::new(AndroidMonitor { + _phantom: std::marker::PhantomData, + })) +} \ No newline at end of file diff --git a/src/path_monitor/apple.rs b/src/path_monitor/apple.rs new file mode 100644 index 0000000..681d2b2 --- /dev/null +++ b/src/path_monitor/apple.rs @@ -0,0 +1,246 @@ +//! Apple platform implementation using direct Network.framework FFI +//! +//! Uses direct C bindings to Network.framework for monitoring network path changes. + +use super::*; +use libc::{c_void, freeifaddrs, getifaddrs, if_nametoindex, ifaddrs, AF_INET, AF_INET6}; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::ptr; +use std::sync::Arc; + +// Include network_sys as a submodule +#[path = "network_sys.rs"] +mod network_sys; +use network_sys::*; + +pub struct AppleDirectMonitor { + monitor: Option, + queue: Option, + callback_holder: Option>>>, + update_block: Option>, +} + +unsafe impl Send for AppleDirectMonitor {} +unsafe impl Sync for AppleDirectMonitor {} + +impl Drop for AppleDirectMonitor { + fn drop(&mut self) { + unsafe { + if let Some(monitor) = self.monitor { + nw_path_monitor_cancel(monitor); + nw_release(monitor as *mut c_void); + } + if let Some(queue) = self.queue { + dispatch_release(queue); + } + } + // Block will be released when dropped + self.update_block = None; + } +} + +impl PlatformMonitor for AppleDirectMonitor { + fn list_interfaces(&self) -> Result, Error> { + // Use getifaddrs to get interface information + unsafe { + let mut ifap: *mut ifaddrs = ptr::null_mut(); + if getifaddrs(&mut ifap) != 0 { + return Err(Error::PlatformError("Failed to get interfaces".into())); + } + + let mut interfaces_map: HashMap = HashMap::new(); + let mut current = ifap; + + while !current.is_null() { + let ifa = &*current; + if let Some(name) = ifa.ifa_name.as_ref() { + let name_str = CStr::from_ptr(name).to_string_lossy().to_string(); + let name_cstring = CString::new(name_str.as_str()).unwrap(); + + // Get interface index + let if_index = if_nametoindex(name_cstring.as_ptr()); + + let interface = interfaces_map.entry(name_str.clone()).or_insert(Interface { + name: name_str.clone(), + index: if_index, + ips: Vec::new(), + status: Status::Unknown, + interface_type: detect_interface_type(&name_str), + is_expensive: detect_expensive_interface(&name_str), + }); + + // Check if interface is up + if ifa.ifa_flags & libc::IFF_UP as u32 != 0 { + interface.status = Status::Up; + } else { + interface.status = Status::Down; + } + + // Extract IP addresses + if let Some(addr) = ifa.ifa_addr.as_ref() { + match addr.sa_family as i32 { + AF_INET => { + let sockaddr = addr as *const _ as *const libc::sockaddr_in; + let ip = Ipv4Addr::from((*sockaddr).sin_addr.s_addr.to_be()); + interface.ips.push(IpAddr::V4(ip)); + } + AF_INET6 => { + let sockaddr = addr as *const _ as *const libc::sockaddr_in6; + let ip = Ipv6Addr::from((*sockaddr).sin6_addr.s6_addr); + interface.ips.push(IpAddr::V6(ip)); + } + _ => {} + } + } + } + current = ifa.ifa_next; + } + + // Enhance with Network.framework info if we have a monitor + if let Some(_monitor) = self.monitor { + // We can't easily get the current path synchronously without blocks + // This is a limitation of the API design + // In practice, the callback mechanism would keep this updated + } + + freeifaddrs(ifap); + Ok(interfaces_map.into_values().collect()) + } + } + + fn start_watching( + &mut self, + callback: Box, + ) -> PlatformHandle { + self.callback_holder = Some(Arc::new(Mutex::new(callback))); + + unsafe { + // Create NWPathMonitor + let monitor = nw_path_monitor_create(); + if monitor.is_null() { + return Box::new(MonitorStopHandle { monitor: None }); + } + self.monitor = Some(monitor); + + // Create dispatch queue + let queue_name = CString::new("com.tapsrs.pathmonitor").unwrap(); + let queue = dispatch_queue_create(queue_name.as_ptr(), ptr::null()); + if queue.is_null() { + nw_release(monitor as *mut c_void); + self.monitor = None; + return Box::new(MonitorStopHandle { monitor: None }); + } + self.queue = Some(queue); + + // Set up path update handler + let callback_holder = self.callback_holder.as_ref().unwrap().clone(); + let update_block = PathUpdateBlock::new(move |path: nw_path_t| { + // Get path status + let status = nw_path_get_status(path); + let is_expensive = nw_path_is_expensive(path); + let is_constrained = nw_path_is_constrained(path); + + // Check what interfaces are being used + let uses_wifi = nw_path_uses_interface_type(path, NW_INTERFACE_TYPE_WIFI); + let uses_cellular = nw_path_uses_interface_type(path, NW_INTERFACE_TYPE_CELLULAR); + let uses_wired = nw_path_uses_interface_type(path, NW_INTERFACE_TYPE_WIRED); + + // Log the change + log::info!("Path changed: status={}, expensive={}, constrained={}, wifi={}, cellular={}, wired={}", + status, is_expensive, is_constrained, uses_wifi, uses_cellular, uses_wired); + + // Notify via callback + let callback = callback_holder.lock().unwrap(); + callback(ChangeEvent::PathChanged { + description: format!("Network path changed (status: {}, expensive: {}, wifi: {}, cellular: {}, wired: {})", + match status { + 1 => "satisfied", + 2 => "unsatisfied", + 3 => "satisfiable", + _ => "invalid", + }, + is_expensive, + uses_wifi, + uses_cellular, + uses_wired + ), + }); + }); + + nw_path_monitor_set_update_handler(monitor, update_block.as_ptr()); + nw_path_monitor_set_queue(monitor, queue); + nw_path_monitor_start(monitor); + + self.update_block = Some(Box::new(update_block)); + + // Return handle that will stop monitoring when dropped + Box::new(MonitorStopHandle { + monitor: self.monitor, + }) + } + } +} + +struct MonitorStopHandle { + monitor: Option, +} + +unsafe impl Send for MonitorStopHandle {} + +impl Drop for MonitorStopHandle { + fn drop(&mut self) { + if let Some(monitor) = self.monitor { + unsafe { + nw_path_monitor_cancel(monitor); + } + } + } +} + +fn detect_interface_type(name: &str) -> String { + match name { + // Loopback + "lo0" | "lo" => "loopback".to_string(), + + // WiFi - en0 is typically WiFi on macOS + "en0" => "wifi".to_string(), + + // Ethernet - other en interfaces + name if name.starts_with("en") => "ethernet".to_string(), + + // Cellular/Mobile data + name if name.starts_with("pdp_ip") => "cellular".to_string(), + + // Thunderbolt bridge + name if name.starts_with("bridge") => "bridge".to_string(), + + // VPN interfaces + name if name.starts_with("utun") => "vpn".to_string(), + name if name.starts_with("ipsec") => "vpn".to_string(), + name if name.starts_with("ppp") => "vpn".to_string(), + + // Bluetooth PAN + name if name.starts_with("awdl") => "awdl".to_string(), // Apple Wireless Direct Link + + // FireWire + name if name.starts_with("fw") => "firewire".to_string(), + + // Default + _ => "unknown".to_string(), + } +} + +fn detect_expensive_interface(name: &str) -> bool { + // Mark cellular and VPN connections as expensive by default + matches!(detect_interface_type(name).as_str(), "cellular" | "vpn") +} + +pub fn create_platform_impl() -> Result, Error> { + Ok(Box::new(AppleDirectMonitor { + monitor: None, + queue: None, + callback_holder: None, + update_block: None, + })) +} diff --git a/src/path_monitor/integration.rs b/src/path_monitor/integration.rs new file mode 100644 index 0000000..484499d --- /dev/null +++ b/src/path_monitor/integration.rs @@ -0,0 +1,148 @@ +//! Integration with Transport Services Connection API +//! +//! This module shows how path monitoring integrates with the +//! Transport Services Connection establishment and management. + +use super::*; +use crate::connection::Connection; +use std::sync::Weak; + +/// Extension trait for Connection to support path monitoring +pub trait ConnectionPathMonitoring { + /// Enable automatic path migration based on network changes + fn enable_path_monitoring(&self) -> Result; + + /// Get current network path information + fn get_current_path(&self) -> Option; +} + +/// Path-aware connection manager +pub struct PathAwareConnectionManager { + monitor: NetworkMonitor, + connections: Arc>>>, +} + +impl PathAwareConnectionManager { + pub fn new() -> Result { + Ok(Self { + monitor: NetworkMonitor::new()?, + connections: Arc::new(Mutex::new(Vec::new())), + }) + } + + /// Register a connection for path monitoring + pub fn register_connection(&self, conn: Weak) { + self.connections.lock().unwrap().push(conn); + } + + /// Start monitoring and managing paths for all connections + pub fn start_monitoring(&self) -> MonitorHandle { + let connections = self.connections.clone(); + + self.monitor.watch_changes(move |event| { + let mut conns = connections.lock().unwrap(); + + // Clean up dead weak references + conns.retain(|conn| conn.strong_count() > 0); + + // Handle the event for each connection + for conn_weak in conns.iter() { + if let Some(_conn) = conn_weak.upgrade() { + match &event { + ChangeEvent::PathChanged { description } => { + log::info!("Path changed for connection: {}", description); + // TODO: Trigger connection migration if supported + } + ChangeEvent::Removed(interface) => { + log::warn!("Interface {} removed", interface.name); + // TODO: Check if this affects the connection + } + ChangeEvent::Modified { old, new } => { + if old.status == Status::Up && new.status == Status::Down { + log::warn!("Interface {} went down", new.name); + // TODO: Trigger failover if this is the current path + } + } + _ => {} + } + } + } + }) + } + + /// Get available paths for a connection + pub fn get_available_paths(&self) -> Result, Error> { + self.monitor.list_interfaces() + } + + /// Select best path based on connection requirements + pub fn select_best_path( + &self, + prefer_wifi: bool, + avoid_expensive: bool, + ) -> Result, Error> { + let interfaces = self.monitor.list_interfaces()?; + + let mut candidates: Vec<_> = interfaces + .into_iter() + .filter(|iface| { + iface.status == Status::Up + && !iface.ips.is_empty() + && iface.interface_type != "loopback" + }) + .collect(); + + if avoid_expensive { + candidates.retain(|iface| !iface.is_expensive); + } + + if prefer_wifi { + // Sort to put wifi interfaces first + candidates.sort_by( + |a, b| match (&a.interface_type[..], &b.interface_type[..]) { + ("wifi", "wifi") => std::cmp::Ordering::Equal, + ("wifi", _) => std::cmp::Ordering::Less, + (_, "wifi") => std::cmp::Ordering::Greater, + _ => std::cmp::Ordering::Equal, + }, + ); + } + + Ok(candidates.into_iter().next()) + } +} + +/// Multipath policy implementation based on RFC 9622 +#[derive(Debug, Clone)] +pub enum MultipathMode { + /// Don't use multiple paths + Disabled, + /// Actively use multiple paths + Active, + /// Use multiple paths if peer requests + Passive, +} + +/// Path selection preferences +#[derive(Debug, Clone)] +pub struct PathPreferences { + /// Prefer specific interface types + pub preferred_types: Vec, + /// Avoid expensive (metered) connections + pub avoid_expensive: bool, + /// Minimum number of paths to maintain + pub min_paths: usize, + /// Maximum number of paths to use + pub max_paths: usize, +} + +impl Default for PathPreferences { + fn default() -> Self { + Self { + preferred_types: vec!["wifi".to_string(), "ethernet".to_string()], + avoid_expensive: true, + min_paths: 1, + max_paths: 2, + } + } +} diff --git a/src/path_monitor/linux.rs b/src/path_monitor/linux.rs new file mode 100644 index 0000000..92ceeae --- /dev/null +++ b/src/path_monitor/linux.rs @@ -0,0 +1,186 @@ +//! Linux platform implementation using rtnetlink +//! +//! Uses rtnetlink for monitoring network interface and address changes. + +use super::*; +use futures::stream::TryStreamExt; +use netlink_packet_route::address::AddressMessage; +use netlink_packet_route::link::LinkMessage; +use rtnetlink::{new_connection, Handle}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use tokio::runtime::Runtime; + +pub struct LinuxMonitor { + handle: Handle, + runtime: Arc, +} + +impl PlatformMonitor for LinuxMonitor { + fn list_interfaces(&self) -> Result, Error> { + let handle = self.handle.clone(); + let runtime = self.runtime.clone(); + + runtime.block_on(async { + let mut interfaces = Vec::new(); + + // Get all links + let mut links = handle.link().get().execute(); + while let Some(msg) = links + .try_next() + .await + .map_err(|e| Error::PlatformError(format!("Failed to get links: {}", e)))? + { + if let Some(interface) = parse_link_message(&msg).await { + interfaces.push(interface); + } + } + + // Get addresses for each interface + for interface in &mut interfaces { + let mut addrs = handle.address().get().execute(); + while let Some(msg) = addrs.try_next().await.unwrap_or(None) { + if let Some(addr) = parse_address_message(&msg, interface.index) { + interface.ips.push(addr); + } + } + } + + Ok(interfaces) + }) + } + + fn start_watching( + &mut self, + callback: Box, + ) -> PlatformHandle { + let _handle = self.handle.clone(); + let runtime = self.runtime.clone(); + let _callback = Arc::new(Mutex::new(callback)); + let stop_flag = Arc::new(AtomicBool::new(false)); + let thread_stop_flag = stop_flag.clone(); + + // Spawn a thread to run the async monitoring + let watcher = thread::spawn(move || { + runtime.block_on(async { + // This is a simplified version - actual implementation would + // subscribe to netlink events and process them + while !thread_stop_flag.load(Ordering::Relaxed) { + tokio::time::sleep(Duration::from_secs(1)).await; + // Check for changes and call callback + } + }); + }); + + Box::new(LinuxMonitorHandle { + watcher_handle: Some(watcher), + stop_flag, + }) + } +} + +struct LinuxMonitorHandle { + watcher_handle: Option>, + stop_flag: Arc, +} + +impl Drop for LinuxMonitorHandle { + fn drop(&mut self) { + // Signal the watcher thread to stop + self.stop_flag.store(true, Ordering::Relaxed); + if let Some(handle) = self.watcher_handle.take() { + // It's generally good practice to join the thread + // to ensure it has cleaned up properly. + let _ = handle.join(); + } + } +} + +async fn parse_link_message(msg: &LinkMessage) -> Option { + let mut name = String::new(); + let mut status = Status::Unknown; + let index = msg.header.index; + + // Parse attributes + for attr in &msg.attributes { + use netlink_packet_route::link::LinkAttribute; + match attr { + LinkAttribute::IfName(n) => name = n.clone(), + LinkAttribute::OperState(state) => { + use netlink_packet_route::link::State; + status = match state { + State::Up => Status::Up, + State::Down => Status::Down, + _ => Status::Unknown, + }; + } + _ => {} + } + } + + if name.is_empty() { + return None; + } + + Some(Interface { + name: name.clone(), + index, + ips: Vec::new(), + status, + interface_type: detect_interface_type(&name), + is_expensive: false, + }) +} + +fn parse_address_message(msg: &AddressMessage, if_index: u32) -> Option { + if msg.header.index != if_index { + return None; + } + + for attr in &msg.attributes { + use netlink_packet_route::address::AddressAttribute; + match attr { + AddressAttribute::Address(addr) => { + // addr is IpAddr, not bytes + return Some(addr.clone()); + } + _ => {} + } + } + + None +} + +fn detect_interface_type(name: &str) -> String { + if name.starts_with("eth") { + "ethernet".to_string() + } else if name.starts_with("wlan") || name.starts_with("wlp") { + "wifi".to_string() + } else if name.starts_with("wwan") { + "cellular".to_string() + } else if name.starts_with("lo") { + "loopback".to_string() + } else { + "unknown".to_string() + } +} + +pub fn create_platform_impl() -> Result, Error> { + let runtime = Arc::new( + Runtime::new() + .map_err(|e| Error::PlatformError(format!("Failed to create runtime: {}", e)))?, + ); + + let (conn, handle, _) = runtime.block_on(async { + new_connection().map_err(|e| { + Error::PlatformError(format!("Failed to create netlink connection: {}", e)) + }) + })?; + + // Spawn connection handler + runtime.spawn(conn); + + Ok(Box::new(LinuxMonitor { handle, runtime })) +} diff --git a/src/path_monitor/mod.rs b/src/path_monitor/mod.rs new file mode 100644 index 0000000..d15558a --- /dev/null +++ b/src/path_monitor/mod.rs @@ -0,0 +1,152 @@ +//! Network path monitoring implementation for Transport Services +//! +//! This module provides cross-platform network interface and path monitoring, +//! allowing applications to track network changes and adapt connections accordingly. + +use std::net::IpAddr; +#[cfg(target_vendor = "apple")] +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::sync::{Arc, Mutex}; + +// Platform-specific implementations +#[cfg(target_vendor = "apple")] +mod apple; + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(target_os = "android")] +mod android; + +pub mod integration; + +// Common types across platforms +#[derive(Debug, Clone)] +pub struct Interface { + pub name: String, // e.g., "en0", "eth0" + pub index: u32, // Interface index + pub ips: Vec, // List of assigned IPs + pub status: Status, // Up/Down/Unknown + pub interface_type: String, // e.g., "wifi", "ethernet", "cellular" + pub is_expensive: bool, // e.g., metered like cellular +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Status { + Up, + Down, + Unknown, +} + +#[derive(Debug, Clone)] +pub enum ChangeEvent { + Added(Interface), + Removed(Interface), + Modified { old: Interface, new: Interface }, + PathChanged { description: String }, // Generic path change info +} + +// The main API struct +pub struct NetworkMonitor { + // Internal state, e.g., Arc> + inner: Arc>>, +} + +impl NetworkMonitor { + /// Create a new monitor + pub fn new() -> Result { + let inner = create_platform_impl()?; + Ok(Self { + inner: Arc::new(Mutex::new(inner)), + }) + } + + /// List current interfaces synchronously + pub fn list_interfaces(&self) -> Result, Error> { + let guard = self.inner.lock().unwrap(); + guard.list_interfaces() + } + + /// Start watching for changes; returns a handle to stop + pub fn watch_changes(&self, callback: F) -> MonitorHandle + where + F: Fn(ChangeEvent) + Send + 'static, + { + let mut guard = self.inner.lock().unwrap(); + let handle = guard.start_watching(Box::new(callback)); + MonitorHandle { _inner: handle } // RAII to stop on drop + } +} + +// Handle to stop monitoring (drops the watcher) +pub struct MonitorHandle { + _inner: PlatformHandle, // Platform-specific drop logic +} + +#[derive(Debug)] +pub enum Error { + PlatformError(String), + PermissionDenied, + NotSupported, + // etc. +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::PlatformError(msg) => write!(f, "Platform error: {}", msg), + Error::PermissionDenied => write!(f, "Permission denied"), + Error::NotSupported => write!(f, "Operation not supported on this platform"), + } + } +} + +impl std::error::Error for Error {} + +// Platform abstraction trait +trait PlatformMonitor { + fn list_interfaces(&self) -> Result, Error>; + fn start_watching( + &mut self, + callback: Box, + ) -> PlatformHandle; +} + +type PlatformHandle = Box; // Platform-specific handle + +// Platform implementation factory +#[cfg(target_vendor = "apple")] +fn create_platform_impl() -> Result, Error> { + apple::create_platform_impl() +} + +#[cfg(target_os = "linux")] +fn create_platform_impl() -> Result, Error> { + linux::create_platform_impl() +} + +#[cfg(target_os = "windows")] +fn create_platform_impl() -> Result, Error> { + windows::create_platform_impl() +} + +#[cfg(target_os = "android")] +fn create_platform_impl() -> Result, Error> { + android::create_platform_impl() +} + +#[cfg(not(any( + target_vendor = "apple", + target_os = "linux", + target_os = "windows", + target_os = "android" +)))] +fn create_platform_impl() -> Result, Error> { + Err(Error::NotSupported) +} + +#[cfg(test)] +mod tests; diff --git a/src/path_monitor/network_sys.rs b/src/path_monitor/network_sys.rs new file mode 100644 index 0000000..139cf81 --- /dev/null +++ b/src/path_monitor/network_sys.rs @@ -0,0 +1,198 @@ +//! Direct FFI bindings for Network.framework +//! +//! Since objc2 doesn't support Network.framework yet, we use direct C bindings. + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +use libc::{c_char, c_int, c_void}; + +// Opaque types +pub enum nw_path_monitor {} +pub type nw_path_monitor_t = *mut nw_path_monitor; + +pub enum nw_path {} +pub type nw_path_t = *mut nw_path; + +pub enum nw_interface {} +pub type nw_interface_t = *mut nw_interface; + +pub enum nw_endpoint {} +pub type nw_endpoint_t = *mut nw_endpoint; + +pub enum nw_protocol_options {} +pub type nw_protocol_options_t = *mut nw_protocol_options; + +// Dispatch types +pub type dispatch_queue_t = *mut c_void; +pub type dispatch_block_t = *const c_void; + +// Interface types +pub type nw_interface_type_t = c_int; +pub const NW_INTERFACE_TYPE_OTHER: nw_interface_type_t = 0; +pub const NW_INTERFACE_TYPE_WIFI: nw_interface_type_t = 1; +pub const NW_INTERFACE_TYPE_CELLULAR: nw_interface_type_t = 2; +pub const NW_INTERFACE_TYPE_WIRED: nw_interface_type_t = 3; +pub const NW_INTERFACE_TYPE_LOOPBACK: nw_interface_type_t = 4; + +// Path status +pub type nw_path_status_t = c_int; +pub const NW_PATH_STATUS_INVALID: nw_path_status_t = 0; +pub const NW_PATH_STATUS_SATISFIED: nw_path_status_t = 1; +pub const NW_PATH_STATUS_UNSATISFIED: nw_path_status_t = 2; +pub const NW_PATH_STATUS_SATISFIABLE: nw_path_status_t = 3; + +#[link(name = "Network", kind = "framework")] +extern "C" { + // Path monitor functions + pub fn nw_path_monitor_create() -> nw_path_monitor_t; + pub fn nw_path_monitor_create_with_type( + required_interface_type: nw_interface_type_t, + ) -> nw_path_monitor_t; + pub fn nw_path_monitor_set_queue(monitor: nw_path_monitor_t, queue: dispatch_queue_t); + pub fn nw_path_monitor_start(monitor: nw_path_monitor_t); + pub fn nw_path_monitor_cancel(monitor: nw_path_monitor_t); + + // Path functions + pub fn nw_path_get_status(path: nw_path_t) -> nw_path_status_t; + pub fn nw_path_is_expensive(path: nw_path_t) -> bool; + pub fn nw_path_is_constrained(path: nw_path_t) -> bool; + pub fn nw_path_uses_interface_type( + path: nw_path_t, + interface_type: nw_interface_type_t, + ) -> bool; + pub fn nw_path_enumerate_interfaces(path: nw_path_t, enumerate_block: dispatch_block_t) + -> bool; + + // Interface functions + pub fn nw_interface_get_type(interface: nw_interface_t) -> nw_interface_type_t; + pub fn nw_interface_get_name(interface: nw_interface_t) -> *const c_char; + pub fn nw_interface_get_index(interface: nw_interface_t) -> u32; + + // Object management + pub fn nw_retain(obj: *mut c_void) -> *mut c_void; + pub fn nw_release(obj: *mut c_void); +} + +// Dispatch queue functions +#[link(name = "System", kind = "dylib")] +extern "C" { + pub fn dispatch_queue_create(label: *const c_char, attr: *const c_void) -> dispatch_queue_t; + pub fn dispatch_release(object: dispatch_queue_t); +} + +// Block support for Objective-C blocks +use std::marker::PhantomData; +use std::mem; +use std::os::raw::c_ulong; + +// Block structure for Objective-C blocks +#[repr(C)] +pub struct Block { + isa: *const c_void, + flags: c_int, + reserved: c_int, + invoke: unsafe extern "C" fn(*mut Block, nw_path_t), + descriptor: *const BlockDescriptor, + closure: F, +} + +#[repr(C)] +pub struct BlockDescriptor { + reserved: c_ulong, + size: c_ulong, + copy_helper: Option, + dispose_helper: Option, + signature: *const c_char, + _phantom: PhantomData, +} + +impl Block +where + F: FnMut(nw_path_t), +{ + pub fn new(closure: F) -> *mut Self { + let descriptor = Box::new(BlockDescriptor:: { + reserved: 0, + size: mem::size_of::>() as c_ulong, + copy_helper: Some(copy_helper::), + dispose_helper: Some(dispose_helper::), + signature: b"v@?@\0".as_ptr() as *const c_char, // void (^)(nw_path_t) + _phantom: PhantomData, + }); + + let mut block = Box::new(Block { + isa: unsafe { &_NSConcreteStackBlock as *const _ as *const c_void }, + flags: (1 << 25) | (1 << 24), // BLOCK_HAS_COPY_DISPOSE | BLOCK_HAS_SIGNATURE + reserved: 0, + invoke: invoke::, + descriptor: Box::into_raw(descriptor), + closure, + }); + + // Copy to heap + unsafe { + let heap_block = _Block_copy(block.as_mut() as *mut _ as *const c_void); + let _ = Box::into_raw(block); // Leak the stack block + heap_block as *mut Self + } + } +} + +unsafe extern "C" fn invoke(block_ptr: *mut Block, path: nw_path_t) +where + F: FnMut(nw_path_t), +{ + let block = &mut *block_ptr; + (block.closure)(path); +} + +unsafe extern "C" fn copy_helper(_dst: *mut c_void, _src: *const c_void) { + // For our use case, we don't need to implement copy +} + +unsafe extern "C" fn dispose_helper(_block: *mut c_void) { + // Cleanup will happen when block is released +} + +extern "C" { + static _NSConcreteStackBlock: c_void; + fn _Block_copy(block: *const c_void) -> *mut c_void; + fn _Block_release(block: *const c_void); +} + +// Wrapper for safe block handling +pub struct PathUpdateBlock { + block: *mut c_void, +} + +impl PathUpdateBlock { + pub fn new(closure: F) -> Self + where + F: FnMut(nw_path_t) + 'static, + { + let block = Block::new(closure); + PathUpdateBlock { + block: block as *mut c_void, + } + } + + pub fn as_ptr(&self) -> *mut c_void { + self.block + } +} + +impl Drop for PathUpdateBlock { + fn drop(&mut self) { + unsafe { + _Block_release(self.block); + } + } +} + +unsafe impl Send for PathUpdateBlock {} + +#[link(name = "Network", kind = "framework")] +extern "C" { + pub fn nw_path_monitor_set_update_handler(monitor: nw_path_monitor_t, handler: *mut c_void); +} diff --git a/src/path_monitor/tests.rs b/src/path_monitor/tests.rs new file mode 100644 index 0000000..80f2258 --- /dev/null +++ b/src/path_monitor/tests.rs @@ -0,0 +1,118 @@ +//! Tests for the path monitor module + +#[cfg(test)] +mod tests { + use super::super::*; + use std::sync::{Arc, Mutex}; + use std::thread; + use std::time::Duration; + + #[test] + fn test_create_network_monitor() { + // This test might fail on unsupported platforms + match NetworkMonitor::new() { + Ok(_monitor) => { + // Successfully created monitor + } + Err(Error::NotSupported) => { + // Platform not supported, which is expected for some platforms + } + Err(e) => { + panic!("Unexpected error creating NetworkMonitor: {:?}", e); + } + } + } + + #[test] + fn test_list_interfaces() { + match NetworkMonitor::new() { + Ok(monitor) => { + match monitor.list_interfaces() { + Ok(interfaces) => { + // Should have at least a loopback interface on most systems + assert!(!interfaces.is_empty(), "No interfaces found"); + + // Check that interfaces have required fields + for interface in interfaces { + assert!(!interface.name.is_empty()); + // Most systems have at least one IP on loopback + if interface.interface_type == "loopback" { + assert!(!interface.ips.is_empty()); + } + } + } + Err(e) => { + eprintln!("Failed to list interfaces: {:?}", e); + } + } + } + Err(Error::NotSupported) => { + // Skip test on unsupported platforms + } + Err(e) => { + panic!("Failed to create monitor: {:?}", e); + } + } + } + + #[test] + fn test_monitor_handle_drop() { + // Test that the monitor handle properly stops monitoring when dropped + match NetworkMonitor::new() { + Ok(monitor) => { + let events = Arc::new(Mutex::new(Vec::new())); + let events_clone = events.clone(); + + { + let _handle = monitor.watch_changes(move |event| { + events_clone.lock().unwrap().push(format!("{:?}", event)); + }); + // Handle is dropped here + } + + // Give some time for cleanup + thread::sleep(Duration::from_millis(100)); + + // No more events should be received after handle is dropped + let initial_count = events.lock().unwrap().len(); + thread::sleep(Duration::from_millis(100)); + let final_count = events.lock().unwrap().len(); + + assert_eq!( + initial_count, final_count, + "Events received after handle dropped" + ); + } + Err(Error::NotSupported) => { + // Skip test on unsupported platforms + } + Err(e) => { + panic!("Failed to create monitor: {:?}", e); + } + } + } + + #[test] + fn test_interface_status() { + match NetworkMonitor::new() { + Ok(monitor) => { + if let Ok(interfaces) = monitor.list_interfaces() { + for interface in interfaces { + // Status should be one of the defined values + match interface.status { + Status::Up | Status::Down | Status::Unknown => { + // Valid status + } + } + } + } + } + Err(Error::NotSupported) => { + // Skip test on unsupported platforms + } + Err(_) => { + // Ignore other errors in this test + } + } + } +} diff --git a/src/path_monitor/windows.rs b/src/path_monitor/windows.rs new file mode 100644 index 0000000..5f7db8d --- /dev/null +++ b/src/path_monitor/windows.rs @@ -0,0 +1,341 @@ +//! Windows platform implementation using IP Helper API +//! +//! Uses NotifyUnicastIpAddressChange and GetAdaptersAddresses for monitoring. + +use super::*; +use std::collections::HashMap; +use std::ffi::c_void; +use std::net::IpAddr; +use std::sync::Mutex; + +use ::windows::Win32::Foundation::{ + ERROR_ADDRESS_NOT_ASSOCIATED, ERROR_BUFFER_OVERFLOW, + ERROR_INVALID_PARAMETER, ERROR_NOT_ENOUGH_MEMORY, ERROR_NO_DATA, ERROR_SUCCESS, + NO_ERROR, WIN32_ERROR, BOOLEAN, HANDLE, +}; +use ::windows::Win32::NetworkManagement::IpHelper::{ + CancelMibChangeNotify2, GetAdaptersAddresses, NotifyUnicastIpAddressChange, + MIB_NOTIFICATION_TYPE, MIB_UNICASTIPADDRESS_ROW, GAA_FLAG_SKIP_ANYCAST, + GAA_FLAG_SKIP_MULTICAST, IP_ADAPTER_ADDRESSES_LH, +}; +use ::windows::Win32::NetworkManagement::Ndis::IfOperStatusDown; +use ::windows::Win32::Networking::WinSock::{ + AF_INET, AF_INET6, AF_UNSPEC, SOCKADDR_IN, SOCKADDR_IN6, +}; + +// Interface type constants from Windows SDK +const IF_TYPE_ETHERNET_CSMACD: u32 = 6; +const IF_TYPE_IEEE80211: u32 = 71; +const IF_TYPE_SOFTWARE_LOOPBACK: u32 = 24; +const IF_TYPE_WWANPP: u32 = 243; +const IF_TYPE_WWANPP2: u32 = 244; + +/// State for tracking interface changes +struct WatchState { + /// The last known list of interfaces for diffing + prev_interfaces: Vec, + /// User's callback wrapped for thread safety + cb: Box, +} + +/// Windows-specific monitor implementation +pub struct WindowsMonitor { + /// Current state for change detection + state: Option>>, +} + +unsafe impl Send for WindowsMonitor {} +unsafe impl Sync for WindowsMonitor {} + +impl WindowsMonitor { + /// List all network interfaces using GetAdaptersAddresses + fn list_interfaces_internal() -> Result, Error> { + let mut interfaces = Vec::new(); + + // Microsoft recommends a 15 KB initial buffer + let start_size = 15 * 1024; + let mut buf: Vec = vec![0; start_size]; + let mut size_pointer: u32 = start_size as u32; + + unsafe { + loop { + let bufptr = buf.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH; + let res = GetAdaptersAddresses( + AF_UNSPEC.0 as u32, + GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST, + None, + Some(bufptr), + &mut size_pointer, + ); + + match WIN32_ERROR(res) { + ERROR_SUCCESS => break, + ERROR_ADDRESS_NOT_ASSOCIATED => { + return Err(Error::PlatformError("Address not associated".to_string())) + } + ERROR_BUFFER_OVERFLOW => { + buf.resize(size_pointer as usize, 0); + continue; + } + ERROR_INVALID_PARAMETER => { + return Err(Error::PlatformError("Invalid parameter".to_string())) + } + ERROR_NOT_ENOUGH_MEMORY => { + return Err(Error::PlatformError("Not enough memory".to_string())) + } + ERROR_NO_DATA => return Ok(Vec::new()), // No interfaces + _ => { + return Err(Error::PlatformError(format!( + "GetAdaptersAddresses failed with error: {}", + res + ))) + } + } + } + + // Parse the adapter list + let mut adapter_ptr = buf.as_ptr() as *const IP_ADAPTER_ADDRESSES_LH; + while !adapter_ptr.is_null() { + let adapter = &*adapter_ptr; + + // Skip interfaces that are down + if adapter.OperStatus == IfOperStatusDown { + adapter_ptr = adapter.Next; + continue; + } + + // Get interface name + let name = adapter + .FriendlyName + .to_string() + .unwrap_or_else(|_| format!("Unknown{}", adapter.Ipv6IfIndex)); + + // Collect IP addresses + let mut ips = vec![]; + let mut unicast_ptr = adapter.FirstUnicastAddress; + while !unicast_ptr.is_null() { + let unicast = &*unicast_ptr; + let sockaddr = &*unicast.Address.lpSockaddr; + + let ip = match sockaddr.sa_family { + AF_INET => { + let sockaddr_in = &*(unicast.Address.lpSockaddr as *const SOCKADDR_IN); + IpAddr::V4(sockaddr_in.sin_addr.into()) + } + AF_INET6 => { + let sockaddr_in6 = &*(unicast.Address.lpSockaddr as *const SOCKADDR_IN6); + IpAddr::V6(sockaddr_in6.sin6_addr.into()) + } + _ => { + unicast_ptr = unicast.Next; + continue; + } + }; + + ips.push(ip); + unicast_ptr = unicast.Next; + } + + let interface = Interface { + name, + index: adapter.Ipv6IfIndex, // Use IPv6 index as it's more consistent + ips, + status: if adapter.OperStatus == IfOperStatusDown { + Status::Down + } else { + Status::Up + }, + interface_type: detect_interface_type(adapter.IfType), + is_expensive: false, // TODO: Detect from connection profile API + }; + + interfaces.push(interface); + adapter_ptr = adapter.Next; + } + } + + Ok(interfaces) + } +} + +impl PlatformMonitor for WindowsMonitor { + fn list_interfaces(&self) -> Result, Error> { + Self::list_interfaces_internal() + } + + fn start_watching( + &mut self, + callback: Box, + ) -> PlatformHandle { + // Get initial interface list + let prev_interfaces = Self::list_interfaces_internal().unwrap_or_default(); + + // Create the watch state + let state = Arc::new(Mutex::new(WatchState { + prev_interfaces, + cb: callback, + })); + + // Get a raw pointer to pass to the Windows API + let state_ptr = Arc::as_ptr(&state) as *const c_void; + + // Store the state in self to keep it alive + self.state = Some(state.clone()); + + let mut handle = HANDLE::default(); + + unsafe { + let res = NotifyUnicastIpAddressChange( + AF_UNSPEC, + Some(notif_callback), + Some(state_ptr), + BOOLEAN(0), // Not initial notification + &mut handle, + ); + + match res { + NO_ERROR => { + // Trigger an initial update to establish baseline + if let Ok(new_list) = Self::list_interfaces_internal() { + handle_notif(&mut state.lock().unwrap(), new_list); + } + + Box::new(WindowsWatchHandle { + handle, + _state: state, + }) + } + _ => { + // Return a dummy handle that does nothing + Box::new(WindowsWatchHandle { + handle: HANDLE::default(), + _state: state, + }) + } + } + } + } +} + +/// Handle for canceling the network change notifications +struct WindowsWatchHandle { + handle: HANDLE, + _state: Arc>, // Keep state alive +} + +unsafe impl Send for WindowsWatchHandle {} + +impl Drop for WindowsWatchHandle { + fn drop(&mut self) { + unsafe { + if !self.handle.is_invalid() { + let _ = CancelMibChangeNotify2(self.handle); + } + } + } +} + +/// Callback invoked by Windows when network changes occur +unsafe extern "system" fn notif_callback( + ctx: *const c_void, + _row: *const MIB_UNICASTIPADDRESS_ROW, + _notification_type: MIB_NOTIFICATION_TYPE, +) { + if ctx.is_null() { + return; + } + + let state_ptr = ctx as *const Mutex; + let state_mutex = &*state_ptr; + + if let Ok(mut state_guard) = state_mutex.lock() { + if let Ok(new_list) = WindowsMonitor::list_interfaces_internal() { + handle_notif(&mut state_guard, new_list); + } + } +} + +/// Handle a notification by comparing old and new interface lists +fn handle_notif(state: &mut WatchState, new_interfaces: Vec) { + // Create maps for efficient comparison + let old_map: HashMap = state.prev_interfaces + .iter() + .map(|iface| (iface.index, iface)) + .collect(); + + let new_map: HashMap = new_interfaces + .iter() + .map(|iface| (iface.index, iface)) + .collect(); + + // Find additions + for (index, new_iface) in &new_map { + if !old_map.contains_key(index) { + (state.cb)(ChangeEvent::Added((*new_iface).clone())); + } + } + + // Find removals + for (index, old_iface) in &old_map { + if !new_map.contains_key(index) { + (state.cb)(ChangeEvent::Removed((*old_iface).clone())); + } + } + + // Find modifications + for (index, new_iface) in &new_map { + if let Some(old_iface) = old_map.get(index) { + if !interfaces_equal(old_iface, new_iface) { + (state.cb)(ChangeEvent::Modified { + old: (*old_iface).clone(), + new: (*new_iface).clone(), + }); + } + } + } + + // Update the stored state + state.prev_interfaces = new_interfaces; +} + +/// Compare two interfaces for equality +fn interfaces_equal(a: &Interface, b: &Interface) -> bool { + a.name == b.name + && a.index == b.index + && a.status == b.status + && a.interface_type == b.interface_type + && a.is_expensive == b.is_expensive + && ips_equal(&a.ips, &b.ips) +} + +/// Compare two IP lists for equality (order-independent) +fn ips_equal(a: &[IpAddr], b: &[IpAddr]) -> bool { + if a.len() != b.len() { + return false; + } + + let mut a_sorted = a.to_vec(); + let mut b_sorted = b.to_vec(); + a_sorted.sort(); + b_sorted.sort(); + + a_sorted == b_sorted +} + +/// Detect interface type from Windows interface type constant +fn detect_interface_type(if_type: u32) -> String { + match if_type { + IF_TYPE_ETHERNET_CSMACD => "ethernet".to_string(), + IF_TYPE_IEEE80211 => "wifi".to_string(), + IF_TYPE_WWANPP | IF_TYPE_WWANPP2 => "cellular".to_string(), + IF_TYPE_SOFTWARE_LOOPBACK => "loopback".to_string(), + _ => "unknown".to_string(), + } +} + +/// Create the platform implementation +pub fn create_platform_impl() -> Result, Error> { + Ok(Box::new(WindowsMonitor { + state: None, + })) +} \ No newline at end of file diff --git a/src/tests/integration_tests.rs b/src/tests/integration_tests.rs index dfd3d54..ab05340 100644 --- a/src/tests/integration_tests.rs +++ b/src/tests/integration_tests.rs @@ -360,7 +360,7 @@ async fn test_listener_accept_connection() { SecurityParameters::new_disabled(), ); - let mut listener = preconn.listen().await.unwrap(); + let listener = preconn.listen().await.unwrap(); let listen_addr = listener.local_addr().await.unwrap(); // Connect from client with short timeout @@ -397,7 +397,7 @@ async fn test_client_server_data_exchange() { SecurityParameters::new_disabled(), ); - let mut listener = server_preconn.listen().await.unwrap(); + let listener = server_preconn.listen().await.unwrap(); let listen_addr = listener.local_addr().await.unwrap(); // Server accept loop @@ -462,7 +462,7 @@ async fn test_listener_multiple_clients() { SecurityParameters::new_disabled(), ); - let mut listener = preconn.listen().await.unwrap(); + let listener = preconn.listen().await.unwrap(); let listen_addr = listener.local_addr().await.unwrap(); // Spawn multiple clients @@ -520,7 +520,7 @@ async fn test_listener_connection_limit_integration() { SecurityParameters::new_disabled(), ); - let mut listener = preconn.listen().await.unwrap(); + let listener = preconn.listen().await.unwrap(); let listen_addr = listener.local_addr().await.unwrap(); // Set connection limit to 2 @@ -579,7 +579,7 @@ async fn test_rendezvous_peer_to_peer() { ); // Start peer A rendezvous - let (conn_a, mut listener_a) = preconn_a.rendezvous().await.unwrap(); + let (conn_a, listener_a) = preconn_a.rendezvous().await.unwrap(); let addr_a = listener_a.local_addr().await.unwrap(); // Peer B diff --git a/src/tests/listener_tests.rs b/src/tests/listener_tests.rs index 2e1b9bb..35e482b 100644 --- a/src/tests/listener_tests.rs +++ b/src/tests/listener_tests.rs @@ -91,7 +91,7 @@ async fn test_listener_accept_with_connection() { SecurityParameters::default(), ); - let mut listener = preconn.listen().await.unwrap(); + let listener = preconn.listen().await.unwrap(); let bound_addr = listener.local_addr().await.unwrap(); // Spawn a client connection with short timeout @@ -164,7 +164,7 @@ async fn test_listener_connection_limit() { SecurityParameters::default(), ); - let mut listener = preconn.listen().await.unwrap(); + let listener = preconn.listen().await.unwrap(); let bound_addr = listener.local_addr().await.unwrap(); // Set connection limit to 1 @@ -211,7 +211,7 @@ async fn test_listener_event_stream() { SecurityParameters::default(), ); - let mut listener = preconn.listen().await.unwrap(); + let listener = preconn.listen().await.unwrap(); let bound_addr = listener.local_addr().await.unwrap(); // Connect and check event @@ -257,7 +257,7 @@ async fn test_listener_multiple_connections() { SecurityParameters::default(), ); - let mut listener = preconn.listen().await.unwrap(); + let listener = preconn.listen().await.unwrap(); let bound_addr = listener.local_addr().await.unwrap(); // Spawn multiple clients diff --git a/src/tests/rendezvous_tests.rs b/src/tests/rendezvous_tests.rs index d29d5f9..ebe3bc4 100644 --- a/src/tests/rendezvous_tests.rs +++ b/src/tests/rendezvous_tests.rs @@ -203,7 +203,7 @@ async fn test_rendezvous_incoming_connection() { SecurityParameters::default(), ); - let (connection, mut listener) = preconn.rendezvous().await.unwrap(); + let (connection, listener) = preconn.rendezvous().await.unwrap(); let listen_addr = listener.local_addr().await.unwrap(); // Connect to the listener