fix(wayland): turn off outputs that connect while the screens are off (#12)
* fix(wayland): turn off outputs that connect while the screens are off * test(wayland): share the client bootstrap and document the reconnect claim
- commit
- 31f302d0cbb32e276e625bf28eeee932c67a01eb
- author
- bnema · · 2026-10-03 12:51 UTC
- committer
- GitHub · · 2026-10-03 12:51 UTC
- parent
- 131f96f
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown - refs
- tag: v0.5.1
3 files · +126 -28
M README.md +1 -1
| @@ -79,7 +79,7 @@ auth.mode = password | |||
| 79 | 79 | | `auth.pin-env` | environment variable name (with `env`) | none | | |
| 80 | 80 | | `auth.pin-entry` | `pass` entry (with `pass`) | none | | |
| 81 | 81 | ||
| 82 | - | Durations use Go syntax (`30s`, `5m`, `1h`), up to 24h. The order of the actions comes from their delays: for example, `lock.after = 20s` with `screens.off-after = 50s` turns the screens off 30 seconds after the lock. | |
| 82 | + | Durations use Go syntax (`30s`, `5m`, `1h`), up to 24h. The order of the actions comes from their delays: for example, `lock.after = 20s` with `screens.off-after = 50s` turns the screens off 30 seconds after the lock. A screen that connects while the screens are off, such as a display that reconnects from deep sleep, is turned off too. | |
| 83 | 83 | ||
| 84 | 84 | The lock prompt shows on every output. Set `lock.output` to show it only on that output while it is connected; the other outputs then show black. A change applies from the next lock. `wlr-randr` lists the output names. | |
| 85 | 85 | ||
| @@ -79,7 +79,7 @@ auth.mode = password | |||||
| 79 | | `auth.pin-env` | environment variable name (with `env`) | none | | 79 | | `auth.pin-env` | environment variable name (with `env`) | none | | ||
| 80 | | `auth.pin-entry` | `pass` entry (with `pass`) | none | | 80 | | `auth.pin-entry` | `pass` entry (with `pass`) | none | | ||
| 81 | 81 | ||||
| 82 | - | Durations use Go syntax (`30s`, `5m`, `1h`), up to 24h. The order of the actions comes from their delays: for example, `lock.after = 20s` with `screens.off-after = 50s` turns the screens off 30 seconds after the lock. | 82 | + | Durations use Go syntax (`30s`, `5m`, `1h`), up to 24h. The order of the actions comes from their delays: for example, `lock.after = 20s` with `screens.off-after = 50s` turns the screens off 30 seconds after the lock. A screen that connects while the screens are off, such as a display that reconnects from deep sleep, is turned off too. |
| 83 | 83 | ||||
| 84 | The lock prompt shows on every output. Set `lock.output` to show it only on that output while it is connected; the other outputs then show black. A change applies from the next lock. `wlr-randr` lists the output names. | 84 | The lock prompt shows on every output. Set `lock.output` to show it only on that output while it is connected; the other outputs then show black. A change applies from the next lock. `wlr-randr` lists the output names. | ||
| 85 | 85 | ||||
M internal/adapters/wayland/client.go +20 -0
| @@ -72,6 +72,10 @@ type Client struct { | |||
| 72 | 72 | deadlines map[time.Duration]bool | |
| 73 | 73 | generation, next uint64 | |
| 74 | 74 | dirty bool | |
| 75 | + | // screensOff is the latest requested power state (OutputPower command | |
| 76 | + | // or input wake): an output that appears while it is set (a display | |
| 77 | + | // reconnecting from deep sleep) is turned off too. | |
| 78 | + | screensOff bool | |
| 75 | 79 | } | |
| 76 | 80 | ||
| 77 | 81 | // New takes ownership of conn. Run is called exactly once; no other reader or | |
| @@ -291,10 +295,24 @@ func (c *Client) handle(ctx context.Context, out chan<- ports.WaylandEvent, n no | |||
| 291 | 295 | if c.outputs[o.global] != o || o.failed { | |
| 292 | 296 | return nil | |
| 293 | 297 | } | |
| 298 | + | first := !o.modeKnown | |
| 294 | 299 | o.mode, o.modeKnown = n.mode, true | |
| 295 | 300 | if n.mode == outputpower.MODE_ON { | |
| 296 | 301 | o.off = false | |
| 297 | 302 | } | |
| 303 | + | // The first mode is the initial state of a new power control. An | |
| 304 | + | // output that appears while the screens are off is turned off and | |
| 305 | + | // claimed, even if it is already off (a compositor may keep a | |
| 306 | + | // reconnected output off), so that activity turns it on. This is | |
| 307 | + | // the one exception to never claiming an output already off. | |
| 308 | + | if first && c.screensOff { | |
| 309 | + | if n.mode == outputpower.MODE_ON { | |
| 310 | + | if err := o.power.SetMode(outputpower.MODE_OFF); err != nil { | |
| 311 | + | return err | |
| 312 | + | } | |
| 313 | + | } | |
| 314 | + | o.off = true | |
| 315 | + | } | |
| 298 | 316 | case 5: | |
| 299 | 317 | o := n.output | |
| 300 | 318 | if c.outputs[o.global] != o || o.failed { | |
| @@ -379,6 +397,7 @@ func (c *Client) reconcile() error { | |||
| 379 | 397 | } | |
| 380 | 398 | o.off = false | |
| 381 | 399 | o.failed = false | |
| 400 | + | o.modeKnown = false // a new control reports its initial mode | |
| 382 | 401 | } | |
| 383 | 402 | if c.power != nil { | |
| 384 | 403 | if err := c.power.Destroy(); err != nil { | |
| @@ -514,6 +533,7 @@ func (c *Client) destroyNotification(p *notification) error { | |||
| 514 | 533 | return p.proxy.Destroy() | |
| 515 | 534 | } | |
| 516 | 535 | func (c *Client) setPower(on bool) error { | |
| 536 | + | c.screensOff = !on | |
| 517 | 537 | for _, o := range c.outputs { | |
| 518 | 538 | if o.power == nil || o.failed { | |
| 519 | 539 | continue | |
| @@ -72,6 +72,10 @@ type Client struct { | |||||
| 72 | deadlines map[time.Duration]bool | 72 | deadlines map[time.Duration]bool | ||
| 73 | generation, next uint64 | 73 | generation, next uint64 | ||
| 74 | dirty bool | 74 | dirty bool | ||
| 75 | + | // screensOff is the latest requested power state (OutputPower command | |||
| 76 | + | // or input wake): an output that appears while it is set (a display | |||
| 77 | + | // reconnecting from deep sleep) is turned off too. | |||
| 78 | + | screensOff bool | |||
| 75 | } | 79 | } | ||
| 76 | 80 | ||||
| 77 | // New takes ownership of conn. Run is called exactly once; no other reader or | 81 | // New takes ownership of conn. Run is called exactly once; no other reader or | ||
| @@ -291,10 +295,24 @@ func (c *Client) handle(ctx context.Context, out chan<- ports.WaylandEvent, n no | |||||
| 291 | if c.outputs[o.global] != o || o.failed { | 295 | if c.outputs[o.global] != o || o.failed { | ||
| 292 | return nil | 296 | return nil | ||
| 293 | } | 297 | } | ||
| 298 | + | first := !o.modeKnown | |||
| 294 | o.mode, o.modeKnown = n.mode, true | 299 | o.mode, o.modeKnown = n.mode, true | ||
| 295 | if n.mode == outputpower.MODE_ON { | 300 | if n.mode == outputpower.MODE_ON { | ||
| 296 | o.off = false | 301 | o.off = false | ||
| 297 | } | 302 | } | ||
| 303 | + | // The first mode is the initial state of a new power control. An | |||
| 304 | + | // output that appears while the screens are off is turned off and | |||
| 305 | + | // claimed, even if it is already off (a compositor may keep a | |||
| 306 | + | // reconnected output off), so that activity turns it on. This is | |||
| 307 | + | // the one exception to never claiming an output already off. | |||
| 308 | + | if first && c.screensOff { | |||
| 309 | + | if n.mode == outputpower.MODE_ON { | |||
| 310 | + | if err := o.power.SetMode(outputpower.MODE_OFF); err != nil { | |||
| 311 | + | return err | |||
| 312 | + | } | |||
| 313 | + | } | |||
| 314 | + | o.off = true | |||
| 315 | + | } | |||
| 298 | case 5: | 316 | case 5: | ||
| 299 | o := n.output | 317 | o := n.output | ||
| 300 | if c.outputs[o.global] != o || o.failed { | 318 | if c.outputs[o.global] != o || o.failed { | ||
| @@ -379,6 +397,7 @@ func (c *Client) reconcile() error { | |||||
| 379 | } | 397 | } | ||
| 380 | o.off = false | 398 | o.off = false | ||
| 381 | o.failed = false | 399 | o.failed = false | ||
| 400 | + | o.modeKnown = false // a new control reports its initial mode | |||
| 382 | } | 401 | } | ||
| 383 | if c.power != nil { | 402 | if c.power != nil { | ||
| 384 | if err := c.power.Destroy(); err != nil { | 403 | if err := c.power.Destroy(); err != nil { | ||
| @@ -514,6 +533,7 @@ func (c *Client) destroyNotification(p *notification) error { | |||||
| 514 | return p.proxy.Destroy() | 533 | return p.proxy.Destroy() | ||
| 515 | } | 534 | } | ||
| 516 | func (c *Client) setPower(on bool) error { | 535 | func (c *Client) setPower(on bool) error { | ||
| 536 | + | c.screensOff = !on | |||
| 517 | for _, o := range c.outputs { | 537 | for _, o := range c.outputs { | ||
| 518 | if o.power == nil || o.failed { | 538 | if o.power == nil || o.failed { | ||
| 519 | continue | 539 | continue | ||
M internal/adapters/wayland/client_test.go +105 -27
| @@ -109,59 +109,83 @@ func nextEvent(t *testing.T, out <-chan ports.WaylandEvent) ports.WaylandEvent { | |||
| 109 | 109 | } | |
| 110 | 110 | } | |
| 111 | 111 | ||
| 112 | - | func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) { | |
| 112 | + | // started is a client running against a fake compositor that announced | |
| 113 | + | // its globals and answered the initial bindings (one output, HEADLESS-1). | |
| 114 | + | type started struct { | |
| 115 | + | peer *net.UnixConn | |
| 116 | + | registry, wake, power uint32 | |
| 117 | + | ids map[string]uint32 // bound ID by interface | |
| 118 | + | notifications map[uint32]uint32 // idle notification by timeout (ms) | |
| 119 | + | commands chan ports.WaylandCommand | |
| 120 | + | out chan ports.WaylandEvent | |
| 121 | + | done chan error | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // extraGlobal is one more global announced at start. | |
| 125 | + | type extraGlobal struct { | |
| 126 | + | name uint32 | |
| 127 | + | iface string | |
| 128 | + | version uint32 | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // start runs a client and the initial discovery; extra globals follow | |
| 132 | + | // wl_seat, wl_output, ext_idle_notifier_v1 and zwlr_output_power_manager_v1. | |
| 133 | + | func start(t *testing.T, ctx context.Context, options Options, extra ...extraGlobal) *started { | |
| 134 | + | t.Helper() | |
| 113 | 135 | conn, peer := sockets(t) | |
| 114 | - | c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, zerowrap.New(zerowrap.Config{Output: io.Discard})) | |
| 136 | + | c, err := New(conn, options, zerowrap.New(zerowrap.Config{Output: io.Discard})) | |
| 115 | 137 | if err != nil { | |
| 116 | 138 | t.Fatal(err) | |
| 117 | 139 | } | |
| 118 | 140 | _, _, body := readWire(t, peer) | |
| 119 | - | registry := binary.NativeEndian.Uint32(body) | |
| 120 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 121 | - | defer cancel() | |
| 122 | - | commands := make(chan ports.WaylandCommand, 8) | |
| 123 | - | out := make(chan ports.WaylandEvent, 64) | |
| 124 | - | done := make(chan error, 1) | |
| 125 | - | go func() { done <- c.Run(ctx, commands, out) }() | |
| 141 | + | s := &started{peer: peer, registry: binary.NativeEndian.Uint32(body), ids: map[string]uint32{}, notifications: map[uint32]uint32{}, commands: make(chan ports.WaylandCommand, 8), out: make(chan ports.WaylandEvent, 64), done: make(chan error, 1)} | |
| 142 | + | go func() { s.done <- c.Run(ctx, s.commands, s.out) }() | |
| 126 | 143 | _, _, body = readWire(t, peer) | |
| 127 | - | global(t, peer, registry, 10, "wl_seat", 5) | |
| 128 | - | global(t, peer, registry, 11, "wl_output", 4) | |
| 129 | - | global(t, peer, registry, 12, "ext_idle_notifier_v1", 2) | |
| 130 | - | global(t, peer, registry, 13, "zwlr_output_power_manager_v1", 1) | |
| 131 | - | global(t, peer, registry, 14, "ext_session_lock_manager_v1", 1) | |
| 144 | + | global(t, peer, s.registry, 10, "wl_seat", 5) | |
| 145 | + | global(t, peer, s.registry, 11, "wl_output", 4) | |
| 146 | + | global(t, peer, s.registry, 12, "ext_idle_notifier_v1", 2) | |
| 147 | + | global(t, peer, s.registry, 13, "zwlr_output_power_manager_v1", 1) | |
| 148 | + | for _, g := range extra { | |
| 149 | + | global(t, peer, s.registry, g.name, g.iface, g.version) | |
| 150 | + | } | |
| 132 | 151 | synchronize(t, peer, body) | |
| 133 | - | ids := map[string]uint32{} | |
| 134 | - | notifications := map[uint32]uint32{} | |
| 135 | - | var wake, power uint32 | |
| 136 | 152 | for { | |
| 137 | 153 | id, op, b := readWire(t, peer) | |
| 138 | 154 | if id == 1 { | |
| 139 | 155 | synchronize(t, peer, b) | |
| 140 | 156 | break | |
| 141 | 157 | } | |
| 142 | - | if id == registry { | |
| 158 | + | if id == s.registry { | |
| 143 | 159 | n := int(binary.NativeEndian.Uint32(b[4:])) | |
| 144 | 160 | iface := string(b[8 : 8+n-1]) | |
| 145 | - | ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:]) | |
| 161 | + | s.ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:]) | |
| 146 | 162 | if iface == "wl_output" { | |
| 147 | - | sendWire(t, peer, ids[iface], 4, wireString("HEADLESS-1")) | |
| 163 | + | sendWire(t, peer, s.ids[iface], 4, wireString("HEADLESS-1")) | |
| 148 | 164 | } | |
| 149 | 165 | continue | |
| 150 | 166 | } | |
| 151 | - | if id == ids["ext_idle_notifier_v1"] { | |
| 167 | + | if id == s.ids["ext_idle_notifier_v1"] { | |
| 152 | 168 | child := binary.NativeEndian.Uint32(b) | |
| 153 | - | ms := binary.NativeEndian.Uint32(b[4:]) | |
| 154 | 169 | if op == 2 { | |
| 155 | - | wake = child | |
| 170 | + | s.wake = child | |
| 156 | 171 | } else { | |
| 157 | - | notifications[ms] = child | |
| 172 | + | s.notifications[binary.NativeEndian.Uint32(b[4:])] = child | |
| 158 | 173 | } | |
| 159 | 174 | } | |
| 160 | - | if id == ids["zwlr_output_power_manager_v1"] { | |
| 161 | - | power = binary.NativeEndian.Uint32(b) | |
| 162 | - | sendWire(t, peer, power, 0, words(1)) | |
| 175 | + | if id == s.ids["zwlr_output_power_manager_v1"] { | |
| 176 | + | s.power = binary.NativeEndian.Uint32(b) | |
| 177 | + | sendWire(t, peer, s.power, 0, words(1)) | |
| 163 | 178 | } | |
| 164 | 179 | } | |
| 180 | + | return s | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) { | |
| 184 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 185 | + | defer cancel() | |
| 186 | + | s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, extraGlobal{14, "ext_session_lock_manager_v1", 1}) | |
| 187 | + | peer, registry, ids, notifications, wake, power := s.peer, s.registry, s.ids, s.notifications, s.wake, s.power | |
| 188 | + | commands, out, done := s.commands, s.out, s.done | |
| 165 | 189 | caps := capability(t, out) | |
| 166 | 190 | if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported { | |
| 167 | 191 | t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps) | |
| @@ -263,6 +287,60 @@ func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) { | |||
| 263 | 287 | } | |
| 264 | 288 | } | |
| 265 | 289 | ||
| 290 | + | func TestOutputAddedWhileOffIsTurnedOffAndWakesWithTheOthers(t *testing.T) { | |
| 291 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 292 | + | defer cancel() | |
| 293 | + | s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true}, Generation: 1, Deadlines: []time.Duration{time.Second}}) | |
| 294 | + | peer, registry, wake, power := s.peer, s.registry, s.wake, s.power | |
| 295 | + | commands, out, done := s.commands, s.out, s.done | |
| 296 | + | manager := s.ids["zwlr_output_power_manager_v1"] | |
| 297 | + | capability(t, out) | |
| 298 | + | if manager == 0 || wake == 0 || power == 0 { | |
| 299 | + | t.Fatal(manager, wake, power) | |
| 300 | + | } | |
| 301 | + | commands <- ports.OutputPower{On: false} | |
| 302 | + | if id, op, b := readWire(t, peer); id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 { | |
| 303 | + | t.Fatalf("off %d/%d %x", id, op, b) | |
| 304 | + | } | |
| 305 | + | // addOutput announces an output and answers its power control's initial | |
| 306 | + | // mode; it returns that control. | |
| 307 | + | addOutput := func(name, mode uint32) uint32 { | |
| 308 | + | global(t, peer, registry, name, "wl_output", 4) | |
| 309 | + | var p uint32 | |
| 310 | + | for i := 0; i < 2; i++ { | |
| 311 | + | if id, _, b := readWire(t, peer); id == manager { | |
| 312 | + | p = binary.NativeEndian.Uint32(b) | |
| 313 | + | } | |
| 314 | + | } | |
| 315 | + | sendWire(t, peer, p, 0, words(mode)) | |
| 316 | + | return p | |
| 317 | + | } | |
| 318 | + | // A display reconnecting from deep sleep comes back on: turned off. | |
| 319 | + | on := addOutput(14, 1) | |
| 320 | + | if id, op, b := readWire(t, peer); id != on || op != 0 || binary.NativeEndian.Uint32(b) != 0 { | |
| 321 | + | t.Fatalf("new output off %d/%d %x", id, op, b) | |
| 322 | + | } | |
| 323 | + | // One that comes back off needs no request, but is woken with the rest. | |
| 324 | + | off := addOutput(15, 0) | |
| 325 | + | sendWire(t, peer, wake, 0, nil) | |
| 326 | + | sendWire(t, peer, wake, 1, nil) | |
| 327 | + | woken := map[uint32]bool{} | |
| 328 | + | for range 3 { | |
| 329 | + | id, op, b := readWire(t, peer) | |
| 330 | + | if op != 0 || binary.NativeEndian.Uint32(b) != 1 { | |
| 331 | + | t.Fatalf("wake %d/%d %x", id, op, b) | |
| 332 | + | } | |
| 333 | + | woken[id] = true | |
| 334 | + | } | |
| 335 | + | if !woken[power] || !woken[on] || !woken[off] { | |
| 336 | + | t.Fatal(woken) | |
| 337 | + | } | |
| 338 | + | cancel() | |
| 339 | + | if err := <-done; err != nil { | |
| 340 | + | t.Fatal(err) | |
| 341 | + | } | |
| 342 | + | } | |
| 343 | + | ||
| 266 | 344 | func TestMissingAndDisabledCapabilities(t *testing.T) { | |
| 267 | 345 | conn, peer := sockets(t) | |
| 268 | 346 | c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard})) | |
| @@ -109,59 +109,83 @@ func nextEvent(t *testing.T, out <-chan ports.WaylandEvent) ports.WaylandEvent { | |||||
| 109 | } | 109 | } | ||
| 110 | } | 110 | } | ||
| 111 | 111 | ||||
| 112 | - | func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) { | 112 | + | // started is a client running against a fake compositor that announced |
| 113 | + | // its globals and answered the initial bindings (one output, HEADLESS-1). | |||
| 114 | + | type started struct { | |||
| 115 | + | peer *net.UnixConn | |||
| 116 | + | registry, wake, power uint32 | |||
| 117 | + | ids map[string]uint32 // bound ID by interface | |||
| 118 | + | notifications map[uint32]uint32 // idle notification by timeout (ms) | |||
| 119 | + | commands chan ports.WaylandCommand | |||
| 120 | + | out chan ports.WaylandEvent | |||
| 121 | + | done chan error | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // extraGlobal is one more global announced at start. | |||
| 125 | + | type extraGlobal struct { | |||
| 126 | + | name uint32 | |||
| 127 | + | iface string | |||
| 128 | + | version uint32 | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // start runs a client and the initial discovery; extra globals follow | |||
| 132 | + | // wl_seat, wl_output, ext_idle_notifier_v1 and zwlr_output_power_manager_v1. | |||
| 133 | + | func start(t *testing.T, ctx context.Context, options Options, extra ...extraGlobal) *started { | |||
| 134 | + | t.Helper() | |||
| 113 | conn, peer := sockets(t) | 135 | conn, peer := sockets(t) | ||
| 114 | - | c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, zerowrap.New(zerowrap.Config{Output: io.Discard})) | 136 | + | c, err := New(conn, options, zerowrap.New(zerowrap.Config{Output: io.Discard})) |
| 115 | if err != nil { | 137 | if err != nil { | ||
| 116 | t.Fatal(err) | 138 | t.Fatal(err) | ||
| 117 | } | 139 | } | ||
| 118 | _, _, body := readWire(t, peer) | 140 | _, _, body := readWire(t, peer) | ||
| 119 | - | registry := binary.NativeEndian.Uint32(body) | 141 | + | s := &started{peer: peer, registry: binary.NativeEndian.Uint32(body), ids: map[string]uint32{}, notifications: map[uint32]uint32{}, commands: make(chan ports.WaylandCommand, 8), out: make(chan ports.WaylandEvent, 64), done: make(chan error, 1)} |
| 120 | - | ctx, cancel := context.WithCancel(context.Background()) | 142 | + | go func() { s.done <- c.Run(ctx, s.commands, s.out) }() |
| 121 | - | defer cancel() | |||
| 122 | - | commands := make(chan ports.WaylandCommand, 8) | |||
| 123 | - | out := make(chan ports.WaylandEvent, 64) | |||
| 124 | - | done := make(chan error, 1) | |||
| 125 | - | go func() { done <- c.Run(ctx, commands, out) }() | |||
| 126 | _, _, body = readWire(t, peer) | 143 | _, _, body = readWire(t, peer) | ||
| 127 | - | global(t, peer, registry, 10, "wl_seat", 5) | 144 | + | global(t, peer, s.registry, 10, "wl_seat", 5) |
| 128 | - | global(t, peer, registry, 11, "wl_output", 4) | 145 | + | global(t, peer, s.registry, 11, "wl_output", 4) |
| 129 | - | global(t, peer, registry, 12, "ext_idle_notifier_v1", 2) | 146 | + | global(t, peer, s.registry, 12, "ext_idle_notifier_v1", 2) |
| 130 | - | global(t, peer, registry, 13, "zwlr_output_power_manager_v1", 1) | 147 | + | global(t, peer, s.registry, 13, "zwlr_output_power_manager_v1", 1) |
| 131 | - | global(t, peer, registry, 14, "ext_session_lock_manager_v1", 1) | 148 | + | for _, g := range extra { |
| 149 | + | global(t, peer, s.registry, g.name, g.iface, g.version) | |||
| 150 | + | } | |||
| 132 | synchronize(t, peer, body) | 151 | synchronize(t, peer, body) | ||
| 133 | - | ids := map[string]uint32{} | |||
| 134 | - | notifications := map[uint32]uint32{} | |||
| 135 | - | var wake, power uint32 | |||
| 136 | for { | 152 | for { | ||
| 137 | id, op, b := readWire(t, peer) | 153 | id, op, b := readWire(t, peer) | ||
| 138 | if id == 1 { | 154 | if id == 1 { | ||
| 139 | synchronize(t, peer, b) | 155 | synchronize(t, peer, b) | ||
| 140 | break | 156 | break | ||
| 141 | } | 157 | } | ||
| 142 | - | if id == registry { | 158 | + | if id == s.registry { |
| 143 | n := int(binary.NativeEndian.Uint32(b[4:])) | 159 | n := int(binary.NativeEndian.Uint32(b[4:])) | ||
| 144 | iface := string(b[8 : 8+n-1]) | 160 | iface := string(b[8 : 8+n-1]) | ||
| 145 | - | ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:]) | 161 | + | s.ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:]) |
| 146 | if iface == "wl_output" { | 162 | if iface == "wl_output" { | ||
| 147 | - | sendWire(t, peer, ids[iface], 4, wireString("HEADLESS-1")) | 163 | + | sendWire(t, peer, s.ids[iface], 4, wireString("HEADLESS-1")) |
| 148 | } | 164 | } | ||
| 149 | continue | 165 | continue | ||
| 150 | } | 166 | } | ||
| 151 | - | if id == ids["ext_idle_notifier_v1"] { | 167 | + | if id == s.ids["ext_idle_notifier_v1"] { |
| 152 | child := binary.NativeEndian.Uint32(b) | 168 | child := binary.NativeEndian.Uint32(b) | ||
| 153 | - | ms := binary.NativeEndian.Uint32(b[4:]) | |||
| 154 | if op == 2 { | 169 | if op == 2 { | ||
| 155 | - | wake = child | 170 | + | s.wake = child |
| 156 | } else { | 171 | } else { | ||
| 157 | - | notifications[ms] = child | 172 | + | s.notifications[binary.NativeEndian.Uint32(b[4:])] = child |
| 158 | } | 173 | } | ||
| 159 | } | 174 | } | ||
| 160 | - | if id == ids["zwlr_output_power_manager_v1"] { | 175 | + | if id == s.ids["zwlr_output_power_manager_v1"] { |
| 161 | - | power = binary.NativeEndian.Uint32(b) | 176 | + | s.power = binary.NativeEndian.Uint32(b) |
| 162 | - | sendWire(t, peer, power, 0, words(1)) | 177 | + | sendWire(t, peer, s.power, 0, words(1)) |
| 163 | } | 178 | } | ||
| 164 | } | 179 | } | ||
| 180 | + | return s | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) { | |||
| 184 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 185 | + | defer cancel() | |||
| 186 | + | s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, extraGlobal{14, "ext_session_lock_manager_v1", 1}) | |||
| 187 | + | peer, registry, ids, notifications, wake, power := s.peer, s.registry, s.ids, s.notifications, s.wake, s.power | |||
| 188 | + | commands, out, done := s.commands, s.out, s.done | |||
| 165 | caps := capability(t, out) | 189 | caps := capability(t, out) | ||
| 166 | if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported { | 190 | if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported { | ||
| 167 | t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps) | 191 | t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps) | ||
| @@ -263,6 +287,60 @@ func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) { | |||||
| 263 | } | 287 | } | ||
| 264 | } | 288 | } | ||
| 265 | 289 | ||||
| 290 | + | func TestOutputAddedWhileOffIsTurnedOffAndWakesWithTheOthers(t *testing.T) { | |||
| 291 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 292 | + | defer cancel() | |||
| 293 | + | s := start(t, ctx, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true}, Generation: 1, Deadlines: []time.Duration{time.Second}}) | |||
| 294 | + | peer, registry, wake, power := s.peer, s.registry, s.wake, s.power | |||
| 295 | + | commands, out, done := s.commands, s.out, s.done | |||
| 296 | + | manager := s.ids["zwlr_output_power_manager_v1"] | |||
| 297 | + | capability(t, out) | |||
| 298 | + | if manager == 0 || wake == 0 || power == 0 { | |||
| 299 | + | t.Fatal(manager, wake, power) | |||
| 300 | + | } | |||
| 301 | + | commands <- ports.OutputPower{On: false} | |||
| 302 | + | if id, op, b := readWire(t, peer); id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 { | |||
| 303 | + | t.Fatalf("off %d/%d %x", id, op, b) | |||
| 304 | + | } | |||
| 305 | + | // addOutput announces an output and answers its power control's initial | |||
| 306 | + | // mode; it returns that control. | |||
| 307 | + | addOutput := func(name, mode uint32) uint32 { | |||
| 308 | + | global(t, peer, registry, name, "wl_output", 4) | |||
| 309 | + | var p uint32 | |||
| 310 | + | for i := 0; i < 2; i++ { | |||
| 311 | + | if id, _, b := readWire(t, peer); id == manager { | |||
| 312 | + | p = binary.NativeEndian.Uint32(b) | |||
| 313 | + | } | |||
| 314 | + | } | |||
| 315 | + | sendWire(t, peer, p, 0, words(mode)) | |||
| 316 | + | return p | |||
| 317 | + | } | |||
| 318 | + | // A display reconnecting from deep sleep comes back on: turned off. | |||
| 319 | + | on := addOutput(14, 1) | |||
| 320 | + | if id, op, b := readWire(t, peer); id != on || op != 0 || binary.NativeEndian.Uint32(b) != 0 { | |||
| 321 | + | t.Fatalf("new output off %d/%d %x", id, op, b) | |||
| 322 | + | } | |||
| 323 | + | // One that comes back off needs no request, but is woken with the rest. | |||
| 324 | + | off := addOutput(15, 0) | |||
| 325 | + | sendWire(t, peer, wake, 0, nil) | |||
| 326 | + | sendWire(t, peer, wake, 1, nil) | |||
| 327 | + | woken := map[uint32]bool{} | |||
| 328 | + | for range 3 { | |||
| 329 | + | id, op, b := readWire(t, peer) | |||
| 330 | + | if op != 0 || binary.NativeEndian.Uint32(b) != 1 { | |||
| 331 | + | t.Fatalf("wake %d/%d %x", id, op, b) | |||
| 332 | + | } | |||
| 333 | + | woken[id] = true | |||
| 334 | + | } | |||
| 335 | + | if !woken[power] || !woken[on] || !woken[off] { | |||
| 336 | + | t.Fatal(woken) | |||
| 337 | + | } | |||
| 338 | + | cancel() | |||
| 339 | + | if err := <-done; err != nil { | |||
| 340 | + | t.Fatal(err) | |||
| 341 | + | } | |||
| 342 | + | } | |||
| 343 | + | ||||
| 266 | func TestMissingAndDisabledCapabilities(t *testing.T) { | 344 | func TestMissingAndDisabledCapabilities(t *testing.T) { | ||
| 267 | conn, peer := sockets(t) | 345 | conn, peer := sockets(t) | ||
| 268 | c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard})) | 346 | c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard})) | ||