docs: document desktop screenshot command

This commit is contained in:
Peter Steinberger 2026-05-04 02:01:33 +01:00
parent 02bad5f369
commit d79cba3fa5
No known key found for this signature in database
10 changed files with 83 additions and 11 deletions

View File

@ -7,6 +7,7 @@
- Added `--desktop`, `--browser`, and `crabbox vnc` for optional Linux UI/browser leases, including loopback-only VNC with per-lease passwords and headless browser support without a desktop.
- Added static macOS/Windows VNC endpoint discovery, including SSH-tunneled loopback VNC and trusted static direct VNC on `host:5900`.
- Added `crabbox vnc --open` to start the SSH tunnel and launch the local VNC client for managed desktop leases.
- Added `crabbox screenshot` to save a PNG from a Linux desktop lease without opening a VNC client.
- Added a minimal XFCE desktop profile with panel/window manager for managed VNC leases.
- Clarified static macOS/Windows VNC as existing-host access, not Crabbox-created boxes, so `--open` no longer launches an OS credential prompt unless `--host-managed` is passed.

View File

@ -35,6 +35,7 @@ crabbox config path
crabbox config set-broker --url <url> --token-stdin [--provider hetzner|aws]
crabbox warmup [--provider hetzner|aws|ssh|blacksmith-testbox] [--target linux|macos|windows] [--desktop] [--browser] [--profile <name>] [--idle-timeout <duration>] [--timing-json]
crabbox run [--id <lease-id-or-slug>] [--provider hetzner|aws|ssh|blacksmith-testbox] [--target linux|macos|windows] [--windows-mode normal|wsl2] [--desktop] [--browser] [--shell] [--checksum] [--debug] [--force-sync-large] [--timing-json] [--blacksmith-workflow <workflow>] -- <command...>
crabbox screenshot --id <lease-id-or-slug> [--output <path>]
crabbox sync-plan [--limit <n>]
crabbox history [--lease <lease-id>] [--owner <email>] [--org <name>] [--limit <n>] [--json]
crabbox logs <run-id> [--json]
@ -81,6 +82,7 @@ crabbox warmup --profile project-check
crabbox warmup --desktop --browser
crabbox run --id blue-lobster -- pnpm test:changed
crabbox vnc --id blue-lobster --open
crabbox screenshot --id blue-lobster --output desktop.png
crabbox run --id blue-lobster --shell 'pnpm install --frozen-lockfile && pnpm test'
crabbox stop blue-lobster
```
@ -232,6 +234,7 @@ Flags:
--debug print sync timing and itemized rsync output
--junit <paths> comma-separated remote JUnit XML paths to attach to run history
--open open local VNC client for `crabbox vnc`
--output <path> local PNG path for `crabbox screenshot`
--reclaim claim an existing lease for the current repo
--timing-json print a final JSON timing record
--blacksmith-org <org> Blacksmith organization

View File

@ -26,6 +26,7 @@ Command docs live here, one file per top-level command. Keep `docs/cli.md` as th
- [actions](actions.md)
- [ssh](ssh.md)
- [vnc](vnc.md)
- [screenshot](screenshot.md)
- [inspect](inspect.md)
- [stop](stop.md)
- [cleanup](cleanup.md)

View File

@ -0,0 +1,40 @@
# screenshot
`crabbox screenshot` captures a PNG from a Linux desktop lease without opening a
VNC client.
```sh
crabbox warmup --desktop
crabbox screenshot --id blue-lobster
crabbox screenshot --id blue-lobster --output desktop.png
```
The command resolves and touches the lease like `crabbox ssh`, verifies that the
lease has `desktop=true`, waits for the loopback desktop/VNC service, then
streams a PNG over SSH from `DISPLAY=:99`.
If `--output` is omitted, Crabbox writes:
```text
crabbox-<slug-or-id>-screenshot.png
```
Screenshots are currently supported for Linux desktop leases. Static macOS and
Windows targets are existing host machines, not Crabbox-created desktops, so
`screenshot` rejects those targets instead of capturing your local or home-host
desktop by accident.
Flags:
```text
--id <lease-id-or-slug>
--provider hetzner|aws|ssh
--target linux|macos|windows
--windows-mode normal|wsl2
--static-host <host>
--static-user <user>
--static-port <port>
--static-work-root <path>
--output <path>
--reclaim
```

View File

@ -41,6 +41,7 @@ This page maps user-facing behavior back to implementation files. Keep docs desc
- Worker cloud-init bootstrap: `worker/src/bootstrap.ts`
- Desktop/browser capability flags, env injection, and VNC checks: `internal/cli/capabilities.go`, `internal/cli/run.go`
- VNC tunnel command: `internal/cli/vnc.go`
- Desktop screenshot command: `internal/cli/screenshot.go`
- Interactive desktop/VNC contract: `docs/features/interactive-desktop-vnc.md`
Bootstrap is intentionally tiny unless optional lease capabilities are requested:

View File

@ -92,6 +92,8 @@ func (a App) Run(ctx context.Context, args []string) error {
return a.ssh(ctx, args[1:])
case "vnc":
return a.vnc(ctx, args[1:])
case "screenshot":
return a.screenshot(ctx, args[1:])
case "inspect":
return a.inspect(ctx, args[1:])
case "stop", "release":
@ -145,6 +147,7 @@ Commands:
actions Register GitHub Actions runners or dispatch workflows
ssh Print the SSH command for a lease
vnc Print or open VNC connection details for a desktop lease
screenshot Capture a PNG from a desktop lease
inspect Print lease/provider details; add --json for scripts
stop Release a lease or delete a direct-provider machine
cleanup Sweep expired direct-provider machines
@ -157,6 +160,7 @@ Common Flows:
crabbox run --id blue-lobster --shell 'pnpm install --frozen-lockfile && pnpm test'
crabbox ssh --id blue-lobster
crabbox vnc --id blue-lobster --open
crabbox screenshot --id blue-lobster --output desktop.png
crabbox inspect --id blue-lobster --json
crabbox history --lease cbx_abcdef123456
crabbox logs run_123

View File

@ -39,7 +39,7 @@ func TestCloudInitDesktopProfile(t *testing.T) {
got := cloudInit(cfg, "ssh-ed25519 test")
for _, want := range []string{
"xvfb xfce4 xfce4-terminal x11vnc xauth dbus-x11",
"x11-xserver-utils xterm",
"x11-xserver-utils xterm scrot",
"/etc/systemd/system/crabbox-xvfb.service",
"/etc/systemd/system/crabbox-desktop.service",
"/usr/local/bin/crabbox-desktop-session",

View File

@ -84,7 +84,7 @@ func defaultScreenshotPath(leaseID, slug string) string {
}
func captureDesktopScreenshot(ctx context.Context, target SSHTarget, outputPath string) error {
if err := os.MkdirAll(filepath.Dir(absOrDot(outputPath)), 0o755); err != nil {
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
return exit(2, "create screenshot directory: %v", err)
}
file, err := os.Create(outputPath)
@ -105,14 +105,6 @@ func captureDesktopScreenshot(ctx context.Context, target SSHTarget, outputPath
return nil
}
func absOrDot(path string) string {
dir := filepath.Dir(path)
if dir == "" {
return "."
}
return dir
}
func runSSHToWriter(ctx context.Context, target SSHTarget, remote string, stdout io.Writer) error {
remote = wrapRemoteForTarget(target, remote)
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)

View File

@ -0,0 +1,30 @@
package cli
import (
"strings"
"testing"
)
func TestDefaultScreenshotPath(t *testing.T) {
if got := defaultScreenshotPath("cbx_123", "Blue Lobster"); got != "crabbox-blue-lobster-screenshot.png" {
t.Fatalf("path=%q", got)
}
if got := defaultScreenshotPath("cbx_123", ""); got != "crabbox-cbx-123-screenshot.png" {
t.Fatalf("fallback path=%q", got)
}
}
func TestScreenshotRemoteCommandUsesDesktopDisplayAndPNG(t *testing.T) {
got := screenshotRemoteCommand()
for _, want := range []string{
`DISPLAY="${DISPLAY:-:99}"`,
"command -v scrot",
"scrot -z -o",
"cat \"$tmp\"",
"import -window root png:-",
} {
if !strings.Contains(got, want) {
t.Fatalf("screenshot command missing %q:\n%s", want, got)
}
}
}

View File

@ -75,7 +75,7 @@ describe("cloud-init bootstrap", () => {
expect(got).toContain("ExecStart=/usr/bin/startxfce4");
expect(got).toContain("systemctl is-active --quiet crabbox-desktop.service");
expect(got).toContain("systemctl is-active --quiet crabbox-desktop-session.service");
expect(got).toContain("x11-xserver-utils xterm");
expect(got).toContain("x11-xserver-utils xterm scrot");
expect(got).toContain("xsetroot -solid '#20242b'");
expect(got).toContain("xterm -title 'Crabbox Desktop'");
expect(got).toContain("(umask 077 && openssl rand -base64 18 > /var/lib/crabbox/vnc.password)");