/pyvirtualdisplay/abstractdisplay.py

http://github.com/ponty/PyVirtualDisplay · Python · 395 lines · 276 code · 65 blank · 54 comment · 63 complexity · 79bd23b08f505ac126aa5f14b1caf221 MD5 · raw file

  1. import fnmatch
  2. import logging
  3. import os
  4. import select
  5. import signal
  6. import subprocess
  7. import tempfile
  8. import time
  9. from threading import Lock
  10. from easyprocess import EasyProcess, EasyProcessError
  11. from pyvirtualdisplay import xauth
  12. from pyvirtualdisplay.util import get_helptext, platform_is_osx
  13. log = logging.getLogger(__name__)
  14. # try:
  15. # import fcntl
  16. # except ImportError:
  17. # fcntl = None
  18. _mutex = Lock()
  19. _mutex_popen = Lock()
  20. _MIN_DISPLAY_NR = 1000
  21. _USED_DISPLAY_NR_LIST = []
  22. _X_START_TIMEOUT = 10
  23. _X_START_TIME_STEP = 0.1
  24. _X_START_WAIT = 0.1
  25. class XStartTimeoutError(Exception):
  26. pass
  27. class XStartError(Exception):
  28. pass
  29. def _lock_files():
  30. tmpdir = "/tmp"
  31. try:
  32. ls = os.listdir(tmpdir)
  33. except FileNotFoundError:
  34. log.warning("missing /tmp")
  35. return []
  36. pattern = ".X*-lock"
  37. names = fnmatch.filter(ls, pattern)
  38. ls = [os.path.join(tmpdir, child) for child in names]
  39. ls = [p for p in ls if os.path.isfile(p)]
  40. return ls
  41. def _search_for_display():
  42. # search for free display
  43. ls = list(map(lambda x: int(x.split("X")[1].split("-")[0]), _lock_files()))
  44. if len(ls):
  45. display = max(_MIN_DISPLAY_NR, max(ls) + 3)
  46. else:
  47. display = _MIN_DISPLAY_NR
  48. return display
  49. class AbstractDisplay(object):
  50. """
  51. Common parent for X servers (Xvfb,Xephyr,Xvnc)
  52. """
  53. def __init__(self, program, use_xauth, retries, extra_args, manage_global_env):
  54. self._extra_args = extra_args
  55. self._retries = retries
  56. self._program = program
  57. self.stdout = None
  58. self.stderr = None
  59. self.old_display_var = None
  60. self._subproc = None
  61. self.display = None
  62. self._is_started = False
  63. self._manage_global_env = manage_global_env
  64. self._reset_global_env = False
  65. self._pipe_wfd = None
  66. self._retries_current = 0
  67. helptext = get_helptext(program)
  68. self._has_displayfd = "-displayfd" in helptext
  69. if not self._has_displayfd:
  70. log.debug("-displayfd flag is missing.")
  71. PYVIRTUALDISPLAY_DISPLAYFD = os.environ.get("PYVIRTUALDISPLAY_DISPLAYFD")
  72. if PYVIRTUALDISPLAY_DISPLAYFD:
  73. log.debug("PYVIRTUALDISPLAY_DISPLAYFD=%s", PYVIRTUALDISPLAY_DISPLAYFD)
  74. # '0'->false, '1'->true
  75. self._has_displayfd = bool(int(PYVIRTUALDISPLAY_DISPLAYFD))
  76. else:
  77. # TODO: macos: displayfd is available on XQuartz-2.7.11 but it doesn't work, always 0 is returned
  78. if platform_is_osx():
  79. self._has_displayfd = False
  80. self._check_flags(helptext)
  81. if use_xauth and not xauth.is_installed():
  82. raise xauth.NotFoundError()
  83. self._use_xauth = use_xauth
  84. self._old_xauth = None
  85. self._xauth_filename = None
  86. def _check_flags(self, helptext):
  87. pass
  88. def _cmd(self):
  89. raise NotImplementedError()
  90. def _redirect_display(self, on):
  91. """
  92. on:
  93. * True -> set $DISPLAY to virtual screen
  94. * False -> set $DISPLAY to original screen
  95. :param on: bool
  96. """
  97. d = self.new_display_var if on else self.old_display_var
  98. if d is None:
  99. log.debug("unset $DISPLAY")
  100. try:
  101. del os.environ["DISPLAY"]
  102. except KeyError:
  103. log.warning("$DISPLAY was already unset.")
  104. else:
  105. log.debug("set $DISPLAY=%s", d)
  106. os.environ["DISPLAY"] = d
  107. def _env(self):
  108. env = os.environ.copy()
  109. env["DISPLAY"] = self.new_display_var
  110. return env
  111. def start(self):
  112. """
  113. start display
  114. :rtype: self
  115. """
  116. if self._is_started:
  117. raise XStartError(self, "Display was started twice.")
  118. self._is_started = True
  119. if self._has_displayfd:
  120. self._start1_has_displayfd()
  121. else:
  122. i = 0
  123. while True:
  124. self._retries_current = i + 1
  125. try:
  126. self._start1()
  127. break
  128. except XStartError:
  129. log.warning("start failed %s", i + 1)
  130. time.sleep(0.05)
  131. i += 1
  132. if i >= self._retries:
  133. raise XStartError(
  134. "No success after %s retries. Last stderr: %s"
  135. % (self._retries, self.stderr)
  136. )
  137. if self._manage_global_env:
  138. self._redirect_display(True)
  139. self._reset_global_env = True
  140. def _popen(self, use_pass_fds):
  141. with _mutex_popen:
  142. if use_pass_fds:
  143. self._subproc = subprocess.Popen(
  144. self._command,
  145. pass_fds=[self._pipe_wfd],
  146. stdout=subprocess.PIPE,
  147. stderr=subprocess.PIPE,
  148. shell=False,
  149. )
  150. else:
  151. self._subproc = subprocess.Popen(
  152. self._command,
  153. stdout=subprocess.PIPE,
  154. stderr=subprocess.PIPE,
  155. shell=False,
  156. )
  157. def _start1_has_displayfd(self):
  158. # stdout doesn't work on osx -> create own pipe
  159. rfd, self._pipe_wfd = os.pipe()
  160. self._command = self._cmd() + self._extra_args
  161. log.debug("command: %s", self._command)
  162. self._popen(use_pass_fds=True)
  163. self.display = int(self._wait_for_pipe_text(rfd))
  164. os.close(rfd)
  165. os.close(self._pipe_wfd)
  166. self.new_display_var = ":%s" % int(self.display)
  167. if self._use_xauth:
  168. self._setup_xauth()
  169. # https://github.com/ponty/PyVirtualDisplay/issues/2
  170. # https://github.com/ponty/PyVirtualDisplay/issues/14
  171. self.old_display_var = os.environ.get("DISPLAY", None)
  172. def _start1(self):
  173. with _mutex:
  174. self.display = _search_for_display()
  175. while self.display in _USED_DISPLAY_NR_LIST:
  176. self.display += 1
  177. self.new_display_var = ":%s" % int(self.display)
  178. _USED_DISPLAY_NR_LIST.append(self.display)
  179. self._command = self._cmd() + self._extra_args
  180. log.debug("command: %s", self._command)
  181. self._popen(use_pass_fds=False)
  182. self.new_display_var = ":%s" % int(self.display)
  183. if self._use_xauth:
  184. self._setup_xauth()
  185. # https://github.com/ponty/PyVirtualDisplay/issues/2
  186. # https://github.com/ponty/PyVirtualDisplay/issues/14
  187. self.old_display_var = os.environ.get("DISPLAY", None)
  188. # wait until X server is active
  189. start_time = time.time()
  190. d = self.new_display_var
  191. ok = False
  192. time.sleep(0.05) # give time for early exit
  193. while True:
  194. if not self.is_alive():
  195. break
  196. try:
  197. xdpyinfo = EasyProcess(["xdpyinfo"], env=self._env())
  198. xdpyinfo.enable_stdout_log = False
  199. xdpyinfo.enable_stderr_log = False
  200. exit_code = xdpyinfo.call().return_code
  201. except EasyProcessError:
  202. log.warning(
  203. "xdpyinfo was not found, X start can not be checked! Please install xdpyinfo!"
  204. )
  205. time.sleep(_X_START_WAIT) # old method
  206. ok = True
  207. break
  208. if exit_code != 0:
  209. pass
  210. else:
  211. log.info('Successfully started X with display "%s".', d)
  212. ok = True
  213. break
  214. if time.time() - start_time >= _X_START_TIMEOUT:
  215. break
  216. time.sleep(_X_START_TIME_STEP)
  217. if not self.is_alive():
  218. log.warning("process exited early. stderr:%s", self.stderr)
  219. msg = "Failed to start process: %s"
  220. raise XStartError(msg % self)
  221. if not ok:
  222. msg = 'Failed to start X on display "%s" (xdpyinfo check failed, stderr:[%s]).'
  223. raise XStartTimeoutError(msg % (d, xdpyinfo.stderr))
  224. def _wait_for_pipe_text(self, rfd):
  225. s = ""
  226. # start_time = time.time()
  227. while True:
  228. (rfd_changed_ls, _, _) = select.select([rfd], [], [], 0.1)
  229. if not self.is_alive():
  230. raise XStartError(
  231. "%s program closed. command: %s stderr: %s"
  232. % (self._program, self._command, self.stderr)
  233. )
  234. if rfd in rfd_changed_ls:
  235. c = os.read(rfd, 1)
  236. if c == b"\n":
  237. break
  238. s += c.decode("ascii")
  239. # if time.time() - start_time >= _X_START_TIMEOUT:
  240. # raise XStartTimeoutError(
  241. # "No reply from program %s. command:%s"
  242. # % (self._program, self._command,)
  243. # )
  244. return s
  245. def stop(self):
  246. """
  247. stop display
  248. :rtype: self
  249. """
  250. if not self._is_started:
  251. raise XStartError("stop() is called before start().")
  252. if self._reset_global_env:
  253. self._redirect_display(False)
  254. if self.is_alive():
  255. try:
  256. try:
  257. self._subproc.terminate()
  258. except AttributeError:
  259. os.kill(self._subproc.pid, signal.SIGKILL)
  260. except OSError as oserror:
  261. log.debug("exception in terminate:%s", oserror)
  262. self._subproc.wait()
  263. self._read_stdout_stderr()
  264. if self._use_xauth:
  265. self._clear_xauth()
  266. return self
  267. def _read_stdout_stderr(self):
  268. if self.stdout is None:
  269. (self.stdout, self.stderr) = self._subproc.communicate()
  270. log.debug("stdout=%s", self.stdout)
  271. log.debug("stderr=%s", self.stderr)
  272. def _setup_xauth(self):
  273. """
  274. Set up the Xauthority file and the XAUTHORITY environment variable.
  275. """
  276. handle, filename = tempfile.mkstemp(
  277. prefix="PyVirtualDisplay.", suffix=".Xauthority"
  278. )
  279. self._xauth_filename = filename
  280. os.close(handle)
  281. # Save old environment
  282. self._old_xauth = {}
  283. self._old_xauth["AUTHFILE"] = os.getenv("AUTHFILE")
  284. self._old_xauth["XAUTHORITY"] = os.getenv("XAUTHORITY")
  285. os.environ["AUTHFILE"] = os.environ["XAUTHORITY"] = filename
  286. cookie = xauth.generate_mcookie()
  287. xauth.call("add", self.new_display_var, ".", cookie)
  288. def _clear_xauth(self):
  289. """
  290. Clear the Xauthority file and restore the environment variables.
  291. """
  292. os.remove(self._xauth_filename)
  293. for varname in ["AUTHFILE", "XAUTHORITY"]:
  294. if self._old_xauth[varname] is None:
  295. del os.environ[varname]
  296. else:
  297. os.environ[varname] = self._old_xauth[varname]
  298. self._old_xauth = None
  299. def __enter__(self):
  300. """used by the :keyword:`with` statement"""
  301. self.start()
  302. return self
  303. def __exit__(self, *exc_info):
  304. """used by the :keyword:`with` statement"""
  305. self.stop()
  306. def is_alive(self):
  307. if not self._subproc:
  308. return False
  309. return self.return_code is None
  310. @property
  311. def return_code(self):
  312. if not self._subproc:
  313. return None
  314. rc = self._subproc.poll()
  315. if rc is not None:
  316. # proc exited
  317. self._read_stdout_stderr()
  318. return rc
  319. @property
  320. def pid(self):
  321. """
  322. PID (:attr:`subprocess.Popen.pid`)
  323. :rtype: int
  324. """
  325. if self._subproc:
  326. return self._subproc.pid