turbovnc: Add programs.turbovnc, add test for headless software OpenGL

Changed files
+230
nixos
pkgs
tools
admin
turbovnc
+1
nixos/modules/module-list.nix
···
./programs/tmux.nix
./programs/traceroute.nix
./programs/tsm-client.nix
+
./programs/turbovnc.nix
./programs/udevil.nix
./programs/usbtop.nix
./programs/vim.nix
+54
nixos/modules/programs/turbovnc.nix
···
+
# Global configuration for the SSH client.
+
+
{ config, lib, pkgs, ... }:
+
+
with lib;
+
+
let
+
cfg = config.programs.turbovnc;
+
in
+
{
+
options = {
+
+
programs.turbovnc = {
+
+
ensureHeadlessSoftwareOpenGL = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
Whether to set up NixOS such that TurboVNC's built-in software OpenGL
+
implementation works.
+
+
This will enable <option>hardware.opengl.enable</option> so that OpenGL
+
programs can find Mesa's llvmpipe drivers.
+
+
Setting this option to <code>false</code> does not mean that software
+
OpenGL won't work; it may still work depending on your system
+
configuration.
+
+
This option is also intended to generate warnings if you are using some
+
configuration that's incompatible with using headless software OpenGL
+
in TurboVNC.
+
'';
+
};
+
+
};
+
+
};
+
+
config = mkIf cfg.ensureHeadlessSoftwareOpenGL {
+
+
# TurboVNC has builtin support for Mesa llvmpipe's `swrast`
+
# software rendering to implemnt GLX (OpenGL on Xorg).
+
# However, just building TurboVNC with support for that is not enough
+
# (it only takes care of the X server side part of OpenGL);
+
# the indiviudual applications (e.g. `glxgears`) also need to directly load
+
# the OpenGL libs.
+
# Thus, this creates `/run/opengl-driver` populated by Mesa so that the applications
+
# can find the llvmpipe `swrast.so` software rendering DRI lib via `libglvnd`.
+
# This comment exists to explain why `hardware.` is involved,
+
# even though 100% software rendering is used.
+
hardware.opengl.enable = true;
+
+
};
+
}
+1
nixos/tests/all-tests.nix
···
trickster = handleTest ./trickster.nix {};
trilium-server = handleTestOn ["x86_64-linux"] ./trilium-server.nix {};
tuptime = handleTest ./tuptime.nix {};
+
turbovnc-headless-server = handleTest ./turbovnc-headless-server.nix {};
ucg = handleTest ./ucg.nix {};
udisks2 = handleTest ./udisks2.nix {};
unbound = handleTest ./unbound.nix {};
+171
nixos/tests/turbovnc-headless-server.nix
···
+
import ./make-test-python.nix ({ pkgs, lib, ... }: {
+
name = "turbovnc-headless-server";
+
meta = {
+
maintainers = with lib.maintainers; [ nh2 ];
+
};
+
+
machine = { pkgs, ... }: {
+
+
environment.systemPackages = with pkgs; [
+
glxinfo
+
procps # for `pkill`, `pidof` in the test
+
scrot # for screenshotting Xorg
+
turbovnc
+
];
+
+
programs.turbovnc.ensureHeadlessSoftwareOpenGL = true;
+
+
networking.firewall = {
+
# Reject instead of drop, for failures instead of hangs.
+
rejectPackets = true;
+
allowedTCPPorts = [
+
5900 # VNC :0, for seeing what's going on in the server
+
];
+
};
+
+
# So that we can ssh into the VM, see e.g.
+
# http://blog.patapon.info/nixos-local-vm/#accessing-the-vm-with-ssh
+
services.openssh.enable = true;
+
services.openssh.permitRootLogin = "yes";
+
users.extraUsers.root.password = "";
+
users.mutableUsers = false;
+
};
+
+
testScript = ''
+
def wait_until_terminated_or_succeeds(
+
termination_check_shell_command,
+
success_check_shell_command,
+
get_detail_message_fn,
+
retries=60,
+
retry_sleep=0.5,
+
):
+
def check_success():
+
command_exit_code, _output = machine.execute(success_check_shell_command)
+
return command_exit_code == 0
+
+
for _ in range(retries):
+
exit_check_exit_code, _output = machine.execute(termination_check_shell_command)
+
is_terminated = exit_check_exit_code != 0
+
if is_terminated:
+
if check_success():
+
return
+
else:
+
details = get_detail_message_fn()
+
raise Exception(
+
f"termination check ({termination_check_shell_command}) triggered without command succeeding ({success_check_shell_command}); details: {details}"
+
)
+
else:
+
if check_success():
+
return
+
time.sleep(retry_sleep)
+
+
if not check_success():
+
details = get_detail_message_fn()
+
raise Exception(
+
f"action timed out ({success_check_shell_command}); details: {details}"
+
)
+
+
+
# Below we use the pattern:
+
# (cmd | tee stdout.log) 3>&1 1>&2 2>&3 | tee stderr.log
+
# to capture both stderr and stdout while also teeing them, see:
+
# https://unix.stackexchange.com/questions/6430/how-to-redirect-stderr-and-stdout-to-different-files-and-also-display-in-termina/6431#6431
+
+
+
# Starts headless VNC server, backgrounding it.
+
def start_xvnc():
+
xvnc_command = " ".join(
+
[
+
"Xvnc",
+
":0",
+
"-iglx",
+
"-auth /root/.Xauthority",
+
"-geometry 1240x900",
+
"-depth 24",
+
"-rfbwait 5000",
+
"-deferupdate 1",
+
"-verbose",
+
"-securitytypes none",
+
# We don't enforce localhost listening such that we
+
# can connect from outside the VM using
+
# env QEMU_NET_OPTS=hostfwd=tcp::5900-:5900 $(nix-build nixos/tests/turbovnc-headless-server.nix -A driver)/bin/nixos-test-driver
+
# for testing purposes, and so that we can in the future
+
# add another test case that connects the TurboVNC client.
+
# "-localhost",
+
]
+
)
+
machine.execute(
+
# Note trailing & for backgrounding.
+
f"({xvnc_command} | tee /tmp/Xvnc.stdout) 3>&1 1>&2 2>&3 | tee /tmp/Xvnc.stderr &",
+
)
+
+
+
# Waits until the server log message that tells us that GLX is ready
+
# (requires `-verbose` above), avoiding screenshoting racing below.
+
def wait_until_xvnc_glx_ready():
+
machine.wait_until_succeeds("test -f /tmp/Xvnc.stderr")
+
wait_until_terminated_or_succeeds(
+
termination_check_shell_command="pidof Xvnc",
+
success_check_shell_command="grep 'GLX: Initialized DRISWRAST' /tmp/Xvnc.stderr",
+
get_detail_message_fn=lambda: "Contents of /tmp/Xvnc.stderr:\n"
+
+ machine.succeed("cat /tmp/Xvnc.stderr"),
+
)
+
+
+
# Checks that we detect glxgears failing when
+
# `LIBGL_DRIVERS_PATH=/nonexistent` is set
+
# (in which case software rendering should not work).
+
def test_glxgears_failing_with_bad_driver_path():
+
machine.execute(
+
# Note trailing & for backgrounding.
+
"(env DISPLAY=:0 LIBGL_DRIVERS_PATH=/nonexistent glxgears -info | tee /tmp/glxgears-should-fail.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears-should-fail.stderr &"
+
)
+
machine.wait_until_succeeds("test -f /tmp/glxgears-should-fail.stderr")
+
wait_until_terminated_or_succeeds(
+
termination_check_shell_command="pidof glxgears",
+
success_check_shell_command="grep 'libGL error: failed to load driver: swrast' /tmp/glxgears-should-fail.stderr",
+
get_detail_message_fn=lambda: "Contents of /tmp/glxgears-should-fail.stderr:\n"
+
+ machine.succeed("cat /tmp/glxgears-should-fail.stderr"),
+
)
+
machine.wait_until_fails("pidof glxgears")
+
+
+
# Starts glxgears, backgrounding it. Waits until it prints the `GL_RENDERER`.
+
# Does not quit glxgears.
+
def test_glxgears_prints_renderer():
+
machine.execute(
+
# Note trailing & for backgrounding.
+
"(env DISPLAY=:0 glxgears -info | tee /tmp/glxgears.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears.stderr &"
+
)
+
machine.wait_until_succeeds("test -f /tmp/glxgears.stderr")
+
wait_until_terminated_or_succeeds(
+
termination_check_shell_command="pidof glxgears",
+
success_check_shell_command="grep 'GL_RENDERER' /tmp/glxgears.stdout",
+
get_detail_message_fn=lambda: "Contents of /tmp/glxgears.stderr:\n"
+
+ machine.succeed("cat /tmp/glxgears.stderr"),
+
)
+
+
+
with subtest("Start Xvnc"):
+
start_xvnc()
+
wait_until_xvnc_glx_ready()
+
+
with subtest("Ensure bad driver path makes glxgears fail"):
+
test_glxgears_failing_with_bad_driver_path()
+
+
with subtest("Run 3D application (glxgears)"):
+
test_glxgears_prints_renderer()
+
+
# Take screenshot; should display the glxgears.
+
machine.succeed("scrot --display :0 /tmp/glxgears.png")
+
+
# Copy files down.
+
machine.copy_from_vm("/tmp/glxgears.png")
+
machine.copy_from_vm("/tmp/glxgears.stdout")
+
machine.copy_from_vm("/tmp/glxgears-should-fail.stdout")
+
machine.copy_from_vm("/tmp/glxgears-should-fail.stderr")
+
machine.copy_from_vm("/tmp/Xvnc.stdout")
+
machine.copy_from_vm("/tmp/Xvnc.stderr")
+
'';
+
+
})
+3
pkgs/tools/admin/turbovnc/default.nix
···
{ lib
, stdenv
, fetchFromGitHub
+
, nixosTests
# Dependencies
, cmake
···
--prefix JAVA_HOME : "${lib.makeLibraryPath [ openjdk ]}/openjdk" \
--prefix PATH : ${lib.makeBinPath [ openssh ]}
'';
+
+
passthru.tests.turbovnc-headless-server = nixosTests.turbovnc-headless-server;
meta = {
homepage = "https://turbovnc.org/";