Skip to content

Commit a3107ab

Browse files
committed
1.1
1 parent b98ae07 commit a3107ab

File tree

6 files changed

+178
-20
lines changed

6 files changed

+178
-20
lines changed

IsThereNet.xcodeproj/project.pbxproj

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
C781D43C2B4592580050D04A /* IsThereNetApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C781D43B2B4592580050D04A /* IsThereNetApp.swift */; };
1111
C781D4402B4592590050D04A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C781D43F2B4592590050D04A /* Assets.xcassets */; };
1212
C781D4432B4592590050D04A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C781D4422B4592590050D04A /* Preview Assets.xcassets */; };
13+
C781D44D2B4815510050D04A /* fping in Resources */ = {isa = PBXBuildFile; fileRef = C781D44C2B4815510050D04A /* fping */; };
1314
/* End PBXBuildFile section */
1415

1516
/* Begin PBXFileReference section */
@@ -18,6 +19,7 @@
1819
C781D43F2B4592590050D04A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
1920
C781D4422B4592590050D04A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
2021
C781D4442B4592590050D04A /* IsThereNet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IsThereNet.entitlements; sourceTree = "<group>"; };
22+
C781D44C2B4815510050D04A /* fping */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = fping; sourceTree = "<group>"; };
2123
/* End PBXFileReference section */
2224

2325
/* Begin PBXFrameworksBuildPhase section */
@@ -50,6 +52,7 @@
5052
C781D43A2B4592580050D04A /* IsThereNet */ = {
5153
isa = PBXGroup;
5254
children = (
55+
C781D44C2B4815510050D04A /* fping */,
5356
C781D43B2B4592580050D04A /* IsThereNetApp.swift */,
5457
C781D43F2B4592590050D04A /* Assets.xcassets */,
5558
C781D4442B4592590050D04A /* IsThereNet.entitlements */,
@@ -124,6 +127,7 @@
124127
isa = PBXResourcesBuildPhase;
125128
buildActionMask = 2147483647;
126129
files = (
130+
C781D44D2B4815510050D04A /* fping in Resources */,
127131
C781D4432B4592590050D04A /* Preview Assets.xcassets in Resources */,
128132
C781D4402B4592590050D04A /* Assets.xcassets in Resources */,
129133
);
@@ -269,19 +273,22 @@
269273
CODE_SIGN_ENTITLEMENTS = IsThereNet/IsThereNet.entitlements;
270274
CODE_SIGN_STYLE = Automatic;
271275
COMBINE_HIDPI_IMAGES = YES;
272-
CURRENT_PROJECT_VERSION = 1;
276+
CURRENT_PROJECT_VERSION = 2;
273277
DEVELOPMENT_ASSET_PATHS = "\"IsThereNet/Preview Content\"";
274278
DEVELOPMENT_TEAM = RDDXV84A73;
275279
ENABLE_HARDENED_RUNTIME = YES;
276280
ENABLE_PREVIEWS = YES;
277281
GENERATE_INFOPLIST_FILE = YES;
282+
INFOPLIST_KEY_CFBundleDisplayName = IsThereNet;
283+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
278284
INFOPLIST_KEY_LSUIElement = YES;
279285
INFOPLIST_KEY_NSHumanReadableCopyright = "";
280286
LD_RUNPATH_SEARCH_PATHS = (
281287
"$(inherited)",
282288
"@executable_path/../Frameworks",
283289
);
284-
MARKETING_VERSION = 1.0;
290+
MACOSX_DEPLOYMENT_TARGET = 11.0;
291+
MARKETING_VERSION = 1.1;
285292
PRODUCT_BUNDLE_IDENTIFIER = com.lowtechguys.IsThereNet;
286293
PRODUCT_NAME = "$(TARGET_NAME)";
287294
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -297,19 +304,22 @@
297304
CODE_SIGN_ENTITLEMENTS = IsThereNet/IsThereNet.entitlements;
298305
CODE_SIGN_STYLE = Automatic;
299306
COMBINE_HIDPI_IMAGES = YES;
300-
CURRENT_PROJECT_VERSION = 1;
307+
CURRENT_PROJECT_VERSION = 2;
301308
DEVELOPMENT_ASSET_PATHS = "\"IsThereNet/Preview Content\"";
302309
DEVELOPMENT_TEAM = RDDXV84A73;
303310
ENABLE_HARDENED_RUNTIME = YES;
304311
ENABLE_PREVIEWS = YES;
305312
GENERATE_INFOPLIST_FILE = YES;
313+
INFOPLIST_KEY_CFBundleDisplayName = IsThereNet;
314+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
306315
INFOPLIST_KEY_LSUIElement = YES;
307316
INFOPLIST_KEY_NSHumanReadableCopyright = "";
308317
LD_RUNPATH_SEARCH_PATHS = (
309318
"$(inherited)",
310319
"@executable_path/../Frameworks",
311320
);
312-
MARKETING_VERSION = 1.0;
321+
MACOSX_DEPLOYMENT_TARGET = 11.0;
322+
MARKETING_VERSION = 1.1;
313323
PRODUCT_BUNDLE_IDENTIFIER = com.lowtechguys.IsThereNet;
314324
PRODUCT_NAME = "$(TARGET_NAME)";
315325
SWIFT_EMIT_LOC_STRINGS = YES;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Bucket
3+
uuid = "529A2695-CE10-4B98-A773-42B1A6F2C532"
4+
type = "1"
5+
version = "2.0">
6+
</Bucket>

IsThereNet/IsThereNet.entitlements

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88
<true/>
99
<key>com.apple.security.network.client</key>
1010
<true/>
11+
<key>com.apple.security.network.server</key>
12+
<true/>
1113
</dict>
1214
</plist>

IsThereNet/IsThereNetApp.swift

Lines changed: 147 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,25 @@ import Cocoa
1010
import Combine
1111
import Foundation
1212
import Network
13+
import os.log
1314
import ServiceManagement
1415
import 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+
1632
private 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?
106170
private var lastColor: NSColor?
107171
private var lastHideAfter: TimeInterval?
108172
private 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+
109182
private var monitor: NWPathMonitor?
183+
private var process: Process? {
184+
didSet {
185+
oldValue?.terminate()
186+
lastPingStatus = nil
187+
}
188+
}
110189
private 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

112209
func 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
155291
struct 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
}

IsThereNet/fping

205 KB
Binary file not shown.

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Colors:
2424

2525
- 🟢 **Green**: connected *(fades out after 5 seconds)*
2626
- 🔴 **Red**: disconnected *(stays on screen until connection is restored)*
27-
- 🟡 **Yellow**: might need a second check *(fades out after 5 seconds)*
27+
- 🟡 **Yellow**: slow internet *(fades out after 10 seconds)*
2828

2929
The top status line does not appear in screenshots and does not interfere with clicking on the menu bar.
3030

@@ -56,3 +56,11 @@ If you want to monitor more complex network conditions, you can use a few differ
5656

5757
- [iStat Menus](https://bjango.com/mac/istatmenus/) which is a paid app but does a lot more than just network monitoring (CPU, RAM, Disk, etc)
5858
- [PeakHour](https://peakhourapp.com/) which is a subscription-based app that does a lot of network monitoring, latency checks, etc
59+
60+
## Logging
61+
62+
IsThereNet logs internet connection status changes to:
63+
64+
- the system log (accessible via Console.app)
65+
- to a file in `~/Library/Containers/com.lowtechguys.IsThereNet/Data/Library/Caches/IsThereNet.log`
66+
- to the command line if you run the binary directly

0 commit comments

Comments
 (0)