''' anatomist.headless module implements a headless (off-screen) version of
Anatomist, and some helper functions.
The main entry point is HeadlessAnatomist:
>>> import anatomist.headless as ana
>>> a = ana.HeadlessAnatomist()
Other functions are used by HeadlessAnatomist implementation.
'''
from __future__ import print_function
from __future__ import absolute_import
from soma import subprocess
import os
from soma.subprocess import Popen, check_output, call
import atexit
import time
import distutils.spawn
import ctypes
import sys
from six.moves import range
from io import StringIO
xvfb = None
original_display = None
hanatomist = None
[docs]def setup_virtualGL():
''' Load VirtualGL libraries and LD_PRELOAD env variable to run the current
process via VirtualGL.
.. warning::
If the current process has already used some libraries (libX11? libGL
certainly), setting VirtualGL libs afterwards may cause segfaults and
program crashes. So it is not safe to use it unless you are sure to do
it straight at the beginning of the program, prior to importing many
modules.
Unfortunately, I don't know how to test it.
'''
if os.environ.get('VGL_ISACTIVE') == '1':
return True
try:
preload = ['libdlfaker']
# vglrun may use either librrfaker or libvglfaker depending on its
# version.
try:
vglfaker = ctypes.CDLL('librrfaker.so', ctypes.RTLD_GLOBAL)
preload.append('librrfaker.so')
except:
vglfaker = ctypes.CDLL('libvglfaker.so', ctypes.RTLD_GLOBAL)
preload.append('libvglfaker.so')
dlfaker = ctypes.CDLL('libdlfaker.so', ctypes.RTLD_GLOBAL)
os.environ['LD_PRELOAD'] = ':'.join(preload)
os.environ['VGL_ISACTIVE'] = '1'
except:
return False
return True
[docs]def test_glx(glxinfo_cmd=None, xdpyinfo_cmd=None, timeout=5.):
''' Test the presence of the GLX module in the X server, by running
glxinfo or xdpyinfo command
Parameters
----------
glxinfo_cmd: str or list
glxinfo command: may be a string ('glxinfo') or a list, which allows
running it through a wrapper, ex: ['vglrun', 'glxinfo']
xdpyinfo_cmd: str or list
xdpyinfo command: may be a string ('xdpyinfo') or a list, which allows
running it through a wrapper, ex: ['vglrun', 'xdpyinfo']. xdpyinfo is
only used if glxinfo is not present, and can produce an inaccurate
result (some xvfb servers advertise a GLX extension which does not
work in fact).
timeout: float (optional)
try several times to connect the X server while waiting for it to
startup. If 0, try only once and return.
Returns
-------
2 if GLX is recognized trough glxinfo (trustable), 1 if GLX is recognized
through xdpyinfo (not always trustable), 0 otherwise.
'''
if glxinfo_cmd is None:
glxinfo_cmd = distutils.spawn.find_executable('glxinfo')
if glxinfo_cmd not in (None, []):
glxinfo = u''
#glxinfo = StringIO()
t0 = time.time()
t1 = 0
while glxinfo == u'' and t1 <= timeout:
# universal_newlines = open stdout/stderr in text mode (Unicode)
process = Popen(glxinfo_cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
try:
glxinfo, glxerr = process.communicate(timeout=5)
except TimeoutExpired:
process.kill()
glxinfo, glxerr = process.communicate()
raise subprocess.TimeoutExpired(process.args, 5,
output=glxinfo)
retcode = process.poll()
if retcode != 0:
if u'unable to open display' not in glxerr:
# failed for another reason: probably GLX is not working
break
time.sleep(0.01)
t1 = time.time() - t0
if glxinfo != u'' or t1 > timeout:
if u' GLX Visuals' not in glxinfo:
return 0
else:
return 2
# here glxinfo has not been used or is not working
if xdpyinfo_cmd is None:
xdpyinfo_cmd = distutils.spawn.find_executable('xdpyinfo')
dpyinfo = u''
t0 = time.time()
t1 = 0
while dpyinfo == u'' and t1 <= timeout:
try:
# universal_newlines = open stdout/stderr in text mode (Unicode)
dpyinfo = check_output(xdpyinfo_cmd,
universal_newlines=True)
except Exception as e:
time.sleep(0.01)
t1 = time.time() - t0
if u'GLX' not in dpyinfo:
return 0
else:
return 1
[docs]def test_opengl(pid=None, verbose=False):
''' Test the presence of OpenGL libraries (and which ones) in the specified
Unix process. Works only on Linux (or maybe ELF Unixes).
Parameters
----------
pid: int (optional)
process id to look OpenbGL libs in. Default: current process
verbose: bool (optional)
if True, print found libs
Returns
-------
set of loaded libGL libraries
'''
if pid is None:
pid = os.getpid()
gl_libs = set()
for line in open('/proc/%d/maps' % pid).readlines():
lib = line.split()[-1]
if lib not in gl_libs and lib.find('libGL.so.') != -1:
gl_libs.add(lib)
if verbose:
print(lib)
return gl_libs
[docs]def test_qapp():
''' If QtGui is already loaded, switching to VirtualGL in the running
process leads to segfaults.
Moreover if QApplication is instantiated, the display is already connected
and cannot change in Qt afterwards.
'''
mods = ('PyQt4.QtGui', 'PyQt5.QtGui', 'PySide.QtGui')
for mod in mods:
if mod in sys.modules:
from soma.qt_gui.qt_backend import Qt
if Qt.QApplication.instance() is not None:
return 'QApp'
return 'QtGui'
return None
[docs]def find_mesa():
''' Try to find a software Mesa library in the libraries search path.
Parses the LD_LIBRARY_PATH env variable and libs listed by the command
"ldconfig -p", looks for a mesa/ subdir containing a libGL.so.1 file.
Returns
-------
Mesa library file with full path, or None if not found
'''
paths = os.environ.get('LD_LIBRARY_PATH')
ldconfig = check_output(['ldconfig', '-p'])
paths2 = [os.path.dirname(p.split()[-1])
for p in ldconfig.split('\n')[1:-1]]
if paths:
paths = paths.split(':')
else:
paths = []
spaths = set(paths)
for p in paths2:
if p not in spaths:
paths.append(p)
spaths.add(p)
for path in paths:
test_gl = os.path.join(path, 'mesa', 'libGL.so.1')
if os.path.exists(test_gl):
return test_gl
return None
# def terminate_xvfb():
#global xvfb
#global original_display
# if xvfb:
# xvfb.terminate()
# xvfb.wait()
#xvfb = None
# if original_display:
#os.environ['DISPLAY'] = original_display
# else:
#del os.environ['DISPLAY']
class PrCtlError(Exception):
pass
[docs]def on_parent_exit(signame):
"""
Return a function to be run in a child process which will trigger SIGNAME
to be sent when the parent process dies
found on https://gist.github.com/evansd/2346614
"""
import signal
from ctypes import cdll
# Constant taken from http://linux.die.net/include/linux/prctl.h
PR_SET_PDEATHSIG = 1
signum = getattr(signal, signame)
def set_parent_exit_signal():
# http://linux.die.net/man/2/prctl
result = cdll['libc.so.6'].prctl(PR_SET_PDEATHSIG, signum)
if result != 0:
raise PrCtlError('prctl failed with error code %s' % result)
return set_parent_exit_signal
[docs]def setup_headless(allow_virtualgl=True, force_virtualgl=False):
''' Sets up a headless virtual X server and tunes the current process
libraries to use it appropriately.
.. warning::
calling this function may run a Xvfb process, and change the
current process libraries to use VirtualGL or Mesa GL.
If OpenGL library or Qt QtGui module is loaded, then VirtualGL will not be
allowed to prevent crashes.
If Qt QApplication is instantiated, headless mode is disabled because Qt
is already connected to a display that cannot change afterwards.
If no configuration proves to work, raise an exception.
Parameters
----------
allow_virtualgl: bool (optional)
If False, VirtualGL will not be attempted. Default is True.
Use it if you experience crashes in your programs: it probably means
that some incompatible libraries have alrealy been loaded.
force_virtualgl: bool (optional)
only meaningful if allow_virtualgl is True. If force_virtualgl True,
virtualGL will be attempted even if the X server advertises a GLX
extension. This is useful when GLX is present but does not work when
OpenGL is used.
'''
global xvfb
global original_display
if xvfb:
# already setup
return
if sys.platform in ('darwin', 'win32'):
# not a X11 implementation
return
qtapp = test_qapp()
if qtapp == 'QApp':
# QApplication has already opened the current display: we cannot change
# it afterwards.
print('QApplication already instantiated, headless Anatomist is not '
'possible.')
return
use_xvfb = True
glxinfo_cmd = distutils.spawn.find_executable('glxinfo')
xdpyinfo_cmd = distutils.spawn.find_executable('xdpyinfo')
# if not xdpyinfo_cmd:
# not a X client, probably not Linux
#use_xvfb = False
xvfb_cmd = distutils.spawn.find_executable('Xvfb')
if not xvfb_cmd:
use_xvfb = False
if use_xvfb:
for display in range(100):
if not os.path.exists('/tmp/.X11-unix/X%d' % display) \
and not os.path.exists('/tmp/.X%d-lock' % display):
break
else:
raise RuntimeError('Too many X servers')
xvfb = Popen(['Xvfb', '-screen', '0', '1280x1024x24',
'+extension', 'GLX', ':%d' % display],
preexec_fn=on_parent_exit('SIGINT'))
original_display = os.environ.get('DISPLAY', None)
os.environ['DISPLAY'] = ':%d' % display
glx = test_glx(glxinfo_cmd=glxinfo_cmd, xdpyinfo_cmd=xdpyinfo_cmd)
gl_libs = set()
if not glx:
gl_libs = test_opengl(verbose=True)
if len(gl_libs) != 0:
print('OpenGL lib already loaded. Using Xvfb will not be '
'possible.')
if (glx < 2 or force_virtualgl) and not gl_libs and allow_virtualgl \
and qtapp is None:
# try VirtualGL
vgl = distutils.spawn.find_executable('vglrun')
if vgl:
print('VirtualGL found.')
vglglxinfo_cmd = None
vglxdpyinfo_cmd = None
if glxinfo_cmd:
vglglxinfo_cmd = [vgl, glxinfo_cmd]
if xdpyinfo_cmd:
vglxdpyinfo_cmd = [vgl, xdpyinfo_cmd]
if test_glx(glxinfo_cmd=vglglxinfo_cmd,
xdpyinfo_cmd=vglxdpyinfo_cmd, timeout=0):
print('VirtualGL should work.')
glx = setup_virtualGL()
if glx:
print('Running through VirtualGL + Xvfb: '
'this is optimal.')
else:
print('But VirtualGL could not be loaded...')
# test_opengl(verbose=True)
if not glx and not gl_libs:
# try Mesa, if found
mesa = find_mesa()
if mesa:
print('MESA found:', mesa)
mesa_lib = ctypes.CDLL(mesa, ctypes.RTLD_GLOBAL)
os.environ['LD_PRELOAD'] = mesa
os.environ['LD_LIBRARY_PATH'] \
= os.path.dirname(mesa) + ':' \
+ os.getenv('LD_LIBRARY_PATH')
# re-run Xvfb using new path
xvfb.terminate()
xvfb.wait()
xvfb = Popen(['Xvfb', '-screen', '0', '1280x1024x24',
'+extension', 'GLX', ':%d' % display],
preexec_fn=on_parent_exit('SIGINT'))
#self.mesa_lib = mesa_lib
glx = test_glx(glxinfo_cmd, xdpyinfo_cmd)
if glx:
print('Running using Mesa software OpenGL: performance '
'will be slow. To get faster results, and if X '
'server connection can be obtained, consider '
'installing VirtualGL (http://virtualgl.org) '
'and running again before loading QtGui.')
else:
print('Mesa not found.')
if not glx:
print('The current Xvfb does not have a GLX extension. '
'Aborting it.')
xvfb.terminate()
xvfb.wait()
xvfb = None
if original_display is not None:
os.environ['DISPLAY'] = original_display
else:
del os.environ['DISPLAY']
use_xvfb = False
#raise RuntimeError('GLX extension missing')
if not use_xvfb:
if xdpyinfo_cmd:
glx = test_glx(glxinfo_cmd, xdpyinfo_cmd, 0)
if not glx:
raise RuntimeError('GLX extension missing')
print('Headless Anatomist running in normal (non-headless) mode')
# this is not needed any longer, since on_parent_exit() is passed to
# Popen
# if xvfb is not None:
# atexit.register(terminate_xvfb)
[docs]def HeadlessAnatomist(*args, **kwargs):
''' Implements an off-screen headless Anatomist.
.. warning:: Only usable with X11.
Needs Xvfb and xdpyinfo commands to be available, and possibly
VirtualGL or Mesa.
All X rendering is deported to a virtual X server (Xvfb) which doesn't
actually display things.
Depending on the OpenGL implementation / driver, Xvfb will not
necessarily support the GLX extension. This especially happens with
NVidia OpenGL on Linux.
To overcome this, HeadlessAnatomist will automatically attempt to use
VirtualGL (http://www.virtualgl.org), but:
* all application OpenGL rendering will be redirected through
VirtualGL, OpenGL calls will be modified.
* VirtualGL deports the rendering to a working X server, thus this one
has to exist (other than Xvfb), to be running, and to have working
OpenGL.
If VirtualGL is not available, or not working (no X server), then
HeadlessAnatomist will attempt to find a software Mesa library and
use it. This has other side effects, since all openGL calls will be
software.
.. note::
This implementation connects to a virtual X server, then runs a
regular Anatomist. This has several limiations:
* All the widgets from the application will be redirected to this
display: it is not possible to mix on-screen and off-screen
rendering, or a regular on-screen Qt application with off-screen
anatomist snapshoting.
* HeadlessAnatomist should be instantiated before any OpenGL
library is loaded to allow tweaking, using either VirtualGL
(http://virtualgl.org), or a software Mesa OpenGL if one can be
found and is useable.
This means any Qt module should *not* be imported yet, including
anatomist.api.
If OpenGL is already loaded, just hope its implementation will be
compatible with Xvfb.
* HeadlessAnatomist must be instantiated before any Qt widget is
rendered, because Qt does not allow mixed-X displays. Once a
widget is created, all others will go to the same display.
Otherwise, returns the unique instance of the configured Anatomist class
anatomist.api.Anatomist, as configured via
anatomist.setDefaultImplementation(), to it can use
any of the different implementations of the Anatomist API.
If OpenGL has already been loaded, or Xvfb cannot be made to work, and
if a regular X server conection is working, then a regular, on-screen
Anatomist will be used.
Parameters are passed to Anatomist constructor, except the following
keyword arguments:
allow_virtualgl: bool (optional, default: True)
If False, VirtualGL will not be attempted. Default is True.
Use it if you experience crashes in your programs: it probably means
that some incompatible libraries have alrealy been loaded.
force_virtualgl: bool (optional, default: False)
only meaningful if allow_virtualgl is True. If force_virtualgl True,
virtualGL will be attempted even if the X server advertises a GLX
extension through the xdpyinfo command (if glxinfo is OK, then this
command is trusted). This is useful when GLX is present but does not
work when OpenGL is used.
'''
global hanatomist
if hanatomist:
return hanatomist
allow_virtualgl = True
force_virtualgl = False
if 'allow_virtualgl' in kwargs:
allow_virtualgl = kwargs['allow_virtualgl']
kwargs = dict(kwargs)
del kwargs['allow_virtualgl']
if 'force_virtualgl' in kwargs:
force_virtualgl = kwargs['force_virtualgl']
kwargs = dict(kwargs)
del kwargs['force_virtualgl']
setup_headless(allow_virtualgl=allow_virtualgl,
force_virtualgl=force_virtualgl)
from anatomist.api import Anatomist
# def __del__ana(self):
#atexit._exithandlers.remove((terminate_xvfb, (), {}))
# terminate_xvfb()
# def createWindow_ana(self, wintype, **kwargs):
#options = kwargs.get('options', {})
# if 'hidden' not in options:
#options['hidden'] = True
#kwargs = dict(kwargs)
#kwargs['options'] = options
# return self._old_createWindow(wintype, **kwargs)
hanatomist = Anatomist()
#Anatomist.__del__ = __del__ana
#Anatomist._old_createWindow = Anatomist.createWindow
#Anatomist.createWindow = createWindow_ana
return hanatomist