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 = { | |
Vadim Sh.
2017/07/28 20:54:55
do we really need all that stuff?...
M-A Ruel
2017/07/28 20:58:46
No, I want to make it live then trim based on what
M-A Ruel
2017/07/28 21:03:43
Actually, VM *do* report temperatures. That's so a
| |
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: | |
Vadim Sh.
2017/07/28 20:54:55
nit: remove extra space after 'in'
M-A Ruel
2017/07/28 20:58:46
Done.
| |
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 |