Source code for functional_harness.x_server

#!/usr/bin/env python3
"""Wrapper for easily setting up and tearing down a test X server"""

__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__license__ = "MIT"

# Silence PyLint being flat-out wrong about MyPy type annotations
# complaining about my grouped imports
# pylint: disable=unsubscriptable-object,invalid-sequence-index
# pylint: disable=wrong-import-order

import logging, os, random, shutil, subprocess, tempfile  # nosec
from contextlib import contextmanager
from distutils.spawn import find_executable

from .env_general import env_vars

# -- Type-Annotation Imports --
from typing import Dict, Generator, List, Tuple

log = logging.getLogger(__name__)


[docs]def _init_x_server(argv: List[str], verbose: bool=False ) -> Tuple[subprocess.Popen, bytes]: """Wrapper for starting an X server with the given command line :param argv: The command-line to execute :param verbose: If :any:`False`, redirect the X server's ``stdout`` and ``stderr`` to :file:`/dev/null` :returns: The process object for the X server. :raises subprocess.CalledProcessError: The X server exited with an unexpected error. """ # Launch the X server read_pipe, write_pipe = os.pipe() argv += ['+xinerama', '-displayfd', str(write_pipe)] # pylint: disable=unexpected-keyword-arg,no-member if verbose: xproc = subprocess.Popen(argv, pass_fds=[write_pipe]) # nosec else: xproc = subprocess.Popen(argv, pass_fds=[write_pipe], # nosec stderr=subprocess.STDOUT, stdout=subprocess.DEVNULL) display = os.read(read_pipe, 128).strip() return xproc, display
[docs]@contextmanager def x_server(argv: List[str], screens: Dict[int, str] ) -> Generator[Dict[str, str], None, None]: """Context manager to launch and then clean up an X server. :param argv: The command to launch the test X server and any arguments not relating to defining the attached screens. :param screens: A :any:`dict <dict>` mapping screen numbers to ``WxHxDEPTH`` strings. (eg. ``{0: '1024x768x32'}``) :raises subprocess.CalledProcessError: The X server or :command:`xauth` failed unexpectedly. :raises FileNotFoundError: Could not find either the :command:`xauth` command or ``argv[0]``. :raises PermissionError: Somehow, we lack write permission inside a directory created by :func:`tempfile.mkdtemp`. :raises ValueError: ``argv[0]`` was not an X server binary we know how to specify monitor rectangles for. (either :command:`Xvfb` or :command:`Xephyr`) :raises UnicodeDecodeError: The X server's ``-displayfd`` option wrote a value to the given FD which could not be decoded as UTF-8 when it should have been part of the 7-bit ASCII subset of UTF-8. .. todo:: Either don't accept an arbitrary ``argv`` string as input to :func:`x_server` or default to a behaviour likely to work with other X servers rather than erroring out. """ # Check for missing requirements for cmd in ['xauth', argv[0]]: if not find_executable(cmd): # pylint: disable=undefined-variable raise FileNotFoundError( # NOQA "Cannot find required command {!r}".format(cmd)) x_server = None tempdir = tempfile.mkdtemp() try: # Because random.getrandbits gets interpreted as a variable length, # *ensure* we've got the right number of hex digits magic_cookie = b'' while len(magic_cookie) < 32: magic_cookie += hex(random.getrandbits(128))[2:34].encode('ascii') magic_cookie = magic_cookie[:32] assert len(magic_cookie) == 32, len(magic_cookie) # nosec xauthfile = os.path.join(tempdir, 'Xauthority') env = {'XAUTHORITY': xauthfile} open(xauthfile, 'w').close() # create empty file # Convert `screens` into the format Xorg servers expect screen_argv = [] for screen_num, screen_geom in screens.items(): if 'Xvfb' in argv[0]: screen_argv.extend(['-screen', '%d' % screen_num, screen_geom]) elif 'Xephyr' in argv[0]: screen_argv.extend(['-screen', screen_geom]) else: raise ValueError("Unrecognized X server. Cannot infer format " "for specifying screen geometry.") # Initialize an X server on a free display number x_server, display_num = _init_x_server(argv + screen_argv) # Set up the environment and authorization env['DISPLAY'] = ':%s' % display_num.decode('utf8') subprocess.check_call( # nosec ['xauth', 'add', env['DISPLAY'], '.', magic_cookie], env=env) # FIXME: This xauth call once had a random failure. Retry. with env_vars(env): yield env finally: if x_server: x_server.terminate() shutil.rmtree(tempdir)