Chromium Code Reviews| Index: appengine/swarming/swarming_bot/api/platforms/osx.py |
| diff --git a/appengine/swarming/swarming_bot/api/platforms/osx.py b/appengine/swarming/swarming_bot/api/platforms/osx.py |
| index 27b51cbb107c359a7a6fc3b6ea7424e475a4aa83..f950d83fad37aeed83ab9df37436c89a26881fe3 100644 |
| --- a/appengine/swarming/swarming_bot/api/platforms/osx.py |
| +++ b/appengine/swarming/swarming_bot/api/platforms/osx.py |
| @@ -9,12 +9,12 @@ import ctypes |
| import logging |
| import os |
| import platform |
| +import plistlib |
| import re |
| +import struct |
| import subprocess |
| import time |
| -import plistlib |
| - |
| from utils import tools |
| import common |
| @@ -24,6 +24,280 @@ import gpu |
| ## Private stuff. |
| +iokit = ctypes.CDLL( |
| + '/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit') |
| +# https://developer.apple.com/documentation/iokit/1514274-ioconnectcallstructmethod |
| +iokit.IOConnectCallStructMethod.argtypes = [ |
| + ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p, ctypes.c_ulonglong, |
| + ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulonglong), |
| +] |
| +iokit.IOConnectCallStructMethod.restype = ctypes.c_int |
| + |
| +# https://developer.apple.com/documentation/iokit/1514515-ioserviceopen |
| +iokit.IOServiceOpen.argtypes = [ |
| + ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), |
| +] |
| +iokit.IOServiceOpen.restype = ctypes.c_int |
| + |
| +# https://developer.apple.com/documentation/iokit/1514687-ioservicematching |
| +iokit.IOServiceMatching.restype = ctypes.c_void_p |
| + |
| +# https://developer.apple.com/documentation/iokit/1514494-ioservicegetmatchingservices |
| +iokit.IOServiceGetMatchingServices.argtypes = [ |
| + ctypes.c_uint, ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint), |
| +] |
| +iokit.IOServiceGetMatchingServices.restype = ctypes.c_int |
| + |
| +# https://developer.apple.com/documentation/iokit/1514741-ioiteratornext |
| +iokit.IOIteratorNext.argtypes = [ctypes.c_uint] |
| +iokit.IOIteratorNext.restype = ctypes.c_uint |
| + |
| +# https://developer.apple.com/documentation/iokit/1514627-ioobjectrelease |
| +iokit.IOObjectRelease.argtypes = [ctypes.c_uint] |
| +iokit.IOObjectRelease.restype = ctypes.c_int |
| + |
| + |
| +libkern = ctypes.CDLL('/usr/lib/system/libsystem_kernel.dylib') |
| +libkern.mach_task_self.restype = ctypes.c_uint |
| + |
| + |
| +class _SMC_KeyDataVersion(ctypes.Structure): |
| + _fields_ = [ |
| + ('major', ctypes.c_uint8), |
| + ('minor', ctypes.c_uint8), |
| + ('build', ctypes.c_uint8), |
| + ('reserved', ctypes.c_uint8), |
| + ('release', ctypes.c_uint16), |
| + ] |
| + |
| + |
| +class _SMC_KeyDataLimits(ctypes.Structure): |
| + _fields_ = [ |
| + ('version', ctypes.c_uint16), |
| + ('length', ctypes.c_uint16), |
| + ('cpu', ctypes.c_uint32), |
| + ('gpu', ctypes.c_uint32), |
| + ('mem', ctypes.c_uint32), |
| + ] |
| + |
| + |
| +class _SMC_KeyDataInfo(ctypes.Structure): |
| + _fields_ = [ |
| + ('size', ctypes.c_uint32), |
| + ('type', ctypes.c_uint32), |
| + ('attributes', ctypes.c_uint8), |
| + ] |
| + |
| + |
| +class _SMC_KeyData(ctypes.Structure): |
| + _fields_ = [ |
| + ('key', ctypes.c_uint32), |
| + ('version', _SMC_KeyDataVersion), |
| + ('pLimitData', _SMC_KeyDataLimits), |
| + ('keyInfo', _SMC_KeyDataInfo), |
| + ('result', ctypes.c_uint8), |
| + ('status', ctypes.c_uint8), |
| + ('data8', ctypes.c_uint8), |
| + ('data32', ctypes.c_uint32), |
| + ('bytes', ctypes.c_ubyte * 32), |
| + ] |
| + |
| + |
| +class _SMC_Value(ctypes.Structure): |
| + _fields_ = [ |
| + ('key', (ctypes.c_ubyte * 5)), |
| + ('size', ctypes.c_uint32), |
| + ('type', (ctypes.c_ubyte * 5)), |
| + ('bytes', ctypes.c_ubyte * 32), |
| + ] |
| + |
| + |
| +# http://bxr.su/OpenBSD/sys/dev/isa/asmc.c is a great list of sensors. |
| +# |
| +# The following other sensor were found on a MBP 2012 via brute forcing but |
| +# their signification is unknown: |
| +# TC0E, TC0F, TH0A, TH0B, TH0V, TP0P, TS0D, TS0P |
| +_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
|
| + 'TA0P': u'ambient', # 'hdd bay 1', |
| + 'TA0S': u'pci slot 1 pos 1', |
| + 'TA1S': u'pci slot 1 pos 2', |
| + 'TA3S': u'pci slot 2 pos 2', |
| + 'TB0T': u'enclosure bottom', |
| + 'TB2T': u'enclosure bottom 3', |
| + 'TC0D': u'cpu0 die core', |
| + 'TC0P': u'cpu0 proximity', |
| + 'TC1D': u'cpu1', |
| + 'TCAH': u'cpu0', |
| + 'TCDH': u'cpu3', |
| + 'TG0D': u'gpu0 diode', |
| + 'TG0P': u'gpu0 proximity', |
| + 'TG1H': u'gpu heatsink 2', |
| + 'TH0P': u'hdd bay 1', |
| + 'TH2P': u'hdd bay 3', |
| + 'TL0P': u'lcd proximity', |
| + 'TM0P': u'mem bank a1', |
| + 'TM1P': u'mem bank a2', |
| + 'TM2P': u'mem bank a3', |
| + 'TM3P': u'mem bank a4', |
| + 'TM4P': u'mem bank a5', |
| + 'TM5P': u'mem bank a6', |
| + 'TM6P': u'mem bank a7', |
| + 'TM7P': u'mem bank a8', |
| + 'TM8P': u'mem bank b1', |
| + 'TM9P': u'mem bank b2', |
| + 'TMA1': u'ram a1', |
| + 'TMA3': u'ram a3', |
| + 'TMAP': u'mem bank b3', |
| + 'TMB1': u'ram b1', |
| + 'TMB3': u'ram b3', |
| + 'TMBP': u'mem bank b4', |
| + 'TMCP': u'mem bank b5', |
| + 'TMDP': u'mem bank b6', |
| + 'TMEP': u'mem bank b7', |
| + 'TMFP': u'mem bank b8', |
| + 'TN0D': u'northbridge die core', |
| + 'TN0P': u'northbridge proximity', |
| + 'TO0P': u'optical drive', |
| + 'TW0P': u'wireless airport card', |
| + 'Th0H': u'main heatsink a', |
| + 'Th2H': u'main heatsink c', |
| + 'Tm0P': u'memory controller', |
| + 'Tp0C': u'power supply 1', |
| + 'Tp1C': u'power supply 2', |
| + 'Tp2P': u'power supply 3', |
| + 'Tp4P': u'power supply 5', |
| + 'TA1P': u'ambient 2', |
| + 'TA2S': u'pci slot 2 pos 1', |
| + 'TB1T': u'enclosure bottom 2', |
| + 'TB3T': u'enclosure bottom 4', |
| + 'TC0H': u'cpu0 heatsink', |
| + 'TC2D': u'cpu2', |
| + 'TC3D': u'cpu3', |
| + 'TCBH': u'cpu1', |
| + 'TCCH': u'cpu2', |
| + 'TG0H': u'gpu0 heatsink', |
| + 'TH1P': u'hdd bay 2', |
| + 'TH3P': u'hdd bay 4', |
| + 'TM0S': u'mem module a1', |
| + 'TM1S': u'mem module a2', |
| + 'TM2S': u'mem module a3', |
| + 'TM3S': u'mem module a4', |
| + 'TM4S': u'mem module a5', |
| + 'TM5S': u'mem module a6', |
| + 'TM6S': u'mem module a7', |
| + 'TM7S': u'mem module a8', |
| + 'TM8S': u'mem module b1', |
| + 'TM9S': u'mem module b2', |
| + 'TMA2': u'ram a2', |
| + 'TMA4': u'ram a4', |
| + 'TMAS': u'mem module b3', |
| + 'TMB2': u'ram b2', |
| + 'TMB4': u'ram b4', |
| + 'TMBS': u'mem module b4', |
| + 'TMCS': u'mem module b5', |
| + 'TMDS': u'mem module b6', |
| + 'TMES': u'mem module b7', |
| + 'TMFS': u'mem module b8', |
| + 'TN0H': u'northbridge', |
| + 'TN1P': u'northbridge 2', |
| + 'TS0C': u'expansion slots', |
| + 'Th1H': u'main heatsink b', |
| + 'Tp0P': u'power supply 1', |
| + 'Tp1P': u'power supply 2', |
| + 'Tp3P': u'power supply 4', |
| + 'Tp5P': u'power supply 6', |
| +} |
| + |
| +# _sensor_found_cache is set on the first call to _SMC_get_values. |
| +_sensor_found_cache = None |
| + |
| + |
| +@tools.cached |
| +def _SMC_open(): |
| + """Opens the default SMC driver and returns the first device. |
| + |
| + It leaves the device handle open for the duration of the process. |
| + """ |
| + # There should be only one. |
| + itr = ctypes.c_uint() |
| + result = iokit.IOServiceGetMatchingServices( |
| + 0, iokit.IOServiceMatching('AppleSMC'), ctypes.byref(itr)) |
| + if result: |
| + logging.error('failed to get AppleSMC (%d)', result) |
| + return None |
| + dev = iokit.IOIteratorNext(itr) |
| + iokit.IOObjectRelease(itr) |
| + if not dev: |
| + logging.error('no SMC found') |
| + return None |
| + conn = ctypes.c_uint() |
| + if iokit.IOServiceOpen(dev, libkern.mach_task_self(), 0, ctypes.byref(conn)): |
| + logging.error('failed to open AppleSMC (%d)', result) |
| + return None |
| + return conn |
| + |
| + |
| +def _SMC_call(conn, index, indata, outdata): |
| + """Executes a call to the SMC subsystem.""" |
| + return iokit.IOConnectCallStructMethod( |
| + conn, |
| + ctypes.c_uint(index), |
| + ctypes.cast(ctypes.pointer(indata), ctypes.c_void_p), |
| + ctypes.c_ulonglong(ctypes.sizeof(_SMC_KeyData)), |
| + ctypes.cast(ctypes.pointer(outdata), ctypes.c_void_p), |
| + ctypes.pointer(ctypes.c_ulonglong(ctypes.sizeof(_SMC_KeyData)))) |
| + |
| + |
| +def _SMC_read_key(conn, key): |
| + """Retrieves an unprocessed key value.""" |
| + KERNEL_INDEX_SMC = 2 |
| + |
| + # Call with SMC_CMD_READ_KEYINFO. |
| + # TODO(maruel): Keep cache of result size. |
| + indata = _SMC_KeyData(key=struct.unpack('>i', key)[0], data8=9) |
| + outdata = _SMC_KeyData() |
| + if _SMC_call(conn, KERNEL_INDEX_SMC, indata, outdata): |
| + logging.error('SMC call to get key info failed') |
| + return None |
| + |
| + # Call with SMC_CMD_READ_BYTES. |
| + val = _SMC_Value(size=outdata.keyInfo.size) |
| + for i, x in enumerate(struct.pack('>i', outdata.keyInfo.type)): |
| + val.type[i] = ord(x) |
| + # pylint: disable=attribute-defined-outside-init |
| + indata.data8 = 5 |
| + indata.keyInfo.size = val.size |
| + if _SMC_call(conn, KERNEL_INDEX_SMC, indata, outdata): |
| + logging.error('SMC call to get data info failed') |
| + return None |
| + val.bytes = outdata.bytes |
| + return val |
| + |
| + |
| +def _SMC_get_value(conn, key): |
| + """Returns a processed measurement via AppleSMC for a specified key. |
| + |
| + Returns None on failure. |
| + """ |
| + val = _SMC_read_key(conn, key) |
| + if not val or not val.size: |
| + return None |
| + t = ''.join(map(chr, val.type)) |
| + if t == 'sp78\0' and val.size == 2: |
| + # Format is first byte signed int8, second byte uint8 fractional. |
| + return float(ctypes.c_int8(val.bytes[0]).value) + (val.bytes[1] / 256.) |
| + if t == 'fpe2\0' and val.size == 2: |
| + # Format is unsigned 14 bits big endian, 2 bits fractional. |
| + return ( |
| + float((val.bytes[0] << 6) + (val.bytes[1] >> 2)) + |
| + (val.bytes[1] & 3) / 4.) |
| + # TODO(maruel): Handler other formats like 64 bits long. This is used for fan |
| + # speed. |
| + logging.error( |
| + '_SMC_get_value(%s) got unknown format: %s of %d bytes', key, t, val.size) |
| + return None |
| + |
| + |
| @tools.cached |
| def _get_system_profiler(data_type): |
| """Returns an XML about the system display properties.""" |
| @@ -172,7 +446,8 @@ def get_hardware_model_string(): |
| A string like Macmini5,3 or MacPro6,1. |
| """ |
| try: |
| - return subprocess.check_output(['sysctl', '-n', 'hw.model']).rstrip() |
| + return unicode( |
| + subprocess.check_output(['sysctl', '-n', 'hw.model']).rstrip()) |
| except (OSError, subprocess.CalledProcessError): |
| return None |
| @@ -260,6 +535,33 @@ def get_cpuinfo(): |
| } |
| +def get_temperatures(): |
| + """Returns the temperatures in Celsius.""" |
| + global _sensor_found_cache |
| + conn = _SMC_open() |
| + if not conn: |
| + return None |
| + |
| + out = {} |
| + if _sensor_found_cache is None: |
| + _sensor_found_cache = set() |
| + # Populate the cache of the sensors found on this system, so that the next |
| + # call can only get the actual sensors. |
| + # Note: It is relatively fast to brute force all the possible names. |
| + for key, name in _sensor_names.iteritems(): |
| + value = _SMC_get_value(conn, key) |
| + if value is not None: |
| + _sensor_found_cache.add(key) |
| + out[name] = value |
| + return out |
| + |
| + 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.
|
| + value = _SMC_get_value(conn, key) |
| + if value is not None: |
| + out[_sensor_names[key]] = value |
| + return out |
| + |
| + |
| @tools.cached |
| def get_monitor_hidpi(): |
| """Returns True if the monitor is hidpi. |
| @@ -284,12 +586,13 @@ def get_monitor_hidpi(): |
| is_hidpi(card['spdisplays_ndrvs']) |
| for card in _get_system_profiler('SPDisplaysDataType') |
| if 'spdisplays_ndrvs' in card) |
| - return str(int(hidpi)) |
| + return unicode(int(hidpi)) |
| def get_xcode_versions(): |
| """Returns all Xcode versions installed.""" |
| - return sorted(xcode['version'] for xcode in get_xcode_state().itervalues()) |
| + return sorted( |
| + unicode(xcode['version']) for xcode in get_xcode_state().itervalues()) |
| @tools.cached |