New package ByteLystDiagnostics with: - Core types: DiagnosticsSession, TraceSpan, LogEntry, Breadcrumb - DiagnosticsClient: actor-based singleton with polling - BreadcrumbTrail: ring buffer (max 100) for timeline - NetworkInterceptor: URLProtocol-based HTTP capture - DeviceState: battery, memory, storage, network, thermal - 20+ XCTest unit tests Features: - configure()/start()/stop() lifecycle - trace() async span wrapper - log() with breadcrumb integration - breadcrumb() manual timeline markers - ETag-based config polling - 30-second batch flush Platforms: iOS 15+, macOS 13+, watchOS 8+, tvOS 15+
192 lines
6.1 KiB
Swift
192 lines
6.1 KiB
Swift
import Foundation
|
|
import UIKit
|
|
#if os(iOS)
|
|
import SystemConfiguration
|
|
#endif
|
|
|
|
/// Device state collector
|
|
public struct DeviceStateCollector {
|
|
|
|
/// Collect current device state
|
|
public static func collect() -> DiagnosticsDeviceState {
|
|
#if os(iOS)
|
|
return DiagnosticsDeviceState(
|
|
memoryMB: getMemoryUsage(),
|
|
batteryLevel: getBatteryLevel(),
|
|
isCharging: getIsCharging(),
|
|
storageMB: getStorageUsage(),
|
|
networkType: getNetworkType(),
|
|
isOnline: getIsOnline(),
|
|
thermalState: getThermalState()
|
|
)
|
|
#elseif os(macOS)
|
|
return DiagnosticsDeviceState(
|
|
memoryMB: getMemoryUsage(),
|
|
batteryLevel: nil,
|
|
isCharging: nil,
|
|
storageMB: nil,
|
|
networkType: nil,
|
|
isOnline: getIsOnline(),
|
|
thermalState: nil
|
|
)
|
|
#else
|
|
return DiagnosticsDeviceState(
|
|
memoryMB: nil,
|
|
batteryLevel: nil,
|
|
isCharging: nil,
|
|
storageMB: nil,
|
|
networkType: nil,
|
|
isOnline: true,
|
|
thermalState: nil
|
|
)
|
|
#endif
|
|
}
|
|
|
|
#if os(iOS)
|
|
private static func getMemoryUsage() -> Int? {
|
|
var info = mach_task_basic_info()
|
|
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
|
|
|
|
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
|
|
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
|
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
|
}
|
|
}
|
|
|
|
guard kerr == KERN_SUCCESS else { return nil }
|
|
return Int(info.resident_size / 1024 / 1024)
|
|
}
|
|
|
|
private static func getBatteryLevel() -> Double? {
|
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
|
return Double(UIDevice.current.batteryLevel)
|
|
}
|
|
|
|
private static func getIsCharging() -> Bool? {
|
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
|
return UIDevice.current.batteryState == .charging
|
|
}
|
|
|
|
private static func getStorageUsage() -> Int? {
|
|
do {
|
|
let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
|
|
if let totalSize = attributes[.systemSize] as? NSNumber,
|
|
let freeSize = attributes[.systemFreeSize] as? NSNumber {
|
|
let usedSize = totalSize.int64Value - freeSize.int64Value
|
|
return Int(usedSize / 1024 / 1024)
|
|
}
|
|
} catch {
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func getNetworkType() -> String? {
|
|
// Simplified - would need more complex reachability check for actual implementation
|
|
if getIsOnline() {
|
|
return "wifi" // Default assumption
|
|
}
|
|
return "offline"
|
|
}
|
|
|
|
private static func getThermalState() -> DiagnosticsThermalState? {
|
|
switch ProcessInfo.processInfo.thermalState {
|
|
case .nominal:
|
|
return .nominal
|
|
case .fair:
|
|
return .fair
|
|
case .serious:
|
|
return .serious
|
|
case .critical:
|
|
return .critical
|
|
@unknown default:
|
|
return nil
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private static func getIsOnline() -> Bool {
|
|
#if os(iOS) || os(macOS)
|
|
var zeroAddress = sockaddr_in()
|
|
zeroAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
zeroAddress.sin_family = sa_family_t(AF_INET)
|
|
|
|
guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, {
|
|
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
SCNetworkReachabilityCreateWithAddress(nil, $0)
|
|
}
|
|
}) else {
|
|
return false
|
|
}
|
|
|
|
var flags: SCNetworkReachabilityFlags = []
|
|
if !SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) {
|
|
return false
|
|
}
|
|
|
|
let isReachable = flags.contains(.reachable)
|
|
let needsConnection = flags.contains(.connectionRequired)
|
|
|
|
return isReachable && !needsConnection
|
|
#else
|
|
return true
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Connectivity Monitoring
|
|
|
|
#if os(iOS) || os(macOS)
|
|
import SystemConfiguration
|
|
|
|
/// Monitor network connectivity changes
|
|
public final class ConnectivityMonitor {
|
|
private var reachability: SCNetworkReachability?
|
|
private var callback: ((Bool) -> Void)?
|
|
|
|
public init() {
|
|
var zeroAddress = sockaddr_in()
|
|
zeroAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
zeroAddress.sin_family = sa_family_t(AF_INET)
|
|
|
|
reachability = withUnsafePointer(to: &zeroAddress) {
|
|
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
SCNetworkReachabilityCreateWithAddress(nil, $0)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func startMonitoring(callback: @escaping (Bool) -> Void) {
|
|
self.callback = callback
|
|
|
|
guard let reachability = reachability else { return }
|
|
|
|
let context = SCNetworkReachabilityContext(
|
|
version: 0,
|
|
info: Unmanaged.passUnretained(self).toOpaque(),
|
|
retain: nil,
|
|
release: nil,
|
|
copyDescription: nil
|
|
)
|
|
|
|
SCNetworkReachabilitySetCallback(reachability, { (_, flags, info) in
|
|
guard let info = info else { return }
|
|
let monitor = Unmanaged<ConnectivityMonitor>.fromOpaque(info).takeUnretainedValue()
|
|
|
|
let isReachable = flags.contains(.reachable)
|
|
let needsConnection = flags.contains(.connectionRequired)
|
|
let isConnected = isReachable && !needsConnection
|
|
|
|
monitor.callback?(isConnected)
|
|
}, &context)
|
|
|
|
SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue)
|
|
}
|
|
|
|
public func stopMonitoring() {
|
|
guard let reachability = reachability else { return }
|
|
SCNetworkReachabilityUnscheduleFromRunLoop(reachability, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue)
|
|
}
|
|
}
|
|
#endif
|