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"""
6Provides graphics related utils, like capturing screenshots or checking on
7the state of the graphics driver.
8"""
9
10import collections
11import glob
12import logging
13import os
14import re
15import sys
16import 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.
21import uinput
22
23from autotest_lib.client.bin import utils
24from autotest_lib.client.common_lib import error
25from autotest_lib.client.cros import power_utils
26from autotest_lib.client.cros.graphics import drm
27
28
29# TODO(ihf): Remove xcommand for non-freon builds.
30def 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.
47def 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.
61XSET = 'LD_LIBRARY_PATH=/usr/local/lib xset'
62
63def 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
73def 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
89def 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
97def 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
105def 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
113def 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
122def 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
131def 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
142def 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.
155uinput_device_keyboard = None
156uinput_device_touch = None
157uinput_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.
161UINPUT_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.
174UINPUT_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]
182UINPUT_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
191def _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
202def _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
213def _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
224def _uinput_translate_name(event_name):
225    """
226    Translates string |event_name| to uinput event.
227    """
228    return getattr(uinput, event_name)
229
230
231def _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
240def _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
249def _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
258def 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.
269def 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
279def 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.
300def 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
319def 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
350def 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
375def 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
400def 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
430Connector = 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
441CRTC = 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
450def 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
465def _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
481def _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
495def 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
505def 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
519def 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
555def 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
587def 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
600def 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
616def 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
629def 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
637def 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
657def 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
675def 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
687def 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
725def 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
736def 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
742def 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.
750def 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
758def 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.
767def 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
783def 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
799def 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
817def 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
831def 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
862def 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
872def 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
885def 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
897class 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
1029class 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