├── .github └── workflows │ └── swift.yml ├── .gitignore ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources └── bclm │ ├── SMC.swift │ ├── main.swift │ └── persist.swift └── Tests ├── LinuxMain.swift └── bclmTests ├── XCTestManifests.swift └── bclmTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: make build 21 | 22 | - uses: actions/upload-artifact@v3 23 | with: 24 | name: bclm 25 | path: .build/apple/Products/Release/bclm 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | Package.resolved 7 | 8 | # Release files 9 | config.json 10 | bclm.zip 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Zack Elia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prefix ?= /usr/local 2 | bindir = $(prefix)/bin 3 | 4 | build: 5 | swift build -c release --disable-sandbox --arch arm64 --arch x86_64 6 | strip .build/apple/Products/Release/bclm 7 | 8 | install: build 9 | mkdir -p "$(bindir)" 10 | install ".build/apple/Products/Release/bclm" "$(bindir)" 11 | 12 | uninstall: 13 | rm -rf "$(bindir)/bclm" 14 | 15 | test: 16 | swift build -c debug --build-tests 17 | sudo swift test --skip-build 18 | 19 | clean: 20 | rm -rf .build 21 | 22 | .PHONY: build install uninstall clean 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "bclm", 8 | dependencies: [ 9 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.1.0") 10 | ], 11 | targets: [ 12 | .target( 13 | name: "bclm", 14 | dependencies: [ 15 | .product(name: "ArgumentParser", package: "swift-argument-parser") 16 | ]), 17 | .testTarget( 18 | name: "bclmTests", 19 | dependencies: ["bclm"]), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BCLM 2 | 3 | BCLM is a wrapper to read and write battery charge level max (BCLM)/CHWA values to the System Management Controller (SMC) on Mac computers. It supports both Intel and Apple silicon. This project was inspired by several battery management solutions, including Apple's own battery health management. 4 | 5 | The purpose of limiting the battery's max charge is to prolong battery health and to prevent damage to the battery. Various sources show that the optimal charge range for operation of lithium-ion batteries is between 40% and 80%, commonly referred to as the 40-80 rule [[1]](https://www.apple.com/batteries/why-lithium-ion/)[[2]](https://www.eeworldonline.com/why-you-should-stop-fully-charging-your-smartphone-now/)[[3]](https://www.csmonitor.com/Technology/Tech/2014/0103/40-80-rule-New-tip-for-extending-battery-life). This project is especially helpful to people who leave their Macs on the charger all day, every day. 6 | 7 | ## Installation 8 | 9 | The easiest method to install BCLM is through `brew`. 10 | 11 | BCLM is written in Swift and is also trivial to compile. Currently, it can only be compiled on macOS Catalina (10.15) or higher but it can run on OS X Mavericks (10.9) or higher. 12 | 13 | A release zip is also provided with a signed and notarized binary for those who do not have development tools or are on an older macOS version. 14 | 15 | ### Brew 16 | 17 | ``` 18 | $ brew tap zackelia/formulae 19 | $ brew install bclm 20 | ``` 21 | 22 | ### From Source 23 | 24 | ``` 25 | $ make build 26 | $ make test 27 | $ sudo make install 28 | ``` 29 | 30 | ### From Releases 31 | 32 | ``` 33 | $ unzip bclm.zip 34 | $ sudo mkdir -p /usr/local/bin 35 | $ sudo cp bclm /usr/local/bin 36 | ``` 37 | 38 | Note: For older versions of macOS, it may be necessary to install the [Swift 5 Runtime Support for Command Line Tools](https://support.apple.com/kb/dl1998?locale=en_US) if you get the following error: `dyld: Symbol not found` 39 | 40 | ## Usage 41 | 42 | ``` 43 | $ bclm 44 | OVERVIEW: Battery Charge Level Max (BCLM) Utility. 45 | 46 | USAGE: bclm 47 | 48 | OPTIONS: 49 | --version Show the version. 50 | -h, --help Show help information. 51 | 52 | SUBCOMMANDS: 53 | read Reads the BCLM value. 54 | write Writes a BCLM value. 55 | persist Persists bclm. 56 | unpersist Unpersists bclm. 57 | 58 | See 'bclm help ' for detailed help. 59 | ``` 60 | 61 | For Intel machines, when writing values, macOS charges slightly beyond the set value (~3%). In order to display 80% when fully charged, it is recommended to set the BCLM value to 77%. When charging while system is shut down or sleeping, the charging can go beyond set value more than average 3%. 62 | 63 | ``` 64 | $ sudo bclm write 77 65 | $ bclm read 66 | 77 67 | ``` 68 | 69 | For Apple silicon machines, only the values 80 and 100 are supported and firmware >= 13.0 is required. 70 | 71 | Note that in order to write values, the program must be run as root. This is not required for reading values. 72 | 73 | ## Persistence 74 | 75 | The SMC can be reset by a startup shortcut or various other technical reasons. To ensure that the BCLM is always at its intended value, it should be persisted. 76 | 77 | This will create a new plist in `/Library/LaunchDaemons` and load it via `launchctl`. It will persist with the current BCLM value and will update on subsequent BCLM writes. 78 | 79 | ``` 80 | $ sudo bclm persist 81 | ``` 82 | 83 | Likewise, it can be unpersisted which will unload the service and remove the plist. 84 | 85 | ``` 86 | $ sudo bclm unpersist 87 | ``` 88 | -------------------------------------------------------------------------------- /Sources/bclm/SMC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMC.swift 3 | // SMCKit 4 | // 5 | // The MIT License 6 | // 7 | // Copyright (C) 2014-2017 beltex 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import IOKit 28 | import Foundation 29 | 30 | //------------------------------------------------------------------------------ 31 | // MARK: Type Aliases 32 | //------------------------------------------------------------------------------ 33 | 34 | // http://stackoverflow.com/a/22383661 35 | 36 | /// Floating point, unsigned, 14 bits exponent, 2 bits fraction 37 | public typealias FPE2 = (UInt8, UInt8) 38 | 39 | /// Floating point, signed, 7 bits exponent, 8 bits fraction 40 | public typealias SP78 = (UInt8, UInt8) 41 | 42 | public typealias SMCBytes = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, 43 | UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, 44 | UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, 45 | UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, 46 | UInt8, UInt8, UInt8, UInt8) 47 | 48 | //------------------------------------------------------------------------------ 49 | // MARK: Standard Library Extensions 50 | //------------------------------------------------------------------------------ 51 | 52 | extension UInt32 { 53 | 54 | init(fromBytes bytes: (UInt8, UInt8, UInt8, UInt8)) { 55 | // TODO: Broken up due to "Expression was too complex" error as of 56 | // Swift 4. 57 | 58 | let byte0 = UInt32(bytes.0) << 24 59 | let byte1 = UInt32(bytes.1) << 16 60 | let byte2 = UInt32(bytes.2) << 8 61 | let byte3 = UInt32(bytes.3) 62 | 63 | self = byte0 | byte1 | byte2 | byte3 64 | } 65 | } 66 | 67 | extension Bool { 68 | 69 | init(fromByte byte: UInt8) { 70 | self = byte == 1 ? true : false 71 | } 72 | } 73 | 74 | public extension Int { 75 | 76 | init(fromFPE2 bytes: FPE2) { 77 | self = (Int(bytes.0) << 6) + (Int(bytes.1) >> 2) 78 | } 79 | 80 | func toFPE2() -> FPE2 { 81 | return (UInt8(self >> 6), UInt8((self << 2) ^ ((self >> 6) << 8))) 82 | } 83 | } 84 | 85 | extension Double { 86 | 87 | init(fromSP78 bytes: SP78) { 88 | // FIXME: Handle second byte 89 | let sign = bytes.0 & 0x80 == 0 ? 1.0 : -1.0 90 | self = sign * Double(bytes.0 & 0x7F) // AND to mask sign bit 91 | } 92 | } 93 | 94 | // Thanks to Airspeed Velocity for the great idea! 95 | // http://airspeedvelocity.net/2015/05/22/my-talk-at-swift-summit/ 96 | public extension FourCharCode { 97 | 98 | init(fromString str: String) { 99 | precondition(str.count == 4) 100 | 101 | self = str.utf8.reduce(0) { sum, character in 102 | return sum << 8 | UInt32(character) 103 | } 104 | } 105 | 106 | init(fromStaticString str: StaticString) { 107 | precondition(str.utf8CodeUnitCount == 4) 108 | 109 | self = str.withUTF8Buffer { buffer in 110 | // TODO: Broken up due to "Expression was too complex" error as of 111 | // Swift 4. 112 | 113 | let byte0 = UInt32(buffer[0]) << 24 114 | let byte1 = UInt32(buffer[1]) << 16 115 | let byte2 = UInt32(buffer[2]) << 8 116 | let byte3 = UInt32(buffer[3]) 117 | 118 | return byte0 | byte1 | byte2 | byte3 119 | } 120 | } 121 | 122 | func toString() -> String { 123 | return String(describing: UnicodeScalar(self >> 24 & 0xff)!) + 124 | String(describing: UnicodeScalar(self >> 16 & 0xff)!) + 125 | String(describing: UnicodeScalar(self >> 8 & 0xff)!) + 126 | String(describing: UnicodeScalar(self & 0xff)!) 127 | } 128 | } 129 | 130 | //------------------------------------------------------------------------------ 131 | // MARK: Defined by AppleSMC.kext 132 | //------------------------------------------------------------------------------ 133 | 134 | /// Defined by AppleSMC.kext 135 | /// 136 | /// This is the predefined struct that must be passed to communicate with the 137 | /// AppleSMC driver. While the driver is closed source, the definition of this 138 | /// struct happened to appear in the Apple PowerManagement project at around 139 | /// version 211, and soon after disappeared. It can be seen in the PrivateLib.c 140 | /// file under pmconfigd. Given that it is C code, this is the closest 141 | /// translation to Swift from a type perspective. 142 | /// 143 | /// ### Issues 144 | /// 145 | /// * Padding for struct alignment when passed over to C side 146 | /// * Size of struct must be 80 bytes 147 | /// * C array's are bridged as tuples 148 | /// 149 | /// http://www.opensource.apple.com/source/PowerManagement/PowerManagement-211/ 150 | public struct SMCParamStruct { 151 | 152 | /// I/O Kit function selector 153 | public enum Selector: UInt8 { 154 | case kSMCHandleYPCEvent = 2 155 | case kSMCReadKey = 5 156 | case kSMCWriteKey = 6 157 | case kSMCGetKeyFromIndex = 8 158 | case kSMCGetKeyInfo = 9 159 | } 160 | 161 | /// Return codes for SMCParamStruct.result property 162 | public enum Result: UInt8 { 163 | case kSMCSuccess = 0 164 | case kSMCError = 1 165 | case kSMCKeyNotFound = 132 166 | } 167 | 168 | public struct SMCVersion { 169 | var major: CUnsignedChar = 0 170 | var minor: CUnsignedChar = 0 171 | var build: CUnsignedChar = 0 172 | var reserved: CUnsignedChar = 0 173 | var release: CUnsignedShort = 0 174 | } 175 | 176 | public struct SMCPLimitData { 177 | var version: UInt16 = 0 178 | var length: UInt16 = 0 179 | var cpuPLimit: UInt32 = 0 180 | var gpuPLimit: UInt32 = 0 181 | var memPLimit: UInt32 = 0 182 | } 183 | 184 | public struct SMCKeyInfoData { 185 | /// How many bytes written to SMCParamStruct.bytes 186 | var dataSize: IOByteCount32 = 0 187 | 188 | /// Type of data written to SMCParamStruct.bytes. This lets us know how 189 | /// to interpret it (translate it to human readable) 190 | var dataType: UInt32 = 0 191 | 192 | var dataAttributes: UInt8 = 0 193 | } 194 | 195 | /// FourCharCode telling the SMC what we want 196 | var key: UInt32 = 0 197 | 198 | var vers = SMCVersion() 199 | 200 | var pLimitData = SMCPLimitData() 201 | 202 | var keyInfo = SMCKeyInfoData() 203 | 204 | /// Padding for struct alignment when passed over to C side 205 | var padding: UInt16 = 0 206 | 207 | /// Result of an operation 208 | var result: UInt8 = 0 209 | 210 | var status: UInt8 = 0 211 | 212 | /// Method selector 213 | var data8: UInt8 = 0 214 | 215 | var data32: UInt32 = 0 216 | 217 | /// Data returned from the SMC 218 | var bytes: SMCBytes = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 219 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 220 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 221 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 222 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 223 | UInt8(0), UInt8(0)) 224 | } 225 | 226 | //------------------------------------------------------------------------------ 227 | // MARK: SMC Client 228 | //------------------------------------------------------------------------------ 229 | 230 | /// SMC data type information 231 | public struct DataTypes { 232 | 233 | /// Fan information struct 234 | public static let FDS = 235 | DataType(type: FourCharCode(fromStaticString: "{fds"), size: 16) 236 | public static let Flag = 237 | DataType(type: FourCharCode(fromStaticString: "flag"), size: 1) 238 | /// See type aliases 239 | public static let FPE2 = 240 | DataType(type: FourCharCode(fromStaticString: "fpe2"), size: 2) 241 | /// See type aliases 242 | public static let SP78 = 243 | DataType(type: FourCharCode(fromStaticString: "sp78"), size: 2) 244 | public static let UInt8 = 245 | DataType(type: FourCharCode(fromStaticString: "ui8 "), size: 1) 246 | public static let UInt32 = 247 | DataType(type: FourCharCode(fromStaticString: "ui32"), size: 4) 248 | } 249 | 250 | public struct SMCKey { 251 | let code: FourCharCode 252 | let info: DataType 253 | } 254 | 255 | public struct DataType: Equatable { 256 | let type: FourCharCode 257 | let size: UInt32 258 | } 259 | 260 | public func ==(lhs: DataType, rhs: DataType) -> Bool { 261 | return lhs.type == rhs.type && lhs.size == rhs.size 262 | } 263 | 264 | /// Apple System Management Controller (SMC) user-space client for Intel-based 265 | /// Macs. Works by talking to the AppleSMC.kext (kernel extension), the closed 266 | /// source driver for the SMC. 267 | public struct SMCKit { 268 | 269 | public enum SMCError: Error { 270 | 271 | /// AppleSMC driver not found 272 | case driverNotFound 273 | 274 | /// Failed to open a connection to the AppleSMC driver 275 | case failedToOpen 276 | 277 | /// This SMC key is not valid on this machine 278 | case keyNotFound(code: String) 279 | 280 | /// Requires root privileges 281 | case notPrivileged 282 | 283 | /// Fan speed must be > 0 && <= fanMaxSpeed 284 | case unsafeFanSpeed 285 | 286 | /// https://developer.apple.com/library/mac/qa/qa1075/_index.html 287 | /// 288 | /// - parameter kIOReturn: I/O Kit error code 289 | /// - parameter SMCResult: SMC specific return code 290 | case unknown(kIOReturn: kern_return_t, SMCResult: UInt8) 291 | } 292 | 293 | /// Connection to the SMC driver 294 | fileprivate static var connection: io_connect_t = 0 295 | 296 | /// Open connection to the SMC driver. This must be done first before any 297 | /// other calls 298 | public static func open() throws { 299 | let service = IOServiceGetMatchingService(kIOMasterPortDefault, 300 | IOServiceMatching("AppleSMC")) 301 | 302 | if service == 0 { throw SMCError.driverNotFound } 303 | 304 | let result = IOServiceOpen(service, mach_task_self_, 0, 305 | &SMCKit.connection) 306 | IOObjectRelease(service) 307 | 308 | if result != kIOReturnSuccess { throw SMCError.failedToOpen } 309 | } 310 | 311 | /// Close connection to the SMC driver 312 | @discardableResult 313 | public static func close() -> Bool { 314 | let result = IOServiceClose(SMCKit.connection) 315 | return result == kIOReturnSuccess ? true : false 316 | } 317 | 318 | /// Get information about a key 319 | public static func keyInformation(_ key: FourCharCode) throws -> DataType { 320 | var inputStruct = SMCParamStruct() 321 | 322 | inputStruct.key = key 323 | inputStruct.data8 = SMCParamStruct.Selector.kSMCGetKeyInfo.rawValue 324 | 325 | let outputStruct = try callDriver(&inputStruct) 326 | 327 | return DataType(type: outputStruct.keyInfo.dataType, 328 | size: outputStruct.keyInfo.dataSize) 329 | } 330 | 331 | /// Get information about the key at index 332 | public static func keyInformationAtIndex(_ index: Int) throws -> 333 | FourCharCode { 334 | var inputStruct = SMCParamStruct() 335 | 336 | inputStruct.data8 = SMCParamStruct.Selector.kSMCGetKeyFromIndex.rawValue 337 | inputStruct.data32 = UInt32(index) 338 | 339 | let outputStruct = try callDriver(&inputStruct) 340 | 341 | return outputStruct.key 342 | } 343 | 344 | public static func getKey(_ code: String, type: DataType) -> SMCKey { 345 | let key = SMCKey(code: FourCharCode(fromString: code), info: type) 346 | return key 347 | } 348 | 349 | /// Read data of a key 350 | public static func readData(_ key: SMCKey) throws -> SMCBytes { 351 | var inputStruct = SMCParamStruct() 352 | 353 | inputStruct.key = key.code 354 | inputStruct.keyInfo.dataSize = UInt32(key.info.size) 355 | inputStruct.data8 = SMCParamStruct.Selector.kSMCReadKey.rawValue 356 | 357 | let outputStruct = try callDriver(&inputStruct) 358 | 359 | return outputStruct.bytes 360 | } 361 | 362 | /// Write data for a key 363 | public static func writeData(_ key: SMCKey, data: SMCBytes) throws { 364 | var inputStruct = SMCParamStruct() 365 | 366 | inputStruct.key = key.code 367 | inputStruct.bytes = data 368 | inputStruct.keyInfo.dataSize = UInt32(key.info.size) 369 | inputStruct.data8 = SMCParamStruct.Selector.kSMCWriteKey.rawValue 370 | 371 | _ = try callDriver(&inputStruct) 372 | } 373 | 374 | /// Make an actual call to the SMC driver 375 | public static func callDriver(_ inputStruct: inout SMCParamStruct, 376 | selector: SMCParamStruct.Selector = .kSMCHandleYPCEvent) 377 | throws -> SMCParamStruct { 378 | assert(MemoryLayout.stride == 80, "SMCParamStruct size is != 80") 379 | 380 | var outputStruct = SMCParamStruct() 381 | let inputStructSize = MemoryLayout.stride 382 | var outputStructSize = MemoryLayout.stride 383 | 384 | let result = IOConnectCallStructMethod(SMCKit.connection, 385 | UInt32(selector.rawValue), 386 | &inputStruct, 387 | inputStructSize, 388 | &outputStruct, 389 | &outputStructSize) 390 | 391 | switch (result, outputStruct.result) { 392 | case (kIOReturnSuccess, SMCParamStruct.Result.kSMCSuccess.rawValue): 393 | return outputStruct 394 | case (kIOReturnSuccess, SMCParamStruct.Result.kSMCKeyNotFound.rawValue): 395 | throw SMCError.keyNotFound(code: inputStruct.key.toString()) 396 | case (kIOReturnNotPrivileged, _): 397 | throw SMCError.notPrivileged 398 | default: 399 | throw SMCError.unknown(kIOReturn: result, 400 | SMCResult: outputStruct.result) 401 | } 402 | } 403 | } 404 | 405 | //------------------------------------------------------------------------------ 406 | // MARK: General 407 | //------------------------------------------------------------------------------ 408 | 409 | extension SMCKit { 410 | 411 | /// Get all valid SMC keys for this machine 412 | public static func allKeys() throws -> [SMCKey] { 413 | let count = try keyCount() 414 | var keys = [SMCKey]() 415 | 416 | for i in 0 ..< count { 417 | let key = try keyInformationAtIndex(i) 418 | let info = try keyInformation(key) 419 | keys.append(SMCKey(code: key, info: info)) 420 | } 421 | 422 | return keys 423 | } 424 | 425 | /// Get the number of valid SMC keys for this machine 426 | public static func keyCount() throws -> Int { 427 | let key = SMCKey(code: FourCharCode(fromStaticString: "#KEY"), 428 | info: DataTypes.UInt32) 429 | 430 | let data = try readData(key) 431 | return Int(UInt32(fromBytes: (data.0, data.1, data.2, data.3))) 432 | } 433 | 434 | /// Is this key valid on this machine? 435 | public static func isKeyFound(_ code: FourCharCode) throws -> Bool { 436 | do { 437 | _ = try keyInformation(code) 438 | } catch SMCError.keyNotFound { return false } 439 | 440 | return true 441 | } 442 | } 443 | 444 | //------------------------------------------------------------------------------ 445 | // MARK: Temperature 446 | //------------------------------------------------------------------------------ 447 | 448 | /// The list is NOT exhaustive. In addition, the names of the sensors may not be 449 | /// mapped to the correct hardware component. 450 | /// 451 | /// ### Sources 452 | /// 453 | /// * powermetrics(1) 454 | /// * https://www.apple.com/downloads/dashboard/status/istatpro.html 455 | /// * https://github.com/hholtmann/smcFanControl 456 | /// * https://github.com/jedda/OSX-Monitoring-Tools 457 | /// * http://www.opensource.apple.com/source/net_snmp/ 458 | /// * http://www.parhelia.ch/blog/statics/k3_keys.html 459 | public struct TemperatureSensors { 460 | 461 | public static let AMBIENT_AIR_0 = TemperatureSensor(name: "AMBIENT_AIR_0", 462 | code: FourCharCode(fromStaticString: "TA0P")) 463 | public static let AMBIENT_AIR_1 = TemperatureSensor(name: "AMBIENT_AIR_1", 464 | code: FourCharCode(fromStaticString: "TA1P")) 465 | // Via powermetrics(1) 466 | public static let CPU_0_DIE = TemperatureSensor(name: "CPU_0_DIE", 467 | code: FourCharCode(fromStaticString: "TC0F")) 468 | public static let CPU_0_DIODE = TemperatureSensor(name: "CPU_0_DIODE", 469 | code: FourCharCode(fromStaticString: "TC0D")) 470 | public static let CPU_0_HEATSINK = TemperatureSensor(name: "CPU_0_HEATSINK", 471 | code: FourCharCode(fromStaticString: "TC0H")) 472 | public static let CPU_0_PROXIMITY = 473 | TemperatureSensor(name: "CPU_0_PROXIMITY", 474 | code: FourCharCode(fromStaticString: "TC0P")) 475 | public static let ENCLOSURE_BASE_0 = 476 | TemperatureSensor(name: "ENCLOSURE_BASE_0", 477 | code: FourCharCode(fromStaticString: "TB0T")) 478 | public static let ENCLOSURE_BASE_1 = 479 | TemperatureSensor(name: "ENCLOSURE_BASE_1", 480 | code: FourCharCode(fromStaticString: "TB1T")) 481 | public static let ENCLOSURE_BASE_2 = 482 | TemperatureSensor(name: "ENCLOSURE_BASE_2", 483 | code: FourCharCode(fromStaticString: "TB2T")) 484 | public static let ENCLOSURE_BASE_3 = 485 | TemperatureSensor(name: "ENCLOSURE_BASE_3", 486 | code: FourCharCode(fromStaticString: "TB3T")) 487 | public static let GPU_0_DIODE = TemperatureSensor(name: "GPU_0_DIODE", 488 | code: FourCharCode(fromStaticString: "TG0D")) 489 | public static let GPU_0_HEATSINK = TemperatureSensor(name: "GPU_0_HEATSINK", 490 | code: FourCharCode(fromStaticString: "TG0H")) 491 | public static let GPU_0_PROXIMITY = 492 | TemperatureSensor(name: "GPU_0_PROXIMITY", 493 | code: FourCharCode(fromStaticString: "TG0P")) 494 | public static let HDD_PROXIMITY = TemperatureSensor(name: "HDD_PROXIMITY", 495 | code: FourCharCode(fromStaticString: "TH0P")) 496 | public static let HEATSINK_0 = TemperatureSensor(name: "HEATSINK_0", 497 | code: FourCharCode(fromStaticString: "Th0H")) 498 | public static let HEATSINK_1 = TemperatureSensor(name: "HEATSINK_1", 499 | code: FourCharCode(fromStaticString: "Th1H")) 500 | public static let HEATSINK_2 = TemperatureSensor(name: "HEATSINK_2", 501 | code: FourCharCode(fromStaticString: "Th2H")) 502 | public static let LCD_PROXIMITY = TemperatureSensor(name: "LCD_PROXIMITY", 503 | code: FourCharCode(fromStaticString: "TL0P")) 504 | public static let MEM_SLOT_0 = TemperatureSensor(name: "MEM_SLOT_0", 505 | code: FourCharCode(fromStaticString: "TM0S")) 506 | public static let MEM_SLOTS_PROXIMITY = 507 | TemperatureSensor(name: "MEM_SLOTS_PROXIMITY", 508 | code: FourCharCode(fromStaticString: "TM0P")) 509 | public static let MISC_PROXIMITY = TemperatureSensor(name: "MISC_PROXIMITY", 510 | code: FourCharCode(fromStaticString: "Tm0P")) 511 | public static let NORTHBRIDGE = TemperatureSensor(name: "NORTHBRIDGE", 512 | code: FourCharCode(fromStaticString: "TN0H")) 513 | public static let NORTHBRIDGE_DIODE = 514 | TemperatureSensor(name: "NORTHBRIDGE_DIODE", 515 | code: FourCharCode(fromStaticString: "TN0D")) 516 | public static let NORTHBRIDGE_PROXIMITY = 517 | TemperatureSensor(name: "NORTHBRIDGE_PROXIMITY", 518 | code: FourCharCode(fromStaticString: "TN0P")) 519 | public static let ODD_PROXIMITY = TemperatureSensor(name: "ODD_PROXIMITY", 520 | code: FourCharCode(fromStaticString: "TO0P")) 521 | public static let PALM_REST = TemperatureSensor(name: "PALM_REST", 522 | code: FourCharCode(fromStaticString: "Ts0P")) 523 | public static let PWR_SUPPLY_PROXIMITY = 524 | TemperatureSensor(name: "PWR_SUPPLY_PROXIMITY", 525 | code: FourCharCode(fromStaticString: "Tp0P")) 526 | public static let THUNDERBOLT_0 = TemperatureSensor(name: "THUNDERBOLT_0", 527 | code: FourCharCode(fromStaticString: "TI0P")) 528 | public static let THUNDERBOLT_1 = TemperatureSensor(name: "THUNDERBOLT_1", 529 | code: FourCharCode(fromStaticString: "TI1P")) 530 | 531 | public static let all = [AMBIENT_AIR_0.code: AMBIENT_AIR_0, 532 | AMBIENT_AIR_1.code: AMBIENT_AIR_1, 533 | CPU_0_DIE.code: CPU_0_DIE, 534 | CPU_0_DIODE.code: CPU_0_DIODE, 535 | CPU_0_HEATSINK.code: CPU_0_HEATSINK, 536 | CPU_0_PROXIMITY.code: CPU_0_PROXIMITY, 537 | ENCLOSURE_BASE_0.code: ENCLOSURE_BASE_0, 538 | ENCLOSURE_BASE_1.code: ENCLOSURE_BASE_1, 539 | ENCLOSURE_BASE_2.code: ENCLOSURE_BASE_2, 540 | ENCLOSURE_BASE_3.code: ENCLOSURE_BASE_3, 541 | GPU_0_DIODE.code: GPU_0_DIODE, 542 | GPU_0_HEATSINK.code: GPU_0_HEATSINK, 543 | GPU_0_PROXIMITY.code: GPU_0_PROXIMITY, 544 | HDD_PROXIMITY.code: HDD_PROXIMITY, 545 | HEATSINK_0.code: HEATSINK_0, 546 | HEATSINK_1.code: HEATSINK_1, 547 | HEATSINK_2.code: HEATSINK_2, 548 | MEM_SLOT_0.code: MEM_SLOT_0, 549 | MEM_SLOTS_PROXIMITY.code: MEM_SLOTS_PROXIMITY, 550 | PALM_REST.code: PALM_REST, 551 | LCD_PROXIMITY.code: LCD_PROXIMITY, 552 | MISC_PROXIMITY.code: MISC_PROXIMITY, 553 | NORTHBRIDGE.code: NORTHBRIDGE, 554 | NORTHBRIDGE_DIODE.code: NORTHBRIDGE_DIODE, 555 | NORTHBRIDGE_PROXIMITY.code: NORTHBRIDGE_PROXIMITY, 556 | ODD_PROXIMITY.code: ODD_PROXIMITY, 557 | PWR_SUPPLY_PROXIMITY.code: PWR_SUPPLY_PROXIMITY, 558 | THUNDERBOLT_0.code: THUNDERBOLT_0, 559 | THUNDERBOLT_1.code: THUNDERBOLT_1] 560 | } 561 | 562 | public struct TemperatureSensor { 563 | public let name: String 564 | public let code: FourCharCode 565 | } 566 | 567 | public enum TemperatureUnit { 568 | case celius 569 | case fahrenheit 570 | case kelvin 571 | 572 | public static func toFahrenheit(_ celius: Double) -> Double { 573 | // https://en.wikipedia.org/wiki/Fahrenheit#Definition_and_conversions 574 | return (celius * 1.8) + 32 575 | } 576 | 577 | public static func toKelvin(_ celius: Double) -> Double { 578 | // https://en.wikipedia.org/wiki/Kelvin 579 | return celius + 273.15 580 | } 581 | } 582 | 583 | extension SMCKit { 584 | 585 | public static func allKnownTemperatureSensors() throws -> 586 | [TemperatureSensor] { 587 | var sensors = [TemperatureSensor]() 588 | 589 | for sensor in TemperatureSensors.all.values { 590 | if try isKeyFound(sensor.code) { sensors.append(sensor) } 591 | } 592 | 593 | return sensors 594 | } 595 | 596 | public static func allUnknownTemperatureSensors() throws -> [TemperatureSensor] { 597 | let keys = try allKeys() 598 | 599 | return keys.filter { $0.code.toString().hasPrefix("T") && 600 | $0.info == DataTypes.SP78 && 601 | TemperatureSensors.all[$0.code] == nil } 602 | .map { TemperatureSensor(name: "Unknown", code: $0.code) } 603 | } 604 | 605 | /// Get current temperature of a sensor 606 | public static func temperature(_ sensorCode: FourCharCode, 607 | unit: TemperatureUnit = .celius) throws -> Double { 608 | let data = try readData(SMCKey(code: sensorCode, info: DataTypes.SP78)) 609 | 610 | let temperatureInCelius = Double(fromSP78: (data.0, data.1)) 611 | 612 | switch unit { 613 | case .celius: 614 | return temperatureInCelius 615 | case .fahrenheit: 616 | return TemperatureUnit.toFahrenheit(temperatureInCelius) 617 | case .kelvin: 618 | return TemperatureUnit.toKelvin(temperatureInCelius) 619 | } 620 | } 621 | } 622 | 623 | //------------------------------------------------------------------------------ 624 | // MARK: Fan 625 | //------------------------------------------------------------------------------ 626 | 627 | public struct Fan { 628 | // TODO: Should we start the fan id from 1 instead of 0? 629 | public let id: Int 630 | public let name: String 631 | public let minSpeed: Int 632 | public let maxSpeed: Int 633 | } 634 | 635 | extension SMCKit { 636 | 637 | public static func allFans() throws -> [Fan] { 638 | let count = try fanCount() 639 | var fans = [Fan]() 640 | 641 | for i in 0 ..< count { 642 | fans.append(try SMCKit.fan(i)) 643 | } 644 | 645 | return fans 646 | } 647 | 648 | public static func fan(_ id: Int) throws -> Fan { 649 | let name = try fanName(id) 650 | let minSpeed = try fanMinSpeed(id) 651 | let maxSpeed = try fanMaxSpeed(id) 652 | return Fan(id: id, name: name, minSpeed: minSpeed, maxSpeed: maxSpeed) 653 | } 654 | 655 | /// Number of fans this machine has. All Intel based Macs, except for the 656 | /// 2015 MacBook (8,1), have at least 1 657 | public static func fanCount() throws -> Int { 658 | let key = SMCKey(code: FourCharCode(fromStaticString: "FNum"), 659 | info: DataTypes.UInt8) 660 | 661 | let data = try readData(key) 662 | return Int(data.0) 663 | } 664 | 665 | public static func fanName(_ id: Int) throws -> String { 666 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)ID"), 667 | info: DataTypes.FDS) 668 | let data = try readData(key) 669 | 670 | // The last 12 bytes of '{fds' data type, a custom struct defined by the 671 | // AppleSMC.kext that is 16 bytes, contains the fan name 672 | let c1 = String(UnicodeScalar(data.4)) 673 | let c2 = String(UnicodeScalar(data.5)) 674 | let c3 = String(UnicodeScalar(data.6)) 675 | let c4 = String(UnicodeScalar(data.7)) 676 | let c5 = String(UnicodeScalar(data.8)) 677 | let c6 = String(UnicodeScalar(data.9)) 678 | let c7 = String(UnicodeScalar(data.10)) 679 | let c8 = String(UnicodeScalar(data.11)) 680 | let c9 = String(UnicodeScalar(data.12)) 681 | let c10 = String(UnicodeScalar(data.13)) 682 | let c11 = String(UnicodeScalar(data.14)) 683 | let c12 = String(UnicodeScalar(data.15)) 684 | 685 | let name = c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9 + c10 + c11 + c12 686 | 687 | let characterSet = CharacterSet.whitespaces 688 | return name.trimmingCharacters(in: characterSet) 689 | } 690 | 691 | public static func fanCurrentSpeed(_ id: Int) throws -> Int { 692 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)Ac"), 693 | info: DataTypes.FPE2) 694 | 695 | let data = try readData(key) 696 | return Int(fromFPE2: (data.0, data.1)) 697 | } 698 | 699 | public static func fanMinSpeed(_ id: Int) throws -> Int { 700 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)Mn"), 701 | info: DataTypes.FPE2) 702 | 703 | let data = try readData(key) 704 | return Int(fromFPE2: (data.0, data.1)) 705 | } 706 | 707 | public static func fanMaxSpeed(_ id: Int) throws -> Int { 708 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)Mx"), 709 | info: DataTypes.FPE2) 710 | 711 | let data = try readData(key) 712 | return Int(fromFPE2: (data.0, data.1)) 713 | } 714 | 715 | /// Requires root privileges. By minimum we mean that OS X can interject and 716 | /// raise the fan speed if needed, however it will not go below this. 717 | /// 718 | /// WARNING: You are playing with hardware here, BE CAREFUL. 719 | /// 720 | /// - Throws: Of note, `SMCKit.SMCError`'s `UnsafeFanSpeed` and `NotPrivileged` 721 | public static func fanSetMinSpeed(_ id: Int, speed: Int) throws { 722 | let maxSpeed = try fanMaxSpeed(id) 723 | if speed <= 0 || speed > maxSpeed { throw SMCError.unsafeFanSpeed } 724 | 725 | let data = speed.toFPE2() 726 | let bytes: SMCBytes = (data.0, data.1, UInt8(0), UInt8(0), UInt8(0), UInt8(0), 727 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 728 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 729 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 730 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 731 | UInt8(0), UInt8(0)) 732 | 733 | let key = SMCKey(code: FourCharCode(fromString: "F\(id)Mn"), 734 | info: DataTypes.FPE2) 735 | 736 | try writeData(key, data: bytes) 737 | } 738 | } 739 | 740 | //------------------------------------------------------------------------------ 741 | // MARK: Miscellaneous 742 | //------------------------------------------------------------------------------ 743 | 744 | public struct batteryInfo { 745 | public let batteryCount: Int 746 | public let isACPresent: Bool 747 | public let isBatteryPowered: Bool 748 | public let isBatteryOk: Bool 749 | public let isCharging: Bool 750 | } 751 | 752 | extension SMCKit { 753 | 754 | public static func isOpticalDiskDriveFull() throws -> Bool { 755 | // TODO: Should we catch key not found? That just means the machine 756 | // doesn't have an ODD. Returning false though is not fully correct. 757 | // Maybe we could throw a no ODD error instead? 758 | let key = SMCKey(code: FourCharCode(fromStaticString: "MSDI"), 759 | info: DataTypes.Flag) 760 | 761 | let data = try readData(key) 762 | return Bool(fromByte: data.0) 763 | } 764 | 765 | public static func batteryInformation() throws -> batteryInfo { 766 | let batteryCountKey = 767 | SMCKey(code: FourCharCode(fromStaticString: "BNum"), 768 | info: DataTypes.UInt8) 769 | let batteryPoweredKey = 770 | SMCKey(code: FourCharCode(fromStaticString: "BATP"), 771 | info: DataTypes.Flag) 772 | let batteryInfoKey = 773 | SMCKey(code: FourCharCode(fromStaticString: "BSIn"), 774 | info: DataTypes.UInt8) 775 | 776 | let batteryCountData = try readData(batteryCountKey) 777 | let batteryCount = Int(batteryCountData.0) 778 | 779 | let isBatteryPoweredData = try readData(batteryPoweredKey) 780 | let isBatteryPowered = Bool(fromByte: isBatteryPoweredData.0) 781 | 782 | let batteryInfoData = try readData(batteryInfoKey) 783 | let isCharging = batteryInfoData.0 & 1 == 1 ? true : false 784 | let isACPresent = (batteryInfoData.0 >> 1) & 1 == 1 ? true : false 785 | let isBatteryOk = (batteryInfoData.0 >> 6) & 1 == 1 ? true : false 786 | 787 | return batteryInfo(batteryCount: batteryCount, isACPresent: isACPresent, 788 | isBatteryPowered: isBatteryPowered, 789 | isBatteryOk: isBatteryOk, 790 | isCharging: isCharging) 791 | } 792 | } 793 | -------------------------------------------------------------------------------- /Sources/bclm/main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | 4 | #if arch(x86_64) 5 | let BCLM_KEY = "BCLM" 6 | #else 7 | let BCLM_KEY = "CHWA" 8 | #endif 9 | 10 | struct BCLM: ParsableCommand { 11 | static let configuration = CommandConfiguration( 12 | abstract: "Battery Charge Level Max (BCLM) Utility.", 13 | version: "0.1.0", 14 | subcommands: [Read.self, Write.self, Persist.self, Unpersist.self]) 15 | 16 | struct Read: ParsableCommand { 17 | static let configuration = CommandConfiguration( 18 | abstract: "Reads the BCLM value.") 19 | 20 | func run() { 21 | do { 22 | try SMCKit.open() 23 | } catch { 24 | print(error) 25 | } 26 | 27 | let key = SMCKit.getKey(BCLM_KEY, type: DataTypes.UInt8) 28 | do { 29 | let status = try SMCKit.readData(key).0 30 | #if arch(x86_64) 31 | print(status) 32 | #else 33 | print(status == 1 ? 80 : 100) 34 | #endif 35 | } catch { 36 | print(error) 37 | } 38 | } 39 | } 40 | 41 | struct Write: ParsableCommand { 42 | static let configuration = CommandConfiguration( 43 | abstract: "Writes a BCLM value.") 44 | 45 | #if arch(x86_64) 46 | @Argument(help: "The value to set (50-100)") 47 | var value: Int 48 | #else 49 | @Argument(help: "The value to set (80 or 100)") 50 | var value: Int 51 | #endif 52 | 53 | func validate() throws { 54 | guard getuid() == 0 else { 55 | throw ValidationError("Must run as root.") 56 | } 57 | 58 | #if arch(x86_64) 59 | guard value >= 50 && value <= 100 else { 60 | throw ValidationError("Value must be between 50 and 100.") 61 | } 62 | #else 63 | guard value == 80 || value == 100 else { 64 | throw ValidationError("Value must be either 80 or 100.") 65 | } 66 | #endif 67 | } 68 | 69 | func run() { 70 | do { 71 | try SMCKit.open() 72 | } catch { 73 | print(error) 74 | } 75 | 76 | let bclm_key = SMCKit.getKey(BCLM_KEY, type: DataTypes.UInt8) 77 | 78 | #if arch(x86_64) 79 | let bfcl_key = SMCKit.getKey("BFCL", type: DataTypes.UInt8) 80 | 81 | let bclm_bytes: SMCBytes = ( 82 | UInt8(value), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 83 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 84 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 85 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 86 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 87 | ) 88 | 89 | let bfcl_bytes: SMCBytes = ( 90 | UInt8(value - 5), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 91 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 92 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 93 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 94 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 95 | ) 96 | 97 | do { 98 | try SMCKit.writeData(bclm_key, data: bclm_bytes) 99 | } catch { 100 | print(error) 101 | } 102 | 103 | // USB-C Macs do not have the BFCL key since they don't have the 104 | // charging indicator 105 | do { 106 | try SMCKit.writeData(bfcl_key, data: bfcl_bytes) 107 | } catch SMCKit.SMCError.keyNotFound { 108 | // Do nothing 109 | } catch { 110 | print(error) 111 | } 112 | #else 113 | let bclm_bytes: SMCBytes = ( 114 | UInt8(value == 80 ? 1 : 0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 115 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 116 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 117 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), 118 | UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0) 119 | ) 120 | 121 | do { 122 | try SMCKit.writeData(bclm_key, data: bclm_bytes) 123 | } catch { 124 | print(error) 125 | } 126 | #endif 127 | if (isPersistent()) { 128 | updatePlist(value) 129 | } 130 | } 131 | } 132 | 133 | struct Persist: ParsableCommand { 134 | static let configuration = CommandConfiguration( 135 | abstract: "Persists bclm on reboot.") 136 | 137 | func validate() throws { 138 | guard getuid() == 0 else { 139 | throw ValidationError("Must run as root.") 140 | } 141 | } 142 | 143 | func run() { 144 | do { 145 | try SMCKit.open() 146 | } catch { 147 | print(error) 148 | } 149 | 150 | let key = SMCKit.getKey(BCLM_KEY, type: DataTypes.UInt8) 151 | do { 152 | let status = try SMCKit.readData(key).0 153 | #if arch(x86_64) 154 | updatePlist(Int(status)) 155 | #else 156 | updatePlist(Int(status) == 1 ? 80 : 100) 157 | #endif 158 | } catch { 159 | print(error) 160 | } 161 | 162 | persist(true) 163 | } 164 | } 165 | 166 | struct Unpersist: ParsableCommand { 167 | static let configuration = CommandConfiguration( 168 | abstract: "Unpersists bclm on reboot.") 169 | 170 | func validate() throws { 171 | guard getuid() == 0 else { 172 | throw ValidationError("Must run as root.") 173 | } 174 | } 175 | 176 | func run() { 177 | persist(false) 178 | } 179 | } 180 | } 181 | 182 | BCLM.main() 183 | -------------------------------------------------------------------------------- /Sources/bclm/persist.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let launchctl = "/bin/launchctl" 4 | let plist = "com.zackelia.bclm.plist" 5 | let plist_path = "/Library/LaunchDaemons/\(plist)" 6 | 7 | struct Preferences: Codable { 8 | var Label: String 9 | var RunAtLoad: Bool 10 | var ProgramArguments: [String] 11 | } 12 | 13 | func persist(_ enable: Bool) { 14 | if isPersistent() && enable { 15 | fputs("Already persisting!\n", stderr) 16 | return 17 | } 18 | if !isPersistent() && !enable { 19 | fputs("Already not persisting!\n", stderr) 20 | return 21 | } 22 | 23 | let process = Process() 24 | let pipe = Pipe() 25 | 26 | var load: String 27 | if (enable) { 28 | load = "load" 29 | } else { 30 | load = "unload" 31 | } 32 | 33 | process.launchPath = launchctl 34 | process.arguments = [load, plist_path] 35 | process.standardOutput = pipe 36 | process.standardError = pipe 37 | 38 | process.launch() 39 | 40 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 41 | let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) 42 | 43 | if (output != nil && !output!.isEmpty) { 44 | print(output!) 45 | } 46 | 47 | if !enable { 48 | do { 49 | try FileManager.default.removeItem(at: URL(fileURLWithPath: plist_path)) 50 | } catch { 51 | print(error) 52 | } 53 | } 54 | } 55 | 56 | func isPersistent() -> Bool { 57 | let process = Process() 58 | let pipe = Pipe() 59 | 60 | process.launchPath = launchctl 61 | process.arguments = ["list"] 62 | process.standardOutput = pipe 63 | process.standardError = pipe 64 | 65 | process.launch() 66 | 67 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 68 | let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) 69 | 70 | if (output != nil && output!.contains(plist)) { 71 | return true 72 | } else { 73 | return false 74 | } 75 | } 76 | 77 | func updatePlist(_ value: Int) { 78 | let preferences = Preferences( 79 | Label: plist, 80 | RunAtLoad: true, 81 | ProgramArguments: [ 82 | Bundle.main.executablePath! as String, 83 | "write", 84 | String(value) 85 | ] 86 | ) 87 | 88 | let path = URL(fileURLWithPath: plist_path) 89 | 90 | let encoder = PropertyListEncoder() 91 | encoder.outputFormat = .xml 92 | 93 | do { 94 | let data = try encoder.encode(preferences) 95 | try data.write(to: path) 96 | } catch { 97 | print(error) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import bclmTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += bclmTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/bclmTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(bclmTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/bclmTests/bclmTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | 4 | let launchctl = "/bin/launchctl" 5 | let plist = "com.zackelia.bclm.plist" 6 | let plist_path = "/Library/LaunchDaemons/\(plist)" 7 | 8 | final class bclmTests: XCTestCase { 9 | 10 | /// Helper method to run bclm 11 | func bclm(args: String...) -> String! { 12 | // Some of the APIs that we use below are available in macOS 10.13 and above. 13 | guard #available(macOS 10.13, *) else { 14 | XCTFail("macOS version >= 10.13 required to run tests.") 15 | return "" 16 | } 17 | 18 | let binary = self.productsDirectory.appendingPathComponent("bclm") 19 | let process = Process() 20 | let pipe = Pipe() 21 | 22 | process.executableURL = binary 23 | process.arguments = args 24 | process.standardOutput = pipe 25 | process.standardError = pipe 26 | 27 | do { 28 | try process.run() 29 | } 30 | catch { 31 | XCTFail("Could not start bclm process.") 32 | } 33 | 34 | process.waitUntilExit() 35 | 36 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 37 | let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) 38 | 39 | return output 40 | } 41 | 42 | /// Helper method to check if persistent 43 | func isPersistent() -> Bool { 44 | let process = Process() 45 | let pipe = Pipe() 46 | 47 | process.launchPath = launchctl 48 | process.arguments = ["list"] 49 | process.standardOutput = pipe 50 | process.standardError = pipe 51 | 52 | process.launch() 53 | 54 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 55 | let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) 56 | 57 | if (output != nil && output!.contains(plist)) { 58 | return true 59 | } else { 60 | return false 61 | } 62 | } 63 | 64 | /// Helper method to run bclm read 65 | func readBCLM() -> String! { 66 | return bclm(args: "read") 67 | } 68 | 69 | /// Helper method to run bclm write 70 | func writeBCLM(value: Int) -> String! { 71 | return bclm(args: "write", String(value)) 72 | } 73 | 74 | /// Helper method to run bclm persist 75 | func persistBCLM() -> String! { 76 | return bclm(args: "persist") 77 | } 78 | 79 | /// Helper method to run bclm unpersist 80 | func unpersistBCLM() -> String! { 81 | return bclm(args: "unpersist") 82 | } 83 | 84 | /// Verify that reading the bclm returns a value 85 | func testRead() { 86 | var bclm: Int! 87 | 88 | bclm = Int(readBCLM()!)! 89 | XCTAssertNotNil(bclm) 90 | } 91 | 92 | /// Verify that writing a valid bclm value works 93 | func testWriteValid() { 94 | var bclm: Int! 95 | var output: String! 96 | 97 | // Get the current value to not mess up the runner's configuration 98 | bclm = Int(readBCLM()!)! 99 | 100 | output = writeBCLM(value: bclm)! 101 | XCTAssertEqual(output, "") 102 | } 103 | 104 | // Verify that writing an invalid bclm value did not work 105 | func testWriteInvalid() throws { 106 | var output: String! 107 | 108 | output = writeBCLM(value: 101)! 109 | XCTAssertNotEqual(output, "") 110 | 111 | output = writeBCLM(value: 0)! 112 | XCTAssertNotEqual(output, "") 113 | 114 | #if arch(arm64) 115 | output = writeBCLM(value: 79)! 116 | XCTAssertNotEqual(output, "") 117 | #endif 118 | } 119 | 120 | /// Verify that persisting works 121 | func testPersist() { 122 | var output: String! 123 | var persist: Bool! 124 | 125 | // Get the current value to not mess up the runner's configuration 126 | persist = isPersistent() 127 | 128 | _ = unpersistBCLM() 129 | XCTAssertFalse(isPersistent()) 130 | do { 131 | try FileManager.default.removeItem(at: URL(fileURLWithPath: plist_path)) 132 | } catch { 133 | // Not an error if the file didn't exist 134 | } 135 | 136 | output = persistBCLM()! 137 | XCTAssertEqual(output, "") 138 | XCTAssertTrue(isPersistent()) 139 | XCTAssertTrue(FileManager.default.fileExists(atPath: plist_path)) 140 | 141 | // Second call shouldn't fail, but it should print an error 142 | output = persistBCLM()! 143 | XCTAssertNotEqual(output, "") 144 | XCTAssertTrue(isPersistent()) 145 | XCTAssertTrue(FileManager.default.fileExists(atPath: plist_path)) 146 | 147 | // Restore runner setup 148 | if !persist { 149 | _ = unpersistBCLM() 150 | } 151 | } 152 | 153 | /// Verify that unpersisting works 154 | func testUnpersist() { 155 | var output: String! 156 | var persist: Bool! 157 | 158 | // Get the current value to not mess up the runner's configuration 159 | persist = isPersistent() 160 | 161 | _ = persistBCLM() 162 | XCTAssertTrue(isPersistent()) 163 | 164 | output = unpersistBCLM()! 165 | XCTAssertEqual(output, "") 166 | XCTAssertFalse(isPersistent()) 167 | XCTAssertFalse(FileManager.default.fileExists(atPath: plist_path)) 168 | 169 | // Second call shouldn't fail, but it should print an error 170 | output = unpersistBCLM()! 171 | XCTAssertNotEqual(output, "") 172 | XCTAssertFalse(isPersistent()) 173 | XCTAssertFalse(FileManager.default.fileExists(atPath: plist_path)) 174 | 175 | // Restore runner setup 176 | if persist { 177 | _ = persistBCLM() 178 | } 179 | } 180 | 181 | /// Returns path to the built products directory. 182 | var productsDirectory: URL { 183 | #if os(macOS) 184 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 185 | return bundle.bundleURL.deletingLastPathComponent() 186 | } 187 | fatalError("couldn't find the products directory") 188 | #else 189 | return Bundle.main.bundleURL 190 | #endif 191 | } 192 | 193 | static var allTests = [ 194 | ("testRead", testRead), 195 | ("testWriteValid", testWriteValid), 196 | ("testWriteInvalid", testWriteInvalid), 197 | ("testPersist", testPersist), 198 | ("testUnpersist", testUnpersist), 199 | ] 200 | } 201 | --------------------------------------------------------------------------------