fix(drm): retry an enable modeset refused before DRM master returns
On a VT switch back, the seat can report the session active before the kernel gives DRM master back. The enable modeset then fails with EACCES, the output is marked disabled and nothing retries it: the screen stays frozen until the next VT switch. Retry the enable on a bounded backoff (20 ms doubling to 1 s, at most 40 attempts) while the seat stays active. A new seat change or a successful enable ends the retry. The seat-change handling moves into one setActive helper shared by both paths.
- commit
- 18cc6d48a74d1befeb077a352cf40f004d91c2cd
- author
- brice · · 2026-10-10 14:11 UTC
- committer
- brice · · 2026-10-10 15:39 UTC
- parent
- 3305370
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +232 -35
M internal/adapters/drm/output.go +77 -35
| @@ -1098,6 +1098,57 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1098 | 1098 | cursorWake = o.cursor.wake | |
| 1099 | 1099 | } | |
| 1100 | 1100 | securityChanges := o.SecurityChanges | |
| 1101 | + | reacquireTimer := time.NewTimer(time.Hour) | |
| 1102 | + | reacquireTimer.Stop() | |
| 1103 | + | defer reacquireTimer.Stop() | |
| 1104 | + | var reacquire masterRetry | |
| 1105 | + | // setActive applies a seat change. skip reports that the loop must | |
| 1106 | + | // go on without touching the frame state (refused or protected). | |
| 1107 | + | setActive := func(on bool) (skip bool, err error) { | |
| 1108 | + | o.observeSecurity() | |
| 1109 | + | if o.protected { | |
| 1110 | + | o.invalidateSecurity() | |
| 1111 | + | enabled = on | |
| 1112 | + | if on && o.fbs[0] == 0 { | |
| 1113 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 1114 | + | if !o.commitFailed(err, &enabled) { | |
| 1115 | + | return true, err | |
| 1116 | + | } | |
| 1117 | + | reacquire.refused(err, &enabled) | |
| 1118 | + | return true, nil | |
| 1119 | + | } | |
| 1120 | + | } | |
| 1121 | + | reacquire.done() | |
| 1122 | + | return true, nil | |
| 1123 | + | } | |
| 1124 | + | // Modeset on every enable: a fast disable+enable can coalesce to one true. | |
| 1125 | + | if on { | |
| 1126 | + | enabled = true // this enable may supersede a previous master loss | |
| 1127 | + | var err error | |
| 1128 | + | if o.fbs[0] == 0 { | |
| 1129 | + | err = o.showImages(r, imagesDriver, nil) | |
| 1130 | + | } else { | |
| 1131 | + | err = o.modeset() | |
| 1132 | + | } | |
| 1133 | + | if err != nil { | |
| 1134 | + | if !o.commitFailed(err, &enabled) { | |
| 1135 | + | return true, err | |
| 1136 | + | } | |
| 1137 | + | reacquire.refused(err, &enabled) | |
| 1138 | + | return true, nil // epoch retry or wait disabled for master reacquisition | |
| 1139 | + | } | |
| 1140 | + | reacquire.done() | |
| 1141 | + | o.observeSecurity() | |
| 1142 | + | if o.protected { | |
| 1143 | + | return true, nil | |
| 1144 | + | } | |
| 1145 | + | o.probeAsync(r) | |
| 1146 | + | dirty = haveScene | |
| 1147 | + | } | |
| 1148 | + | enabled = on | |
| 1149 | + | o.log.Info().Bool("enabled", on).Msg("output") | |
| 1150 | + | return false, nil | |
| 1151 | + | } | |
| 1101 | 1152 | for { | |
| 1102 | 1153 | if o.observeSecurity() || haveScene && !o.sceneCurrent(scene) { | |
| 1103 | 1154 | scene, haveScene, dirty = ports.Scene{}, false, false | |
| @@ -1203,6 +1254,14 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1203 | 1254 | } else { | |
| 1204 | 1255 | protectTimer.Stop() | |
| 1205 | 1256 | } | |
| 1257 | + | // An enable refused for lack of DRM master is retried on time. | |
| 1258 | + | var reacquireDue <-chan time.Time | |
| 1259 | + | if d, ok := reacquire.next(); ok { | |
| 1260 | + | reacquireTimer.Reset(d) | |
| 1261 | + | reacquireDue = reacquireTimer.C | |
| 1262 | + | } else { | |
| 1263 | + | reacquireTimer.Stop() | |
| 1264 | + | } | |
| 1206 | 1265 | // A commit whose event never comes must not stop the output. | |
| 1207 | 1266 | stuck := o.frame.wait(enabled) | |
| 1208 | 1267 | stateDirty := false | |
| @@ -1268,44 +1327,27 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1268 | 1327 | ack <- struct{}{} | |
| 1269 | 1328 | continue | |
| 1270 | 1329 | case on := <-active: | |
| 1271 | - | o.observeSecurity() | |
| 1272 | - | if o.protected { | |
| 1273 | - | o.invalidateSecurity() | |
| 1274 | - | enabled = on | |
| 1275 | - | if on && o.fbs[0] == 0 { | |
| 1276 | - | if err := o.showImages(r, imagesDriver, nil); err != nil { | |
| 1277 | - | if !o.commitFailed(err, &enabled) { | |
| 1278 | - | return err | |
| 1279 | - | } | |
| 1280 | - | } | |
| 1281 | - | } | |
| 1330 | + | reacquire.done() | |
| 1331 | + | skip, err := setActive(on) | |
| 1332 | + | if err != nil { | |
| 1333 | + | return err | |
| 1334 | + | } | |
| 1335 | + | if skip { | |
| 1282 | 1336 | continue | |
| 1283 | 1337 | } | |
| 1284 | - | // Modeset on every enable: a fast disable+enable can coalesce to one true. | |
| 1285 | - | if on { | |
| 1286 | - | enabled = true // this enable may supersede a previous master loss | |
| 1287 | - | var err error | |
| 1288 | - | if o.fbs[0] == 0 { | |
| 1289 | - | err = o.showImages(r, imagesDriver, nil) | |
| 1290 | - | } else { | |
| 1291 | - | err = o.modeset() | |
| 1292 | - | } | |
| 1293 | - | if err != nil { | |
| 1294 | - | if !o.commitFailed(err, &enabled) { | |
| 1295 | - | return err | |
| 1296 | - | } | |
| 1297 | - | continue // epoch retry or wait disabled for master reacquisition | |
| 1298 | - | } | |
| 1299 | - | o.observeSecurity() | |
| 1300 | - | if o.protected { | |
| 1301 | - | continue | |
| 1302 | - | } | |
| 1303 | - | o.probeAsync(r) | |
| 1304 | - | dirty = haveScene | |
| 1305 | - | stateDirty = true | |
| 1338 | + | stateDirty = on | |
| 1339 | + | case <-reacquireDue: | |
| 1340 | + | // The seat reported active before the kernel gave DRM master | |
| 1341 | + | // back: the enable modeset was refused. Try it again. | |
| 1342 | + | reacquire.attempts++ | |
| 1343 | + | skip, err := setActive(true) | |
| 1344 | + | if err != nil { | |
| 1345 | + | return err | |
| 1306 | 1346 | } | |
| 1307 | - | enabled = on | |
| 1308 | - | o.log.Info().Bool("enabled", on).Msg("output") | |
| 1347 | + | if skip { | |
| 1348 | + | continue | |
| 1349 | + | } | |
| 1350 | + | stateDirty = true | |
| 1309 | 1351 | case ev := <-o.flipped: | |
| 1310 | 1352 | o.observeSecurity() | |
| 1311 | 1353 | if !o.completed(ev, seen) { | |
| @@ -1098,6 +1098,57 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1098 | cursorWake = o.cursor.wake | 1098 | cursorWake = o.cursor.wake | ||
| 1099 | } | 1099 | } | ||
| 1100 | securityChanges := o.SecurityChanges | 1100 | securityChanges := o.SecurityChanges | ||
| 1101 | + | reacquireTimer := time.NewTimer(time.Hour) | |||
| 1102 | + | reacquireTimer.Stop() | |||
| 1103 | + | defer reacquireTimer.Stop() | |||
| 1104 | + | var reacquire masterRetry | |||
| 1105 | + | // setActive applies a seat change. skip reports that the loop must | |||
| 1106 | + | // go on without touching the frame state (refused or protected). | |||
| 1107 | + | setActive := func(on bool) (skip bool, err error) { | |||
| 1108 | + | o.observeSecurity() | |||
| 1109 | + | if o.protected { | |||
| 1110 | + | o.invalidateSecurity() | |||
| 1111 | + | enabled = on | |||
| 1112 | + | if on && o.fbs[0] == 0 { | |||
| 1113 | + | if err := o.showImages(r, imagesDriver, nil); err != nil { | |||
| 1114 | + | if !o.commitFailed(err, &enabled) { | |||
| 1115 | + | return true, err | |||
| 1116 | + | } | |||
| 1117 | + | reacquire.refused(err, &enabled) | |||
| 1118 | + | return true, nil | |||
| 1119 | + | } | |||
| 1120 | + | } | |||
| 1121 | + | reacquire.done() | |||
| 1122 | + | return true, nil | |||
| 1123 | + | } | |||
| 1124 | + | // Modeset on every enable: a fast disable+enable can coalesce to one true. | |||
| 1125 | + | if on { | |||
| 1126 | + | enabled = true // this enable may supersede a previous master loss | |||
| 1127 | + | var err error | |||
| 1128 | + | if o.fbs[0] == 0 { | |||
| 1129 | + | err = o.showImages(r, imagesDriver, nil) | |||
| 1130 | + | } else { | |||
| 1131 | + | err = o.modeset() | |||
| 1132 | + | } | |||
| 1133 | + | if err != nil { | |||
| 1134 | + | if !o.commitFailed(err, &enabled) { | |||
| 1135 | + | return true, err | |||
| 1136 | + | } | |||
| 1137 | + | reacquire.refused(err, &enabled) | |||
| 1138 | + | return true, nil // epoch retry or wait disabled for master reacquisition | |||
| 1139 | + | } | |||
| 1140 | + | reacquire.done() | |||
| 1141 | + | o.observeSecurity() | |||
| 1142 | + | if o.protected { | |||
| 1143 | + | return true, nil | |||
| 1144 | + | } | |||
| 1145 | + | o.probeAsync(r) | |||
| 1146 | + | dirty = haveScene | |||
| 1147 | + | } | |||
| 1148 | + | enabled = on | |||
| 1149 | + | o.log.Info().Bool("enabled", on).Msg("output") | |||
| 1150 | + | return false, nil | |||
| 1151 | + | } | |||
| 1101 | for { | 1152 | for { | ||
| 1102 | if o.observeSecurity() || haveScene && !o.sceneCurrent(scene) { | 1153 | if o.observeSecurity() || haveScene && !o.sceneCurrent(scene) { | ||
| 1103 | scene, haveScene, dirty = ports.Scene{}, false, false | 1154 | scene, haveScene, dirty = ports.Scene{}, false, false | ||
| @@ -1203,6 +1254,14 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1203 | } else { | 1254 | } else { | ||
| 1204 | protectTimer.Stop() | 1255 | protectTimer.Stop() | ||
| 1205 | } | 1256 | } | ||
| 1257 | + | // An enable refused for lack of DRM master is retried on time. | |||
| 1258 | + | var reacquireDue <-chan time.Time | |||
| 1259 | + | if d, ok := reacquire.next(); ok { | |||
| 1260 | + | reacquireTimer.Reset(d) | |||
| 1261 | + | reacquireDue = reacquireTimer.C | |||
| 1262 | + | } else { | |||
| 1263 | + | reacquireTimer.Stop() | |||
| 1264 | + | } | |||
| 1206 | // A commit whose event never comes must not stop the output. | 1265 | // A commit whose event never comes must not stop the output. | ||
| 1207 | stuck := o.frame.wait(enabled) | 1266 | stuck := o.frame.wait(enabled) | ||
| 1208 | stateDirty := false | 1267 | stateDirty := false | ||
| @@ -1268,44 +1327,27 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1268 | ack <- struct{}{} | 1327 | ack <- struct{}{} | ||
| 1269 | continue | 1328 | continue | ||
| 1270 | case on := <-active: | 1329 | case on := <-active: | ||
| 1271 | - | o.observeSecurity() | 1330 | + | reacquire.done() |
| 1272 | - | if o.protected { | 1331 | + | skip, err := setActive(on) |
| 1273 | - | o.invalidateSecurity() | 1332 | + | if err != nil { |
| 1274 | - | enabled = on | 1333 | + | return err |
| 1275 | - | if on && o.fbs[0] == 0 { | 1334 | + | } |
| 1276 | - | if err := o.showImages(r, imagesDriver, nil); err != nil { | 1335 | + | if skip { |
| 1277 | - | if !o.commitFailed(err, &enabled) { | |||
| 1278 | - | return err | |||
| 1279 | - | } | |||
| 1280 | - | } | |||
| 1281 | - | } | |||
| 1282 | continue | 1336 | continue | ||
| 1283 | } | 1337 | } | ||
| 1284 | - | // Modeset on every enable: a fast disable+enable can coalesce to one true. | 1338 | + | stateDirty = on |
| 1285 | - | if on { | 1339 | + | case <-reacquireDue: |
| 1286 | - | enabled = true // this enable may supersede a previous master loss | 1340 | + | // The seat reported active before the kernel gave DRM master |
| 1287 | - | var err error | 1341 | + | // back: the enable modeset was refused. Try it again. |
| 1288 | - | if o.fbs[0] == 0 { | 1342 | + | reacquire.attempts++ |
| 1289 | - | err = o.showImages(r, imagesDriver, nil) | 1343 | + | skip, err := setActive(true) |
| 1290 | - | } else { | 1344 | + | if err != nil { |
| 1291 | - | err = o.modeset() | 1345 | + | return err |
| 1292 | - | } | |||
| 1293 | - | if err != nil { | |||
| 1294 | - | if !o.commitFailed(err, &enabled) { | |||
| 1295 | - | return err | |||
| 1296 | - | } | |||
| 1297 | - | continue // epoch retry or wait disabled for master reacquisition | |||
| 1298 | - | } | |||
| 1299 | - | o.observeSecurity() | |||
| 1300 | - | if o.protected { | |||
| 1301 | - | continue | |||
| 1302 | - | } | |||
| 1303 | - | o.probeAsync(r) | |||
| 1304 | - | dirty = haveScene | |||
| 1305 | - | stateDirty = true | |||
| 1306 | } | 1346 | } | ||
| 1307 | - | enabled = on | 1347 | + | if skip { |
| 1308 | - | o.log.Info().Bool("enabled", on).Msg("output") | 1348 | + | continue |
| 1349 | + | } | |||
| 1350 | + | stateDirty = true | |||
| 1309 | case ev := <-o.flipped: | 1351 | case ev := <-o.flipped: | ||
| 1310 | o.observeSecurity() | 1352 | o.observeSecurity() | ||
| 1311 | if !o.completed(ev, seen) { | 1353 | if !o.completed(ev, seen) { | ||
A internal/adapters/drm/reacquire.go +39 -0
| @@ -0,0 +1,39 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // masterRetry retries an enable modeset that the kernel refused because | |
| 6 | + | // DRM master was not back yet. The seat can report the session active | |
| 7 | + | // before the kernel hands master back; without a retry the output stays | |
| 8 | + | // dark until the next VT switch. | |
| 9 | + | type masterRetry struct { | |
| 10 | + | pending bool | |
| 11 | + | attempts int | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | const ( | |
| 15 | + | masterRetryFirst = 20 * time.Millisecond | |
| 16 | + | masterRetryMax = time.Second | |
| 17 | + | // masterRetryLimit bounds the retries of one enable (about 30 s). | |
| 18 | + | masterRetryLimit = 40 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // refused records an enable refused with err; only a lost master is | |
| 22 | + | // retried, and enabled stays false until a retry succeeds. | |
| 23 | + | func (m *masterRetry) refused(err error, enabled *bool) { | |
| 24 | + | if !lostMaster(err) || *enabled { | |
| 25 | + | return | |
| 26 | + | } | |
| 27 | + | m.pending = true | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // done ends the retry: the enable succeeded or the seat changed again. | |
| 31 | + | func (m *masterRetry) done() { *m = masterRetry{} } | |
| 32 | + | ||
| 33 | + | // next is the delay before the next attempt, if one is due. | |
| 34 | + | func (m *masterRetry) next() (time.Duration, bool) { | |
| 35 | + | if !m.pending || m.attempts >= masterRetryLimit { | |
| 36 | + | return 0, false | |
| 37 | + | } | |
| 38 | + | return min(masterRetryFirst<<min(m.attempts, 6), masterRetryMax), true | |
| 39 | + | } | |
| @@ -0,0 +1,39 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // masterRetry retries an enable modeset that the kernel refused because | |||
| 6 | + | // DRM master was not back yet. The seat can report the session active | |||
| 7 | + | // before the kernel hands master back; without a retry the output stays | |||
| 8 | + | // dark until the next VT switch. | |||
| 9 | + | type masterRetry struct { | |||
| 10 | + | pending bool | |||
| 11 | + | attempts int | |||
| 12 | + | } | |||
| 13 | + | ||||
| 14 | + | const ( | |||
| 15 | + | masterRetryFirst = 20 * time.Millisecond | |||
| 16 | + | masterRetryMax = time.Second | |||
| 17 | + | // masterRetryLimit bounds the retries of one enable (about 30 s). | |||
| 18 | + | masterRetryLimit = 40 | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // refused records an enable refused with err; only a lost master is | |||
| 22 | + | // retried, and enabled stays false until a retry succeeds. | |||
| 23 | + | func (m *masterRetry) refused(err error, enabled *bool) { | |||
| 24 | + | if !lostMaster(err) || *enabled { | |||
| 25 | + | return | |||
| 26 | + | } | |||
| 27 | + | m.pending = true | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // done ends the retry: the enable succeeded or the seat changed again. | |||
| 31 | + | func (m *masterRetry) done() { *m = masterRetry{} } | |||
| 32 | + | ||||
| 33 | + | // next is the delay before the next attempt, if one is due. | |||
| 34 | + | func (m *masterRetry) next() (time.Duration, bool) { | |||
| 35 | + | if !m.pending || m.attempts >= masterRetryLimit { | |||
| 36 | + | return 0, false | |||
| 37 | + | } | |||
| 38 | + | return min(masterRetryFirst<<min(m.attempts, 6), masterRetryMax), true | |||
| 39 | + | } | |||
A internal/adapters/drm/reacquire_test.go +116 -0
| @@ -0,0 +1,116 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "testing" | |
| 7 | + | "testing/synctest" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |
| 12 | + | "github.com/stretchr/testify/mock" | |
| 13 | + | "golang.org/x/sys/unix" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func TestMasterRetryBackoff(t *testing.T) { | |
| 17 | + | var m masterRetry | |
| 18 | + | enabled := false | |
| 19 | + | if _, ok := m.next(); ok { | |
| 20 | + | t.Fatal("retry due before a refusal") | |
| 21 | + | } | |
| 22 | + | m.refused(unix.EBUSY, &enabled) | |
| 23 | + | if _, ok := m.next(); ok { | |
| 24 | + | t.Fatal("a busy refusal is retried by the busy path, not here") | |
| 25 | + | } | |
| 26 | + | m.refused(unix.EACCES, &enabled) | |
| 27 | + | d, ok := m.next() | |
| 28 | + | if !ok || d != masterRetryFirst { | |
| 29 | + | t.Fatalf("first retry %v %v", d, ok) | |
| 30 | + | } | |
| 31 | + | m.attempts = 30 | |
| 32 | + | if d, _ := m.next(); d != masterRetryMax { | |
| 33 | + | t.Fatalf("capped retry %v", d) | |
| 34 | + | } | |
| 35 | + | m.attempts = masterRetryLimit | |
| 36 | + | if _, ok := m.next(); ok { | |
| 37 | + | t.Fatal("retries past the limit") | |
| 38 | + | } | |
| 39 | + | m.done() | |
| 40 | + | if _, ok := m.next(); ok { | |
| 41 | + | t.Fatal("retry after done") | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // The seat reports active before the kernel gives DRM master back: the | |
| 46 | + | // enable modeset is refused, then retried until it succeeds, without a | |
| 47 | + | // second seat change. | |
| 48 | + | func TestEnableRefusedForMasterIsRetried(t *testing.T) { | |
| 49 | + | synctest.Test(t, enableRefusedForMasterIsRetried) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func enableRefusedForMasterIsRetried(t *testing.T) { | |
| 53 | + | refuse := true | |
| 54 | + | o, k, commits, commitMu := testOutputRules(t, func(_ *atomicReq, flags uint32) error { | |
| 55 | + | if refuse && flags&atomicAllowModes != 0 && flags&atomicTestOnly == 0 { | |
| 56 | + | return unix.EACCES | |
| 57 | + | } | |
| 58 | + | return nil | |
| 59 | + | }) | |
| 60 | + | o.cursor, o.tearing = nil, false | |
| 61 | + | r := portsmocks.NewMockRenderer(t) | |
| 62 | + | r.EXPECT().SetHDR(mock.Anything).Return().Maybe() | |
| 63 | + | r.EXPECT().UseTarget(mock.Anything).Return().Maybe() | |
| 64 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Maybe() | |
| 65 | + | r.EXPECT().Close().Return().Once() | |
| 66 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil).Maybe() | |
| 67 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |
| 68 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 69 | + | active := make(chan bool, 1) | |
| 70 | + | active <- false | |
| 71 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 72 | + | done := make(chan error, 1) | |
| 73 | + | go func() { | |
| 74 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | |
| 75 | + | }() | |
| 76 | + | modesets := func() int { | |
| 77 | + | commitMu.Lock() | |
| 78 | + | defer commitMu.Unlock() | |
| 79 | + | n := 0 | |
| 80 | + | for _, c := range *commits { | |
| 81 | + | if c.flags&atomicAllowModes != 0 && c.flags&atomicTestOnly == 0 { | |
| 82 | + | n++ | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | return n | |
| 86 | + | } | |
| 87 | + | synctest.Wait() | |
| 88 | + | active <- true | |
| 89 | + | synctest.Wait() | |
| 90 | + | if n := modesets(); n != 1 { | |
| 91 | + | t.Fatalf("%d modesets after the refused enable", n) | |
| 92 | + | } | |
| 93 | + | time.Sleep(masterRetryFirst) | |
| 94 | + | synctest.Wait() | |
| 95 | + | if n := modesets(); n != 2 { | |
| 96 | + | t.Fatalf("%d modesets after the first retry", n) | |
| 97 | + | } | |
| 98 | + | commitMu.Lock() | |
| 99 | + | refuse = false | |
| 100 | + | commitMu.Unlock() | |
| 101 | + | time.Sleep(2 * masterRetryFirst) | |
| 102 | + | synctest.Wait() | |
| 103 | + | if n := modesets(); n != 3 { | |
| 104 | + | t.Fatalf("%d modesets after the retry that succeeds", n) | |
| 105 | + | } | |
| 106 | + | // Once enabled, nothing retries any more. | |
| 107 | + | time.Sleep(masterRetryMax * 2) | |
| 108 | + | synctest.Wait() | |
| 109 | + | if n := modesets(); n != 3 { | |
| 110 | + | t.Fatalf("%d modesets after success", n) | |
| 111 | + | } | |
| 112 | + | cancel() | |
| 113 | + | if err := <-done; err != nil && !errors.Is(err, context.Canceled) { | |
| 114 | + | t.Fatal(err) | |
| 115 | + | } | |
| 116 | + | } | |
| @@ -0,0 +1,116 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "testing" | |||
| 7 | + | "testing/synctest" | |||
| 8 | + | "time" | |||
| 9 | + | ||||
| 10 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 11 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 12 | + | "github.com/stretchr/testify/mock" | |||
| 13 | + | "golang.org/x/sys/unix" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func TestMasterRetryBackoff(t *testing.T) { | |||
| 17 | + | var m masterRetry | |||
| 18 | + | enabled := false | |||
| 19 | + | if _, ok := m.next(); ok { | |||
| 20 | + | t.Fatal("retry due before a refusal") | |||
| 21 | + | } | |||
| 22 | + | m.refused(unix.EBUSY, &enabled) | |||
| 23 | + | if _, ok := m.next(); ok { | |||
| 24 | + | t.Fatal("a busy refusal is retried by the busy path, not here") | |||
| 25 | + | } | |||
| 26 | + | m.refused(unix.EACCES, &enabled) | |||
| 27 | + | d, ok := m.next() | |||
| 28 | + | if !ok || d != masterRetryFirst { | |||
| 29 | + | t.Fatalf("first retry %v %v", d, ok) | |||
| 30 | + | } | |||
| 31 | + | m.attempts = 30 | |||
| 32 | + | if d, _ := m.next(); d != masterRetryMax { | |||
| 33 | + | t.Fatalf("capped retry %v", d) | |||
| 34 | + | } | |||
| 35 | + | m.attempts = masterRetryLimit | |||
| 36 | + | if _, ok := m.next(); ok { | |||
| 37 | + | t.Fatal("retries past the limit") | |||
| 38 | + | } | |||
| 39 | + | m.done() | |||
| 40 | + | if _, ok := m.next(); ok { | |||
| 41 | + | t.Fatal("retry after done") | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // The seat reports active before the kernel gives DRM master back: the | |||
| 46 | + | // enable modeset is refused, then retried until it succeeds, without a | |||
| 47 | + | // second seat change. | |||
| 48 | + | func TestEnableRefusedForMasterIsRetried(t *testing.T) { | |||
| 49 | + | synctest.Test(t, enableRefusedForMasterIsRetried) | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func enableRefusedForMasterIsRetried(t *testing.T) { | |||
| 53 | + | refuse := true | |||
| 54 | + | o, k, commits, commitMu := testOutputRules(t, func(_ *atomicReq, flags uint32) error { | |||
| 55 | + | if refuse && flags&atomicAllowModes != 0 && flags&atomicTestOnly == 0 { | |||
| 56 | + | return unix.EACCES | |||
| 57 | + | } | |||
| 58 | + | return nil | |||
| 59 | + | }) | |||
| 60 | + | o.cursor, o.tearing = nil, false | |||
| 61 | + | r := portsmocks.NewMockRenderer(t) | |||
| 62 | + | r.EXPECT().SetHDR(mock.Anything).Return().Maybe() | |||
| 63 | + | r.EXPECT().UseTarget(mock.Anything).Return().Maybe() | |||
| 64 | + | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Maybe() | |||
| 65 | + | r.EXPECT().Close().Return().Once() | |||
| 66 | + | k.EXPECT().createBlob(mock.Anything).Return(99, nil).Maybe() | |||
| 67 | + | k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe() | |||
| 68 | + | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |||
| 69 | + | active := make(chan bool, 1) | |||
| 70 | + | active <- false | |||
| 71 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 72 | + | done := make(chan error, 1) | |||
| 73 | + | go func() { | |||
| 74 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil) | |||
| 75 | + | }() | |||
| 76 | + | modesets := func() int { | |||
| 77 | + | commitMu.Lock() | |||
| 78 | + | defer commitMu.Unlock() | |||
| 79 | + | n := 0 | |||
| 80 | + | for _, c := range *commits { | |||
| 81 | + | if c.flags&atomicAllowModes != 0 && c.flags&atomicTestOnly == 0 { | |||
| 82 | + | n++ | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 85 | + | return n | |||
| 86 | + | } | |||
| 87 | + | synctest.Wait() | |||
| 88 | + | active <- true | |||
| 89 | + | synctest.Wait() | |||
| 90 | + | if n := modesets(); n != 1 { | |||
| 91 | + | t.Fatalf("%d modesets after the refused enable", n) | |||
| 92 | + | } | |||
| 93 | + | time.Sleep(masterRetryFirst) | |||
| 94 | + | synctest.Wait() | |||
| 95 | + | if n := modesets(); n != 2 { | |||
| 96 | + | t.Fatalf("%d modesets after the first retry", n) | |||
| 97 | + | } | |||
| 98 | + | commitMu.Lock() | |||
| 99 | + | refuse = false | |||
| 100 | + | commitMu.Unlock() | |||
| 101 | + | time.Sleep(2 * masterRetryFirst) | |||
| 102 | + | synctest.Wait() | |||
| 103 | + | if n := modesets(); n != 3 { | |||
| 104 | + | t.Fatalf("%d modesets after the retry that succeeds", n) | |||
| 105 | + | } | |||
| 106 | + | // Once enabled, nothing retries any more. | |||
| 107 | + | time.Sleep(masterRetryMax * 2) | |||
| 108 | + | synctest.Wait() | |||
| 109 | + | if n := modesets(); n != 3 { | |||
| 110 | + | t.Fatalf("%d modesets after success", n) | |||
| 111 | + | } | |||
| 112 | + | cancel() | |||
| 113 | + | if err := <-done; err != nil && !errors.Is(err, context.Canceled) { | |||
| 114 | + | t.Fatal(err) | |||
| 115 | + | } | |||
| 116 | + | } | |||