Home | History | Annotate | Download | only in graphics
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """
      6 Provides graphics related utils, like capturing screenshots or checking on
      7 the state of the graphics driver.
      8 """
      9 
     10 import collections
     11 import glob
     12 import logging
     13 import os
     14 import re
     15 import sys
     16 import time
     17 #import traceback
     18 # Please limit the use of the uinput library to this file. Try not to spread
     19 # dependencies and abstract as much as possible to make switching to a different
     20 # input library in the future easier.
     21 import uinput
     22 
     23 from autotest_lib.client.bin import utils
     24 from autotest_lib.client.common_lib import error
     25 from autotest_lib.client.cros import power_utils
     26 from autotest_lib.client.cros.graphics import drm
     27 
     28 
     29 # TODO(ihf): Remove xcommand for non-freon builds.
     30 def xcommand(cmd, user=None):
     31     """
     32     Add the necessary X setup to a shell command that needs to connect to the X
     33     server.
     34     @param cmd: the command line string
     35     @param user: if not None su command to desired user.
     36     @return a modified command line string with necessary X setup
     37     """
     38     logging.warning('xcommand will be deprecated under freon!')
     39     #traceback.print_stack()
     40     if user is not None:
     41         cmd = 'su %s -c \'%s\'' % (user, cmd)
     42     if not utils.is_freon():
     43         cmd = 'DISPLAY=:0 XAUTHORITY=/home/chronos/.Xauthority ' + cmd
     44     return cmd
     45 
     46 # TODO(ihf): Remove xsystem for non-freon builds.
     47 def xsystem(cmd, user=None):
     48     """
     49     Run the command cmd, using utils.system, after adding the necessary
     50     setup to connect to the X server.
     51 
     52     @param cmd: The command.
     53     @param user: The user to switch to, or None for the current user.
     54     @param timeout: Optional timeout.
     55     @param ignore_status: Whether to check the return code of the command.
     56     """
     57     return utils.system(xcommand(cmd, user))
     58 
     59 
     60 # TODO(ihf): Remove XSET for non-freon builds.
     61 XSET = 'LD_LIBRARY_PATH=/usr/local/lib xset'
     62 
     63 def screen_disable_blanking():
     64     """ Called from power_Backlight to disable screen blanking. """
     65     if utils.is_freon():
     66         # We don't have to worry about unexpected screensavers or DPMS here.
     67         return
     68     xsystem(XSET + ' s off')
     69     xsystem(XSET + ' dpms 0 0 0')
     70     xsystem(XSET + ' -dpms')
     71 
     72 
     73 def screen_disable_energy_saving():
     74     """ Called from power_Consumption to immediately disable energy saving. """
     75     if utils.is_freon():
     76         # All we need to do here is enable displays via Chrome.
     77         power_utils.set_display_power(power_utils.DISPLAY_POWER_ALL_ON)
     78         return
     79     # Disable X screen saver
     80     xsystem(XSET + ' s 0 0')
     81     # Disable DPMS Standby/Suspend/Off
     82     xsystem(XSET + ' dpms 0 0 0')
     83     # Force monitor on
     84     screen_switch_on(on=1)
     85     # Save off X settings
     86     xsystem(XSET + ' q')
     87 
     88 
     89 def screen_switch_on(on):
     90     """Turn the touch screen on/off."""
     91     if on:
     92         xsystem(XSET + ' dpms force on')
     93     else:
     94         xsystem(XSET + ' dpms force off')
     95 
     96 
     97 def screen_toggle_fullscreen():
     98     """Toggles fullscreen mode."""
     99     if utils.is_freon():
    100         press_keys(['KEY_F11'])
    101     else:
    102         press_key_X('F11')
    103 
    104 
    105 def screen_toggle_mirrored():
    106     """Toggles the mirrored screen."""
    107     if utils.is_freon():
    108         press_keys(['KEY_LEFTCTRL', 'KEY_F4'])
    109     else:
    110         press_key_X('ctrl+F4')
    111 
    112 
    113 def hide_cursor():
    114     """Hides mouse cursor."""
    115     # Send a keystroke to hide the cursor.
    116     if utils.is_freon():
    117         press_keys(['KEY_UP'])
    118     else:
    119         press_key_X('Up')
    120 
    121 
    122 def hide_typing_cursor():
    123     """Hides typing cursor."""
    124     # Press the tab key to move outside the typing bar.
    125     if utils.is_freon():
    126         press_keys(['KEY_TAB'])
    127     else:
    128         press_key_X('Tab')
    129 
    130 
    131 def screen_wakeup():
    132     """Wake up the screen if it is dark."""
    133     # Move the mouse a little bit to wake up the screen.
    134     if utils.is_freon():
    135         device = _get_uinput_device_mouse_rel()
    136         _uinput_emit(device, 'REL_X', 1)
    137         _uinput_emit(device, 'REL_X', -1)
    138     else:
    139         xsystem('xdotool mousemove_relative 1 1')
    140 
    141 
    142 def switch_screen_on(on):
    143     """
    144     Turn the touch screen on/off.
    145 
    146     @param on: On or off.
    147     """
    148     if on:
    149         xsystem(XSET + ' dpms force on')
    150     else:
    151         xsystem(XSET + ' dpms force off')
    152 
    153 
    154 # Don't create a device during build_packages or for tests that don't need it.
    155 uinput_device_keyboard = None
    156 uinput_device_touch = None
    157 uinput_device_mouse_rel = None
    158 
    159 # Don't add more events to this list than are used. For a complete list of
    160 # available events check python2.7/site-packages/uinput/ev.py.
    161 UINPUT_DEVICE_EVENTS_KEYBOARD = [
    162     uinput.KEY_F4,
    163     uinput.KEY_F11,
    164     uinput.KEY_KPPLUS,
    165     uinput.KEY_KPMINUS,
    166     uinput.KEY_LEFTCTRL,
    167     uinput.KEY_TAB,
    168     uinput.KEY_UP,
    169     uinput.KEY_DOWN,
    170     uinput.KEY_LEFT,
    171     uinput.KEY_RIGHT
    172 ]
    173 # TODO(ihf): Find an ABS sequence that actually works.
    174 UINPUT_DEVICE_EVENTS_TOUCH = [
    175     uinput.BTN_TOUCH,
    176     uinput.ABS_MT_SLOT,
    177     uinput.ABS_MT_POSITION_X + (0, 2560, 0, 0),
    178     uinput.ABS_MT_POSITION_Y + (0, 1700, 0, 0),
    179     uinput.ABS_MT_TRACKING_ID + (0, 10, 0, 0),
    180     uinput.BTN_TOUCH
    181 ]
    182 UINPUT_DEVICE_EVENTS_MOUSE_REL = [
    183     uinput.REL_X,
    184     uinput.REL_Y,
    185     uinput.BTN_MOUSE,
    186     uinput.BTN_LEFT,
    187     uinput.BTN_RIGHT
    188 ]
    189 
    190 
    191 def _get_uinput_device_keyboard():
    192     """
    193     Lazy initialize device and return it. We don't want to create a device
    194     during build_packages or for tests that don't need it, hence init with None.
    195     """
    196     global uinput_device_keyboard
    197     if uinput_device_keyboard is None:
    198         uinput_device_keyboard = uinput.Device(UINPUT_DEVICE_EVENTS_KEYBOARD)
    199     return uinput_device_keyboard
    200 
    201 
    202 def _get_uinput_device_mouse_rel():
    203     """
    204     Lazy initialize device and return it. We don't want to create a device
    205     during build_packages or for tests that don't need it, hence init with None.
    206     """
    207     global uinput_device_mouse_rel
    208     if uinput_device_mouse_rel is None:
    209         uinput_device_mouse_rel = uinput.Device(UINPUT_DEVICE_EVENTS_MOUSE_REL)
    210     return uinput_device_mouse_rel
    211 
    212 
    213 def _get_uinput_device_touch():
    214     """
    215     Lazy initialize device and return it. We don't want to create a device
    216     during build_packages or for tests that don't need it, hence init with None.
    217     """
    218     global uinput_device_touch
    219     if uinput_device_touch is None:
    220         uinput_device_touch = uinput.Device(UINPUT_DEVICE_EVENTS_TOUCH)
    221     return uinput_device_touch
    222 
    223 
    224 def _uinput_translate_name(event_name):
    225     """
    226     Translates string |event_name| to uinput event.
    227     """
    228     return getattr(uinput, event_name)
    229 
    230 
    231 def _uinput_emit(device, event_name, value, syn=True):
    232     """
    233     Wrapper for uinput.emit. Emits event with value.
    234     Example: ('REL_X', 20), ('BTN_RIGHT', 1)
    235     """
    236     event = _uinput_translate_name(event_name)
    237     device.emit(event, value, syn)
    238 
    239 
    240 def _uinput_emit_click(device, event_name, syn=True):
    241     """
    242     Wrapper for uinput.emit_click. Emits click event. Only KEY and BTN events
    243     are accepted, otherwise ValueError is raised. Example: 'KEY_A'
    244     """
    245     event = _uinput_translate_name(event_name)
    246     device.emit_click(event, syn)
    247 
    248 
    249 def _uinput_emit_combo(device, event_names, syn=True):
    250     """
    251     Wrapper for uinput.emit_combo. Emits sequence of events.
    252     Example: ['KEY_LEFTCTRL', 'KEY_LEFTALT', 'KEY_F5']
    253     """
    254     events = [_uinput_translate_name(en) for en in event_names]
    255     device.emit_combo(events, syn)
    256 
    257 
    258 def press_keys(key_list):
    259     """Presses the given keys as one combination.
    260 
    261     Please do not leak uinput dependencies outside of the file.
    262 
    263     @param key: A list of key strings, e.g. ['LEFTCTRL', 'F4']
    264     """
    265     _uinput_emit_combo(_get_uinput_device_keyboard(), key_list)
    266 
    267 
    268 # TODO(ihf): Remove press_key_X for non-freon builds.
    269 def press_key_X(key_str):
    270     """Presses the given keys as one combination.
    271     @param key: A string of keys, e.g. 'ctrl+F4'.
    272     """
    273     if utils.is_freon():
    274         raise error.TestFail('freon: press_key_X not implemented')
    275     command = 'xdotool key %s' % key_str
    276     xsystem(command)
    277 
    278 
    279 def click_mouse():
    280     """Just click the mouse.
    281     Presumably only hacky tests use this function.
    282     """
    283     logging.info('click_mouse()')
    284     # Move a little to make the cursor appear.
    285     device = _get_uinput_device_mouse_rel()
    286     _uinput_emit(device, 'REL_X', 1)
    287     # Some sleeping is needed otherwise events disappear.
    288     time.sleep(0.1)
    289     # Move cursor back to not drift.
    290     _uinput_emit(device, 'REL_X', -1)
    291     time.sleep(0.1)
    292     # Click down.
    293     _uinput_emit(device, 'BTN_LEFT', 1)
    294     time.sleep(0.2)
    295     # Release click.
    296     _uinput_emit(device, 'BTN_LEFT', 0)
    297 
    298 
    299 # TODO(ihf): this function is broken. Make it work.
    300 def activate_focus_at(rel_x, rel_y):
    301     """Clicks with the mouse at screen position (x, y).
    302 
    303     This is a pretty hacky method. Using this will probably lead to
    304     flaky tests as page layout changes over time.
    305     @param rel_x: relative horizontal position between 0 and 1.
    306     @param rel_y: relattive vertical position between 0 and 1.
    307     """
    308     width, height = get_internal_resolution()
    309     device = _get_uinput_device_touch()
    310     _uinput_emit(device, 'ABS_MT_SLOT', 0, syn=False)
    311     _uinput_emit(device, 'ABS_MT_TRACKING_ID', 1, syn=False)
    312     _uinput_emit(device, 'ABS_MT_POSITION_X', int(rel_x * width), syn=False)
    313     _uinput_emit(device, 'ABS_MT_POSITION_Y', int(rel_y * height), syn=False)
    314     _uinput_emit(device, 'BTN_TOUCH', 1, syn=True)
    315     time.sleep(0.2)
    316     _uinput_emit(device, 'BTN_TOUCH', 0, syn=True)
    317 
    318 
    319 def take_screenshot(resultsdir, fname_prefix, extension='png'):
    320     """Take screenshot and save to a new file in the results dir.
    321     Args:
    322       @param resultsdir:   Directory to store the output in.
    323       @param fname_prefix: Prefix for the output fname.
    324       @param extension:    String indicating file format ('png', 'jpg', etc).
    325     Returns:
    326       the path of the saved screenshot file
    327     """
    328 
    329     old_exc_type = sys.exc_info()[0]
    330 
    331     next_index = len(glob.glob(
    332         os.path.join(resultsdir, '%s-*.%s' % (fname_prefix, extension))))
    333     screenshot_file = os.path.join(
    334         resultsdir, '%s-%d.%s' % (fname_prefix, next_index, extension))
    335     logging.info('Saving screenshot to %s.', screenshot_file)
    336 
    337     try:
    338         image = drm.crtcScreenshot()
    339         image.save(screenshot_file)
    340     except Exception as err:
    341         # Do not raise an exception if the screenshot fails while processing
    342         # another exception.
    343         if old_exc_type is None:
    344             raise
    345         logging.error(err)
    346 
    347     return screenshot_file
    348 
    349 
    350 def take_screenshot_crop_by_height(fullpath, final_height, x_offset_pixels,
    351                                    y_offset_pixels):
    352     """
    353     Take a screenshot, crop to final height starting at given (x, y) coordinate.
    354     Image width will be adjusted to maintain original aspect ratio).
    355 
    356     @param fullpath: path, fullpath of the file that will become the image file.
    357     @param final_height: integer, height in pixels of resulting image.
    358     @param x_offset_pixels: integer, number of pixels from left margin
    359                             to begin cropping.
    360     @param y_offset_pixels: integer, number of pixels from top margin
    361                             to begin cropping.
    362     """
    363     image = drm.crtcScreenshot()
    364     image.crop()
    365     width, height = image.size
    366     # Preserve aspect ratio: Wf / Wi == Hf / Hi
    367     final_width = int(width * (float(final_height) / height))
    368     box = (x_offset_pixels, y_offset_pixels,
    369            x_offset_pixels + final_width, y_offset_pixels + final_height)
    370     cropped = image.crop(box)
    371     cropped.save(fullpath)
    372     return fullpath
    373 
    374 
    375 def take_screenshot_crop_x(fullpath, box=None):
    376     """
    377     Take a screenshot using import tool, crop according to dim given by the box.
    378     @param fullpath: path, full path to save the image to.
    379     @param box: 4-tuple giving the upper left and lower right pixel coordinates.
    380     """
    381 
    382     if box:
    383         img_w, img_h, upperx, uppery = box
    384         cmd = ('/usr/local/bin/import -window root -depth 8 -crop '
    385                       '%dx%d+%d+%d' % (img_w, img_h, upperx, uppery))
    386     else:
    387         cmd = ('/usr/local/bin/import -window root -depth 8')
    388 
    389     old_exc_type = sys.exc_info()[0]
    390     try:
    391         xsystem('%s %s' % (cmd, fullpath))
    392     except Exception as err:
    393         # Do not raise an exception if the screenshot fails while processing
    394         # another exception.
    395         if old_exc_type is None:
    396             raise
    397         logging.error(err)
    398 
    399 
    400 def take_screenshot_crop(fullpath, box=None, crtc_id=None):
    401     """
    402     Take a screenshot using import tool, crop according to dim given by the box.
    403     @param fullpath: path, full path to save the image to.
    404     @param box: 4-tuple giving the upper left and lower right pixel coordinates.
    405     """
    406     if not utils.is_freon():
    407         return take_screenshot_crop_x(fullpath, box)
    408     if crtc_id is not None:
    409         image = drm.crtcScreenshot(crtc_id)
    410     else:
    411         image = drm.crtcScreenshot(get_internal_crtc())
    412     if box:
    413         image = image.crop(box)
    414     image.save(fullpath)
    415     return fullpath
    416 
    417 
    418 _MODETEST_CONNECTOR_PATTERN = re.compile(
    419     r'^(\d+)\s+\d+\s+(connected|disconnected)\s+(\S+)\s+\d+x\d+\s+\d+\s+\d+')
    420 
    421 _MODETEST_MODE_PATTERN = re.compile(
    422     r'\s+.+\d+\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+\d+\s+\d+\s+\d+\s+flags:.+type:'
    423     r' preferred')
    424 
    425 _MODETEST_CRTCS_START_PATTERN = re.compile(r'^id\s+fb\s+pos\s+size')
    426 
    427 _MODETEST_CRTC_PATTERN = re.compile(
    428     r'^(\d+)\s+(\d+)\s+\((\d+),(\d+)\)\s+\((\d+)x(\d+)\)')
    429 
    430 Connector = collections.namedtuple(
    431     'Connector', [
    432         'cid',  # connector id (integer)
    433         'ctype',  # connector type, e.g. 'eDP', 'HDMI-A', 'DP'
    434         'connected',  # boolean
    435         'size',  # current screen size, e.g. (1024, 768)
    436         'encoder',  # encoder id (integer)
    437         # list of resolution tuples, e.g. [(1920,1080), (1600,900), ...]
    438         'modes',
    439     ])
    440 
    441 CRTC = collections.namedtuple(
    442     'CRTC', [
    443         'id',  # crtc id
    444         'fb',  # fb id
    445         'pos',  # position, e.g. (0,0)
    446         'size',  # size, e.g. (1366,768)
    447     ])
    448 
    449 
    450 def get_display_resolution():
    451     """
    452     Parses output of modetest to determine the display resolution of the dut.
    453     @return: tuple, (w,h) resolution of device under test.
    454     """
    455     if not utils.is_freon():
    456         return _get_display_resolution_x()
    457 
    458     connectors = get_modetest_connectors()
    459     for connector in connectors:
    460         if connector.connected:
    461             return connector.size
    462     return None
    463 
    464 
    465 def _get_display_resolution_x():
    466     """
    467     Used temporarily while Daisy's modetest isn't working
    468     TODO(dhaddock): remove when no longer needed
    469     @return: tuple, (w,h) resolution of device under test.
    470     """
    471     env_vars = 'DISPLAY=:0.0 ' \
    472                               'XAUTHORITY=/home/chronos/.Xauthority'
    473     cmd = '%s xrandr | egrep -o "current [0-9]* x [0-9]*"' % env_vars
    474     output = utils.system_output(cmd)
    475     match = re.search(r'(\d+) x (\d+)', output)
    476     if len(match.groups()) == 2:
    477         return int(match.group(1)), int(match.group(2))
    478     return None
    479 
    480 
    481 def _get_num_outputs_connected():
    482     """
    483     Parses output of modetest to determine the number of connected displays
    484     @return: The number of connected displays
    485     """
    486     connected = 0
    487     connectors = get_modetest_connectors()
    488     for connector in connectors:
    489         if connector.connected:
    490             connected = connected + 1
    491 
    492     return connected
    493 
    494 
    495 def get_num_outputs_on():
    496     """
    497     Retrieves the number of connected outputs that are on.
    498 
    499     Return value: integer value of number of connected outputs that are on.
    500     """
    501 
    502     return _get_num_outputs_connected()
    503 
    504 
    505 def call_xrandr(args_string=''):
    506     """
    507     Calls xrandr with the args given by args_string.
    508 
    509     e.g. call_xrandr('--output LVDS1 --off') will invoke:
    510         'xrandr --output LVDS1 --off'
    511 
    512     @param args_string: A single string containing all arguments.
    513 
    514     Return value: Output of xrandr
    515     """
    516     return utils.system_output(xcommand('xrandr %s' % args_string))
    517 
    518 
    519 def get_modetest_connectors():
    520     """
    521     Retrieves a list of Connectors using modetest.
    522 
    523     Return value: List of Connectors.
    524     """
    525     connectors = []
    526     modetest_output = utils.system_output('modetest -c')
    527     for line in modetest_output.splitlines():
    528         # First search for a new connector.
    529         connector_match = re.match(_MODETEST_CONNECTOR_PATTERN, line)
    530         if connector_match is not None:
    531             cid = int(connector_match.group(1))
    532             connected = False
    533             if connector_match.group(2) == 'connected':
    534                 connected = True
    535             ctype = connector_match.group(3)
    536             size = (-1, -1)
    537             encoder = -1
    538             modes = None
    539             connectors.append(
    540                 Connector(cid, ctype, connected, size, encoder, modes))
    541         else:
    542             # See if we find corresponding line with modes, sizes etc.
    543             mode_match = re.match(_MODETEST_MODE_PATTERN, line)
    544             if mode_match is not None:
    545                 size = (int(mode_match.group(1)), int(mode_match.group(2)))
    546                 # Update display size of last connector in list.
    547                 c = connectors.pop()
    548                 connectors.append(
    549                     Connector(
    550                         c.cid, c.ctype, c.connected, size, c.encoder,
    551                         c.modes))
    552     return connectors
    553 
    554 
    555 def get_modetest_crtcs():
    556     """
    557     Returns a list of CRTC data.
    558 
    559     Sample:
    560         [CRTC(id=19, fb=50, pos=(0, 0), size=(1366, 768)),
    561          CRTC(id=22, fb=54, pos=(0, 0), size=(1920, 1080))]
    562     """
    563     crtcs = []
    564     modetest_output = utils.system_output('modetest -p')
    565     found = False
    566     for line in modetest_output.splitlines():
    567         if found:
    568             crtc_match = re.match(_MODETEST_CRTC_PATTERN, line)
    569             if crtc_match is not None:
    570                 crtc_id = int(crtc_match.group(1))
    571                 fb = int(crtc_match.group(2))
    572                 x = int(crtc_match.group(3))
    573                 y = int(crtc_match.group(4))
    574                 width = int(crtc_match.group(5))
    575                 height = int(crtc_match.group(6))
    576                 # CRTCs with fb=0 are disabled, but lets skip anything with
    577                 # trivial width/height just in case.
    578                 if not (fb == 0 or width == 0 or height == 0):
    579                     crtcs.append(CRTC(crtc_id, fb, (x, y), (width, height)))
    580             elif line and not line[0].isspace():
    581                 return crtcs
    582         if re.match(_MODETEST_CRTCS_START_PATTERN, line) is not None:
    583             found = True
    584     return crtcs
    585 
    586 
    587 def get_modetest_output_state():
    588     """
    589     Reduce the output of get_modetest_connectors to a dictionary of connector/active states.
    590     """
    591     connectors = get_modetest_connectors()
    592     outputs = {}
    593     for connector in connectors:
    594         # TODO(ihf): Figure out why modetest output needs filtering.
    595         if connector.connected:
    596             outputs[connector.ctype] = connector.connected
    597     return outputs
    598 
    599 
    600 def get_output_rect(output):
    601     """Gets the size and position of the given output on the screen buffer.
    602 
    603     @param output: The output name as a string.
    604 
    605     @return A tuple of the rectangle (width, height, fb_offset_x,
    606             fb_offset_y) of ints.
    607     """
    608     connectors = get_modetest_connectors()
    609     for connector in connectors:
    610         if connector.ctype == output:
    611             # Concatenate two 2-tuples to 4-tuple.
    612             return connector.size + (0, 0)  # TODO(ihf): Should we use CRTC.pos?
    613     return (0, 0, 0, 0)
    614 
    615 
    616 def get_internal_resolution():
    617     if utils.is_freon():
    618         if has_internal_display():
    619             crtcs = get_modetest_crtcs()
    620             if len(crtcs) > 0:
    621                 return crtcs[0].size
    622         return (-1, -1)
    623     else:
    624         connector = get_internal_connector_name()
    625         width, height, _, _ = get_output_rect_x(connector)
    626         return (width, height)
    627 
    628 
    629 def has_internal_display():
    630     """Checks whether the DUT is equipped with an internal display.
    631 
    632     @return True if internal display is present; False otherwise.
    633     """
    634     return bool(get_internal_connector_name())
    635 
    636 
    637 def get_external_resolution():
    638     """Gets the resolution of the external display.
    639 
    640     @return A tuple of (width, height) or None if no external display is
    641             connected.
    642     """
    643     if utils.is_freon():
    644         offset = 1 if has_internal_display() else 0
    645         crtcs = get_modetest_crtcs()
    646         if len(crtcs) > offset and crtcs[offset].size != (0, 0):
    647             return crtcs[offset].size
    648         return None
    649     else:
    650         connector = get_external_connector_name()
    651         width, height, _, _ = get_output_rect_x(connector)
    652         if width == 0 and height == 0:
    653             return None
    654         return (width, height)
    655 
    656 
    657 def get_output_rect_x(output):
    658     """Gets the size and position of the given output on the screen buffer.
    659 
    660     @param output: The output name as a string.
    661 
    662     @return A tuple of the rectangle (width, height, fb_offset_x,
    663             fb_offset_y) of ints.
    664     """
    665     regexp = re.compile(
    666             r'^([-A-Za-z0-9]+)\s+connected\s+(\d+)x(\d+)\+(\d+)\+(\d+)',
    667             re.M)
    668     match = regexp.findall(call_xrandr())
    669     for m in match:
    670         if m[0] == output:
    671             return (int(m[1]), int(m[2]), int(m[3]), int(m[4]))
    672     return (0, 0, 0, 0)
    673 
    674 
    675 def get_display_output_state():
    676     """
    677     Retrieves output status of connected display(s).
    678 
    679     Return value: dictionary of connected display states.
    680     """
    681     if utils.is_freon():
    682         return get_modetest_output_state()
    683     else:
    684         return get_xrandr_output_state()
    685 
    686 
    687 def get_xrandr_output_state():
    688     """
    689     Retrieves output status of connected display(s) using xrandr.
    690 
    691     When xrandr report a display is "connected", it doesn't mean the
    692     display is active. For active display, it will have '*' after display mode.
    693 
    694     Return value: dictionary of connected display states.
    695                   key = output name
    696                   value = True if the display is active; False otherwise.
    697     """
    698     output = call_xrandr().split('\n')
    699     xrandr_outputs = {}
    700     current_output_name = ''
    701 
    702     # Parse output of xrandr, line by line.
    703     for line in output:
    704         if line.startswith('Screen'):
    705             continue
    706         # If the line contains "connected", it is a connected display, as
    707         # opposed to a disconnected output.
    708         if line.find(' connected') != -1:
    709             current_output_name = line.split()[0]
    710             # Temporarily mark it as inactive until we see a '*' afterward.
    711             xrandr_outputs[current_output_name] = False
    712             continue
    713 
    714         # If "connected" was not found, this is a line that shows a display
    715         # mode, e.g:    1920x1080      50.0     60.0     24.0
    716         # Check if this has an asterisk indicating it's on.
    717         if line.find('*') != -1 and current_output_name:
    718             xrandr_outputs[current_output_name] = True
    719             # Reset the output name since this should not be set more than once.
    720             current_output_name = ''
    721 
    722     return xrandr_outputs
    723 
    724 
    725 def set_xrandr_output(output_name, enable):
    726     """
    727     Sets the output given by |output_name| on or off.
    728 
    729     Parameters:
    730         output_name       name of output, e.g. 'HDMI1', 'LVDS1', 'DP1'
    731         enable            True or False, indicating whether to turn on or off
    732     """
    733     call_xrandr('--output %s --%s' % (output_name, 'auto' if enable else 'off'))
    734 
    735 
    736 def set_modetest_output(output_name, enable):
    737     # TODO(ihf): figure out what to do here. Don't think this is the right command.
    738     # modetest -s <connector_id>[,<connector_id>][@<crtc_id>]:<mode>[-<vrefresh>][@<format>]  set a mode
    739     pass
    740 
    741 
    742 def set_display_output(output_name, enable):
    743     """
    744     Sets the output given by |output_name| on or off.
    745     """
    746     set_modetest_output(output_name, enable)
    747 
    748 
    749 # TODO(ihf): Fix this for multiple external connectors.
    750 def get_external_crtc(index=0):
    751     offset = 1 if has_internal_display() else 0
    752     crtcs = get_modetest_crtcs()
    753     if len(crtcs) > offset + index:
    754         return crtcs[offset + index].id
    755     return -1
    756 
    757 
    758 def get_internal_crtc():
    759     if has_internal_display():
    760         crtcs = get_modetest_crtcs()
    761         if len(crtcs) > 0:
    762             return crtcs[0].id
    763     return -1
    764 
    765 
    766 # TODO(ihf): Fix this for multiple external connectors.
    767 def get_external_connector_name():
    768     """Gets the name of the external output connector.
    769 
    770     @return The external output connector name as a string, if any.
    771             Otherwise, return False.
    772     """
    773     outputs = get_display_output_state()
    774     for output in outputs.iterkeys():
    775         if outputs[output] and (output.startswith('HDMI')
    776                 or output.startswith('DP')
    777                 or output.startswith('DVI')
    778                 or output.startswith('VGA')):
    779             return output
    780     return False
    781 
    782 
    783 def get_internal_connector_name():
    784     """Gets the name of the internal output connector.
    785 
    786     @return The internal output connector name as a string, if any.
    787             Otherwise, return False.
    788     """
    789     outputs = get_display_output_state()
    790     for output in outputs.iterkeys():
    791         # reference: chromium_org/chromeos/display/output_util.cc
    792         if (output.startswith('eDP')
    793                 or output.startswith('LVDS')
    794                 or output.startswith('DSI')):
    795             return output
    796     return False
    797 
    798 
    799 def wait_output_connected(output):
    800     """Wait for output to connect.
    801 
    802     @param output: The output name as a string.
    803 
    804     @return: True if output is connected; False otherwise.
    805     """
    806     def _is_connected(output):
    807         """Helper function."""
    808         outputs = get_display_output_state()
    809         if output not in outputs:
    810             return False
    811         return outputs[output]
    812 
    813     return utils.wait_for_value(lambda: _is_connected(output),
    814                                 expected_value=True)
    815 
    816 
    817 def set_content_protection(output_name, state):
    818     """
    819     Sets the content protection to the given state.
    820 
    821     @param output_name: The output name as a string.
    822     @param state: One of the states 'Undesired', 'Desired', or 'Enabled'
    823 
    824     """
    825     if utils.is_freon():
    826         raise error.TestFail('freon: set_content_protection not implemented')
    827     call_xrandr('--output %s --set "Content Protection" %s' %
    828                 (output_name, state))
    829 
    830 
    831 def get_content_protection(output_name):
    832     """
    833     Gets the state of the content protection.
    834 
    835     @param output_name: The output name as a string.
    836     @return: A string of the state, like 'Undesired', 'Desired', or 'Enabled'.
    837              False if not supported.
    838 
    839     """
    840     if utils.is_freon():
    841         raise error.TestFail('freon: get_content_protection not implemented')
    842 
    843     output = call_xrandr('--verbose').split('\n')
    844     current_output_name = ''
    845 
    846     # Parse output of xrandr, line by line.
    847     for line in output:
    848         # If the line contains 'connected', it is a connected display.
    849         if line.find(' connected') != -1:
    850             current_output_name = line.split()[0]
    851             continue
    852         if current_output_name != output_name:
    853             continue
    854         # Search the line like: 'Content Protection:     Undesired'
    855         match = re.search(r'Content Protection:\t(\w+)', line)
    856         if match:
    857             return match.group(1)
    858 
    859     return False
    860 
    861 
    862 def is_sw_rasterizer():
    863     """Return true if OpenGL is using a software rendering."""
    864     cmd = utils.wflinfo_cmd() + ' | grep "OpenGL renderer string"'
    865     output = utils.run(cmd)
    866     result = output.stdout.splitlines()[0]
    867     logging.info('wflinfo: %s', result)
    868     # TODO(ihf): Find exhaustive error conditions (especially ARM).
    869     return 'llvmpipe' in result.lower() or 'soft' in result.lower()
    870 
    871 
    872 def get_gles_version():
    873     cmd = utils.wflinfo_cmd()
    874     wflinfo = utils.system_output(cmd, retain_output=False, ignore_status=False)
    875     # OpenGL version string: OpenGL ES 3.0 Mesa 10.5.0-devel
    876     version = re.findall(r'OpenGL version string: '
    877                          r'OpenGL ES ([0-9]+).([0-9]+)', wflinfo)
    878     if version:
    879         version_major = int(version[0][0])
    880         version_minor = int(version[0][1])
    881         return (version_major, version_minor)
    882     return (None, None)
    883 
    884 
    885 def get_egl_version():
    886     cmd = 'eglinfo'
    887     eglinfo = utils.system_output(cmd, retain_output=False, ignore_status=False)
    888     # EGL version string: 1.4 (DRI2)
    889     version = re.findall(r'EGL version string: ([0-9]+).([0-9]+)', eglinfo)
    890     if version:
    891         version_major = int(version[0][0])
    892         version_minor = int(version[0][1])
    893         return (version_major, version_minor)
    894     return (None, None)
    895 
    896 
    897 class GraphicsKernelMemory(object):
    898     """
    899     Reads from sysfs to determine kernel gem objects and memory info.
    900     """
    901     # These are sysfs fields that will be read by this test.  For different
    902     # architectures, the sysfs field paths are different.  The "paths" are given
    903     # as lists of strings because the actual path may vary depending on the
    904     # system.  This test will read from the first sysfs path in the list that is
    905     # present.
    906     # e.g. ".../memory" vs ".../gpu_memory" -- if the system has either one of
    907     # these, the test will read from that path.
    908     amdgpu_fields = {
    909         'gem_objects': ['/sys/kernel/debug/dri/0/amdgpu_gem_info'],
    910         'memory': ['/sys/kernel/debug/dri/0/amdgpu_gtt_mm'],
    911     }
    912     arm_fields = {}
    913     exynos_fields = {
    914         'gem_objects': ['/sys/kernel/debug/dri/?/exynos_gem_objects'],
    915         'memory': ['/sys/class/misc/mali0/device/memory',
    916                    '/sys/class/misc/mali0/device/gpu_memory'],
    917     }
    918     mediatek_fields = {}  # TODO(crosbug.com/p/58189) add nodes
    919     # TODO Add memory nodes once the GPU patches landed.
    920     rockchip_fields = {}
    921     tegra_fields = {
    922         'memory': ['/sys/kernel/debug/memblock/memory'],
    923     }
    924     i915_fields = {
    925         'gem_objects': ['/sys/kernel/debug/dri/0/i915_gem_objects'],
    926         'memory': ['/sys/kernel/debug/dri/0/i915_gem_gtt'],
    927     }
    928 
    929     arch_fields = {
    930         'amdgpu': amdgpu_fields,
    931         'arm': arm_fields,
    932         'exynos5': exynos_fields,
    933         'i915': i915_fields,
    934         'mediatek': mediatek_fields,
    935         'rockchip': rockchip_fields,
    936         'tegra': tegra_fields,
    937     }
    938 
    939     num_errors = 0
    940 
    941     def get_memory_keyvals(self):
    942         """
    943         Reads the graphics memory values and returns them as keyvals.
    944         """
    945         keyvals = {}
    946 
    947         # Get architecture type and list of sysfs fields to read.
    948         soc = utils.get_cpu_soc_family()
    949 
    950         arch = utils.get_cpu_arch()
    951         if arch == 'x86_64' or arch == 'i386':
    952             pci_vga_device = utils.run("lspci | grep VGA").stdout.rstrip('\n')
    953             if "Advanced Micro Devices" in pci_vga_device:
    954                 soc = 'amdgpu'
    955             elif "Intel Corporation" in pci_vga_device:
    956                 soc = 'i915'
    957 
    958         if not soc in self.arch_fields:
    959             raise error.TestFail('Error: Architecture "%s" not yet supported.' % soc)
    960         fields = self.arch_fields[soc]
    961 
    962         for field_name in fields:
    963             possible_field_paths = fields[field_name]
    964             field_value = None
    965             for path in possible_field_paths:
    966                 if utils.system('ls %s' % path):
    967                     continue
    968                 field_value = utils.system_output('cat %s' % path)
    969                 break
    970 
    971             if not field_value:
    972                 logging.error('Unable to find any sysfs paths for field "%s"',
    973                               field_name)
    974                 self.num_errors += 1
    975                 continue
    976 
    977             parsed_results = GraphicsKernelMemory._parse_sysfs(field_value)
    978 
    979             for key in parsed_results:
    980                 keyvals['%s_%s' % (field_name, key)] = parsed_results[key]
    981 
    982             if 'bytes' in parsed_results and parsed_results['bytes'] == 0:
    983                 logging.error('%s reported 0 bytes', field_name)
    984                 self.num_errors += 1
    985 
    986         keyvals['meminfo_MemUsed'] = (utils.read_from_meminfo('MemTotal') -
    987                                       utils.read_from_meminfo('MemFree'))
    988         keyvals['meminfo_SwapUsed'] = (utils.read_from_meminfo('SwapTotal') -
    989                                        utils.read_from_meminfo('SwapFree'))
    990         return keyvals
    991 
    992     @staticmethod
    993     def _parse_sysfs(output):
    994         """
    995         Parses output of graphics memory sysfs to determine the number of
    996         buffer objects and bytes.
    997 
    998         Arguments:
    999             output      Unprocessed sysfs output
   1000         Return value:
   1001             Dictionary containing integer values of number bytes and objects.
   1002             They may have the keys 'bytes' and 'objects', respectively.  However
   1003             the result may not contain both of these values.
   1004         """
   1005         results = {}
   1006         labels = ['bytes', 'objects']
   1007 
   1008         for line in output.split('\n'):
   1009             # Strip any commas to make parsing easier.
   1010             line_words = line.replace(',', '').split()
   1011 
   1012             prev_word = None
   1013             for word in line_words:
   1014                 # When a label has been found, the previous word should be the
   1015                 # value. e.g. "3200 bytes"
   1016                 if word in labels and word not in results and prev_word:
   1017                     logging.info(prev_word)
   1018                     results[word] = int(prev_word)
   1019 
   1020                 prev_word = word
   1021 
   1022             # Once all values has been parsed, return.
   1023             if len(results) == len(labels):
   1024                 return results
   1025 
   1026         return results
   1027 
   1028 
   1029 class GraphicsStateChecker(object):
   1030     """
   1031     Analyzes the state of the GPU and log history. Should be instantiated at the
   1032     beginning of each graphics_* test.
   1033     """
   1034     crash_blacklist = []
   1035     dirty_writeback_centisecs = 0
   1036     existing_hangs = {}
   1037 
   1038     _BROWSER_VERSION_COMMAND = '/opt/google/chrome/chrome --version'
   1039     _HANGCHECK = ['drm:i915_hangcheck_elapsed', 'drm:i915_hangcheck_hung',
   1040                   'Hangcheck timer elapsed...']
   1041     _HANGCHECK_WARNING = ['render ring idle']
   1042     _MESSAGES_FILE = '/var/log/messages'
   1043 
   1044     def __init__(self, raise_error_on_hang=True):
   1045         """
   1046         Analyzes the initial state of the GPU and log history.
   1047         """
   1048         # Attempt flushing system logs every second instead of every 10 minutes.
   1049         self.dirty_writeback_centisecs = utils.get_dirty_writeback_centisecs()
   1050         utils.set_dirty_writeback_centisecs(100)
   1051         self._raise_error_on_hang = raise_error_on_hang
   1052         logging.info(utils.get_board_with_frequency_and_memory())
   1053         self.graphics_kernel_memory = GraphicsKernelMemory()
   1054 
   1055         if utils.get_cpu_arch() != 'arm':
   1056             if is_sw_rasterizer():
   1057                 raise error.TestFail('Refusing to run on SW rasterizer.')
   1058             logging.info('Initialize: Checking for old GPU hangs...')
   1059             messages = open(self._MESSAGES_FILE, 'r')
   1060             for line in messages:
   1061                 for hang in self._HANGCHECK:
   1062                     if hang in line:
   1063                         logging.info(line)
   1064                         self.existing_hangs[line] = line
   1065             messages.close()
   1066 
   1067     def finalize(self):
   1068         """
   1069         Analyzes the state of the GPU, log history and emits warnings or errors
   1070         if the state changed since initialize. Also makes a note of the Chrome
   1071         version for later usage in the perf-dashboard.
   1072         """
   1073         utils.set_dirty_writeback_centisecs(self.dirty_writeback_centisecs)
   1074         new_gpu_hang = False
   1075         new_gpu_warning = False
   1076         if utils.get_cpu_arch() != 'arm':
   1077             logging.info('Cleanup: Checking for new GPU hangs...')
   1078             messages = open(self._MESSAGES_FILE, 'r')
   1079             for line in messages:
   1080                 for hang in self._HANGCHECK:
   1081                     if hang in line:
   1082                         if not line in self.existing_hangs.keys():
   1083                             logging.info(line)
   1084                             for warn in self._HANGCHECK_WARNING:
   1085                                 if warn in line:
   1086                                     new_gpu_warning = True
   1087                                     logging.warning(
   1088                                         'Saw GPU hang warning during test.')
   1089                                 else:
   1090                                     logging.warning('Saw GPU hang during test.')
   1091                                     new_gpu_hang = True
   1092             messages.close()
   1093 
   1094             if is_sw_rasterizer():
   1095                 logging.warning('Finished test on SW rasterizer.')
   1096                 raise error.TestFail('Finished test on SW rasterizer.')
   1097             if self._raise_error_on_hang and new_gpu_hang:
   1098                 raise error.TestError('Detected GPU hang during test.')
   1099             if new_gpu_hang:
   1100                 raise error.TestWarn('Detected GPU hang during test.')
   1101             if new_gpu_warning:
   1102                 raise error.TestWarn('Detected GPU warning during test.')
   1103 
   1104 
   1105     def get_memory_access_errors(self):
   1106         """ Returns the number of errors while reading memory stats. """
   1107         return self.graphics_kernel_memory.num_errors
   1108 
   1109     def get_memory_keyvals(self):
   1110         """ Returns memory stats. """
   1111         return self.graphics_kernel_memory.get_memory_keyvals()
   1112