OLD | NEW |
1 # Copyright 2015 The LUCI Authors. All rights reserved. | 1 # Copyright 2015 The LUCI Authors. All rights reserved. |
2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
4 | 4 |
5 """OSX specific utility functions.""" | 5 """OSX specific utility functions.""" |
6 | 6 |
7 import cgi | 7 import cgi |
8 import ctypes | 8 import ctypes |
9 import logging | 9 import logging |
10 import os | 10 import os |
11 import platform | 11 import platform |
| 12 import plistlib |
12 import re | 13 import re |
| 14 import struct |
13 import subprocess | 15 import subprocess |
14 import time | 16 import time |
15 | 17 |
16 import plistlib | |
17 | |
18 from utils import tools | 18 from utils import tools |
19 | 19 |
20 import common | 20 import common |
21 import gpu | 21 import gpu |
22 | 22 |
23 | 23 |
24 ## Private stuff. | 24 ## Private stuff. |
25 | 25 |
26 | 26 |
| 27 iokit = ctypes.CDLL( |
| 28 '/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit') |
| 29 # https://developer.apple.com/documentation/iokit/1514274-ioconnectcallstructmet
hod |
| 30 iokit.IOConnectCallStructMethod.argtypes = [ |
| 31 ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p, ctypes.c_ulonglong, |
| 32 ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulonglong), |
| 33 ] |
| 34 iokit.IOConnectCallStructMethod.restype = ctypes.c_int |
| 35 |
| 36 # https://developer.apple.com/documentation/iokit/1514515-ioserviceopen |
| 37 iokit.IOServiceOpen.argtypes = [ |
| 38 ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), |
| 39 ] |
| 40 iokit.IOServiceOpen.restype = ctypes.c_int |
| 41 |
| 42 # https://developer.apple.com/documentation/iokit/1514687-ioservicematching |
| 43 iokit.IOServiceMatching.restype = ctypes.c_void_p |
| 44 |
| 45 # https://developer.apple.com/documentation/iokit/1514494-ioservicegetmatchingse
rvices |
| 46 iokit.IOServiceGetMatchingServices.argtypes = [ |
| 47 ctypes.c_uint, ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint), |
| 48 ] |
| 49 iokit.IOServiceGetMatchingServices.restype = ctypes.c_int |
| 50 |
| 51 # https://developer.apple.com/documentation/iokit/1514741-ioiteratornext |
| 52 iokit.IOIteratorNext.argtypes = [ctypes.c_uint] |
| 53 iokit.IOIteratorNext.restype = ctypes.c_uint |
| 54 |
| 55 # https://developer.apple.com/documentation/iokit/1514627-ioobjectrelease |
| 56 iokit.IOObjectRelease.argtypes = [ctypes.c_uint] |
| 57 iokit.IOObjectRelease.restype = ctypes.c_int |
| 58 |
| 59 |
| 60 libkern = ctypes.CDLL('/usr/lib/system/libsystem_kernel.dylib') |
| 61 libkern.mach_task_self.restype = ctypes.c_uint |
| 62 |
| 63 |
| 64 class _SMC_KeyDataVersion(ctypes.Structure): |
| 65 _fields_ = [ |
| 66 ('major', ctypes.c_uint8), |
| 67 ('minor', ctypes.c_uint8), |
| 68 ('build', ctypes.c_uint8), |
| 69 ('reserved', ctypes.c_uint8), |
| 70 ('release', ctypes.c_uint16), |
| 71 ] |
| 72 |
| 73 |
| 74 class _SMC_KeyDataLimits(ctypes.Structure): |
| 75 _fields_ = [ |
| 76 ('version', ctypes.c_uint16), |
| 77 ('length', ctypes.c_uint16), |
| 78 ('cpu', ctypes.c_uint32), |
| 79 ('gpu', ctypes.c_uint32), |
| 80 ('mem', ctypes.c_uint32), |
| 81 ] |
| 82 |
| 83 |
| 84 class _SMC_KeyDataInfo(ctypes.Structure): |
| 85 _fields_ = [ |
| 86 ('size', ctypes.c_uint32), |
| 87 ('type', ctypes.c_uint32), |
| 88 ('attributes', ctypes.c_uint8), |
| 89 ] |
| 90 |
| 91 |
| 92 class _SMC_KeyData(ctypes.Structure): |
| 93 _fields_ = [ |
| 94 ('key', ctypes.c_uint32), |
| 95 ('version', _SMC_KeyDataVersion), |
| 96 ('pLimitData', _SMC_KeyDataLimits), |
| 97 ('keyInfo', _SMC_KeyDataInfo), |
| 98 ('result', ctypes.c_uint8), |
| 99 ('status', ctypes.c_uint8), |
| 100 ('data8', ctypes.c_uint8), |
| 101 ('data32', ctypes.c_uint32), |
| 102 ('bytes', ctypes.c_ubyte * 32), |
| 103 ] |
| 104 |
| 105 |
| 106 class _SMC_Value(ctypes.Structure): |
| 107 _fields_ = [ |
| 108 ('key', (ctypes.c_ubyte * 5)), |
| 109 ('size', ctypes.c_uint32), |
| 110 ('type', (ctypes.c_ubyte * 5)), |
| 111 ('bytes', ctypes.c_ubyte * 32), |
| 112 ] |
| 113 |
| 114 |
| 115 # http://bxr.su/OpenBSD/sys/dev/isa/asmc.c is a great list of sensors. |
| 116 # |
| 117 # The following other sensor were found on a MBP 2012 via brute forcing but |
| 118 # their signification is unknown: |
| 119 # TC0E, TC0F, TH0A, TH0B, TH0V, TP0P, TS0D, TS0P |
| 120 _sensor_names = { |
| 121 'TA0P': u'ambient', # 'hdd bay 1', |
| 122 'TA0S': u'pci slot 1 pos 1', |
| 123 'TA1S': u'pci slot 1 pos 2', |
| 124 'TA3S': u'pci slot 2 pos 2', |
| 125 'TB0T': u'enclosure bottom', |
| 126 'TB2T': u'enclosure bottom 3', |
| 127 'TC0D': u'cpu0 die core', |
| 128 'TC0P': u'cpu0 proximity', |
| 129 'TC1D': u'cpu1', |
| 130 'TCAH': u'cpu0', |
| 131 'TCDH': u'cpu3', |
| 132 'TG0D': u'gpu0 diode', |
| 133 'TG0P': u'gpu0 proximity', |
| 134 'TG1H': u'gpu heatsink 2', |
| 135 'TH0P': u'hdd bay 1', |
| 136 'TH2P': u'hdd bay 3', |
| 137 'TL0P': u'lcd proximity', |
| 138 'TM0P': u'mem bank a1', |
| 139 'TM1P': u'mem bank a2', |
| 140 'TM2P': u'mem bank a3', |
| 141 'TM3P': u'mem bank a4', |
| 142 'TM4P': u'mem bank a5', |
| 143 'TM5P': u'mem bank a6', |
| 144 'TM6P': u'mem bank a7', |
| 145 'TM7P': u'mem bank a8', |
| 146 'TM8P': u'mem bank b1', |
| 147 'TM9P': u'mem bank b2', |
| 148 'TMA1': u'ram a1', |
| 149 'TMA3': u'ram a3', |
| 150 'TMAP': u'mem bank b3', |
| 151 'TMB1': u'ram b1', |
| 152 'TMB3': u'ram b3', |
| 153 'TMBP': u'mem bank b4', |
| 154 'TMCP': u'mem bank b5', |
| 155 'TMDP': u'mem bank b6', |
| 156 'TMEP': u'mem bank b7', |
| 157 'TMFP': u'mem bank b8', |
| 158 'TN0D': u'northbridge die core', |
| 159 'TN0P': u'northbridge proximity', |
| 160 'TO0P': u'optical drive', |
| 161 'TW0P': u'wireless airport card', |
| 162 'Th0H': u'main heatsink a', |
| 163 'Th2H': u'main heatsink c', |
| 164 'Tm0P': u'memory controller', |
| 165 'Tp0C': u'power supply 1', |
| 166 'Tp1C': u'power supply 2', |
| 167 'Tp2P': u'power supply 3', |
| 168 'Tp4P': u'power supply 5', |
| 169 'TA1P': u'ambient 2', |
| 170 'TA2S': u'pci slot 2 pos 1', |
| 171 'TB1T': u'enclosure bottom 2', |
| 172 'TB3T': u'enclosure bottom 4', |
| 173 'TC0H': u'cpu0 heatsink', |
| 174 'TC2D': u'cpu2', |
| 175 'TC3D': u'cpu3', |
| 176 'TCBH': u'cpu1', |
| 177 'TCCH': u'cpu2', |
| 178 'TG0H': u'gpu0 heatsink', |
| 179 'TH1P': u'hdd bay 2', |
| 180 'TH3P': u'hdd bay 4', |
| 181 'TM0S': u'mem module a1', |
| 182 'TM1S': u'mem module a2', |
| 183 'TM2S': u'mem module a3', |
| 184 'TM3S': u'mem module a4', |
| 185 'TM4S': u'mem module a5', |
| 186 'TM5S': u'mem module a6', |
| 187 'TM6S': u'mem module a7', |
| 188 'TM7S': u'mem module a8', |
| 189 'TM8S': u'mem module b1', |
| 190 'TM9S': u'mem module b2', |
| 191 'TMA2': u'ram a2', |
| 192 'TMA4': u'ram a4', |
| 193 'TMAS': u'mem module b3', |
| 194 'TMB2': u'ram b2', |
| 195 'TMB4': u'ram b4', |
| 196 'TMBS': u'mem module b4', |
| 197 'TMCS': u'mem module b5', |
| 198 'TMDS': u'mem module b6', |
| 199 'TMES': u'mem module b7', |
| 200 'TMFS': u'mem module b8', |
| 201 'TN0H': u'northbridge', |
| 202 'TN1P': u'northbridge 2', |
| 203 'TS0C': u'expansion slots', |
| 204 'Th1H': u'main heatsink b', |
| 205 'Tp0P': u'power supply 1', |
| 206 'Tp1P': u'power supply 2', |
| 207 'Tp3P': u'power supply 4', |
| 208 'Tp5P': u'power supply 6', |
| 209 } |
| 210 |
| 211 # _sensor_found_cache is set on the first call to _SMC_get_values. |
| 212 _sensor_found_cache = None |
| 213 |
| 214 |
| 215 @tools.cached |
| 216 def _SMC_open(): |
| 217 """Opens the default SMC driver and returns the first device. |
| 218 |
| 219 It leaves the device handle open for the duration of the process. |
| 220 """ |
| 221 # There should be only one. |
| 222 itr = ctypes.c_uint() |
| 223 result = iokit.IOServiceGetMatchingServices( |
| 224 0, iokit.IOServiceMatching('AppleSMC'), ctypes.byref(itr)) |
| 225 if result: |
| 226 logging.error('failed to get AppleSMC (%d)', result) |
| 227 return None |
| 228 dev = iokit.IOIteratorNext(itr) |
| 229 iokit.IOObjectRelease(itr) |
| 230 if not dev: |
| 231 logging.error('no SMC found') |
| 232 return None |
| 233 conn = ctypes.c_uint() |
| 234 if iokit.IOServiceOpen(dev, libkern.mach_task_self(), 0, ctypes.byref(conn)): |
| 235 logging.error('failed to open AppleSMC (%d)', result) |
| 236 return None |
| 237 return conn |
| 238 |
| 239 |
| 240 def _SMC_call(conn, index, indata, outdata): |
| 241 """Executes a call to the SMC subsystem.""" |
| 242 return iokit.IOConnectCallStructMethod( |
| 243 conn, |
| 244 ctypes.c_uint(index), |
| 245 ctypes.cast(ctypes.pointer(indata), ctypes.c_void_p), |
| 246 ctypes.c_ulonglong(ctypes.sizeof(_SMC_KeyData)), |
| 247 ctypes.cast(ctypes.pointer(outdata), ctypes.c_void_p), |
| 248 ctypes.pointer(ctypes.c_ulonglong(ctypes.sizeof(_SMC_KeyData)))) |
| 249 |
| 250 |
| 251 def _SMC_read_key(conn, key): |
| 252 """Retrieves an unprocessed key value.""" |
| 253 KERNEL_INDEX_SMC = 2 |
| 254 |
| 255 # Call with SMC_CMD_READ_KEYINFO. |
| 256 # TODO(maruel): Keep cache of result size. |
| 257 indata = _SMC_KeyData(key=struct.unpack('>i', key)[0], data8=9) |
| 258 outdata = _SMC_KeyData() |
| 259 if _SMC_call(conn, KERNEL_INDEX_SMC, indata, outdata): |
| 260 logging.error('SMC call to get key info failed') |
| 261 return None |
| 262 |
| 263 # Call with SMC_CMD_READ_BYTES. |
| 264 val = _SMC_Value(size=outdata.keyInfo.size) |
| 265 for i, x in enumerate(struct.pack('>i', outdata.keyInfo.type)): |
| 266 val.type[i] = ord(x) |
| 267 # pylint: disable=attribute-defined-outside-init |
| 268 indata.data8 = 5 |
| 269 indata.keyInfo.size = val.size |
| 270 if _SMC_call(conn, KERNEL_INDEX_SMC, indata, outdata): |
| 271 logging.error('SMC call to get data info failed') |
| 272 return None |
| 273 val.bytes = outdata.bytes |
| 274 return val |
| 275 |
| 276 |
| 277 def _SMC_get_value(conn, key): |
| 278 """Returns a processed measurement via AppleSMC for a specified key. |
| 279 |
| 280 Returns None on failure. |
| 281 """ |
| 282 val = _SMC_read_key(conn, key) |
| 283 if not val or not val.size: |
| 284 return None |
| 285 t = ''.join(map(chr, val.type)) |
| 286 if t == 'sp78\0' and val.size == 2: |
| 287 # Format is first byte signed int8, second byte uint8 fractional. |
| 288 return float(ctypes.c_int8(val.bytes[0]).value) + (val.bytes[1] / 256.) |
| 289 if t == 'fpe2\0' and val.size == 2: |
| 290 # Format is unsigned 14 bits big endian, 2 bits fractional. |
| 291 return ( |
| 292 float((val.bytes[0] << 6) + (val.bytes[1] >> 2)) + |
| 293 (val.bytes[1] & 3) / 4.) |
| 294 # TODO(maruel): Handler other formats like 64 bits long. This is used for fan |
| 295 # speed. |
| 296 logging.error( |
| 297 '_SMC_get_value(%s) got unknown format: %s of %d bytes', key, t, val.size) |
| 298 return None |
| 299 |
| 300 |
27 @tools.cached | 301 @tools.cached |
28 def _get_system_profiler(data_type): | 302 def _get_system_profiler(data_type): |
29 """Returns an XML about the system display properties.""" | 303 """Returns an XML about the system display properties.""" |
30 sp = subprocess.check_output( | 304 sp = subprocess.check_output( |
31 ['system_profiler', data_type, '-xml']) | 305 ['system_profiler', data_type, '-xml']) |
32 return plistlib.readPlistFromString(sp)[0]['_items'] | 306 return plistlib.readPlistFromString(sp)[0]['_items'] |
33 | 307 |
34 | 308 |
35 @tools.cached | 309 @tools.cached |
36 def _get_libc(): | 310 def _get_libc(): |
(...skipping 128 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
165 | 439 |
166 | 440 |
167 @tools.cached | 441 @tools.cached |
168 def get_hardware_model_string(): | 442 def get_hardware_model_string(): |
169 """Returns the Mac model string. | 443 """Returns the Mac model string. |
170 | 444 |
171 Returns: | 445 Returns: |
172 A string like Macmini5,3 or MacPro6,1. | 446 A string like Macmini5,3 or MacPro6,1. |
173 """ | 447 """ |
174 try: | 448 try: |
175 return subprocess.check_output(['sysctl', '-n', 'hw.model']).rstrip() | 449 return unicode( |
| 450 subprocess.check_output(['sysctl', '-n', 'hw.model']).rstrip()) |
176 except (OSError, subprocess.CalledProcessError): | 451 except (OSError, subprocess.CalledProcessError): |
177 return None | 452 return None |
178 | 453 |
179 | 454 |
180 @tools.cached | 455 @tools.cached |
181 def get_os_version_number(): | 456 def get_os_version_number(): |
182 """Returns the normalized OS version number as a string. | 457 """Returns the normalized OS version number as a string. |
183 | 458 |
184 Returns: | 459 Returns: |
185 Version as a string like '10.12.4' | 460 Version as a string like '10.12.4' |
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
253 u'model': [ | 528 u'model': [ |
254 int(values['machdep.cpu.family']), int(values['machdep.cpu.model']), | 529 int(values['machdep.cpu.family']), int(values['machdep.cpu.model']), |
255 int(values['machdep.cpu.stepping']), | 530 int(values['machdep.cpu.stepping']), |
256 int(values['machdep.cpu.microcode_version']), | 531 int(values['machdep.cpu.microcode_version']), |
257 ], | 532 ], |
258 u'name': values[u'machdep.cpu.brand_string'], | 533 u'name': values[u'machdep.cpu.brand_string'], |
259 u'vendor': values[u'machdep.cpu.vendor'], | 534 u'vendor': values[u'machdep.cpu.vendor'], |
260 } | 535 } |
261 | 536 |
262 | 537 |
| 538 def get_temperatures(): |
| 539 """Returns the temperatures in Celsius.""" |
| 540 global _sensor_found_cache |
| 541 conn = _SMC_open() |
| 542 if not conn: |
| 543 return None |
| 544 |
| 545 out = {} |
| 546 if _sensor_found_cache is None: |
| 547 _sensor_found_cache = set() |
| 548 # Populate the cache of the sensors found on this system, so that the next |
| 549 # call can only get the actual sensors. |
| 550 # Note: It is relatively fast to brute force all the possible names. |
| 551 for key, name in _sensor_names.iteritems(): |
| 552 value = _SMC_get_value(conn, key) |
| 553 if value is not None: |
| 554 _sensor_found_cache.add(key) |
| 555 out[name] = value |
| 556 return out |
| 557 |
| 558 for key in _sensor_found_cache: |
| 559 value = _SMC_get_value(conn, key) |
| 560 if value is not None: |
| 561 out[_sensor_names[key]] = value |
| 562 return out |
| 563 |
| 564 |
263 @tools.cached | 565 @tools.cached |
264 def get_monitor_hidpi(): | 566 def get_monitor_hidpi(): |
265 """Returns True if the monitor is hidpi. | 567 """Returns True if the monitor is hidpi. |
266 | 568 |
267 On 10.12.3 and earlier, the following could be used to detect an hidpi | 569 On 10.12.3 and earlier, the following could be used to detect an hidpi |
268 display: | 570 display: |
269 <key>spdisplays_retina</key> | 571 <key>spdisplays_retina</key> |
270 <string>spdisplays_yes</string> | 572 <string>spdisplays_yes</string> |
271 | 573 |
272 On 10.12.4 and later, the key above doesn't exist anymore. Fall back to search | 574 On 10.12.4 and later, the key above doesn't exist anymore. Fall back to search |
273 for: | 575 for: |
274 <key>spdisplays_display_type</key> | 576 <key>spdisplays_display_type</key> |
275 <string>spdisplays_built-in_retinaLCD</string> | 577 <string>spdisplays_built-in_retinaLCD</string> |
276 """ | 578 """ |
277 def is_hidpi(displays): | 579 def is_hidpi(displays): |
278 return any( | 580 return any( |
279 d.get('spdisplays_retina') == 'spdisplays_yes' or | 581 d.get('spdisplays_retina') == 'spdisplays_yes' or |
280 'retina' in d.get('spdisplays_display_type', '').lower() | 582 'retina' in d.get('spdisplays_display_type', '').lower() |
281 for d in displays) | 583 for d in displays) |
282 | 584 |
283 hidpi = any( | 585 hidpi = any( |
284 is_hidpi(card['spdisplays_ndrvs']) | 586 is_hidpi(card['spdisplays_ndrvs']) |
285 for card in _get_system_profiler('SPDisplaysDataType') | 587 for card in _get_system_profiler('SPDisplaysDataType') |
286 if 'spdisplays_ndrvs' in card) | 588 if 'spdisplays_ndrvs' in card) |
287 return str(int(hidpi)) | 589 return unicode(int(hidpi)) |
288 | 590 |
289 | 591 |
290 def get_xcode_versions(): | 592 def get_xcode_versions(): |
291 """Returns all Xcode versions installed.""" | 593 """Returns all Xcode versions installed.""" |
292 return sorted(xcode['version'] for xcode in get_xcode_state().itervalues()) | 594 return sorted( |
| 595 unicode(xcode['version']) for xcode in get_xcode_state().itervalues()) |
293 | 596 |
294 | 597 |
295 @tools.cached | 598 @tools.cached |
296 def get_physical_ram(): | 599 def get_physical_ram(): |
297 """Returns the amount of installed RAM in Mb, rounded to the nearest number. | 600 """Returns the amount of installed RAM in Mb, rounded to the nearest number. |
298 """ | 601 """ |
299 CTL_HW = 6 | 602 CTL_HW = 6 |
300 HW_MEMSIZE = 24 | 603 HW_MEMSIZE = 24 |
301 result = ctypes.c_uint64(0) | 604 result = ctypes.c_uint64(0) |
302 _sysctl(CTL_HW, HW_MEMSIZE, result) | 605 _sysctl(CTL_HW, HW_MEMSIZE, result) |
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
389 header = ( | 692 header = ( |
390 '<?xml version="1.0" encoding="UTF-8"?>\n' | 693 '<?xml version="1.0" encoding="UTF-8"?>\n' |
391 '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ' | 694 '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ' |
392 '"http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' | 695 '"http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' |
393 '<plist version="1.0">\n' | 696 '<plist version="1.0">\n' |
394 ' <dict>\n' | 697 ' <dict>\n' |
395 + ''.join(' %s\n' % l for l in entries) + | 698 + ''.join(' %s\n' % l for l in entries) + |
396 ' </dict>\n' | 699 ' </dict>\n' |
397 '</plist>\n') | 700 '</plist>\n') |
398 return header | 701 return header |
OLD | NEW |