Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67bfa9f681 | ||
|
|
4973a67ca9 |
@@ -9,6 +9,7 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
160334CC765E091A7CBE21AA /* MMCPULoadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE92B1A8D7FDBB6499448E1 /* MMCPULoadTests.swift */; };
|
||||
1879A3DCF6E305D7F2CD472E /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2345555FF9780B02A90CC7A9 /* CoreFoundation.framework */; };
|
||||
1B15F48F6B19E9EA4556ECD5 /* MMStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78325870C7CD8312AECA2236 /* MMStorageTests.swift */; };
|
||||
1B3F1C8ABF0ADFAA290FF6F6 /* MMSMCParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 375AA781A2AA0A20B119C7B7 /* MMSMCParser.m */; };
|
||||
3C0AC14F803C813287D8A663 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007F48A5A599AB4E8D216D16 /* IOKit.framework */; };
|
||||
4419FE7554489B23976E2970 /* MMCPULoadProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = D68FC8100D08DB6202F46339 /* MMCPULoadProvider.m */; };
|
||||
@@ -21,6 +22,7 @@
|
||||
CD91FC663B19DA1FEA3D49D7 /* MMAppleSMCClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 5555436CD3CB8A73F5D6504F /* MMAppleSMCClient.m */; };
|
||||
EAFAF5A10502D4ACC64F4720 /* MMTelemetryCoordinator.m in Sources */ = {isa = PBXBuildFile; fileRef = F9F9AD1268F906761032AF4D /* MMTelemetryCoordinator.m */; };
|
||||
EC9FA3E4A119277B89E73F8D /* MMSMCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940D608A1AF80F75584E744E /* MMSMCTests.swift */; };
|
||||
F2F7A141F2452EC1B4FB45D8 /* MMStorageTelemetryProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 6DCBCEA450103E2404761B78 /* MMStorageTelemetryProvider.m */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -42,13 +44,16 @@
|
||||
2EC3EDFCD5C50CB352FCED87 /* MacMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacMonitorTests.swift; sourceTree = "<group>"; };
|
||||
32340166F6100A08105308D3 /* MMTelemetryCoordinator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMTelemetryCoordinator.h; sourceTree = "<group>"; };
|
||||
375AA781A2AA0A20B119C7B7 /* MMSMCParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MMSMCParser.m; sourceTree = "<group>"; };
|
||||
46DE3AE12DAE26C4FE58034C /* MMStorageTelemetryProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMStorageTelemetryProvider.h; sourceTree = "<group>"; };
|
||||
4CE92B1A8D7FDBB6499448E1 /* MMCPULoadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MMCPULoadTests.swift; sourceTree = "<group>"; };
|
||||
5555436CD3CB8A73F5D6504F /* MMAppleSMCClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MMAppleSMCClient.m; sourceTree = "<group>"; };
|
||||
5635101F862DE680856BDE6E /* MMTelemetryDomain.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMTelemetryDomain.h; sourceTree = "<group>"; };
|
||||
56E285A0A1746AE659981F22 /* MacMonitorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacMonitorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
57845310A3D4EC67E48A3B11 /* MMCPULoadProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMCPULoadProvider.h; sourceTree = "<group>"; };
|
||||
6D0E5CAF25DFA9A1D2A698A0 /* MMTelemetryProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMTelemetryProvider.h; sourceTree = "<group>"; };
|
||||
6DCBCEA450103E2404761B78 /* MMStorageTelemetryProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MMStorageTelemetryProvider.m; sourceTree = "<group>"; };
|
||||
726005CB5A6C86E7D80D3CA0 /* MMAppleSMCClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMAppleSMCClient.h; sourceTree = "<group>"; };
|
||||
78325870C7CD8312AECA2236 /* MMStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MMStorageTests.swift; sourceTree = "<group>"; };
|
||||
8FBB436F3D9472194D8C6EDD /* MMSMCDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMSMCDefines.h; sourceTree = "<group>"; };
|
||||
940D608A1AF80F75584E744E /* MMSMCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MMSMCTests.swift; sourceTree = "<group>"; };
|
||||
991AEEAF262F3C6BE930BC77 /* MMMemoryTelemetryProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MMMemoryTelemetryProvider.m; sourceTree = "<group>"; };
|
||||
@@ -137,6 +142,7 @@
|
||||
children = (
|
||||
78383CED205BCA772701AE48 /* CPU */,
|
||||
EE2F5EFC9DC33CEDA79A131F /* Memory */,
|
||||
B6EB3F6545276C40212C2992 /* Storage */,
|
||||
);
|
||||
path = Telemetry;
|
||||
sourceTree = "<group>";
|
||||
@@ -149,6 +155,15 @@
|
||||
path = Hardware;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B6EB3F6545276C40212C2992 /* Storage */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
46DE3AE12DAE26C4FE58034C /* MMStorageTelemetryProvider.h */,
|
||||
6DCBCEA450103E2404761B78 /* MMStorageTelemetryProvider.m */,
|
||||
);
|
||||
path = Storage;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C80C3EEE8F912F4B214102E3 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -165,6 +180,7 @@
|
||||
4CE92B1A8D7FDBB6499448E1 /* MMCPULoadTests.swift */,
|
||||
099DCF7EF03A58ADA40A94A1 /* MMMemoryTests.swift */,
|
||||
940D608A1AF80F75584E744E /* MMSMCTests.swift */,
|
||||
78325870C7CD8312AECA2236 /* MMStorageTests.swift */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
@@ -277,6 +293,7 @@
|
||||
160334CC765E091A7CBE21AA /* MMCPULoadTests.swift in Sources */,
|
||||
6201FD599FED061F6C736E8B /* MMMemoryTests.swift in Sources */,
|
||||
EC9FA3E4A119277B89E73F8D /* MMSMCTests.swift in Sources */,
|
||||
1B15F48F6B19E9EA4556ECD5 /* MMStorageTests.swift in Sources */,
|
||||
9BCE74C5454F81EF80492E75 /* MacMonitorTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -290,6 +307,7 @@
|
||||
4419FE7554489B23976E2970 /* MMCPULoadProvider.m in Sources */,
|
||||
B9C2D6574E808B517BA34C83 /* MMMemoryTelemetryProvider.m in Sources */,
|
||||
1B3F1C8ABF0ADFAA290FF6F6 /* MMSMCParser.m in Sources */,
|
||||
F2F7A141F2452EC1B4FB45D8 /* MMStorageTelemetryProvider.m in Sources */,
|
||||
EAFAF5A10502D4ACC64F4720 /* MMTelemetryCoordinator.m in Sources */,
|
||||
88387CDAE2AC1CC74EF89B57 /* MacMonitorApp.swift in Sources */,
|
||||
A7609CE56D49EC3419987468 /* SystemTelemetryStore.swift in Sources */,
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
// Telemetry Providers
|
||||
#import "MMCPULoadProvider.h"
|
||||
#import "MMMemoryTelemetryProvider.h"
|
||||
#import "MMStorageTelemetryProvider.h"
|
||||
|
||||
#endif /* MacMonitor_Bridging_Header_h */
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef MMStorageTelemetryProvider_h
|
||||
#define MMStorageTelemetryProvider_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <sys/mount.h>
|
||||
#import "MMTelemetryProvider.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* MMStorageTelemetryProvider
|
||||
* Samples mounted disk volumes, APFS containers, used/free storage capacity,
|
||||
* and mount filesystem attributes.
|
||||
*/
|
||||
@interface MMStorageTelemetryProvider : NSObject <MMTelemetryProvider>
|
||||
|
||||
/**
|
||||
* Static parser method for converting statfs structures into volume dictionaries.
|
||||
*/
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)parseStatFSBuffer:(const struct statfs *)buffer count:(int)count;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#endif /* MMStorageTelemetryProvider_h */
|
||||
@@ -0,0 +1,108 @@
|
||||
#import "MMStorageTelemetryProvider.h"
|
||||
|
||||
@implementation MMStorageTelemetryProvider
|
||||
|
||||
- (MMTelemetryDomain)domain {
|
||||
return MMTelemetryDomainStorage;
|
||||
}
|
||||
|
||||
- (NSString *)providerIdentifier {
|
||||
return @"com.i3omb.macmonitor.telemetry.storage";
|
||||
}
|
||||
|
||||
- (BOOL)isAvailable {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)parseStatFSBuffer:(const struct statfs *)buffer count:(int)count {
|
||||
if (!buffer || count <= 0) return @[];
|
||||
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *volumes = [NSMutableArray arrayWithCapacity:count];
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const struct statfs *mnt = &buffer[i];
|
||||
|
||||
// Only include local mounts
|
||||
if (!(mnt->f_flags & MNT_LOCAL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *mountPoint = [NSString stringWithUTF8String:mnt->f_mntonname];
|
||||
NSString *deviceName = [NSString stringWithUTF8String:mnt->f_mntfromname];
|
||||
NSString *fsType = [NSString stringWithUTF8String:mnt->f_fstypename];
|
||||
|
||||
// Exclude virtual/internal devfs, autodefs, and synthetic system roots
|
||||
if ([fsType isEqualToString:@"devfs"] ||
|
||||
[fsType isEqualToString:@"autofs"] ||
|
||||
[mountPoint hasPrefix:@"/System/Volumes/Data/"] ||
|
||||
[deviceName containsString:@"com.apple.os.update"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t blockSize = (uint64_t)mnt->f_bsize;
|
||||
uint64_t totalBytes = (uint64_t)mnt->f_blocks * blockSize;
|
||||
uint64_t freeBytes = (uint64_t)mnt->f_bfree * blockSize;
|
||||
uint64_t availBytes = (uint64_t)mnt->f_bavail * blockSize;
|
||||
uint64_t usedBytes = (totalBytes >= freeBytes) ? (totalBytes - freeBytes) : 0;
|
||||
|
||||
if (totalBytes == 0) continue;
|
||||
|
||||
double usedPercent = ((double)usedBytes / (double)totalBytes) * 100.0;
|
||||
if (usedPercent < 0.0) usedPercent = 0.0;
|
||||
if (usedPercent > 100.0) usedPercent = 100.0;
|
||||
|
||||
NSString *volumeName = [mountPoint lastPathComponent];
|
||||
if ([mountPoint isEqualToString:@"/"]) {
|
||||
volumeName = @"Macintosh HD";
|
||||
}
|
||||
|
||||
BOOL isReadOnly = (mnt->f_flags & MNT_RDONLY) != 0;
|
||||
|
||||
[volumes addObject:@{
|
||||
@"volumeName": volumeName,
|
||||
@"mountPoint": mountPoint,
|
||||
@"devicePath": deviceName,
|
||||
@"fsType": fsType,
|
||||
@"totalBytes": @(totalBytes),
|
||||
@"usedBytes": @(usedBytes),
|
||||
@"freeBytes": @(freeBytes),
|
||||
@"availableBytes": @(availBytes),
|
||||
@"usedPercent": @(usedPercent),
|
||||
@"isReadOnly": @(isReadOnly)
|
||||
}];
|
||||
}
|
||||
|
||||
return [volumes copy];
|
||||
}
|
||||
|
||||
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
|
||||
struct statfs *mntbuf = NULL;
|
||||
int count = getmntinfo(&mntbuf, MNT_WAIT);
|
||||
|
||||
if (count <= 0 || mntbuf == NULL) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.Storage"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"getmntinfo returned no mounted filesystems"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSArray *volumes = [MMStorageTelemetryProvider parseStatFSBuffer:mntbuf count:count];
|
||||
|
||||
uint64_t totalStorage = 0;
|
||||
uint64_t totalUsed = 0;
|
||||
for (NSDictionary *v in volumes) {
|
||||
totalStorage += [v[@"totalBytes"] unsignedLongLongValue];
|
||||
totalUsed += [v[@"usedBytes"] unsignedLongLongValue];
|
||||
}
|
||||
|
||||
return @{
|
||||
@"volumes": volumes,
|
||||
@"volumeCount": @(volumes.count),
|
||||
@"aggregateTotalBytes": @(totalStorage),
|
||||
@"aggregateUsedBytes": @(totalUsed)
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,64 @@
|
||||
import XCTest
|
||||
@testable import MacMonitor
|
||||
|
||||
final class MMStorageTests: XCTestCase {
|
||||
|
||||
func testStatFSBufferParsing() {
|
||||
var stat = statfs()
|
||||
stat.f_bsize = 4096
|
||||
stat.f_blocks = 125_000_000 // 500 GB
|
||||
stat.f_bfree = 50_000_000 // 200 GB
|
||||
stat.f_bavail = 45_000_000
|
||||
stat.f_flags = UInt32(MNT_LOCAL)
|
||||
|
||||
let mountPath = "/Volumes/External"
|
||||
let devicePath = "/dev/disk2s1"
|
||||
let fsType = "apfs"
|
||||
|
||||
_ = mountPath.withCString { mntPtr in
|
||||
strncpy(&stat.f_mntonname.0, mntPtr, Int(MNAMELEN))
|
||||
}
|
||||
_ = devicePath.withCString { devPtr in
|
||||
strncpy(&stat.f_mntfromname.0, devPtr, Int(MNAMELEN))
|
||||
}
|
||||
_ = fsType.withCString { fsPtr in
|
||||
strncpy(&stat.f_fstypename.0, fsPtr, Int(MFSTYPENAMELEN))
|
||||
}
|
||||
|
||||
withUnsafePointer(to: stat) { ptr in
|
||||
let volumes = MMStorageTelemetryProvider.parseStatFSBuffer(ptr, count: 1)
|
||||
XCTAssertEqual(volumes.count, 1)
|
||||
|
||||
let vol = volumes.first!
|
||||
XCTAssertEqual(vol["volumeName"] as? String, "External")
|
||||
XCTAssertEqual(vol["mountPoint"] as? String, "/Volumes/External")
|
||||
XCTAssertEqual(vol["devicePath"] as? String, "/dev/disk2s1")
|
||||
XCTAssertEqual(vol["fsType"] as? String, "apfs")
|
||||
|
||||
let total = vol["totalBytes"] as? UInt64 ?? 0
|
||||
let free = vol["freeBytes"] as? UInt64 ?? 0
|
||||
let used = vol["usedBytes"] as? UInt64 ?? 0
|
||||
let pct = vol["usedPercent"] as? Double ?? 0
|
||||
|
||||
XCTAssertEqual(total, 512_000_000_000)
|
||||
XCTAssertEqual(free, 204_800_000_000)
|
||||
XCTAssertEqual(used, 307_200_000_000)
|
||||
XCTAssertEqual(pct, 60.0, accuracy: 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
func testLiveStorageProvider() throws {
|
||||
let provider = MMStorageTelemetryProvider()
|
||||
XCTAssertTrue(provider.isAvailable)
|
||||
XCTAssertEqual(provider.domain, .storage)
|
||||
|
||||
let sample = try provider.sampleTelemetry()
|
||||
let volumes = sample["volumes"] as? [[String: Any]]
|
||||
XCTAssertNotNil(volumes)
|
||||
XCTAssertGreaterThan(volumes?.count ?? 0, 0)
|
||||
|
||||
let firstVol = volumes?.first
|
||||
XCTAssertNotNil(firstVol?["mountPoint"])
|
||||
XCTAssertGreaterThan(firstVol?["totalBytes"] as? UInt64 ?? 0, 0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user