Skip to content

Commit b0e08be

Browse files
committed
wrapper: start mutter before chromium maps its window
Chrome on X11 negotiates client-side decorations (no WM titlebar) with the window manager at window-map time. If mutter isn't registered yet, the hint has no listener; when mutter later starts it reparents the existing chromium window with default SSD — a WM titlebar with a close button appears on top of chrome's own frame. Move mutter's start to immediately after the X server comes up (was after the dbus wait) and have chromium-launcher gate on mutter being registered before exec'ing chromium (only when not --headless).
1 parent f58186b commit b0e08be

3 files changed

Lines changed: 47 additions & 12 deletions

File tree

server/cmd/chromium-launcher/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ func main() {
5050
fmt.Fprintf(os.Stderr, "warning: X display :1 not responsive after %s\n", d)
5151
}
5252

53+
// Headful: wait for mutter to register before exec'ing chromium. If
54+
// chromium maps its window with no WM present, the CSD hint it sends has
55+
// no listener; mutter starts later, reparents the existing window, and
56+
// applies default SSD — i.e., the titlebar with the close X. Headless
57+
// has no WM, so skip.
58+
if !*headless {
59+
if d := x11.WaitForMutter(20 * time.Second); d >= 20*time.Second {
60+
fmt.Fprintf(os.Stderr, "warning: mutter not registered after %s\n", d)
61+
}
62+
}
63+
5364
baseFlags := os.Getenv("CHROMIUM_FLAGS")
5465
runtimeTokens, err := chromiumflags.ReadOptionalFlagFile(*runtimeFlagsPath)
5566
if err != nil {

server/cmd/wrapper/main.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,16 @@ func main() {
151151
// (baked into the image at build time, see shared/envoy/bake-certs.sh)
152152
// so it trusts the forward proxy on first start with no runtime cert
153153
// work to wait on. chromium-launcher internally waits for the X server
154-
// before exec'ing chromium, so we start it in parallel with the X
155-
// server to overlap chromium-launcher's preamble with display startup.
156-
// chromedriver listens on 9225 immediately and only attaches to
157-
// chromium on session creation, so it can come up alongside everything.
158-
// mutter has no internal X-wait, so it's started after the X server is
159-
// confirmed up; neko is started with mutter on the WebRTC path.
154+
// and (headful) for mutter before exec'ing chromium, so we start it in
155+
// parallel with the X server to overlap chromium-launcher's preamble
156+
// with display startup. chromedriver listens on 9225 immediately and
157+
// only attaches to chromium on session creation, so it can come up
158+
// alongside everything. mutter has no internal X-wait, so it's started
159+
// as soon as the X server is confirmed up — chromium-launcher gates on
160+
// it so chrome can negotiate CSD with the WM before mapping its window
161+
// (without it, mutter reparents the existing window with default SSD
162+
// and a titlebar appears). neko reads the active display mode at start,
163+
// so it's deferred until after the dbus wait on the WebRTC path.
160164
xServer := "xorg"
161165
if prof == profileHeadless {
162166
xServer = "xvfb"
@@ -171,13 +175,12 @@ func main() {
171175
browserStart := time.Now()
172176
startAll(xServer, "dbus", "chromedriver", "chromium")
173177
waitForX(defaultDisplay, 20*time.Second)
174-
waitForSocket(dbusSocket, 10*time.Second)
175178
if prof == profileHeadful {
176-
post := []string{"mutter"}
177-
if webrtc {
178-
post = append(post, "neko")
179-
}
180-
startAll(post...)
179+
startAll("mutter")
180+
}
181+
waitForSocket(dbusSocket, 10*time.Second)
182+
if prof == profileHeadful && webrtc {
183+
startAll("neko")
181184
}
182185
browserDone := time.Now()
183186

server/lib/x11/x11.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package x11
33

44
import (
55
"net"
6+
"os/exec"
67
"strings"
78
"time"
89
)
@@ -34,3 +35,23 @@ func WaitForDisplay(display string, timeout time.Duration) time.Duration {
3435
}
3536
return time.Since(start)
3637
}
38+
39+
// WaitForMutter blocks until the mutter window manager has registered with
40+
// the X server, returning time spent waiting. Chromium negotiates CSD (no WM
41+
// titlebar) at window-map time; if mutter isn't up yet, it later reparents the
42+
// already-mapped window with default SSD decoration and a titlebar appears.
43+
// Polls `xdotool search --class mutter` to match the wrapper's readiness check.
44+
//
45+
// If the deadline elapses, WaitForMutter still returns; callers can compare
46+
// the returned duration against timeout to detect a miss.
47+
func WaitForMutter(timeout time.Duration) time.Duration {
48+
start := time.Now()
49+
deadline := start.Add(timeout)
50+
for time.Now().Before(deadline) {
51+
if exec.Command("xdotool", "search", "--class", "mutter").Run() == nil {
52+
return time.Since(start)
53+
}
54+
time.Sleep(50 * time.Millisecond)
55+
}
56+
return time.Since(start)
57+
}

0 commit comments

Comments
 (0)