Skip to content

Commit 68b6a59

Browse files
authored
feat: add experimental Wayland desktop env (#152)
1 parent 46887c6 commit 68b6a59

35 files changed

Lines changed: 1148 additions & 49 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 0.18.1 - Unreleased
44

5+
### Added
6+
7+
- Added an experimental Linux `--desktop-env wayland` profile using Sway, WayVNC, Wayland browser launch env, and `grim` screenshots while keeping XFCE as the default desktop.
8+
59
### Fixed
610

711
- Fixed Linux desktop theme setup so WebVNC sessions install and prefer native Arc-Dark/other dark XFCE themes instead of custom-painting panel and window chrome.

docs/commands/run.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ profile directory or app-specific auth artifact when tests need a logged-in
8686
browser.
8787

8888
`--desktop` provisions or requires a visible desktop/VNC session and injects
89-
`CRABBOX_DESKTOP=1`; POSIX desktop targets also use `DISPLAY=:99`. It does not
90-
imply a browser. Use `--desktop --browser` for headed browser automation in the
91-
VNC-visible session.
89+
`CRABBOX_DESKTOP=1`. Linux defaults to XFCE on `DISPLAY=:99`; leases created
90+
with `--desktop-env wayland` expose `XDG_RUNTIME_DIR` and `WAYLAND_DISPLAY`
91+
from `/var/lib/crabbox/desktop.env` instead. It does not imply a browser. Use
92+
`--desktop --browser` for headed browser automation in the VNC-visible session.
9293

9394
`--tailscale` asks new managed Linux leases to join the configured tailnet.
9495
`--network` selects how Crabbox resolves SSH for reused leases and for the final

docs/commands/warmup.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,11 @@ Warmup records a local claim tying the lease to the current repo; `--reclaim` ov
186186
browser automation. Managed Linux tries Google Chrome stable first, then a
187187
Chromium package fallback.
188188

189-
`--desktop` provisions Xvfb, slim XFCE, and loopback-bound x11vnc for visible UI
190-
automation and operator takeover. It does not imply a browser. Use
191-
`--desktop --browser` when a headed browser should run in the visible display.
189+
`--desktop` provisions a visible UI and loopback-bound VNC for automation and
190+
operator takeover. Linux defaults to Xvfb, slim XFCE, and x11vnc; use
191+
`--desktop-env wayland` for the experimental Sway/WayVNC profile on Ubuntu
192+
24.04-compatible images. It does not imply a browser. Use `--desktop --browser`
193+
when a headed browser should run in the visible display.
192194

193195
`--code` provisions `code-server` for Linux leases and enables
194196
`crabbox code --id <lease>` to bridge the workspace through the authenticated

internal/cli/artifacts.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,9 @@ func (a App) artifactsVideo(ctx context.Context, args []string) error {
385385
if contactWidth <= 0 {
386386
return exit(2, "artifacts video --contact-sheet-width must be positive")
387387
}
388+
if err := rejectWaylandDesktopVideoTarget(ctx, target, "artifacts video"); err != nil {
389+
return err
390+
}
388391
if err := captureDesktopVideo(ctx, target, output, duration, fps); err != nil {
389392
printRescue(a.Stdout, classifyDesktopFailure(err.Error()), err.Error(), desktopDoctorCommand(rescueContext{Cfg: cfg, Target: target, LeaseID: leaseID}))
390393
return err
@@ -649,6 +652,9 @@ func captureDesktopVideo(ctx context.Context, target SSHTarget, outputPath strin
649652
if target.TargetOS != targetLinux {
650653
return exit(2, "artifacts video currently requires target=linux or native Windows desktop capture")
651654
}
655+
if err := rejectWaylandDesktopVideoTarget(ctx, target, "desktop video capture"); err != nil {
656+
return err
657+
}
652658
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil && filepath.Dir(outputPath) != "." {
653659
return exit(2, "create video directory: %v", err)
654660
}
@@ -674,6 +680,11 @@ func desktopVideoRemoteCommand(duration time.Duration, fps float64) string {
674680
seconds := strconv.FormatFloat(duration.Seconds(), 'f', 3, 64)
675681
frameRate := strconv.FormatFloat(fps, 'f', 3, 64)
676682
return fmt.Sprintf(`set -eu
683+
if [ -f /var/lib/crabbox/desktop.env ]; then . /var/lib/crabbox/desktop.env; fi
684+
if [ "${CRABBOX_DESKTOP_ENV:-xfce}" = "wayland" ]; then
685+
echo "video capture does not support --desktop-env wayland yet; use an X11 desktop" >&2
686+
exit 2
687+
fi
677688
export DISPLAY="${DISPLAY:-:99}"
678689
if ! command -v ffmpeg >/dev/null 2>&1; then
679690
echo "missing ffmpeg; warm a new --desktop lease or install ffmpeg" >&2
@@ -689,6 +700,17 @@ ffmpeg -hide_banner -loglevel error -y -f x11grab -video_size "$size" -framerate
689700
`, frameRate, seconds)
690701
}
691702

703+
func rejectWaylandDesktopVideoTarget(ctx context.Context, target SSHTarget, command string) error {
704+
if target.TargetOS != targetLinux {
705+
return nil
706+
}
707+
env, err := probeDesktopEnv(ctx, target)
708+
if err != nil {
709+
return nil
710+
}
711+
return rejectWaylandDesktopVideoEnv(env, command)
712+
}
713+
692714
func captureWindowsDesktopVideo(ctx context.Context, target SSHTarget, outputPath string, duration time.Duration, fps float64) error {
693715
if _, err := exec.LookPath("ffmpeg"); err != nil {
694716
return exit(2, "ffmpeg is required to encode Windows desktop video locally: %v", err)

internal/cli/bootstrap.go

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -592,10 +592,15 @@ func cloudInitOptionalReadyChecks(cfg Config) string {
592592
b.WriteString(" grep -Eq '^100\\.' /var/lib/crabbox/tailscale-ipv4\n")
593593
}
594594
if cfg.Desktop {
595-
b.WriteString(" systemctl is-active --quiet crabbox-xvfb.service\n")
596-
b.WriteString(" systemctl is-active --quiet crabbox-desktop.service\n")
597-
b.WriteString(" systemctl is-active --quiet crabbox-desktop-session.service\n")
598-
b.WriteString(" systemctl is-active --quiet crabbox-x11vnc.service\n")
595+
if normalizedDesktopEnv(cfg.DesktopEnv) == desktopEnvWayland {
596+
b.WriteString(" systemctl is-active --quiet crabbox-desktop.service\n")
597+
b.WriteString(" systemctl is-active --quiet crabbox-wayvnc.service\n")
598+
} else {
599+
b.WriteString(" systemctl is-active --quiet crabbox-xvfb.service\n")
600+
b.WriteString(" systemctl is-active --quiet crabbox-desktop.service\n")
601+
b.WriteString(" systemctl is-active --quiet crabbox-desktop-session.service\n")
602+
b.WriteString(" systemctl is-active --quiet crabbox-x11vnc.service\n")
603+
}
599604
b.WriteString(" ss -ltn | grep -q '127.0.0.1:5900'\n")
600605
}
601606
if cfg.Browser {
@@ -616,7 +621,9 @@ func cloudInitOptionalWriteFiles(cfg Config) string {
616621
if cfg.Provider == "gcp" {
617622
parts = append(parts, cloudInitGCPExpiryGuardFiles())
618623
}
619-
if cfg.Desktop {
624+
if cfg.Desktop && normalizedDesktopEnv(cfg.DesktopEnv) == desktopEnvWayland {
625+
parts = append(parts, cloudInitWaylandDesktopWriteFiles())
626+
} else if cfg.Desktop {
620627
parts = append(parts, ` - path: /etc/systemd/system/crabbox-xvfb.service
621628
permissions: '0644'
622629
content: |
@@ -882,12 +889,149 @@ func cloudInitOptionalWriteFiles(cfg Config) string {
882889
return strings.Join(parts, "\n")
883890
}
884891

892+
func cloudInitWaylandDesktopWriteFiles() string {
893+
return ` - path: /usr/local/bin/crabbox-start-wayland-desktop
894+
permissions: '0755'
895+
content: |
896+
#!/bin/sh
897+
set -eu
898+
runtime="${XDG_RUNTIME_DIR:-/tmp/crabbox-runtime-$(id -u)}"
899+
install -d -m 0700 "$runtime"
900+
export XDG_RUNTIME_DIR="$runtime"
901+
export WLR_BACKENDS=headless
902+
export WLR_LIBINPUT_NO_DEVICES=1
903+
export WLR_RENDERER="${WLR_RENDERER:-pixman}"
904+
export MOZ_ENABLE_WAYLAND=1
905+
exec dbus-run-session sway --unsupported-gpu
906+
- path: /usr/local/bin/crabbox-sway-status
907+
permissions: '0755'
908+
content: |
909+
#!/bin/sh
910+
while :; do
911+
printf 'Crabbox Wayland - Mod+Enter/D terminal - %s\n' "$(date '+%H:%M:%S')"
912+
sleep 1
913+
done
914+
- path: /etc/systemd/system/crabbox-desktop.service
915+
permissions: '0644'
916+
content: |
917+
[Unit]
918+
Description=Crabbox Wayland desktop session
919+
After=network.target
920+
921+
[Service]
922+
User=crabbox
923+
Environment=WLR_BACKENDS=headless
924+
Environment=WLR_LIBINPUT_NO_DEVICES=1
925+
Environment=WLR_RENDERER=pixman
926+
ExecStart=/usr/local/bin/crabbox-start-wayland-desktop
927+
Restart=always
928+
929+
[Install]
930+
WantedBy=multi-user.target
931+
- path: /usr/local/bin/crabbox-start-wayvnc
932+
permissions: '0755'
933+
content: |
934+
#!/bin/sh
935+
set -eu
936+
runtime="${XDG_RUNTIME_DIR:-/tmp/crabbox-runtime-$(id -u)}"
937+
export XDG_RUNTIME_DIR="$runtime"
938+
for i in $(seq 1 60); do
939+
for socket in "$XDG_RUNTIME_DIR"/wayland-*; do
940+
[ -S "$socket" ] || continue
941+
export WAYLAND_DISPLAY="${socket##*/}"
942+
cat >/var/lib/crabbox/desktop.env <<EOF
943+
CRABBOX_DESKTOP_ENV=wayland
944+
XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR
945+
WAYLAND_DISPLAY=$WAYLAND_DISPLAY
946+
EOF
947+
exec /usr/bin/wayvnc --config "$HOME/.config/wayvnc/config" --render-cursor --max-fps=30
948+
done
949+
sleep 1
950+
done
951+
echo "wayland socket not ready" >&2
952+
exit 1
953+
- path: /etc/systemd/system/crabbox-wayvnc.service
954+
permissions: '0644'
955+
content: |
956+
[Unit]
957+
Description=Crabbox loopback WayVNC server
958+
After=crabbox-desktop.service
959+
Requires=crabbox-desktop.service
960+
961+
[Service]
962+
User=crabbox
963+
ExecStart=/usr/local/bin/crabbox-start-wayvnc
964+
Restart=always
965+
966+
[Install]
967+
WantedBy=multi-user.target
968+
`
969+
}
970+
885971
func cloudInitOptionalBootstrap(cfg Config) string {
886972
var parts []string
887973
if cfg.Tailscale.Enabled {
888974
parts = append(parts, cloudInitTailscaleBootstrap(cfg))
889975
}
890-
if cfg.Desktop {
976+
if cfg.Desktop && normalizedDesktopEnv(cfg.DesktopEnv) == desktopEnvWayland {
977+
parts = append(parts, ` retry apt-get install -y --no-install-recommends sway wayvnc foot grim slurp wtype wl-clipboard dbus-user-session xwayland xdg-desktop-portal-wlr fonts-dejavu-core fonts-liberation iproute2 openssl procps
978+
install -d -m 0750 -o crabbox -g crabbox /var/lib/crabbox
979+
if [ ! -s /var/lib/crabbox/vnc.password ]; then
980+
(umask 077 && openssl rand -base64 18 > /var/lib/crabbox/vnc.password)
981+
fi
982+
chown crabbox:crabbox /var/lib/crabbox/vnc.password
983+
chmod 0600 /var/lib/crabbox/vnc.password
984+
crabbox_uid="$(id -u crabbox)"
985+
crabbox_runtime="/tmp/crabbox-runtime-$crabbox_uid"
986+
install -d -m 0700 -o crabbox -g crabbox "$crabbox_runtime"
987+
install -d -m 0700 -o crabbox -g crabbox /home/crabbox/.config/sway /home/crabbox/.config/wayvnc
988+
cat >/home/crabbox/.config/sway/config <<'SWAY'
989+
set $mod Mod4
990+
set $term foot
991+
set $menu foot --title='Crabbox Desktop'
992+
output * resolution 1920x1080 position 0,0
993+
default_border pixel 2
994+
default_floating_border pixel 2
995+
gaps inner 8
996+
gaps outer 12
997+
focus_follows_mouse no
998+
client.focused #2dd4bf #1f2937 #f8fafc #2dd4bf #2dd4bf
999+
client.unfocused #475569 #111827 #cbd5e1 #475569 #475569
1000+
bindsym $mod+Return exec $term
1001+
bindsym $mod+d exec $menu
1002+
bindsym $mod+Shift+c reload
1003+
bindsym $mod+Shift+q kill
1004+
for_window [app_id="foot"] floating enable, resize set width 900 px height 640 px, move position 48 px 72 px
1005+
for_window [app_id="google-chrome"] floating enable, resize set width 1500 px height 900 px, move position 360 px 96 px
1006+
for_window [app_id="chromium"] floating enable, resize set width 1500 px height 900 px, move position 360 px 96 px
1007+
exec foot --title='Crabbox Desktop'
1008+
bar {
1009+
position top
1010+
status_command /usr/local/bin/crabbox-sway-status
1011+
colors {
1012+
background #111827
1013+
statusline #e5e7eb
1014+
focused_workspace #2dd4bf #1f2937 #f8fafc
1015+
inactive_workspace #1f2937 #111827 #cbd5e1
1016+
}
1017+
}
1018+
SWAY
1019+
cat >/home/crabbox/.config/wayvnc/config <<'WAYVNC'
1020+
address=127.0.0.1
1021+
port=5900
1022+
enable_auth=false
1023+
xkb_layout=us
1024+
WAYVNC
1025+
cat >/var/lib/crabbox/desktop.env <<EOF
1026+
CRABBOX_DESKTOP_ENV=wayland
1027+
XDG_RUNTIME_DIR=$crabbox_runtime
1028+
WAYLAND_DISPLAY=wayland-1
1029+
EOF
1030+
chown -R crabbox:crabbox /home/crabbox/.config/sway /home/crabbox/.config/wayvnc /var/lib/crabbox/desktop.env
1031+
chmod 0644 /var/lib/crabbox/desktop.env
1032+
systemctl daemon-reload
1033+
systemctl enable --now crabbox-desktop.service crabbox-wayvnc.service`)
1034+
} else if cfg.Desktop {
8911035
parts = append(parts, ` retry apt-get install -y --no-install-recommends xvfb xfce4-session xfwm4 xfce4-panel xfdesktop4 xfce4-terminal xfconf xfce4-settings x11vnc xauth dbus-x11 x11-xserver-utils xterm scrot ffmpeg xdotool wmctrl xclip xsel fonts-dejavu-core fonts-liberation iproute2 openssl arc-theme
8921036
install -d -m 0750 -o crabbox -g crabbox /var/lib/crabbox
8931037
if [ ! -s /var/lib/crabbox/vnc.password ]; then
@@ -896,6 +1040,9 @@ func cloudInitOptionalBootstrap(cfg Config) string {
8961040
x11vnc -storepasswd "$(cat /var/lib/crabbox/vnc.password)" /var/lib/crabbox/vnc.pass >/dev/null
8971041
chown crabbox:crabbox /var/lib/crabbox/vnc.password /var/lib/crabbox/vnc.pass
8981042
chmod 0600 /var/lib/crabbox/vnc.password /var/lib/crabbox/vnc.pass
1043+
printf 'CRABBOX_DESKTOP_ENV=xfce\nDISPLAY=:99\n' >/var/lib/crabbox/desktop.env
1044+
chown crabbox:crabbox /var/lib/crabbox/desktop.env
1045+
chmod 0644 /var/lib/crabbox/desktop.env
8991046
CRABBOX_DESKTOP_USER=crabbox /usr/local/bin/crabbox-configure-desktop-theme
9001047
systemctl daemon-reload
9011048
systemctl enable --now crabbox-xvfb.service crabbox-desktop.service crabbox-desktop-session.service crabbox-x11vnc.service`)
@@ -930,7 +1077,11 @@ func cloudInitOptionalBootstrap(cfg Config) string {
9301077
install -d -m 0755 /etc/opt/chrome/policies/managed /etc/chromium/policies/managed
9311078
printf '%s\n' '{"DefaultBrowserSettingEnabled":false,"MetricsReportingEnabled":false,"PromotionalTabsEnabled":false}' > /etc/opt/chrome/policies/managed/crabbox.json
9321079
cp /etc/opt/chrome/policies/managed/crabbox.json /etc/chromium/policies/managed/crabbox.json
933-
printf '%s\n' '#!/bin/sh' "exec \"$browser_path\" --no-first-run --no-default-browser-check --disable-default-apps --window-size=1500,900 --window-position=80,80 \"\$@\"" > "$browser_wrapper"
1080+
if [ -f /var/lib/crabbox/desktop.env ] && grep -q '^CRABBOX_DESKTOP_ENV=wayland$' /var/lib/crabbox/desktop.env; then
1081+
printf '%s\n' '#!/bin/sh' 'if [ -f /var/lib/crabbox/desktop.env ]; then . /var/lib/crabbox/desktop.env; fi' 'export XDG_RUNTIME_DIR WAYLAND_DISPLAY' 'export MOZ_ENABLE_WAYLAND=1' "exec \"$browser_path\" --no-first-run --no-default-browser-check --disable-default-apps --hide-crash-restore-bubble --ozone-platform=wayland --window-size=1500,900 --window-position=80,80 \"\$@\"" > "$browser_wrapper"
1082+
else
1083+
printf '%s\n' '#!/bin/sh' "exec \"$browser_path\" --no-first-run --no-default-browser-check --disable-default-apps --hide-crash-restore-bubble --window-size=1500,900 --window-position=80,80 \"\$@\"" > "$browser_wrapper"
1084+
fi
9341085
chmod 0755 "$browser_wrapper"
9351086
printf 'CHROME_BIN=%s\nBROWSER=%s\n' "$browser_wrapper" "$browser_wrapper" > /var/lib/crabbox/browser.env
9361087
chown crabbox:crabbox /var/lib/crabbox/browser.env

internal/cli/bootstrap_test.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,55 @@ func TestCloudInitDesktopProfile(t *testing.T) {
134134
}
135135
}
136136

137+
func TestCloudInitWaylandDesktopProfile(t *testing.T) {
138+
cfg := baseConfig()
139+
cfg.Desktop = true
140+
cfg.Browser = true
141+
cfg.DesktopEnv = "wayland"
142+
got := cloudInit(cfg, "ssh-ed25519 test")
143+
for _, want := range []string{
144+
"sway wayvnc foot grim slurp wtype wl-clipboard",
145+
"xdg-desktop-portal-wlr",
146+
"/usr/local/bin/crabbox-start-wayland-desktop",
147+
"/etc/systemd/system/crabbox-wayvnc.service",
148+
"CRABBOX_DESKTOP_ENV=wayland",
149+
"WLR_BACKENDS=headless",
150+
"WLR_RENDERER=pixman",
151+
"exec dbus-run-session sway --unsupported-gpu",
152+
"bindsym $mod+Return exec $term",
153+
"bindsym $mod+d exec $menu",
154+
"set $menu foot --title='Crabbox Desktop'",
155+
" for_window [app_id=\"google-chrome\"] floating enable",
156+
"/usr/local/bin/crabbox-sway-status",
157+
"status_command /usr/local/bin/crabbox-sway-status",
158+
`for socket in "$XDG_RUNTIME_DIR"/wayland-*`,
159+
`WAYLAND_DISPLAY="${socket##*/}"`,
160+
"wayvnc --config \"$HOME/.config/wayvnc/config\" --render-cursor --max-fps=30",
161+
"systemctl is-active --quiet crabbox-wayvnc.service",
162+
"systemctl enable --now crabbox-desktop.service crabbox-wayvnc.service",
163+
"--ozone-platform=wayland",
164+
} {
165+
if !strings.Contains(got, want) {
166+
t.Fatalf("cloudInit(wayland desktop) missing %q", want)
167+
}
168+
}
169+
for _, notWant := range []string{
170+
"startxfce4",
171+
"crabbox-x11vnc.service",
172+
"x11vnc -storepasswd",
173+
"crabbox-xvfb.service",
174+
"XDG_RUNTIME_DIR=/tmp/crabbox-runtime-1000",
175+
"\nset $mod",
176+
"\nSWAY",
177+
"\nCRABBOX_DESKTOP_ENV=wayland",
178+
"\nEOF",
179+
} {
180+
if strings.Contains(got, notWant) {
181+
t.Fatalf("cloudInit(wayland desktop) contains %q", notWant)
182+
}
183+
}
184+
}
185+
137186
func TestCloudInitBrowserWrapper(t *testing.T) {
138187
cfg := baseConfig()
139188
cfg.Browser = true
@@ -148,12 +197,12 @@ func TestCloudInitBrowserWrapper(t *testing.T) {
148197
"apt-cache show chromium-browser",
149198
"/etc/opt/chrome/policies/managed/crabbox.json",
150199
"/usr/local/bin/crabbox-browser",
151-
`--no-first-run --no-default-browser-check --disable-default-apps --window-size=1500,900 --window-position=80,80`,
200+
`--no-first-run --no-default-browser-check --disable-default-apps --hide-crash-restore-bubble --window-size=1500,900 --window-position=80,80`,
152201
"/var/lib/crabbox/browser.env",
153202
"test -x \"$BROWSER\"",
154203
"\"$BROWSER\" --version >/dev/null",
155204
"printf '%s\\n' '{\"DefaultBrowserSettingEnabled\":false,\"MetricsReportingEnabled\":false,\"PromotionalTabsEnabled\":false}' > /etc/opt/chrome/policies/managed/crabbox.json",
156-
"printf '%s\\n' '#!/bin/sh' \"exec \\\"$browser_path\\\" --no-first-run --no-default-browser-check --disable-default-apps --window-size=1500,900 --window-position=80,80 \\\"\\$@\\\"\" > \"$browser_wrapper\"",
205+
"printf '%s\\n' '#!/bin/sh' \"exec \\\"$browser_path\\\" --no-first-run --no-default-browser-check --disable-default-apps --hide-crash-restore-bubble --window-size=1500,900 --window-position=80,80 \\\"\\$@\\\"\" > \"$browser_wrapper\"",
157206
} {
158207
if !strings.Contains(got, want) {
159208
t.Fatalf("cloudInit(browser) missing %q", want)

0 commit comments

Comments
 (0)