diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7a84e2620f82..21f421abfece 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -177,6 +177,7 @@ ./programs/tmux.nix ./programs/traceroute.nix ./programs/tsm-client.nix + ./programs/turbovnc.nix ./programs/udevil.nix ./programs/usbtop.nix ./programs/vim.nix diff --git a/nixos/modules/programs/turbovnc.nix b/nixos/modules/programs/turbovnc.nix new file mode 100644 index 000000000000..e6f8836aa367 --- /dev/null +++ b/nixos/modules/programs/turbovnc.nix @@ -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 so that OpenGL + programs can find Mesa's llvmpipe drivers. + + Setting this option to false 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; + + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 251f24a9a089..3ce71b0abe6d 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -408,6 +408,7 @@ in 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 {}; diff --git a/nixos/tests/turbovnc-headless-server.nix b/nixos/tests/turbovnc-headless-server.nix new file mode 100644 index 000000000000..35da9a53d2db --- /dev/null +++ b/nixos/tests/turbovnc-headless-server.nix @@ -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") + ''; + +}) diff --git a/pkgs/tools/admin/turbovnc/default.nix b/pkgs/tools/admin/turbovnc/default.nix index 16ae53d25b91..33d248ffde88 100644 --- a/pkgs/tools/admin/turbovnc/default.nix +++ b/pkgs/tools/admin/turbovnc/default.nix @@ -1,6 +1,7 @@ { lib , stdenv , fetchFromGitHub +, nixosTests # Dependencies , cmake @@ -101,6 +102,8 @@ stdenv.mkDerivation rec { --prefix PATH : ${lib.makeBinPath [ openssh ]} ''; + passthru.tests.turbovnc-headless-server = nixosTests.turbovnc-headless-server; + meta = { homepage = "https://turbovnc.org/"; license = lib.licenses.gpl2Plus;