/pyvirtualdisplay/abstractdisplay.py
http://github.com/ponty/PyVirtualDisplay · Python · 395 lines · 276 code · 65 blank · 54 comment · 63 complexity · 79bd23b08f505ac126aa5f14b1caf221 MD5 · raw file
- import fnmatch
- import logging
- import os
- import select
- import signal
- import subprocess
- import tempfile
- import time
- from threading import Lock
- from easyprocess import EasyProcess, EasyProcessError
- from pyvirtualdisplay import xauth
- from pyvirtualdisplay.util import get_helptext, platform_is_osx
- log = logging.getLogger(__name__)
- # try:
- # import fcntl
- # except ImportError:
- # fcntl = None
- _mutex = Lock()
- _mutex_popen = Lock()
- _MIN_DISPLAY_NR = 1000
- _USED_DISPLAY_NR_LIST = []
- _X_START_TIMEOUT = 10
- _X_START_TIME_STEP = 0.1
- _X_START_WAIT = 0.1
- class XStartTimeoutError(Exception):
- pass
- class XStartError(Exception):
- pass
- def _lock_files():
- tmpdir = "/tmp"
- try:
- ls = os.listdir(tmpdir)
- except FileNotFoundError:
- log.warning("missing /tmp")
- return []
- pattern = ".X*-lock"
- names = fnmatch.filter(ls, pattern)
- ls = [os.path.join(tmpdir, child) for child in names]
- ls = [p for p in ls if os.path.isfile(p)]
- return ls
- def _search_for_display():
- # search for free display
- ls = list(map(lambda x: int(x.split("X")[1].split("-")[0]), _lock_files()))
- if len(ls):
- display = max(_MIN_DISPLAY_NR, max(ls) + 3)
- else:
- display = _MIN_DISPLAY_NR
- return display
- class AbstractDisplay(object):
- """
- Common parent for X servers (Xvfb,Xephyr,Xvnc)
- """
- def __init__(self, program, use_xauth, retries, extra_args, manage_global_env):
- self._extra_args = extra_args
- self._retries = retries
- self._program = program
- self.stdout = None
- self.stderr = None
- self.old_display_var = None
- self._subproc = None
- self.display = None
- self._is_started = False
- self._manage_global_env = manage_global_env
- self._reset_global_env = False
- self._pipe_wfd = None
- self._retries_current = 0
- helptext = get_helptext(program)
- self._has_displayfd = "-displayfd" in helptext
- if not self._has_displayfd:
- log.debug("-displayfd flag is missing.")
- PYVIRTUALDISPLAY_DISPLAYFD = os.environ.get("PYVIRTUALDISPLAY_DISPLAYFD")
- if PYVIRTUALDISPLAY_DISPLAYFD:
- log.debug("PYVIRTUALDISPLAY_DISPLAYFD=%s", PYVIRTUALDISPLAY_DISPLAYFD)
- # '0'->false, '1'->true
- self._has_displayfd = bool(int(PYVIRTUALDISPLAY_DISPLAYFD))
- else:
- # TODO: macos: displayfd is available on XQuartz-2.7.11 but it doesn't work, always 0 is returned
- if platform_is_osx():
- self._has_displayfd = False
- self._check_flags(helptext)
- if use_xauth and not xauth.is_installed():
- raise xauth.NotFoundError()
- self._use_xauth = use_xauth
- self._old_xauth = None
- self._xauth_filename = None
- def _check_flags(self, helptext):
- pass
- def _cmd(self):
- raise NotImplementedError()
- def _redirect_display(self, on):
- """
- on:
- * True -> set $DISPLAY to virtual screen
- * False -> set $DISPLAY to original screen
- :param on: bool
- """
- d = self.new_display_var if on else self.old_display_var
- if d is None:
- log.debug("unset $DISPLAY")
- try:
- del os.environ["DISPLAY"]
- except KeyError:
- log.warning("$DISPLAY was already unset.")
- else:
- log.debug("set $DISPLAY=%s", d)
- os.environ["DISPLAY"] = d
- def _env(self):
- env = os.environ.copy()
- env["DISPLAY"] = self.new_display_var
- return env
- def start(self):
- """
- start display
- :rtype: self
- """
- if self._is_started:
- raise XStartError(self, "Display was started twice.")
- self._is_started = True
- if self._has_displayfd:
- self._start1_has_displayfd()
- else:
- i = 0
- while True:
- self._retries_current = i + 1
- try:
- self._start1()
- break
- except XStartError:
- log.warning("start failed %s", i + 1)
- time.sleep(0.05)
- i += 1
- if i >= self._retries:
- raise XStartError(
- "No success after %s retries. Last stderr: %s"
- % (self._retries, self.stderr)
- )
- if self._manage_global_env:
- self._redirect_display(True)
- self._reset_global_env = True
- def _popen(self, use_pass_fds):
- with _mutex_popen:
- if use_pass_fds:
- self._subproc = subprocess.Popen(
- self._command,
- pass_fds=[self._pipe_wfd],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- shell=False,
- )
- else:
- self._subproc = subprocess.Popen(
- self._command,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- shell=False,
- )
- def _start1_has_displayfd(self):
- # stdout doesn't work on osx -> create own pipe
- rfd, self._pipe_wfd = os.pipe()
- self._command = self._cmd() + self._extra_args
- log.debug("command: %s", self._command)
- self._popen(use_pass_fds=True)
- self.display = int(self._wait_for_pipe_text(rfd))
- os.close(rfd)
- os.close(self._pipe_wfd)
- self.new_display_var = ":%s" % int(self.display)
- if self._use_xauth:
- self._setup_xauth()
- # https://github.com/ponty/PyVirtualDisplay/issues/2
- # https://github.com/ponty/PyVirtualDisplay/issues/14
- self.old_display_var = os.environ.get("DISPLAY", None)
- def _start1(self):
- with _mutex:
- self.display = _search_for_display()
- while self.display in _USED_DISPLAY_NR_LIST:
- self.display += 1
- self.new_display_var = ":%s" % int(self.display)
- _USED_DISPLAY_NR_LIST.append(self.display)
- self._command = self._cmd() + self._extra_args
- log.debug("command: %s", self._command)
- self._popen(use_pass_fds=False)
- self.new_display_var = ":%s" % int(self.display)
- if self._use_xauth:
- self._setup_xauth()
- # https://github.com/ponty/PyVirtualDisplay/issues/2
- # https://github.com/ponty/PyVirtualDisplay/issues/14
- self.old_display_var = os.environ.get("DISPLAY", None)
- # wait until X server is active
- start_time = time.time()
- d = self.new_display_var
- ok = False
- time.sleep(0.05) # give time for early exit
- while True:
- if not self.is_alive():
- break
- try:
- xdpyinfo = EasyProcess(["xdpyinfo"], env=self._env())
- xdpyinfo.enable_stdout_log = False
- xdpyinfo.enable_stderr_log = False
- exit_code = xdpyinfo.call().return_code
- except EasyProcessError:
- log.warning(
- "xdpyinfo was not found, X start can not be checked! Please install xdpyinfo!"
- )
- time.sleep(_X_START_WAIT) # old method
- ok = True
- break
- if exit_code != 0:
- pass
- else:
- log.info('Successfully started X with display "%s".', d)
- ok = True
- break
- if time.time() - start_time >= _X_START_TIMEOUT:
- break
- time.sleep(_X_START_TIME_STEP)
- if not self.is_alive():
- log.warning("process exited early. stderr:%s", self.stderr)
- msg = "Failed to start process: %s"
- raise XStartError(msg % self)
- if not ok:
- msg = 'Failed to start X on display "%s" (xdpyinfo check failed, stderr:[%s]).'
- raise XStartTimeoutError(msg % (d, xdpyinfo.stderr))
- def _wait_for_pipe_text(self, rfd):
- s = ""
- # start_time = time.time()
- while True:
- (rfd_changed_ls, _, _) = select.select([rfd], [], [], 0.1)
- if not self.is_alive():
- raise XStartError(
- "%s program closed. command: %s stderr: %s"
- % (self._program, self._command, self.stderr)
- )
- if rfd in rfd_changed_ls:
- c = os.read(rfd, 1)
- if c == b"\n":
- break
- s += c.decode("ascii")
- # if time.time() - start_time >= _X_START_TIMEOUT:
- # raise XStartTimeoutError(
- # "No reply from program %s. command:%s"
- # % (self._program, self._command,)
- # )
- return s
- def stop(self):
- """
- stop display
- :rtype: self
- """
- if not self._is_started:
- raise XStartError("stop() is called before start().")
- if self._reset_global_env:
- self._redirect_display(False)
- if self.is_alive():
- try:
- try:
- self._subproc.terminate()
- except AttributeError:
- os.kill(self._subproc.pid, signal.SIGKILL)
- except OSError as oserror:
- log.debug("exception in terminate:%s", oserror)
- self._subproc.wait()
- self._read_stdout_stderr()
- if self._use_xauth:
- self._clear_xauth()
- return self
- def _read_stdout_stderr(self):
- if self.stdout is None:
- (self.stdout, self.stderr) = self._subproc.communicate()
- log.debug("stdout=%s", self.stdout)
- log.debug("stderr=%s", self.stderr)
- def _setup_xauth(self):
- """
- Set up the Xauthority file and the XAUTHORITY environment variable.
- """
- handle, filename = tempfile.mkstemp(
- prefix="PyVirtualDisplay.", suffix=".Xauthority"
- )
- self._xauth_filename = filename
- os.close(handle)
- # Save old environment
- self._old_xauth = {}
- self._old_xauth["AUTHFILE"] = os.getenv("AUTHFILE")
- self._old_xauth["XAUTHORITY"] = os.getenv("XAUTHORITY")
- os.environ["AUTHFILE"] = os.environ["XAUTHORITY"] = filename
- cookie = xauth.generate_mcookie()
- xauth.call("add", self.new_display_var, ".", cookie)
- def _clear_xauth(self):
- """
- Clear the Xauthority file and restore the environment variables.
- """
- os.remove(self._xauth_filename)
- for varname in ["AUTHFILE", "XAUTHORITY"]:
- if self._old_xauth[varname] is None:
- del os.environ[varname]
- else:
- os.environ[varname] = self._old_xauth[varname]
- self._old_xauth = None
- def __enter__(self):
- """used by the :keyword:`with` statement"""
- self.start()
- return self
- def __exit__(self, *exc_info):
- """used by the :keyword:`with` statement"""
- self.stop()
- def is_alive(self):
- if not self._subproc:
- return False
- return self.return_code is None
- @property
- def return_code(self):
- if not self._subproc:
- return None
- rc = self._subproc.poll()
- if rc is not None:
- # proc exited
- self._read_stdout_stderr()
- return rc
- @property
- def pid(self):
- """
- PID (:attr:`subprocess.Popen.pid`)
- :rtype: int
- """
- if self._subproc:
- return self._subproc.pid