├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SwiftyPing │ └── SwiftyPing.swift ├── SwiftyPingTest ├── SwiftyPing.swift ├── SwiftyPingTest.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── samiyrjanheikki.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── samiyrjanheikki.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── SwiftyPingTest │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift └── SwiftyPingTestMac │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── Main.storyboard │ ├── Info.plist │ ├── SwiftyPingTestMac.entitlements │ └── ViewController.swift └── Tests ├── LinuxMain.swift └── SwiftyPingTests ├── SwiftyPingTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | ehthumbs.db 7 | Thumbs.db 8 | 9 | SwiftyPingTest/SwiftyPingTest.xcodeproj/xcshareddata/ 10 | SwiftyPingTest/SwiftyPingTest.xcodeproj/xcuserdata/samiyrjanheikki.xcuserdatad/xcdebugger/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 imas145 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftyPing", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SwiftyPing", 12 | targets: ["SwiftyPing"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "SwiftyPing", 23 | dependencies: []), 24 | .testTarget( 25 | name: "SwiftyPingTests", 26 | dependencies: ["SwiftyPing"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftyPing 2 | ICMP ping client for Swift 5 3 | 4 | ### SwiftyPing is an easy-to-use, one file ICMP ping client 5 | This project is based on SwiftPing: https://github.com/ankitthakur/SwiftPing. 6 | 7 | ### Usage 8 | ```swift 9 | 10 | // Ping indefinitely 11 | let pinger = try? SwiftyPing(host: "1.1.1.1", configuration: PingConfiguration(interval: 0.5, with: 5), queue: DispatchQueue.global()) 12 | pinger?.observer = { (response) in 13 | let duration = response.duration 14 | print(duration) 15 | } 16 | try? pinger?.startPinging() 17 | 18 | // Ping once 19 | let once = try? SwiftyPing(host: "1.1.1.1", configuration: PingConfiguration(interval: 0.5, with: 5), queue: DispatchQueue.global()) 20 | once?.observer = { (response) in 21 | let duration = response.duration 22 | print(duration) 23 | } 24 | once?.targetCount = 1 25 | try? once?.startPinging() 26 | 27 | ``` 28 | ### Installation 29 | Just drop the SwiftyPing.swift file to your project. Using SwiftyPing for a Mac application requires allowing Network->Incoming Connections and Network->Outgoing Connections in the application sandbox. 30 | 31 | You can also use Swift Package Manager: 32 | 33 | ```swift 34 | .Package(url: "https://github.com/samiyr/SwiftyPing.git", branch: "master") 35 | ``` 36 | 37 | ### Future development and contributions 38 | I made this project based on what I need, so I probably won't be adding any features unless I really need them. I will maintain it (meaning bug fixes and support for new Swift versions) for some time at least. However, you can submit a pull request and I'll take a look. Please try to keep the overall coding style. 39 | 40 | ### Caveats 41 | This is low-level code, basically C code translated to Swift. This means that there are unsafe casts from raw bytes to Swift structs, for which Swift's usual type safety checks no longer apply. These can fail ungracefully (throwing an exception), and may even be used as an exploit (I'm not a security researcher and thus don't have the expertise to say for sure), so use with caution, especially if pinging untrusted hosts. 42 | 43 | Also, while I think that the API is now stable, I don't make any guarantees – some new version might break old stuff. 44 | 45 | ### License 46 | Use pretty much however you want. Officially licensed under MIT. 47 | -------------------------------------------------------------------------------- /Sources/SwiftyPing/SwiftyPing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyPing.swift 3 | // SwiftyPing 4 | // 5 | // Created by Sami Yrjänheikki on 6.8.2018. 6 | // Copyright © 2018 Sami Yrjänheikki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Darwin 11 | 12 | #if os(iOS) 13 | import UIKit 14 | #endif 15 | 16 | public typealias Observer = ((_ response: PingResponse) -> Void) 17 | public typealias FinishedCallback = ((_ result: PingResult) -> Void) 18 | 19 | /// Represents a ping delegate. 20 | public protocol PingDelegate { 21 | /// Called when a ping response is received. 22 | /// - Parameter response: A `PingResponse` object representing the echo reply. 23 | func didReceive(response: PingResponse) 24 | } 25 | 26 | /// Describes all possible errors thrown within `SwiftyPing` 27 | public enum PingError: Error, Equatable { 28 | // Response errors 29 | 30 | /// The response took longer to arrive than `configuration.timeoutInterval`. 31 | case responseTimeout 32 | 33 | // Response validation errors 34 | 35 | /// The response length was too short. 36 | case invalidLength(received: Int) 37 | /// The received checksum doesn't match the calculated one. 38 | case checksumMismatch(received: UInt16, calculated: UInt16) 39 | /// Response `type` was invalid. 40 | case invalidType(received: ICMPType.RawValue) 41 | /// Response `code` was invalid. 42 | case invalidCode(received: UInt8) 43 | /// Response `identifier` doesn't match what was sent. 44 | case identifierMismatch(received: UInt16, expected: UInt16) 45 | /// Response `sequenceNumber` doesn't match. 46 | case invalidSequenceIndex(received: UInt16, expected: UInt16) 47 | 48 | // Host resolve errors 49 | /// Unknown error occured within host lookup. 50 | case unknownHostError 51 | /// Address lookup failed. 52 | case addressLookupError 53 | /// Host was not found. 54 | case hostNotFound 55 | /// Address data could not be converted to `sockaddr`. 56 | case addressMemoryError 57 | 58 | // Request errors 59 | /// An error occured while sending the request. 60 | case requestError 61 | /// The request send timed out. Note that this is not "the" timeout, 62 | /// that would be `responseTimeout`. This timeout means that 63 | /// the ping request wasn't even sent within the timeout interval. 64 | case requestTimeout 65 | 66 | // Internal errors 67 | /// Checksum is out-of-bounds for `UInt16` in `computeCheckSum`. This shouldn't occur, but if it does, this error ensures that the app won't crash. 68 | case checksumOutOfBounds 69 | /// Unexpected payload length. 70 | case unexpectedPayloadLength 71 | /// Unspecified package creation error. 72 | case packageCreationFailed 73 | /// For some reason, the socket is `nil`. This shouldn't ever happen, but just in case... 74 | case socketNil 75 | /// The ICMP header offset couldn't be calculated. 76 | case invalidHeaderOffset 77 | /// Failed to change socket options, in particular SIGPIPE. 78 | case socketOptionsSetError(err: Int32) 79 | } 80 | 81 | // MARK: SwiftyPing 82 | 83 | /// Class representing socket info, which contains a `SwiftyPing` instance and the identifier. 84 | public class SocketInfo { 85 | public weak var pinger: SwiftyPing? 86 | public let identifier: UInt16 87 | 88 | public init(pinger: SwiftyPing, identifier: UInt16) { 89 | self.pinger = pinger 90 | self.identifier = identifier 91 | } 92 | } 93 | 94 | /// Represents a single ping instance. A ping instance has a single destination. 95 | public class SwiftyPing: NSObject { 96 | /// Describes the ping host destination. 97 | public struct Destination { 98 | /// The host name, can be a IP address or a URL. 99 | public let host: String 100 | /// IPv4 address of the host. 101 | public let ipv4Address: Data 102 | /// Socket address of `ipv4Address`. 103 | public var socketAddress: sockaddr_in? { return ipv4Address.socketAddressInternet } 104 | /// IP address of the host. 105 | public var ip: String? { 106 | guard let address = socketAddress else { return nil } 107 | return String(cString: inet_ntoa(address.sin_addr), encoding: .ascii) 108 | } 109 | 110 | /// Resolves the `host`. 111 | public static func getIPv4AddressFromHost(host: String) throws -> Data { 112 | var streamError = CFStreamError() 113 | let cfhost = CFHostCreateWithName(nil, host as CFString).takeRetainedValue() 114 | let status = CFHostStartInfoResolution(cfhost, .addresses, &streamError) 115 | 116 | var data: Data? 117 | if !status { 118 | if Int32(streamError.domain) == kCFStreamErrorDomainNetDB { 119 | throw PingError.addressLookupError 120 | } else { 121 | throw PingError.unknownHostError 122 | } 123 | } else { 124 | var success: DarwinBoolean = false 125 | guard let addresses = CFHostGetAddressing(cfhost, &success)?.takeUnretainedValue() as? [Data] else { 126 | throw PingError.hostNotFound 127 | } 128 | 129 | for address in addresses { 130 | let addrin = address.socketAddress 131 | if address.count >= MemoryLayout.size && addrin.sa_family == UInt8(AF_INET) { 132 | data = address 133 | break 134 | } 135 | } 136 | 137 | if data?.count == 0 || data == nil { 138 | throw PingError.hostNotFound 139 | } 140 | } 141 | guard let returnData = data else { throw PingError.unknownHostError } 142 | return returnData 143 | } 144 | 145 | } 146 | // MARK: - Initialization 147 | /// Ping host 148 | public let destination: Destination 149 | /// Ping configuration 150 | public let configuration: PingConfiguration 151 | /// This closure gets called with ping responses. 152 | public var observer: Observer? 153 | /// This closure gets called when pinging stops, either when `targetCount` is reached or pinging is stopped explicitly with `stop()` or `halt()`. 154 | public var finished: FinishedCallback? 155 | /// This delegate gets called with ping responses. 156 | public var delegate: PingDelegate? 157 | /// The number of pings to make. Default is `nil`, which means no limit. 158 | public var targetCount: Int? 159 | 160 | /// The current ping count, starting from 0. 161 | public var currentCount: UInt64 { 162 | return trueSequenceIndex 163 | } 164 | /// Array of all ping responses sent to the `observer`. 165 | public private(set) var responses: [PingResponse] = [] 166 | /// A random identifier which is a part of the ping request. 167 | private let identifier = UInt16.random(in: 0..? 179 | 180 | /// When the current request was sent. 181 | private var sequenceStart = Date() 182 | /// The current sequence number. 183 | private var _sequenceIndex: UInt16 = 0 184 | private var sequenceIndex: UInt16 { 185 | get { 186 | _serial_property.sync { self._sequenceIndex } 187 | } 188 | set { 189 | _serial_property.sync { self._sequenceIndex = newValue } 190 | } 191 | } 192 | /// The true sequence number. 193 | private var _trueSequenceIndex: UInt64 = 0 194 | private var trueSequenceIndex: UInt64 { 195 | get { 196 | _serial_property.sync { self._trueSequenceIndex } 197 | } 198 | set { 199 | _serial_property.sync { self._trueSequenceIndex = newValue } 200 | } 201 | } 202 | 203 | private var erroredIndices = [Int]() 204 | /// Initializes a pinger. 205 | /// - Parameter destination: Specifies the host. 206 | /// - Parameter configuration: A configuration object which can be used to customize pinging behavior. 207 | /// - Parameter queue: All responses are delivered through this dispatch queue. 208 | public init(destination: Destination, configuration: PingConfiguration, queue: DispatchQueue) throws { 209 | self.destination = destination 210 | self.configuration = configuration 211 | self.currentQueue = queue 212 | 213 | super.init() 214 | try createSocket() 215 | 216 | #if os(iOS) 217 | if configuration.handleBackgroundTransitions { 218 | addAppStateNotifications() 219 | } 220 | #endif 221 | } 222 | 223 | #if os(iOS) 224 | /// Adds notification observers for iOS app state changes. 225 | private func addAppStateNotifications() { 226 | NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) 227 | NotificationCenter.default.addObserver(self, selector: #selector(didEnterForeground), name: UIApplication.didBecomeActiveNotification, object: nil) 228 | } 229 | 230 | /// A flag to determine whether the pinger was halted automatically by an app state change. 231 | private var autoHalted = false 232 | /// Called on `UIApplication.didEnterBackgroundNotification`. 233 | @objc private func didEnterBackground() { 234 | autoHalted = true 235 | haltPinging(resetSequence: false) 236 | } 237 | /// Called on ` UIApplication.didBecomeActiveNotification`. 238 | @objc private func didEnterForeground() { 239 | if autoHalted { 240 | autoHalted = false 241 | try? startPinging() 242 | } 243 | } 244 | #endif 245 | 246 | // MARK: - Convenience Initializers 247 | /// Initializes a pinger from an IPv4 address string. 248 | /// - Parameter ipv4Address: The host's IP address. 249 | /// - Parameter configuration: A configuration object which can be used to customize pinging behavior. 250 | /// - Parameter queue: All responses are delivered through this dispatch queue. 251 | public convenience init(ipv4Address: String, config configuration: PingConfiguration, queue: DispatchQueue) throws { 252 | var socketAddress = sockaddr_in() 253 | 254 | socketAddress.sin_len = UInt8(MemoryLayout.size) 255 | socketAddress.sin_family = UInt8(AF_INET) 256 | socketAddress.sin_port = 0 257 | socketAddress.sin_addr.s_addr = inet_addr(ipv4Address.cString(using: .utf8)) 258 | let data = Data(bytes: &socketAddress, count: MemoryLayout.size) 259 | 260 | let destination = Destination(host: ipv4Address, ipv4Address: data) 261 | try self.init(destination: destination, configuration: configuration, queue: queue) 262 | } 263 | /// Initializes a pinger from a given host string. 264 | /// - Parameter host: A string describing the host. This can be an IP address or host name. 265 | /// - Parameter configuration: A configuration object which can be used to customize pinging behavior. 266 | /// - Parameter queue: All responses are delivered through this dispatch queue. 267 | /// - Throws: A `PingError` if the given host could not be resolved. 268 | public convenience init(host: String, configuration: PingConfiguration, queue: DispatchQueue) throws { 269 | let result = try Destination.getIPv4AddressFromHost(host: host) 270 | let destination = Destination(host: host, ipv4Address: result) 271 | try self.init(destination: destination, configuration: configuration, queue: queue) 272 | } 273 | 274 | /// Initializes a CFSocket. 275 | /// - Throws: If setting a socket options flag fails, throws a `PingError.socketOptionsSetError(:)`. 276 | private func createSocket() throws { 277 | try _serial.sync { 278 | // Create a socket context... 279 | let info = SocketInfo(pinger: self, identifier: identifier) 280 | unmanagedSocketInfo = Unmanaged.passRetained(info) 281 | var context = CFSocketContext(version: 0, info: unmanagedSocketInfo!.toOpaque(), retain: nil, release: nil, copyDescription: nil) 282 | 283 | // ...and a socket... 284 | socket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, CFSocketCallBackType.dataCallBack.rawValue, { socket, type, address, data, info in 285 | // Socket callback closure 286 | guard let socket = socket, let info = info, let data = data else { return } 287 | let socketInfo = Unmanaged.fromOpaque(info).takeUnretainedValue() 288 | let ping = socketInfo.pinger 289 | if (type as CFSocketCallBackType) == CFSocketCallBackType.dataCallBack { 290 | let cfdata = Unmanaged.fromOpaque(data).takeUnretainedValue() 291 | ping?.socket(socket: socket, didReadData: cfdata as Data) 292 | } 293 | }, &context) 294 | 295 | // Disable SIGPIPE, see issue #15 on GitHub. 296 | let handle = CFSocketGetNative(socket) 297 | var value: Int32 = 1 298 | let err = setsockopt(handle, SOL_SOCKET, SO_NOSIGPIPE, &value, socklen_t(MemoryLayout.size(ofValue: value))) 299 | guard err == 0 else { 300 | throw PingError.socketOptionsSetError(err: err) 301 | } 302 | 303 | // Set TTL 304 | if var ttl = configuration.timeToLive { 305 | let err = setsockopt(handle, IPPROTO_IP, IP_TTL, &ttl, socklen_t(MemoryLayout.size(ofValue: ttl))) 306 | guard err == 0 else { 307 | throw PingError.socketOptionsSetError(err: err) 308 | } 309 | } 310 | 311 | // ...and add it to the main run loop. 312 | socketSource = CFSocketCreateRunLoopSource(nil, socket, 0) 313 | CFRunLoopAddSource(CFRunLoopGetMain(), socketSource, .commonModes) 314 | } 315 | } 316 | 317 | // MARK: - Tear-down 318 | private func tearDown() { 319 | if socketSource != nil { 320 | CFRunLoopSourceInvalidate(socketSource) 321 | socketSource = nil 322 | } 323 | if socket != nil { 324 | CFSocketInvalidate(socket) 325 | socket = nil 326 | } 327 | unmanagedSocketInfo?.release() 328 | unmanagedSocketInfo = nil 329 | timeoutTimer?.invalidate() 330 | timeoutTimer = nil 331 | } 332 | deinit { 333 | tearDown() 334 | } 335 | 336 | // MARK: - Single ping 337 | 338 | private var _isPinging = false 339 | private var isPinging: Bool { 340 | get { 341 | return _serial_property.sync { self._isPinging } 342 | } 343 | set { 344 | _serial_property.sync { self._isPinging = newValue } 345 | } 346 | } 347 | 348 | private var _timeoutTimer: Timer? 349 | private var timeoutTimer: Timer? { 350 | get { 351 | return _serial_property.sync { self._timeoutTimer } 352 | } 353 | set { 354 | _serial_property.sync { self._timeoutTimer = newValue } 355 | } 356 | } 357 | 358 | private func sendPing() { 359 | if isPinging || killswitch { 360 | return 361 | } 362 | isPinging = true 363 | sequenceStart = Date() 364 | 365 | let timer = Timer(timeInterval: self.configuration.timeoutInterval, target: self, selector: #selector(self.timeout), userInfo: nil, repeats: false) 366 | RunLoop.main.add(timer, forMode: .common) 367 | self.timeoutTimer = timer 368 | 369 | _serial.async { 370 | let address = self.destination.ipv4Address 371 | do { 372 | let icmpPackage = try self.createICMPPackage(identifier: UInt16(self.identifier), sequenceNumber: UInt16(self.sequenceIndex)) 373 | 374 | guard let socket = self.socket else { return } 375 | let socketError = CFSocketSendData(socket, address as CFData, icmpPackage as CFData, self.configuration.timeoutInterval) 376 | 377 | if socketError != .success { 378 | var error: PingError? 379 | 380 | switch socketError { 381 | case .error: error = .requestError 382 | case .timeout: error = .requestTimeout 383 | default: break 384 | } 385 | let response = PingResponse(identifier: self.identifier, 386 | ipAddress: self.destination.ip, 387 | sequenceNumber: self.sequenceIndex, 388 | trueSequenceNumber: self.trueSequenceIndex, 389 | duration: self.timeIntervalSinceStart, 390 | error: error, 391 | byteCount: nil, 392 | ipHeader: nil) 393 | 394 | self.erroredIndices.append(Int(self.sequenceIndex)) 395 | self.isPinging = false 396 | self.informObserver(of: response) 397 | 398 | return self.scheduleNextPing() 399 | } 400 | } catch { 401 | let pingError: PingError 402 | if let err = error as? PingError { 403 | pingError = err 404 | } else { 405 | pingError = .packageCreationFailed 406 | } 407 | let response = PingResponse(identifier: self.identifier, 408 | ipAddress: self.destination.ip, 409 | sequenceNumber: self.sequenceIndex, 410 | trueSequenceNumber: self.trueSequenceIndex, 411 | duration: self.timeIntervalSinceStart, 412 | error: pingError, 413 | byteCount: nil, 414 | ipHeader: nil) 415 | self.erroredIndices.append(Int(self.sequenceIndex)) 416 | self.isPinging = false 417 | self.informObserver(of: response) 418 | 419 | return self.scheduleNextPing() 420 | } 421 | } 422 | } 423 | 424 | private var timeIntervalSinceStart: TimeInterval { 425 | return Date().timeIntervalSince(sequenceStart) 426 | } 427 | 428 | @objc private func timeout() { 429 | let error = PingError.responseTimeout 430 | let response = PingResponse(identifier: self.identifier, 431 | ipAddress: self.destination.ip, 432 | sequenceNumber: self.sequenceIndex, 433 | trueSequenceNumber: self.trueSequenceIndex, 434 | duration: timeIntervalSinceStart, 435 | error: error, 436 | byteCount: nil, 437 | ipHeader: nil) 438 | 439 | erroredIndices.append(Int(sequenceIndex)) 440 | self.isPinging = false 441 | informObserver(of: response) 442 | 443 | incrementSequenceIndex() 444 | scheduleNextPing() 445 | } 446 | 447 | private func informObserver(of response: PingResponse) { 448 | responses.append(response) 449 | if killswitch { return } 450 | currentQueue.sync { 451 | self.observer?(response) 452 | self.delegate?.didReceive(response: response) 453 | } 454 | } 455 | 456 | // MARK: - Continuous ping 457 | 458 | private func isTargetCountReached() -> Bool { 459 | if let target = targetCount { 460 | if sequenceIndex >= target { 461 | return true 462 | } 463 | } 464 | return false 465 | } 466 | 467 | private func shouldSchedulePing() -> Bool { 468 | if killswitch { return false } 469 | if isTargetCountReached() { return false } 470 | return true 471 | } 472 | private func scheduleNextPing() { 473 | if isTargetCountReached() { 474 | if configuration.haltAfterTarget { 475 | haltPinging() 476 | } else { 477 | informFinishedStatus(trueSequenceIndex) 478 | } 479 | } 480 | if shouldSchedulePing() { 481 | _serial.asyncAfter(deadline: .now() + configuration.pingInterval) { 482 | self.sendPing() 483 | } 484 | } 485 | } 486 | private func informFinishedStatus(_ sequenceIndex: UInt64) { 487 | if let callback = finished { 488 | var roundtrip: PingResult.Roundtrip? = nil 489 | let roundtripTimes = responses.filter { $0.error == nil }.map { $0.duration } 490 | if roundtripTimes.count != 0, let min = roundtripTimes.min(), let max = roundtripTimes.max() { 491 | let count = Double(roundtripTimes.count) 492 | let total = roundtripTimes.reduce(0, +) 493 | let avg = total / count 494 | let variance = roundtripTimes.reduce(0, { $0 + ($1 - avg) * ($1 - avg) }) 495 | let stddev = sqrt(variance / count) 496 | 497 | roundtrip = PingResult.Roundtrip(minimum: min, maximum: max, average: avg, standardDeviation: stddev) 498 | } 499 | 500 | let result = PingResult(responses: responses, packetsTransmitted: sequenceIndex, packetsReceived: UInt64(roundtripTimes.count), roundtrip: roundtrip) 501 | callback(result) 502 | } 503 | } 504 | 505 | private let _serial = DispatchQueue(label: "SwiftyPing internal") 506 | private let _serial_property = DispatchQueue(label: "SwiftyPing internal property") 507 | 508 | private var _killswitch = false 509 | private var killswitch: Bool { 510 | get { 511 | return _serial_property.sync { self._killswitch } 512 | } 513 | set { 514 | _serial_property.sync { self._killswitch = newValue } 515 | } 516 | } 517 | 518 | /// Start pinging the host. 519 | public func startPinging() throws { 520 | if socket == nil { 521 | try createSocket() 522 | } 523 | killswitch = false 524 | sendPing() 525 | } 526 | 527 | /// Stop pinging the host. 528 | /// - Parameter resetSequence: Controls whether the sequence index should be set back to zero. 529 | public func stopPinging(resetSequence: Bool = true) { 530 | killswitch = true 531 | isPinging = false 532 | let count = trueSequenceIndex 533 | if resetSequence { 534 | sequenceIndex = 0 535 | trueSequenceIndex = 0 536 | erroredIndices.removeAll() 537 | } 538 | informFinishedStatus(count) 539 | } 540 | /// Stops pinging the host and destroys the CFSocket object. 541 | /// - Parameter resetSequence: Controls whether the sequence index should be set back to zero. 542 | public func haltPinging(resetSequence: Bool = true) { 543 | stopPinging(resetSequence: resetSequence) 544 | tearDown() 545 | } 546 | 547 | private func incrementSequenceIndex() { 548 | // Handle overflow gracefully 549 | if sequenceIndex >= UInt16.max { 550 | sequenceIndex = 0 551 | } else { 552 | sequenceIndex += 1 553 | } 554 | 555 | if trueSequenceIndex >= UInt64.max { 556 | trueSequenceIndex = 0 557 | } else { 558 | trueSequenceIndex += 1 559 | } 560 | } 561 | 562 | // MARK: - Socket callback 563 | private func socket(socket: CFSocket, didReadData data: Data?) { 564 | if killswitch { return } 565 | 566 | guard let data = data else { return } 567 | var validationError: PingError? = nil 568 | 569 | do { 570 | let validation = try validateResponse(from: data) 571 | if !validation { return } 572 | } catch let error as PingError { 573 | validationError = error 574 | } catch { 575 | print("Unhandled error thrown: \(error)") 576 | } 577 | 578 | timeoutTimer?.invalidate() 579 | var ipHeader: IPHeader? = nil 580 | if validationError == nil { 581 | ipHeader = data.withUnsafeBytes({ $0.load(as: IPHeader.self) }) 582 | } 583 | let response = PingResponse(identifier: identifier, 584 | ipAddress: destination.ip, 585 | sequenceNumber: sequenceIndex, 586 | trueSequenceNumber: trueSequenceIndex, 587 | duration: timeIntervalSinceStart, 588 | error: validationError, 589 | byteCount: data.count, 590 | ipHeader: ipHeader) 591 | isPinging = false 592 | informObserver(of: response) 593 | 594 | incrementSequenceIndex() 595 | scheduleNextPing() 596 | } 597 | 598 | // MARK: - ICMP package 599 | 600 | /// Creates an ICMP package. 601 | private func createICMPPackage(identifier: UInt16, sequenceNumber: UInt16) throws -> Data { 602 | var header = ICMPHeader(type: ICMPType.EchoRequest.rawValue, 603 | code: 0, 604 | checksum: 0, 605 | identifier: CFSwapInt16HostToBig(identifier), 606 | sequenceNumber: CFSwapInt16HostToBig(sequenceNumber), 607 | payload: fingerprint.uuid) 608 | 609 | let delta = configuration.payloadSize - MemoryLayout.size 610 | var additional = [UInt8]() 611 | if delta > 0 { 612 | additional = (0...size) + Data(additional) 619 | return package 620 | } 621 | 622 | private func computeChecksum(header: ICMPHeader, additionalPayload: [UInt8]) throws -> UInt16 { 623 | let typecode = Data([header.type, header.code]).withUnsafeBytes { $0.load(as: UInt16.self) } 624 | var sum = UInt64(typecode) + UInt64(header.identifier) + UInt64(header.sequenceNumber) 625 | let payload = convert(payload: header.payload) + additionalPayload 626 | 627 | guard payload.count % 2 == 0 else { throw PingError.unexpectedPayloadLength } 628 | 629 | var i = 0 630 | while i < payload.count { 631 | guard payload.indices.contains(i + 1) else { throw PingError.unexpectedPayloadLength } 632 | // Convert two 8 byte ints to one 16 byte int 633 | sum += Data([payload[i], payload[i + 1]]).withUnsafeBytes { UInt64($0.load(as: UInt16.self)) } 634 | i += 2 635 | } 636 | while sum >> 16 != 0 { 637 | sum = (sum & 0xffff) + (sum >> 16) 638 | } 639 | 640 | guard sum < UInt16.max else { throw PingError.checksumOutOfBounds } 641 | 642 | return ~UInt16(sum) 643 | } 644 | 645 | private func icmpHeaderOffset(of packet: Data) -> Int? { 646 | if packet.count >= MemoryLayout.size + MemoryLayout.size { 647 | let ipHeader = packet.withUnsafeBytes({ $0.load(as: IPHeader.self) }) 648 | if ipHeader.versionAndHeaderLength & 0xF0 == 0x40 && ipHeader.protocol == IPPROTO_ICMP { 649 | let headerLength = Int(ipHeader.versionAndHeaderLength) & 0x0F * MemoryLayout.size 650 | if packet.count >= headerLength + MemoryLayout.size { 651 | return headerLength 652 | } 653 | } 654 | } 655 | return nil 656 | } 657 | 658 | private func convert(payload: uuid_t) -> [UInt8] { 659 | let p = payload 660 | return [p.0, p.1, p.2, p.3, p.4, p.5, p.6, p.7, p.8, p.9, p.10, p.11, p.12, p.13, p.14, p.15].map { UInt8($0) } 661 | } 662 | 663 | private func validateResponse(from data: Data) throws -> Bool { 664 | guard data.count >= MemoryLayout.size + MemoryLayout.size else { 665 | throw PingError.invalidLength(received: data.count) 666 | } 667 | 668 | guard let headerOffset = icmpHeaderOffset(of: data) else { throw PingError.invalidHeaderOffset } 669 | let payloadSize = data.count - headerOffset - MemoryLayout.size 670 | let icmpHeader = data.withUnsafeBytes({ $0.load(fromByteOffset: headerOffset, as: ICMPHeader.self) }) 671 | let payload = data.subdata(in: (data.count - payloadSize)...size 808 | /// If set to `true`, when `targetCount` is reached (if set), the pinging will be halted instead of stopped. This means that the socket will be released and will be recreated if more pings are requested. Defaults to `true`. 809 | public var haltAfterTarget: Bool = true 810 | 811 | /// Initializes a `PingConfiguration` object with the given parameters. 812 | /// - Parameter interval: The time between consecutive pings in seconds. Defaults to 1. 813 | /// - Parameter timeout: Timeout interval in seconds. Defaults to 5. 814 | public init(interval: TimeInterval = 1, with timeout: TimeInterval = 5) { 815 | pingInterval = interval 816 | timeoutInterval = timeout 817 | } 818 | /// Initializes a `PingConfiguration` object with the given interval. 819 | /// - Parameter interval: The time between consecutive pings in seconds. 820 | /// - Note: Timeout interval will be set to 5 seconds. 821 | public init(interval: TimeInterval) { 822 | self.init(interval: interval, with: 5) 823 | } 824 | } 825 | 826 | // MARK: - Data Extensions 827 | 828 | public extension Data { 829 | /// Expresses a chunk of data as a socket address. 830 | var socketAddress: sockaddr { 831 | return withUnsafeBytes { $0.load(as: sockaddr.self) } 832 | } 833 | /// Expresses a chunk of data as an internet-style socket address. 834 | var socketAddressInternet: sockaddr_in { 835 | return withUnsafeBytes { $0.load(as: sockaddr_in.self) } 836 | } 837 | } 838 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyPing.swift 3 | // SwiftyPing 4 | // 5 | // Created by Sami Yrjänheikki on 6.8.2018. 6 | // Copyright © 2018 Sami Yrjänheikki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Darwin 11 | 12 | #if os(iOS) 13 | import UIKit 14 | #endif 15 | 16 | public typealias Observer = ((_ response: PingResponse) -> Void) 17 | public typealias FinishedCallback = ((_ result: PingResult) -> Void) 18 | 19 | /// Represents a ping delegate. 20 | public protocol PingDelegate { 21 | /// Called when a ping response is received. 22 | /// - Parameter response: A `PingResponse` object representing the echo reply. 23 | func didReceive(response: PingResponse) 24 | } 25 | 26 | /// Describes all possible errors thrown within `SwiftyPing` 27 | public enum PingError: Error, Equatable { 28 | // Response errors 29 | 30 | /// The response took longer to arrive than `configuration.timeoutInterval`. 31 | case responseTimeout 32 | 33 | // Response validation errors 34 | 35 | /// The response length was too short. 36 | case invalidLength(received: Int) 37 | /// The received checksum doesn't match the calculated one. 38 | case checksumMismatch(received: UInt16, calculated: UInt16) 39 | /// Response `type` was invalid. 40 | case invalidType(received: ICMPType.RawValue) 41 | /// Response `code` was invalid. 42 | case invalidCode(received: UInt8) 43 | /// Response `identifier` doesn't match what was sent. 44 | case identifierMismatch(received: UInt16, expected: UInt16) 45 | /// Response `sequenceNumber` doesn't match. 46 | case invalidSequenceIndex(received: UInt16, expected: UInt16) 47 | 48 | // Host resolve errors 49 | /// Unknown error occured within host lookup. 50 | case unknownHostError 51 | /// Address lookup failed. 52 | case addressLookupError 53 | /// Host was not found. 54 | case hostNotFound 55 | /// Address data could not be converted to `sockaddr`. 56 | case addressMemoryError 57 | 58 | // Request errors 59 | /// An error occured while sending the request. 60 | case requestError 61 | /// The request send timed out. Note that this is not "the" timeout, 62 | /// that would be `responseTimeout`. This timeout means that 63 | /// the ping request wasn't even sent within the timeout interval. 64 | case requestTimeout 65 | 66 | // Internal errors 67 | /// Checksum is out-of-bounds for `UInt16` in `computeCheckSum`. This shouldn't occur, but if it does, this error ensures that the app won't crash. 68 | case checksumOutOfBounds 69 | /// Unexpected payload length. 70 | case unexpectedPayloadLength 71 | /// Unspecified package creation error. 72 | case packageCreationFailed 73 | /// For some reason, the socket is `nil`. This shouldn't ever happen, but just in case... 74 | case socketNil 75 | /// The ICMP header offset couldn't be calculated. 76 | case invalidHeaderOffset 77 | /// Failed to change socket options, in particular SIGPIPE. 78 | case socketOptionsSetError(err: Int32) 79 | } 80 | 81 | // MARK: SwiftyPing 82 | 83 | /// Class representing socket info, which contains a `SwiftyPing` instance and the identifier. 84 | public class SocketInfo { 85 | public weak var pinger: SwiftyPing? 86 | public let identifier: UInt16 87 | 88 | public init(pinger: SwiftyPing, identifier: UInt16) { 89 | self.pinger = pinger 90 | self.identifier = identifier 91 | } 92 | } 93 | 94 | /// Represents a single ping instance. A ping instance has a single destination. 95 | public class SwiftyPing: NSObject { 96 | /// Describes the ping host destination. 97 | public struct Destination { 98 | /// The host name, can be a IP address or a URL. 99 | public let host: String 100 | /// IPv4 address of the host. 101 | public let ipv4Address: Data 102 | /// Socket address of `ipv4Address`. 103 | public var socketAddress: sockaddr_in? { return ipv4Address.socketAddressInternet } 104 | /// IP address of the host. 105 | public var ip: String? { 106 | guard let address = socketAddress else { return nil } 107 | return String(cString: inet_ntoa(address.sin_addr), encoding: .ascii) 108 | } 109 | 110 | /// Resolves the `host`. 111 | public static func getIPv4AddressFromHost(host: String) throws -> Data { 112 | var streamError = CFStreamError() 113 | let cfhost = CFHostCreateWithName(nil, host as CFString).takeRetainedValue() 114 | let status = CFHostStartInfoResolution(cfhost, .addresses, &streamError) 115 | 116 | var data: Data? 117 | if !status { 118 | if Int32(streamError.domain) == kCFStreamErrorDomainNetDB { 119 | throw PingError.addressLookupError 120 | } else { 121 | throw PingError.unknownHostError 122 | } 123 | } else { 124 | var success: DarwinBoolean = false 125 | guard let addresses = CFHostGetAddressing(cfhost, &success)?.takeUnretainedValue() as? [Data] else { 126 | throw PingError.hostNotFound 127 | } 128 | 129 | for address in addresses { 130 | let addrin = address.socketAddress 131 | if address.count >= MemoryLayout.size && addrin.sa_family == UInt8(AF_INET) { 132 | data = address 133 | break 134 | } 135 | } 136 | 137 | if data?.count == 0 || data == nil { 138 | throw PingError.hostNotFound 139 | } 140 | } 141 | guard let returnData = data else { throw PingError.unknownHostError } 142 | return returnData 143 | } 144 | 145 | } 146 | // MARK: - Initialization 147 | /// Ping host 148 | public let destination: Destination 149 | /// Ping configuration 150 | public let configuration: PingConfiguration 151 | /// This closure gets called with ping responses. 152 | public var observer: Observer? 153 | /// This closure gets called when pinging stops, either when `targetCount` is reached or pinging is stopped explicitly with `stop()` or `halt()`. 154 | public var finished: FinishedCallback? 155 | /// This delegate gets called with ping responses. 156 | public var delegate: PingDelegate? 157 | /// The number of pings to make. Default is `nil`, which means no limit. 158 | public var targetCount: Int? 159 | 160 | /// The current ping count, starting from 0. 161 | public var currentCount: UInt64 { 162 | return trueSequenceIndex 163 | } 164 | /// Array of all ping responses sent to the `observer`. 165 | public private(set) var responses: [PingResponse] = [] 166 | /// A random identifier which is a part of the ping request. 167 | private let identifier = UInt16.random(in: 0..? 179 | 180 | /// When the current request was sent. 181 | private var sequenceStart = Date() 182 | /// The current sequence number. 183 | private var _sequenceIndex: UInt16 = 0 184 | private var sequenceIndex: UInt16 { 185 | get { 186 | _serial_property.sync { self._sequenceIndex } 187 | } 188 | set { 189 | _serial_property.sync { self._sequenceIndex = newValue } 190 | } 191 | } 192 | /// The true sequence number. 193 | private var _trueSequenceIndex: UInt64 = 0 194 | private var trueSequenceIndex: UInt64 { 195 | get { 196 | _serial_property.sync { self._trueSequenceIndex } 197 | } 198 | set { 199 | _serial_property.sync { self._trueSequenceIndex = newValue } 200 | } 201 | } 202 | 203 | private var erroredIndices = [Int]() 204 | /// Initializes a pinger. 205 | /// - Parameter destination: Specifies the host. 206 | /// - Parameter configuration: A configuration object which can be used to customize pinging behavior. 207 | /// - Parameter queue: All responses are delivered through this dispatch queue. 208 | public init(destination: Destination, configuration: PingConfiguration, queue: DispatchQueue) throws { 209 | self.destination = destination 210 | self.configuration = configuration 211 | self.currentQueue = queue 212 | 213 | super.init() 214 | try createSocket() 215 | 216 | #if os(iOS) 217 | if configuration.handleBackgroundTransitions { 218 | addAppStateNotifications() 219 | } 220 | #endif 221 | } 222 | 223 | #if os(iOS) 224 | /// Adds notification observers for iOS app state changes. 225 | private func addAppStateNotifications() { 226 | NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) 227 | NotificationCenter.default.addObserver(self, selector: #selector(didEnterForeground), name: UIApplication.didBecomeActiveNotification, object: nil) 228 | } 229 | 230 | /// A flag to determine whether the pinger was halted automatically by an app state change. 231 | private var autoHalted = false 232 | /// Called on `UIApplication.didEnterBackgroundNotification`. 233 | @objc private func didEnterBackground() { 234 | autoHalted = true 235 | haltPinging(resetSequence: false) 236 | } 237 | /// Called on ` UIApplication.didBecomeActiveNotification`. 238 | @objc private func didEnterForeground() { 239 | if autoHalted { 240 | autoHalted = false 241 | try? startPinging() 242 | } 243 | } 244 | #endif 245 | 246 | // MARK: - Convenience Initializers 247 | /// Initializes a pinger from an IPv4 address string. 248 | /// - Parameter ipv4Address: The host's IP address. 249 | /// - Parameter configuration: A configuration object which can be used to customize pinging behavior. 250 | /// - Parameter queue: All responses are delivered through this dispatch queue. 251 | public convenience init(ipv4Address: String, config configuration: PingConfiguration, queue: DispatchQueue) throws { 252 | var socketAddress = sockaddr_in() 253 | 254 | socketAddress.sin_len = UInt8(MemoryLayout.size) 255 | socketAddress.sin_family = UInt8(AF_INET) 256 | socketAddress.sin_port = 0 257 | socketAddress.sin_addr.s_addr = inet_addr(ipv4Address.cString(using: .utf8)) 258 | let data = Data(bytes: &socketAddress, count: MemoryLayout.size) 259 | 260 | let destination = Destination(host: ipv4Address, ipv4Address: data) 261 | try self.init(destination: destination, configuration: configuration, queue: queue) 262 | } 263 | /// Initializes a pinger from a given host string. 264 | /// - Parameter host: A string describing the host. This can be an IP address or host name. 265 | /// - Parameter configuration: A configuration object which can be used to customize pinging behavior. 266 | /// - Parameter queue: All responses are delivered through this dispatch queue. 267 | /// - Throws: A `PingError` if the given host could not be resolved. 268 | public convenience init(host: String, configuration: PingConfiguration, queue: DispatchQueue) throws { 269 | let result = try Destination.getIPv4AddressFromHost(host: host) 270 | let destination = Destination(host: host, ipv4Address: result) 271 | try self.init(destination: destination, configuration: configuration, queue: queue) 272 | } 273 | 274 | /// Initializes a CFSocket. 275 | /// - Throws: If setting a socket options flag fails, throws a `PingError.socketOptionsSetError(:)`. 276 | private func createSocket() throws { 277 | try _serial.sync { 278 | // Create a socket context... 279 | let info = SocketInfo(pinger: self, identifier: identifier) 280 | unmanagedSocketInfo = Unmanaged.passRetained(info) 281 | var context = CFSocketContext(version: 0, info: unmanagedSocketInfo!.toOpaque(), retain: nil, release: nil, copyDescription: nil) 282 | 283 | // ...and a socket... 284 | socket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, CFSocketCallBackType.dataCallBack.rawValue, { socket, type, address, data, info in 285 | // Socket callback closure 286 | guard let socket = socket, let info = info, let data = data else { return } 287 | let socketInfo = Unmanaged.fromOpaque(info).takeUnretainedValue() 288 | let ping = socketInfo.pinger 289 | if (type as CFSocketCallBackType) == CFSocketCallBackType.dataCallBack { 290 | let cfdata = Unmanaged.fromOpaque(data).takeUnretainedValue() 291 | ping?.socket(socket: socket, didReadData: cfdata as Data) 292 | } 293 | }, &context) 294 | 295 | // Disable SIGPIPE, see issue #15 on GitHub. 296 | let handle = CFSocketGetNative(socket) 297 | var value: Int32 = 1 298 | let err = setsockopt(handle, SOL_SOCKET, SO_NOSIGPIPE, &value, socklen_t(MemoryLayout.size(ofValue: value))) 299 | guard err == 0 else { 300 | throw PingError.socketOptionsSetError(err: err) 301 | } 302 | 303 | // Set TTL 304 | if var ttl = configuration.timeToLive { 305 | let err = setsockopt(handle, IPPROTO_IP, IP_TTL, &ttl, socklen_t(MemoryLayout.size(ofValue: ttl))) 306 | guard err == 0 else { 307 | throw PingError.socketOptionsSetError(err: err) 308 | } 309 | } 310 | 311 | // ...and add it to the main run loop. 312 | socketSource = CFSocketCreateRunLoopSource(nil, socket, 0) 313 | CFRunLoopAddSource(CFRunLoopGetMain(), socketSource, .commonModes) 314 | } 315 | } 316 | 317 | // MARK: - Tear-down 318 | private func tearDown() { 319 | if socketSource != nil { 320 | CFRunLoopSourceInvalidate(socketSource) 321 | socketSource = nil 322 | } 323 | if socket != nil { 324 | CFSocketInvalidate(socket) 325 | socket = nil 326 | } 327 | unmanagedSocketInfo?.release() 328 | unmanagedSocketInfo = nil 329 | timeoutTimer?.invalidate() 330 | timeoutTimer = nil 331 | } 332 | deinit { 333 | tearDown() 334 | } 335 | 336 | // MARK: - Single ping 337 | 338 | private var _isPinging = false 339 | private var isPinging: Bool { 340 | get { 341 | return _serial_property.sync { self._isPinging } 342 | } 343 | set { 344 | _serial_property.sync { self._isPinging = newValue } 345 | } 346 | } 347 | 348 | private var _timeoutTimer: Timer? 349 | private var timeoutTimer: Timer? { 350 | get { 351 | return _serial_property.sync { self._timeoutTimer } 352 | } 353 | set { 354 | _serial_property.sync { self._timeoutTimer = newValue } 355 | } 356 | } 357 | 358 | private func sendPing() { 359 | if isPinging || killswitch { 360 | return 361 | } 362 | isPinging = true 363 | sequenceStart = Date() 364 | 365 | let timer = Timer(timeInterval: self.configuration.timeoutInterval, target: self, selector: #selector(self.timeout), userInfo: nil, repeats: false) 366 | RunLoop.main.add(timer, forMode: .common) 367 | self.timeoutTimer = timer 368 | 369 | _serial.async { 370 | let address = self.destination.ipv4Address 371 | do { 372 | let icmpPackage = try self.createICMPPackage(identifier: UInt16(self.identifier), sequenceNumber: UInt16(self.sequenceIndex)) 373 | 374 | guard let socket = self.socket else { return } 375 | let socketError = CFSocketSendData(socket, address as CFData, icmpPackage as CFData, self.configuration.timeoutInterval) 376 | 377 | if socketError != .success { 378 | var error: PingError? 379 | 380 | switch socketError { 381 | case .error: error = .requestError 382 | case .timeout: error = .requestTimeout 383 | default: break 384 | } 385 | let response = PingResponse(identifier: self.identifier, 386 | ipAddress: self.destination.ip, 387 | sequenceNumber: self.sequenceIndex, 388 | trueSequenceNumber: self.trueSequenceIndex, 389 | duration: self.timeIntervalSinceStart, 390 | error: error, 391 | byteCount: nil, 392 | ipHeader: nil) 393 | 394 | self.erroredIndices.append(Int(self.sequenceIndex)) 395 | self.isPinging = false 396 | self.informObserver(of: response) 397 | 398 | return self.scheduleNextPing() 399 | } 400 | } catch { 401 | let pingError: PingError 402 | if let err = error as? PingError { 403 | pingError = err 404 | } else { 405 | pingError = .packageCreationFailed 406 | } 407 | let response = PingResponse(identifier: self.identifier, 408 | ipAddress: self.destination.ip, 409 | sequenceNumber: self.sequenceIndex, 410 | trueSequenceNumber: self.trueSequenceIndex, 411 | duration: self.timeIntervalSinceStart, 412 | error: pingError, 413 | byteCount: nil, 414 | ipHeader: nil) 415 | self.erroredIndices.append(Int(self.sequenceIndex)) 416 | self.isPinging = false 417 | self.informObserver(of: response) 418 | 419 | return self.scheduleNextPing() 420 | } 421 | } 422 | } 423 | 424 | private var timeIntervalSinceStart: TimeInterval { 425 | return Date().timeIntervalSince(sequenceStart) 426 | } 427 | 428 | @objc private func timeout() { 429 | let error = PingError.responseTimeout 430 | let response = PingResponse(identifier: self.identifier, 431 | ipAddress: self.destination.ip, 432 | sequenceNumber: self.sequenceIndex, 433 | trueSequenceNumber: self.trueSequenceIndex, 434 | duration: timeIntervalSinceStart, 435 | error: error, 436 | byteCount: nil, 437 | ipHeader: nil) 438 | 439 | erroredIndices.append(Int(sequenceIndex)) 440 | self.isPinging = false 441 | informObserver(of: response) 442 | 443 | incrementSequenceIndex() 444 | scheduleNextPing() 445 | } 446 | 447 | private func informObserver(of response: PingResponse) { 448 | responses.append(response) 449 | if killswitch { return } 450 | currentQueue.sync { 451 | self.observer?(response) 452 | self.delegate?.didReceive(response: response) 453 | } 454 | } 455 | 456 | // MARK: - Continuous ping 457 | 458 | private func isTargetCountReached() -> Bool { 459 | if let target = targetCount { 460 | if sequenceIndex >= target { 461 | return true 462 | } 463 | } 464 | return false 465 | } 466 | 467 | private func shouldSchedulePing() -> Bool { 468 | if killswitch { return false } 469 | if isTargetCountReached() { return false } 470 | return true 471 | } 472 | private func scheduleNextPing() { 473 | if isTargetCountReached() { 474 | if configuration.haltAfterTarget { 475 | haltPinging() 476 | } else { 477 | informFinishedStatus(trueSequenceIndex) 478 | } 479 | } 480 | if shouldSchedulePing() { 481 | _serial.asyncAfter(deadline: .now() + configuration.pingInterval) { 482 | self.sendPing() 483 | } 484 | } 485 | } 486 | private func informFinishedStatus(_ sequenceIndex: UInt64) { 487 | if let callback = finished { 488 | var roundtrip: PingResult.Roundtrip? = nil 489 | let roundtripTimes = responses.filter { $0.error == nil }.map { $0.duration } 490 | if roundtripTimes.count != 0, let min = roundtripTimes.min(), let max = roundtripTimes.max() { 491 | let count = Double(roundtripTimes.count) 492 | let total = roundtripTimes.reduce(0, +) 493 | let avg = total / count 494 | let variance = roundtripTimes.reduce(0, { $0 + ($1 - avg) * ($1 - avg) }) 495 | let stddev = sqrt(variance / count) 496 | 497 | roundtrip = PingResult.Roundtrip(minimum: min, maximum: max, average: avg, standardDeviation: stddev) 498 | } 499 | 500 | let result = PingResult(responses: responses, packetsTransmitted: sequenceIndex, packetsReceived: UInt64(roundtripTimes.count), roundtrip: roundtrip) 501 | callback(result) 502 | } 503 | } 504 | 505 | private let _serial = DispatchQueue(label: "SwiftyPing internal") 506 | private let _serial_property = DispatchQueue(label: "SwiftyPing internal property") 507 | 508 | private var _killswitch = false 509 | private var killswitch: Bool { 510 | get { 511 | return _serial_property.sync { self._killswitch } 512 | } 513 | set { 514 | _serial_property.sync { self._killswitch = newValue } 515 | } 516 | } 517 | 518 | /// Start pinging the host. 519 | public func startPinging() throws { 520 | if socket == nil { 521 | try createSocket() 522 | } 523 | killswitch = false 524 | sendPing() 525 | } 526 | 527 | /// Stop pinging the host. 528 | /// - Parameter resetSequence: Controls whether the sequence index should be set back to zero. 529 | public func stopPinging(resetSequence: Bool = true) { 530 | killswitch = true 531 | isPinging = false 532 | let count = trueSequenceIndex 533 | if resetSequence { 534 | sequenceIndex = 0 535 | trueSequenceIndex = 0 536 | erroredIndices.removeAll() 537 | } 538 | informFinishedStatus(count) 539 | } 540 | /// Stops pinging the host and destroys the CFSocket object. 541 | /// - Parameter resetSequence: Controls whether the sequence index should be set back to zero. 542 | public func haltPinging(resetSequence: Bool = true) { 543 | stopPinging(resetSequence: resetSequence) 544 | tearDown() 545 | } 546 | 547 | private func incrementSequenceIndex() { 548 | // Handle overflow gracefully 549 | if sequenceIndex >= UInt16.max { 550 | sequenceIndex = 0 551 | } else { 552 | sequenceIndex += 1 553 | } 554 | 555 | if trueSequenceIndex >= UInt64.max { 556 | trueSequenceIndex = 0 557 | } else { 558 | trueSequenceIndex += 1 559 | } 560 | } 561 | 562 | // MARK: - Socket callback 563 | private func socket(socket: CFSocket, didReadData data: Data?) { 564 | if killswitch { return } 565 | 566 | guard let data = data else { return } 567 | var validationError: PingError? = nil 568 | 569 | do { 570 | let validation = try validateResponse(from: data) 571 | if !validation { return } 572 | } catch let error as PingError { 573 | validationError = error 574 | } catch { 575 | print("Unhandled error thrown: \(error)") 576 | } 577 | 578 | timeoutTimer?.invalidate() 579 | var ipHeader: IPHeader? = nil 580 | if validationError == nil { 581 | ipHeader = data.withUnsafeBytes({ $0.load(as: IPHeader.self) }) 582 | } 583 | let response = PingResponse(identifier: identifier, 584 | ipAddress: destination.ip, 585 | sequenceNumber: sequenceIndex, 586 | trueSequenceNumber: trueSequenceIndex, 587 | duration: timeIntervalSinceStart, 588 | error: validationError, 589 | byteCount: data.count, 590 | ipHeader: ipHeader) 591 | isPinging = false 592 | informObserver(of: response) 593 | 594 | incrementSequenceIndex() 595 | scheduleNextPing() 596 | } 597 | 598 | // MARK: - ICMP package 599 | 600 | /// Creates an ICMP package. 601 | private func createICMPPackage(identifier: UInt16, sequenceNumber: UInt16) throws -> Data { 602 | var header = ICMPHeader(type: ICMPType.EchoRequest.rawValue, 603 | code: 0, 604 | checksum: 0, 605 | identifier: CFSwapInt16HostToBig(identifier), 606 | sequenceNumber: CFSwapInt16HostToBig(sequenceNumber), 607 | payload: fingerprint.uuid) 608 | 609 | let delta = configuration.payloadSize - MemoryLayout.size 610 | var additional = [UInt8]() 611 | if delta > 0 { 612 | additional = (0...size) + Data(additional) 619 | return package 620 | } 621 | 622 | private func computeChecksum(header: ICMPHeader, additionalPayload: [UInt8]) throws -> UInt16 { 623 | let typecode = Data([header.type, header.code]).withUnsafeBytes { $0.load(as: UInt16.self) } 624 | var sum = UInt64(typecode) + UInt64(header.identifier) + UInt64(header.sequenceNumber) 625 | let payload = convert(payload: header.payload) + additionalPayload 626 | 627 | guard payload.count % 2 == 0 else { throw PingError.unexpectedPayloadLength } 628 | 629 | var i = 0 630 | while i < payload.count { 631 | guard payload.indices.contains(i + 1) else { throw PingError.unexpectedPayloadLength } 632 | // Convert two 8 byte ints to one 16 byte int 633 | sum += Data([payload[i], payload[i + 1]]).withUnsafeBytes { UInt64($0.load(as: UInt16.self)) } 634 | i += 2 635 | } 636 | while sum >> 16 != 0 { 637 | sum = (sum & 0xffff) + (sum >> 16) 638 | } 639 | 640 | guard sum < UInt16.max else { throw PingError.checksumOutOfBounds } 641 | 642 | return ~UInt16(sum) 643 | } 644 | 645 | private func icmpHeaderOffset(of packet: Data) -> Int? { 646 | if packet.count >= MemoryLayout.size + MemoryLayout.size { 647 | let ipHeader = packet.withUnsafeBytes({ $0.load(as: IPHeader.self) }) 648 | if ipHeader.versionAndHeaderLength & 0xF0 == 0x40 && ipHeader.protocol == IPPROTO_ICMP { 649 | let headerLength = Int(ipHeader.versionAndHeaderLength) & 0x0F * MemoryLayout.size 650 | if packet.count >= headerLength + MemoryLayout.size { 651 | return headerLength 652 | } 653 | } 654 | } 655 | return nil 656 | } 657 | 658 | private func convert(payload: uuid_t) -> [UInt8] { 659 | let p = payload 660 | return [p.0, p.1, p.2, p.3, p.4, p.5, p.6, p.7, p.8, p.9, p.10, p.11, p.12, p.13, p.14, p.15].map { UInt8($0) } 661 | } 662 | 663 | private func validateResponse(from data: Data) throws -> Bool { 664 | guard data.count >= MemoryLayout.size + MemoryLayout.size else { 665 | throw PingError.invalidLength(received: data.count) 666 | } 667 | 668 | guard let headerOffset = icmpHeaderOffset(of: data) else { throw PingError.invalidHeaderOffset } 669 | let payloadSize = data.count - headerOffset - MemoryLayout.size 670 | let icmpHeader = data.withUnsafeBytes({ $0.load(fromByteOffset: headerOffset, as: ICMPHeader.self) }) 671 | let payload = data.subdata(in: (data.count - payloadSize)...size 808 | /// If set to `true`, when `targetCount` is reached (if set), the pinging will be halted instead of stopped. This means that the socket will be released and will be recreated if more pings are requested. Defaults to `true`. 809 | public var haltAfterTarget: Bool = true 810 | 811 | /// Initializes a `PingConfiguration` object with the given parameters. 812 | /// - Parameter interval: The time between consecutive pings in seconds. Defaults to 1. 813 | /// - Parameter timeout: Timeout interval in seconds. Defaults to 5. 814 | public init(interval: TimeInterval = 1, with timeout: TimeInterval = 5) { 815 | pingInterval = interval 816 | timeoutInterval = timeout 817 | } 818 | /// Initializes a `PingConfiguration` object with the given interval. 819 | /// - Parameter interval: The time between consecutive pings in seconds. 820 | /// - Note: Timeout interval will be set to 5 seconds. 821 | public init(interval: TimeInterval) { 822 | self.init(interval: interval, with: 5) 823 | } 824 | } 825 | 826 | // MARK: - Data Extensions 827 | 828 | public extension Data { 829 | /// Expresses a chunk of data as a socket address. 830 | var socketAddress: sockaddr { 831 | return withUnsafeBytes { $0.load(as: sockaddr.self) } 832 | } 833 | /// Expresses a chunk of data as an internet-style socket address. 834 | var socketAddressInternet: sockaddr_in { 835 | return withUnsafeBytes { $0.load(as: sockaddr_in.self) } 836 | } 837 | } 838 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8880CB7E21537EA8007955EA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8880CB7D21537EA8007955EA /* AppDelegate.swift */; }; 11 | 8880CB8021537EA8007955EA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8880CB7F21537EA8007955EA /* ViewController.swift */; }; 12 | 8880CB8321537EA9007955EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8880CB8121537EA9007955EA /* Main.storyboard */; }; 13 | 8880CB8521537EAA007955EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8880CB8421537EAA007955EA /* Assets.xcassets */; }; 14 | 8880CB8821537EAA007955EA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8880CB8621537EAA007955EA /* LaunchScreen.storyboard */; }; 15 | 8880CB9021537EE5007955EA /* SwiftyPing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8880CB8F21537EE5007955EA /* SwiftyPing.swift */; }; 16 | 8893DA5525EBB55700521E78 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8893DA5425EBB55700521E78 /* AppDelegate.swift */; }; 17 | 8893DA5725EBB55700521E78 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8893DA5625EBB55700521E78 /* ViewController.swift */; }; 18 | 8893DA5925EBB55800521E78 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8893DA5825EBB55800521E78 /* Assets.xcassets */; }; 19 | 8893DA5C25EBB55800521E78 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8893DA5A25EBB55800521E78 /* Main.storyboard */; }; 20 | 8893DA6625EBB6CA00521E78 /* SwiftyPing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8880CB8F21537EE5007955EA /* SwiftyPing.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | 8880CB7A21537EA8007955EA /* SwiftyPingTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyPingTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 8880CB7D21537EA8007955EA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 8880CB7F21537EA8007955EA /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 27 | 8880CB8221537EA9007955EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 28 | 8880CB8421537EAA007955EA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 8880CB8721537EAA007955EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | 8880CB8921537EAA007955EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 8880CB8F21537EE5007955EA /* SwiftyPing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyPing.swift; sourceTree = ""; }; 32 | 8893DA5225EBB55700521E78 /* SwiftyPingTestMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyPingTestMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 8893DA5425EBB55700521E78 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | 8893DA5625EBB55700521E78 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 35 | 8893DA5825EBB55800521E78 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 36 | 8893DA5B25EBB55800521E78 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 37 | 8893DA5D25EBB55800521E78 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | 8893DA5E25EBB55800521E78 /* SwiftyPingTestMac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftyPingTestMac.entitlements; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 8880CB7721537EA8007955EA /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | 8893DA4F25EBB55700521E78 /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | /* End PBXFrameworksBuildPhase section */ 57 | 58 | /* Begin PBXGroup section */ 59 | 8880CB7121537EA8007955EA = { 60 | isa = PBXGroup; 61 | children = ( 62 | 8880CB8F21537EE5007955EA /* SwiftyPing.swift */, 63 | 8880CB7C21537EA8007955EA /* SwiftyPingTest */, 64 | 8893DA5325EBB55700521E78 /* SwiftyPingTestMac */, 65 | 8880CB7B21537EA8007955EA /* Products */, 66 | ); 67 | sourceTree = ""; 68 | }; 69 | 8880CB7B21537EA8007955EA /* Products */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 8880CB7A21537EA8007955EA /* SwiftyPingTest.app */, 73 | 8893DA5225EBB55700521E78 /* SwiftyPingTestMac.app */, 74 | ); 75 | name = Products; 76 | sourceTree = ""; 77 | }; 78 | 8880CB7C21537EA8007955EA /* SwiftyPingTest */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 8880CB7D21537EA8007955EA /* AppDelegate.swift */, 82 | 8880CB7F21537EA8007955EA /* ViewController.swift */, 83 | 8880CB8121537EA9007955EA /* Main.storyboard */, 84 | 8880CB8421537EAA007955EA /* Assets.xcassets */, 85 | 8880CB8621537EAA007955EA /* LaunchScreen.storyboard */, 86 | 8880CB8921537EAA007955EA /* Info.plist */, 87 | ); 88 | path = SwiftyPingTest; 89 | sourceTree = ""; 90 | }; 91 | 8893DA5325EBB55700521E78 /* SwiftyPingTestMac */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 8893DA5425EBB55700521E78 /* AppDelegate.swift */, 95 | 8893DA5625EBB55700521E78 /* ViewController.swift */, 96 | 8893DA5825EBB55800521E78 /* Assets.xcassets */, 97 | 8893DA5A25EBB55800521E78 /* Main.storyboard */, 98 | 8893DA5D25EBB55800521E78 /* Info.plist */, 99 | 8893DA5E25EBB55800521E78 /* SwiftyPingTestMac.entitlements */, 100 | ); 101 | path = SwiftyPingTestMac; 102 | sourceTree = ""; 103 | }; 104 | /* End PBXGroup section */ 105 | 106 | /* Begin PBXNativeTarget section */ 107 | 8880CB7921537EA8007955EA /* SwiftyPingTest */ = { 108 | isa = PBXNativeTarget; 109 | buildConfigurationList = 8880CB8C21537EAA007955EA /* Build configuration list for PBXNativeTarget "SwiftyPingTest" */; 110 | buildPhases = ( 111 | 8880CB7621537EA8007955EA /* Sources */, 112 | 8880CB7721537EA8007955EA /* Frameworks */, 113 | 8880CB7821537EA8007955EA /* Resources */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = SwiftyPingTest; 120 | productName = SwiftyPingTest; 121 | productReference = 8880CB7A21537EA8007955EA /* SwiftyPingTest.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | 8893DA5125EBB55700521E78 /* SwiftyPingTestMac */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = 8893DA6125EBB55800521E78 /* Build configuration list for PBXNativeTarget "SwiftyPingTestMac" */; 127 | buildPhases = ( 128 | 8893DA4E25EBB55700521E78 /* Sources */, 129 | 8893DA4F25EBB55700521E78 /* Frameworks */, 130 | 8893DA5025EBB55700521E78 /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = SwiftyPingTestMac; 137 | productName = SwiftyPingTestMac; 138 | productReference = 8893DA5225EBB55700521E78 /* SwiftyPingTestMac.app */; 139 | productType = "com.apple.product-type.application"; 140 | }; 141 | /* End PBXNativeTarget section */ 142 | 143 | /* Begin PBXProject section */ 144 | 8880CB7221537EA8007955EA /* Project object */ = { 145 | isa = PBXProject; 146 | attributes = { 147 | LastSwiftUpdateCheck = 1240; 148 | LastUpgradeCheck = 1200; 149 | ORGANIZATIONNAME = "Sami Yrjänheikki"; 150 | TargetAttributes = { 151 | 8880CB7921537EA8007955EA = { 152 | CreatedOnToolsVersion = 10.0; 153 | LastSwiftMigration = 1200; 154 | }; 155 | 8893DA5125EBB55700521E78 = { 156 | CreatedOnToolsVersion = 12.4; 157 | }; 158 | }; 159 | }; 160 | buildConfigurationList = 8880CB7521537EA8007955EA /* Build configuration list for PBXProject "SwiftyPingTest" */; 161 | compatibilityVersion = "Xcode 9.3"; 162 | developmentRegion = en; 163 | hasScannedForEncodings = 0; 164 | knownRegions = ( 165 | en, 166 | Base, 167 | ); 168 | mainGroup = 8880CB7121537EA8007955EA; 169 | productRefGroup = 8880CB7B21537EA8007955EA /* Products */; 170 | projectDirPath = ""; 171 | projectRoot = ""; 172 | targets = ( 173 | 8880CB7921537EA8007955EA /* SwiftyPingTest */, 174 | 8893DA5125EBB55700521E78 /* SwiftyPingTestMac */, 175 | ); 176 | }; 177 | /* End PBXProject section */ 178 | 179 | /* Begin PBXResourcesBuildPhase section */ 180 | 8880CB7821537EA8007955EA /* Resources */ = { 181 | isa = PBXResourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | 8880CB8821537EAA007955EA /* LaunchScreen.storyboard in Resources */, 185 | 8880CB8521537EAA007955EA /* Assets.xcassets in Resources */, 186 | 8880CB8321537EA9007955EA /* Main.storyboard in Resources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | 8893DA5025EBB55700521E78 /* Resources */ = { 191 | isa = PBXResourcesBuildPhase; 192 | buildActionMask = 2147483647; 193 | files = ( 194 | 8893DA5925EBB55800521E78 /* Assets.xcassets in Resources */, 195 | 8893DA5C25EBB55800521E78 /* Main.storyboard in Resources */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | /* End PBXResourcesBuildPhase section */ 200 | 201 | /* Begin PBXSourcesBuildPhase section */ 202 | 8880CB7621537EA8007955EA /* Sources */ = { 203 | isa = PBXSourcesBuildPhase; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | 8880CB9021537EE5007955EA /* SwiftyPing.swift in Sources */, 207 | 8880CB8021537EA8007955EA /* ViewController.swift in Sources */, 208 | 8880CB7E21537EA8007955EA /* AppDelegate.swift in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | 8893DA4E25EBB55700521E78 /* Sources */ = { 213 | isa = PBXSourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | 8893DA6625EBB6CA00521E78 /* SwiftyPing.swift in Sources */, 217 | 8893DA5725EBB55700521E78 /* ViewController.swift in Sources */, 218 | 8893DA5525EBB55700521E78 /* AppDelegate.swift in Sources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | /* End PBXSourcesBuildPhase section */ 223 | 224 | /* Begin PBXVariantGroup section */ 225 | 8880CB8121537EA9007955EA /* Main.storyboard */ = { 226 | isa = PBXVariantGroup; 227 | children = ( 228 | 8880CB8221537EA9007955EA /* Base */, 229 | ); 230 | name = Main.storyboard; 231 | sourceTree = ""; 232 | }; 233 | 8880CB8621537EAA007955EA /* LaunchScreen.storyboard */ = { 234 | isa = PBXVariantGroup; 235 | children = ( 236 | 8880CB8721537EAA007955EA /* Base */, 237 | ); 238 | name = LaunchScreen.storyboard; 239 | sourceTree = ""; 240 | }; 241 | 8893DA5A25EBB55800521E78 /* Main.storyboard */ = { 242 | isa = PBXVariantGroup; 243 | children = ( 244 | 8893DA5B25EBB55800521E78 /* Base */, 245 | ); 246 | name = Main.storyboard; 247 | sourceTree = ""; 248 | }; 249 | /* End PBXVariantGroup section */ 250 | 251 | /* Begin XCBuildConfiguration section */ 252 | 8880CB8A21537EAA007955EA /* Debug */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | ALWAYS_SEARCH_USER_PATHS = NO; 256 | CLANG_ANALYZER_NONNULL = YES; 257 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 258 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 259 | CLANG_CXX_LIBRARY = "libc++"; 260 | CLANG_ENABLE_MODULES = YES; 261 | CLANG_ENABLE_OBJC_ARC = YES; 262 | CLANG_ENABLE_OBJC_WEAK = YES; 263 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 264 | CLANG_WARN_BOOL_CONVERSION = YES; 265 | CLANG_WARN_COMMA = YES; 266 | CLANG_WARN_CONSTANT_CONVERSION = YES; 267 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 268 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 269 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 270 | CLANG_WARN_EMPTY_BODY = YES; 271 | CLANG_WARN_ENUM_CONVERSION = YES; 272 | CLANG_WARN_INFINITE_RECURSION = YES; 273 | CLANG_WARN_INT_CONVERSION = YES; 274 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 276 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 278 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 279 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 280 | CLANG_WARN_STRICT_PROTOTYPES = YES; 281 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 282 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | CODE_SIGN_IDENTITY = "iPhone Developer"; 286 | COPY_PHASE_STRIP = NO; 287 | DEBUG_INFORMATION_FORMAT = dwarf; 288 | ENABLE_STRICT_OBJC_MSGSEND = YES; 289 | ENABLE_TESTABILITY = YES; 290 | GCC_C_LANGUAGE_STANDARD = gnu11; 291 | GCC_DYNAMIC_NO_PIC = NO; 292 | GCC_NO_COMMON_BLOCKS = YES; 293 | GCC_OPTIMIZATION_LEVEL = 0; 294 | GCC_PREPROCESSOR_DEFINITIONS = ( 295 | "DEBUG=1", 296 | "$(inherited)", 297 | ); 298 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 299 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 300 | GCC_WARN_UNDECLARED_SELECTOR = YES; 301 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 302 | GCC_WARN_UNUSED_FUNCTION = YES; 303 | GCC_WARN_UNUSED_VARIABLE = YES; 304 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 305 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 306 | MTL_FAST_MATH = YES; 307 | ONLY_ACTIVE_ARCH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 310 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 311 | }; 312 | name = Debug; 313 | }; 314 | 8880CB8B21537EAA007955EA /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ALWAYS_SEARCH_USER_PATHS = NO; 318 | CLANG_ANALYZER_NONNULL = YES; 319 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 320 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 321 | CLANG_CXX_LIBRARY = "libc++"; 322 | CLANG_ENABLE_MODULES = YES; 323 | CLANG_ENABLE_OBJC_ARC = YES; 324 | CLANG_ENABLE_OBJC_WEAK = YES; 325 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 326 | CLANG_WARN_BOOL_CONVERSION = YES; 327 | CLANG_WARN_COMMA = YES; 328 | CLANG_WARN_CONSTANT_CONVERSION = YES; 329 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 330 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 331 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 332 | CLANG_WARN_EMPTY_BODY = YES; 333 | CLANG_WARN_ENUM_CONVERSION = YES; 334 | CLANG_WARN_INFINITE_RECURSION = YES; 335 | CLANG_WARN_INT_CONVERSION = YES; 336 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 337 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 338 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 339 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 340 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 341 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 342 | CLANG_WARN_STRICT_PROTOTYPES = YES; 343 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 344 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 345 | CLANG_WARN_UNREACHABLE_CODE = YES; 346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 347 | CODE_SIGN_IDENTITY = "iPhone Developer"; 348 | COPY_PHASE_STRIP = NO; 349 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 350 | ENABLE_NS_ASSERTIONS = NO; 351 | ENABLE_STRICT_OBJC_MSGSEND = YES; 352 | GCC_C_LANGUAGE_STANDARD = gnu11; 353 | GCC_NO_COMMON_BLOCKS = YES; 354 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 355 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 356 | GCC_WARN_UNDECLARED_SELECTOR = YES; 357 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 358 | GCC_WARN_UNUSED_FUNCTION = YES; 359 | GCC_WARN_UNUSED_VARIABLE = YES; 360 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 361 | MTL_ENABLE_DEBUG_INFO = NO; 362 | MTL_FAST_MATH = YES; 363 | SDKROOT = iphoneos; 364 | SWIFT_COMPILATION_MODE = wholemodule; 365 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 366 | VALIDATE_PRODUCT = YES; 367 | }; 368 | name = Release; 369 | }; 370 | 8880CB8D21537EAA007955EA /* Debug */ = { 371 | isa = XCBuildConfiguration; 372 | buildSettings = { 373 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 374 | CODE_SIGN_STYLE = Automatic; 375 | DEVELOPMENT_TEAM = 382EB97XNX; 376 | INFOPLIST_FILE = SwiftyPingTest/Info.plist; 377 | LD_RUNPATH_SEARCH_PATHS = ( 378 | "$(inherited)", 379 | "@executable_path/Frameworks", 380 | ); 381 | PRODUCT_BUNDLE_IDENTIFIER = com.samiyrjanheikki.SwiftyPingTest; 382 | PRODUCT_NAME = "$(TARGET_NAME)"; 383 | SWIFT_VERSION = 5.0; 384 | TARGETED_DEVICE_FAMILY = "1,2"; 385 | }; 386 | name = Debug; 387 | }; 388 | 8880CB8E21537EAA007955EA /* Release */ = { 389 | isa = XCBuildConfiguration; 390 | buildSettings = { 391 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 392 | CODE_SIGN_STYLE = Automatic; 393 | DEVELOPMENT_TEAM = 382EB97XNX; 394 | INFOPLIST_FILE = SwiftyPingTest/Info.plist; 395 | LD_RUNPATH_SEARCH_PATHS = ( 396 | "$(inherited)", 397 | "@executable_path/Frameworks", 398 | ); 399 | PRODUCT_BUNDLE_IDENTIFIER = com.samiyrjanheikki.SwiftyPingTest; 400 | PRODUCT_NAME = "$(TARGET_NAME)"; 401 | SWIFT_VERSION = 5.0; 402 | TARGETED_DEVICE_FAMILY = "1,2"; 403 | }; 404 | name = Release; 405 | }; 406 | 8893DA5F25EBB55800521E78 /* Debug */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 410 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 411 | CODE_SIGN_ENTITLEMENTS = SwiftyPingTestMac/SwiftyPingTestMac.entitlements; 412 | CODE_SIGN_STYLE = Automatic; 413 | COMBINE_HIDPI_IMAGES = YES; 414 | DEVELOPMENT_TEAM = 382EB97XNX; 415 | ENABLE_HARDENED_RUNTIME = YES; 416 | INFOPLIST_FILE = SwiftyPingTestMac/Info.plist; 417 | LD_RUNPATH_SEARCH_PATHS = ( 418 | "$(inherited)", 419 | "@executable_path/../Frameworks", 420 | ); 421 | MACOSX_DEPLOYMENT_TARGET = 11.1; 422 | PRODUCT_BUNDLE_IDENTIFIER = com.samiyrjanheikki.SwiftyPingTestMac; 423 | PRODUCT_NAME = "$(TARGET_NAME)"; 424 | SDKROOT = macosx; 425 | SWIFT_VERSION = 5.0; 426 | }; 427 | name = Debug; 428 | }; 429 | 8893DA6025EBB55800521E78 /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 433 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 434 | CODE_SIGN_ENTITLEMENTS = SwiftyPingTestMac/SwiftyPingTestMac.entitlements; 435 | CODE_SIGN_STYLE = Automatic; 436 | COMBINE_HIDPI_IMAGES = YES; 437 | DEVELOPMENT_TEAM = 382EB97XNX; 438 | ENABLE_HARDENED_RUNTIME = YES; 439 | INFOPLIST_FILE = SwiftyPingTestMac/Info.plist; 440 | LD_RUNPATH_SEARCH_PATHS = ( 441 | "$(inherited)", 442 | "@executable_path/../Frameworks", 443 | ); 444 | MACOSX_DEPLOYMENT_TARGET = 11.1; 445 | PRODUCT_BUNDLE_IDENTIFIER = com.samiyrjanheikki.SwiftyPingTestMac; 446 | PRODUCT_NAME = "$(TARGET_NAME)"; 447 | SDKROOT = macosx; 448 | SWIFT_VERSION = 5.0; 449 | }; 450 | name = Release; 451 | }; 452 | /* End XCBuildConfiguration section */ 453 | 454 | /* Begin XCConfigurationList section */ 455 | 8880CB7521537EA8007955EA /* Build configuration list for PBXProject "SwiftyPingTest" */ = { 456 | isa = XCConfigurationList; 457 | buildConfigurations = ( 458 | 8880CB8A21537EAA007955EA /* Debug */, 459 | 8880CB8B21537EAA007955EA /* Release */, 460 | ); 461 | defaultConfigurationIsVisible = 0; 462 | defaultConfigurationName = Release; 463 | }; 464 | 8880CB8C21537EAA007955EA /* Build configuration list for PBXNativeTarget "SwiftyPingTest" */ = { 465 | isa = XCConfigurationList; 466 | buildConfigurations = ( 467 | 8880CB8D21537EAA007955EA /* Debug */, 468 | 8880CB8E21537EAA007955EA /* Release */, 469 | ); 470 | defaultConfigurationIsVisible = 0; 471 | defaultConfigurationName = Release; 472 | }; 473 | 8893DA6125EBB55800521E78 /* Build configuration list for PBXNativeTarget "SwiftyPingTestMac" */ = { 474 | isa = XCConfigurationList; 475 | buildConfigurations = ( 476 | 8893DA5F25EBB55800521E78 /* Debug */, 477 | 8893DA6025EBB55800521E78 /* Release */, 478 | ); 479 | defaultConfigurationIsVisible = 0; 480 | defaultConfigurationName = Release; 481 | }; 482 | /* End XCConfigurationList section */ 483 | }; 484 | rootObject = 8880CB7221537EA8007955EA /* Project object */; 485 | } 486 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest.xcodeproj/project.xcworkspace/xcuserdata/samiyrjanheikki.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samiyr/SwiftyPing/05591bc0047e41e0e1d98135c6bc457192a72d39/SwiftyPingTest/SwiftyPingTest.xcodeproj/project.xcworkspace/xcuserdata/samiyrjanheikki.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest.xcodeproj/xcuserdata/samiyrjanheikki.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftyPingTest.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | SwiftyPingTest.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | SwiftyPingTestMac.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 1 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | 8880CB7921537EA8007955EA 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftyPingTest 4 | // 5 | // Created by Sami Yrjänheikki on 20/09/2018. 6 | // Copyright © 2018 Sami Yrjänheikki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTest/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftyPingTest 4 | // 5 | // Created by Sami Yrjänheikki on 20/09/2018. 6 | // Copyright © 2018 Sami Yrjänheikki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | textView.text = "" 18 | } 19 | 20 | @IBAction func start(_ sender: Any) { 21 | startPinging() 22 | } 23 | @IBAction func stop(_ sender: Any) { 24 | ping?.stopPinging() 25 | } 26 | @IBAction func halt(_ sender: Any) { 27 | ping?.haltPinging() 28 | } 29 | var ping: SwiftyPing? 30 | func startPinging() { 31 | do { 32 | let host = "1.1.1.1" 33 | ping = try SwiftyPing(host: host, configuration: PingConfiguration(interval: 1.0, with: 1), queue: DispatchQueue.global()) 34 | ping?.observer = { (response) in 35 | DispatchQueue.main.async { 36 | var message = "\(response.duration * 1000) ms" 37 | if let error = response.error { 38 | if error == .responseTimeout { 39 | message = "Timeout \(message)" 40 | } else { 41 | print(error) 42 | message = error.localizedDescription 43 | } 44 | } 45 | self.textView.text.append(contentsOf: "\nPing #\(response.trueSequenceNumber): \(message)") 46 | self.textView.scrollRangeToVisible(NSRange(location: self.textView.text.count - 1, length: 1)) 47 | } 48 | } 49 | ping?.finished = { (result) in 50 | DispatchQueue.main.async { 51 | var message = "\n--- \(host) ping statistics ---\n" 52 | message += "\(result.packetsTransmitted) transmitted, \(result.packetsReceived) received" 53 | if let loss = result.packetLoss { 54 | message += String(format: "\n%.1f%% packet loss\n", loss * 100) 55 | } else { 56 | message += "\n" 57 | } 58 | if let roundtrip = result.roundtrip { 59 | message += String(format: "round-trip min/avg/max/stddev = %.3f/%.3f/%.3f/%.3f ms", roundtrip.minimum * 1000, roundtrip.average * 1000, roundtrip.maximum * 1000, roundtrip.standardDeviation * 1000) 60 | } 61 | self.textView.text.append(contentsOf: message) 62 | self.textView.scrollRangeToVisible(NSRange(location: self.textView.text.count - 1, length: 1)) 63 | } 64 | } 65 | // ping?.targetCount = 1 66 | try ping?.startPinging() 67 | } catch { 68 | textView.text = error.localizedDescription 69 | } 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTestMac/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftyPingTestMac 4 | // 5 | // Created by Sami Yrjänheikki on 28.2.2021. 6 | // Copyright © 2021 Sami Yrjänheikki. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @main 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | 16 | 17 | func applicationDidFinishLaunching(_ aNotification: Notification) { 18 | // Insert code here to initialize your application 19 | } 20 | 21 | func applicationWillTerminate(_ aNotification: Notification) { 22 | // Insert code here to tear down your application 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTestMac/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTestMac/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTestMac/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTestMac/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | Default 530 | 531 | 532 | 533 | 534 | 535 | 536 | Left to Right 537 | 538 | 539 | 540 | 541 | 542 | 543 | Right to Left 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | Default 555 | 556 | 557 | 558 | 559 | 560 | 561 | Left to Right 562 | 563 | 564 | 565 | 566 | 567 | 568 | Right to Left 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 722 | 732 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTestMac/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2021 Sami Yrjänheikki. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTestMac/SwiftyPingTestMac.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftyPingTest/SwiftyPingTestMac/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftyPingTestMac 4 | // 5 | // Created by Sami Yrjänheikki on 28.2.2021. 6 | // Copyright © 2021 Sami Yrjänheikki. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | 13 | @IBOutlet var textView: NSTextView! 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | textView.string = "" 18 | textView.isEditable = false 19 | } 20 | 21 | @IBAction func start(_ sender: Any) { 22 | startPinging() 23 | } 24 | @IBAction func stop(_ sender: Any) { 25 | ping?.stopPinging() 26 | } 27 | @IBAction func halt(_ sender: Any) { 28 | ping?.haltPinging() 29 | } 30 | var ping: SwiftyPing? 31 | func startPinging() { 32 | do { 33 | ping = try SwiftyPing(host: "1.1.1.1", configuration: PingConfiguration(interval: 1.0, with: 1), queue: DispatchQueue.global()) 34 | ping?.observer = { (response) in 35 | DispatchQueue.main.async { 36 | var message = "\(response.duration! * 1000) ms" 37 | if let error = response.error { 38 | if error == .responseTimeout { 39 | message = "Timeout \(message)" 40 | } else { 41 | print(error) 42 | message = error.localizedDescription 43 | } 44 | } 45 | self.textView.string.append(contentsOf: "\nPing #\(response.sequenceNumber): \(message)") 46 | self.textView.scrollRangeToVisible(NSRange(location: self.textView.string.count - 1, length: 1)) 47 | } 48 | } 49 | // ping?.targetCount = 1 50 | try ping?.startPinging() 51 | } catch { 52 | textView.string = error.localizedDescription 53 | } 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftyPingTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftyPingTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SwiftyPingTests/SwiftyPingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyPing 3 | 4 | final class SwiftyPingTests: XCTestCase { 5 | } 6 | -------------------------------------------------------------------------------- /Tests/SwiftyPingTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SwiftyPingTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------