@@ -14,51 +14,6 @@ import os.log
1414import ServiceManagement
1515import SwiftUI
1616
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-
32- private var window : NSWindow = {
33- let w = NSWindow (
34- contentRect: NSRect ( x: 0 , y: 0 , width: NSScreen . main!. frame. width, height: 20 ) ,
35- styleMask: [ . fullSizeContentView, . borderless] ,
36- backing: . buffered,
37- defer: false
38- )
39- w. backgroundColor = . clear
40- w. level = NSWindow . Level ( Int ( CGShieldingWindowLevel ( ) ) )
41-
42- w. isOpaque = false
43- w. hasShadow = false
44- w. hidesOnDeactivate = false
45- w. ignoresMouseEvents = true
46- w. isReleasedWhenClosed = false
47- w. isMovableByWindowBackground = false
48-
49- w. sharingType = . none
50- w. setAccessibilityRole ( . popover)
51- w. setAccessibilitySubrole ( . unknown)
52-
53- w. collectionBehavior = [ . canJoinAllSpaces, . stationary, . ignoresCycle, . fullScreenDisallowsTiling]
54- w. alphaValue = 0.0
55-
56- return w
57- } ( )
58- private var windowController = NSWindowController ( window: window)
59- private var fader : DispatchWorkItem ? { didSet { oldValue? . cancel ( ) } }
60- private var closer : DispatchWorkItem ? { didSet { oldValue? . cancel ( ) } }
61-
6217private func mainAsyncAfter( _ duration: TimeInterval , _ action: @escaping ( ) -> Void ) -> DispatchWorkItem {
6318 let workItem = DispatchWorkItem { action ( ) }
6419 DispatchQueue . main. asyncAfter ( deadline: . now( ) + duration, execute: workItem)
@@ -113,20 +68,6 @@ private func drawColoredTopLine(_ color: NSColor, hideAfter: TimeInterval = 5) {
11368 }
11469}
11570
116- extension NSWindow {
117- func fade( to alpha: CGFloat , duration: TimeInterval = 1.0 , then: ( ( ) -> Void ) ? = nil ) {
118- NSAnimationContext . runAnimationGroup { ctx in
119- ctx. duration = duration
120- ctx. timingFunction = CAMediaTimingFunction ( name: . easeOut)
121- animator ( ) . alphaValue = alpha
122- } completionHandler: { then ? ( ) }
123- }
124- }
125-
126- extension NSAppearance {
127- var isDark : Bool { name == . vibrantDark || name == . darkAqua }
128- }
129-
13071private enum PingStatus : Equatable {
13172 case reachable( Double )
13273 case timedOut
@@ -217,6 +158,11 @@ func start() {
217158 NotificationCenter . default. publisher ( for: NSApplication . willTerminateNotification)
218159 . sink { _ in
219160 process? . terminate ( )
161+ if let stream = CONFIG_FS_WATCHER {
162+ FSEventStreamStop ( stream)
163+ FSEventStreamInvalidate ( stream)
164+ CONFIG_FS_WATCHER = nil
165+ }
220166 }
221167 . store ( in: & observers)
222168 NotificationCenter . default. publisher ( for: NSApplication . didChangeScreenParametersNotification)
@@ -260,24 +206,24 @@ func start() {
260206 #endif
261207}
262208
263- let MS_REGEX_PATTERN : NSRegularExpression = try ! NSRegularExpression ( pattern: " ([0-9.]+) ms " , options: [ ] )
264-
265209func startPingMonitor( ) {
266210 DispatchQueue . main. async {
267211 pingRestartTask = nil
268212
269213 process = Process ( )
270214 process!. launchPath = FPING
271- process!. arguments = [ " --loop " , " --size " , " 12 " , " --timeout " , " 1000 " , " --interval " , " 5000 " , " 1.1.1.1 " ]
215+ process!. arguments = [ " --loop " , " --size " , " 12 " , " --timeout " , " \( CONFIG . pingTimeoutSeconds . ms ) " , " --interval " , " \( CONFIG . pingIntervalSeconds . ms ) " , CONFIG . pingIP ]
272216 process!. qualityOfService = . userInteractive
273217
274218 let pipe = Pipe ( )
275219 process!. standardOutput = pipe
276220 pipe. fileHandleForReading. readabilityHandler = { fh in
277221 guard let line = String ( data: fh. availableData, encoding: . utf8) , !line. isEmpty else {
278222 fh. readabilityHandler = nil
279- DispatchQueue . main. async { process = nil }
280- pingRestartTask = mainAsyncAfter ( 5 ) { startPingMonitor ( ) }
223+ DispatchQueue . main. async {
224+ process = nil
225+ pingRestartTask = mainAsyncAfter ( 5 ) { startPingMonitor ( ) }
226+ }
281227 return
282228 }
283229 #if DEBUG
@@ -307,7 +253,7 @@ func startPingMonitor() {
307253 slowCounter = MAX_COUNTS
308254 fastCounter = MAX_COUNTS
309255 timeoutCounter = MAX_COUNTS
310- lastPingStatus = ms > 300 ? . slow( ms) : . reachable( ms)
256+ lastPingStatus = ms > CONFIG . pingSlowThresholdMilliseconds ? . slow( ms) : . reachable( ms)
311257 return
312258 }
313259
@@ -347,8 +293,6 @@ func startPingMonitor() {
347293 }
348294}
349295
350- private let MAX_COUNTS = 2
351-
352296private var slowCounter = MAX_COUNTS
353297private var timeoutCounter = MAX_COUNTS
354298private var fastCounter = MAX_COUNTS
@@ -364,3 +308,143 @@ struct IsThereNetApp: App {
364308
365309 var body : some Scene { Settings { EmptyView ( ) } }
366310}
311+
312+ // MARK: Constants
313+
314+ private let MS_REGEX_PATTERN : NSRegularExpression = try ! NSRegularExpression ( pattern: " ([0-9.]+) ms " , options: [ ] )
315+ private let MAX_COUNTS = 2
316+ private let FPING = Bundle . main. path ( forResource: " fping " , ofType: nil ) !
317+ private let LOG_PATH = FileManager . default. urls ( for: . cachesDirectory, in: . userDomainMask) [ 0 ] . appendingPathComponent ( " IsThereNet.log " )
318+ private let LOG_FILE : FileHandle ? = {
319+ guard FileManager . default. fileExists ( atPath: LOG_PATH . path) || FileManager . default. createFile ( atPath: LOG_PATH . path, contents: nil , attributes: nil ) else {
320+ print ( " Failed to create log file " )
321+ return nil
322+ }
323+ guard let file = try ? FileHandle ( forUpdating: LOG_PATH) else {
324+ print ( " Failed to open log file " )
325+ return nil
326+ }
327+ print ( " Logging to \( LOG_PATH . path) " )
328+ return file
329+ } ( )
330+ private let CONFIG_PATH = FileManager . default. urls ( for: . applicationSupportDirectory, in: . userDomainMask) [ 0 ] . appendingPathComponent ( " config.json " )
331+
332+ // MARK: Config
333+
334+ private struct Config : Codable , Equatable {
335+ var pingIP = " 1.1.1.1 "
336+ var pingIntervalSeconds = 5.0
337+ var pingTimeoutSeconds = 1.0
338+ var pingSlowThresholdMilliseconds = 300.0
339+ }
340+
341+ private var CONFIG_FS_WATCHER : FSEventStreamRef ?
342+ private var CONFIG : Config = {
343+ print ( " Watching config path: \( CONFIG_PATH . path) " )
344+
345+ CONFIG_FS_WATCHER = FSEventStreamCreate (
346+ kCFAllocatorDefault,
347+ { _, _, _, _, flags, _ in
348+ guard flags. pointee != kFSEventStreamEventFlagHistoryDone else {
349+ return
350+ }
351+
352+ guard let data = try ? Data ( contentsOf: CONFIG_PATH) else {
353+ log ( " Failed to read config.json " )
354+ return
355+ }
356+ guard let config = try ? JSONDecoder ( ) . decode ( Config . self, from: data) else {
357+ log ( " Failed to decode config.json " )
358+ return
359+ }
360+ guard config != CONFIG else {
361+ return
362+ }
363+
364+ CONFIG = config
365+ log ( " Config updated: \( CONFIG) " )
366+
367+ DispatchQueue . main. async {
368+ guard process != nil else {
369+ return
370+ }
371+ process? . terminate ( )
372+ process = nil
373+ pingRestartTask = mainAsyncAfter ( 1 ) { startPingMonitor ( ) }
374+ }
375+ } ,
376+ nil , [ CONFIG_PATH . path] as [ NSString ] as NSArray as CFArray ,
377+ FSEventStreamEventId ( UInt32 ( truncatingIfNeeded: kFSEventStreamEventIdSinceNow) ) , 0.5 as CFTimeInterval ,
378+ FSEventStreamCreateFlags ( kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes)
379+ )
380+ if let stream = CONFIG_FS_WATCHER {
381+ FSEventStreamSetDispatchQueue ( stream, DispatchQueue . main)
382+ FSEventStreamStart ( stream)
383+ }
384+
385+ guard let data = try ? Data ( contentsOf: CONFIG_PATH) , let config = try ? JSONDecoder ( ) . decode ( Config . self, from: data) else {
386+ let defaultConfig = Config ( )
387+ let prettyJsonEncoder = JSONEncoder ( )
388+ prettyJsonEncoder. outputFormatting = . prettyPrinted
389+ try ? prettyJsonEncoder. encode ( defaultConfig) . write ( to: CONFIG_PATH)
390+
391+ return defaultConfig
392+ }
393+ return config
394+ } ( )
395+
396+ // MARK: Extensions
397+
398+ extension Double {
399+ var intround : Int { Int ( rounded ( ) ) }
400+ }
401+
402+ extension TimeInterval {
403+ var ms : Int { ( self * 1000 ) . intround }
404+ }
405+
406+ extension NSWindow {
407+ func fade( to alpha: CGFloat , duration: TimeInterval = 1.0 , then: ( ( ) -> Void ) ? = nil ) {
408+ NSAnimationContext . runAnimationGroup { ctx in
409+ ctx. duration = duration
410+ ctx. timingFunction = CAMediaTimingFunction ( name: . easeOut)
411+ animator ( ) . alphaValue = alpha
412+ } completionHandler: { then ? ( ) }
413+ }
414+ }
415+
416+ extension NSAppearance {
417+ var isDark : Bool { name == . vibrantDark || name == . darkAqua }
418+ }
419+
420+ // MARK: Window
421+
422+ private var window : NSWindow = {
423+ let w = NSWindow (
424+ contentRect: NSRect ( x: 0 , y: 0 , width: NSScreen . main!. frame. width, height: 20 ) ,
425+ styleMask: [ . fullSizeContentView, . borderless] ,
426+ backing: . buffered,
427+ defer: false
428+ )
429+ w. backgroundColor = . clear
430+ w. level = NSWindow . Level ( Int ( CGShieldingWindowLevel ( ) ) )
431+
432+ w. isOpaque = false
433+ w. hasShadow = false
434+ w. hidesOnDeactivate = false
435+ w. ignoresMouseEvents = true
436+ w. isReleasedWhenClosed = false
437+ w. isMovableByWindowBackground = false
438+
439+ w. sharingType = . none
440+ w. setAccessibilityRole ( . popover)
441+ w. setAccessibilitySubrole ( . unknown)
442+
443+ w. collectionBehavior = [ . canJoinAllSpaces, . stationary, . ignoresCycle, . fullScreenDisallowsTiling]
444+ w. alphaValue = 0.0
445+
446+ return w
447+ } ( )
448+ private var windowController = NSWindowController ( window: window)
449+ private var fader : DispatchWorkItem ? { didSet { oldValue? . cancel ( ) } }
450+ private var closer : DispatchWorkItem ? { didSet { oldValue? . cancel ( ) } }
0 commit comments