Skip to content

Commit 7ccbfad

Browse files
committed
Config file support, fixes #7
1 parent 8f3cf2b commit 7ccbfad

File tree

2 files changed

+155
-71
lines changed

2 files changed

+155
-71
lines changed

IsThereNet.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@
296296
CODE_SIGN_ENTITLEMENTS = IsThereNet/IsThereNet.entitlements;
297297
CODE_SIGN_STYLE = Automatic;
298298
COMBINE_HIDPI_IMAGES = YES;
299-
CURRENT_PROJECT_VERSION = 5;
299+
CURRENT_PROJECT_VERSION = 10;
300300
DEVELOPMENT_ASSET_PATHS = "\"IsThereNet/Preview Content\"";
301301
DEVELOPMENT_TEAM = RDDXV84A73;
302302
ENABLE_HARDENED_RUNTIME = YES;
@@ -311,7 +311,7 @@
311311
"@executable_path/../Frameworks",
312312
);
313313
MACOSX_DEPLOYMENT_TARGET = 11.0;
314-
MARKETING_VERSION = 1.3;
314+
MARKETING_VERSION = 1.4;
315315
PRODUCT_BUNDLE_IDENTIFIER = com.lowtechguys.IsThereNet;
316316
PRODUCT_NAME = "$(TARGET_NAME)";
317317
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -327,7 +327,7 @@
327327
CODE_SIGN_ENTITLEMENTS = IsThereNet/IsThereNet.entitlements;
328328
CODE_SIGN_STYLE = Automatic;
329329
COMBINE_HIDPI_IMAGES = YES;
330-
CURRENT_PROJECT_VERSION = 5;
330+
CURRENT_PROJECT_VERSION = 10;
331331
DEVELOPMENT_ASSET_PATHS = "\"IsThereNet/Preview Content\"";
332332
DEVELOPMENT_TEAM = RDDXV84A73;
333333
ENABLE_HARDENED_RUNTIME = YES;
@@ -342,7 +342,7 @@
342342
"@executable_path/../Frameworks",
343343
);
344344
MACOSX_DEPLOYMENT_TARGET = 11.0;
345-
MARKETING_VERSION = 1.3;
345+
MARKETING_VERSION = 1.4;
346346
PRODUCT_BUNDLE_IDENTIFIER = com.lowtechguys.IsThereNet;
347347
PRODUCT_NAME = "$(TARGET_NAME)";
348348
SWIFT_EMIT_LOC_STRINGS = YES;

IsThereNet/IsThereNetApp.swift

Lines changed: 151 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,51 +14,6 @@ import os.log
1414
import ServiceManagement
1515
import 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-
6217
private 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-
13071
private 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-
265209
func 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-
352296
private var slowCounter = MAX_COUNTS
353297
private var timeoutCounter = MAX_COUNTS
354298
private 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

Comments
 (0)