parent
24aa5fefe4
commit
b46f6cba79
@ -0,0 +1,54 @@ |
||||
# 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; |
||||
|
||||
}; |
||||
} |
@ -0,0 +1,171 @@ |
||||
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") |
||||
''; |
||||
|
||||
}) |
Loading…
Reference in new issue