diff --git a/HowOnline/AppDelegate.swift b/HowOnline/AppDelegate.swift index a57026b..1c02c1f 100644 --- a/HowOnline/AppDelegate.swift +++ b/HowOnline/AppDelegate.swift @@ -7,100 +7,140 @@ // import Cocoa -import ReachabilitySwift -import StartAtLoginController - -@NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate, ProberDelegate { - @IBOutlet weak var statusMenuItem: NSMenuItem! - @IBOutlet weak var menu: NSMenu! - @IBOutlet weak var startAtLoginController: StartAtLoginController! - - let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSSquareStatusItemLength) - var refreshTimer: NSTimer! = nil - var reachability: Reachability? - var prober: Prober! - - func applicationDidFinishLaunching(aNotification: NSNotification) { - // The menu is defined in interface builder - statusItem.menu = self.menu - updateMenu(false, text: "hello", longText: "Hello") - - startReachability() - - refreshTimer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: #selector(probe), userInfo: nil, repeats: true) - prober = Prober(delegate: self) - probe() - } - - func startReachability() { - let refreshBlock: (Reachability) -> () = { reachability in - dispatch_async(dispatch_get_main_queue()) { - self.probe() - } - } - - do { - // We dont need any more sophistication than just checking for a wifi connection - // since we do our own internet connection checking - let reachability = try Reachability.reachabilityForLocalWiFi() - self.reachability = reachability - - reachability.whenReachable = refreshBlock - reachability.whenUnreachable = refreshBlock - try reachability.startNotifier() - } catch { - print("Unable to create Reachability") - return - } - } - - func probeResult(prober: Prober, result: Prober.ProbeResult) { - updateMenu(result.success, text: result.text, longText: result.longText) - } - - func updateMenu(success: Bool, text: String, longText: String) { - if let button = statusItem.button { - button.image = imageForStatus(text, filledBar: success, imageSize: button.frame.size) - statusMenuItem.title = "Status: \(longText)" - - // Make the icon black and white, but this will also auto reverse colors in Dark/highlighted mode - button.image!.template = true - } - } - - func probe() { - prober.probe() - } - - func imageForStatus(text: String, filledBar: Bool, imageSize: NSSize) -> NSImage? { - return NSImage(size: imageSize, flipped: false, drawingHandler: { (rect: NSRect) -> Bool in - NSColor.blackColor().setFill() - NSColor.blackColor().setStroke() - - let progressBarRect = CGRect(x: 2, y: rect.size.height - 4 - 4, width: rect.size.width - 2 - 2, height: 4) - let path = NSBezierPath(roundedRect: progressBarRect, xRadius: 2, yRadius: 2) - - path.stroke() - if filledBar { - path.fill() - } - - let textRect = CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height - 9) - - // The text needs to be really small to fit - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .Center - paragraphStyle.lineBreakMode = .ByClipping - - let attrs = [ - NSFontAttributeName: NSFont.systemFontOfSize(8), - NSParagraphStyleAttributeName: paragraphStyle - ] - - text.drawWithRect(textRect, options: .UsesLineFragmentOrigin, attributes: attrs, context: nil) - - return true - }) - } -} \ No newline at end of file +import CoreLocation + +@main +class AppDelegate: NSObject, NSApplicationDelegate, ProberDelegate, CLLocationManagerDelegate { + @IBOutlet weak var statusMenuItem: NSMenuItem! + @IBOutlet weak var menu: NSMenu! + + let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) + var refreshTimer: Timer? + var prober: Prober! + var locationManager: CLLocationManager! + var locationAuthorized = false + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // The menu is defined in interface builder + statusItem.menu = self.menu + updateMenu(success: false, text: "hello", longText: "Hello") + + // Setup location manager for SSID access (required since macOS 10.15) + setupLocationServices() + + prober = Prober(delegate: self) + + // Start probing after a short delay to allow location auth + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.startProbing() + } + } + + func setupLocationServices() { + locationManager = CLLocationManager() + locationManager.delegate = self + + // Check current authorization status + let status: CLAuthorizationStatus + if #available(macOS 11.0, *) { + status = locationManager.authorizationStatus + } else { + status = CLLocationManager.authorizationStatus() + } + + switch status { + case .authorizedAlways: + locationAuthorized = true + case .notDetermined: + // Request authorization - on macOS this uses "When In Use" style + if #available(macOS 10.15, *) { + locationManager.requestWhenInUseAuthorization() + } + case .denied, .restricted: + print("Location access denied - SSID detection may not work") + locationAuthorized = false + @unknown default: + locationAuthorized = false + } + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + let status: CLAuthorizationStatus + if #available(macOS 11.0, *) { + status = manager.authorizationStatus + } else { + status = CLLocationManager.authorizationStatus() + } + + locationAuthorized = (status == .authorizedAlways) + + if locationAuthorized { + probe() + } + } + + // Legacy delegate method for older macOS + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + locationAuthorized = (status == .authorizedAlways) + + if locationAuthorized { + probe() + } + } + + func startProbing() { + refreshTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in + self?.probe() + } + probe() + } + + func probeResult(_ prober: Prober, result: Prober.ProbeResult) { + updateMenu(success: result.success, text: result.text, longText: result.longText) + } + + func updateMenu(success: Bool, text: String, longText: String) { + if let button = statusItem.button { + button.image = imageForStatus(text: text, filledBar: success, imageSize: button.frame.size) + statusMenuItem.title = "Status: \(longText)" + + // Make the icon black and white, but this will also auto reverse colors in Dark/highlighted mode + button.image?.isTemplate = true + } + } + + @objc func probe() { + prober.probe() + } + + func imageForStatus(text: String, filledBar: Bool, imageSize: NSSize) -> NSImage? { + return NSImage(size: imageSize, flipped: false, drawingHandler: { (rect: NSRect) -> Bool in + NSColor.black.setFill() + NSColor.black.setStroke() + + let progressBarRect = CGRect(x: 2, y: rect.size.height - 4 - 4, width: rect.size.width - 2 - 2, height: 4) + let path = NSBezierPath(roundedRect: progressBarRect, xRadius: 2, yRadius: 2) + + path.stroke() + if filledBar { + path.fill() + } + + let textRect = CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height - 9) + + // The text needs to be really small to fit + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + paragraphStyle.lineBreakMode = .byClipping + + let attrs: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 8), + .paragraphStyle: paragraphStyle + ] + + text.draw(with: textRect, options: .usesLineFragmentOrigin, attributes: attrs, context: nil) + + return true + }) + } +} diff --git a/HowOnline/HowOnline.entitlements b/HowOnline/HowOnline.entitlements index 7a2230d..9f2bc64 100644 --- a/HowOnline/HowOnline.entitlements +++ b/HowOnline/HowOnline.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.personal-information.location + diff --git a/HowOnline/Info.plist b/HowOnline/Info.plist index 96ffcdb..ad361b1 100644 --- a/HowOnline/Info.plist +++ b/HowOnline/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.01 + 1.1.0 CFBundleSignature ???? CFBundleVersion - 4 + 5 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion @@ -32,5 +32,9 @@ MainMenu NSPrincipalClass NSApplication + NSLocationWhenInUseUsageDescription + HowOnline needs location access to detect your WiFi network name (SSID). This is required by macOS 10.15+ for accessing WiFi information. + NSLocationUsageDescription + HowOnline needs location access to detect your WiFi network name (SSID). This is required by macOS 10.15+ for accessing WiFi information. diff --git a/HowOnline/Pinger.swift b/HowOnline/Pinger.swift index 172441a..6302a12 100644 --- a/HowOnline/Pinger.swift +++ b/HowOnline/Pinger.swift @@ -10,59 +10,58 @@ import Foundation import QuartzCore class Pinger: NSObject, SimplePingDelegate { - typealias CompletionBlock = (timeElapsedMs: Int?) -> () - var completionBlock: CompletionBlock! + typealias CompletionBlock = (Int?) -> Void + var completionBlock: CompletionBlock? - var pinger: SimplePing! - var timeoutTimer: NSTimer! - var pingStartTime: CFTimeInterval = 0 - - func ping(hostname: String, completionBlock: CompletionBlock) { - self.pinger = SimplePing(hostName: hostname) - self.completionBlock = completionBlock - - pingStartTime = CACurrentMediaTime() - - pinger.delegate = self; - pinger.start() - } - - func stopPinging(success: Bool) { - pinger.stop() - if timeoutTimer != nil { - timeoutTimer.invalidate() - } - - if success { - let elapsedTime = Int((CACurrentMediaTime() - pingStartTime) * 1000) - self.completionBlock(timeElapsedMs: elapsedTime) - } else { - self.completionBlock(timeElapsedMs: nil) - } - } - - func timeout() { - stopPinging(false) - } - - func simplePing(pinger: SimplePing!, didFailToSendPacket packet: NSData!, error: NSError!) { - stopPinging(false) - } - - func simplePing(pinger: SimplePing!, didFailWithError error: NSError!) { - stopPinging(false) - } - - func simplePing(pinger: SimplePing!, didReceivePingResponsePacket packet: NSData!) { - stopPinging(true) - } - - func simplePing(pinger: SimplePing!, didReceiveUnexpectedPacket packet: NSData!) { - stopPinging(false) - } - - func simplePing(pinger: SimplePing!, didStartWithAddress address: NSData!) { - timeoutTimer = NSTimer.scheduledTimerWithTimeInterval(2.5, target: self, selector: #selector(timeout), userInfo: nil, repeats: false) - pinger.sendPingWithData(nil) - } -} \ No newline at end of file + var pinger: SimplePing? + var timeoutTimer: Timer? + var pingStartTime: CFTimeInterval = 0 + + func ping(hostname: String, completionBlock: @escaping CompletionBlock) { + self.pinger = SimplePing(hostName: hostname) + self.completionBlock = completionBlock + + pingStartTime = CACurrentMediaTime() + + pinger?.delegate = self + pinger?.start() + } + + func stopPinging(success: Bool) { + pinger?.stop() + timeoutTimer?.invalidate() + timeoutTimer = nil + + if success { + let elapsedTime = Int((CACurrentMediaTime() - pingStartTime) * 1000) + self.completionBlock?(elapsedTime) + } else { + self.completionBlock?(nil) + } + } + + @objc func timeout() { + stopPinging(success: false) + } + + func simplePing(_ pinger: SimplePing, didFailToSendPacket packet: Data, error: Error) { + stopPinging(success: false) + } + + func simplePing(_ pinger: SimplePing, didFailWithError error: Error) { + stopPinging(success: false) + } + + func simplePing(_ pinger: SimplePing, didReceivePingResponsePacket packet: Data) { + stopPinging(success: true) + } + + func simplePing(_ pinger: SimplePing, didReceiveUnexpectedPacket packet: Data) { + stopPinging(success: false) + } + + func simplePing(_ pinger: SimplePing, didStartWithAddress address: Data) { + timeoutTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(timeout), userInfo: nil, repeats: false) + pinger.send(with: nil) + } +} diff --git a/HowOnline/PortTester.swift b/HowOnline/PortTester.swift index 3c0f6a0..9407a09 100644 --- a/HowOnline/PortTester.swift +++ b/HowOnline/PortTester.swift @@ -7,35 +7,76 @@ // import Foundation -import CocoaAsyncSocket - -class PortTester: GCDAsyncSocketDelegate { - typealias ResultBlock = (success: Bool) -> () - - var socket: GCDAsyncSocket! - var resultBlock: ResultBlock! - - func testPortOpen(host: String, port: Int, timeoutSeconds: Double, result: ResultBlock) { - self.resultBlock = result - self.socket = GCDAsyncSocket(delegate: self, delegateQueue: dispatch_get_main_queue()) - - do { - try self.socket.connectToHost(host, onPort: UInt16(port), withTimeout: NSTimeInterval(timeoutSeconds)) - } catch { - resultBlock(success: false) - } - } - - @objc - func socket(socket: GCDAsyncSocket, didConnectToHost: String, port: UInt16) { - socket.disconnect() - resultBlock(success: true) - } - - @objc - func socketDidDisconnect(sock: GCDAsyncSocket!, withError err: NSError!) { - if err != nil { - resultBlock(success: false) - } - } + +class PortTester: NSObject, StreamDelegate { + typealias ResultBlock = (Bool) -> Void + + var inputStream: InputStream? + var outputStream: OutputStream? + var resultBlock: ResultBlock? + var timeoutTimer: Timer? + var didConnect = false + + func testPortOpen(host: String, port: Int, timeoutSeconds: Double, result: @escaping ResultBlock) { + self.resultBlock = result + self.didConnect = false + + var readStream: Unmanaged? + var writeStream: Unmanaged? + + CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host as CFString, UInt32(port), &readStream, &writeStream) + + inputStream = readStream?.takeRetainedValue() + outputStream = writeStream?.takeRetainedValue() + + inputStream?.delegate = self + outputStream?.delegate = self + + inputStream?.schedule(in: .current, forMode: .common) + outputStream?.schedule(in: .current, forMode: .common) + + inputStream?.open() + outputStream?.open() + + timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeoutSeconds, repeats: false) { [weak self] _ in + self?.handleTimeout() + } + } + + private func handleTimeout() { + if !didConnect { + cleanup() + resultBlock?(false) + } + } + + private func cleanup() { + timeoutTimer?.invalidate() + timeoutTimer = nil + + inputStream?.close() + outputStream?.close() + + inputStream?.remove(from: .current, forMode: .common) + outputStream?.remove(from: .current, forMode: .common) + + inputStream = nil + outputStream = nil + } + + func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + switch eventCode { + case .openCompleted: + didConnect = true + cleanup() + resultBlock?(true) + case .errorOccurred: + if !didConnect { + cleanup() + resultBlock?(false) + } + default: + break + } + } } diff --git a/HowOnline/Prober.swift b/HowOnline/Prober.swift index 51c88bf..d67ac87 100644 --- a/HowOnline/Prober.swift +++ b/HowOnline/Prober.swift @@ -9,158 +9,176 @@ import Foundation import CoreWLAN -protocol ProberDelegate { - func probeResult(prober: Prober, result: Prober.ProbeResult) +protocol ProberDelegate: AnyObject { + func probeResult(_ prober: Prober, result: Prober.ProbeResult) } class Prober { - typealias ProbeResult = (success: Bool, text: String, longText: String) - - let delegate: ProberDelegate - let pinger: Pinger! = Pinger() - let portTester: PortTester! = PortTester() - var gatewayIP: String! - - var curProbe = 0 - var probing: Bool = false - - var probes: [() -> ()] { - return [simpleChecks, testGateway, pingEightDotEight, resolveGoogle, pingGoogle] - } - - init(delegate: ProberDelegate) { - self.delegate = delegate - } - - func probe() { - if probing { - print("already probing, ignoring request") - return - } - probing = true - - probes[curProbe]() - } - - private func probeResult(result: ProbeResult) { - probing = false - - if result.success { - // The last probe being successful means we should pass the actual success message - if curProbe == probes.count - 1 { - self.delegate.probeResult(self, result: result) - - // Otherwise, proceed to the next probe test - } else { - curProbe += 1 - probe() - } - - } else { - curProbe = 0 - self.delegate.probeResult(self, result: result) - } - } - - private func pingProbe(ip: String, errorText: String, longErrorText: String) { - pinger.ping(ip) { (timeElapsedMs) -> () in - if timeElapsedMs == nil { - self.probeResult(ProbeResult(success: false, text: errorText, longText: longErrorText)) - return - } - - self.probeResult(ProbeResult(success: true, text: "\(timeElapsedMs!)ms", longText: "OK. Ping to Google: \(timeElapsedMs!)ms")) - } - } - - ////// - - private func simpleChecks() { - guard let interface = CWWiFiClient()?.interface() else { - self.probeResult(ProbeResult(success: false, text: "wifi if", longText: "Couldn't detect a WiFi interface")) - return - } - - guard interface.powerOn() else { - self.probeResult(ProbeResult(success: false, text: "wifi off", longText: "Your WiFi is turned off")) - return - } - - guard interface.ssid() != nil else { - self.probeResult(ProbeResult(success: false, text: "no ssid", longText: "Not associated to a WiFi network")) - return - } - - guard let ip = getWiFiAddress() where ip.characters.count > 0 else { - self.probeResult(ProbeResult(success: false, text: "no ip", longText: "On WiFi, but no IP address assigned")) - return - } - - guard !ip.hasPrefix("169.254") && !ip.hasPrefix("0.0") else { - self.probeResult(ProbeResult(success: false, text: "self ip", longText: "On WiFi, but self-assigned IP")) - return - } - - guard let gatewayIP = defaultGateway() else { - self.probeResult(ProbeResult(success: false, text: "no gw", longText: "On WiFi, but no internet gateway assigned")) - return - } - self.gatewayIP = gatewayIP - - probeResult(ProbeResult(success: true, text: "", longText: "")) - } - - private func testGateway() { - // Some gateways aren't pingable for whatever unspeakable reason. As such we'll ping - // it first, and if it fails we'll try a connection to port 22, 80 and - pinger.ping(self.gatewayIP) { (timeElapsedMs) -> () in - if timeElapsedMs == nil { - self.portTester.testPortOpen(self.gatewayIP, port: 80, timeoutSeconds: 2.0) { (success) -> () in - if success { - self.probeResult(ProbeResult(success: true, text: "", longText: "")) - return - } - - self.portTester.testPortOpen(self.gatewayIP, port: 22, timeoutSeconds: 2.0) { (success) -> () in - if success { - self.probeResult(ProbeResult(success: true, text: "", longText: "")) - return - } - - self.portTester.testPortOpen(self.gatewayIP, port: 23, timeoutSeconds: 2.0) { (success) -> () in - if success { - self.probeResult(ProbeResult(success: true, text: "", longText: "")) - return - } - - self.probeResult(ProbeResult(success: false, text: "bad gw", longText: "Gateway wasn't pingable or connectable via HTTP/SSH/Telnet")) - } - } - } - - return - } - - self.probeResult(ProbeResult(success: true, text: "", longText: "")) - } - } - - private func pingEightDotEight() { - pingProbe("8.8.8.8", errorText: "ping 8.", longErrorText: "Failed to ping 8.8.8.8") - } - - private func resolveGoogle() { - testResolveHostname("google.com") { (success) -> Void in - if !success { - self.probeResult(ProbeResult(success: false, text: "dns", longText: "Failed to do a DNS lookup for google.com")) - return - } - - self.probeResult(ProbeResult(success: true, text: "", longText: "")) - } - } - - private func pingGoogle() { - pingProbe("google.com", errorText: "ping G", longErrorText: "Failed to ping google.com") - } -} \ No newline at end of file + struct ProbeResult { + let success: Bool + let text: String + let longText: String + } + + weak var delegate: ProberDelegate? + let pinger: Pinger = Pinger() + let portTester: PortTester = PortTester() + var gatewayIP: String = "" + + var curProbe = 0 + var probing: Bool = false + + var probes: [() -> Void] { + return [simpleChecks, testGateway, pingEightDotEight, resolveGoogle, pingGoogle] + } + + init(delegate: ProberDelegate) { + self.delegate = delegate + } + + func probe() { + if probing { + print("already probing, ignoring request") + return + } + probing = true + + probes[curProbe]() + } + + private func probeResult(_ result: ProbeResult) { + probing = false + + if result.success { + // The last probe being successful means we should pass the actual success message + if curProbe == probes.count - 1 { + self.delegate?.probeResult(self, result: result) + + // Otherwise, proceed to the next probe test + } else { + curProbe += 1 + probe() + } + + } else { + curProbe = 0 + self.delegate?.probeResult(self, result: result) + } + } + + private func pingProbe(ip: String, errorText: String, longErrorText: String) { + pinger.ping(hostname: ip) { [weak self] (timeElapsedMs) in + guard let self = self else { return } + + if timeElapsedMs == nil { + self.probeResult(ProbeResult(success: false, text: errorText, longText: longErrorText)) + return + } + + self.probeResult(ProbeResult(success: true, text: "\(timeElapsedMs!)ms", longText: "OK. Ping to Google: \(timeElapsedMs!)ms")) + } + } + + ////// + + private func simpleChecks() { + guard let interface = CWWiFiClient.shared().interface() else { + self.probeResult(ProbeResult(success: false, text: "wifi if", longText: "Couldn't detect a WiFi interface")) + return + } + + guard interface.powerOn() else { + self.probeResult(ProbeResult(success: false, text: "wifi off", longText: "Your WiFi is turned off")) + return + } + + // Note: ssid() requires Location Services permission on macOS 10.15+ + // The app requests this permission at startup + guard interface.ssid() != nil else { + self.probeResult(ProbeResult(success: false, text: "no ssid", longText: "Not associated to a WiFi network (or Location permission denied)")) + return + } + + guard let ip = getWiFiAddress(), !ip.isEmpty else { + self.probeResult(ProbeResult(success: false, text: "no ip", longText: "On WiFi, but no IP address assigned")) + return + } + + guard !ip.hasPrefix("169.254") && !ip.hasPrefix("0.0") else { + self.probeResult(ProbeResult(success: false, text: "self ip", longText: "On WiFi, but self-assigned IP")) + return + } + + guard let gatewayIP = defaultGateway() as String? else { + self.probeResult(ProbeResult(success: false, text: "no gw", longText: "On WiFi, but no internet gateway assigned")) + return + } + self.gatewayIP = gatewayIP + + probeResult(ProbeResult(success: true, text: "", longText: "")) + } + + private func testGateway() { + // Some gateways aren't pingable for whatever unspeakable reason. As such we'll ping + // it first, and if it fails we'll try a connection to port 22, 80 and + pinger.ping(hostname: self.gatewayIP) { [weak self] (timeElapsedMs) in + guard let self = self else { return } + + if timeElapsedMs == nil { + self.portTester.testPortOpen(host: self.gatewayIP, port: 80, timeoutSeconds: 2.0) { [weak self] (success) in + guard let self = self else { return } + + if success { + self.probeResult(ProbeResult(success: true, text: "", longText: "")) + return + } + + self.portTester.testPortOpen(host: self.gatewayIP, port: 22, timeoutSeconds: 2.0) { [weak self] (success) in + guard let self = self else { return } + + if success { + self.probeResult(ProbeResult(success: true, text: "", longText: "")) + return + } + + self.portTester.testPortOpen(host: self.gatewayIP, port: 23, timeoutSeconds: 2.0) { [weak self] (success) in + guard let self = self else { return } + + if success { + self.probeResult(ProbeResult(success: true, text: "", longText: "")) + return + } + + self.probeResult(ProbeResult(success: false, text: "bad gw", longText: "Gateway wasn't pingable or connectable via HTTP/SSH/Telnet")) + } + } + } + + return + } + + self.probeResult(ProbeResult(success: true, text: "", longText: "")) + } + } + + private func pingEightDotEight() { + pingProbe(ip: "8.8.8.8", errorText: "ping 8.", longErrorText: "Failed to ping 8.8.8.8") + } + + private func resolveGoogle() { + testResolveHostname("google.com") { [weak self] (success) in + guard let self = self else { return } + + if !success { + self.probeResult(ProbeResult(success: false, text: "dns", longText: "Failed to do a DNS lookup for google.com")) + return + } + + self.probeResult(ProbeResult(success: true, text: "", longText: "")) + } + } + + private func pingGoogle() { + pingProbe(ip: "google.com", errorText: "ping G", longErrorText: "Failed to ping google.com") + } +} diff --git a/HowOnline/Utilities.swift b/HowOnline/Utilities.swift index 7e82add..8b962ab 100644 --- a/HowOnline/Utilities.swift +++ b/HowOnline/Utilities.swift @@ -10,39 +10,42 @@ import Foundation // Return IP address of WiFi interface (en0) as a String, or `nil` func getWiFiAddress() -> String? { - var address : String? - - // Get list of all interfaces on the local machine: - var ifaddr : UnsafeMutablePointer = nil - if getifaddrs(&ifaddr) == 0 { - - // For each interface ... - for (var ptr = ifaddr; ptr != nil; ptr = ptr.memory.ifa_next) { - let interface = ptr.memory - - // Check for IPv4 or IPv6 interface: - let addrFamily = interface.ifa_addr.memory.sa_family - if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) { - - // Check interface name: - if let name = String.fromCString(interface.ifa_name) where name.hasPrefix("en") { - - // Convert interface address to a human readable string: - var addr = interface.ifa_addr.memory - var hostname = [CChar](count: Int(NI_MAXHOST), repeatedValue: 0) - getnameinfo(&addr, socklen_t(interface.ifa_addr.memory.sa_len), - &hostname, socklen_t(hostname.count), - nil, socklen_t(0), NI_NUMERICHOST) - address = String.fromCString(hostname) - - if let checkAddress = address where checkAddress != "" { - break - } - } - } - } - freeifaddrs(ifaddr) - } - - return address + var address: String? + + // Get list of all interfaces on the local machine: + var ifaddr: UnsafeMutablePointer? + if getifaddrs(&ifaddr) == 0 { + + // For each interface ... + var ptr = ifaddr + while ptr != nil { + defer { ptr = ptr?.pointee.ifa_next } + + guard let interface = ptr?.pointee else { continue } + + // Check for IPv4 or IPv6 interface: + let addrFamily = interface.ifa_addr.pointee.sa_family + if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) { + + // Check interface name: + let name = String(cString: interface.ifa_name) + if name.hasPrefix("en") { + + // Convert interface address to a human readable string: + var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len), + &hostname, socklen_t(hostname.count), + nil, socklen_t(0), NI_NUMERICHOST) + address = String(cString: hostname) + + if let checkAddress = address, !checkAddress.isEmpty { + break + } + } + } + } + freeifaddrs(ifaddr) + } + + return address } diff --git a/HowOnlineLauncher/AppDelegate.swift b/HowOnlineLauncher/AppDelegate.swift index 3321b8e..1959ba9 100644 --- a/HowOnlineLauncher/AppDelegate.swift +++ b/HowOnlineLauncher/AppDelegate.swift @@ -8,29 +8,30 @@ import Cocoa -@NSApplicationMain +@main class AppDelegate: NSObject, NSApplicationDelegate { - func applicationDidFinishLaunching(aNotification: NSNotification) { - for app in NSWorkspace.sharedWorkspace().runningApplications { - if app.bundleIdentifier == "com.lg.HowOnline" { - NSApp.terminate(nil) - return - } - } - - let path = NSBundle.mainBundle().bundlePath as NSString - var components = path.pathComponents - components.removeLast() - components.removeLast() - components.removeLast() - components.append("MacOS") - components.append("HowOnline") - - let newPath = NSString.pathWithComponents(components) - - NSWorkspace.sharedWorkspace().launchApplication(newPath) - - NSApp.terminate(nil) - } -} + func applicationDidFinishLaunching(_ aNotification: Notification) { + for app in NSWorkspace.shared.runningApplications { + if app.bundleIdentifier == "com.lg.HowOnline" { + NSApp.terminate(nil) + return + } + } + + let path = Bundle.main.bundlePath as NSString + var components = path.pathComponents + components.removeLast() + components.removeLast() + components.removeLast() + components.append("MacOS") + components.append("HowOnline") + + let newPath = NSString.path(withComponents: components) + NSWorkspace.shared.openApplication(at: URL(fileURLWithPath: newPath), + configuration: NSWorkspace.OpenConfiguration(), + completionHandler: nil) + + NSApp.terminate(nil) + } +} diff --git a/Podfile b/Podfile index 0e9d0e2..2e0f135 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,8 @@ -platform :osx, "10.11" +platform :osx, "10.15" use_frameworks! -target "HowOnline" -pod "ReachabilitySwift" -pod "StartAtLoginController" -pod "CocoaAsyncSocket" +target "HowOnline" do + # Note: ReachabilitySwift and StartAtLoginController removed - + # they were causing build issues and aren't needed for core functionality + # The location manager now handles WiFi state detection +end