Compare commits
3
Commits
72a7000c19
...
dba5af397f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dba5af397f | ||
|
|
558a19f6ae | ||
|
|
4dedf17436 |
+27
@@ -0,0 +1,27 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon
|
||||
|
||||
# Xcode & DerivedData
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-builder
|
||||
*.hmap
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
|
||||
# Xcode user state
|
||||
xcuserdata/
|
||||
*.xcuserdatad
|
||||
*.xcuserstate
|
||||
|
||||
# Swift Package Manager
|
||||
.build/
|
||||
.swiftpm/
|
||||
|
||||
# Build outputs & Archives
|
||||
*.dmg
|
||||
*.zip
|
||||
*.app
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.get-task-allow</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,523 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1879A3DCF6E305D7F2CD472E /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2345555FF9780B02A90CC7A9 /* CoreFoundation.framework */; };
|
||||
1B3F1C8ABF0ADFAA290FF6F6 /* MMSMCParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 375AA781A2AA0A20B119C7B7 /* MMSMCParser.m */; };
|
||||
3C0AC14F803C813287D8A663 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007F48A5A599AB4E8D216D16 /* IOKit.framework */; };
|
||||
88387CDAE2AC1CC74EF89B57 /* MacMonitorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEA6BD38BA8D887B1DB64A9 /* MacMonitorApp.swift */; };
|
||||
9BCE74C5454F81EF80492E75 /* MacMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EC3EDFCD5C50CB352FCED87 /* MacMonitorTests.swift */; };
|
||||
A7609CE56D49EC3419987468 /* SystemTelemetryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B4E94FB2886C60D5548A2A /* SystemTelemetryStore.swift */; };
|
||||
A9FCAEC0BF27A419DB148C4A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E142AE61F9B87757997870DD /* ContentView.swift */; };
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
09422FEC7F2490CC38ED2F77 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 50A0A2115D830ED94B0DCD89 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 8DA8F83F9F811331FC56346B;
|
||||
remoteInfo = MacMonitor;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
007F48A5A599AB4E8D216D16 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
|
||||
1AD9EEE571CD904BBCD1C96B /* MMSMCParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMSMCParser.h; sourceTree = "<group>"; };
|
||||
2345555FF9780B02A90CC7A9 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||
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>"; };
|
||||
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; };
|
||||
6D0E5CAF25DFA9A1D2A698A0 /* MMTelemetryProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMTelemetryProvider.h; sourceTree = "<group>"; };
|
||||
726005CB5A6C86E7D80D3CA0 /* MMAppleSMCClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMAppleSMCClient.h; 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>"; };
|
||||
ADEA6BD38BA8D887B1DB64A9 /* MacMonitorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacMonitorApp.swift; sourceTree = "<group>"; };
|
||||
E142AE61F9B87757997870DD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
E4B4E94FB2886C60D5548A2A /* SystemTelemetryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemTelemetryStore.swift; sourceTree = "<group>"; };
|
||||
F5EC59DCDA5FABDA409CDBCB /* MacMonitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacMonitor.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F9F9AD1268F906761032AF4D /* MMTelemetryCoordinator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MMTelemetryCoordinator.m; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
232654D97C29A6EB7C0150DA /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3C0AC14F803C813287D8A663 /* IOKit.framework in Frameworks */,
|
||||
1879A3DCF6E305D7F2CD472E /* CoreFoundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
1E4B6EF5F92A76F77E44AEAD = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D4E3C74114E2D44711BCFAD /* Sources */,
|
||||
C9A24E2CE4CBD45E8F5175DD /* Tests */,
|
||||
E56B76815ED75B28A13D6814 /* Frameworks */,
|
||||
C80C3EEE8F912F4B214102E3 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D4E3C74114E2D44711BCFAD /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4D8D8B40632C525CBC1D0E43 /* App */,
|
||||
54787E6270EA7B9A1BE11359 /* Core */,
|
||||
9844EC526E3527F6A582179F /* Hardware */,
|
||||
3E4A0B53C583BD0B7A68EACE /* UI */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3E4A0B53C583BD0B7A68EACE /* UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E142AE61F9B87757997870DD /* ContentView.swift */,
|
||||
);
|
||||
path = UI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4D8D8B40632C525CBC1D0E43 /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ADEA6BD38BA8D887B1DB64A9 /* MacMonitorApp.swift */,
|
||||
E4B4E94FB2886C60D5548A2A /* SystemTelemetryStore.swift */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54787E6270EA7B9A1BE11359 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
32340166F6100A08105308D3 /* MMTelemetryCoordinator.h */,
|
||||
F9F9AD1268F906761032AF4D /* MMTelemetryCoordinator.m */,
|
||||
5635101F862DE680856BDE6E /* MMTelemetryDomain.h */,
|
||||
6D0E5CAF25DFA9A1D2A698A0 /* MMTelemetryProvider.h */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9844EC526E3527F6A582179F /* Hardware */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E012FBB26EF9E35A2FAF87E2 /* AppleSMC */,
|
||||
);
|
||||
path = Hardware;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C80C3EEE8F912F4B214102E3 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F5EC59DCDA5FABDA409CDBCB /* MacMonitor.app */,
|
||||
56E285A0A1746AE659981F22 /* MacMonitorTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C9A24E2CE4CBD45E8F5175DD /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2EC3EDFCD5C50CB352FCED87 /* MacMonitorTests.swift */,
|
||||
940D608A1AF80F75584E744E /* MMSMCTests.swift */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E012FBB26EF9E35A2FAF87E2 /* AppleSMC */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
726005CB5A6C86E7D80D3CA0 /* MMAppleSMCClient.h */,
|
||||
5555436CD3CB8A73F5D6504F /* MMAppleSMCClient.m */,
|
||||
8FBB436F3D9472194D8C6EDD /* MMSMCDefines.h */,
|
||||
1AD9EEE571CD904BBCD1C96B /* MMSMCParser.h */,
|
||||
375AA781A2AA0A20B119C7B7 /* MMSMCParser.m */,
|
||||
);
|
||||
path = AppleSMC;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E56B76815ED75B28A13D6814 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2345555FF9780B02A90CC7A9 /* CoreFoundation.framework */,
|
||||
007F48A5A599AB4E8D216D16 /* IOKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
8DA8F83F9F811331FC56346B /* MacMonitor */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = F8BFF00A996663DD56D3BBC8 /* Build configuration list for PBXNativeTarget "MacMonitor" */;
|
||||
buildPhases = (
|
||||
9F478799591119C7B9BC9B18 /* Sources */,
|
||||
232654D97C29A6EB7C0150DA /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = MacMonitor;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = MacMonitor;
|
||||
productReference = F5EC59DCDA5FABDA409CDBCB /* MacMonitor.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
AC0AE508FCA91494F76BAB20 /* MacMonitorTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7116650F4E57A5D713138F34 /* Build configuration list for PBXNativeTarget "MacMonitorTests" */;
|
||||
buildPhases = (
|
||||
28403A95C96F704B96652C2E /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
E940019BD253D9933F8340A5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = MacMonitorTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = MacMonitorTests;
|
||||
productReference = 56E285A0A1746AE659981F22 /* MacMonitorTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
50A0A2115D830ED94B0DCD89 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1600;
|
||||
TargetAttributes = {
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 00AAEAE8F34AB900B7EE6E43 /* Build configuration list for PBXProject "MacMonitor" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 1E4B6EF5F92A76F77E44AEAD;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = C80C3EEE8F912F4B214102E3 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
8DA8F83F9F811331FC56346B /* MacMonitor */,
|
||||
AC0AE508FCA91494F76BAB20 /* MacMonitorTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
28403A95C96F704B96652C2E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EC9FA3E4A119277B89E73F8D /* MMSMCTests.swift in Sources */,
|
||||
9BCE74C5454F81EF80492E75 /* MacMonitorTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9F478799591119C7B9BC9B18 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A9FCAEC0BF27A419DB148C4A /* ContentView.swift in Sources */,
|
||||
CD91FC663B19DA1FEA3D49D7 /* MMAppleSMCClient.m in Sources */,
|
||||
1B3F1C8ABF0ADFAA290FF6F6 /* MMSMCParser.m in Sources */,
|
||||
EAFAF5A10502D4ACC64F4720 /* MMTelemetryCoordinator.m in Sources */,
|
||||
88387CDAE2AC1CC74EF89B57 /* MacMonitorApp.swift in Sources */,
|
||||
A7609CE56D49EC3419987468 /* SystemTelemetryStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
E940019BD253D9933F8340A5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 8DA8F83F9F811331FC56346B /* MacMonitor */;
|
||||
targetProxy = 09422FEC7F2490CC38ED2F77 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
3BE0BD5FC635F9E80F31CE13 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = MacMonitor.entitlements;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(SRCROOT)/Sources/Core",
|
||||
"$(SRCROOT)/Sources/Bridging",
|
||||
"$(SRCROOT)/Sources/Hardware/**",
|
||||
);
|
||||
INFOPLIST_KEY_CFBundleDisplayName = MacMonitor;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.i3omb.MacMonitor;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Sources/Bridging/MacMonitor-Bridging-Header.h";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
59F6FC23B6D4F0B126319453 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(SRCROOT)/Sources/Core",
|
||||
"$(SRCROOT)/Sources/Bridging",
|
||||
"$(SRCROOT)/Sources/Hardware/**",
|
||||
);
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.i3omb.MacMonitorTests;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Sources/Bridging/MacMonitor-Bridging-Header.h";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MacMonitor.app/Contents/MacOS/MacMonitor";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
5B1AD1F77021853BC57757D7 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ARCHS = x86_64;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VALID_ARCHS = x86_64;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
5D8000DCD693316A897E9E66 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(SRCROOT)/Sources/Core",
|
||||
"$(SRCROOT)/Sources/Bridging",
|
||||
"$(SRCROOT)/Sources/Hardware/**",
|
||||
);
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.i3omb.MacMonitorTests;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Sources/Bridging/MacMonitor-Bridging-Header.h";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MacMonitor.app/Contents/MacOS/MacMonitor";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
67DE32FBD89D242E194436AA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ARCHS = x86_64;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VALID_ARCHS = x86_64;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7FA38ECBD02E9FC203FFF0EA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = MacMonitor.entitlements;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(SRCROOT)/Sources/Core",
|
||||
"$(SRCROOT)/Sources/Bridging",
|
||||
"$(SRCROOT)/Sources/Hardware/**",
|
||||
);
|
||||
INFOPLIST_KEY_CFBundleDisplayName = MacMonitor;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.i3omb.MacMonitor;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Sources/Bridging/MacMonitor-Bridging-Header.h";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
00AAEAE8F34AB900B7EE6E43 /* Build configuration list for PBXProject "MacMonitor" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
67DE32FBD89D242E194436AA /* Debug */,
|
||||
5B1AD1F77021853BC57757D7 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
7116650F4E57A5D713138F34 /* Build configuration list for PBXNativeTarget "MacMonitorTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
5D8000DCD693316A897E9E66 /* Debug */,
|
||||
59F6FC23B6D4F0B126319453 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
F8BFF00A996663DD56D3BBC8 /* Build configuration list for PBXNativeTarget "MacMonitor" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3BE0BD5FC635F9E80F31CE13 /* Debug */,
|
||||
7FA38ECBD02E9FC203FFF0EA /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 50A0A2115D830ED94B0DCD89 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,15 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct MacMonitorApp: App {
|
||||
@State private var store = SystemTelemetryStore.shared
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
.windowStyle(.titleBar)
|
||||
.windowToolbarStyle(.unified)
|
||||
.defaultSize(width: 960, height: 640)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Observation
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
public final class SystemTelemetryStore {
|
||||
public static let shared = SystemTelemetryStore()
|
||||
|
||||
public private(set) var isRunning: Bool = false
|
||||
public private(set) var lastUpdateTimestamp: Date = .now
|
||||
public private(set) var sampleInterval: TimeInterval = 1.0
|
||||
|
||||
/// Raw snapshots organized by provider identifier
|
||||
public private(set) var latestSnapshot: [String: [String: Any]] = [:]
|
||||
|
||||
/// System identification
|
||||
public let hostModel: String
|
||||
public let osVersion: String
|
||||
public let kernelVersion: String
|
||||
public let physicalCpuCount: Int
|
||||
public let logicalCpuCount: Int
|
||||
|
||||
private let coordinator = MMTelemetryCoordinator.shared
|
||||
|
||||
private init() {
|
||||
self.hostModel = SystemTelemetryStore.readSysctlString("hw.model") ?? "Intel Mac"
|
||||
self.osVersion = ProcessInfo.processInfo.operatingSystemVersionString
|
||||
self.kernelVersion = SystemTelemetryStore.readSysctlString("kern.osrelease") ?? "Unknown Kernel"
|
||||
self.physicalCpuCount = SystemTelemetryStore.readSysctlInt("hw.physicalcpu") ?? 1
|
||||
self.logicalCpuCount = SystemTelemetryStore.readSysctlInt("hw.logicalcpu") ?? 1
|
||||
|
||||
setupCoordinator()
|
||||
}
|
||||
|
||||
private func setupCoordinator() {
|
||||
coordinator.sampleInterval = self.sampleInterval
|
||||
coordinator.snapshotHandler = { [weak self] snapshot in
|
||||
guard let self else { return }
|
||||
self.latestSnapshot = snapshot
|
||||
self.lastUpdateTimestamp = Date()
|
||||
}
|
||||
}
|
||||
|
||||
public func start() {
|
||||
coordinator.start()
|
||||
isRunning = coordinator.isRunning
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
coordinator.stop()
|
||||
isRunning = coordinator.isRunning
|
||||
}
|
||||
|
||||
public func setSampleInterval(_ interval: TimeInterval) {
|
||||
self.sampleInterval = interval
|
||||
coordinator.sampleInterval = interval
|
||||
}
|
||||
|
||||
public func triggerManualSample() {
|
||||
coordinator.sampleImmediately()
|
||||
}
|
||||
|
||||
public var registeredProviderCount: Int {
|
||||
coordinator.registeredProviders().count
|
||||
}
|
||||
|
||||
private static func readSysctlString(_ name: String) -> String? {
|
||||
var size: size_t = 0
|
||||
sysctlbyname(name, nil, &size, nil, 0)
|
||||
guard size > 0 else { return nil }
|
||||
var buffer = [CChar](repeating: 0, count: size)
|
||||
sysctlbyname(name, &buffer, &size, nil, 0)
|
||||
return String(cString: buffer)
|
||||
}
|
||||
|
||||
private static func readSysctlInt(_ name: String) -> Int? {
|
||||
var value: Int32 = 0
|
||||
var size = MemoryLayout<Int32>.size
|
||||
let result = sysctlbyname(name, &value, &size, nil, 0)
|
||||
return result == 0 ? Int(value) : nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef MacMonitor_Bridging_Header_h
|
||||
#define MacMonitor_Bridging_Header_h
|
||||
|
||||
// Core Architecture & Telemetry Contracts
|
||||
#import "MMTelemetryDomain.h"
|
||||
#import "MMTelemetryProvider.h"
|
||||
#import "MMTelemetryCoordinator.h"
|
||||
|
||||
// Hardware Abstraction - AppleSMC
|
||||
#import "MMSMCDefines.h"
|
||||
#import "MMSMCParser.h"
|
||||
#import "MMAppleSMCClient.h"
|
||||
|
||||
#endif /* MacMonitor_Bridging_Header_h */
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef MMTelemetryCoordinator_h
|
||||
#define MMTelemetryCoordinator_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MMTelemetryProvider.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^MMTelemetrySnapshotHandler)(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *snapshot);
|
||||
|
||||
/**
|
||||
* MMTelemetryCoordinator
|
||||
* Central dispatcher orchestrating telemetry sampling loops across all registered providers
|
||||
* on a dedicated low-priority background queue (QOS_CLASS_UTILITY).
|
||||
*/
|
||||
@interface MMTelemetryCoordinator : NSObject
|
||||
|
||||
@property (class, readonly, strong) MMTelemetryCoordinator *sharedCoordinator;
|
||||
|
||||
/**
|
||||
* Current polling interval in seconds. Defaults to 1.0 second.
|
||||
*/
|
||||
@property (nonatomic, assign) NSTimeInterval sampleInterval;
|
||||
|
||||
/**
|
||||
* Whether the sampling timer is actively running.
|
||||
*/
|
||||
@property (nonatomic, readonly, getter=isRunning) BOOL running;
|
||||
|
||||
/**
|
||||
* Snapshot delivery callback dispatched on the main thread whenever a new sampling cycle completes.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) MMTelemetrySnapshotHandler snapshotHandler;
|
||||
|
||||
/**
|
||||
* Registers a provider. Thread-safe.
|
||||
*/
|
||||
- (void)registerProvider:(id<MMTelemetryProvider>)provider;
|
||||
|
||||
/**
|
||||
* Unregisters a provider by its unique identifier. Thread-safe.
|
||||
*/
|
||||
- (void)unregisterProviderWithIdentifier:(NSString *)identifier;
|
||||
|
||||
/**
|
||||
* Returns a list of currently registered providers.
|
||||
*/
|
||||
- (NSArray<id<MMTelemetryProvider>> *)registeredProviders;
|
||||
|
||||
/**
|
||||
* Starts the recurring telemetry dispatch timer.
|
||||
*/
|
||||
- (void)start;
|
||||
|
||||
/**
|
||||
* Stops the recurring telemetry dispatch timer.
|
||||
*/
|
||||
- (void)stop;
|
||||
|
||||
/**
|
||||
* Triggers an immediate one-shot sample pass across all providers.
|
||||
*/
|
||||
- (void)sampleImmediately;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#endif /* MMTelemetryCoordinator_h */
|
||||
@@ -0,0 +1,171 @@
|
||||
#import "MMTelemetryCoordinator.h"
|
||||
#import <os/lock.h>
|
||||
|
||||
@interface MMTelemetryCoordinator () {
|
||||
os_unfair_lock _lock;
|
||||
dispatch_queue_t _telemetryQueue;
|
||||
dispatch_source_t _timerSource;
|
||||
NSMutableDictionary<NSString *, id<MMTelemetryProvider>> *_providers;
|
||||
BOOL _running;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MMTelemetryCoordinator
|
||||
|
||||
+ (instancetype)sharedCoordinator {
|
||||
static MMTelemetryCoordinator *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_lock = OS_UNFAIR_LOCK_INIT;
|
||||
_sampleInterval = 1.0;
|
||||
_providers = [NSMutableDictionary dictionary];
|
||||
|
||||
dispatch_queue_attr_t qosAttr = dispatch_queue_attr_make_with_qos_class(
|
||||
DISPATCH_QUEUE_SERIAL,
|
||||
QOS_CLASS_UTILITY,
|
||||
0
|
||||
);
|
||||
_telemetryQueue = dispatch_queue_create("com.i3omb.macmonitor.telemetry", qosAttr);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stop];
|
||||
}
|
||||
|
||||
- (void)setSampleInterval:(NSTimeInterval)sampleInterval {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
if (sampleInterval < 0.1) {
|
||||
sampleInterval = 0.1; // clamp minimum sampling interval to 100ms
|
||||
}
|
||||
_sampleInterval = sampleInterval;
|
||||
BOOL shouldRestart = _running;
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
|
||||
if (shouldRestart) {
|
||||
[self stop];
|
||||
[self start];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isRunning {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
BOOL r = _running;
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
- (void)registerProvider:(id<MMTelemetryProvider>)provider {
|
||||
if (!provider || !provider.providerIdentifier) return;
|
||||
|
||||
os_unfair_lock_lock(&_lock);
|
||||
_providers[provider.providerIdentifier] = provider;
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
|
||||
if ([provider respondsToSelector:@selector(startMonitoring)]) {
|
||||
[provider startMonitoring];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)unregisterProviderWithIdentifier:(NSString *)identifier {
|
||||
if (!identifier) return;
|
||||
|
||||
id<MMTelemetryProvider> provider = nil;
|
||||
os_unfair_lock_lock(&_lock);
|
||||
provider = _providers[identifier];
|
||||
[_providers removeObjectForKey:identifier];
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
|
||||
if (provider && [provider respondsToSelector:@selector(stopMonitoring)]) {
|
||||
[provider stopMonitoring];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<id<MMTelemetryProvider>> *)registeredProviders {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
NSArray *list = [_providers.allValues copy];
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return list;
|
||||
}
|
||||
|
||||
- (void)start {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
if (_running) {
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return;
|
||||
}
|
||||
_running = YES;
|
||||
|
||||
_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _telemetryQueue);
|
||||
uint64_t intervalNs = (uint64_t)(_sampleInterval * NSEC_PER_SEC);
|
||||
dispatch_source_set_timer(_timerSource, dispatch_time(DISPATCH_TIME_NOW, 0), intervalNs, intervalNs / 10);
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_source_set_event_handler(_timerSource, ^{
|
||||
[weakSelf performSamplePass];
|
||||
});
|
||||
|
||||
dispatch_resume(_timerSource);
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
if (!_running) {
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return;
|
||||
}
|
||||
_running = NO;
|
||||
if (_timerSource) {
|
||||
dispatch_source_cancel(_timerSource);
|
||||
_timerSource = nil;
|
||||
}
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
}
|
||||
|
||||
- (void)sampleImmediately {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_async(_telemetryQueue, ^{
|
||||
[weakSelf performSamplePass];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)performSamplePass {
|
||||
@autoreleasepool {
|
||||
NSArray<id<MMTelemetryProvider>> *providersToSample = [self registeredProviders];
|
||||
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *snapshot =
|
||||
[NSMutableDictionary dictionaryWithCapacity:providersToSample.count];
|
||||
|
||||
for (id<MMTelemetryProvider> provider in providersToSample) {
|
||||
@autoreleasepool {
|
||||
if (!provider.isAvailable) {
|
||||
continue;
|
||||
}
|
||||
NSError *sampleError = nil;
|
||||
NSDictionary<NSString *, id> *metrics = [provider sampleTelemetryWithError:&sampleError];
|
||||
if (metrics) {
|
||||
snapshot[provider.providerIdentifier] = metrics;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, NSDictionary<NSString *, id> *> *immutableSnapshot = [snapshot copy];
|
||||
MMTelemetrySnapshotHandler handler = self.snapshotHandler;
|
||||
if (handler) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
handler(immutableSnapshot);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef MMTelemetryDomain_h
|
||||
#define MMTelemetryDomain_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, MMTelemetryDomain) {
|
||||
MMTelemetryDomainSystem NS_SWIFT_NAME(system) = 0,
|
||||
MMTelemetryDomainCPU NS_SWIFT_NAME(cpu),
|
||||
MMTelemetryDomainThermal NS_SWIFT_NAME(thermal),
|
||||
MMTelemetryDomainFan NS_SWIFT_NAME(fan),
|
||||
MMTelemetryDomainMemory NS_SWIFT_NAME(memory),
|
||||
MMTelemetryDomainStorage NS_SWIFT_NAME(storage),
|
||||
MMTelemetryDomainNetwork NS_SWIFT_NAME(network),
|
||||
MMTelemetryDomainProcess NS_SWIFT_NAME(process),
|
||||
MMTelemetryDomainGPU NS_SWIFT_NAME(gpu),
|
||||
MMTelemetryDomainPower NS_SWIFT_NAME(power),
|
||||
MMTelemetryDomainAudio NS_SWIFT_NAME(audio),
|
||||
MMTelemetryDomainPeripherals NS_SWIFT_NAME(peripherals),
|
||||
MMTelemetryDomainDisplay NS_SWIFT_NAME(display)
|
||||
};
|
||||
|
||||
static inline NSString * _Nonnull MMTelemetryDomainToString(MMTelemetryDomain domain) {
|
||||
switch (domain) {
|
||||
case MMTelemetryDomainSystem: return @"System";
|
||||
case MMTelemetryDomainCPU: return @"CPU";
|
||||
case MMTelemetryDomainThermal: return @"Thermal";
|
||||
case MMTelemetryDomainFan: return @"Fan";
|
||||
case MMTelemetryDomainMemory: return @"Memory";
|
||||
case MMTelemetryDomainStorage: return @"Storage";
|
||||
case MMTelemetryDomainNetwork: return @"Network";
|
||||
case MMTelemetryDomainProcess: return @"Process";
|
||||
case MMTelemetryDomainGPU: return @"GPU";
|
||||
case MMTelemetryDomainPower: return @"Power";
|
||||
case MMTelemetryDomainAudio: return @"Audio";
|
||||
case MMTelemetryDomainPeripherals: return @"Peripherals";
|
||||
case MMTelemetryDomainDisplay: return @"Display";
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* MMTelemetryDomain_h */
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef MMTelemetryProvider_h
|
||||
#define MMTelemetryProvider_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MMTelemetryDomain.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* MMTelemetryProvider
|
||||
* Protocol defining the standard contract for all low-level telemetry sampling engines.
|
||||
*/
|
||||
@protocol MMTelemetryProvider <NSObject>
|
||||
|
||||
@property (nonatomic, readonly) MMTelemetryDomain domain;
|
||||
@property (nonatomic, readonly, copy) NSString *providerIdentifier;
|
||||
@property (nonatomic, readonly, getter=isAvailable) BOOL available;
|
||||
|
||||
/**
|
||||
* Samples instantaneous or delta telemetry metrics.
|
||||
* Called exclusively on the background utility telemetry queue: com.i3omb.macmonitor.telemetry.
|
||||
* Returns an immutable dictionary containing scalar values, strings, or arrays.
|
||||
*/
|
||||
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error;
|
||||
|
||||
@optional
|
||||
- (void)startMonitoring;
|
||||
- (void)stopMonitoring;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#endif /* MMTelemetryProvider_h */
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef MMAppleSMCClient_h
|
||||
#define MMAppleSMCClient_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MMSMCDefines.h"
|
||||
#import "MMSMCParser.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* MMAppleSMCClient
|
||||
* Thread-safe singleton providing direct communication with the AppleSMC driver
|
||||
* on Intel Mac platforms via IOKit user client calls.
|
||||
*/
|
||||
@interface MMAppleSMCClient : NSObject
|
||||
|
||||
@property (class, readonly, strong) MMAppleSMCClient *sharedClient;
|
||||
|
||||
/**
|
||||
* Whether the AppleSMC IOKit driver connection is open and active.
|
||||
*/
|
||||
@property (nonatomic, readonly, getter=isAvailable) BOOL available;
|
||||
|
||||
/**
|
||||
* Total number of SMC keys advertised by hardware (#KEY), or 0 if unavailable.
|
||||
*/
|
||||
@property (nonatomic, readonly) NSInteger totalKeyCount;
|
||||
|
||||
/**
|
||||
* Explicitly opens the IOKit AppleSMC connection.
|
||||
*/
|
||||
- (BOOL)openWithError:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Closes the active IOKit AppleSMC connection.
|
||||
*/
|
||||
- (void)close;
|
||||
|
||||
/**
|
||||
* Reads raw bytes for a given 4-character SMC key (e.g. "TC0P", "F0Ac").
|
||||
*/
|
||||
- (nullable NSData *)readBytesForKey:(NSString *)key error:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Reads and automatically decodes a numeric value according to its SMC type descriptor.
|
||||
*/
|
||||
- (nullable NSNumber *)readNumericValueForKey:(NSString *)key error:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Returns the FourCC data type string for an SMC key (e.g. "sp78", "fpe2", "ui16").
|
||||
*/
|
||||
- (nullable NSString *)readDataTypeForKey:(NSString *)key error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#endif /* MMAppleSMCClient_h */
|
||||
@@ -0,0 +1,245 @@
|
||||
#import "MMAppleSMCClient.h"
|
||||
#import <IOKit/IOKitLib.h>
|
||||
#import <os/lock.h>
|
||||
|
||||
@interface MMAppleSMCClient () {
|
||||
os_unfair_lock _lock;
|
||||
io_connect_t _connection;
|
||||
NSMutableDictionary<NSString *, NSValue *> *_keyInfoCache;
|
||||
BOOL _opened;
|
||||
NSInteger _totalKeyCount;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MMAppleSMCClient
|
||||
|
||||
+ (instancetype)sharedClient {
|
||||
static MMAppleSMCClient *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_lock = OS_UNFAIR_LOCK_INIT;
|
||||
_keyInfoCache = [NSMutableDictionary dictionary];
|
||||
[self openWithError:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self close];
|
||||
}
|
||||
|
||||
- (BOOL)isAvailable {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
BOOL avail = (_connection != IO_OBJECT_NULL && _opened);
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return avail;
|
||||
}
|
||||
|
||||
- (NSInteger)totalKeyCount {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
NSInteger count = _totalKeyCount;
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return count;
|
||||
}
|
||||
|
||||
- (BOOL)openWithError:(NSError **)error {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
if (_opened && _connection != IO_OBJECT_NULL) {
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return YES;
|
||||
}
|
||||
|
||||
CFMutableDictionaryRef matching = IOServiceMatching("AppleSMCClient");
|
||||
if (!matching) {
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.SMC"
|
||||
code:-1
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to create AppleSMCClient matching dictionary"}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, matching);
|
||||
if (service == IO_OBJECT_NULL) {
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.SMC"
|
||||
code:-2
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"AppleSMCClient service not found (running on non-Mac or unsupported VM)"}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
kern_return_t kr = IOServiceOpen(service, mach_task_self(), 0, &_connection);
|
||||
IOObjectRelease(service);
|
||||
|
||||
if (kr != KERN_SUCCESS || _connection == IO_OBJECT_NULL) {
|
||||
_connection = IO_OBJECT_NULL;
|
||||
_opened = NO;
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.SMC"
|
||||
code:kr
|
||||
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"IOServiceOpen failed with code: 0x%x", kr]}];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
_opened = YES;
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
|
||||
// Probe total keys count
|
||||
NSNumber *keysCount = [self readNumericValueForKey:@"#KEY" error:nil];
|
||||
if (keysCount) {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
_totalKeyCount = [keysCount integerValue];
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
os_unfair_lock_lock(&_lock);
|
||||
if (_connection != IO_OBJECT_NULL) {
|
||||
IOServiceClose(_connection);
|
||||
_connection = IO_OBJECT_NULL;
|
||||
}
|
||||
_opened = NO;
|
||||
[_keyInfoCache removeAllObjects];
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
}
|
||||
|
||||
- (BOOL)callSMCWithInput:(const SMCKeyData_t *)input output:(SMCKeyData_t *)output {
|
||||
if (_connection == IO_OBJECT_NULL) return NO;
|
||||
|
||||
size_t inputSize = sizeof(SMCKeyData_t);
|
||||
size_t outputSize = sizeof(SMCKeyData_t);
|
||||
|
||||
kern_return_t kr = IOConnectCallStructMethod(
|
||||
_connection,
|
||||
kSMCHandleYPCEvent,
|
||||
input,
|
||||
inputSize,
|
||||
output,
|
||||
&outputSize
|
||||
);
|
||||
|
||||
return (kr == KERN_SUCCESS && output->result == 0);
|
||||
}
|
||||
|
||||
- (BOOL)fetchKeyInfo:(NSString *)key info:(SMCKeyInfoData *)outInfo {
|
||||
if (key.length != 4) return NO;
|
||||
|
||||
os_unfair_lock_lock(&_lock);
|
||||
NSValue *cached = _keyInfoCache[key];
|
||||
if (cached) {
|
||||
[cached getValue:outInfo size:sizeof(SMCKeyInfoData)];
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return YES;
|
||||
}
|
||||
|
||||
SMCKeyData_t input = {0};
|
||||
SMCKeyData_t output = {0};
|
||||
|
||||
input.key = MMSMCToFourCharCode([key UTF8String]);
|
||||
input.data8 = kSMCGetKeyInfo;
|
||||
|
||||
BOOL success = [self callSMCWithInput:&input output:&output];
|
||||
if (success) {
|
||||
*outInfo = output.keyInfo;
|
||||
_keyInfoCache[key] = [NSValue value:outInfo withObjCType:@encode(SMCKeyInfoData)];
|
||||
}
|
||||
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
return success;
|
||||
}
|
||||
|
||||
- (nullable NSData *)readBytesForKey:(NSString *)key error:(NSError **)error {
|
||||
if (![self isAvailable]) {
|
||||
if (![self openWithError:error]) return nil;
|
||||
}
|
||||
|
||||
SMCKeyInfoData info = {0};
|
||||
if (![self fetchKeyInfo:key info:&info]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.SMC"
|
||||
code:-3
|
||||
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to retrieve key info for '%@'", key]}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (info.dataSize > 32) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.SMC"
|
||||
code:-4
|
||||
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Data size %lu exceeds 32 bytes for key '%@'", (unsigned long)info.dataSize, key]}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
os_unfair_lock_lock(&_lock);
|
||||
SMCKeyData_t input = {0};
|
||||
SMCKeyData_t output = {0};
|
||||
|
||||
input.key = MMSMCToFourCharCode([key UTF8String]);
|
||||
input.keyInfo.dataSize = info.dataSize;
|
||||
input.data8 = kSMCReadKey;
|
||||
|
||||
BOOL success = [self callSMCWithInput:&input output:&output];
|
||||
os_unfair_lock_unlock(&_lock);
|
||||
|
||||
if (!success) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.SMC"
|
||||
code:-5
|
||||
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"SMC read command failed for key '%@'", key]}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSData dataWithBytes:output.bytes length:info.dataSize];
|
||||
}
|
||||
|
||||
- (nullable NSString *)readDataTypeForKey:(NSString *)key error:(NSError **)error {
|
||||
SMCKeyInfoData info = {0};
|
||||
if (![self fetchKeyInfo:key info:&info]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.SMC"
|
||||
code:-3
|
||||
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to retrieve key info for '%@'", key]}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
char typeStr[5] = {0};
|
||||
MMSMCFromFourCharCode(info.dataType, typeStr);
|
||||
return [NSString stringWithUTF8String:typeStr];
|
||||
}
|
||||
|
||||
- (nullable NSNumber *)readNumericValueForKey:(NSString *)key error:(NSError **)error {
|
||||
SMCKeyInfoData info = {0};
|
||||
if (![self fetchKeyInfo:key info:&info]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *data = [self readBytesForKey:key error:error];
|
||||
if (!data) return nil;
|
||||
|
||||
char typeStr[5] = {0};
|
||||
MMSMCFromFourCharCode(info.dataType, typeStr);
|
||||
NSString *dataType = [NSString stringWithUTF8String:typeStr];
|
||||
|
||||
return [MMSMCParser decodeValueWithDataType:dataType bytes:data.bytes size:data.length];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef MMSMCDefines_h
|
||||
#define MMSMCDefines_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <IOKit/IOKitLib.h>
|
||||
|
||||
#define KERNEL_INDEX_SMC 2
|
||||
|
||||
// SMC Call Selectors
|
||||
#define kSMCUserClientOpen 0
|
||||
#define kSMCUserClientClose 1
|
||||
#define kSMCHandleYPCEvent 2
|
||||
#define kSMCReadKey 5
|
||||
#define kSMCWriteKey 6
|
||||
#define kSMCGetKeyFromIndex 8
|
||||
#define kSMCGetKeyInfo 9
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
typedef struct {
|
||||
unsigned char major;
|
||||
unsigned char minor;
|
||||
unsigned char build;
|
||||
unsigned char reserved[1];
|
||||
unsigned short release;
|
||||
} SMCVersion;
|
||||
|
||||
typedef struct {
|
||||
uint16_t version;
|
||||
uint16_t length;
|
||||
uint32_t cpuPLimit;
|
||||
uint32_t gpuPLimit;
|
||||
uint32_t memPLimit;
|
||||
} SMCPLimitData;
|
||||
|
||||
typedef struct {
|
||||
IOByteCount dataSize;
|
||||
UInt32 dataType;
|
||||
UInt8 dataAttributes;
|
||||
} SMCKeyInfoData;
|
||||
|
||||
typedef struct {
|
||||
UInt32 key;
|
||||
SMCVersion vers;
|
||||
SMCPLimitData pLimitData;
|
||||
SMCKeyInfoData keyInfo;
|
||||
UInt8 result;
|
||||
UInt8 status;
|
||||
UInt8 data8;
|
||||
UInt32 data32;
|
||||
UInt8 bytes[32];
|
||||
} SMCKeyData_t;
|
||||
|
||||
typedef struct {
|
||||
char key[5];
|
||||
UInt32 dataSize;
|
||||
char dataType[5];
|
||||
UInt8 bytes[32];
|
||||
} SMCVal_t;
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
static inline UInt32 MMSMCToFourCharCode(const char * _Nonnull str) {
|
||||
UInt32 code = 0;
|
||||
for (int i = 0; i < 4 && str[i] != '\0'; i++) {
|
||||
code = (code << 8) | (UInt8)str[i];
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
static inline void MMSMCFromFourCharCode(UInt32 code, char * _Nonnull outStr) {
|
||||
outStr[0] = (char)((code >> 24) & 0xFF);
|
||||
outStr[1] = (char)((code >> 16) & 0xFF);
|
||||
outStr[2] = (char)((code >> 8) & 0xFF);
|
||||
outStr[3] = (char)(code & 0xFF);
|
||||
outStr[4] = '\0';
|
||||
}
|
||||
|
||||
#endif /* MMSMCDefines_h */
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef MMSMCParser_h
|
||||
#define MMSMCParser_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MMSMCParser : NSObject
|
||||
|
||||
/**
|
||||
* Decodes a signed 8.8 fixed-point format (sp78), used widely for Intel Mac temperature sensors.
|
||||
*/
|
||||
+ (float)decodeSP78:(const uint8_t *)bytes;
|
||||
|
||||
/**
|
||||
* Decodes an unsigned 14.2 fixed-point format (fpe2), used for Fan RPM values.
|
||||
*/
|
||||
+ (float)decodeFPE2:(const uint8_t *)bytes;
|
||||
|
||||
/**
|
||||
* Decodes a standard IEEE 754 32-bit floating point value (flt).
|
||||
*/
|
||||
+ (float)decodeFLT:(const uint8_t *)bytes;
|
||||
|
||||
/**
|
||||
* Decodes an unsigned 8-bit integer (ui8).
|
||||
*/
|
||||
+ (uint8_t)decodeUI8:(const uint8_t *)bytes;
|
||||
|
||||
/**
|
||||
* Decodes a big-endian unsigned 16-bit integer (ui16).
|
||||
*/
|
||||
+ (uint16_t)decodeUI16:(const uint8_t *)bytes;
|
||||
|
||||
/**
|
||||
* Decodes a big-endian unsigned 32-bit integer (ui32).
|
||||
*/
|
||||
+ (uint32_t)decodeUI32:(const uint8_t *)bytes;
|
||||
|
||||
/**
|
||||
* Generic parser taking an SMC dataType FourCC string ("sp78", "fpe2", "flt", "ui8 ", "ui16", "ui32", "flag")
|
||||
* and returning a parsed NSNumber.
|
||||
*/
|
||||
+ (nullable NSNumber *)decodeValueWithDataType:(NSString *)dataType bytes:(const uint8_t *)bytes size:(NSUInteger)size;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#endif /* MMSMCParser_h */
|
||||
@@ -0,0 +1,67 @@
|
||||
#import "MMSMCParser.h"
|
||||
#import <string.h>
|
||||
|
||||
@implementation MMSMCParser
|
||||
|
||||
+ (float)decodeSP78:(const uint8_t *)bytes {
|
||||
if (!bytes) return 0.0f;
|
||||
int16_t raw = (int16_t)(((uint16_t)bytes[0] << 8) | (uint16_t)bytes[1]);
|
||||
return (float)raw / 256.0f;
|
||||
}
|
||||
|
||||
+ (float)decodeFPE2:(const uint8_t *)bytes {
|
||||
if (!bytes) return 0.0f;
|
||||
uint16_t raw = ((uint16_t)bytes[0] << 8) | (uint16_t)bytes[1];
|
||||
return (float)raw / 4.0f;
|
||||
}
|
||||
|
||||
+ (float)decodeFLT:(const uint8_t *)bytes {
|
||||
if (!bytes) return 0.0f;
|
||||
float val = 0.0f;
|
||||
memcpy(&val, bytes, sizeof(float));
|
||||
return val;
|
||||
}
|
||||
|
||||
+ (uint8_t)decodeUI8:(const uint8_t *)bytes {
|
||||
if (!bytes) return 0;
|
||||
return bytes[0];
|
||||
}
|
||||
|
||||
+ (uint16_t)decodeUI16:(const uint8_t *)bytes {
|
||||
if (!bytes) return 0;
|
||||
return ((uint16_t)bytes[0] << 8) | (uint16_t)bytes[1];
|
||||
}
|
||||
|
||||
+ (uint32_t)decodeUI32:(const uint8_t *)bytes {
|
||||
if (!bytes) return 0;
|
||||
return ((uint32_t)bytes[0] << 24) |
|
||||
((uint32_t)bytes[1] << 16) |
|
||||
((uint32_t)bytes[2] << 8) |
|
||||
(uint32_t)bytes[3];
|
||||
}
|
||||
|
||||
+ (nullable NSNumber *)decodeValueWithDataType:(NSString *)dataType bytes:(const uint8_t *)bytes size:(NSUInteger)size {
|
||||
if (!bytes || size == 0) return nil;
|
||||
|
||||
NSString *trimmedType = [dataType stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
if ([trimmedType isEqualToString:@"sp78"]) {
|
||||
if (size >= 2) return @([self decodeSP78:bytes]);
|
||||
} else if ([trimmedType isEqualToString:@"fpe2"]) {
|
||||
if (size >= 2) return @([self decodeFPE2:bytes]);
|
||||
} else if ([trimmedType isEqualToString:@"flt"]) {
|
||||
if (size >= 4) return @([self decodeFLT:bytes]);
|
||||
} else if ([trimmedType isEqualToString:@"ui8"]) {
|
||||
if (size >= 1) return @([self decodeUI8:bytes]);
|
||||
} else if ([trimmedType isEqualToString:@"ui16"]) {
|
||||
if (size >= 2) return @([self decodeUI16:bytes]);
|
||||
} else if ([trimmedType isEqualToString:@"ui32"]) {
|
||||
if (size >= 4) return @([self decodeUI32:bytes]);
|
||||
} else if ([trimmedType isEqualToString:@"flag"]) {
|
||||
if (size >= 1) return @(bytes[0] != 0);
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,227 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct ContentView: View {
|
||||
@State private var store = SystemTelemetryStore.shared
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
NavigationSplitView {
|
||||
sidebarContent
|
||||
} detail: {
|
||||
detailContent
|
||||
}
|
||||
.frame(minWidth: 800, minHeight: 520)
|
||||
.onAppear {
|
||||
store.start()
|
||||
}
|
||||
}
|
||||
|
||||
private var sidebarContent: some View {
|
||||
List {
|
||||
Section("System Status") {
|
||||
Label("Dashboard", systemImage: "gauge.with.needle")
|
||||
Label("Architecture", systemImage: "cpu")
|
||||
}
|
||||
|
||||
Section("Telemetry Domains") {
|
||||
Label("CPU & Thermals", systemImage: "flame")
|
||||
Label("Fans & Cooling", systemImage: "fanblades")
|
||||
Label("Memory & Swap", systemImage: "memorychip")
|
||||
Label("Disks & Volumes", systemImage: "internaldrive")
|
||||
Label("Processes", systemImage: "list.bullet.rectangle")
|
||||
Label("Network", systemImage: "network")
|
||||
Label("Graphics (GPU)", systemImage: "display")
|
||||
}
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.navigationSplitViewColumnWidth(min: 200, ideal: 220, max: 280)
|
||||
}
|
||||
|
||||
private var detailContent: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
// Header Banner
|
||||
headerCard
|
||||
|
||||
// Telemetry Engine Control
|
||||
engineControlCard
|
||||
|
||||
// System Specifications
|
||||
specsCard
|
||||
|
||||
// Providers Status
|
||||
providersCard
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
.navigationTitle("MacMonitor Dashboard")
|
||||
}
|
||||
|
||||
private var headerCard: some View {
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
Image(systemName: "macpro.gen3.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 48, height: 48)
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(store.hostModel)
|
||||
.font(.title2.bold())
|
||||
Text("Intel x86_64 • \(store.osVersion)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(store.isRunning ? Color.green : Color.red)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(store.isRunning ? "ENGINE ACTIVE" : "ENGINE PAUSED")
|
||||
.font(.caption.bold())
|
||||
.foregroundStyle(store.isRunning ? .green : .secondary)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(.quaternary, in: Capsule())
|
||||
}
|
||||
.padding(16)
|
||||
.background(.background, in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
|
||||
}
|
||||
|
||||
private var engineControlCard: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Telemetry Coordinator Engine")
|
||||
.font(.headline)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Button(action: {
|
||||
if store.isRunning {
|
||||
store.stop()
|
||||
} else {
|
||||
store.start()
|
||||
}
|
||||
}) {
|
||||
Label(store.isRunning ? "Pause Engine" : "Start Engine",
|
||||
systemImage: store.isRunning ? "pause.fill" : "play.fill")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(store.isRunning ? .orange : .green)
|
||||
|
||||
Button(action: {
|
||||
store.triggerManualSample()
|
||||
}) {
|
||||
Label("Sample Now", systemImage: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Spacer()
|
||||
|
||||
Picker("Polling Interval:", selection: Binding(
|
||||
get: { store.sampleInterval },
|
||||
set: { store.setSampleInterval($0) }
|
||||
)) {
|
||||
Text("0.5s").tag(0.5)
|
||||
Text("1.0s").tag(1.0)
|
||||
Text("2.0s").tag(2.0)
|
||||
Text("5.0s").tag(5.0)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(width: 220)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Last Sample Cycle: \(store.lastUpdateTimestamp.formatted(date: .omitted, time: .standard))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Text("Queue: com.i3omb.macmonitor.telemetry (QOS_UTILITY)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(.background, in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
|
||||
}
|
||||
|
||||
private var specsCard: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Host Architecture & Kernel")
|
||||
.font(.headline)
|
||||
|
||||
Grid(alignment: .leading, horizontalSpacing: 24, verticalSpacing: 10) {
|
||||
GridRow {
|
||||
Text("Hardware Model:").foregroundStyle(.secondary)
|
||||
Text(store.hostModel).bold()
|
||||
Text("Physical Cores:").foregroundStyle(.secondary)
|
||||
Text("\(store.physicalCpuCount)").bold()
|
||||
}
|
||||
GridRow {
|
||||
Text("Darwin Kernel:").foregroundStyle(.secondary)
|
||||
Text(store.kernelVersion).bold()
|
||||
Text("Logical Cores:").foregroundStyle(.secondary)
|
||||
Text("\(store.logicalCpuCount)").bold()
|
||||
}
|
||||
GridRow {
|
||||
Text("Target Architecture:").foregroundStyle(.secondary)
|
||||
Text("Intel x86_64").bold()
|
||||
Text("Deployment Target:").foregroundStyle(.secondary)
|
||||
Text("macOS 14.0 (Sonoma)").bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(.background, in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
|
||||
}
|
||||
|
||||
private var providersCard: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Registered Telemetry Providers")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text("\(store.registeredProviderCount) Active")
|
||||
.font(.caption.bold())
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.blue.opacity(0.15))
|
||||
.foregroundStyle(.blue)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
if store.latestSnapshot.isEmpty {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Telemetry coordinator initialized. Awaiting provider metrics.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
} else {
|
||||
ForEach(Array(store.latestSnapshot.keys.sorted()), id: \.self) { key in
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
Text(key)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
Spacer()
|
||||
Text("\(store.latestSnapshot[key]?.count ?? 0) metrics")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(.background, in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import XCTest
|
||||
@testable import MacMonitor
|
||||
|
||||
final class MMSMCTests: XCTestCase {
|
||||
|
||||
func testSP78TemperatureDecoding() {
|
||||
// 0x36, 0x80 -> 54.5°C in sp78 (signed 8.8 fixed point: 0x36 = 54, 0x80 = 128 / 256 = 0.5)
|
||||
let bytes: [UInt8] = [0x36, 0x80]
|
||||
let temp = MMSMCParser.decodeSP78(bytes)
|
||||
XCTAssertEqual(Double(temp), 54.5, accuracy: 0.001)
|
||||
|
||||
// Negative temperature: -10.25°C -> -10.25 * 256 = -2624 = 0xF5C0
|
||||
let negBytes: [UInt8] = [0xF5, 0xC0]
|
||||
let negTemp = MMSMCParser.decodeSP78(negBytes)
|
||||
XCTAssertEqual(Double(negTemp), -10.25, accuracy: 0.001)
|
||||
}
|
||||
|
||||
func testFPE2FanRPMDecoding() {
|
||||
// 1800 RPM in fpe2 (unsigned 14.2 fixed point: 1800 * 4 = 7200 = 0x1C20)
|
||||
let bytes: [UInt8] = [0x1C, 0x20]
|
||||
let rpm = MMSMCParser.decodeFPE2(bytes)
|
||||
XCTAssertEqual(Double(rpm), 1800.0, accuracy: 0.001)
|
||||
|
||||
// 2450.5 RPM: 2450.5 * 4 = 9802 = 0x264A
|
||||
let bytesFrac: [UInt8] = [0x26, 0x4A]
|
||||
let rpmFrac = MMSMCParser.decodeFPE2(bytesFrac)
|
||||
XCTAssertEqual(Double(rpmFrac), 2450.5, accuracy: 0.001)
|
||||
}
|
||||
|
||||
func testFLTDecoding() {
|
||||
// 3.14159f in IEEE 754 float
|
||||
var original: Float = 3.14159
|
||||
let bytes = withUnsafeBytes(of: &original) { Array($0) }
|
||||
let decoded = MMSMCParser.decodeFLT(bytes)
|
||||
XCTAssertEqual(Double(decoded), 3.14159, accuracy: 0.0001)
|
||||
}
|
||||
|
||||
func testIntegerDecoding() {
|
||||
// UI8
|
||||
let byteUI8: [UInt8] = [0x7F]
|
||||
XCTAssertEqual(MMSMCParser.decodeUI8(byteUI8), 127)
|
||||
|
||||
// UI16 big endian (0x1234 = 4660)
|
||||
let bytesUI16: [UInt8] = [0x12, 0x34]
|
||||
XCTAssertEqual(MMSMCParser.decodeUI16(bytesUI16), 4660)
|
||||
|
||||
// UI32 big endian (0x12345678 = 305419896)
|
||||
let bytesUI32: [UInt8] = [0x12, 0x34, 0x56, 0x78]
|
||||
XCTAssertEqual(MMSMCParser.decodeUI32(bytesUI32), 305419896)
|
||||
}
|
||||
|
||||
func testGenericDecoderWithType() {
|
||||
let tempBytes: [UInt8] = [0x41, 0x00] // 65.0°C in sp78
|
||||
let val = MMSMCParser.decodeValue(withDataType: "sp78", bytes: tempBytes, size: 2)
|
||||
XCTAssertNotNil(val)
|
||||
XCTAssertEqual(val?.doubleValue ?? 0, 65.0, accuracy: 0.001)
|
||||
|
||||
let flagBytes: [UInt8] = [0x01]
|
||||
let flagVal = MMSMCParser.decodeValue(withDataType: "flag", bytes: flagBytes, size: 1)
|
||||
XCTAssertNotNil(flagVal)
|
||||
XCTAssertEqual(flagVal?.boolValue, true)
|
||||
}
|
||||
|
||||
func testSMCClientSingletonAndKeyCount() {
|
||||
let client = MMAppleSMCClient.shared
|
||||
XCTAssertNotNil(client)
|
||||
// On physical Intel Macs, client.isAvailable should be true and totalKeyCount > 0.
|
||||
// In headless environments or VMs without AppleSMC, it gracefully degrades.
|
||||
if client.isAvailable {
|
||||
XCTAssertGreaterThan(client.totalKeyCount, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import XCTest
|
||||
@testable import MacMonitor
|
||||
|
||||
final class MacMonitorCoreTests: XCTestCase {
|
||||
func testCoordinatorLifecycle() {
|
||||
let coordinator = MMTelemetryCoordinator.shared
|
||||
XCTAssertNotNil(coordinator)
|
||||
|
||||
coordinator.sampleInterval = 0.5
|
||||
XCTAssertEqual(coordinator.sampleInterval, 0.5)
|
||||
|
||||
coordinator.start()
|
||||
XCTAssertTrue(coordinator.isRunning)
|
||||
|
||||
coordinator.stop()
|
||||
XCTAssertFalse(coordinator.isRunning)
|
||||
}
|
||||
|
||||
func testTelemetryDomainConversion() {
|
||||
let domainStr = MMTelemetryDomainToString(.cpu)
|
||||
XCTAssertEqual(domainStr, "CPU")
|
||||
|
||||
let thermalStr = MMTelemetryDomainToString(.thermal)
|
||||
XCTAssertEqual(thermalStr, "Thermal")
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
name: MacMonitor
|
||||
options:
|
||||
bundleIdPrefix: com.i3omb
|
||||
deploymentTarget:
|
||||
macOS: "14.0"
|
||||
xcodeVersion: "16.0"
|
||||
createIntermediateGroups: true
|
||||
|
||||
settings:
|
||||
base:
|
||||
ARCHS: "x86_64"
|
||||
ONLY_ACTIVE_ARCH: NO
|
||||
VALID_ARCHS: "x86_64"
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
SWIFT_VERSION: "5.0"
|
||||
CLANG_ENABLE_OBJC_ARC: YES
|
||||
CLANG_ENABLE_MODULES: YES
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
CODE_SIGNING_ALLOWED: NO
|
||||
CODE_SIGN_IDENTITY: ""
|
||||
GCC_C_LANGUAGE_STANDARD: "gnu17"
|
||||
CLANG_CXX_LANGUAGE_STANDARD: "gnu++20"
|
||||
|
||||
targets:
|
||||
MacMonitor:
|
||||
type: application
|
||||
platform: macOS
|
||||
deploymentTarget: "14.0"
|
||||
sources:
|
||||
- path: Sources
|
||||
excludes:
|
||||
- "Bridging/MacMonitor-Bridging-Header.h"
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: "com.i3omb.MacMonitor"
|
||||
INFOPLIST_KEY_CFBundleDisplayName: "MacMonitor"
|
||||
INFOPLIST_KEY_NSPrincipalClass: "NSApplication"
|
||||
CODE_SIGN_ENTITLEMENTS: "MacMonitor.entitlements"
|
||||
SWIFT_OBJC_BRIDGING_HEADER: "Sources/Bridging/MacMonitor-Bridging-Header.h"
|
||||
HEADER_SEARCH_PATHS:
|
||||
- "$(SRCROOT)/Sources/Core"
|
||||
- "$(SRCROOT)/Sources/Bridging"
|
||||
- "$(SRCROOT)/Sources/Hardware/**"
|
||||
dependencies:
|
||||
- sdk: IOKit.framework
|
||||
- sdk: CoreFoundation.framework
|
||||
|
||||
MacMonitorTests:
|
||||
type: bundle.unit-test
|
||||
platform: macOS
|
||||
deploymentTarget: "14.0"
|
||||
sources:
|
||||
- path: Tests
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: "com.i3omb.MacMonitorTests"
|
||||
SWIFT_OBJC_BRIDGING_HEADER: "Sources/Bridging/MacMonitor-Bridging-Header.h"
|
||||
HEADER_SEARCH_PATHS:
|
||||
- "$(SRCROOT)/Sources/Core"
|
||||
- "$(SRCROOT)/Sources/Bridging"
|
||||
- "$(SRCROOT)/Sources/Hardware/**"
|
||||
dependencies:
|
||||
- target: MacMonitor
|
||||
Reference in New Issue
Block a user