Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/cmd/api/api/chromium_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, plan *
if s.isNekoEnabled() {
err = s.setResolutionViaNeko(ctx, w, h, rr)
} else {
err = s.setResolutionXorgViaXrandr(ctx, w, h, rr, false)
err = s.setResolutionXorgViaXrandr(ctx, w, h, rr)
}
if err != nil {
return cfg500ConfigureStep(chromiumConfigureStepDisplay, err.Error())
Expand Down
77 changes: 69 additions & 8 deletions server/cmd/api/api/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,27 @@ func (s *ApiService) PatchDisplay(ctx context.Context, req oapi.PatchDisplayRequ
err = s.setResolutionViaNeko(ctx, width, height, refreshRate)
} else {
log.Info("using xrandr for Xorg resolution change (Neko disabled)")
err = s.setResolutionXorgViaXrandr(ctx, width, height, refreshRate, restartChrome)
err = s.setResolutionXorgViaXrandr(ctx, width, height, refreshRate)
}
if err == nil && restartChrome {
if restartErr := s.restartChromiumAndWait(ctx, "resolution change"); restartErr != nil {
log.Error("failed to restart chromium after resolution change", "error", restartErr)
// Re-assert the maximized window state via CDP after the X root
// resize. Mutter reflows a window in windowState=maximized (or
// fullscreen) to fill the new root automatically, so this single
// idempotent call is all we need post-resize. The previous
// approach of restarting chromium so it could re-apply
// --start-maximized cost ~9s per resize and also wiped browser-
// side state (Emulation.* overrides, devtools sessions). The
// restart_chromium request field is still accepted for API
// compatibility but no longer triggers a restart on this path.
//
// The CDP call is the only thing that recovers a window already
// in the "normal" state, so its failure is fatal: returning 200
// after a CDP error could leave the browser window stuck at the
// old size while the X root is at the new size, and the caller
// would have no signal of the mismatch.
if err == nil {
if cdpErr := s.setWindowMaximizedViaCDP(ctx); cdpErr != nil {
log.Error("CDP maximize re-assert failed after Xorg resolution change", "error", cdpErr)
err = fmt.Errorf("CDP maximize re-assert failed: %w", cdpErr)
}
}
} else if len(stopped) > 0 {
Expand Down Expand Up @@ -224,18 +240,27 @@ func (s *ApiService) probeDisplayMode(ctx context.Context) string {
}

// setResolutionXorgViaXrandr changes resolution for Xorg using xrandr (fallback when Neko is disabled)
func (s *ApiService) setResolutionXorgViaXrandr(ctx context.Context, width, height, refreshRate int, restartChrome bool) error {
func (s *ApiService) setResolutionXorgViaXrandr(ctx context.Context, width, height, refreshRate int) error {
log := logger.FromContext(ctx)
display := s.resolveDisplayFromEnv()

// The headful Xorg dummy driver exposes DUMMY0, not "default". The
// historical `xrandr --output default --mode ...` command exits 0 while
// silently doing nothing on this driver. Default to DUMMY0 and let an
// env var override it for any non-standard image layout.
output := strings.TrimSpace(os.Getenv("KERNEL_IMAGES_XRANDR_OUTPUT"))
if output == "" {
output = "DUMMY0"
}

// Build xrandr command - if refresh rate is specified, use the specific modeline
var xrandrCmd string
if refreshRate > 0 {
modeName := fmt.Sprintf("%dx%d_%d.00", width, height, refreshRate)
xrandrCmd = fmt.Sprintf("xrandr --output default --mode %s", modeName)
log.Info("using specific modeline", "mode", modeName)
xrandrCmd = fmt.Sprintf("xrandr --output %s --mode %s", output, modeName)
log.Info("using specific modeline", "output", output, "mode", modeName)
} else {
xrandrCmd = fmt.Sprintf("xrandr -s %dx%d", width, height)
xrandrCmd = fmt.Sprintf("xrandr --output %s --size %dx%d", output, width, height)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid xrandr --size used as per-output option

High Severity

The else branch (when refreshRate <= 0) constructs xrandr --output DUMMY0 --size WxH, but --size is a legacy RandR 1.0/1.1 global screen option (long form of -s), not a per-output option. Per-output resolution changes require --mode, not --size. Combining --output with --size is invalid and will either silently no-op or error. The refreshRate > 0 branch correctly uses --mode. The test comment at line 460 of e2e_display_resize_window_test.go still references the old working form (xrandr -s WxH), confirming the intent was a global size set, but the code was incorrectly rewritten to use --output with --size.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9f62371. Configure here.

}

args := []string{"-lc", xrandrCmd}
Expand Down Expand Up @@ -367,6 +392,42 @@ func (s *ApiService) backgroundResizeXvfb(ctx context.Context, width, height int
s.viewportMu.Unlock()
}

// setWindowMaximizedViaCDP re-asserts that the chromium OS window is in
// the "maximized" state via Browser.setWindowBounds. After a successful
// xrandr/Neko resize on the headful path, mutter will reflow a maximized
// window to fill the new X root — so this call is the entirety of what
// the server needs to do post-resize. It replaces the previous approach
// of restarting chromium so it could re-apply --start-maximized, which
// cost a multi-second restart and also reset other browser-side state
// (notably Emulation.* overrides).
//
// The call is idempotent and cheap: when the window is already maximized
// the helper returns immediately without sending any CDP commands.
func (s *ApiService) setWindowMaximizedViaCDP(ctx context.Context) error {
log := logger.FromContext(ctx)

upstreamURL := s.upstreamMgr.Current()
if upstreamURL == "" {
return fmt.Errorf("devtools upstream not available")
}

cdpCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

client, err := cdpclient.Dial(cdpCtx, upstreamURL)
if err != nil {
return fmt.Errorf("failed to connect to devtools: %w", err)
}
defer client.Close()

if err := client.SetWindowBoundsMaximized(cdpCtx); err != nil {
return fmt.Errorf("CDP setWindowBoundsMaximized: %w", err)
}

log.Info("re-asserted maximized window state via CDP")
return nil
}

// setViewportViaCDP resizes the browser viewport using the CDP
// Emulation.setDeviceMetricsOverride command. This is near-instant and does
// not require restarting Chromium or Xvfb.
Expand Down
Loading
Loading