Running Xvfb on a RHEL Shared Host (without X)
This is how I compiled the Xorg Server for RHEL on a CentOS machine with modifications to create a portable Xvfb binary.
Xvfb (X virtual framebuffer) is an in-memory display server for Linux and Unix-like OSes. It enables running graphical applications without a display such as running a headless browser (e.g. A full-blown Firefox instance without a display nor input devices). Out of the box it needs elevated access, or rather, it needs access to certain paths and auxiliary binaries that only an elevated user can control (i.e. /usr/bin/xkbcomp
or /usr/share/X11/xkb
). On a shared host X is most likely not installed, so that compounds the problem. Here is how I got it working on a shared host running 64-bit RHEL (CentOS).
Initially I copied over the Xvfb
binary and shared libraries like this to the shared host. This was sufficient to run ./Xvfb
itself, except Xvfb
wanted to compile a keymap file to /tmp/server-99.xkm
using binary /usr/bin/xkbcomp
. Suppose you were to blissfully get your hosting provider to upload xkbcomp
and its shared libraries to that path, the next problem is that the needed keymap files are in the non-existent [system path]/X11/xkb
folder (e.g. X11/xkb/rules/evdev
), but X isn’t installed. Rats.
A clever person on StackOverflow suggested hacking the Xvfb
binary with string manipulation in order to trick/bypass the xkbcomp
(keymap compiling) section. Using strings
on Xvfb
he tracked down this bit of code in xkb/ddxLoad.c
:
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | if (asprintf(&buf, "\"%s%sxkbcomp\" -w %d %s -xkm \"%s\" " "-em1 %s -emp %s -eml %s \"%s%s.xkm\"", xkbbindir, xkbbindirsep, ((xkbDebugFlags < 2) ? 1 : ((xkbDebugFlags > 10) ? 10 : (int) xkbDebugFlags)), xkbbasedirflag ? xkbbasedirflag : "", xkmfile, PRE_ERROR_MSG, ERROR_PREFIX, POST_ERROR_MSG1, xkm_output_dir, keymap) == -1) buf = NULL; free(xkbbasedirflag); if (!buf) { LogMessage(X_ERROR, "XKB: Could not invoke xkbcomp: not enough memory\n"); return NULL; } #ifndef WIN32 out = Popen(buf, "w"); #else out = fopen(tmpname, "w"); #endif |
The idea is to patch the binary with another command which merely copies a pre-compiled keymap file to the correct destination and returns successfully. The full procedure is here. This almost works, except we don’t know 100% of the time what the destination compiled keymap file is supposed to be called. It takes the form /tmp/server-[1..99].xkm
plus we only have limited real estate in replacing the string above. I tried to patch the string with a shell NOP command instead (:) and manually copied the default.xkm
file, but other problems happened later. Good try though.
Prerequisites:
- Virtual machine with 64-bit CentOS built with GLibc 2.12 (e.g. CentOS 6.8, here)
- Source code of X11 (X11R7.7 is here)
- Internet connection and root access on the source CentOS machine
First, I obtained a VMWare image of CentOS 6.8 (with GLibc 2.12) from osboxes.org. It’s already set up with X.
dhclient -v
from root to fix this. Also, be sure VMWare Tools is installed if you use the VMWare image of CentOS.I then verified the Glibc version with ldd --version
to make sure it matches that of my shared host (2.12).
1 2 | [osboxes@localhost ~]$ ldd --version ldd (GNU libc) 2.12 |
Before going any further, it’s best to yum update
all the packages before doing anything else.
yum update
on the VMWare image above will take a long time and may require 500+ MB. You may have to reinstall VMWare Tools afterwards.Next, I downloaded and unpacked the most recent X11 source code (X11R7.7) from https://www.x.org/wiki/Releases/Download/. In the source folder I needed to modify these files:
- xkb/xkbInit.c
- xkb/ddxLoad.c
Near the bottom of xkbInit.c
is the function XkbProcessArguments(int argc, char *argv[], int i)
. At the bottom I added this code to allow environment variables to change the hard-coded locations used in keymap compiling.
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 | return 2; } // ADDED - Change xkbcomp bin directory with an environment variable char *xkbBinDir = getenv("XKB_BINDIR"); if (xkbBinDir) { XkbBinDirectory = Xstrdup(xkbBinDir); } // ADDED - Change base xkb directory with an environment variable char *xkbBaseDir = getenv("XKBDIR"); if (xkbBaseDir) { XkbBaseDirectory = Xstrdup(xkbBaseDir); } return 0; } |
This got me to the point where xkbcomp
is looking in the right folders, but wouldn’t it be nice if I could omit all the extra folders and files needed to compile a default keymap?
To this end I manually compiled a default keymap default.xkm
from default.xkb
using the following description1:
1 2 3 4 5 6 7 | xkb_keymap "default" { xkb_keycodes { include "evdev+aliases(qwerty)" }; xkb_types { include "complete" }; xkb_compatibility { include "complete" }; xkb_symbols { include "pc+us+inet(evdev)" }; xkb_geometry { include "pc(pc105)" }; }; |
and running this command to compile it:
1 | xkbcomp -xkm default.xkb |
This results in a default.xkm
which can be copied into the same folder supplied to XKBDIR
. This next modification will use the above-created keymap.
Update: Here is the default.xkm file I used (right click + save as).
Near the top of ddxLoad.c
is the function RunXkbComp(xkbcomp_buffer_callback callback, void *userdata)
. I made the following changes to use the pre-made XKM file:
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | RunXkbComp(xkbcomp_buffer_callback callback, void *userdata) { FILE *out; char *buf = NULL, keymap[PATH_MAX], xkm_output_dir[PATH_MAX]; const char *emptystring = ""; char *xkbbasedirflag = NULL; const char *xkbbindir = emptystring; const char *xkbbindirsep = emptystring; #ifdef WIN32 char tmpname[PATH_MAX]; const char *xkmfile = tmpname; #else // MODIFICATION - Now using 'default.xkm' file to satisfy xkbcomp // const char *xkmfile = "-"; const char *xkmfile = "default.xkm"; #endif |
With the above two source file modifications, the xkbcomp command will be constructed like this:
153 154 155 156 157 158 159 160 161 | asprintf(&buf, "\"%s%sxkbcomp\" -w %d %s -xkm \"%s\" " "-em1 %s -emp %s -eml %s \"%s%s.xkm\"", xkbbindir, xkbbindirsep, ((xkbDebugFlags < 2) ? 1 : ((xkbDebugFlags > 10) ? 10 : (int) xkbDebugFlags)), xkbbasedirflag ? xkbbasedirflag : "", xkmfile, PRE_ERROR_MSG, ERROR_PREFIX, POST_ERROR_MSG1, xkm_output_dir, keymap) |
which effectively becomes a command similar to the following which performs a check on the default.xkm
keymap file and copies it to the /tmp
folder
1 | $XKB_BINDIR/xkbcomp -w 1 -R$XKBDIR -xkm "default.xkm" "/tmp/server-99.xkm" |
Next, several dependencies need to be installed to compile Xvfb. To find out which, I cd
‘d to the X11 source files and ran ./configure
as an unprivileged user (I used the default osboxes account in the VMWare image). ./configure
will helpfully die at dependency failures. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [osboxes@localhost xorg-server-1.17.4]$ ./configure checking for a BSD-compatible install... /usr/bin/install -c checking whether build environment is sane... yes checking for a thread-safe mkdir -p... /bin/mkdir -p checking for gawk... gawk checking whether make sets $(MAKE)... yes checking whether make supports nested variables... yes checking for style of include used by make... GNU checking for gcc... no checking for cc... no checking for cl.exe... no configure: error: in `/home/osboxes/xorg-server-1.17.4': configure: error: no acceptable C compiler found in $PATH See `config.log' for more details |
This means a compiler is not present. I use GCC – $ yum search gcc
.
The next dependency missing was pixman.
1 2 3 4 | checking for PIXMAN... no configure: error: Package requirements (pixman-1 >= 0.27.2) were not met: No package 'pixman-1' found |
When messages like this arose, I needed to find the package which supplies the missing dependency using yum search
.
1 2 3 4 5 6 7 8 9 10 11 | [root@localhost osboxes]# yum search pixman Loaded plugins: fastestmirror, refresh-packagekit, security Loading mirror speeds from cached hostfile * base: mirror.it.ubc.ca * extras: ca.mirror.babylon.network * updates: mirror.it.ubc.ca ================================================ N/S Matched: pixman ================================================= pixman.i686 : Pixel manipulation library pixman.x86_64 : Pixel manipulation library pixman-devel.i686 : Pixel manipulation library development package pixman-devel.x86_64 : Pixel manipulation library development package |
I actually want to install the devel.x86_64 version of each dependency so I can make a portable binary of Xvfb.
Here are all the dependencies and how I installed them:
- PIXMAN (pixman-1 >= 0.27.2) –
yum install pixman-devel.x86_64
- LIBDRM (libdrm >= 2.3.0) –
yum install libdrm-devel.x86_64
- XLIB (x11) –
yum install libX11-devel.x86_64
- GL (glproto >= 1.4.17 gl >= 9.2.0) –
yum install mesa-libGL-devel.x86_64
- SHA1 (OpenSSL) –
yum install openssl-devel.x86_64
- xtrans –
yum install xorg-x11-xtrans-devel.noarch
- xkbfile –
yum install libxkbfile-devel.x86_64
- xfont –
yum install libXfont-devel.x86_64
- PCIACCESS (pciaccess >= 0.12.901) –
yum install libpciaccess-devel.x86_64
- xcb/xcb_keysyms.h –
yum install xcb-util-keysyms-devel.x86_64
Finally I was able to run ./configure
and successfully finish the script.
Next, I ran make
in the same sources directory to build the Xvfb
binary. It is found in [sources]/hw/vfb
.
Now this binary, and this binary alone, I uploaded to my shared host and checked the linked dependencies:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | $ ldd ./Xvfb linux-vdso.so.1 => (0x00007ffe7d18f000) libcrypto.so.10 => /usr/lib64/libcrypto.so.10 (0x00000037c4000000) libdl.so.2 => /lib64/libdl.so.2 (0x00000037c2800000) libGL.so.1 => not found libpthread.so.0 => /lib64/libpthread.so.0 (0x00000037c2400000) libpixman-1.so.0 => /usr/lib64/libpixman-1.so.0 (0x00000037ca400000) libXfont.so.1 => /usr/lib64/libXfont.so.1 (0x00007f70b987d000) libXau.so.6 => /usr/lib64/libXau.so.6 (0x00000037c7800000) libm.so.6 => /lib64/libm.so.6 (0x00000037c3000000) librt.so.1 => /lib64/librt.so.1 (0x00000037c2c00000) libc.so.6 => /lib64/libc.so.6 (0x00000037c2000000) libz.so.1 => /lib64/libz.so.1 (0x00000037c3400000) /lib64/ld-linux-x86-64.so.2 (0x00000037c1c00000) libfreetype.so.6 => /usr/lib64/libfreetype.so.6 (0x00000037c7000000) libfontenc.so.1 => /usr/lib64/libfontenc.so.1 (0x00007f70b9675000) |
The good news is it only depends on one shared library – libGL.so.1
(548 Kb) – so I can copy that from my VMWare CentOS to my shared host. I set LD_LIBRARY_PATH
to a common path for my lib64 libraries. Here is how I found and copied the library.
On the CentOS VMWare
1 2 3 4 5 | # find / -name libGL.so.1 /usr/lib64/libGL.so.1 # readlink /usr/lib64/libGL.so.1 libGL.so.1.2.0 # cp /usr/lib64/libGL.so.1.2.0 /tmp/libGL.so.1 |
Then I copied the /tmp/libGL.so.1
to my shared host in the same directory as I set LD_LIBRARY_PATH
.
ldd libGL.so.1
shows that it depends on other libraries as well. libglapi.so.0
and libXxf86vm.so.1
need to be copied to the shared host too in a similar manner as libGL.so.1
.Once these three libraries are uploaded to the shared host, I could test Xvfb.
1 2 3 4 5 6 7 8 | drakes@a2plcpnl1390 [~/experiments/xorg/xvfb]$ ./Xvfb :99 ^Z [1]+ Stopped ./Xvfb :99 drakes@a2plcpnl1390 [~/experiments/xorg/xvfb]$ ps PID TTY TIME CMD 384377 pts/0 00:00:00 bash 505717 pts/0 00:00:00 Xvfb 506301 pts/0 00:00:00 ps |
And that is how I got Xvfb to run on a shared host without X and without root access.