Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(202)

Side by Side Diff: appengine/swarming/swarming_bot/api/platforms/osx.py

Issue 2987023002: swarming: report CPU temperature on OSX as state. (Closed)
Patch Set: Generalize Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « appengine/swarming/swarming_bot/api/os_utilities.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
OLDNEW
« no previous file with comments | « appengine/swarming/swarming_bot/api/os_utilities.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698