@@ -10,9 +10,25 @@ import Cocoa
1010import Combine
1111import Foundation
1212import Network
13+ import os. log
1314import ServiceManagement
1415import SwiftUI
1516
17+ let FPING = Bundle . main. path ( forResource: " fping " , ofType: nil ) !
18+ let LOG_PATH = FileManager . default. urls ( for: . cachesDirectory, in: . userDomainMask) [ 0 ] . appendingPathComponent ( " IsThereNet.log " )
19+ let LOG_FILE : FileHandle ? = {
20+ guard FileManager . default. fileExists ( atPath: LOG_PATH . path) || FileManager . default. createFile ( atPath: LOG_PATH . path, contents: nil , attributes: nil ) else {
21+ print ( " Failed to create log file " )
22+ return nil
23+ }
24+ guard let file = try ? FileHandle ( forUpdating: LOG_PATH) else {
25+ print ( " Failed to open log file " )
26+ return nil
27+ }
28+ print ( " Logging to \( LOG_PATH . path) " )
29+ return file
30+ } ( )
31+
1632private var window : NSWindow = {
1733 let w = NSWindow (
1834 contentRect: NSRect ( x: 0 , y: 0 , width: NSScreen . main!. frame. width, height: 20 ) ,
@@ -76,7 +92,10 @@ private func drawColoredTopLine(_ color: NSColor, hideAfter: TimeInterval = 5) {
7692
7793 windowController. showWindow ( nil )
7894 window. fade ( to: 1.0 ) {
79- window. fade ( to: 0.5 )
95+ guard let appearance = menubarIcon? . button? . effectiveAppearance, appearance. isDark else {
96+ return
97+ }
98+ window. fade ( to: 0.7 )
8099 }
81100
82101 guard hideAfter > 0 else { return }
@@ -103,13 +122,94 @@ extension NSWindow {
103122 }
104123}
105124
125+ extension NSAppearance {
126+ var isDark : Bool { name == . vibrantDark || name == . darkAqua }
127+ }
128+
129+ private enum PingStatus : Equatable {
130+ case reachable( Double )
131+ case timedOut
132+ case slow( Double )
133+
134+ var color : NSColor {
135+ switch self {
136+ case . reachable: . systemGreen
137+ case . timedOut: . systemRed
138+ case . slow: . systemYellow
139+ }
140+ }
141+
142+ var hideAfter : TimeInterval {
143+ switch self {
144+ case . reachable: 5
145+ case . timedOut: 0
146+ case . slow: 10
147+ }
148+ }
149+
150+ var message : String {
151+ switch self {
152+ case let . reachable( time) : " OK ( \( time) ms) "
153+ case . timedOut: " TIMEOUT "
154+ case let . slow( time) : " SLOW ( \( time) ms) "
155+ }
156+ }
157+
158+ static func == ( lhs: PingStatus , rhs: PingStatus ) -> Bool {
159+ switch ( lhs, rhs) {
160+ case ( . reachable, . reachable) : true
161+ case ( . timedOut, . timedOut) : true
162+ case ( . slow, . slow) : true
163+ default : false
164+ }
165+ }
166+
167+ }
168+
169+ private var menubarIcon : NSStatusItem ?
106170private var lastColor : NSColor ?
107171private var lastHideAfter : TimeInterval ?
108172private var lastStatus : NWPath . Status ?
173+ private var lastPingStatus : PingStatus ? {
174+ didSet {
175+ guard let lastPingStatus, lastPingStatus != oldValue else { return }
176+
177+ drawColoredTopLine ( lastPingStatus. color, hideAfter: lastPingStatus. hideAfter)
178+ log ( " Internet connection: \( lastPingStatus. message) " )
179+ }
180+ }
181+
109182private var monitor : NWPathMonitor ?
183+ private var process : Process ? {
184+ didSet {
185+ oldValue? . terminate ( )
186+ lastPingStatus = nil
187+ }
188+ }
110189private var observers : [ AnyCancellable ] = [ ]
190+ private let dateFormatter : DateFormatter = {
191+ let d = DateFormatter ( )
192+ d. dateFormat = " yyyy-MM-dd'T'HH:mm:ss.SSS'Z' "
193+ return d
194+ } ( )
195+
196+ private func log( _ message: String ) {
197+ let line = " \( dateFormatter. string ( from: Date ( ) ) ) \( message) "
198+
199+ print ( line)
200+ os_log ( " %{public}@ " , message)
201+
202+ guard let LOG_FILE else {
203+ return
204+ }
205+ LOG_FILE . seekToEndOfFile ( )
206+ LOG_FILE . write ( " \( line) \n " . data ( using: . utf8) !)
207+ }
111208
112209func start( ) {
210+ NotificationCenter . default. publisher ( for: NSApplication . didFinishLaunchingNotification)
211+ . sink { _ in menubarIcon = NSStatusBar . system. statusItem ( withLength: 1 ) }
212+ . store ( in: & observers)
113213 NotificationCenter . default. publisher ( for: NSApplication . didChangeScreenParametersNotification)
114214 . throttle ( for: 0.5 , scheduler: DispatchQueue . main, latest: true )
115215 . sink { _ in
@@ -128,18 +228,15 @@ func start() {
128228 lastStatus = path. status
129229
130230 switch path. status {
131- case . satisfied:
132- print ( " Internet connection: ON " )
133- drawColoredTopLine ( . systemGreen , hideAfter : 5 )
231+ case . satisfied, . requiresConnection :
232+ log ( " Internet connection: CHECKING " )
233+ startPingMonitor ( )
134234 case . unsatisfied:
135- print ( " Internet connection: OFF " )
235+ log ( " Internet connection: OFF " )
236+ DispatchQueue . main. async { process = nil }
136237 drawColoredTopLine ( . systemRed, hideAfter: 0 )
137- case . requiresConnection:
138- print ( " Internet connection: MAYBE " )
139- drawColoredTopLine ( . systemOrange, hideAfter: 5 )
140238 @unknown default :
141- print ( " Internet connection: UNKNOWN " )
142- drawColoredTopLine ( . systemYellow, hideAfter: 5 )
239+ log ( " Internet connection: \( path. status) " )
143240 }
144241 }
145242 monitor!. start ( queue: DispatchQueue . global ( ) )
@@ -151,13 +248,48 @@ func start() {
151248 #endif
152249}
153250
251+ let MS_REGEX_PATTERN : NSRegularExpression = try ! NSRegularExpression ( pattern: " ([0-9.]+) ms " , options: [ ] )
252+
253+ func startPingMonitor( ) {
254+ DispatchQueue . main. async {
255+ process = Process ( )
256+ process!. launchPath = FPING
257+ process!. arguments = [ " --loop " , " --size " , " 12 " , " --timeout " , " 500 " , " --interval " , " 10000 " , " 1.1.1.1 " ]
258+ process!. qualityOfService = . background
259+
260+ let pipe = Pipe ( )
261+ process!. standardOutput = pipe
262+ pipe. fileHandleForReading. readabilityHandler = { fh in
263+ guard let line = String ( data: fh. availableData, encoding: . utf8) , !line. isEmpty else {
264+ fh. readabilityHandler = nil
265+ DispatchQueue . main. async { process = nil }
266+ return
267+ }
268+ #if DEBUG
269+ print ( line)
270+ #endif
271+
272+ /*
273+ * fping output:
274+ * REACHABLE: `1.1.1.1 : [0], 20 bytes, 7.66 ms (7.66 avg, 0% loss)`
275+ * TIMEOUT: `1.1.1.1 : [0], timed out (NaN avg, 100% loss)`
276+ * SLOW: `1.1.1.1 : [0], 20 bytes, 127.66 ms (127.66 avg, 0% loss)`
277+ */
278+
279+ if line. contains ( " timed out " ) {
280+ lastPingStatus = . timedOut
281+ } else if let match = MS_REGEX_PATTERN . firstMatch ( in: line, options: [ ] , range: NSRange ( location: 0 , length: line. count) ) , let ms = Double ( ( line as NSString ) . substring ( with: match. range ( at: 1 ) ) ) {
282+ lastPingStatus = ms > 100 ? . slow( ms) : . reachable( ms)
283+ }
284+ }
285+
286+ process!. launch ( )
287+ }
288+ }
289+
154290@main
155291struct IsThereNetApp : App {
156292 init ( ) { start ( ) }
157293
158- var body : some Scene {
159- Settings {
160- EmptyView ( )
161- }
162- }
294+ var body : some Scene { Settings { EmptyView ( ) } }
163295}
0 commit comments