1{ pkgs, lib, ... }:
2{
3 name = "turbovnc-headless-server";
4 meta = {
5 maintainers = with lib.maintainers; [ nh2 ];
6 };
7
8 nodes.machine =
9 { pkgs, ... }:
10 {
11
12 environment.systemPackages = with pkgs; [
13 mesa-demos
14 procps # for `pkill`, `pidof` in the test
15 scrot # for screenshotting Xorg
16 turbovnc
17 ];
18
19 programs.turbovnc.ensureHeadlessSoftwareOpenGL = true;
20
21 networking.firewall = {
22 # Reject instead of drop, for failures instead of hangs.
23 rejectPackets = true;
24 allowedTCPPorts = [
25 5900 # VNC :0, for seeing what's going on in the server
26 ];
27 };
28
29 # So that we can ssh into the VM, see e.g.
30 # https://nixos.org/manual/nixos/stable/#sec-nixos-test-port-forwarding
31 services.openssh.enable = true;
32 users.mutableUsers = false;
33 # `test-instrumentation.nix` already sets an empty root password.
34 # The following have to all be set to allow an empty SSH login password.
35 services.openssh.settings.PermitRootLogin = "yes";
36 services.openssh.settings.PermitEmptyPasswords = "yes";
37 security.pam.services.sshd.allowNullPassword = true; # the default `UsePam yes` makes this necessary
38 };
39
40 testScript = ''
41 def wait_until_terminated_or_succeeds(
42 termination_check_shell_command,
43 success_check_shell_command,
44 get_detail_message_fn,
45 retries=60,
46 retry_sleep=0.5,
47 ):
48 def check_success():
49 command_exit_code, _output = machine.execute(success_check_shell_command)
50 return command_exit_code == 0
51
52 for _ in range(retries):
53 exit_check_exit_code, _output = machine.execute(termination_check_shell_command)
54 is_terminated = exit_check_exit_code != 0
55 if is_terminated:
56 if check_success():
57 return
58 else:
59 details = get_detail_message_fn()
60 raise Exception(
61 f"termination check ({termination_check_shell_command}) triggered without command succeeding ({success_check_shell_command}); details: {details}"
62 )
63 else:
64 if check_success():
65 return
66 import time
67 time.sleep(retry_sleep)
68
69 if not check_success():
70 details = get_detail_message_fn()
71 raise Exception(
72 f"action timed out ({success_check_shell_command}); details: {details}"
73 )
74
75
76 # Below we use the pattern:
77 # (cmd | tee stdout.log) 3>&1 1>&2 2>&3 | tee stderr.log
78 # to capture both stderr and stdout while also teeing them, see:
79 # https://unix.stackexchange.com/questions/6430/how-to-redirect-stderr-and-stdout-to-different-files-and-also-display-in-termina/6431#6431
80
81
82 # Starts headless VNC server, backgrounding it.
83 def start_xvnc():
84 xvnc_command = " ".join(
85 [
86 "Xvnc",
87 ":0",
88 "-iglx",
89 "-auth /root/.Xauthority",
90 "-geometry 1240x900",
91 "-depth 24",
92 "-rfbwait 5000",
93 "-deferupdate 1",
94 "-verbose",
95 "-securitytypes none",
96 # We don't enforce localhost listening such that we
97 # can connect from outside the VM using
98 # env QEMU_NET_OPTS=hostfwd=tcp::5900-:5900 $(nix-build nixos/tests/turbovnc-headless-server.nix -A driver)/bin/nixos-test-driver
99 # for testing purposes, and so that we can in the future
100 # add another test case that connects the TurboVNC client.
101 # "-localhost",
102 ]
103 )
104 machine.execute(
105 # Note trailing & for backgrounding.
106 f"({xvnc_command} | tee /tmp/Xvnc.stdout) 3>&1 1>&2 2>&3 | tee /tmp/Xvnc.stderr >&2 &",
107 )
108
109
110 # Waits until the server log message that tells us that GLX is ready
111 # (requires `-verbose` above), avoiding screenshoting racing below.
112 def wait_until_xvnc_glx_ready():
113 machine.wait_until_succeeds("test -f /tmp/Xvnc.stderr")
114 wait_until_terminated_or_succeeds(
115 termination_check_shell_command="pidof Xvnc",
116 success_check_shell_command="grep 'GLX: Initialized DRISWRAST' /tmp/Xvnc.stderr",
117 get_detail_message_fn=lambda: "Contents of /tmp/Xvnc.stderr:\n"
118 + machine.succeed("cat /tmp/Xvnc.stderr"),
119 )
120
121
122 # Starts glxgears, backgrounding it. Waits until it prints the `GL_RENDERER`.
123 # Does not quit glxgears.
124 def test_glxgears_prints_renderer():
125 machine.execute(
126 # Note trailing & for backgrounding.
127 "(env DISPLAY=:0 glxgears -info | tee /tmp/glxgears.stdout) 3>&1 1>&2 2>&3 | tee /tmp/glxgears.stderr >&2 &"
128 )
129 machine.wait_until_succeeds("test -f /tmp/glxgears.stderr")
130 wait_until_terminated_or_succeeds(
131 termination_check_shell_command="pidof glxgears",
132 success_check_shell_command="grep 'GL_RENDERER' /tmp/glxgears.stdout",
133 get_detail_message_fn=lambda: "Contents of /tmp/glxgears.stderr:\n"
134 + machine.succeed("cat /tmp/glxgears.stderr"),
135 )
136
137
138 with subtest("Start Xvnc"):
139 start_xvnc()
140 wait_until_xvnc_glx_ready()
141
142 with subtest("Run 3D application (glxgears)"):
143 test_glxgears_prints_renderer()
144
145 # Take screenshot; should display the glxgears.
146 machine.succeed("scrot --display :0 /tmp/glxgears.png")
147
148 # Copy files down.
149 machine.copy_from_vm("/tmp/glxgears.png")
150 machine.copy_from_vm("/tmp/glxgears.stdout")
151 machine.copy_from_vm("/tmp/Xvnc.stdout")
152 machine.copy_from_vm("/tmp/Xvnc.stderr")
153 '';
154
155}