Skip to content

Commit 94146e7

Browse files
committed
[ENH] Centralize virtual/physical $DISPLAYs
This PR addresses several (related) problems: - Makes `$DISPLAY` and the config `display_variable` optional (close #2055) - Should fix #1403 since xvfb-run is not used anymore. - Relates to #1400: - Will reduce its impact because now Xvfb is called only once and only if it is absolutely necessary - May make it worse in some cases when Xvfb fails to create a listener (root needed?). This PR adds a `config.get_display()` which identifies what display should be used, and creates a virtual one if necessary. Also adds a few unit tests to the config object to make sure precedence is fulfilled.
1 parent 9a988d6 commit 94146e7

File tree

3 files changed

+98
-51
lines changed

3 files changed

+98
-51
lines changed

nipype/interfaces/base.py

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,32 +1010,6 @@ def _check_version_requirements(self, trait_object, raise_exception=True):
10101010
version, max_ver))
10111011
return unavailable_traits
10121012

1013-
def _run_wrapper(self, runtime):
1014-
sysdisplay = os.getenv('DISPLAY')
1015-
if self._redirect_x:
1016-
try:
1017-
from xvfbwrapper import Xvfb
1018-
except ImportError:
1019-
iflogger.error('Xvfb wrapper could not be imported')
1020-
raise
1021-
1022-
vdisp = Xvfb(nolisten='tcp')
1023-
vdisp.start()
1024-
try:
1025-
vdisp_num = vdisp.new_display
1026-
except AttributeError: # outdated version of xvfbwrapper
1027-
vdisp_num = vdisp.vdisplay_num
1028-
1029-
iflogger.info('Redirecting X to :%d' % vdisp_num)
1030-
runtime.environ['DISPLAY'] = ':%d' % vdisp_num
1031-
1032-
runtime = self._run_interface(runtime)
1033-
1034-
if self._redirect_x:
1035-
vdisp.stop()
1036-
1037-
return runtime
1038-
10391013
def _run_interface(self, runtime):
10401014
""" Core function that executes interface
10411015
"""
@@ -1071,6 +1045,9 @@ def run(self, **inputs):
10711045

10721046
# initialize provenance tracking
10731047
env = deepcopy(dict(os.environ))
1048+
if self._redirect_x:
1049+
env['DISPLAY'] = config.get_display()
1050+
10741051
runtime = Bunch(cwd=os.getcwd(),
10751052
returncode=None,
10761053
duration=None,
@@ -1080,8 +1057,9 @@ def run(self, **inputs):
10801057
platform=platform.platform(),
10811058
hostname=platform.node(),
10821059
version=self.version)
1060+
10831061
try:
1084-
runtime = self._run_wrapper(runtime)
1062+
runtime = self._run_interface(runtime)
10851063
outputs = self.aggregate_outputs(runtime)
10861064
runtime.endTime = dt.isoformat(dt.utcnow())
10871065
timediff = parseutc(runtime.endTime) - parseutc(runtime.startTime)
@@ -1446,7 +1424,7 @@ def get_max_resources_used(pid, mem_mb, num_threads, pyfunc=False):
14461424
return mem_mb, num_threads
14471425

14481426

1449-
def run_command(runtime, output=None, timeout=0.01, redirect_x=False):
1427+
def run_command(runtime, output=None, timeout=0.01):
14501428
"""Run a command, read stdout and stderr, prefix with timestamp.
14511429
14521430
The returned runtime contains a merged stdout+stderr log with timestamps
@@ -1458,13 +1436,6 @@ def run_command(runtime, output=None, timeout=0.01, redirect_x=False):
14581436
# Init variables
14591437
PIPE = subprocess.PIPE
14601438
cmdline = runtime.cmdline
1461-
1462-
if redirect_x:
1463-
exist_xvfb, _ = _exists_in_path('xvfb-run', runtime.environ)
1464-
if not exist_xvfb:
1465-
raise RuntimeError('Xvfb was not found, X redirection aborted')
1466-
cmdline = 'xvfb-run -a ' + cmdline
1467-
14681439
env = _canonicalize_env(runtime.environ)
14691440

14701441
default_encoding = locale.getdefaultlocale()[1]
@@ -1727,14 +1698,6 @@ def help(cls, returnhelp=False):
17271698
print(allhelp)
17281699

17291700
def _get_environ(self):
1730-
out_environ = {}
1731-
if not self._redirect_x:
1732-
try:
1733-
display_var = config.get('execution', 'display_variable')
1734-
out_environ = {'DISPLAY': display_var}
1735-
except NoOptionError:
1736-
pass
1737-
iflogger.debug(out_environ)
17381701
if isdefined(self.inputs.environ):
17391702
out_environ.update(self.inputs.environ)
17401703
return out_environ
@@ -1754,10 +1717,6 @@ def version_from_command(self, flag='-v'):
17541717
o, e = proc.communicate()
17551718
return o
17561719

1757-
def _run_wrapper(self, runtime):
1758-
runtime = self._run_interface(runtime)
1759-
return runtime
1760-
17611720
def _run_interface(self, runtime, correct_return_codes=(0,)):
17621721
"""Execute command via subprocess
17631722
@@ -1785,8 +1744,7 @@ def _run_interface(self, runtime, correct_return_codes=(0,)):
17851744
setattr(runtime, 'command_path', cmd_path)
17861745
setattr(runtime, 'dependencies', get_dependencies(executable_name,
17871746
runtime.environ))
1788-
runtime = run_command(runtime, output=self.inputs.terminal_output,
1789-
redirect_x=self._redirect_x)
1747+
runtime = run_command(runtime, output=self.inputs.terminal_output)
17901748
if runtime.returncode is None or \
17911749
runtime.returncode not in correct_return_codes:
17921750
self.raise_exception(runtime)

nipype/utils/config.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
[execution]
4545
create_report = true
4646
crashdump_dir = %s
47-
display_variable = :1
4847
hash_method = timestamp
4948
job_finished_timeout = 5
5049
keep_inputs = false
@@ -82,7 +81,8 @@ def mkdir_p(path):
8281

8382

8483
class NipypeConfig(object):
85-
"""Base nipype config class
84+
"""
85+
Base nipype config class
8686
"""
8787

8888
def __init__(self, *args, **kwargs):
@@ -91,6 +91,7 @@ def __init__(self, *args, **kwargs):
9191
config_file = os.path.join(config_dir, 'nipype.cfg')
9292
self.data_file = os.path.join(config_dir, 'nipype.json')
9393
self._config.readfp(StringIO(default_cfg))
94+
self._display = None
9495
if os.path.exists(config_dir):
9596
self._config.read([config_file, 'nipype.cfg'])
9697

@@ -172,3 +173,50 @@ def update_matplotlib(self):
172173
def enable_provenance(self):
173174
self._config.set('execution', 'write_provenance', 'true')
174175
self._config.set('execution', 'hash_method', 'content')
176+
177+
def get_display(self):
178+
"""Returns the first display available"""
179+
180+
# Check if an Xorg server is listening
181+
# import subprocess as sp
182+
# if not hasattr(sp, 'DEVNULL'):
183+
# setattr(sp, 'DEVNULL', os.devnull)
184+
# x_listening = bool(sp.call('ps au | grep -v grep | grep -i xorg',
185+
# shell=True, stdout=sp.DEVNULL))
186+
187+
if self._display is not None:
188+
return ':%d' % self._display.vdisplay_num
189+
190+
sysdisplay = None
191+
if self._config.has_option('execution', 'display_variable'):
192+
sysdisplay = self._config.get('execution', 'display_variable')
193+
194+
sysdisplay = sysdisplay or os.getenv('DISPLAY')
195+
if sysdisplay:
196+
from collections import namedtuple
197+
def _mock():
198+
pass
199+
200+
# Store a fake Xvfb object
201+
ndisp = int(sysdisplay.split(':')[-1])
202+
Xvfb = namedtuple('Xvfb', ['vdisplay_num', 'stop'])
203+
self._display = Xvfb(ndisp, _mock)
204+
return sysdisplay
205+
206+
else:
207+
try:
208+
from xvfbwrapper import Xvfb
209+
except ImportError:
210+
raise RuntimeError(
211+
'A display server was required, but $DISPLAY is not defined '
212+
' and Xvfb could not be imported.')
213+
214+
self._display = Xvfb(nolisten='tcp')
215+
self._display.start()
216+
217+
# Older versions of Xvfb used vdisplay_num
218+
if hasattr(self._display, 'new_display'):
219+
setattr(self._display, 'vdisplay_num',
220+
self._display.new_display)
221+
222+
return ':%d' % self._display.vdisplay_num

nipype/utils/tests/test_config.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
from __future__ import print_function, division, unicode_literals, absolute_import
5+
import os
6+
import pytest
7+
from nipype import config
8+
9+
@pytest.mark.parametrize('dispnum', range(5))
10+
def test_display_config(monkeypatch, dispnum):
11+
"""Check that the display_variable option is used"""
12+
config._display = None
13+
dispstr = ':%d' % dispnum
14+
config.set('execution', 'display_variable', dispstr)
15+
monkeypatch.delitem(os.environ, 'DISPLAY', raising=False)
16+
assert config.get_display() == config.get('execution', 'display_variable')
17+
18+
@pytest.mark.parametrize('dispnum', range(5))
19+
def test_display_system(monkeypatch, dispnum):
20+
"""Check that when only a $DISPLAY is defined, it is used"""
21+
config._display = None
22+
config._config.remove_option('execution', 'display_variable')
23+
dispstr = ':%d' % dispnum
24+
monkeypatch.setitem(os.environ, 'DISPLAY', dispstr)
25+
assert config.get_display() == dispstr
26+
27+
def test_display_config_and_system(monkeypatch):
28+
"""Check that when only both config and $DISPLAY are defined, the config takes precedence"""
29+
config._display = None
30+
dispstr = ':10'
31+
config.set('execution', 'display_variable', dispstr)
32+
monkeypatch.setitem(os.environ, 'DISPLAY', dispstr)
33+
assert config.get_display() == dispstr
34+
35+
def test_display_noconfig_nosystem(monkeypatch):
36+
"""Check that when no display is specified, a virtual Xvfb is used"""
37+
config._display = None
38+
if config.has_option('execution', 'display_variable'):
39+
config._config.remove_option('execution', 'display_variable')
40+
monkeypatch.delitem(os.environ, 'DISPLAY', raising=False)
41+
assert int(config.get_display().split(':')[-1]) > 80

0 commit comments

Comments
 (0)