refactor(daemon): deepen attachment lifecycle authority (#224)
* refactor(daemon): deepen attachment lifecycle authority * fix(daemon): validate attachment effect authority
- commit
- 5a95bf33d48d875c69f1bd931436e18b9ff13e13
- author
- bnema · · 2026-08-27 13:15 UTC
- committer
- GitHub · · 2026-08-27 13:15 UTC
- parent
- d77542a
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
67 files · +1184 -1056
M internal/usecase/daemon/attachment_connection.go +68 -78
| @@ -2,44 +2,42 @@ package daemon | |||||
| 2 | 2 | ||||
| 3 | import "github.com/bnema/vev/internal/ports" | 3 | import "github.com/bnema/vev/internal/ports" | ||
| 4 | 4 | ||||
| 5 | - | // attachmentConnectionToken is the exact capability for one registered | 5 | + | // attachmentCapability is the exact capability for one registered |
| 6 | - | // attachment connection incarnation. It is invalid as soon as any of the | 6 | + | // Attachment Connection incarnation. It is invalid as soon as its Session |
| 7 | - | // session, attachment membership, connection generation, transport link, or | 7 | + | // membership, generation, Transport incarnation, or render lease changes. |
| 8 | - | // coordinator lease changes. | 8 | + | type attachmentCapability struct { |
| 9 | - | type attachmentConnectionToken struct { | |||
| 10 | sess *session | 9 | sess *session | ||
| 11 | ac *attachedClient | 10 | ac *attachedClient | ||
| 12 | generation uint64 | 11 | generation uint64 | ||
| 13 | transport transportSnapshot | 12 | transport transportSnapshot | ||
| 14 | lease *attachmentLease | 13 | lease *attachmentLease | ||
| 15 | - | effect *attachmentEffectTicket | |||
| 16 | // rebase marks a cross-session move whose first paint starts a dependency- | 14 | // rebase marks a cross-session move whose first paint starts a dependency- | ||
| 17 | // free output chain after publication releases architecture locks. | 15 | // free output chain after publication releases architecture locks. | ||
| 18 | rebase bool | 16 | rebase bool | ||
| 19 | } | 17 | } | ||
| 20 | 18 | ||||
| 21 | - | func attachmentToken(entry *session, ac *attachedClient, tr ports.Transport) attachmentConnectionToken { | 19 | + | func captureAttachmentCapability(entry *session, ac *attachedClient, tr ports.Transport) attachmentCapability { |
| 22 | if entry == nil || entry.core() == nil || ac == nil || tr == nil { | 20 | if entry == nil || entry.core() == nil || ac == nil || tr == nil { | ||
| 23 | - | return attachmentConnectionToken{} | 21 | + | return attachmentCapability{} |
| 24 | } | 22 | } | ||
| 25 | // Capture one concrete link incarnation before taking the session lock. The | 23 | // Capture one concrete link incarnation before taking the session lock. The | ||
| 26 | // same snapshot is revalidated below; do not take a second snapshot and | 24 | // same snapshot is revalidated below; do not take a second snapshot and | ||
| 27 | // accidentally bind a replacement link. | 25 | // accidentally bind a replacement link. | ||
| 28 | transport := ac.transportSnapshot() | 26 | transport := ac.transportSnapshot() | ||
| 29 | if transport.transport != tr { | 27 | if transport.transport != tr { | ||
| 30 | - | return attachmentConnectionToken{} | 28 | + | return attachmentCapability{} |
| 31 | } | 29 | } | ||
| 32 | - | if ac.beforeAttachmentTokenValidation != nil { | 30 | + | if ac.beforeAttachmentCapabilityValidation != nil { |
| 33 | - | ac.beforeAttachmentTokenValidation() | 31 | + | ac.beforeAttachmentCapabilityValidation() |
| 34 | } | 32 | } | ||
| 35 | core := entry.core() | 33 | core := entry.core() | ||
| 36 | core.mu.Lock() | 34 | core.mu.Lock() | ||
| 37 | if !attachmentRegisteredLocked(entry, ac) || !ac.transportSnapshotCurrent(transport) { | 35 | if !attachmentRegisteredLocked(entry, ac) || !ac.transportSnapshotCurrent(transport) { | ||
| 38 | core.mu.Unlock() | 36 | core.mu.Unlock() | ||
| 39 | - | return attachmentConnectionToken{} | 37 | + | return attachmentCapability{} |
| 40 | } | 38 | } | ||
| 41 | - | generation := ac.connectionGeneration.Load() | 39 | + | generation := ac.lifecycle.generation.Load() |
| 42 | - | token := attachmentConnectionToken{ | 40 | + | token := attachmentCapability{ |
| 43 | sess: entry, | 41 | sess: entry, | ||
| 44 | ac: ac, | 42 | ac: ac, | ||
| 45 | generation: generation, | 43 | generation: generation, | ||
| @@ -50,101 +48,93 @@ func attachmentToken(entry *session, ac *attachedClient, tr ports.Transport) att | |||||
| 50 | } | 48 | } | ||
| 51 | core.mu.Unlock() | 49 | core.mu.Unlock() | ||
| 52 | if !token.current() { | 50 | if !token.current() { | ||
| 53 | - | return attachmentConnectionToken{} | 51 | + | return attachmentCapability{} |
| 54 | } | 52 | } | ||
| 55 | - | ac.bootstrapAttachmentCapability(token) | 53 | + | ac.lifecycle.installInitialCapability(token) |
| 56 | return token | 54 | return token | ||
| 57 | } | 55 | } | ||
| 58 | 56 | ||||
| 59 | - | func (s *session) attachmentToken(ac *attachedClient, tr ports.Transport) attachmentConnectionToken { | 57 | + | func (s *session) captureAttachmentCapability(ac *attachedClient, tr ports.Transport) attachmentCapability { |
| 60 | - | return attachmentToken(s, ac, tr) | 58 | + | return captureAttachmentCapability(s, ac, tr) |
| 61 | } | 59 | } | ||
| 62 | 60 | ||||
| 63 | - | // current validates every immutable identity captured by the token. Membership | 61 | + | func (t attachmentCapability) sameIdentity(other attachmentCapability) bool { |
| 64 | - | // is the sole authority; there is no owner, replacement, or compatibility role. | 62 | + | return t.sess != nil && t.ac != nil && t.sess == other.sess && t.ac == other.ac && |
| 65 | - | func (t attachmentConnectionToken) current() bool { | 63 | + | t.generation == other.generation && t.transport.transport == other.transport.transport && |
| 66 | - | return t.sess != nil && t.ac != nil && | 64 | + | t.transport.incarnation == other.transport.incarnation && t.lease == other.lease |
| 67 | - | t.ac.connectionGeneration.Load() == t.generation && | |||
| 68 | - | t.ac.currentAttachmentSession() == t.sess && | |||
| 69 | - | t.ac.transportSnapshotCurrent(t.transport) && | |||
| 70 | - | attachmentRegistered(t.sess, t.ac) | |||
| 71 | } | 65 | } | ||
| 72 | 66 | ||||
| 73 | - | // attachmentCurrent additionally validates the optional coordinator lease used | 67 | + | func (t attachmentCapability) matchesConnectionSnapshot(sess *session, ac *attachedClient, transport transportSnapshot) bool { |
| 74 | - | // by render and resize effects. Attachments without that optional lease still | 68 | + | return t.sess == sess && t.ac == ac && ac != nil && t.transport == transport && |
| 75 | - | // own their independent connection lifecycle. | 69 | + | ac.lifecycle.identityCurrent(t) |
| 76 | - | func (t attachmentConnectionToken) attachmentCurrent() bool { | 70 | + | } |
| 77 | - | if !t.current() || t.ac.currentAttachmentSession() != t.sess { | 71 | + | |
| 72 | + | func (l *attachmentLifecycle) identityCurrent(capability attachmentCapability) bool { | |||
| 73 | + | return l != nil && capability.sess != nil && capability.ac != nil && | |||
| 74 | + | capability.ac.lifecycle.generation.Load() == capability.generation && | |||
| 75 | + | capability.ac.currentAttachmentSession() == capability.sess && | |||
| 76 | + | capability.ac.transportSnapshotCurrent(capability.transport) | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // current validates every immutable identity captured by the capability. | |||
| 80 | + | // Membership is the sole authority; there is no owner, replacement, or | |||
| 81 | + | // compatibility role. | |||
| 82 | + | func (l *attachmentLifecycle) current(capability attachmentCapability) bool { | |||
| 83 | + | if !l.identityCurrent(capability) || !attachmentRegistered(capability.sess, capability.ac) { | |||
| 78 | return false | 84 | return false | ||
| 79 | } | 85 | } | ||
| 80 | - | if t.lease == nil { | 86 | + | if capability.lease == nil { |
| 81 | return true | 87 | return true | ||
| 82 | } | 88 | } | ||
| 83 | - | rc := t.sess.core().coordinator.Load() | 89 | + | rc := capability.sess.core().coordinator.Load() |
| 84 | - | return rc != nil && t.lease.attachment == t.ac && rc.leaseCurrent(t.lease, true) | 90 | + | return rc != nil && capability.lease.attachment == capability.ac && rc.leaseCurrent(capability.lease, false) |
| 85 | } | 91 | } | ||
| 86 | 92 | ||||
| 87 | - | func (t attachmentConnectionToken) attachmentEffectCurrent() bool { | 93 | + | func (t attachmentCapability) current() bool { |
| 88 | - | if t.effect != nil && !t.effect.ended.Load() { | 94 | + | return t.ac != nil && t.ac.lifecycle.current(t) |
| 89 | - | return true | |||
| 90 | - | } | |||
| 91 | - | return t.attachmentCurrent() | |||
| 92 | } | 95 | } | ||
| 93 | 96 | ||||
| 94 | - | func beginAttachmentLeaseEffect(sess *session, ac *attachedClient, lease *attachmentLease) (*attachmentEffectTicket, bool) { | 97 | + | func beginAttachmentLeaseEffect(sess *session, ac *attachedClient, lease *attachmentLease) (*attachmentEffect, bool) { |
| 95 | if sess == nil || ac == nil || lease == nil { | 98 | if sess == nil || ac == nil || lease == nil { | ||
| 96 | return nil, false | 99 | return nil, false | ||
| 97 | } | 100 | } | ||
| 98 | - | token := attachmentToken(sess, ac, ac.transport()) | 101 | + | token := captureAttachmentCapability(sess, ac, ac.transport()) |
| 99 | token.lease = lease | 102 | token.lease = lease | ||
| 100 | return ac.beginAttachmentEffect(token) | 103 | return ac.beginAttachmentEffect(token) | ||
| 101 | } | 104 | } | ||
| 102 | 105 | ||||
| 103 | - | func (t *attachmentConnectionToken) endAttachmentEffect() { | 106 | + | func (t attachmentCapability) currentInSessionLocked(sess *session) bool { |
| 104 | - | if t == nil || t.effect == nil { | 107 | + | return t.sess == sess && t.ac != nil && t.ac.lifecycle.currentSessionLocked(t) |
| 105 | - | return | |||
| 106 | - | } | |||
| 107 | - | t.effect.End() | |||
| 108 | - | t.effect = nil | |||
| 109 | } | 108 | } | ||
| 110 | 109 | ||||
| 111 | - | // attachmentEffectCurrentSessionLocked validates authority at a session-owned | 110 | + | func (t attachmentCapability) currentInSessionAndLeaseLocked(sess *session, ac *attachedClient, coordinator *renderCoordinator) bool { |
| 112 | - | // mutation boundary where sess.core().mu is already held. A live effect ticket | 111 | + | return t.sess == sess && t.ac == ac && ac != nil && |
| 113 | - | // preserves the exact capability it captured for the effect because attachment | 112 | + | ac.lifecycle.currentSessionAndLeaseLocked(t, coordinator) |
| 114 | - | // transitions drain the gate before changing identity. This differs from | 113 | + | } |
| 115 | - | // attachmentCurrent, which revalidates the attachment's currently published identity. | 114 | + | |
| 116 | - | func (t attachmentConnectionToken) attachmentEffectCurrentSessionLocked() bool { | 115 | + | // currentSessionLocked validates capability authority where sess.core().mu is |
| 117 | - | if t.effect != nil && !t.effect.ended.Load() { | 116 | + | // already held. An admitted effect does not use this comparison: a transition |
| 118 | - | return true | 117 | + | // cannot change identity until that effect ends. |
| 119 | - | } | 118 | + | func (l *attachmentLifecycle) currentSessionLocked(capability attachmentCapability) bool { |
| 120 | - | if t.sess == nil || t.ac == nil || | 119 | + | if !l.identityCurrent(capability) || !attachmentRegisteredLocked(capability.sess, capability.ac) { |
| 121 | - | t.ac.connectionGeneration.Load() != t.generation || | |||
| 122 | - | !t.ac.transportSnapshotCurrent(t.transport) || | |||
| 123 | - | t.ac.currentAttachmentSession() != t.sess || | |||
| 124 | - | !attachmentRegisteredLocked(t.sess, t.ac) { | |||
| 125 | return false | 120 | return false | ||
| 126 | } | 121 | } | ||
| 127 | - | if t.lease == nil { | 122 | + | if capability.lease == nil { |
| 128 | return true | 123 | return true | ||
| 129 | } | 124 | } | ||
| 130 | - | rc := t.sess.core().coordinator.Load() | 125 | + | rc := capability.sess.core().coordinator.Load() |
| 131 | - | return rc != nil && t.lease.attachment == t.ac && rc.leaseCurrent(t.lease, true) | 126 | + | return rc != nil && capability.lease.attachment == capability.ac && rc.leaseCurrent(capability.lease, false) |
| 132 | } | 127 | } | ||
| 133 | 128 | ||||
| 134 | - | func (t attachmentConnectionToken) sendControl(frame ports.Frame) error { | 129 | + | // currentSessionAndLeaseLocked validates the same capability tuple when the |
| 135 | - | if t.ac == nil || t.transport.transport == nil { | 130 | + | // caller already holds both the Session and coordinator locks. |
| 136 | - | return errAttachmentTransition | 131 | + | func (l *attachmentLifecycle) currentSessionAndLeaseLocked(capability attachmentCapability, coordinator *renderCoordinator) bool { |
| 137 | - | } | 132 | + | if !l.identityCurrent(capability) || !attachmentRegisteredLocked(capability.sess, capability.ac) { |
| 138 | - | t.ac.sendMu.Lock() | 133 | + | return false |
| 139 | - | defer t.ac.sendMu.Unlock() | |||
| 140 | - | if t.effect == nil || t.effect.ended.Load() || !t.ac.transportSnapshotCurrent(t.transport) || | |||
| 141 | - | !t.effect.beginTransportSend(t.transport) { | |||
| 142 | - | return errAttachmentTransition | |||
| 143 | } | 134 | } | ||
| 144 | - | err := t.transport.transport.Send(frame) | 135 | + | if capability.lease == nil { |
| 145 | - | if err != nil { | 136 | + | return true |
| 146 | - | t.effect.reportTransportFailure(t.transport) | |||
| 147 | } | 137 | } | ||
| 148 | - | t.effect.endTransportSend() | 138 | + | return coordinator != nil && capability.lease.attachment == capability.ac && |
| 149 | - | return err | 139 | + | coordinator.leaseCurrentLocked(capability.lease, false) |
| 150 | } | 140 | } | ||
M internal/usecase/daemon/attachment_effect_freeze.go +17 -17
| @@ -60,7 +60,7 @@ func (d *attachmentEffectDrainDeadline) Done() <-chan struct{} { | |||||
| 60 | return d.done | 60 | return d.done | ||
| 61 | } | 61 | } | ||
| 62 | 62 | ||||
| 63 | - | type frozenAttachmentEffectGates struct { | 63 | + | type attachmentTransitionGuard struct { |
| 64 | clients []*attachedClient | 64 | clients []*attachedClient | ||
| 65 | interruptions map[*attachedClient]transportSnapshot | 65 | interruptions map[*attachedClient]transportSnapshot | ||
| 66 | acquired bool | 66 | acquired bool | ||
| @@ -84,7 +84,7 @@ type attachmentEffectFreezeOptions struct { | |||||
| 84 | 84 | ||||
| 85 | // freezeAttachmentEffectGates establishes transition priority in immutable attachment | 85 | // freezeAttachmentEffectGates establishes transition priority in immutable attachment | ||
| 86 | // order, then drains earlier admissions. | 86 | // order, then drains earlier admissions. | ||
| 87 | - | func freezeAttachmentEffectGates(clients ...*attachedClient) frozenAttachmentEffectGates { | 87 | + | func freezeAttachmentEffectGates(clients ...*attachedClient) attachmentTransitionGuard { |
| 88 | return freezeAttachmentEffectGatesWith(attachmentEffectFreezeOptions{}, clients...) | 88 | return freezeAttachmentEffectGatesWith(attachmentEffectFreezeOptions{}, clients...) | ||
| 89 | } | 89 | } | ||
| 90 | 90 | ||||
| @@ -93,11 +93,11 @@ func freezeAttachmentEffectGates(clients ...*attachedClient) frozenAttachmentEff | |||||
| 93 | // behind another owner, only gates acquired by this call are rolled back, in | 93 | // behind another owner, only gates acquired by this call are rolled back, in | ||
| 94 | // reverse order. nonblocking is used by move reservations to avoid inverting | 94 | // reverse order. nonblocking is used by move reservations to avoid inverting | ||
| 95 | // the move/teardown protocols. | 95 | // the move/teardown protocols. | ||
| 96 | - | func freezeAttachmentEffectGatesWith(options attachmentEffectFreezeOptions, clients ...*attachedClient) frozenAttachmentEffectGates { | 96 | + | func freezeAttachmentEffectGatesWith(options attachmentEffectFreezeOptions, clients ...*attachedClient) attachmentTransitionGuard { |
| 97 | ordered := orderedAttachmentEffectClients(clients) | 97 | ordered := orderedAttachmentEffectClients(clients) | ||
| 98 | frozen, acquired := acquireAttachmentEffectGates(ordered, options.done, options.afterFrozen, options.nonblocking) | 98 | frozen, acquired := acquireAttachmentEffectGates(ordered, options.done, options.afterFrozen, options.nonblocking) | ||
| 99 | if !acquired { | 99 | if !acquired { | ||
| 100 | - | return frozenAttachmentEffectGates{} | 100 | + | return attachmentTransitionGuard{} |
| 101 | } | 101 | } | ||
| 102 | 102 | ||||
| 103 | interruptByClient := attachmentTransportInterruptsByClient(options.interrupts) | 103 | interruptByClient := attachmentTransportInterruptsByClient(options.interrupts) | ||
| @@ -121,18 +121,18 @@ func orderedAttachmentEffectClients(clients []*attachedClient) []*attachedClient | |||||
| 121 | continue | 121 | continue | ||
| 122 | } | 122 | } | ||
| 123 | unique[ac] = struct{}{} | 123 | unique[ac] = struct{}{} | ||
| 124 | - | ac.attachmentEffects.immutableOrder() | 124 | + | ac.lifecycle.immutableOrder() |
| 125 | ordered = append(ordered, ac) | 125 | ordered = append(ordered, ac) | ||
| 126 | } | 126 | } | ||
| 127 | sort.Slice(ordered, func(i, j int) bool { | 127 | sort.Slice(ordered, func(i, j int) bool { | ||
| 128 | - | return ordered[i].attachmentEffects.order.Load() < ordered[j].attachmentEffects.order.Load() | 128 | + | return ordered[i].lifecycle.order.Load() < ordered[j].lifecycle.order.Load() |
| 129 | }) | 129 | }) | ||
| 130 | return ordered | 130 | return ordered | ||
| 131 | } | 131 | } | ||
| 132 | 132 | ||||
| 133 | // waitForAttachmentEffectChangeLocked requires g.mu and returns with g.mu held. A | 133 | // waitForAttachmentEffectChangeLocked requires g.mu and returns with g.mu held. A | ||
| 134 | // true result means the overall deadline won and the caller must stop waiting. | 134 | // true result means the overall deadline won and the caller must stop waiting. | ||
| 135 | - | func waitForAttachmentEffectChangeLocked(g *attachmentEffectGate, done func() <-chan struct{}) bool { | 135 | + | func waitForAttachmentEffectChangeLocked(g *attachmentLifecycle, done func() <-chan struct{}) bool { |
| 136 | if done == nil { | 136 | if done == nil { | ||
| 137 | g.cond.Wait() | 137 | g.cond.Wait() | ||
| 138 | return false | 138 | return false | ||
| @@ -156,22 +156,22 @@ func waitForAttachmentEffectChangeLocked(g *attachmentEffectGate, done func() <- | |||||
| 156 | // acquireAttachmentEffectGates establishes exclusive transition ownership in order. | 156 | // acquireAttachmentEffectGates establishes exclusive transition ownership in order. | ||
| 157 | // It must run without architecture locks. Deadline rollback releases only this | 157 | // It must run without architecture locks. Deadline rollback releases only this | ||
| 158 | // call's partial acquisition and does so in reverse order. | 158 | // call's partial acquisition and does so in reverse order. | ||
| 159 | - | func acquireAttachmentEffectGates(ordered []*attachedClient, done func() <-chan struct{}, afterFrozen func(*attachedClient), nonblocking bool) (frozenAttachmentEffectGates, bool) { | 159 | + | func acquireAttachmentEffectGates(ordered []*attachedClient, done func() <-chan struct{}, afterFrozen func(*attachedClient), nonblocking bool) (attachmentTransitionGuard, bool) { |
| 160 | - | frozen := frozenAttachmentEffectGates{clients: make([]*attachedClient, 0, len(ordered))} | 160 | + | frozen := attachmentTransitionGuard{clients: make([]*attachedClient, 0, len(ordered))} |
| 161 | for _, ac := range ordered { | 161 | for _, ac := range ordered { | ||
| 162 | - | g := &ac.attachmentEffects | 162 | + | g := &ac.lifecycle |
| 163 | g.mu.Lock() | 163 | g.mu.Lock() | ||
| 164 | g.initLocked() | 164 | g.initLocked() | ||
| 165 | for g.phase == attachmentEffectsFrozen { | 165 | for g.phase == attachmentEffectsFrozen { | ||
| 166 | if nonblocking { | 166 | if nonblocking { | ||
| 167 | g.mu.Unlock() | 167 | g.mu.Unlock() | ||
| 168 | frozen.unfreeze() | 168 | frozen.unfreeze() | ||
| 169 | - | return frozenAttachmentEffectGates{}, false | 169 | + | return attachmentTransitionGuard{}, false |
| 170 | } | 170 | } | ||
| 171 | if waitForAttachmentEffectChangeLocked(g, done) { | 171 | if waitForAttachmentEffectChangeLocked(g, done) { | ||
| 172 | g.mu.Unlock() | 172 | g.mu.Unlock() | ||
| 173 | frozen.unfreeze() | 173 | frozen.unfreeze() | ||
| 174 | - | return frozenAttachmentEffectGates{}, false | 174 | + | return attachmentTransitionGuard{}, false |
| 175 | } | 175 | } | ||
| 176 | } | 176 | } | ||
| 177 | g.phase = attachmentEffectsFrozen | 177 | g.phase = attachmentEffectsFrozen | ||
| @@ -204,7 +204,7 @@ func interruptFrozenAttachmentTransports(ordered []*attachedClient, interruptByC | |||||
| 204 | if !mayInterrupt { | 204 | if !mayInterrupt { | ||
| 205 | continue | 205 | continue | ||
| 206 | } | 206 | } | ||
| 207 | - | g := &ac.attachmentEffects | 207 | + | g := &ac.lifecycle |
| 208 | g.mu.Lock() | 208 | g.mu.Lock() | ||
| 209 | if gateHasTransportEffectLocked(g, expected) { | 209 | if gateHasTransportEffectLocked(g, expected) { | ||
| 210 | toClose = append(toClose, attachmentTransportInterrupt{ac: ac, transport: expected}) | 210 | toClose = append(toClose, attachmentTransportInterrupt{ac: ac, transport: expected}) | ||
| @@ -221,7 +221,7 @@ func interruptFrozenAttachmentTransports(ordered []*attachedClient, interruptByC | |||||
| 221 | // late tickets can then retire without pinning the owner indefinitely. | 221 | // late tickets can then retire without pinning the owner indefinitely. | ||
| 222 | func drainFrozenAttachmentEffects(ordered []*attachedClient, interruptByClient, interruptions map[*attachedClient]transportSnapshot, done func() <-chan struct{}) bool { | 222 | func drainFrozenAttachmentEffects(ordered []*attachedClient, interruptByClient, interruptions map[*attachedClient]transportSnapshot, done func() <-chan struct{}) bool { | ||
| 223 | for _, ac := range ordered { | 223 | for _, ac := range ordered { | ||
| 224 | - | g := &ac.attachmentEffects | 224 | + | g := &ac.lifecycle |
| 225 | g.mu.Lock() | 225 | g.mu.Lock() | ||
| 226 | for g.inFlight != 0 { | 226 | for g.inFlight != 0 { | ||
| 227 | if waitForAttachmentEffectChangeLocked(g, done) { | 227 | if waitForAttachmentEffectChangeLocked(g, done) { | ||
| @@ -238,7 +238,7 @@ func drainFrozenAttachmentEffects(ordered []*attachedClient, interruptByClient, | |||||
| 238 | return true | 238 | return true | ||
| 239 | } | 239 | } | ||
| 240 | 240 | ||||
| 241 | - | func gateHasTransportEffectLocked(g *attachmentEffectGate, expected transportSnapshot) bool { | 241 | + | func gateHasTransportEffectLocked(g *attachmentLifecycle, expected transportSnapshot) bool { |
| 242 | for _, active := range g.transportEffects { | 242 | for _, active := range g.transportEffects { | ||
| 243 | if active.transport == expected.transport && active.incarnation == expected.incarnation { | 243 | if active.transport == expected.transport && active.incarnation == expected.incarnation { | ||
| 244 | return true | 244 | return true | ||
| @@ -247,9 +247,9 @@ func gateHasTransportEffectLocked(g *attachmentEffectGate, expected transportSna | |||||
| 247 | return false | 247 | return false | ||
| 248 | } | 248 | } | ||
| 249 | 249 | ||||
| 250 | - | func (f frozenAttachmentEffectGates) unfreeze() { | 250 | + | func (f attachmentTransitionGuard) unfreeze() { |
| 251 | for i := len(f.clients) - 1; i >= 0; i-- { | 251 | for i := len(f.clients) - 1; i >= 0; i-- { | ||
| 252 | - | g := &f.clients[i].attachmentEffects | 252 | + | g := &f.clients[i].lifecycle |
| 253 | g.mu.Lock() | 253 | g.mu.Lock() | ||
| 254 | g.initLocked() | 254 | g.initLocked() | ||
| 255 | g.phase = attachmentEffectsStable | 255 | g.phase = attachmentEffectsStable | ||
M internal/usecase/daemon/attachment_effect_gate.go +108 -101
| @@ -15,46 +15,33 @@ const ( | |||||
| 15 | attachmentEffectsFrozen | 15 | attachmentEffectsFrozen | ||
| 16 | ) | 16 | ) | ||
| 17 | 17 | ||||
| 18 | - | // attachmentCapability is the single attachment-local publication consumed by | 18 | + | // attachmentLifecycle owns the immutable committed capability and the |
| 19 | - | // effect admission. It deliberately contains no independently mutable state: | 19 | + | // linearization gate for every attachment-bound observable effect. Transitions |
| 20 | - | // a ticket is admitted only when every field matches the frame's token. | 20 | + | // freeze and drain it before changing membership, generation, Transport, or |
| 21 | - | type attachmentCapability struct { | 21 | + | // render lease identity. |
| 22 | - | sess *session | 22 | + | type attachmentLifecycle struct { |
| 23 | - | generation uint64 | 23 | + | generation atomic.Uint64 |
| 24 | - | transport transportSnapshot | |||
| 25 | - | lease *attachmentLease | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | func capabilityFromToken(token attachmentConnectionToken) attachmentCapability { | |||
| 29 | - | return attachmentCapability{ | |||
| 30 | - | sess: token.sess, | |||
| 31 | - | generation: token.generation, | |||
| 32 | - | transport: token.transport, | |||
| 33 | - | lease: token.lease, | |||
| 34 | - | } | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | func (c attachmentCapability) matches(token attachmentConnectionToken) bool { | |||
| 38 | - | return token.ac != nil && c.sess != nil && token.sess != nil && c.sess == token.sess && | |||
| 39 | - | c.generation == token.generation && c.transport.transport == token.transport.transport && | |||
| 40 | - | c.transport.incarnation == token.transport.incarnation && c.lease == token.lease | |||
| 41 | - | } | |||
| 42 | - | ||||
| 43 | - | type attachmentEffectGate struct { | |||
| 44 | mu sync.Mutex | 24 | mu sync.Mutex | ||
| 45 | cond *sync.Cond | 25 | cond *sync.Cond | ||
| 46 | changed chan struct{} | 26 | changed chan struct{} | ||
| 47 | phase attachmentEffectPhase | 27 | phase attachmentEffectPhase | ||
| 48 | capability attachmentCapability | 28 | capability attachmentCapability | ||
| 49 | inFlight uint64 | 29 | inFlight uint64 | ||
| 50 | - | transportEffects map[*attachmentEffectTicket]transportSnapshot | 30 | + | transportEffects map[*attachmentEffect]transportSnapshot |
| 51 | failedTransport transportSnapshot | 31 | failedTransport transportSnapshot | ||
| 52 | order atomic.Uint64 | 32 | order atomic.Uint64 | ||
| 53 | } | 33 | } | ||
| 54 | 34 | ||||
| 55 | var nextAttachmentEffectGateOrder atomic.Uint64 | 35 | var nextAttachmentEffectGateOrder atomic.Uint64 | ||
| 56 | 36 | ||||
| 57 | - | func (g *attachmentEffectGate) initLocked() { | 37 | + | func (g *attachmentLifecycle) generationValue() uint64 { |
| 38 | + | if g == nil { | |||
| 39 | + | return 0 | |||
| 40 | + | } | |||
| 41 | + | return g.generation.Load() | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func (g *attachmentLifecycle) initLocked() { | |||
| 58 | if g.cond == nil { | 45 | if g.cond == nil { | ||
| 59 | g.cond = sync.NewCond(&g.mu) | 46 | g.cond = sync.NewCond(&g.mu) | ||
| 60 | } | 47 | } | ||
| @@ -63,14 +50,14 @@ func (g *attachmentEffectGate) initLocked() { | |||||
| 63 | } | 50 | } | ||
| 64 | } | 51 | } | ||
| 65 | 52 | ||||
| 66 | - | func (g *attachmentEffectGate) signalLocked() { | 53 | + | func (g *attachmentLifecycle) signalLocked() { |
| 67 | g.initLocked() | 54 | g.initLocked() | ||
| 68 | close(g.changed) | 55 | close(g.changed) | ||
| 69 | g.changed = make(chan struct{}) | 56 | g.changed = make(chan struct{}) | ||
| 70 | g.cond.Broadcast() | 57 | g.cond.Broadcast() | ||
| 71 | } | 58 | } | ||
| 72 | 59 | ||||
| 73 | - | func (g *attachmentEffectGate) immutableOrder() uint64 { | 60 | + | func (g *attachmentLifecycle) immutableOrder() uint64 { |
| 74 | if order := g.order.Load(); order != 0 { | 61 | if order := g.order.Load(); order != 0 { | ||
| 75 | return order | 62 | return order | ||
| 76 | } | 63 | } | ||
| @@ -81,7 +68,7 @@ func (g *attachmentEffectGate) immutableOrder() uint64 { | |||||
| 81 | return g.order.Load() | 68 | return g.order.Load() | ||
| 82 | } | 69 | } | ||
| 83 | 70 | ||||
| 84 | - | func (g *attachmentEffectGate) inFlightCount() uint64 { | 71 | + | func (g *attachmentLifecycle) inFlightCount() uint64 { |
| 85 | if g == nil { | 72 | if g == nil { | ||
| 86 | return 0 | 73 | return 0 | ||
| 87 | } | 74 | } | ||
| @@ -90,17 +77,18 @@ func (g *attachmentEffectGate) inFlightCount() uint64 { | |||||
| 90 | return g.inFlight | 77 | return g.inFlight | ||
| 91 | } | 78 | } | ||
| 92 | 79 | ||||
| 93 | - | // attachmentEffectTicket reserves the exact published capability until End. End is | 80 | + | // attachmentEffect is an explicit admission of one immutable capability. It |
| 94 | - | // idempotent so error paths can safely defer it before doing any observable I/O. | 81 | + | // remains valid until End because transitions freeze admission and drain all |
| 95 | - | type attachmentEffectTicket struct { | 82 | + | // existing effects before changing lifecycle identity. End is idempotent. |
| 96 | - | gate *attachmentEffectGate | 83 | + | type attachmentEffect struct { |
| 97 | - | token attachmentConnectionToken | 84 | + | attachmentCapability |
| 85 | + | lifecycle *attachmentLifecycle | |||
| 98 | actionDaemon *Daemon | 86 | actionDaemon *Daemon | ||
| 99 | action string | 87 | action string | ||
| 100 | ended atomic.Bool | 88 | ended atomic.Bool | ||
| 101 | } | 89 | } | ||
| 102 | 90 | ||||
| 103 | - | func (t *attachmentEffectTicket) bindActionEnd(d *Daemon, action string) { | 91 | + | func (t *attachmentEffect) bindActionEnd(d *Daemon, action string) { |
| 104 | if t == nil || t.ended.Load() { | 92 | if t == nil || t.ended.Load() { | ||
| 105 | return | 93 | return | ||
| 106 | } | 94 | } | ||
| @@ -108,25 +96,31 @@ func (t *attachmentEffectTicket) bindActionEnd(d *Daemon, action string) { | |||||
| 108 | t.action = action | 96 | t.action = action | ||
| 109 | } | 97 | } | ||
| 110 | 98 | ||||
| 111 | - | func (t *attachmentEffectTicket) connectionToken() attachmentConnectionToken { | 99 | + | func (t *attachmentEffect) capability() attachmentCapability { |
| 112 | if t == nil { | 100 | if t == nil { | ||
| 113 | - | return attachmentConnectionToken{} | 101 | + | return attachmentCapability{} |
| 114 | } | 102 | } | ||
| 115 | - | token := t.token | 103 | + | return t.attachmentCapability |
| 116 | - | token.effect = t | 104 | + | } |
| 117 | - | return token | 105 | + | |
| 106 | + | func (t *attachmentEffect) current() bool { | |||
| 107 | + | return t != nil && !t.ended.Load() | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | func (t *attachmentEffect) currentSessionLocked() bool { | |||
| 111 | + | return t.current() | |||
| 118 | } | 112 | } | ||
| 119 | 113 | ||||
| 120 | - | func (t *attachmentEffectTicket) End() { | 114 | + | func (t *attachmentEffect) End() { |
| 121 | - | if t == nil || t.gate == nil || !t.ended.CompareAndSwap(false, true) { | 115 | + | if t == nil || t.lifecycle == nil || !t.ended.CompareAndSwap(false, true) { |
| 122 | return | 116 | return | ||
| 123 | } | 117 | } | ||
| 124 | - | g := t.gate | 118 | + | g := t.lifecycle |
| 125 | g.mu.Lock() | 119 | g.mu.Lock() | ||
| 126 | g.initLocked() | 120 | g.initLocked() | ||
| 127 | if g.inFlight == 0 { | 121 | if g.inFlight == 0 { | ||
| 128 | g.mu.Unlock() | 122 | g.mu.Unlock() | ||
| 129 | - | panic("daemon: attachment effect ticket ended without admission") | 123 | + | panic("daemon: attachment effect ended without admission") |
| 130 | } | 124 | } | ||
| 131 | delete(g.transportEffects, t) | 125 | delete(g.transportEffects, t) | ||
| 132 | g.inFlight-- | 126 | g.inFlight-- | ||
| @@ -137,7 +131,7 @@ func (t *attachmentEffectTicket) End() { | |||||
| 137 | } | 131 | } | ||
| 138 | } | 132 | } | ||
| 139 | 133 | ||||
| 140 | - | func (d *Daemon) endActionAttachmentEffect(effect *attachmentEffectTicket, _ string) { | 134 | + | func (d *Daemon) endActionAttachmentEffect(effect *attachmentEffect, _ string) { |
| 141 | if effect != nil { | 135 | if effect != nil { | ||
| 142 | effect.End() | 136 | effect.End() | ||
| 143 | } | 137 | } | ||
| @@ -146,11 +140,11 @@ func (d *Daemon) endActionAttachmentEffect(effect *attachmentEffectTicket, _ str | |||||
| 146 | // beginTransportSend marks only the interval spent in an external transport | 140 | // beginTransportSend marks only the interval spent in an external transport | ||
| 147 | // send as interruptible. A replacement may close this exact published link to | 141 | // send as interruptible. A replacement may close this exact published link to | ||
| 148 | // release the send without retiring an idle healthy attachment. | 142 | // release the send without retiring an idle healthy attachment. | ||
| 149 | - | func (t *attachmentEffectTicket) beginTransportSend(expected transportSnapshot) bool { | 143 | + | func (t *attachmentEffect) beginTransportSend(expected transportSnapshot) bool { |
| 150 | - | if t == nil || t.gate == nil || t.ended.Load() || expected.transport == nil { | 144 | + | if t == nil || t.lifecycle == nil || t.ended.Load() || expected.transport == nil { |
| 151 | return false | 145 | return false | ||
| 152 | } | 146 | } | ||
| 153 | - | g := t.gate | 147 | + | g := t.lifecycle |
| 154 | g.mu.Lock() | 148 | g.mu.Lock() | ||
| 155 | g.initLocked() | 149 | g.initLocked() | ||
| 156 | if t.ended.Load() || g.capability.transport.transport != expected.transport || | 150 | if t.ended.Load() || g.capability.transport.transport != expected.transport || | ||
| @@ -159,7 +153,7 @@ func (t *attachmentEffectTicket) beginTransportSend(expected transportSnapshot) | |||||
| 159 | return false | 153 | return false | ||
| 160 | } | 154 | } | ||
| 161 | if g.transportEffects == nil { | 155 | if g.transportEffects == nil { | ||
| 162 | - | g.transportEffects = make(map[*attachmentEffectTicket]transportSnapshot) | 156 | + | g.transportEffects = make(map[*attachmentEffect]transportSnapshot) |
| 163 | } | 157 | } | ||
| 164 | g.transportEffects[t] = expected | 158 | g.transportEffects[t] = expected | ||
| 165 | g.signalLocked() | 159 | g.signalLocked() | ||
| @@ -167,11 +161,11 @@ func (t *attachmentEffectTicket) beginTransportSend(expected transportSnapshot) | |||||
| 167 | return true | 161 | return true | ||
| 168 | } | 162 | } | ||
| 169 | 163 | ||||
| 170 | - | func (t *attachmentEffectTicket) reportTransportFailure(expected transportSnapshot) { | 164 | + | func (t *attachmentEffect) reportTransportFailure(expected transportSnapshot) { |
| 171 | - | if t == nil || t.gate == nil || expected.transport == nil { | 165 | + | if t == nil || t.lifecycle == nil || expected.transport == nil { |
| 172 | return | 166 | return | ||
| 173 | } | 167 | } | ||
| 174 | - | g := t.gate | 168 | + | g := t.lifecycle |
| 175 | g.mu.Lock() | 169 | g.mu.Lock() | ||
| 176 | if !t.ended.Load() && g.capability.transport.transport == expected.transport && | 170 | if !t.ended.Load() && g.capability.transport.transport == expected.transport && | ||
| 177 | g.capability.transport.incarnation == expected.incarnation { | 171 | g.capability.transport.incarnation == expected.incarnation { | ||
| @@ -181,11 +175,11 @@ func (t *attachmentEffectTicket) reportTransportFailure(expected transportSnapsh | |||||
| 181 | g.mu.Unlock() | 175 | g.mu.Unlock() | ||
| 182 | } | 176 | } | ||
| 183 | 177 | ||||
| 184 | - | func (t *attachmentEffectTicket) endTransportSend() { | 178 | + | func (t *attachmentEffect) endTransportSend() { |
| 185 | - | if t == nil || t.gate == nil { | 179 | + | if t == nil || t.lifecycle == nil { |
| 186 | return | 180 | return | ||
| 187 | } | 181 | } | ||
| 188 | - | g := t.gate | 182 | + | g := t.lifecycle |
| 189 | g.mu.Lock() | 183 | g.mu.Lock() | ||
| 190 | if _, exists := g.transportEffects[t]; exists { | 184 | if _, exists := g.transportEffects[t]; exists { | ||
| 191 | delete(g.transportEffects, t) | 185 | delete(g.transportEffects, t) | ||
| @@ -194,45 +188,57 @@ func (t *attachmentEffectTicket) endTransportSend() { | |||||
| 194 | g.mu.Unlock() | 188 | g.mu.Unlock() | ||
| 195 | } | 189 | } | ||
| 196 | 190 | ||||
| 191 | + | func (t *attachmentEffect) sendControl(frame ports.Frame) error { | |||
| 192 | + | if !t.current() || t.ac == nil || t.transport.transport == nil { | |||
| 193 | + | return errAttachmentTransition | |||
| 194 | + | } | |||
| 195 | + | t.ac.sendMu.Lock() | |||
| 196 | + | defer t.ac.sendMu.Unlock() | |||
| 197 | + | if !t.current() || !t.ac.transportSnapshotCurrent(t.transport) || !t.beginTransportSend(t.transport) { | |||
| 198 | + | return errAttachmentTransition | |||
| 199 | + | } | |||
| 200 | + | err := t.transport.transport.Send(frame) | |||
| 201 | + | if err != nil { | |||
| 202 | + | t.reportTransportFailure(t.transport) | |||
| 203 | + | } | |||
| 204 | + | t.endTransportSend() | |||
| 205 | + | return err | |||
| 206 | + | } | |||
| 207 | + | ||||
| 197 | // beginAttachmentEffect is the sole attachment-bound effect admission point. The gate is | 208 | // beginAttachmentEffect is the sole attachment-bound effect admission point. The gate is | ||
| 198 | // held only long enough to reserve the capability; the effect itself must run | 209 | // held only long enough to reserve the capability; the effect itself must run | ||
| 199 | // without the gate mutex. | 210 | // without the gate mutex. | ||
| 200 | - | func (ac *attachedClient) beginAttachmentEffect(token attachmentConnectionToken) (*attachmentEffectTicket, bool) { | 211 | + | func (ac *attachedClient) beginAttachmentEffect(token attachmentCapability) (*attachmentEffect, bool) { |
| 201 | - | if ac == nil || token.ac != ac { | 212 | + | if ac == nil || token.ac != ac || !token.current() { |
| 202 | return nil, false | 213 | return nil, false | ||
| 203 | } | 214 | } | ||
| 204 | - | g := &ac.attachmentEffects | 215 | + | g := &ac.lifecycle |
| 205 | g.mu.Lock() | 216 | g.mu.Lock() | ||
| 206 | g.initLocked() | 217 | g.initLocked() | ||
| 207 | - | if g.phase != attachmentEffectsStable || !g.capability.matches(token) { | 218 | + | if g.phase != attachmentEffectsStable || !g.capability.sameIdentity(token) { |
| 208 | g.mu.Unlock() | 219 | g.mu.Unlock() | ||
| 209 | return nil, false | 220 | return nil, false | ||
| 210 | } | 221 | } | ||
| 211 | g.inFlight++ | 222 | g.inFlight++ | ||
| 212 | g.mu.Unlock() | 223 | g.mu.Unlock() | ||
| 213 | - | captured := token | 224 | + | return &attachmentEffect{attachmentCapability: token, lifecycle: g}, true |
| 214 | - | captured.effect = nil | |||
| 215 | - | return &attachmentEffectTicket{gate: g, token: captured}, true | |||
| 216 | } | 225 | } | ||
| 217 | 226 | ||||
| 218 | // beginCurrentAttachmentEffect waits out a transition that already froze ac, then | 227 | // beginCurrentAttachmentEffect waits out a transition that already froze ac, then | ||
| 219 | // admits the capability derived from the session registry. Handshakes use it | 228 | // admits the capability derived from the session registry. Handshakes use it | ||
| 220 | // after Welcome has completed so a replacement blocked behind that send can | 229 | // after Welcome has completed so a replacement blocked behind that send can | ||
| 221 | // publish before readiness or first paint. | 230 | // publish before readiness or first paint. | ||
| 222 | - | func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.Transport) (attachmentConnectionToken, *attachmentEffectTicket, bool) { | 231 | + | func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.Transport) (attachmentCapability, *attachmentEffect, bool) { |
| 223 | //nolint:contextcheck // This compatibility wrapper deliberately delegates with a fresh context; cancellable callers use the context variant below. | 232 | //nolint:contextcheck // This compatibility wrapper deliberately delegates with a fresh context; cancellable callers use the context variant below. | ||
| 224 | return ac.beginCurrentAttachmentEffectContext(context.Background(), sess, tr) | 233 | return ac.beginCurrentAttachmentEffectContext(context.Background(), sess, tr) | ||
| 225 | } | 234 | } | ||
| 226 | 235 | ||||
| 227 | - | func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Context, sess *session, tr ports.Transport) (attachmentConnectionToken, *attachmentEffectTicket, bool) { | 236 | + | func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Context, sess *session, tr ports.Transport) (attachmentCapability, *attachmentEffect, bool) { |
| 228 | - | if ctx == nil { | 237 | + | if ctx == nil || ac == nil || sess == nil || tr == nil { |
| 229 | - | ctx = context.Background() | 238 | + | return attachmentCapability{}, nil, false |
| 230 | - | } | |||
| 231 | - | if ac == nil || sess == nil || tr == nil { | |||
| 232 | - | return attachmentConnectionToken{}, nil, false | |||
| 233 | } | 239 | } | ||
| 234 | for { | 240 | for { | ||
| 235 | - | token := sess.attachmentToken(ac, tr) | 241 | + | token := sess.captureAttachmentCapability(ac, tr) |
| 236 | if token.ac == nil { | 242 | if token.ac == nil { | ||
| 237 | return token, nil, false | 243 | return token, nil, false | ||
| 238 | } | 244 | } | ||
| @@ -240,7 +246,7 @@ func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Contex | |||||
| 240 | return token, ticket, true | 246 | return token, ticket, true | ||
| 241 | } | 247 | } | ||
| 242 | 248 | ||||
| 243 | - | g := &ac.attachmentEffects | 249 | + | g := &ac.lifecycle |
| 244 | g.mu.Lock() | 250 | g.mu.Lock() | ||
| 245 | g.initLocked() | 251 | g.initLocked() | ||
| 246 | if g.phase == attachmentEffectsFrozen { | 252 | if g.phase == attachmentEffectsFrozen { | ||
| @@ -254,8 +260,8 @@ func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Contex | |||||
| 254 | } | 260 | } | ||
| 255 | } | 261 | } | ||
| 256 | g.mu.Unlock() | 262 | g.mu.Unlock() | ||
| 257 | - | // A stable mismatch is only recoverable when a publication raced token | 263 | + | // A stable mismatch is only recoverable when a publication raced capability |
| 258 | - | // capture; an already-stale token proves that happened. A current token | 264 | + | // capture; an already-stale capability proves that happened. A current capability |
| 259 | // with a stable mismatch violates the publication invariant, so fail | 265 | // with a stable mismatch violates the publication invariant, so fail | ||
| 260 | // closed instead of spinning forever. | 266 | // closed instead of spinning forever. | ||
| 261 | if token.current() { | 267 | if token.current() { | ||
| @@ -266,58 +272,59 @@ func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Contex | |||||
| 266 | } | 272 | } | ||
| 267 | } | 273 | } | ||
| 268 | 274 | ||||
| 269 | - | // publishAttachmentCapability is used by direct/headless setup and by transition | 275 | + | // installInitialCapability publishes only an empty lifecycle owner. It supports |
| 270 | - | // publication while the gate is frozen. Production attachment changes publish only | 276 | + | // direct/headless construction without creating a second mutable publication. |
| 271 | - | // through publishFrozenAttachmentCapability. | 277 | + | func (g *attachmentLifecycle) installInitialCapability(capability attachmentCapability) { |
| 272 | - | func (ac *attachedClient) publishAttachmentCapability(token attachmentConnectionToken) { | |||
| 273 | - | if ac == nil { | |||
| 274 | - | return | |||
| 275 | - | } | |||
| 276 | - | g := &ac.attachmentEffects | |||
| 277 | g.mu.Lock() | 278 | g.mu.Lock() | ||
| 278 | g.initLocked() | 279 | g.initLocked() | ||
| 279 | - | g.capability = capabilityFromToken(token) | 280 | + | if g.phase == attachmentEffectsStable && g.capability.sess == nil { |
| 280 | - | g.failedTransport = transportSnapshot{} | 281 | + | g.capability = capability |
| 282 | + | g.failedTransport = transportSnapshot{} | |||
| 283 | + | } | |||
| 281 | g.mu.Unlock() | 284 | g.mu.Unlock() | ||
| 282 | } | 285 | } | ||
| 283 | 286 | ||||
| 284 | - | // bootstrapAttachmentCapability supports direct/headless session construction. It | 287 | + | func (ac *attachedClient) publishFrozenAttachmentCapability(capability attachmentCapability) attachmentCapability { |
| 285 | - | // never changes an existing or frozen production publication. | 288 | + | g := &ac.lifecycle |
| 286 | - | func (ac *attachedClient) bootstrapAttachmentCapability(token attachmentConnectionToken) { | |||
| 287 | - | if ac == nil || token.sess == nil || token.ac != ac { | |||
| 288 | - | return | |||
| 289 | - | } | |||
| 290 | - | g := &ac.attachmentEffects | |||
| 291 | g.mu.Lock() | 289 | g.mu.Lock() | ||
| 292 | g.initLocked() | 290 | g.initLocked() | ||
| 293 | - | if g.phase == attachmentEffectsStable && g.capability.sess == nil { | 291 | + | if g.phase != attachmentEffectsFrozen { |
| 294 | - | g.capability = capabilityFromToken(token) | 292 | + | g.mu.Unlock() |
| 295 | - | g.failedTransport = transportSnapshot{} | 293 | + | panic("daemon: publishing attachment capability without transition freeze") |
| 296 | } | 294 | } | ||
| 295 | + | capability.generation = g.generation.Add(1) | |||
| 296 | + | g.capability = capability | |||
| 297 | + | g.failedTransport = transportSnapshot{} | |||
| 297 | g.mu.Unlock() | 298 | g.mu.Unlock() | ||
| 299 | + | return capability | |||
| 298 | } | 300 | } | ||
| 299 | 301 | ||||
| 300 | - | func (ac *attachedClient) publishFrozenAttachmentCapability(token attachmentConnectionToken) { | 302 | + | // invalidateRetiredAttachmentCapability retires parked identity either while |
| 301 | - | g := &ac.attachmentEffects | 303 | + | // stable with no admitted effects or under a terminal teardown's existing |
| 304 | + | // freeze. Live membership transitions use invalidateFrozenAttachmentCapability. | |||
| 305 | + | func (ac *attachedClient) invalidateRetiredAttachmentCapability() { | |||
| 306 | + | g := &ac.lifecycle | |||
| 302 | g.mu.Lock() | 307 | g.mu.Lock() | ||
| 303 | g.initLocked() | 308 | g.initLocked() | ||
| 304 | - | if g.phase != attachmentEffectsFrozen { | 309 | + | if g.phase != attachmentEffectsFrozen && (g.phase != attachmentEffectsStable || g.inFlight != 0) { |
| 305 | g.mu.Unlock() | 310 | g.mu.Unlock() | ||
| 306 | - | panic("daemon: publishing attachment capability without transition freeze") | 311 | + | panic("daemon: invalidating an active attachment capability") |
| 307 | } | 312 | } | ||
| 308 | - | g.capability = capabilityFromToken(token) | 313 | + | g.generation.Add(1) |
| 314 | + | g.capability = attachmentCapability{} | |||
| 309 | g.failedTransport = transportSnapshot{} | 315 | g.failedTransport = transportSnapshot{} | ||
| 310 | g.mu.Unlock() | 316 | g.mu.Unlock() | ||
| 311 | } | 317 | } | ||
| 312 | 318 | ||||
| 313 | func (ac *attachedClient) invalidateFrozenAttachmentCapability() { | 319 | func (ac *attachedClient) invalidateFrozenAttachmentCapability() { | ||
| 314 | - | g := &ac.attachmentEffects | 320 | + | g := &ac.lifecycle |
| 315 | g.mu.Lock() | 321 | g.mu.Lock() | ||
| 316 | g.initLocked() | 322 | g.initLocked() | ||
| 317 | if g.phase != attachmentEffectsFrozen { | 323 | if g.phase != attachmentEffectsFrozen { | ||
| 318 | g.mu.Unlock() | 324 | g.mu.Unlock() | ||
| 319 | panic("daemon: invalidating attachment capability without transition freeze") | 325 | panic("daemon: invalidating attachment capability without transition freeze") | ||
| 320 | } | 326 | } | ||
| 327 | + | g.generation.Add(1) | |||
| 321 | g.capability = attachmentCapability{} | 328 | g.capability = attachmentCapability{} | ||
| 322 | g.failedTransport = transportSnapshot{} | 329 | g.failedTransport = transportSnapshot{} | ||
| 323 | g.mu.Unlock() | 330 | g.mu.Unlock() | ||
M internal/usecase/daemon/attachment_effect_gate_test.go +73 -61
| @@ -118,7 +118,7 @@ func (t *teardownBlockingTransport) Closed() bool { | |||||
| 118 | } | 118 | } | ||
| 119 | } | 119 | } | ||
| 120 | 120 | ||||
| 121 | - | func startBlockedAttachmentSend(t *testing.T, token attachmentConnectionToken) <-chan struct{} { | 121 | + | func startBlockedAttachmentSend(t *testing.T, token attachmentCapability) <-chan struct{} { |
| 122 | t.Helper() | 122 | t.Helper() | ||
| 123 | ticket, admitted := token.ac.beginAttachmentEffect(token) | 123 | ticket, admitted := token.ac.beginAttachmentEffect(token) | ||
| 124 | require.True(t, admitted) | 124 | require.True(t, admitted) | ||
| @@ -133,19 +133,19 @@ func startBlockedAttachmentSend(t *testing.T, token attachmentConnectionToken) < | |||||
| 133 | 133 | ||||
| 134 | func requireAttachmentEffectGateRetired(t *testing.T, ac *attachedClient) { | 134 | func requireAttachmentEffectGateRetired(t *testing.T, ac *attachedClient) { | ||
| 135 | t.Helper() | 135 | t.Helper() | ||
| 136 | - | ac.attachmentEffects.mu.Lock() | 136 | + | ac.lifecycle.mu.Lock() |
| 137 | - | defer ac.attachmentEffects.mu.Unlock() | 137 | + | defer ac.lifecycle.mu.Unlock() |
| 138 | - | require.Equal(t, attachmentEffectsStable, ac.attachmentEffects.phase) | 138 | + | require.Equal(t, attachmentEffectsStable, ac.lifecycle.phase) |
| 139 | - | require.Zero(t, ac.attachmentEffects.inFlight) | 139 | + | require.Zero(t, ac.lifecycle.inFlight) |
| 140 | - | require.Empty(t, ac.attachmentEffects.transportEffects) | 140 | + | require.Empty(t, ac.lifecycle.transportEffects) |
| 141 | } | 141 | } | ||
| 142 | 142 | ||||
| 143 | func TestBeginCurrentAttachmentEffectFailsClosedOnStableCapabilityMismatch(t *testing.T) { | 143 | func TestBeginCurrentAttachmentEffectFailsClosedOnStableCapabilityMismatch(t *testing.T) { | ||
| 144 | _, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | 144 | _, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 145 | - | current := sess.attachmentToken(ac, ac.transport()) | 145 | + | current := sess.captureAttachmentCapability(ac, ac.transport()) |
| 146 | stale := current | 146 | stale := current | ||
| 147 | stale.generation++ | 147 | stale.generation++ | ||
| 148 | - | ac.publishAttachmentCapability(stale) | 148 | + | ac.installTestAttachmentCapability(stale) |
| 149 | 149 | ||||
| 150 | result := make(chan bool, 1) | 150 | result := make(chan bool, 1) | ||
| 151 | go func() { | 151 | go func() { | ||
| @@ -163,6 +163,16 @@ func TestBeginCurrentAttachmentEffectFailsClosedOnStableCapabilityMismatch(t *te | |||||
| 163 | } | 163 | } | ||
| 164 | } | 164 | } | ||
| 165 | 165 | ||||
| 166 | + | func TestBeginCurrentAttachmentEffectContextRejectsNilContext(t *testing.T) { | |||
| 167 | + | _, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |||
| 168 | + | ||||
| 169 | + | token, effect, admitted := ac.beginCurrentAttachmentEffectContext(nil, sess, ac.transport()) | |||
| 170 | + | ||||
| 171 | + | require.False(t, admitted) | |||
| 172 | + | require.Nil(t, effect) | |||
| 173 | + | require.Equal(t, attachmentCapability{}, token) | |||
| 174 | + | } | |||
| 175 | + | ||||
| 166 | func TestBeginAttachmentLeaseEffectRejectsForeignLease(t *testing.T) { | 176 | func TestBeginAttachmentLeaseEffectRejectsForeignLease(t *testing.T) { | ||
| 167 | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | 177 | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 168 | rc := d.attachCoordinator(sess, nil, ac, true) | 178 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| @@ -173,9 +183,9 @@ func TestBeginAttachmentLeaseEffectRejectsForeignLease(t *testing.T) { | |||||
| 173 | } | 183 | } | ||
| 174 | 184 | ||||
| 175 | func attachmentEffectGatePublicationSnapshot(ac *attachedClient) (attachmentEffectPhase, attachmentCapability) { | 185 | func attachmentEffectGatePublicationSnapshot(ac *attachedClient) (attachmentEffectPhase, attachmentCapability) { | ||
| 176 | - | ac.attachmentEffects.mu.Lock() | 186 | + | ac.lifecycle.mu.Lock() |
| 177 | - | defer ac.attachmentEffects.mu.Unlock() | 187 | + | defer ac.lifecycle.mu.Unlock() |
| 178 | - | return ac.attachmentEffects.phase, ac.attachmentEffects.capability | 188 | + | return ac.lifecycle.phase, ac.lifecycle.capability |
| 179 | } | 189 | } | ||
| 180 | 190 | ||||
| 181 | func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testing.T) { | 191 | func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testing.T) { | ||
| @@ -197,21 +207,21 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi | |||||
| 197 | sess.mu.Unlock() | 207 | sess.mu.Unlock() | ||
| 198 | 208 | ||||
| 199 | rc := d.attachCoordinator(sess, nil, first, true) | 209 | rc := d.attachCoordinator(sess, nil, first, true) | ||
| 200 | - | firstToken := sess.attachmentToken(first, firstTransport) | 210 | + | firstToken := sess.captureAttachmentCapability(first, firstTransport) |
| 201 | firstToken.lease = rc.attachmentLease(first) | 211 | firstToken.lease = rc.attachmentLease(first) | ||
| 202 | - | first.publishAttachmentCapability(firstToken) | 212 | + | first.installTestAttachmentCapability(firstToken) |
| 203 | - | blockedToken := sess.attachmentToken(blocked, blockedTransport) | 213 | + | blockedToken := sess.captureAttachmentCapability(blocked, blockedTransport) |
| 204 | - | blocked.publishAttachmentCapability(blockedToken) | 214 | + | blocked.installTestAttachmentCapability(blockedToken) |
| 205 | - | laterToken := sess.attachmentToken(later, laterTransport) | 215 | + | laterToken := sess.captureAttachmentCapability(later, laterTransport) |
| 206 | - | later.publishAttachmentCapability(laterToken) | 216 | + | later.installTestAttachmentCapability(laterToken) |
| 207 | 217 | ||||
| 208 | // Fix the canonical order so teardown acquires first, waits behind the gate | 218 | // Fix the canonical order so teardown acquires first, waits behind the gate | ||
| 209 | // owned by this test, and never reaches the still-stable later participant. | 219 | // owned by this test, and never reaches the still-stable later participant. | ||
| 210 | - | first.attachmentEffects.immutableOrder() | 220 | + | first.lifecycle.immutableOrder() |
| 211 | - | blocked.attachmentEffects.immutableOrder() | 221 | + | blocked.lifecycle.immutableOrder() |
| 212 | - | later.attachmentEffects.immutableOrder() | 222 | + | later.lifecycle.immutableOrder() |
| 213 | - | require.Less(t, first.attachmentEffects.order.Load(), blocked.attachmentEffects.order.Load()) | 223 | + | require.Less(t, first.lifecycle.order.Load(), blocked.lifecycle.order.Load()) |
| 214 | - | require.Less(t, blocked.attachmentEffects.order.Load(), later.attachmentEffects.order.Load()) | 224 | + | require.Less(t, blocked.lifecycle.order.Load(), later.lifecycle.order.Load()) |
| 215 | 225 | ||||
| 216 | blockedOwner := freezeAttachmentEffectGates(blocked) | 226 | blockedOwner := freezeAttachmentEffectGates(blocked) | ||
| 217 | blockedOwnerHeld := true | 227 | blockedOwnerHeld := true | ||
| @@ -227,9 +237,9 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi | |||||
| 227 | require.Equal(t, attachmentEffectsStable, firstPhase) | 237 | require.Equal(t, attachmentEffectsStable, firstPhase) | ||
| 228 | require.Equal(t, attachmentEffectsFrozen, blockedPhase) | 238 | require.Equal(t, attachmentEffectsFrozen, blockedPhase) | ||
| 229 | require.Equal(t, attachmentEffectsStable, laterPhase) | 239 | require.Equal(t, attachmentEffectsStable, laterPhase) | ||
| 230 | - | firstGeneration := first.connectionGeneration.Load() | 240 | + | firstGeneration := first.lifecycle.generationValue() |
| 231 | - | blockedGeneration := blocked.connectionGeneration.Load() | 241 | + | blockedGeneration := blocked.lifecycle.generationValue() |
| 232 | - | laterGeneration := later.connectionGeneration.Load() | 242 | + | laterGeneration := later.lifecycle.generationValue() |
| 233 | firstSnapshot := first.transportSnapshot() | 243 | firstSnapshot := first.transportSnapshot() | ||
| 234 | blockedSnapshot := blocked.transportSnapshot() | 244 | blockedSnapshot := blocked.transportSnapshot() | ||
| 235 | laterSnapshot := later.transportSnapshot() | 245 | laterSnapshot := later.transportSnapshot() | ||
| @@ -264,9 +274,9 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi | |||||
| 264 | for _, ac := range []*attachedClient{first, blocked, later} { | 274 | for _, ac := range []*attachedClient{first, blocked, later} { | ||
| 265 | require.Same(t, sess, ac.currentSession(), "partial gate acquisition cleared attachment ownership") | 275 | require.Same(t, sess, ac.currentSession(), "partial gate acquisition cleared attachment ownership") | ||
| 266 | } | 276 | } | ||
| 267 | - | require.Equal(t, firstGeneration, first.connectionGeneration.Load()) | 277 | + | require.Equal(t, firstGeneration, first.lifecycle.generationValue()) |
| 268 | - | require.Equal(t, blockedGeneration, blocked.connectionGeneration.Load()) | 278 | + | require.Equal(t, blockedGeneration, blocked.lifecycle.generationValue()) |
| 269 | - | require.Equal(t, laterGeneration, later.connectionGeneration.Load()) | 279 | + | require.Equal(t, laterGeneration, later.lifecycle.generationValue()) |
| 270 | require.True(t, first.transportSnapshotCurrent(firstSnapshot)) | 280 | require.True(t, first.transportSnapshotCurrent(firstSnapshot)) | ||
| 271 | require.True(t, blocked.transportSnapshotCurrent(blockedSnapshot)) | 281 | require.True(t, blocked.transportSnapshotCurrent(blockedSnapshot)) | ||
| 272 | require.True(t, later.transportSnapshotCurrent(laterSnapshot)) | 282 | require.True(t, later.transportSnapshotCurrent(laterSnapshot)) | ||
| @@ -290,7 +300,7 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi | |||||
| 290 | require.True(t, complete.acquired, "rolled-back gate set could not be acquired again") | 300 | require.True(t, complete.acquired, "rolled-back gate set could not be acquired again") | ||
| 291 | require.True(t, complete.drained) | 301 | require.True(t, complete.drained) | ||
| 292 | complete.unfreeze() | 302 | complete.unfreeze() | ||
| 293 | - | for _, token := range []attachmentConnectionToken{firstToken, blockedToken, laterToken} { | 303 | + | for _, token := range []attachmentCapability{firstToken, blockedToken, laterToken} { |
| 294 | ticket, admitted := token.ac.beginAttachmentEffect(token) | 304 | ticket, admitted := token.ac.beginAttachmentEffect(token) | ||
| 295 | require.True(t, admitted, "pre-publication abort changed an attachment capability") | 305 | require.True(t, admitted, "pre-publication abort changed an attachment capability") | ||
| 296 | ticket.End() | 306 | ticket.End() | ||
| @@ -303,7 +313,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) { | |||||
| 303 | snatchedTransport := newTeardownBlockingTransport() | 313 | snatchedTransport := newTeardownBlockingTransport() | ||
| 304 | snatched.replaceTransport(snatchedTransport) | 314 | snatched.replaceTransport(snatchedTransport) | ||
| 305 | d.attachCoordinator(sess, nil, snatched, true) | 315 | d.attachCoordinator(sess, nil, snatched, true) | ||
| 306 | - | snatched.publishAttachmentCapability(sess.attachmentToken(snatched, snatchedTransport)) | 316 | + | snatched.installTestAttachmentCapability(sess.captureAttachmentCapability(snatched, snatchedTransport)) |
| 307 | 317 | ||||
| 308 | activeTransport := newTeardownBlockingTransport() | 318 | activeTransport := newTeardownBlockingTransport() | ||
| 309 | active := &attachedClient{tr: activeTransport, output: newOutputStateStream(), size: snatched.size} | 319 | active := &attachedClient{tr: activeTransport, output: newOutputStateStream(), size: snatched.size} | ||
| @@ -324,8 +334,8 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) { | |||||
| 324 | d.mu.Unlock() | 334 | d.mu.Unlock() | ||
| 325 | d.attachCoordinator(unrelated, nil, unrelatedClient, true) | 335 | d.attachCoordinator(unrelated, nil, unrelatedClient, true) | ||
| 326 | 336 | ||||
| 327 | - | snatchedToken := sess.attachmentToken(snatched, snatchedTransport) | 337 | + | snatchedToken := sess.captureAttachmentCapability(snatched, snatchedTransport) |
| 328 | - | activeToken := sess.attachmentToken(active, activeTransport) | 338 | + | activeToken := sess.captureAttachmentCapability(active, activeTransport) |
| 329 | snatchedDone := startBlockedAttachmentSend(t, snatchedToken) | 339 | snatchedDone := startBlockedAttachmentSend(t, snatchedToken) | ||
| 330 | activeDone := startBlockedAttachmentSend(t, activeToken) | 340 | activeDone := startBlockedAttachmentSend(t, activeToken) | ||
| 331 | awaitTestCompletion(t, snatchedTransport.sendEntered, "snatched send did not block") | 341 | awaitTestCompletion(t, snatchedTransport.sendEntered, "snatched send did not block") | ||
| @@ -359,9 +369,9 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test | |||||
| 359 | stale := newTeardownBlockingTransport() | 369 | stale := newTeardownBlockingTransport() | ||
| 360 | ac.replaceTransport(stale) | 370 | ac.replaceTransport(stale) | ||
| 361 | d.attachCoordinator(sess, nil, ac, true) | 371 | d.attachCoordinator(sess, nil, ac, true) | ||
| 362 | - | ac.publishAttachmentCapability(sess.attachmentToken(ac, stale)) | 372 | + | ac.installTestAttachmentCapability(sess.captureAttachmentCapability(ac, stale)) |
| 363 | 373 | ||||
| 364 | - | token := sess.attachmentToken(ac, stale) | 374 | + | token := sess.captureAttachmentCapability(ac, stale) |
| 365 | sendDone := startBlockedAttachmentSend(t, token) | 375 | sendDone := startBlockedAttachmentSend(t, token) | ||
| 366 | awaitTestCompletion(t, stale.sendEntered, "captured incarnation send did not block") | 376 | awaitTestCompletion(t, stale.sendEntered, "captured incarnation send did not block") | ||
| 367 | fresh := &closeTrackingTransport{} | 377 | fresh := &closeTrackingTransport{} | ||
| @@ -388,7 +398,7 @@ func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T | |||||
| 388 | transport := &closeTrackingTransport{} | 398 | transport := &closeTrackingTransport{} | ||
| 389 | active.replaceTransport(transport) | 399 | active.replaceTransport(transport) | ||
| 390 | d.attachCoordinator(sess, nil, active, true) | 400 | d.attachCoordinator(sess, nil, active, true) | ||
| 391 | - | active.publishAttachmentCapability(sess.attachmentToken(active, transport)) | 401 | + | active.installTestAttachmentCapability(sess.captureAttachmentCapability(active, transport)) |
| 392 | 402 | ||||
| 393 | owner := freezeAttachmentEffectGates(active) | 403 | owner := freezeAttachmentEffectGates(active) | ||
| 394 | require.True(t, owner.acquired) | 404 | require.True(t, owner.acquired) | ||
| @@ -419,9 +429,9 @@ func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) { | |||||
| 419 | blocked := newTeardownBlockingTransport() | 429 | blocked := newTeardownBlockingTransport() | ||
| 420 | active.replaceTransport(blocked) | 430 | active.replaceTransport(blocked) | ||
| 421 | d.attachCoordinator(sess, nil, active, true) | 431 | d.attachCoordinator(sess, nil, active, true) | ||
| 422 | - | active.publishAttachmentCapability(sess.attachmentToken(active, blocked)) | 432 | + | active.installTestAttachmentCapability(sess.captureAttachmentCapability(active, blocked)) |
| 423 | 433 | ||||
| 424 | - | token := sess.attachmentToken(active, blocked) | 434 | + | token := sess.captureAttachmentCapability(active, blocked) |
| 425 | sendDone := startBlockedAttachmentSend(t, token) | 435 | sendDone := startBlockedAttachmentSend(t, token) | ||
| 426 | awaitTestCompletion(t, blocked.sendEntered, "active send did not block") | 436 | awaitTestCompletion(t, blocked.sendEntered, "active send did not block") | ||
| 427 | 437 | ||||
| @@ -445,9 +455,9 @@ func TestRenderSendFailureCleanupIsDeadlineBoundedWhileAttachmentEffectGateIsBus | |||||
| 445 | transport := &closeTrackingTransport{} | 455 | transport := &closeTrackingTransport{} | ||
| 446 | ac.replaceTransport(transport) | 456 | ac.replaceTransport(transport) | ||
| 447 | rc := d.attachCoordinator(sess, nil, ac, true) | 457 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| 448 | - | token := sess.attachmentToken(ac, transport) | 458 | + | token := sess.captureAttachmentCapability(ac, transport) |
| 449 | token.lease = rc.attachmentLease(ac) | 459 | token.lease = rc.attachmentLease(ac) | ||
| 450 | - | ac.publishAttachmentCapability(token) | 460 | + | ac.installTestAttachmentCapability(token) |
| 451 | 461 | ||||
| 452 | clock := &signalClock{timers: make(chan *signalTimer, 1)} | 462 | clock := &signalClock{timers: make(chan *signalTimer, 1)} | ||
| 453 | d.clock = clock | 463 | d.clock = clock | ||
| @@ -476,9 +486,9 @@ func TestAttachmentEffectGateReplacementInterruptsBlockedOldRenderBeforePublicat | |||||
| 476 | old.replaceTransport(oldTransport) | 486 | old.replaceTransport(oldTransport) | ||
| 477 | rc := d.attachCoordinator(sess, nil, old, true) | 487 | rc := d.attachCoordinator(sess, nil, old, true) | ||
| 478 | lease := rc.attachmentLease(old) | 488 | lease := rc.attachmentLease(old) | ||
| 479 | - | token := sess.attachmentToken(old, oldTransport) | 489 | + | token := sess.captureAttachmentCapability(old, oldTransport) |
| 480 | token.lease = lease | 490 | token.lease = lease | ||
| 481 | - | old.publishAttachmentCapability(token) | 491 | + | old.installTestAttachmentCapability(token) |
| 482 | 492 | ||||
| 483 | paintDone := make(chan struct{}) | 493 | paintDone := make(chan struct{}) | ||
| 484 | go func() { | 494 | go func() { | ||
| @@ -530,9 +540,9 @@ func TestAttachmentEffectGateAdmittedEffectCompletesBeforeConflictingTransition( | |||||
| 530 | oldTransport := newDatagramTestTransport() | 540 | oldTransport := newDatagramTestTransport() | ||
| 531 | old.replaceTransport(oldTransport) | 541 | old.replaceTransport(oldTransport) | ||
| 532 | rc := d.attachCoordinator(sess, nil, old, true) | 542 | rc := d.attachCoordinator(sess, nil, old, true) | ||
| 533 | - | token := sess.attachmentToken(old, oldTransport) | 543 | + | token := sess.captureAttachmentCapability(old, oldTransport) |
| 534 | token.lease = rc.attachmentLease(old) | 544 | token.lease = rc.attachmentLease(old) | ||
| 535 | - | old.publishAttachmentCapability(token) | 545 | + | old.installTestAttachmentCapability(token) |
| 536 | 546 | ||||
| 537 | ticket, ok := old.beginAttachmentEffect(token) | 547 | ticket, ok := old.beginAttachmentEffect(token) | ||
| 538 | require.True(t, ok) | 548 | require.True(t, ok) | ||
| @@ -631,15 +641,16 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes | |||||
| 631 | old.replaceTransport(oldTransport) | 641 | old.replaceTransport(oldTransport) | ||
| 632 | old.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: old}, &d.bindings) | 642 | old.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: old}, &d.bindings) | ||
| 633 | rc := d.attachCoordinator(sess, nil, old, true) | 643 | rc := d.attachCoordinator(sess, nil, old, true) | ||
| 634 | - | token := sess.attachmentToken(old, oldTransport) | 644 | + | token := sess.captureAttachmentCapability(old, oldTransport) |
| 635 | token.lease = rc.attachmentLease(old) | 645 | token.lease = rc.attachmentLease(old) | ||
| 646 | + | old.installTestAttachmentCapability(token) | |||
| 636 | if tt.setup != nil { | 647 | if tt.setup != nil { | ||
| 637 | tt.setup(d, sess, old, pty) | 648 | tt.setup(d, sess, old, pty) | ||
| 638 | } | 649 | } | ||
| 639 | 650 | ||||
| 640 | admitted := make(chan struct{}) | 651 | admitted := make(chan struct{}) | ||
| 641 | release := make(chan struct{}) | 652 | release := make(chan struct{}) | ||
| 642 | - | d.afterAttachmentEffectAdmitted = func(attachmentConnectionToken) { | 653 | + | d.afterAttachmentEffectAdmitted = func(attachmentCapability) { |
| 643 | close(admitted) | 654 | close(admitted) | ||
| 644 | <-release | 655 | <-release | ||
| 645 | } | 656 | } | ||
| @@ -672,9 +683,9 @@ func TestAttachedRouteSnapshotIsAcceptedAsAnAttachmentValue(t *testing.T) { | |||||
| 672 | transport := &closeTrackingTransport{} | 683 | transport := &closeTrackingTransport{} | ||
| 673 | ac.replaceTransport(transport) | 684 | ac.replaceTransport(transport) | ||
| 674 | rc := d.attachCoordinator(source, nil, ac, true) | 685 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 675 | - | token := source.attachmentToken(ac, transport) | 686 | + | token := source.captureAttachmentCapability(ac, transport) |
| 676 | token.lease = rc.attachmentLease(ac) | 687 | token.lease = rc.attachmentLease(ac) | ||
| 677 | - | ac.publishAttachmentCapability(token) | 688 | + | ac.installTestAttachmentCapability(token) |
| 678 | 689 | ||||
| 679 | activeRef := ports.RouteRef{Key: 8, Generation: 4} | 690 | activeRef := ports.RouteRef{Key: 8, Generation: 4} | ||
| 680 | activeTarget := testRouteTarget(source.name, 8) | 691 | activeTarget := testRouteTarget(source.name, 8) | ||
| @@ -711,9 +722,9 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T) | |||||
| 711 | transport := &closeTrackingTransport{} | 722 | transport := &closeTrackingTransport{} | ||
| 712 | ac.replaceTransport(transport) | 723 | ac.replaceTransport(transport) | ||
| 713 | rc := d.attachCoordinator(source, nil, ac, true) | 724 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 714 | - | token := source.attachmentToken(ac, transport) | 725 | + | token := source.captureAttachmentCapability(ac, transport) |
| 715 | token.lease = rc.attachmentLease(ac) | 726 | token.lease = rc.attachmentLease(ac) | ||
| 716 | - | ac.publishAttachmentCapability(token) | 727 | + | ac.installTestAttachmentCapability(token) |
| 717 | payload, err := ports.MarshalCommandRequest(ports.CommandRequest{ | 728 | payload, err := ports.MarshalCommandRequest(ports.CommandRequest{ | ||
| 718 | Version: ports.ProtocolVersion, RequestID: 1, Slug: "back-session", Attached: true, | 729 | Version: ports.ProtocolVersion, RequestID: 1, Slug: "back-session", Attached: true, | ||
| 719 | }) | 730 | }) | ||
| @@ -762,9 +773,9 @@ func TestJumpAttentionAdmittedHandoffCrossesSessions(t *testing.T) { | |||||
| 762 | d.sessions[target.id] = target | 773 | d.sessions[target.id] = target | ||
| 763 | 774 | ||||
| 764 | rc := d.attachCoordinator(source, nil, ac, true) | 775 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 765 | - | token := source.attachmentToken(ac, ac.transport()) | 776 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 766 | token.lease = rc.attachmentLease(ac) | 777 | token.lease = rc.attachmentLease(ac) | ||
| 767 | - | ac.publishAttachmentCapability(token) | 778 | + | ac.installTestAttachmentCapability(token) |
| 768 | 779 | ||||
| 769 | d.handleAttachmentClientFrame(token, frameInput([]byte("\x1ba"))) | 780 | d.handleAttachmentClientFrame(token, frameInput([]byte("\x1ba"))) | ||
| 770 | 781 | ||||
| @@ -780,9 +791,9 @@ func TestPickerDeleteDoesNotDeleteSourceAfterInitiatorReplacement(t *testing.T) | |||||
| 780 | 791 | ||||
| 781 | oldTransport := old.transport() | 792 | oldTransport := old.transport() | ||
| 782 | rc := d.attachCoordinator(sess, nil, old, true) | 793 | rc := d.attachCoordinator(sess, nil, old, true) | ||
| 783 | - | token := sess.attachmentToken(old, oldTransport) | 794 | + | token := sess.captureAttachmentCapability(old, oldTransport) |
| 784 | token.lease = rc.attachmentLease(old) | 795 | token.lease = rc.attachmentLease(old) | ||
| 785 | - | old.publishAttachmentCapability(token) | 796 | + | old.installTestAttachmentCapability(token) |
| 786 | 797 | ||||
| 787 | admissionEnded := make(chan struct{}) | 798 | admissionEnded := make(chan struct{}) | ||
| 788 | releaseAction := make(chan struct{}) | 799 | releaseAction := make(chan struct{}) | ||
| @@ -824,9 +835,9 @@ func TestPickerDeleteSourceForCurrentInitiatorDoesNotDeadlock(t *testing.T) { | |||||
| 824 | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | 835 | d, sess, ac, _ := newManualSessionWithPTYs(t, p) | ||
| 825 | d.enterPicker(sess, ac) | 836 | d.enterPicker(sess, ac) | ||
| 826 | rc := d.attachCoordinator(sess, nil, ac, true) | 837 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| 827 | - | token := sess.attachmentToken(ac, ac.transport()) | 838 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 828 | token.lease = rc.attachmentLease(ac) | 839 | token.lease = rc.attachmentLease(ac) | ||
| 829 | - | ac.publishAttachmentCapability(token) | 840 | + | ac.installTestAttachmentCapability(token) |
| 830 | 841 | ||||
| 831 | d.handleAttachmentClientFrame(token, frameInput([]byte("x"))) | 842 | d.handleAttachmentClientFrame(token, frameInput([]byte("x"))) | ||
| 832 | 843 | ||||
| @@ -841,12 +852,13 @@ func TestAttachmentEffectGateAdmittedFirstPaintFinishesBeforeReplacement(t *test | |||||
| 841 | oldTransport := newDatagramTestTransport() | 852 | oldTransport := newDatagramTestTransport() | ||
| 842 | old.replaceTransport(oldTransport) | 853 | old.replaceTransport(oldTransport) | ||
| 843 | rc := d.attachCoordinator(sess, nil, old, true) | 854 | rc := d.attachCoordinator(sess, nil, old, true) | ||
| 844 | - | token := sess.attachmentToken(old, oldTransport) | 855 | + | token := sess.captureAttachmentCapability(old, oldTransport) |
| 845 | token.lease = rc.attachmentLease(old) | 856 | token.lease = rc.attachmentLease(old) | ||
| 857 | + | old.installTestAttachmentCapability(token) | |||
| 846 | 858 | ||||
| 847 | admitted := make(chan struct{}) | 859 | admitted := make(chan struct{}) | ||
| 848 | release := make(chan struct{}) | 860 | release := make(chan struct{}) | ||
| 849 | - | d.afterAttachmentEffectAdmitted = func(attachmentConnectionToken) { | 861 | + | d.afterAttachmentEffectAdmitted = func(attachmentCapability) { |
| 850 | close(admitted) | 862 | close(admitted) | ||
| 851 | <-release | 863 | <-release | ||
| 852 | } | 864 | } | ||
| @@ -875,7 +887,7 @@ func TestAttachmentEffectGateReversedConcurrentTransitionsDoNotDeadlock(t *testi | |||||
| 875 | aTransport := newDatagramTestTransport() | 887 | aTransport := newDatagramTestTransport() | ||
| 876 | a.replaceTransport(aTransport) | 888 | a.replaceTransport(aTransport) | ||
| 877 | d.attachCoordinator(first, nil, a, true) | 889 | d.attachCoordinator(first, nil, a, true) | ||
| 878 | - | first.attachmentToken(a, aTransport) | 890 | + | first.captureAttachmentCapability(a, aTransport) |
| 879 | 891 | ||||
| 880 | bTransport := newDatagramTestTransport() | 892 | bTransport := newDatagramTestTransport() | ||
| 881 | b := &attachedClient{tr: bTransport, output: newOutputStateStream(), size: a.size} | 893 | b := &attachedClient{tr: bTransport, output: newOutputStateStream(), size: a.size} | ||
| @@ -886,7 +898,7 @@ func TestAttachmentEffectGateReversedConcurrentTransitionsDoNotDeadlock(t *testi | |||||
| 886 | d.sessions[second.id] = second | 898 | d.sessions[second.id] = second | ||
| 887 | d.mu.Unlock() | 899 | d.mu.Unlock() | ||
| 888 | d.attachCoordinator(second, nil, b, true) | 900 | d.attachCoordinator(second, nil, b, true) | ||
| 889 | - | second.attachmentToken(b, bTransport) | 901 | + | second.captureAttachmentCapability(b, bTransport) |
| 890 | 902 | ||||
| 891 | start := make(chan struct{}) | 903 | start := make(chan struct{}) | ||
| 892 | results := make(chan error, 2) | 904 | results := make(chan error, 2) | ||
| @@ -933,9 +945,9 @@ func TestAttachmentEffectGateFrozenConnectionRejectsLateEffect(t *testing.T) { | |||||
| 933 | transport := newDatagramTestTransport() | 945 | transport := newDatagramTestTransport() | ||
| 934 | ac.replaceTransport(transport) | 946 | ac.replaceTransport(transport) | ||
| 935 | rc := d.attachCoordinator(sess, nil, ac, true) | 947 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| 936 | - | token := sess.attachmentToken(ac, transport) | 948 | + | token := sess.captureAttachmentCapability(ac, transport) |
| 937 | token.lease = rc.attachmentLease(ac) | 949 | token.lease = rc.attachmentLease(ac) | ||
| 938 | - | ac.publishAttachmentCapability(token) | 950 | + | ac.installTestAttachmentCapability(token) |
| 939 | 951 | ||||
| 940 | frozen := freezeAttachmentEffectGates(ac) | 952 | frozen := freezeAttachmentEffectGates(ac) | ||
| 941 | require.True(t, frozen.acquired) | 953 | require.True(t, frozen.acquired) | ||
A internal/usecase/daemon/attachment_lifecycle_test.go +117 -0
| @@ -0,0 +1,117 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/require" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func (ac *attachedClient) installTestAttachmentCapability(capability attachmentCapability) { | |||
| 10 | + | ac.lifecycle.mu.Lock() | |||
| 11 | + | defer ac.lifecycle.mu.Unlock() | |||
| 12 | + | ac.lifecycle.initLocked() | |||
| 13 | + | if ac.lifecycle.phase != attachmentEffectsStable || ac.lifecycle.inFlight != 0 { | |||
| 14 | + | panic("daemon: installing an active test attachment capability") | |||
| 15 | + | } | |||
| 16 | + | ac.lifecycle.capability = capability | |||
| 17 | + | ac.lifecycle.failedTransport = transportSnapshot{} | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func (s *session) detachIfCurrent(ac *attachedClient) bool { | |||
| 21 | + | s.mu.Lock() | |||
| 22 | + | defer s.mu.Unlock() | |||
| 23 | + | if !attachmentRegisteredLocked(s, ac) || ac.currentAttachmentSession() != s { | |||
| 24 | + | return false | |||
| 25 | + | } | |||
| 26 | + | s.unregisterAttachmentLocked(ac) | |||
| 27 | + | ac.setSession(nil) | |||
| 28 | + | ac.invalidateRetiredAttachmentCapability() | |||
| 29 | + | return true | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func TestAttachmentCapabilityRejectsIndependentIdentityChanges(t *testing.T) { | |||
| 33 | + | tests := []struct { | |||
| 34 | + | name string | |||
| 35 | + | change func(*session, *attachedClient, *renderCoordinator) | |||
| 36 | + | }{ | |||
| 37 | + | { | |||
| 38 | + | name: "session membership removal", | |||
| 39 | + | change: func(sess *session, ac *attachedClient, _ *renderCoordinator) { | |||
| 40 | + | sess.mu.Lock() | |||
| 41 | + | delete(sess.attachments, ac) | |||
| 42 | + | sess.mu.Unlock() | |||
| 43 | + | }, | |||
| 44 | + | }, | |||
| 45 | + | { | |||
| 46 | + | name: "attachment session replacement", | |||
| 47 | + | change: func(_ *session, ac *attachedClient, _ *renderCoordinator) { | |||
| 48 | + | ac.setSession(&session{}) | |||
| 49 | + | }, | |||
| 50 | + | }, | |||
| 51 | + | { | |||
| 52 | + | name: "connection generation increment", | |||
| 53 | + | change: func(_ *session, ac *attachedClient, _ *renderCoordinator) { | |||
| 54 | + | ac.lifecycle.generation.Add(1) | |||
| 55 | + | }, | |||
| 56 | + | }, | |||
| 57 | + | { | |||
| 58 | + | name: "transport replacement", | |||
| 59 | + | change: func(_ *session, ac *attachedClient, _ *renderCoordinator) { | |||
| 60 | + | ac.replaceTransport(&closeTrackingTransport{}) | |||
| 61 | + | }, | |||
| 62 | + | }, | |||
| 63 | + | { | |||
| 64 | + | name: "transport incarnation increment", | |||
| 65 | + | change: func(_ *session, ac *attachedClient, _ *renderCoordinator) { | |||
| 66 | + | ac.replaceTransport(ac.transport()) | |||
| 67 | + | }, | |||
| 68 | + | }, | |||
| 69 | + | { | |||
| 70 | + | name: "render lease rebind", | |||
| 71 | + | change: func(_ *session, ac *attachedClient, rc *renderCoordinator) { | |||
| 72 | + | rc.mu.Lock() | |||
| 73 | + | rc.rebindAttachmentWithReadinessLocked(ac, true) | |||
| 74 | + | rc.mu.Unlock() | |||
| 75 | + | }, | |||
| 76 | + | }, | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | for _, tt := range tests { | |||
| 80 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 81 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |||
| 82 | + | transport := &closeTrackingTransport{} | |||
| 83 | + | ac.replaceTransport(transport) | |||
| 84 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |||
| 85 | + | capability := sess.captureAttachmentCapability(ac, transport) | |||
| 86 | + | capability.lease = rc.attachmentLease(ac) | |||
| 87 | + | ac.installTestAttachmentCapability(capability) | |||
| 88 | + | ||||
| 89 | + | require.True(t, capability.current()) | |||
| 90 | + | tt.change(sess, ac, rc) | |||
| 91 | + | require.False(t, capability.current()) | |||
| 92 | + | effect, admitted := ac.beginAttachmentEffect(capability) | |||
| 93 | + | require.False(t, admitted) | |||
| 94 | + | require.Nil(t, effect) | |||
| 95 | + | }) | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func TestAttachmentCapabilityInvalidationRejectsNewEffects(t *testing.T) { | |||
| 100 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | |||
| 101 | + | transport := &closeTrackingTransport{} | |||
| 102 | + | ac.replaceTransport(transport) | |||
| 103 | + | rc := d.attachCoordinator(sess, nil, ac, true) | |||
| 104 | + | capability := sess.captureAttachmentCapability(ac, transport) | |||
| 105 | + | capability.lease = rc.attachmentLease(ac) | |||
| 106 | + | ac.installTestAttachmentCapability(capability) | |||
| 107 | + | ||||
| 108 | + | frozen := freezeAttachmentEffectGates(ac) | |||
| 109 | + | require.True(t, frozen.acquired) | |||
| 110 | + | require.True(t, frozen.drained) | |||
| 111 | + | ac.invalidateFrozenAttachmentCapability() | |||
| 112 | + | frozen.unfreeze() | |||
| 113 | + | ||||
| 114 | + | require.False(t, capability.current()) | |||
| 115 | + | _, admitted := ac.beginAttachmentEffect(capability) | |||
| 116 | + | require.False(t, admitted) | |||
| 117 | + | } | |||
M internal/usecase/daemon/attachment_output.go +2 -2
| @@ -357,7 +357,7 @@ func (s *attachmentOutput) fenceLocked() (uint64, uint64, uint64) { | |||||
| 357 | } | 357 | } | ||
| 358 | generation, revision := uint64(0), uint64(0) | 358 | generation, revision := uint64(0), uint64(0) | ||
| 359 | if s.attachment != nil { | 359 | if s.attachment != nil { | ||
| 360 | - | generation = s.attachment.connectionGeneration.Load() | 360 | + | generation = s.attachment.lifecycle.generationValue() |
| 361 | revision = s.attachment.view.revision | 361 | revision = s.attachment.view.revision | ||
| 362 | } | 362 | } | ||
| 363 | return generation, s.currentEpochLocked(), revision | 363 | return generation, s.currentEpochLocked(), revision | ||
| @@ -375,7 +375,7 @@ func (s *attachmentOutput) preparedCurrentLocked(generation, epoch, viewRevision | |||||
| 375 | 375 | ||||
| 376 | func (p *preparedOutput) currentLocked() bool { | 376 | func (p *preparedOutput) currentLocked() bool { | ||
| 377 | return p != nil && p.stream != nil && p.stream.preparedCurrentLocked(p.generation, p.epoch, p.viewRevision, p.base) && | 377 | return p != nil && p.stream != nil && p.stream.preparedCurrentLocked(p.generation, p.epoch, p.viewRevision, p.base) && | ||
| 378 | - | (p.stream.attachment == nil || p.stream.attachment.connectionGeneration.Load() == p.connectionGeneration) | 378 | + | (p.stream.attachment == nil || p.stream.attachment.lifecycle.generationValue() == p.connectionGeneration) |
| 379 | } | 379 | } | ||
| 380 | 380 | ||||
| 381 | func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (ports.Frame, error) { | 381 | func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (ports.Frame, error) { | ||
M internal/usecase/daemon/attachment_output_test.go +1 -1
| @@ -188,7 +188,7 @@ func TestPreparedOutputDropsReplacedConnectionAndView(t *testing.T) { | |||||
| 188 | name string | 188 | name string | ||
| 189 | change func(*attachedClient) | 189 | change func(*attachedClient) | ||
| 190 | }{ | 190 | }{ | ||
| 191 | - | {name: "connection generation", change: func(ac *attachedClient) { ac.connectionGeneration.Add(1) }}, | 191 | + | {name: "connection generation", change: func(ac *attachedClient) { ac.lifecycle.generation.Add(1) }}, |
| 192 | {name: "view revision", change: func(ac *attachedClient) { | 192 | {name: "view revision", change: func(ac *attachedClient) { | ||
| 193 | ac.viewMu.Lock() | 193 | ac.viewMu.Lock() | ||
| 194 | ac.view.revision++ | 194 | ac.view.revision++ | ||
M internal/usecase/daemon/attachment_parking_test.go +2 -2
| @@ -103,7 +103,7 @@ func TestResumeRotatesCredentialAndRejectsStaleTransport(t *testing.T) { | |||||
| 103 | sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport) | 103 | sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport) | ||
| 104 | require.NoError(t, err) | 104 | require.NoError(t, err) | ||
| 105 | oldToken := ac.resumeToken | 105 | oldToken := ac.resumeToken | ||
| 106 | - | oldGeneration := ac.connectionGeneration.Load() | 106 | + | oldGeneration := ac.lifecycle.generationValue() |
| 107 | d.clientGone(sess, ac, oldTransport, false) | 107 | d.clientGone(sess, ac, oldTransport, false) | ||
| 108 | 108 | ||||
| 109 | newTransport := &closeTrackingTransport{} | 109 | newTransport := &closeTrackingTransport{} | ||
| @@ -113,7 +113,7 @@ func TestResumeRotatesCredentialAndRejectsStaleTransport(t *testing.T) { | |||||
| 113 | require.Same(t, sess, resumed) | 113 | require.Same(t, sess, resumed) | ||
| 114 | require.Same(t, ac, same) | 114 | require.Same(t, ac, same) | ||
| 115 | require.NotEqual(t, oldToken, ac.resumeToken) | 115 | require.NotEqual(t, oldToken, ac.resumeToken) | ||
| 116 | - | require.Greater(t, ac.connectionGeneration.Load(), oldGeneration) | 116 | + | require.Greater(t, ac.lifecycle.generationValue(), oldGeneration) |
| 117 | require.True(t, d.commitResumeClaim(ac)) | 117 | require.True(t, d.commitResumeClaim(ac)) | ||
| 118 | d.mu.Lock() | 118 | d.mu.Lock() | ||
| 119 | _, oldRetained := d.parked[oldToken] | 119 | _, oldRetained := d.parked[oldToken] | ||
M internal/usecase/daemon/attachment_render_test.go +7 -7
| @@ -123,13 +123,13 @@ func TestAttachmentResizeKeepsPeerWindowAndExpandsSharedContent(t *testing.T) { | |||||
| 123 | rc := sess.renderCoordinator() | 123 | rc := sess.renderCoordinator() | ||
| 124 | lease := rc.attachmentLease(second) | 124 | lease := rc.attachmentLease(second) | ||
| 125 | require.True(t, rc.markAttachmentReady(lease)) | 125 | require.True(t, rc.markAttachmentReady(lease)) | ||
| 126 | - | token := sess.attachmentToken(second, secondTransport) | 126 | + | token := sess.captureAttachmentCapability(second, secondTransport) |
| 127 | token.lease = lease | 127 | token.lease = lease | ||
| 128 | - | ticket, admitted := second.beginAttachmentEffect(token) | 128 | + | second.installTestAttachmentCapability(token) |
| 129 | + | effect, admitted := second.beginAttachmentEffect(token) | |||
| 129 | require.True(t, admitted) | 130 | require.True(t, admitted) | ||
| 130 | - | token.effect = ticket | 131 | + | require.True(t, d.resizeAttachmentForLease(effect, domain.Size{Cols: 120, Rows: 50})) |
| 131 | - | require.True(t, d.resizeAttachmentForLease(token, domain.Size{Cols: 120, Rows: 50})) | 132 | + | effect.End() |
| 132 | - | ticket.End() | |||
| 133 | 133 | ||||
| 134 | tb.mu.Lock() | 134 | tb.mu.Lock() | ||
| 135 | require.Equal(t, domain.Size{Cols: 120, Rows: 48}, tb.size) | 135 | require.Equal(t, domain.Size{Cols: 120, Rows: 48}, tb.size) | ||
| @@ -166,7 +166,7 @@ func TestAttachmentFirstPaintDoesNotWaitForBlockedPeer(t *testing.T) { | |||||
| 166 | awaitTestCompletion(t, oldTransport.entered, "slow attachment did not begin its blocked output") | 166 | awaitTestCompletion(t, oldTransport.entered, "slow attachment did not begin its blocked output") | ||
| 167 | 167 | ||||
| 168 | require.True(t, rc.markAttachmentReady(freshLease)) | 168 | require.True(t, rc.markAttachmentReady(freshLease)) | ||
| 169 | - | token := sess.attachmentToken(fresh, newTransport) | 169 | + | token := sess.captureAttachmentCapability(fresh, newTransport) |
| 170 | token.lease = freshLease | 170 | token.lease = freshLease | ||
| 171 | painted := make(chan bool, 1) | 171 | painted := make(chan bool, 1) | ||
| 172 | go func() { painted <- d.firstPaintForTransition(token) }() | 172 | go func() { painted <- d.firstPaintForTransition(token) }() | ||
| @@ -282,7 +282,7 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) { | |||||
| 282 | require.True(t, rc.markAttachmentReady(firstLease)) | 282 | require.True(t, rc.markAttachmentReady(firstLease)) | ||
| 283 | require.True(t, rc.markAttachmentReady(secondLease)) | 283 | require.True(t, rc.markAttachmentReady(secondLease)) | ||
| 284 | resize := func(ac *attachedClient, tr ports.Transport, size domain.Size) { | 284 | resize := func(ac *attachedClient, tr ports.Transport, size domain.Size) { | ||
| 285 | - | token := sess.attachmentToken(ac, tr) | 285 | + | token := sess.captureAttachmentCapability(ac, tr) |
| 286 | d.handleAttachmentClientFrame(token, ports.Frame{ | 286 | d.handleAttachmentClientFrame(token, ports.Frame{ | ||
| 287 | Type: ports.MsgResize, | 287 | Type: ports.MsgResize, | ||
| 288 | Payload: mustMarshalResize(ports.Resize{Size: size}), | 288 | Payload: mustMarshalResize(ports.Resize{Size: size}), | ||
M internal/usecase/daemon/attachment_route_identity_test.go +4 -4
| @@ -25,9 +25,9 @@ func TestAttachmentTransitionPublishesCommittedRouteIdentity(t *testing.T) { | |||||
| 25 | transport := &closeTrackingTransport{} | 25 | transport := &closeTrackingTransport{} | ||
| 26 | ac.replaceTransport(transport) | 26 | ac.replaceTransport(transport) | ||
| 27 | rc := d.attachCoordinator(source, nil, ac, true) | 27 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 28 | - | token := source.attachmentToken(ac, transport) | 28 | + | token := source.captureAttachmentCapability(ac, transport) |
| 29 | token.lease = rc.attachmentLease(ac) | 29 | token.lease = rc.attachmentLease(ac) | ||
| 30 | - | ac.publishAttachmentCapability(token) | 30 | + | ac.installTestAttachmentCapability(token) |
| 31 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | 31 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | ||
| 32 | 32 | ||||
| 33 | target := &session{ | 33 | target := &session{ | ||
| @@ -75,9 +75,9 @@ func TestAttachmentTransitionTearsDownAfterCommittedIdentitySendFailure(t *testi | |||||
| 75 | transport := &committedIdentityErrorTransport{} | 75 | transport := &committedIdentityErrorTransport{} | ||
| 76 | ac.replaceTransport(transport) | 76 | ac.replaceTransport(transport) | ||
| 77 | rc := d.attachCoordinator(source, nil, ac, true) | 77 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 78 | - | token := source.attachmentToken(ac, transport) | 78 | + | token := source.captureAttachmentCapability(ac, transport) |
| 79 | token.lease = rc.attachmentLease(ac) | 79 | token.lease = rc.attachmentLease(ac) | ||
| 80 | - | ac.publishAttachmentCapability(token) | 80 | + | ac.installTestAttachmentCapability(token) |
| 81 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | 81 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | ||
| 82 | 82 | ||||
| 83 | target := &session{ | 83 | target := &session{ | ||
M internal/usecase/daemon/attachment_transition.go +12 -36
| @@ -24,14 +24,15 @@ type attachmentTransitionRequest struct { | |||||
| 24 | target *session | 24 | target *session | ||
| 25 | next *attachedClient | 25 | next *attachedClient | ||
| 26 | expectedTransport transportSnapshot | 26 | expectedTransport transportSnapshot | ||
| 27 | - | sourceToken *attachmentConnectionToken | 27 | + | sourceCapability *attachmentCapability |
| 28 | + | sourceEffect *attachmentEffect | |||
| 28 | action string | 29 | action string | ||
| 29 | activateTargetTab bool | 30 | activateTargetTab bool | ||
| 30 | targetTabIndex int | 31 | targetTabIndex int | ||
| 31 | copySourceEnvironment bool | 32 | copySourceEnvironment bool | ||
| 32 | - | // preserveAttachment commits attachment-local navigation state without | 33 | + | // preserveAttachment commits Attachment-local navigation state without |
| 33 | - | // changing session membership. The initiating connection token remains the | 34 | + | // changing Session membership. The initiating capability remains the exact |
| 34 | - | // exact authority for this mutation. | 35 | + | // authority for this mutation. |
| 35 | preserveAttachment bool | 36 | preserveAttachment bool | ||
| 36 | expectedSourceTab *tab | 37 | expectedSourceTab *tab | ||
| 37 | // transferExpectedSourceTab permits an installed visible floating pane only | 38 | // transferExpectedSourceTab permits an installed visible floating pane only | ||
| @@ -44,33 +45,8 @@ type attachmentTransitionRequest struct { | |||||
| 44 | attachmentEffectsFrozen bool | 45 | attachmentEffectsFrozen bool | ||
| 45 | } | 46 | } | ||
| 46 | 47 | ||||
| 47 | - | func transitionSourceTokenMatchesRequest(token attachmentConnectionToken, source *session, req attachmentTransitionRequest) bool { | |||
| 48 | - | return source != nil && token.sess != nil && token.sess == source && token.ac == req.next && | |||
| 49 | - | token.generation == req.next.connectionGeneration.Load() && | |||
| 50 | - | token.transport.transport == req.expectedTransport.transport && | |||
| 51 | - | token.transport.incarnation == req.expectedTransport.incarnation | |||
| 52 | - | } | |||
| 53 | - | ||||
| 54 | - | // transitionSourceTokenCurrentLocked requires source.core().mu and, when a | |||
| 55 | - | // lease exists, sourceCoordinator.mu. It is the canonical exact-connection | |||
| 56 | - | // handoff check for client-originated navigation and lifecycle mutations. | |||
| 57 | - | func transitionSourceTokenCurrentLocked(token attachmentConnectionToken, source *session, sourceCoordinator *renderCoordinator, req attachmentTransitionRequest) bool { | |||
| 58 | - | if token.sess != source || token.ac != req.next || | |||
| 59 | - | token.generation != req.next.connectionGeneration.Load() || | |||
| 60 | - | token.sess == nil || token.ac.currentAttachmentSession() != source || | |||
| 61 | - | !attachmentRegisteredLocked(source, req.next) || | |||
| 62 | - | !req.next.transportSnapshotCurrent(token.transport) { | |||
| 63 | - | return false | |||
| 64 | - | } | |||
| 65 | - | if token.lease == nil { | |||
| 66 | - | return true | |||
| 67 | - | } | |||
| 68 | - | return sourceCoordinator != nil && token.lease.attachment == req.next && | |||
| 69 | - | sourceCoordinator.leaseCurrentLocked(token.lease, true) | |||
| 70 | - | } | |||
| 71 | - | ||||
| 72 | type attachmentTransitionResult struct { | 48 | type attachmentTransitionResult struct { | ||
| 73 | - | published attachmentConnectionToken | 49 | + | published attachmentCapability |
| 74 | cleanups []renderLifecycleCleanup | 50 | cleanups []renderLifecycleCleanup | ||
| 75 | sourceGeometrySession *session | 51 | sourceGeometrySession *session | ||
| 76 | } | 52 | } | ||
| @@ -103,7 +79,7 @@ func (d *Daemon) snapshotAttachmentTransition(req attachmentTransitionRequest) ( | |||||
| 103 | d.sessions[sourceCore.id] != source || req.target != nil && d.sessions[targetCore.id] != req.target { | 79 | d.sessions[sourceCore.id] != source || req.target != nil && d.sessions[targetCore.id] != req.target { | ||
| 104 | return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition | 80 | return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition | ||
| 105 | } | 81 | } | ||
| 106 | - | if req.sourceToken != nil && !transitionSourceTokenMatchesRequest(*req.sourceToken, source, req) { | 82 | + | if req.sourceCapability != nil && !req.sourceCapability.matchesConnectionSnapshot(source, req.next, req.expectedTransport) { |
| 107 | return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition | 83 | return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition | ||
| 108 | } | 84 | } | ||
| 109 | req.preflighted = true | 85 | req.preflighted = true | ||
| @@ -112,7 +88,7 @@ func (d *Daemon) snapshotAttachmentTransition(req attachmentTransitionRequest) ( | |||||
| 112 | 88 | ||||
| 113 | // freezeAttachmentTransition drains the snapshotted connection without any | 89 | // freezeAttachmentTransition drains the snapshotted connection without any | ||
| 114 | // architecture lock held. The returned guard remains frozen through publication. | 90 | // architecture lock held. The returned guard remains frozen through publication. | ||
| 115 | - | func (d *Daemon) freezeAttachmentTransition(req attachmentTransitionRequest, participants attachmentTransitionParticipants) (frozenAttachmentEffectGates, error) { | 91 | + | func (d *Daemon) freezeAttachmentTransition(req attachmentTransitionRequest, participants attachmentTransitionParticipants) (attachmentTransitionGuard, error) { |
| 116 | if d.afterAttachmentEffectParticipantsSnapshotted != nil { | 92 | if d.afterAttachmentEffectParticipantsSnapshotted != nil { | ||
| 117 | d.afterAttachmentEffectParticipantsSnapshotted(req.action, participants.clients) | 93 | d.afterAttachmentEffectParticipantsSnapshotted(req.action, participants.clients) | ||
| 118 | } | 94 | } | ||
| @@ -131,7 +107,7 @@ func (d *Daemon) freezeAttachmentTransition(req attachmentTransitionRequest, par | |||||
| 131 | } | 107 | } | ||
| 132 | 108 | ||||
| 133 | // publishAttachmentTransition revalidates the frozen source, creates an | 109 | // publishAttachmentTransition revalidates the frozen source, creates an | ||
| 134 | - | // optional target, and performs exact membership publication under d.mu. | 110 | + | // optional target, and performs exact capability publication under d.mu. |
| 135 | func (d *Daemon) publishAttachmentTransition(req attachmentTransitionRequest) (attachmentTransitionResult, error) { | 111 | func (d *Daemon) publishAttachmentTransition(req attachmentTransitionRequest) (attachmentTransitionResult, error) { | ||
| 136 | d.mu.Lock() | 112 | d.mu.Lock() | ||
| 137 | defer d.mu.Unlock() | 113 | defer d.mu.Unlock() | ||
| @@ -162,8 +138,8 @@ func (d *Daemon) transitionAttachment(req attachmentTransitionRequest) (attachme | |||||
| 162 | if err != nil { | 138 | if err != nil { | ||
| 163 | return attachmentTransitionResult{}, err | 139 | return attachmentTransitionResult{}, err | ||
| 164 | } | 140 | } | ||
| 165 | - | if req.sourceToken != nil { | 141 | + | if req.sourceEffect != nil { |
| 166 | - | d.endActionAttachmentEffect(req.sourceToken.effect, req.action) | 142 | + | d.endActionAttachmentEffect(req.sourceEffect, req.action) |
| 167 | } | 143 | } | ||
| 168 | 144 | ||||
| 169 | frozen, err := d.freezeAttachmentTransition(req, participants) | 145 | frozen, err := d.freezeAttachmentTransition(req, participants) | ||
| @@ -183,7 +159,7 @@ func (d *Daemon) transitionAttachment(req attachmentTransitionRequest) (attachme | |||||
| 183 | if req.ready && result.published.ac != nil && result.published.ac.routeSnapshotCopy().Generation != 0 { | 159 | if req.ready && result.published.ac != nil && result.published.ac.routeSnapshotCopy().Generation != 0 { | ||
| 184 | identityErr := errAttachmentTransition | 160 | identityErr := errAttachmentTransition | ||
| 185 | if effect, admitted := result.published.ac.beginAttachmentEffect(result.published); admitted { | 161 | if effect, admitted := result.published.ac.beginAttachmentEffect(result.published); admitted { | ||
| 186 | - | identityErr = d.sendCommittedRouteIdentityForAttachment(effect.connectionToken()) | 162 | + | identityErr = d.sendCommittedRouteIdentityForAttachment(effect) |
| 187 | effect.End() | 163 | effect.End() | ||
| 188 | } | 164 | } | ||
| 189 | if identityErr != nil { | 165 | if identityErr != nil { | ||
M internal/usecase/daemon/attachment_transition_prelocked_test.go +2 -2
| @@ -55,7 +55,7 @@ func TestValidateAttachmentTransitionPrelockedLeavesMembershipUntouchedOnFailure | |||||
| 55 | require.Equal(t, false, target.attachmentRegistered(next)) | 55 | require.Equal(t, false, target.attachmentRegistered(next)) | ||
| 56 | require.Equal(t, true, target.attachmentRegistered(old)) | 56 | require.Equal(t, true, target.attachmentRegistered(old)) | ||
| 57 | require.Same(t, source, next.currentSession()) | 57 | require.Same(t, source, next.currentSession()) | ||
| 58 | - | require.Equal(t, uint64(0), next.connectionGeneration.Load()) | 58 | + | require.Equal(t, uint64(0), next.lifecycle.generationValue()) |
| 59 | - | require.Equal(t, uint64(0), old.connectionGeneration.Load()) | 59 | + | require.Equal(t, uint64(0), old.lifecycle.generationValue()) |
| 60 | require.Equal(t, 0, testAttachmentTabIndex(target)) | 60 | require.Equal(t, 0, testAttachmentTabIndex(target)) | ||
| 61 | } | 61 | } | ||
M internal/usecase/daemon/attachment_transition_publish.go +12 -16
| @@ -10,7 +10,7 @@ import ( | |||||
| 10 | // effect gate is frozen and d.mu is held. No target is created before this | 10 | // effect gate is frozen and d.mu is held. No target is created before this | ||
| 11 | // succeeds. | 11 | // succeeds. | ||
| 12 | func (d *Daemon) transitionSourcePreflightLocked(req attachmentTransitionRequest) bool { | 12 | func (d *Daemon) transitionSourcePreflightLocked(req attachmentTransitionRequest) bool { | ||
| 13 | - | if req.sourceToken == nil || req.source == nil || req.source.core() == nil || d.sessions[req.source.core().id] != req.source { | 13 | + | if req.sourceCapability == nil || req.source == nil || req.source.core() == nil || d.sessions[req.source.core().id] != req.source { |
| 14 | return false | 14 | return false | ||
| 15 | } | 15 | } | ||
| 16 | d.notices.routingMu.Lock() | 16 | d.notices.routingMu.Lock() | ||
| @@ -22,13 +22,13 @@ func (d *Daemon) transitionSourcePreflightLocked(req attachmentTransitionRequest | |||||
| 22 | coordinator.mu.Lock() | 22 | coordinator.mu.Lock() | ||
| 23 | defer coordinator.mu.Unlock() | 23 | defer coordinator.mu.Unlock() | ||
| 24 | } | 24 | } | ||
| 25 | - | if !transitionSourceTokenCurrentLocked(*req.sourceToken, req.source, coordinator, req) { | 25 | + | if !req.sourceCapability.currentInSessionAndLeaseLocked(req.source, req.next, coordinator) { |
| 26 | return false | 26 | return false | ||
| 27 | } | 27 | } | ||
| 28 | return transitionSourceTabCurrentLocked(req.source, req.expectedSourceTab) | 28 | return transitionSourceTabCurrentLocked(req.source, req.expectedSourceTab) | ||
| 29 | } | 29 | } | ||
| 30 | 30 | ||||
| 31 | - | func (d *Daemon) sourceAttachmentTokenCurrentFrozen(token attachmentConnectionToken) bool { | 31 | + | func (d *Daemon) sourceAttachmentCapabilityCurrentFrozen(token attachmentCapability) bool { |
| 32 | if token.sess == nil || token.ac == nil { | 32 | if token.sess == nil || token.ac == nil { | ||
| 33 | return false | 33 | return false | ||
| 34 | } | 34 | } | ||
| @@ -46,8 +46,7 @@ func (d *Daemon) sourceAttachmentTokenCurrentFrozen(token attachmentConnectionTo | |||||
| 46 | coordinator.mu.Lock() | 46 | coordinator.mu.Lock() | ||
| 47 | defer coordinator.mu.Unlock() | 47 | defer coordinator.mu.Unlock() | ||
| 48 | } | 48 | } | ||
| 49 | - | req := attachmentTransitionRequest{source: token.sess, next: token.ac, expectedTransport: token.transport} | 49 | + | return token.currentInSessionAndLeaseLocked(token.sess, token.ac, coordinator) |
| 50 | - | return transitionSourceTokenCurrentLocked(token, token.sess, coordinator, req) | |||
| 51 | } | 50 | } | ||
| 52 | 51 | ||||
| 53 | func transitionSourceTabCurrentLocked(source *session, expected *tab) bool { | 52 | func transitionSourceTabCurrentLocked(source *session, expected *tab) bool { | ||
| @@ -84,7 +83,6 @@ type attachmentPublication struct { | |||||
| 84 | targetCoordinator *renderCoordinator | 83 | targetCoordinator *renderCoordinator | ||
| 85 | sourceCoordinator *renderCoordinator | 84 | sourceCoordinator *renderCoordinator | ||
| 86 | releaseCoordinators func() | 85 | releaseCoordinators func() | ||
| 87 | - | nextGeneration uint64 | |||
| 88 | } | 86 | } | ||
| 89 | 87 | ||||
| 90 | func (p *attachmentPublication) unlockCoordinators() { | 88 | func (p *attachmentPublication) unlockCoordinators() { | ||
| @@ -124,20 +122,20 @@ func (d *Daemon) validateAttachmentTransitionPrelocked(req attachmentTransitionR | |||||
| 124 | if sourceCore == nil || targetCore == nil || !req.preflighted || !req.attachmentEffectsFrozen || | 122 | if sourceCore == nil || targetCore == nil || !req.preflighted || !req.attachmentEffectsFrozen || | ||
| 125 | req.next == nil || d.closing || d.sessions[sourceCore.id] != source || d.sessions[targetCore.id] != req.target || | 123 | req.next == nil || d.closing || d.sessions[sourceCore.id] != source || d.sessions[targetCore.id] != req.target || | ||
| 126 | req.expectedTransport.transport == nil || !req.next.transportSnapshotCurrent(req.expectedTransport) || | 124 | req.expectedTransport.transport == nil || !req.next.transportSnapshotCurrent(req.expectedTransport) || | ||
| 127 | - | req.preserveAttachment && req.sourceToken == nil { | 125 | + | req.preserveAttachment && req.sourceCapability == nil { |
| 128 | return nil, errAttachmentTransition | 126 | return nil, errAttachmentTransition | ||
| 129 | } | 127 | } | ||
| 130 | 128 | ||||
| 131 | sourceCoordinator := sourceCore.coordinator.Load() | 129 | sourceCoordinator := sourceCore.coordinator.Load() | ||
| 132 | targetCoordinator := d.ensureAttachmentRenderCoordinatorPrelocked(req.target) | 130 | targetCoordinator := d.ensureAttachmentRenderCoordinatorPrelocked(req.target) | ||
| 133 | - | // transitionSourceTokenCurrentLocked also reads the source lease. Hold the | 131 | + | // Capability validation also reads the source lease. Hold the |
| 134 | // source coordinator lock before calling it, following session -> coordinator | 132 | // source coordinator lock before calling it, following session -> coordinator | ||
| 135 | // ordering and preventing a lease rebind from racing validation. | 133 | // ordering and preventing a lease rebind from racing validation. | ||
| 136 | releaseCoordinators := lockAttachmentCoordinators(source, sourceCoordinator, req.target, targetCoordinator) | 134 | releaseCoordinators := lockAttachmentCoordinators(source, sourceCoordinator, req.target, targetCoordinator) | ||
| 137 | if d.afterAttachmentTransitionCoordinatorsLocked != nil { | 135 | if d.afterAttachmentTransitionCoordinatorsLocked != nil { | ||
| 138 | d.afterAttachmentTransitionCoordinatorsLocked() | 136 | d.afterAttachmentTransitionCoordinatorsLocked() | ||
| 139 | } | 137 | } | ||
| 140 | - | if req.sourceToken != nil && !transitionSourceTokenCurrentLocked(*req.sourceToken, source, sourceCoordinator, req) { | 138 | + | if req.sourceCapability != nil && !req.sourceCapability.currentInSessionAndLeaseLocked(source, req.next, sourceCoordinator) { |
| 141 | releaseCoordinators() | 139 | releaseCoordinators() | ||
| 142 | return nil, errAttachmentTransition | 140 | return nil, errAttachmentTransition | ||
| 143 | } | 141 | } | ||
| @@ -155,7 +153,7 @@ func (d *Daemon) validateAttachmentTransitionPrelocked(req attachmentTransitionR | |||||
| 155 | } | 153 | } | ||
| 156 | if !attachmentLifecycleCurrentLocked(req.target, req.expectedTargetLifecycle) || | 154 | if !attachmentLifecycleCurrentLocked(req.target, req.expectedTargetLifecycle) || | ||
| 157 | req.activateTargetTab && !req.target.validTargetTabLocked(req.targetTabIndex) || | 155 | req.activateTargetTab && !req.target.validTargetTabLocked(req.targetTabIndex) || | ||
| 158 | - | req.sourceToken != nil && !transitionSourceTabCurrentForRequestLocked(source, req.expectedSourceTab, req.transferExpectedSourceTab) { | 156 | + | req.sourceCapability != nil && !transitionSourceTabCurrentForRequestLocked(source, req.expectedSourceTab, req.transferExpectedSourceTab) { |
| 159 | releaseCoordinators() | 157 | releaseCoordinators() | ||
| 160 | return nil, errAttachmentTransition | 158 | return nil, errAttachmentTransition | ||
| 161 | } | 159 | } | ||
| @@ -196,7 +194,6 @@ func publishAttachmentOwnershipLocked(publication *attachmentPublication) { | |||||
| 196 | // attachment claim with the replacement transport's current size. | 194 | // attachment claim with the replacement transport's current size. | ||
| 197 | req.target.claimGeometryOwnerLocked(req.next, req.next.geometrySnapshot()) | 195 | req.target.claimGeometryOwnerLocked(req.next, req.next.geometrySnapshot()) | ||
| 198 | } | 196 | } | ||
| 199 | - | publication.nextGeneration = req.next.connectionGeneration.Add(1) | |||
| 200 | req.next.setSession(req.target) | 197 | req.next.setSession(req.target) | ||
| 201 | } | 198 | } | ||
| 202 | 199 | ||||
| @@ -214,14 +211,13 @@ func buildAttachmentPostcommitPlanLocked(publication *attachmentPublication) att | |||||
| 214 | lease = publication.targetCoordinator.attachWithReadinessLocked(req.next, req.ready) | 211 | lease = publication.targetCoordinator.attachWithReadinessLocked(req.next, req.ready) | ||
| 215 | } | 212 | } | ||
| 216 | } | 213 | } | ||
| 217 | - | result.published = attachmentConnectionToken{ | 214 | + | result.published = attachmentCapability{ |
| 218 | sess: req.target, ac: req.next, | 215 | sess: req.target, ac: req.next, | ||
| 219 | - | generation: publication.nextGeneration, | 216 | + | transport: req.expectedTransport, lease: lease, |
| 220 | - | transport: req.expectedTransport, lease: lease, | |||
| 221 | rebase: publication.source != req.target, | 217 | rebase: publication.source != req.target, | ||
| 222 | } | 218 | } | ||
| 223 | if req.attachmentEffectsFrozen { | 219 | if req.attachmentEffectsFrozen { | ||
| 224 | - | req.next.publishFrozenAttachmentCapability(result.published) | 220 | + | result.published = req.next.publishFrozenAttachmentCapability(result.published) |
| 225 | } | 221 | } | ||
| 226 | return result | 222 | return result | ||
| 227 | } | 223 | } | ||
| @@ -229,7 +225,7 @@ func buildAttachmentPostcommitPlanLocked(publication *attachmentPublication) att | |||||
| 229 | func (d *Daemon) publishAttachmentTransitionPrelocked(publication *attachmentPublication) attachmentTransitionResult { | 225 | func (d *Daemon) publishAttachmentTransitionPrelocked(publication *attachmentPublication) attachmentTransitionResult { | ||
| 230 | if publication.req.preserveAttachment { | 226 | if publication.req.preserveAttachment { | ||
| 231 | d.applyTargetStateLocked(publication) | 227 | d.applyTargetStateLocked(publication) | ||
| 232 | - | return attachmentTransitionResult{published: *publication.req.sourceToken} | 228 | + | return attachmentTransitionResult{published: *publication.req.sourceCapability} |
| 233 | } | 229 | } | ||
| 234 | publishAttachmentOwnershipLocked(publication) | 230 | publishAttachmentOwnershipLocked(publication) | ||
| 235 | d.applyTargetStateLocked(publication) | 231 | d.applyTargetStateLocked(publication) | ||
M internal/usecase/daemon/attention.go +9 -5
| @@ -50,16 +50,20 @@ func (d *Daemon) noteAttention(sess *session, tb *tab) { | |||||
| 50 | // happened — it also covers "no target exists", which is routine and not an | 50 | // happened — it also covers "no target exists", which is routine and not an | ||
| 51 | // error. Only a failure to reach a target that does exist is a genuine error. | 51 | // error. Only a failure to reach a target that does exist is a genuine error. | ||
| 52 | func (d *Daemon) jumpAttention(sess *session, ac *attachedClient) error { | 52 | func (d *Daemon) jumpAttention(sess *session, ac *attachedClient) error { | ||
| 53 | - | return d.jumpAttentionForAttachment(sess, ac, attachmentConnectionToken{}) | 53 | + | return d.jumpAttentionForAttachment(sess, ac, nil) |
| 54 | } | 54 | } | ||
| 55 | 55 | ||||
| 56 | - | func (d *Daemon) jumpAttentionForAttachment(sess *session, ac *attachedClient, token attachmentConnectionToken) error { | 56 | + | func (d *Daemon) jumpAttentionForAttachment(sess *session, ac *attachedClient, effect *attachmentEffect) error { |
| 57 | if sess == nil || ac == nil { | 57 | if sess == nil || ac == nil { | ||
| 58 | return nil | 58 | return nil | ||
| 59 | } | 59 | } | ||
| 60 | if idx, ok := oldestAttentionTab(sess); ok { | 60 | if idx, ok := oldestAttentionTab(sess); ok { | ||
| 61 | if sess.switchAttachmentTab(ac, idx) { | 61 | if sess.switchAttachmentTab(ac, idx) { | ||
| 62 | - | d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(ac), false, ac, token.lease) | 62 | + | var lease *attachmentLease |
| 63 | + | if effect != nil { | |||
| 64 | + | lease = effect.lease | |||
| 65 | + | } | |||
| 66 | + | d.activateTabAfterResizeForLease(sess, sess.tabForAttachment(ac), false, ac, lease) | |||
| 63 | d.invalidateRender(sess, ac, true, "attention.go") | 67 | d.invalidateRender(sess, ac, true, "attention.go") | ||
| 64 | } | 68 | } | ||
| 65 | return nil | 69 | return nil | ||
| @@ -70,10 +74,10 @@ func (d *Daemon) jumpAttentionForAttachment(sess *session, ac *attachedClient, t | |||||
| 70 | return nil | 74 | return nil | ||
| 71 | } | 75 | } | ||
| 72 | pickerTarget := picker.Target{Session: target.sessionID, TabIndex: target.tabIndex} | 76 | pickerTarget := picker.Target{Session: target.sessionID, TabIndex: target.tabIndex} | ||
| 73 | - | if token.ac == nil { | 77 | + | if effect == nil { |
| 74 | return d.switchToTarget(sess, ac, pickerTarget) | 78 | return d.switchToTarget(sess, ac, pickerTarget) | ||
| 75 | } | 79 | } | ||
| 76 | - | return d.switchActiveTargetForAttachment(token, pickerTarget) | 80 | + | return d.switchActiveTargetForAttachment(effect, pickerTarget) |
| 77 | } | 81 | } | ||
| 78 | 82 | ||||
| 79 | func oldestAttentionTab(sess *session) (int, bool) { | 83 | func oldestAttentionTab(sess *session) (int, bool) { | ||
M internal/usecase/daemon/client.go +58 -57
| @@ -42,14 +42,11 @@ type attachedClient struct { | |||||
| 42 | terminalCapabilities ports.TerminalCapabilities | 42 | terminalCapabilities ports.TerminalCapabilities | ||
| 43 | navigationCapabilities ports.NavigationCapabilities | 43 | navigationCapabilities ports.NavigationCapabilities | ||
| 44 | startupOverlay ports.StartupOverlay | 44 | startupOverlay ports.StartupOverlay | ||
| 45 | - | // connectionGeneration is the wire-facing capability generation. attachmentEffects is | 45 | + | // lifecycle is the sole authority for attachment capability publication, |
| 46 | - | // the linearization gate for every attachment-bound observable operation. A role | 46 | + | // effect admission, transition freeze/drain, and connection generation. |
| 47 | - | // transition freezes and drains this gate before changing either generation | 47 | + | lifecycle attachmentLifecycle |
| 48 | - | // or the session/coordinator registries. | 48 | + | resumeCapable bool |
| 49 | - | connectionGeneration atomic.Uint64 | 49 | + | resumeToken uint64 |
| 50 | - | attachmentEffects attachmentEffectGate | |||
| 51 | - | resumeCapable bool | |||
| 52 | - | resumeToken uint64 | |||
| 53 | // resumeClaimToken is non-zero only between a parked resume claim and its | 50 | // resumeClaimToken is non-zero only between a parked resume claim and its | ||
| 54 | // successful Welcome. It lets a failed pre-claim handshake restore the old | 51 | // successful Welcome. It lets a failed pre-claim handshake restore the old | ||
| 55 | // credential instead of consuming it. | 52 | // credential instead of consuming it. | ||
| @@ -111,9 +108,9 @@ type attachedClient struct { | |||||
| 111 | // routeSessionPurge distinguishes a brand-new session from a restored | 108 | // routeSessionPurge distinguishes a brand-new session from a restored | ||
| 112 | // stopped session when failed-handshake cleanup removes the live instance. | 109 | // stopped session when failed-handshake cleanup removes the live instance. | ||
| 113 | routeSessionPurge bool | 110 | routeSessionPurge bool | ||
| 114 | - | // beforeAttachmentTokenValidation is a deterministic lifecycle-race seam | 111 | + | // beforeAttachmentCapabilityValidation is a deterministic lifecycle-race Seam |
| 115 | // used only by package tests. | 112 | // used only by package tests. | ||
| 116 | - | beforeAttachmentTokenValidation func() | 113 | + | beforeAttachmentCapabilityValidation func() |
| 117 | } | 114 | } | ||
| 118 | 115 | ||||
| 119 | type cursorOut struct { | 116 | type cursorOut struct { | ||
| @@ -474,7 +471,7 @@ func (ac *attachedClient) sendExpectedTransport(expected transportSnapshot, f po | |||||
| 474 | // attachment's current transport incarnation and admits ticket's interruptible | 471 | // attachment's current transport incarnation and admits ticket's interruptible | ||
| 475 | // transport send. The caller must already hold ac.sendMu and, on success, owns | 472 | // transport send. The caller must already hold ac.sendMu and, on success, owns | ||
| 476 | // both the send and its matching ticket.endTransportSend. | 473 | // both the send and its matching ticket.endTransportSend. | ||
| 477 | - | func (ac *attachedClient) beginExpectedTransportSendLocked(expected transportSnapshot, ticket *attachmentEffectTicket) error { | 474 | + | func (ac *attachedClient) beginExpectedTransportSendLocked(expected transportSnapshot, ticket *attachmentEffect) error { |
| 478 | if expected.transport == nil || !ac.transportSnapshotCurrent(expected) { | 475 | if expected.transport == nil || !ac.transportSnapshotCurrent(expected) { | ||
| 479 | return errTransportReplaced | 476 | return errTransportReplaced | ||
| 480 | } | 477 | } | ||
| @@ -484,7 +481,7 @@ func (ac *attachedClient) beginExpectedTransportSendLocked(expected transportSna | |||||
| 484 | return nil | 481 | return nil | ||
| 485 | } | 482 | } | ||
| 486 | 483 | ||||
| 487 | - | func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportSnapshot, f ports.Frame, ticket *attachmentEffectTicket) error { | 484 | + | func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportSnapshot, f ports.Frame, ticket *attachmentEffect) error { |
| 488 | ac.sendMu.Lock() | 485 | ac.sendMu.Lock() | ||
| 489 | defer ac.sendMu.Unlock() | 486 | defer ac.sendMu.Unlock() | ||
| 490 | // A attachment-bound send requires a live ticket, and reports every rejection, | 487 | // A attachment-bound send requires a live ticket, and reports every rejection, | ||
| @@ -862,37 +859,36 @@ func (d *Daemon) resetScreenDefaultColors(sess *session) { | |||||
| 862 | // firstPaintForTransition is the only active post-transition paint entry | 859 | // firstPaintForTransition is the only active post-transition paint entry | ||
| 863 | // point. It rejects a stale attachment capability before rebasing output and carries | 860 | // point. It rejects a stale attachment capability before rebasing output and carries | ||
| 864 | // the exact coordinator lease through every resize and render effect. | 861 | // the exact coordinator lease through every resize and render effect. | ||
| 865 | - | func (d *Daemon) firstPaintForTransition(token attachmentConnectionToken) bool { | 862 | + | func (d *Daemon) firstPaintForTransition(capability attachmentCapability) bool { |
| 866 | - | if !token.attachmentCurrent() { | 863 | + | if !capability.current() { |
| 867 | return false | 864 | return false | ||
| 868 | } | 865 | } | ||
| 869 | - | ticket, admitted := token.ac.beginAttachmentEffect(token) | 866 | + | effect, admitted := capability.ac.beginAttachmentEffect(capability) |
| 870 | if !admitted { | 867 | if !admitted { | ||
| 871 | return false | 868 | return false | ||
| 872 | } | 869 | } | ||
| 873 | - | token.effect = ticket | 870 | + | defer effect.End() |
| 874 | - | defer token.endAttachmentEffect() | |||
| 875 | if d.afterAttachmentEffectAdmitted != nil { | 871 | if d.afterAttachmentEffectAdmitted != nil { | ||
| 876 | - | d.afterAttachmentEffectAdmitted(token) | 872 | + | d.afterAttachmentEffectAdmitted(effect.capability()) |
| 877 | } | 873 | } | ||
| 878 | - | if token.rebase { | 874 | + | if effect.rebase { |
| 879 | if d.beforeFirstPaintSendWait != nil { | 875 | if d.beforeFirstPaintSendWait != nil { | ||
| 880 | - | d.beforeFirstPaintSendWait(token) | 876 | + | d.beforeFirstPaintSendWait(effect.capability()) |
| 881 | } | 877 | } | ||
| 882 | - | token.ac.sendMu.Lock() | 878 | + | effect.ac.sendMu.Lock() |
| 883 | - | if token.ac.renderStages.handoffRebase != nil { | 879 | + | if effect.ac.renderStages.handoffRebase != nil { |
| 884 | - | token.ac.renderStages.handoffRebase() | 880 | + | effect.ac.renderStages.handoffRebase() |
| 885 | } | 881 | } | ||
| 886 | - | token.ac.rebaseOutput() | 882 | + | effect.ac.rebaseOutput() |
| 887 | // Captures belong to the old session even when pane-local IDs happen | 883 | // Captures belong to the old session even when pane-local IDs happen | ||
| 888 | // to be reused by the destination. | 884 | // to be reused by the destination. | ||
| 889 | - | token.ac.captureFrames = nil | 885 | + | effect.ac.captureFrames = nil |
| 890 | - | token.ac.sendMu.Unlock() | 886 | + | effect.ac.sendMu.Unlock() |
| 891 | } | 887 | } | ||
| 892 | - | if token.sess == nil { | 888 | + | if effect.sess == nil { |
| 893 | return false | 889 | return false | ||
| 894 | } | 890 | } | ||
| 895 | - | d.firstPaintWithLease(token.sess, token.ac, token.lease, true) | 891 | + | d.firstPaintWithLease(effect.sess, effect.ac, effect.lease, true) |
| 896 | return true | 892 | return true | ||
| 897 | } | 893 | } | ||
| 898 | 894 | ||||
| @@ -958,25 +954,25 @@ func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg ports.Theme) | |||||
| 958 | d.invalidateRender(sess, ac, true, "client.go") | 954 | d.invalidateRender(sess, ac, true, "client.go") | ||
| 959 | } | 955 | } | ||
| 960 | 956 | ||||
| 961 | - | func (d *Daemon) applyThemeForAttachment(token attachmentConnectionToken, msg ports.Theme) { | 957 | + | func (d *Daemon) applyThemeForAttachment(effect *attachmentEffect, msg ports.Theme) { |
| 962 | - | if !token.attachmentEffectCurrent() { | 958 | + | if !effect.current() { |
| 963 | return | 959 | return | ||
| 964 | } | 960 | } | ||
| 965 | clientTheme := themeFromMessage(msg) | 961 | clientTheme := themeFromMessage(msg) | ||
| 966 | - | token.ac.setClientTheme(clientTheme) | 962 | + | effect.ac.setClientTheme(clientTheme) |
| 967 | - | sess := token.sess | 963 | + | sess := effect.sess |
| 968 | - | if sess == nil || !token.attachmentEffectCurrent() || !d.applyHostTheme(sess, token.ac, clientTheme, false) { | 964 | + | if sess == nil || !effect.current() || !d.applyHostTheme(sess, effect.ac, clientTheme, false) { |
| 969 | return | 965 | return | ||
| 970 | } | 966 | } | ||
| 971 | - | if !token.attachmentEffectCurrent() { | 967 | + | if !effect.current() { |
| 972 | return | 968 | return | ||
| 973 | } | 969 | } | ||
| 974 | rc := sess.renderCoordinator() | 970 | rc := sess.renderCoordinator() | ||
| 975 | - | if token.lease == nil || rc == nil { | 971 | + | if effect.lease == nil || rc == nil { |
| 976 | - | d.invalidateRender(sess, token.ac, true, "client.go") | 972 | + | d.invalidateRender(sess, effect.ac, true, "client.go") |
| 977 | return | 973 | return | ||
| 978 | } | 974 | } | ||
| 979 | - | rc.invalidateForLease(token.ac, token.lease, renderInvalidation{class: invalidateUrgent, reset: true, producer: "client.go"}) | 975 | + | rc.invalidateForLease(effect.ac, effect.lease, renderInvalidation{class: invalidateUrgent, reset: true, producer: "client.go"}) |
| 980 | } | 976 | } | ||
| 981 | 977 | ||||
| 982 | func (d *Daemon) resize(sess *session, ac *attachedClient, sz domain.Size) { | 978 | func (d *Daemon) resize(sess *session, ac *attachedClient, sz domain.Size) { | ||
| @@ -987,26 +983,26 @@ func (d *Daemon) resize(sess *session, ac *attachedClient, sz domain.Size) { | |||||
| 987 | // attachment the latest shared tab/PTY geometry claimant. Callers hold the | 983 | // attachment the latest shared tab/PTY geometry claimant. Callers hold the | ||
| 988 | // attachment effect admission; output state is rebased before any redraw so | 984 | // attachment effect admission; output state is rebased before any redraw so | ||
| 989 | // every subsequent frame starts a new epoch. | 985 | // every subsequent frame starts a new epoch. | ||
| 990 | - | func (d *Daemon) resizeAttachmentForLease(token attachmentConnectionToken, size domain.Size) bool { | 986 | + | func (d *Daemon) resizeAttachmentForLease(effect *attachmentEffect, size domain.Size) bool { |
| 991 | geometry := domain.Geometry{Size: size} | 987 | geometry := domain.Geometry{Size: size} | ||
| 992 | - | if token.ac != nil { | 988 | + | if effect != nil && effect.ac != nil { |
| 993 | // Direct size-only callers do not have a fresh pixel pair. Preserve the | 989 | // Direct size-only callers do not have a fresh pixel pair. Preserve the | ||
| 994 | // attachment's last complete pair instead of downgrading its claim to | 990 | // attachment's last complete pair instead of downgrading its claim to | ||
| 995 | // cell-only geometry. | 991 | // cell-only geometry. | ||
| 996 | - | geometry = token.ac.geometrySnapshot() | 992 | + | geometry = effect.ac.geometrySnapshot() |
| 997 | geometry.Size = size | 993 | geometry.Size = size | ||
| 998 | } | 994 | } | ||
| 999 | - | return d.resizeAttachmentGeometryForLease(token, geometry) | 995 | + | return d.resizeAttachmentGeometryForLease(effect, geometry) |
| 1000 | } | 996 | } | ||
| 1001 | 997 | ||||
| 1002 | - | func (d *Daemon) resizeAttachmentGeometryForLease(token attachmentConnectionToken, geometry domain.Geometry) bool { | 998 | + | func (d *Daemon) resizeAttachmentGeometryForLease(effect *attachmentEffect, geometry domain.Geometry) bool { |
| 1003 | geometry = geometry.NormalizePixels() | 999 | geometry = geometry.NormalizePixels() | ||
| 1004 | - | if token.ac == nil || !geometry.Valid() || !token.attachmentEffectCurrent() { | 1000 | + | if !effect.current() || effect.ac == nil || !geometry.Valid() { |
| 1005 | return false | 1001 | return false | ||
| 1006 | } | 1002 | } | ||
| 1007 | - | ac := token.ac | 1003 | + | ac := effect.ac |
| 1008 | ac.sendMu.Lock() | 1004 | ac.sendMu.Lock() | ||
| 1009 | - | if !token.attachmentEffectCurrent() || !ac.transportSnapshotCurrent(token.transport) { | 1005 | + | if !effect.current() || !ac.transportSnapshotCurrent(effect.transport) { |
| 1010 | ac.sendMu.Unlock() | 1006 | ac.sendMu.Unlock() | ||
| 1011 | return false | 1007 | return false | ||
| 1012 | } | 1008 | } | ||
| @@ -1036,7 +1032,7 @@ func (d *Daemon) resizeAttachmentGeometryForLease(token attachmentConnectionToke | |||||
| 1036 | d.registerPreviewForSelection(ac) | 1032 | d.registerPreviewForSelection(ac) | ||
| 1037 | } | 1033 | } | ||
| 1038 | 1034 | ||||
| 1039 | - | sess := token.sess | 1035 | + | sess := effect.sess |
| 1040 | if sess == nil { | 1036 | if sess == nil { | ||
| 1041 | return false | 1037 | return false | ||
| 1042 | } | 1038 | } | ||
| @@ -1056,8 +1052,8 @@ func (d *Daemon) resizeAttachmentGeometryForLease(token attachmentConnectionToke | |||||
| 1056 | } | 1052 | } | ||
| 1057 | // Rendering is attachment-scoped. Schedule it through the coordinator so | 1053 | // Rendering is attachment-scoped. Schedule it through the coordinator so | ||
| 1058 | // rapid resize frames coalesce under the attachment lease and deadline. | 1054 | // rapid resize frames coalesce under the attachment lease and deadline. | ||
| 1059 | - | return rc.scheduleResizeForLease(geometry.Size, ac, token.lease, func(uint64) { | 1055 | + | return rc.scheduleResizeForLease(geometry.Size, ac, effect.lease, func(uint64) { |
| 1060 | - | go d.paint(sess, ac, true, token.lease) | 1056 | + | go d.paint(sess, ac, true, effect.lease) |
| 1061 | }) != 0 | 1057 | }) != 0 | ||
| 1062 | } | 1058 | } | ||
| 1063 | 1059 | ||||
| @@ -1071,37 +1067,42 @@ func (d *Daemon) handleClientNotice(sess *session, ac *attachedClient, notice po | |||||
| 1071 | return | 1067 | return | ||
| 1072 | } | 1068 | } | ||
| 1073 | tr := ac.transport() | 1069 | tr := ac.transport() | ||
| 1074 | - | token := sess.attachmentToken(ac, tr) | 1070 | + | token := sess.captureAttachmentCapability(ac, tr) |
| 1075 | if token.lease == nil { | 1071 | if token.lease == nil { | ||
| 1076 | // Direct test/headless callers retain pointer-based routing. Production | 1072 | // Direct test/headless callers retain pointer-based routing. Production | ||
| 1077 | // client frames always carry the exact lease through the attachment path below. | 1073 | // client frames always carry the exact lease through the attachment path below. | ||
| 1078 | d.handleClientNoticeDirect(sess, ac, notice) | 1074 | d.handleClientNoticeDirect(sess, ac, notice) | ||
| 1079 | return | 1075 | return | ||
| 1080 | } | 1076 | } | ||
| 1081 | - | d.handleClientNoticeForAttachment(token, notice) | 1077 | + | effect, admitted := ac.beginAttachmentEffect(token) |
| 1078 | + | if !admitted { | |||
| 1079 | + | return | |||
| 1080 | + | } | |||
| 1081 | + | defer effect.End() | |||
| 1082 | + | d.handleClientNoticeForAttachment(effect, notice) | |||
| 1082 | } | 1083 | } | ||
| 1083 | 1084 | ||||
| 1084 | - | func (d *Daemon) handleClientNoticeForAttachment(token attachmentConnectionToken, notice ports.ClientNotice) { | 1085 | + | func (d *Daemon) handleClientNoticeForAttachment(effect *attachmentEffect, notice ports.ClientNotice) { |
| 1085 | d.notices.routingMu.Lock() | 1086 | d.notices.routingMu.Lock() | ||
| 1086 | - | if !token.attachmentEffectCurrent() { | 1087 | + | if !effect.current() { |
| 1087 | d.notices.routingMu.Unlock() | 1088 | d.notices.routingMu.Unlock() | ||
| 1088 | return | 1089 | return | ||
| 1089 | } | 1090 | } | ||
| 1090 | if d.notices.beforeClientNoticeMutation != nil { | 1091 | if d.notices.beforeClientNoticeMutation != nil { | ||
| 1091 | d.notices.beforeClientNoticeMutation() | 1092 | d.notices.beforeClientNoticeMutation() | ||
| 1092 | } | 1093 | } | ||
| 1093 | - | if !token.attachmentEffectCurrent() { | 1094 | + | if !effect.current() { |
| 1094 | d.notices.routingMu.Unlock() | 1095 | d.notices.routingMu.Unlock() | ||
| 1095 | return | 1096 | return | ||
| 1096 | } | 1097 | } | ||
| 1097 | - | if token.sess == nil { | 1098 | + | if effect.sess == nil { |
| 1098 | d.notices.routingMu.Unlock() | 1099 | d.notices.routingMu.Unlock() | ||
| 1099 | return | 1100 | return | ||
| 1100 | } | 1101 | } | ||
| 1101 | - | repaint := d.mutateClientNotice(token.sess, token.ac, notice) | 1102 | + | repaint := d.mutateClientNotice(effect.sess, effect.ac, notice) |
| 1102 | d.notices.routingMu.Unlock() | 1103 | d.notices.routingMu.Unlock() | ||
| 1103 | - | if repaint && token.attachmentEffectCurrent() { | 1104 | + | if repaint && effect.current() { |
| 1104 | - | d.repaintForNotice(token.ac) | 1105 | + | d.repaintForNotice(effect.ac) |
| 1105 | } | 1106 | } | ||
| 1106 | } | 1107 | } | ||
| 1107 | 1108 | ||||
M internal/usecase/daemon/client_frame_routing.go +67 -74
| @@ -14,26 +14,26 @@ const connectionSnapshotAttempts = 8 | |||||
| 14 | // currentAttachmentConnection retries the lock-free session-to-role snapshot when a | 14 | // currentAttachmentConnection retries the lock-free session-to-role snapshot when a | ||
| 15 | // handoff lands between those reads. The attachment gate still performs the final | 15 | // handoff lands between those reads. The attachment gate still performs the final | ||
| 16 | // mutation admission after this function returns. | 16 | // mutation admission after this function returns. | ||
| 17 | - | func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.Transport) (*session, attachmentConnectionToken, bool) { | 17 | + | func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.Transport) (*session, attachmentCapability, bool) { |
| 18 | for range connectionSnapshotAttempts { | 18 | for range connectionSnapshotAttempts { | ||
| 19 | if !ac.currentTransportIs(tr) { | 19 | if !ac.currentTransportIs(tr) { | ||
| 20 | break | 20 | break | ||
| 21 | } | 21 | } | ||
| 22 | sess := ac.currentAttachmentSession() | 22 | sess := ac.currentAttachmentSession() | ||
| 23 | if sess == nil { | 23 | if sess == nil { | ||
| 24 | - | return nil, attachmentConnectionToken{}, false | 24 | + | return nil, attachmentCapability{}, false |
| 25 | } | 25 | } | ||
| 26 | if d.afterConnectionSessionSnapshot != nil { | 26 | if d.afterConnectionSessionSnapshot != nil { | ||
| 27 | d.afterConnectionSessionSnapshot(sess) | 27 | d.afterConnectionSessionSnapshot(sess) | ||
| 28 | } | 28 | } | ||
| 29 | - | token := attachmentToken(sess, ac, tr) | 29 | + | token := captureAttachmentCapability(sess, ac, tr) |
| 30 | if token.ac == nil || ac.currentAttachmentSession() != sess { | 30 | if token.ac == nil || ac.currentAttachmentSession() != sess { | ||
| 31 | runtime.Gosched() | 31 | runtime.Gosched() | ||
| 32 | continue | 32 | continue | ||
| 33 | } | 33 | } | ||
| 34 | return sess, token, true | 34 | return sess, token, true | ||
| 35 | } | 35 | } | ||
| 36 | - | return nil, attachmentConnectionToken{}, false | 36 | + | return nil, attachmentCapability{}, false |
| 37 | } | 37 | } | ||
| 38 | 38 | ||||
| 39 | // runConnLoop is the per-connection input router: it pumps client messages | 39 | // runConnLoop is the per-connection input router: it pumps client messages | ||
| @@ -74,57 +74,56 @@ func (d *Daemon) runConnLoop(ac *attachedClient) { | |||||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | // handleAttachmentClientFrame owns the attached-client protocol. | 76 | // handleAttachmentClientFrame owns the attached-client protocol. | ||
| 77 | - | func (d *Daemon) handleAttachmentClientFrame(token attachmentConnectionToken, f ports.Frame) bool { | 77 | + | func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f ports.Frame) bool { |
| 78 | - | if !token.attachmentEffectCurrent() { | 78 | + | if !capability.current() { |
| 79 | return false | 79 | return false | ||
| 80 | } | 80 | } | ||
| 81 | if d.afterAttachmentFrameDispatch != nil { | 81 | if d.afterAttachmentFrameDispatch != nil { | ||
| 82 | - | d.afterAttachmentFrameDispatch(token) | 82 | + | d.afterAttachmentFrameDispatch(capability) |
| 83 | } | 83 | } | ||
| 84 | - | ticket, admitted := token.ac.beginAttachmentEffect(token) | 84 | + | effect, admitted := capability.ac.beginAttachmentEffect(capability) |
| 85 | if !admitted { | 85 | if !admitted { | ||
| 86 | return false | 86 | return false | ||
| 87 | } | 87 | } | ||
| 88 | - | token.effect = ticket | 88 | + | defer effect.End() |
| 89 | - | defer token.endAttachmentEffect() | |||
| 90 | if d.afterAttachmentEffectAdmitted != nil { | 89 | if d.afterAttachmentEffectAdmitted != nil { | ||
| 91 | - | d.afterAttachmentEffectAdmitted(token) | 90 | + | d.afterAttachmentEffectAdmitted(effect.capability()) |
| 92 | } | 91 | } | ||
| 93 | switch f.Type { | 92 | switch f.Type { | ||
| 94 | case ports.MsgInput: | 93 | case ports.MsgInput: | ||
| 95 | if in, derr := ports.UnmarshalInput(f.Payload); derr == nil { | 94 | if in, derr := ports.UnmarshalInput(f.Payload); derr == nil { | ||
| 96 | - | d.handleSequencedInputForAttachment(token, in.InputSeq, in.Data) | 95 | + | d.handleSequencedInputForAttachment(effect, in.InputSeq, in.Data) |
| 97 | } | 96 | } | ||
| 98 | case ports.MsgResize: | 97 | case ports.MsgResize: | ||
| 99 | - | if rz, derr := ports.UnmarshalResize(f.Payload); derr == nil && token.attachmentEffectCurrent() { | 98 | + | if rz, derr := ports.UnmarshalResize(f.Payload); derr == nil && effect.current() { |
| 100 | - | d.resizeAttachmentGeometryForLease(token, rz.Geometry()) | 99 | + | d.resizeAttachmentGeometryForLease(effect, rz.Geometry()) |
| 101 | } | 100 | } | ||
| 102 | case ports.MsgTheme: | 101 | case ports.MsgTheme: | ||
| 103 | if th, derr := ports.UnmarshalTheme(f.Payload); derr == nil { | 102 | if th, derr := ports.UnmarshalTheme(f.Payload); derr == nil { | ||
| 104 | - | d.applyThemeForAttachment(token, th) | 103 | + | d.applyThemeForAttachment(effect, th) |
| 105 | } | 104 | } | ||
| 106 | case ports.MsgImagePush: | 105 | case ports.MsgImagePush: | ||
| 107 | if ip, derr := ports.UnmarshalImagePush(f.Payload); derr == nil { | 106 | if ip, derr := ports.UnmarshalImagePush(f.Payload); derr == nil { | ||
| 108 | - | d.handleSequencedImagePushForAttachment(token, ip.InputSeq, ip) | 107 | + | d.handleSequencedImagePushForAttachment(effect, ip.InputSeq, ip) |
| 109 | } | 108 | } | ||
| 110 | case ports.MsgClientNotice: | 109 | case ports.MsgClientNotice: | ||
| 111 | if notice, derr := ports.UnmarshalClientNotice(f.Payload); derr == nil { | 110 | if notice, derr := ports.UnmarshalClientNotice(f.Payload); derr == nil { | ||
| 112 | - | d.handleClientNoticeForAttachment(token, notice) | 111 | + | d.handleClientNoticeForAttachment(effect, notice) |
| 113 | } else { | 112 | } else { | ||
| 114 | d.log.Warn("malformed client notice", "err", derr) | 113 | d.log.Warn("malformed client notice", "err", derr) | ||
| 115 | } | 114 | } | ||
| 116 | case ports.MsgDetach: | 115 | case ports.MsgDetach: | ||
| 117 | - | if token.attachmentEffectCurrent() { | 116 | + | if effect.current() { |
| 118 | - | d.clientGoneForAttachment(token, true) | 117 | + | d.clientGoneForAttachment(effect, true) |
| 119 | return true | 118 | return true | ||
| 120 | } | 119 | } | ||
| 121 | case ports.MsgAck: | 120 | case ports.MsgAck: | ||
| 122 | if ack, derr := ports.UnmarshalAck(f.Payload); derr == nil { | 121 | if ack, derr := ports.UnmarshalAck(f.Payload); derr == nil { | ||
| 123 | - | d.ackOutput(token, ack.Epoch, ack.State) | 122 | + | d.ackOutput(effect, ack.Epoch, ack.State) |
| 124 | } | 123 | } | ||
| 125 | case ports.MsgOutputResetRequest: | 124 | case ports.MsgOutputResetRequest: | ||
| 126 | if _, derr := ports.UnmarshalOutputResetRequest(f.Payload); derr == nil { | 125 | if _, derr := ports.UnmarshalOutputResetRequest(f.Payload); derr == nil { | ||
| 127 | - | d.resetOutput(token) | 126 | + | d.resetOutput(effect) |
| 128 | } | 127 | } | ||
| 129 | case ports.MsgSamePeerSwitchRequest: | 128 | case ports.MsgSamePeerSwitchRequest: | ||
| 130 | request, derr := ports.UnmarshalSamePeerSwitchRequest(f.Payload) | 129 | request, derr := ports.UnmarshalSamePeerSwitchRequest(f.Payload) | ||
| @@ -132,27 +131,27 @@ func (d *Daemon) handleAttachmentClientFrame(token attachmentConnectionToken, f | |||||
| 132 | d.log.Warn("malformed same-peer switch request", "err", derr) | 131 | d.log.Warn("malformed same-peer switch request", "err", derr) | ||
| 133 | break | 132 | break | ||
| 134 | } | 133 | } | ||
| 135 | - | d.switchSamePeerForAttachment(token, request) | 134 | + | d.switchSamePeerForAttachment(effect, request) |
| 136 | case ports.MsgParkedRouteRequest: | 135 | case ports.MsgParkedRouteRequest: | ||
| 137 | request, derr := ports.UnmarshalParkedRouteRequest(f.Payload) | 136 | request, derr := ports.UnmarshalParkedRouteRequest(f.Payload) | ||
| 138 | if derr != nil { | 137 | if derr != nil { | ||
| 139 | d.log.Warn("malformed parked-route request", "err", derr) | 138 | d.log.Warn("malformed parked-route request", "err", derr) | ||
| 140 | break | 139 | break | ||
| 141 | } | 140 | } | ||
| 142 | - | d.handleParkedRouteRequest(token, request) | 141 | + | d.handleParkedRouteRequest(effect, request) |
| 143 | case ports.MsgRecentRouteSnapshot: | 142 | case ports.MsgRecentRouteSnapshot: | ||
| 144 | snapshot, derr := ports.UnmarshalRecentRouteSnapshot(f.Payload) | 143 | snapshot, derr := ports.UnmarshalRecentRouteSnapshot(f.Payload) | ||
| 145 | if derr != nil { | 144 | if derr != nil { | ||
| 146 | d.log.Warn("malformed recent route snapshot", "err", derr) | 145 | d.log.Warn("malformed recent route snapshot", "err", derr) | ||
| 147 | break | 146 | break | ||
| 148 | } | 147 | } | ||
| 149 | - | replayIdentity := token.ac.setRouteSnapshot(snapshot) | 148 | + | replayIdentity := effect.ac.setRouteSnapshot(snapshot) |
| 150 | - | d.invalidateRender(token.sess, token.ac, false, "client_frame_routing.go:route-snapshot") | 149 | + | d.invalidateRender(effect.sess, effect.ac, false, "client_frame_routing.go:route-snapshot") |
| 151 | if !replayIdentity { | 150 | if !replayIdentity { | ||
| 152 | break | 151 | break | ||
| 153 | } | 152 | } | ||
| 154 | - | if err := d.sendCommittedRouteIdentityForAttachment(token); err != nil { | 153 | + | if err := d.sendCommittedRouteIdentityForAttachment(effect); err != nil { |
| 155 | - | d.detachOnAttachmentSendError(token, token.transport.transport) | 154 | + | d.detachOnAttachmentSendError(effect.capability(), effect.transport.transport) |
| 156 | } | 155 | } | ||
| 157 | case ports.MsgRouteAttentionSubscription: | 156 | case ports.MsgRouteAttentionSubscription: | ||
| 158 | subscription, derr := ports.UnmarshalRouteAttentionSubscription(f.Payload) | 157 | subscription, derr := ports.UnmarshalRouteAttentionSubscription(f.Payload) | ||
| @@ -160,8 +159,8 @@ func (d *Daemon) handleAttachmentClientFrame(token attachmentConnectionToken, f | |||||
| 160 | d.log.Warn("malformed route attention subscription", "err", derr) | 159 | d.log.Warn("malformed route attention subscription", "err", derr) | ||
| 161 | break | 160 | break | ||
| 162 | } | 161 | } | ||
| 163 | - | token.ac.setRouteAttentionSubscription(subscription) | 162 | + | effect.ac.setRouteAttentionSubscription(subscription) |
| 164 | - | d.invalidateRender(token.sess, token.ac, false, "client_frame_routing.go:route-attention") | 163 | + | d.invalidateRender(effect.sess, effect.ac, false, "client_frame_routing.go:route-attention") |
| 165 | case ports.MsgRouteNavigationFailure: | 164 | case ports.MsgRouteNavigationFailure: | ||
| 166 | failure, derr := ports.UnmarshalRouteNavigationFailure(f.Payload) | 165 | failure, derr := ports.UnmarshalRouteNavigationFailure(f.Payload) | ||
| 167 | if derr != nil { | 166 | if derr != nil { | ||
| @@ -181,29 +180,29 @@ func (d *Daemon) handleAttachmentClientFrame(token attachmentConnectionToken, f | |||||
| 181 | case ports.RouteFailureNoSuchRoute: | 180 | case ports.RouteFailureNoSuchRoute: | ||
| 182 | message = "that recent route no longer exists" | 181 | message = "that recent route no longer exists" | ||
| 183 | } | 182 | } | ||
| 184 | - | d.notify(token.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, message, nil) | 183 | + | d.notify(effect.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, message, nil) |
| 185 | case ports.MsgCommand: | 184 | case ports.MsgCommand: | ||
| 186 | request, derr := ports.UnmarshalCommandRequest(f.Payload) | 185 | request, derr := ports.UnmarshalCommandRequest(f.Payload) | ||
| 187 | if derr != nil { | 186 | if derr != nil { | ||
| 188 | return false | 187 | return false | ||
| 189 | } | 188 | } | ||
| 190 | - | result := d.executeAttachedCommand(token, request) | 189 | + | result := d.executeAttachedCommand(effect, request) |
| 191 | - | resultToken, ok := d.commandResultToken(token) | 190 | + | resultEffect, ok := d.commandResultEffect(effect) |
| 192 | if !ok { | 191 | if !ok { | ||
| 193 | // A remote handoff or explicit detach deliberately retired the old | 192 | // A remote handoff or explicit detach deliberately retired the old | ||
| 194 | // attachment; it must not be treated as a stale-result send error. | 193 | // attachment; it must not be treated as a stale-result send error. | ||
| 195 | return true | 194 | return true | ||
| 196 | } | 195 | } | ||
| 197 | - | if err := resultToken.sendControl(frameCommandResult(result)); err != nil { | 196 | + | if err := resultEffect.sendControl(frameCommandResult(result)); err != nil { |
| 198 | - | resultToken.endAttachmentEffect() | 197 | + | resultEffect.End() |
| 199 | - | d.detachOnAttachmentSendError(resultToken, resultToken.transport.transport) | 198 | + | d.detachOnAttachmentSendError(resultEffect.capability(), resultEffect.transport.transport) |
| 200 | return true | 199 | return true | ||
| 201 | } | 200 | } | ||
| 202 | - | resultToken.endAttachmentEffect() | 201 | + | resultEffect.End() |
| 203 | case ports.MsgPing: | 202 | case ports.MsgPing: | ||
| 204 | - | if err := token.sendControl(framePong()); err != nil { | 203 | + | if err := effect.sendControl(framePong()); err != nil { |
| 205 | - | token.endAttachmentEffect() | 204 | + | effect.End() |
| 206 | - | d.detachOnAttachmentSendError(token, token.transport.transport) | 205 | + | d.detachOnAttachmentSendError(effect.capability(), effect.transport.transport) |
| 207 | return true | 206 | return true | ||
| 208 | } | 207 | } | ||
| 209 | default: | 208 | default: | ||
| @@ -213,34 +212,28 @@ func (d *Daemon) handleAttachmentClientFrame(token attachmentConnectionToken, f | |||||
| 213 | return false | 212 | return false | ||
| 214 | } | 213 | } | ||
| 215 | 214 | ||||
| 216 | - | // commandResultToken renews the attachment effect after a local navigation | 215 | + | // commandResultEffect renews admission after a local navigation transition |
| 217 | - | // transition ends the command's initiating effect. A retired transport/session | 216 | + | // ends the command's initiating effect. A retired Transport or Session means |
| 218 | - | // means the command deliberately handed off or detached and has no result to | 217 | + | // the command deliberately handed off or detached and has no result carriage. |
| 219 | - | // send on the old carriage. | 218 | + | func (d *Daemon) commandResultEffect(effect *attachmentEffect) (*attachmentEffect, bool) { |
| 220 | - | func (d *Daemon) commandResultToken(token attachmentConnectionToken) (attachmentConnectionToken, bool) { | 219 | + | if effect == nil || effect.ac == nil || !effect.ac.currentTransportIs(effect.transport.transport) { |
| 221 | - | if token.ac == nil || !token.ac.currentTransportIs(token.transport.transport) { | 220 | + | return nil, false |
| 222 | - | return attachmentConnectionToken{}, false | |||
| 223 | } | 221 | } | ||
| 224 | - | sess := token.ac.currentAttachmentSession() | 222 | + | if effect.current() && effect.attachmentCapability.current() { |
| 225 | - | if sess == nil { | 223 | + | return effect, true |
| 226 | - | return attachmentConnectionToken{}, false | |||
| 227 | - | } | |||
| 228 | - | resultToken := attachmentToken(sess, token.ac, token.transport.transport) | |||
| 229 | - | if resultToken.ac == nil { | |||
| 230 | - | return attachmentConnectionToken{}, false | |||
| 231 | } | 224 | } | ||
| 232 | - | if token.effect != nil && !token.effect.ended.Load() && token.current() { | 225 | + | sess := effect.ac.currentAttachmentSession() |
| 233 | - | return token, true | 226 | + | if sess == nil { |
| 227 | + | return nil, false | |||
| 234 | } | 228 | } | ||
| 235 | - | ticket, admitted := token.ac.beginAttachmentEffect(resultToken) | 229 | + | capability := captureAttachmentCapability(sess, effect.ac, effect.transport.transport) |
| 236 | - | if !admitted { | 230 | + | if capability.ac == nil { |
| 237 | - | return attachmentConnectionToken{}, false | 231 | + | return nil, false |
| 238 | } | 232 | } | ||
| 239 | - | resultToken.effect = ticket | 233 | + | return effect.ac.beginAttachmentEffect(capability) |
| 240 | - | return resultToken, true | |||
| 241 | } | 234 | } | ||
| 242 | 235 | ||||
| 243 | - | func (d *Daemon) executeAttachedCommand(token attachmentConnectionToken, request ports.CommandRequest) ports.CommandResult { | 236 | + | func (d *Daemon) executeAttachedCommand(effect *attachmentEffect, request ports.CommandRequest) ports.CommandResult { |
| 244 | result := ports.CommandResult{RequestID: request.RequestID} | 237 | result := ports.CommandResult{RequestID: request.RequestID} | ||
| 245 | if request.Version != ports.ProtocolVersion { | 238 | if request.Version != ports.ProtocolVersion { | ||
| 246 | result.Code = ports.ErrInvalidCommandArgs | 239 | result.Code = ports.ErrInvalidCommandArgs | ||
| @@ -267,8 +260,8 @@ func (d *Daemon) executeAttachedCommand(token attachmentConnectionToken, request | |||||
| 267 | result.Text = "attached commands cannot override their active session target" | 260 | result.Text = "attached commands cannot override their active session target" | ||
| 268 | return result | 261 | return result | ||
| 269 | } | 262 | } | ||
| 270 | - | sess := token.sess | 263 | + | sess := effect.sess |
| 271 | - | if sess == nil || !token.attachmentEffectCurrent() { | 264 | + | if sess == nil || !effect.current() { |
| 272 | result.Code = ports.ErrNoSuchTarget | 265 | result.Code = ports.ErrNoSuchTarget | ||
| 273 | result.Text = "attached session is no longer active" | 266 | result.Text = "attached session is no longer active" | ||
| 274 | return result | 267 | return result | ||
| @@ -292,7 +285,7 @@ func (d *Daemon) executeAttachedCommand(token attachmentConnectionToken, request | |||||
| 292 | // The frame's attachment token is the target capability. No registry/name lookup is | 285 | // The frame's attachment token is the target capability. No registry/name lookup is | ||
| 293 | // performed, so the command cannot escape to another remote session. | 286 | // performed, so the command cannot escape to another remote session. | ||
| 294 | err := sess.runMutation(func() error { | 287 | err := sess.runMutation(func() error { | ||
| 295 | - | return cmd.Run(paletteExec{d: d, sess: sess, ac: token.ac, effect: token.effect}, request.Args) | 288 | + | return cmd.Run(paletteExec{d: d, sess: sess, ac: effect.ac, effect: effect}, request.Args) |
| 296 | }) | 289 | }) | ||
| 297 | if err == nil { | 290 | if err == nil { | ||
| 298 | result.OK = true | 291 | result.OK = true | ||
| @@ -311,13 +304,13 @@ func (d *Daemon) executeAttachedCommand(token attachmentConnectionToken, request | |||||
| 311 | // resetOutput rebases only the exact attachment and transport admitted | 304 | // resetOutput rebases only the exact attachment and transport admitted | ||
| 312 | // by the frame router. sendMu serializes the rebase with every output send; | 305 | // by the frame router. sendMu serializes the rebase with every output send; | ||
| 313 | // transport revalidation under that lock rejects a link replaced while waiting. | 306 | // transport revalidation under that lock rejects a link replaced while waiting. | ||
| 314 | - | func (d *Daemon) resetOutput(token attachmentConnectionToken) bool { | 307 | + | func (d *Daemon) resetOutput(effect *attachmentEffect) bool { |
| 315 | - | ac := token.ac | 308 | + | if !effect.current() || effect.ac == nil { |
| 316 | - | if ac == nil || token.effect == nil || token.effect.ended.Load() { | |||
| 317 | return false | 309 | return false | ||
| 318 | } | 310 | } | ||
| 311 | + | ac := effect.ac | |||
| 319 | ac.sendMu.Lock() | 312 | ac.sendMu.Lock() | ||
| 320 | - | if token.effect.ended.Load() || !token.attachmentCurrent() || ac.output == nil { | 313 | + | if !effect.current() || ac.output == nil { |
| 321 | ac.sendMu.Unlock() | 314 | ac.sendMu.Unlock() | ||
| 322 | return false | 315 | return false | ||
| 323 | } | 316 | } | ||
| @@ -325,15 +318,15 @@ func (d *Daemon) resetOutput(token attachmentConnectionToken) bool { | |||||
| 325 | ac.pipelineCache = composeCacheInput{} | 318 | ac.pipelineCache = composeCacheInput{} | ||
| 326 | ac.pipelineScratch = composeCacheInput{} | 319 | ac.pipelineScratch = composeCacheInput{} | ||
| 327 | ac.sendMu.Unlock() | 320 | ac.sendMu.Unlock() | ||
| 328 | - | go d.paint(token.sess, ac, true, token.lease) | 321 | + | go d.paint(effect.sess, ac, true, effect.lease) |
| 329 | return true | 322 | return true | ||
| 330 | } | 323 | } | ||
| 331 | 324 | ||||
| 332 | - | func (d *Daemon) ackOutput(token attachmentConnectionToken, epoch, state uint64) bool { | 325 | + | func (d *Daemon) ackOutput(effect *attachmentEffect, epoch, state uint64) bool { |
| 333 | - | ac := token.ac | 326 | + | if !effect.current() || effect.ac == nil { |
| 334 | - | if token.effect == nil || token.effect.ended.Load() { | |||
| 335 | return false | 327 | return false | ||
| 336 | } | 328 | } | ||
| 329 | + | ac := effect.ac | |||
| 337 | ac.sendMu.Lock() | 330 | ac.sendMu.Lock() | ||
| 338 | if ac.output == nil { | 331 | if ac.output == nil { | ||
| 339 | ac.sendMu.Unlock() | 332 | ac.sendMu.Unlock() | ||
| @@ -342,8 +335,8 @@ func (d *Daemon) ackOutput(token attachmentConnectionToken, epoch, state uint64) | |||||
| 342 | acknowledged := ac.output.ack(epoch, state) | 335 | acknowledged := ac.output.ack(epoch, state) | ||
| 343 | ac.sendMu.Unlock() | 336 | ac.sendMu.Unlock() | ||
| 344 | if acknowledged { | 337 | if acknowledged { | ||
| 345 | - | if rc := token.sess.renderCoordinator(); rc != nil { | 338 | + | if rc := effect.sess.renderCoordinator(); rc != nil { |
| 346 | - | rc.notifyAckForLease(token.lease) | 339 | + | rc.notifyAckForLease(effect.lease) |
| 347 | } | 340 | } | ||
| 348 | } | 341 | } | ||
| 349 | return acknowledged | 342 | return acknowledged | ||
M internal/usecase/daemon/client_transport_errors.go +13 -12
| @@ -39,25 +39,26 @@ func (d *Daemon) clientGoneWithNotice(sess *session, ac *attachedClient, failed | |||||
| 39 | d.finishClientGone(sess, ac, failed, explicit, notice) | 39 | d.finishClientGone(sess, ac, failed, explicit, notice) | ||
| 40 | } | 40 | } | ||
| 41 | 41 | ||||
| 42 | - | func (d *Daemon) clientGoneForAttachment(token attachmentConnectionToken, explicit bool) bool { | 42 | + | func (d *Daemon) clientGoneForAttachment(effect *attachmentEffect, explicit bool) bool { |
| 43 | - | if token.effect == nil { | 43 | + | if effect == nil { |
| 44 | return false | 44 | return false | ||
| 45 | } | 45 | } | ||
| 46 | - | token.effect.bindActionEnd(d, "detach") | 46 | + | effect.bindActionEnd(d, "detach") |
| 47 | - | token.effect.End() | 47 | + | effect.End() |
| 48 | - | sess := token.sess | 48 | + | capability := effect.capability() |
| 49 | + | sess := capability.sess | |||
| 49 | if sess == nil { | 50 | if sess == nil { | ||
| 50 | return false | 51 | return false | ||
| 51 | } | 52 | } | ||
| 52 | var parkingToken uint64 | 53 | var parkingToken uint64 | ||
| 53 | if !explicit { | 54 | if !explicit { | ||
| 54 | - | parkingToken = d.markParkingInFlight(sess, token.ac) | 55 | + | parkingToken = d.markParkingInFlight(sess, capability.ac) |
| 55 | } | 56 | } | ||
| 56 | - | if !d.detachIfAttachmentCurrent(token) { | 57 | + | if !d.detachIfAttachmentCurrent(capability) { |
| 57 | - | d.clearParkingInFlightIfAbandoned(sess, token.ac, parkingToken) | 58 | + | d.clearParkingInFlightIfAbandoned(sess, capability.ac, parkingToken) |
| 58 | return false | 59 | return false | ||
| 59 | } | 60 | } | ||
| 60 | - | d.finishClientGone(sess, token.ac, token.transport.transport, explicit, true) | 61 | + | d.finishClientGone(sess, capability.ac, capability.transport.transport, explicit, true) |
| 61 | return true | 62 | return true | ||
| 62 | } | 63 | } | ||
| 63 | 64 | ||||
| @@ -131,11 +132,11 @@ func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed por | |||||
| 131 | d.clearParkingInFlightIfAbandoned(sess, ac, parkingToken) | 132 | d.clearParkingInFlightIfAbandoned(sess, ac, parkingToken) | ||
| 132 | } | 133 | } | ||
| 133 | 134 | ||||
| 134 | - | func (d *Daemon) detachOnAttachmentSendError(token attachmentConnectionToken, failed ports.Transport) { | 135 | + | func (d *Daemon) detachOnAttachmentSendError(token attachmentCapability, failed ports.Transport) { |
| 135 | d.detachOnAttachmentSendErrorUntil(token, failed, nil) | 136 | d.detachOnAttachmentSendErrorUntil(token, failed, nil) | ||
| 136 | } | 137 | } | ||
| 137 | 138 | ||||
| 138 | - | func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentConnectionToken, failed ports.Transport, done func() <-chan struct{}) { | 139 | + | func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentCapability, failed ports.Transport, done func() <-chan struct{}) { |
| 139 | // A delayed sender may report after the client has rebound. Only the exact | 140 | // A delayed sender may report after the client has rebound. Only the exact | ||
| 140 | // transport captured by this attachment is allowed to detach either lifecycle. | 141 | // transport captured by this attachment is allowed to detach either lifecycle. | ||
| 141 | if failed == nil || failed != token.transport.transport { | 142 | if failed == nil || failed != token.transport.transport { | ||
| @@ -156,7 +157,7 @@ func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentConnectionToke | |||||
| 156 | // reserveAttachmentSendErrorCleanup accounts for cleanup before End releases the | 157 | // reserveAttachmentSendErrorCleanup accounts for cleanup before End releases the | ||
| 157 | // attachment gate. This closes the WaitGroup Add/Wait race with terminal teardown; | 158 | // attachment gate. This closes the WaitGroup Add/Wait race with terminal teardown; | ||
| 158 | // the returned launch function must be invoked immediately after ticket End. | 159 | // the returned launch function must be invoked immediately after ticket End. | ||
| 159 | - | func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentConnectionToken, failed ports.Transport) func() { | 160 | + | func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentCapability, failed ports.Transport) func() { |
| 160 | d.attachmentCleanupWg.Add(1) | 161 | d.attachmentCleanupWg.Add(1) | ||
| 161 | return func() { | 162 | return func() { | ||
| 162 | go func() { | 163 | go func() { | ||
M internal/usecase/daemon/clipboard.go +24 -24
| @@ -26,27 +26,27 @@ var ( | |||||
| 26 | clipPasteCloseMarker = []byte("\x1b[201~") | 26 | clipPasteCloseMarker = []byte("\x1b[201~") | ||
| 27 | ) | 27 | ) | ||
| 28 | 28 | ||||
| 29 | - | func (d *Daemon) handleSequencedImagePushForAttachment(token attachmentConnectionToken, _ uint64, ip ports.ImagePush) { | 29 | + | func (d *Daemon) handleSequencedImagePushForAttachment(effect *attachmentEffect, _ uint64, ip ports.ImagePush) { |
| 30 | - | if !token.attachmentEffectCurrent() { | 30 | + | if !effect.current() { |
| 31 | return | 31 | return | ||
| 32 | } | 32 | } | ||
| 33 | - | d.handleImagePushForAttachment(token, ip) | 33 | + | d.handleImagePushForAttachment(effect, ip) |
| 34 | } | 34 | } | ||
| 35 | 35 | ||||
| 36 | - | func (d *Daemon) handleImagePushForAttachment(token attachmentConnectionToken, ip ports.ImagePush) { | 36 | + | func (d *Daemon) handleImagePushForAttachment(effect *attachmentEffect, ip ports.ImagePush) { |
| 37 | - | if len(ip.Data) == 0 || len(ip.Data) > maxImagePushSize || !token.attachmentEffectCurrent() { | 37 | + | if len(ip.Data) == 0 || len(ip.Data) > maxImagePushSize || !effect.current() { |
| 38 | return | 38 | return | ||
| 39 | } | 39 | } | ||
| 40 | - | path, err := d.writeClipboardImageForAttachment(token, ip) | 40 | + | path, err := d.writeClipboardImageForAttachment(effect, ip) |
| 41 | if err != nil { | 41 | if err != nil { | ||
| 42 | d.log.Error("writing clipboard image failed", "err", err) | 42 | d.log.Error("writing clipboard image failed", "err", err) | ||
| 43 | return | 43 | return | ||
| 44 | } | 44 | } | ||
| 45 | - | if !token.attachmentEffectCurrent() { | 45 | + | if !effect.current() { |
| 46 | _ = os.Remove(path) | 46 | _ = os.Remove(path) | ||
| 47 | return | 47 | return | ||
| 48 | } | 48 | } | ||
| 49 | - | d.injectClipboardPathForAttachment(token, path) | 49 | + | d.injectClipboardPathForAttachment(effect, path) |
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | // handleImagePush writes an ImagePush's bytes to a temp file and injects the | 52 | // handleImagePush writes an ImagePush's bytes to a temp file and injects the | ||
| @@ -84,18 +84,18 @@ func (d *Daemon) writeClipboardImage(sess *session, ip ports.ImagePush) (string, | |||||
| 84 | return path, nil | 84 | return path, nil | ||
| 85 | } | 85 | } | ||
| 86 | 86 | ||||
| 87 | - | func (d *Daemon) writeClipboardImageForAttachment(token attachmentConnectionToken, ip ports.ImagePush) (string, error) { | 87 | + | func (d *Daemon) writeClipboardImageForAttachment(effect *attachmentEffect, ip ports.ImagePush) (string, error) { |
| 88 | path, err := d.createClipboardImage(ip) | 88 | path, err := d.createClipboardImage(ip) | ||
| 89 | if err != nil { | 89 | if err != nil { | ||
| 90 | return "", err | 90 | return "", err | ||
| 91 | } | 91 | } | ||
| 92 | - | sess := token.sess | 92 | + | sess := effect.sess |
| 93 | if sess == nil { | 93 | if sess == nil { | ||
| 94 | _ = os.Remove(path) | 94 | _ = os.Remove(path) | ||
| 95 | return "", errAttachmentTransition | 95 | return "", errAttachmentTransition | ||
| 96 | } | 96 | } | ||
| 97 | sess.mu.Lock() | 97 | sess.mu.Lock() | ||
| 98 | - | if !token.attachmentEffectCurrentSessionLocked() { | 98 | + | if !effect.currentSessionLocked() { |
| 99 | sess.mu.Unlock() | 99 | sess.mu.Unlock() | ||
| 100 | _ = os.Remove(path) | 100 | _ = os.Remove(path) | ||
| 101 | return "", errAttachmentTransition | 101 | return "", errAttachmentTransition | ||
| @@ -134,18 +134,18 @@ func (d *Daemon) injectClipboardPath(sess *session, path string) { | |||||
| 134 | d.injectClipboardPathToTarget(sess, path, nil) | 134 | d.injectClipboardPathToTarget(sess, path, nil) | ||
| 135 | } | 135 | } | ||
| 136 | 136 | ||||
| 137 | - | func (d *Daemon) injectClipboardPathForAttachment(token attachmentConnectionToken, path string) { | 137 | + | func (d *Daemon) injectClipboardPathForAttachment(effect *attachmentEffect, path string) { |
| 138 | - | sess := token.sess | 138 | + | sess := effect.sess |
| 139 | - | if sess == nil || !token.attachmentEffectCurrent() { | 139 | + | if sess == nil || !effect.current() { |
| 140 | return | 140 | return | ||
| 141 | } | 141 | } | ||
| 142 | - | d.injectClipboardPathToTarget(sess, path, &token) | 142 | + | d.injectClipboardPathToTarget(sess, path, effect) |
| 143 | } | 143 | } | ||
| 144 | 144 | ||||
| 145 | - | func (d *Daemon) injectClipboardPathToTarget(sess *session, path string, token *attachmentConnectionToken) { | 145 | + | func (d *Daemon) injectClipboardPathToTarget(sess *session, path string, effect *attachmentEffect) { |
| 146 | var ac *attachedClient | 146 | var ac *attachedClient | ||
| 147 | - | if token != nil { | 147 | + | if effect != nil { |
| 148 | - | ac = token.ac | 148 | + | ac = effect.ac |
| 149 | } | 149 | } | ||
| 150 | var tb *tab | 150 | var tb *tab | ||
| 151 | if ac != nil { | 151 | if ac != nil { | ||
| @@ -175,7 +175,7 @@ func (d *Daemon) injectClipboardPathToTarget(sess *session, path string, token * | |||||
| 175 | wrapped = append(wrapped, clipPasteCloseMarker...) | 175 | wrapped = append(wrapped, clipPasteCloseMarker...) | ||
| 176 | data = wrapped | 176 | data = wrapped | ||
| 177 | } | 177 | } | ||
| 178 | - | if token != nil && !token.attachmentEffectCurrent() { | 178 | + | if effect != nil && !effect.current() { |
| 179 | return | 179 | return | ||
| 180 | } | 180 | } | ||
| 181 | d.writeToPane(sess, p, data) | 181 | d.writeToPane(sess, p, data) | ||
| @@ -202,7 +202,7 @@ func clipboardExt(mime string) string { | |||||
| 202 | 202 | ||||
| 203 | type clipboardForward struct { | 203 | type clipboardForward struct { | ||
| 204 | owner paneEffectLease | 204 | owner paneEffectLease | ||
| 205 | - | token attachmentConnectionToken | 205 | + | token attachmentCapability |
| 206 | seq []byte | 206 | seq []byte | ||
| 207 | } | 207 | } | ||
| 208 | 208 | ||||
| @@ -237,8 +237,8 @@ func (d *Daemon) forwardClipboardAsync(owner paneEffectLease, b64 string) { | |||||
| 237 | items := make([]clipboardForward, 0, len(attachments)) | 237 | items := make([]clipboardForward, 0, len(attachments)) | ||
| 238 | for _, ac := range attachments { | 238 | for _, ac := range attachments { | ||
| 239 | transport := ac.transport() | 239 | transport := ac.transport() | ||
| 240 | - | token := sess.attachmentToken(ac, transport) | 240 | + | token := sess.captureAttachmentCapability(ac, transport) |
| 241 | - | if token.attachmentCurrent() { | 241 | + | if token.current() { |
| 242 | items = append(items, clipboardForward{owner: owner, token: token, seq: seq}) | 242 | items = append(items, clipboardForward{owner: owner, token: token, seq: seq}) | ||
| 243 | } | 243 | } | ||
| 244 | } | 244 | } | ||
| @@ -254,7 +254,7 @@ func (d *Daemon) forwardClipboardAsync(owner paneEffectLease, b64 string) { | |||||
| 254 | sess.mu.Unlock() | 254 | sess.mu.Unlock() | ||
| 255 | } | 255 | } | ||
| 256 | 256 | ||||
| 257 | - | func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *attachmentEffectTicket) (ports.Transport, error) { | 257 | + | func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *attachmentEffect) (ports.Transport, error) { |
| 258 | token := item.token | 258 | token := item.token | ||
| 259 | if token.ac == nil || ticket == nil || ticket.ended.Load() || token.transport.transport == nil { | 259 | if token.ac == nil || ticket == nil || ticket.ended.Load() || token.transport.transport == nil { | ||
| 260 | return token.transport.transport, errAttachmentTransition | 260 | return token.transport.transport, errAttachmentTransition | ||
| @@ -300,7 +300,7 @@ func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *atta | |||||
| 300 | // admission and marks the transport interval while ownership publication is | 300 | // admission and marks the transport interval while ownership publication is | ||
| 301 | // excluded. Publication therefore orders either before this check (the send is | 301 | // excluded. Publication therefore orders either before this check (the send is | ||
| 302 | // dropped) or after transport admission (the send belongs to the old owner). | 302 | // dropped) or after transport admission (the send belongs to the old owner). | ||
| 303 | - | func beginClipboardOwnerSend(lease paneEffectLease, ticket *attachmentEffectTicket, expected transportSnapshot) bool { | 303 | + | func beginClipboardOwnerSend(lease paneEffectLease, ticket *attachmentEffect, expected transportSnapshot) bool { |
| 304 | if lease.pane == nil || lease.owner == nil || lease.owner.session == nil || lease.owner.tab == nil { | 304 | if lease.pane == nil || lease.owner == nil || lease.owner.session == nil || lease.owner.tab == nil { | ||
| 305 | return false | 305 | return false | ||
| 306 | } | 306 | } | ||
M internal/usecase/daemon/clipboard_test.go +3 -3
| @@ -204,9 +204,9 @@ func chunkReadPTY(t *testing.T, chunks ...[]byte) *portsmocks.MockPTY { | |||||
| 204 | 204 | ||||
| 205 | func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.Transport) { | 205 | func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.Transport) { | ||
| 206 | rc := d.attachCoordinator(sess, nil, ac, true) | 206 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| 207 | - | token := sess.attachmentToken(ac, tr) | 207 | + | token := sess.captureAttachmentCapability(ac, tr) |
| 208 | token.lease = rc.attachmentLease(ac) | 208 | token.lease = rc.attachmentLease(ac) | ||
| 209 | - | ac.publishAttachmentCapability(token) | 209 | + | ac.installTestAttachmentCapability(token) |
| 210 | } | 210 | } | ||
| 211 | 211 | ||||
| 212 | func clipboardOwnerLease(t *testing.T, sess *session) paneEffectLease { | 212 | func clipboardOwnerLease(t *testing.T, sess *session) paneEffectLease { | ||
| @@ -419,7 +419,7 @@ func TestQueuedClipboardRevalidatesOwnerAfterWaitingForClientSendLock(t *testing | |||||
| 419 | close(workerDone) | 419 | close(workerDone) | ||
| 420 | }() | 420 | }() | ||
| 421 | require.Eventually(t, func() bool { | 421 | require.Eventually(t, func() bool { | ||
| 422 | - | return ac.attachmentEffects.inFlightCount() == 1 | 422 | + | return ac.lifecycle.inFlightCount() == 1 |
| 423 | }, time.Second, time.Millisecond, "clipboard send was not admitted before waiting on sendMu") | 423 | }, time.Second, time.Millisecond, "clipboard send was not admitted before waiting on sendMu") | ||
| 424 | 424 | ||||
| 425 | sourceTab := testAttachmentTab(sess) | 425 | sourceTab := testAttachmentTab(sess) | ||
M internal/usecase/daemon/control.go +17 -17
| @@ -300,7 +300,7 @@ type daemonActionTarget struct { | |||||
| 300 | type daemonActionRequest struct { | 300 | type daemonActionRequest struct { | ||
| 301 | kind daemonActionKind | 301 | kind daemonActionKind | ||
| 302 | target daemonActionTarget | 302 | target daemonActionTarget | ||
| 303 | - | effect *attachmentEffectTicket | 303 | + | effect *attachmentEffect |
| 304 | direction layout.Direction | 304 | direction layout.Direction | ||
| 305 | axis layout.Axis | 305 | axis layout.Axis | ||
| 306 | delta int | 306 | delta int | ||
| @@ -513,21 +513,21 @@ func (e controlExec) MovePane(destinationSession, destinationTabID string) error | |||||
| 513 | return errMovePaneInvalid | 513 | return errMovePaneInvalid | ||
| 514 | } | 514 | } | ||
| 515 | attachment := e.target.attachment | 515 | attachment := e.target.attachment | ||
| 516 | - | token := attachmentConnectionToken{} | 516 | + | token := attachmentCapability{} |
| 517 | if attachment != nil { | 517 | if attachment != nil { | ||
| 518 | - | token = e.sess.attachmentToken(attachment, attachment.transport()) | 518 | + | token = e.sess.captureAttachmentCapability(attachment, attachment.transport()) |
| 519 | if token.ac == nil { | 519 | if token.ac == nil { | ||
| 520 | return errMoveStaleTarget | 520 | return errMoveStaleTarget | ||
| 521 | } | 521 | } | ||
| 522 | } | 522 | } | ||
| 523 | return e.d.movePane(movePaneRequest{ | 523 | return e.d.movePane(movePaneRequest{ | ||
| 524 | - | Attachment: attachment, | 524 | + | Attachment: attachment, |
| 525 | - | AttachmentToken: token, | 525 | + | AttachmentCapability: token, |
| 526 | - | Source: sessionMoveLocator(e.sess), | 526 | + | Source: sessionMoveLocator(e.sess), |
| 527 | - | SourceTabID: domain.TabStableID(e.tab.stableID), | 527 | + | SourceTabID: domain.TabStableID(e.tab.stableID), |
| 528 | - | SourcePaneID: domain.PaneStableID(e.target.pane.stableID), | 528 | + | SourcePaneID: domain.PaneStableID(e.target.pane.stableID), |
| 529 | - | Destination: sessionMoveLocator(destination), | 529 | + | Destination: sessionMoveLocator(destination), |
| 530 | - | DestinationTabID: domain.TabStableID(destinationTabID), | 530 | + | DestinationTabID: domain.TabStableID(destinationTabID), |
| 531 | }) | 531 | }) | ||
| 532 | } | 532 | } | ||
| 533 | 533 | ||||
| @@ -540,19 +540,19 @@ func (e controlExec) MoveTab(destinationSession string) error { | |||||
| 540 | return errMovePaneInvalid | 540 | return errMovePaneInvalid | ||
| 541 | } | 541 | } | ||
| 542 | attachment := e.target.attachment | 542 | attachment := e.target.attachment | ||
| 543 | - | token := attachmentConnectionToken{} | 543 | + | token := attachmentCapability{} |
| 544 | if attachment != nil { | 544 | if attachment != nil { | ||
| 545 | - | token = e.sess.attachmentToken(attachment, attachment.transport()) | 545 | + | token = e.sess.captureAttachmentCapability(attachment, attachment.transport()) |
| 546 | if token.ac == nil { | 546 | if token.ac == nil { | ||
| 547 | return errMoveStaleTarget | 547 | return errMoveStaleTarget | ||
| 548 | } | 548 | } | ||
| 549 | } | 549 | } | ||
| 550 | return e.d.moveTab(moveTabRequest{ | 550 | return e.d.moveTab(moveTabRequest{ | ||
| 551 | - | Attachment: attachment, | 551 | + | Attachment: attachment, |
| 552 | - | AttachmentToken: token, | 552 | + | AttachmentCapability: token, |
| 553 | - | Source: sessionMoveLocator(e.sess), | 553 | + | Source: sessionMoveLocator(e.sess), |
| 554 | - | SourceTabID: domain.TabStableID(e.tab.stableID), | 554 | + | SourceTabID: domain.TabStableID(e.tab.stableID), |
| 555 | - | Destination: sessionMoveLocator(destination), | 555 | + | Destination: sessionMoveLocator(destination), |
| 556 | }) | 556 | }) | ||
| 557 | } | 557 | } | ||
| 558 | 558 | ||||
M internal/usecase/daemon/daemon.go +6 -6
| @@ -161,7 +161,7 @@ type Daemon struct { | |||||
| 161 | afterConnectionSessionSnapshot func(*session) | 161 | afterConnectionSessionSnapshot func(*session) | ||
| 162 | // afterAttachmentFrameDispatch is a deterministic test seam after the | 162 | // afterAttachmentFrameDispatch is a deterministic test seam after the | ||
| 163 | // connection loop snapshots a token and before a decoded frame takes effect. | 163 | // connection loop snapshots a token and before a decoded frame takes effect. | ||
| 164 | - | afterAttachmentFrameDispatch func(attachmentConnectionToken) | 164 | + | afterAttachmentFrameDispatch func(attachmentCapability) |
| 165 | // afterAttachmentEffectParticipantsSnapshotted observes immutable role-gate | 165 | // afterAttachmentEffectParticipantsSnapshotted observes immutable role-gate | ||
| 166 | // participants after architecture preflight locks are released and before | 166 | // participants after architecture preflight locks are released and before | ||
| 167 | // the globally ordered freeze begins. | 167 | // the globally ordered freeze begins. | ||
| @@ -214,10 +214,10 @@ type Daemon struct { | |||||
| 214 | beforeResumeParkedSendMu func() | 214 | beforeResumeParkedSendMu func() | ||
| 215 | // afterAttachmentEffectAdmitted is a deterministic test seam after a frame/paint | 215 | // afterAttachmentEffectAdmitted is a deterministic test seam after a frame/paint | ||
| 216 | // reserves its exact capability and before its first observable mutation. | 216 | // reserves its exact capability and before its first observable mutation. | ||
| 217 | - | afterAttachmentEffectAdmitted func(attachmentConnectionToken) | 217 | + | afterAttachmentEffectAdmitted func(attachmentCapability) |
| 218 | // beforeFirstPaintSendWait is a test-only seam immediately before a | 218 | // beforeFirstPaintSendWait is a test-only seam immediately before a | ||
| 219 | // transition first paint waits for the attachment send lock. | 219 | // transition first paint waits for the attachment send lock. | ||
| 220 | - | beforeFirstPaintSendWait func(attachmentConnectionToken) | 220 | + | beforeFirstPaintSendWait func(attachmentCapability) |
| 221 | // afterDelayedKeyEffectAttempt observes whether a timer callback acquired a | 221 | // afterDelayedKeyEffectAttempt observes whether a timer callback acquired a | ||
| 222 | // fresh exact capability before producing PTY, action, or overlay effects. | 222 | // fresh exact capability before producing PTY, action, or overlay effects. | ||
| 223 | afterDelayedKeyEffectAttempt func(bool) | 223 | afterDelayedKeyEffectAttempt func(bool) | ||
| @@ -226,7 +226,7 @@ type Daemon struct { | |||||
| 226 | afterActionAttachmentEffectEnded func(string) | 226 | afterActionAttachmentEffectEnded func(string) | ||
| 227 | // beforeAttachmentSendErrorCleanup pauses asynchronous render-failure retirement | 227 | // beforeAttachmentSendErrorCleanup pauses asynchronous render-failure retirement | ||
| 228 | // after the render ticket ends and before exact lifecycle validation. | 228 | // after the render ticket ends and before exact lifecycle validation. | ||
| 229 | - | beforeAttachmentSendErrorCleanup func(attachmentConnectionToken) | 229 | + | beforeAttachmentSendErrorCleanup func(attachmentCapability) |
| 230 | afterAttachmentSendErrorCleanup func() | 230 | afterAttachmentSendErrorCleanup func() | ||
| 231 | ptys ports.PTYFactory | 231 | ptys ports.PTYFactory | ||
| 232 | clock ports.Clock | 232 | clock ports.Clock | ||
| @@ -1217,7 +1217,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut < | |||||
| 1217 | return | 1217 | return | ||
| 1218 | } | 1218 | } | ||
| 1219 | expected := ac.transportSnapshot() | 1219 | expected := ac.transportSnapshot() | ||
| 1220 | - | welcomeToken := sess.attachmentToken(ac, tr) | 1220 | + | welcomeToken := sess.captureAttachmentCapability(ac, tr) |
| 1221 | welcomeTicket, admitted := ac.beginAttachmentEffect(welcomeToken) | 1221 | welcomeTicket, admitted := ac.beginAttachmentEffect(welcomeToken) | ||
| 1222 | if expected.transport != tr || !admitted || welcomeToken.ac == nil { | 1222 | if expected.transport != tr || !admitted || welcomeToken.ac == nil { | ||
| 1223 | if admitted { | 1223 | if admitted { | ||
| @@ -1267,7 +1267,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut < | |||||
| 1267 | failAttachment() | 1267 | failAttachment() | ||
| 1268 | return | 1268 | return | ||
| 1269 | } | 1269 | } | ||
| 1270 | - | paintToken := sess.attachmentToken(ac, tr) | 1270 | + | paintToken := sess.captureAttachmentCapability(ac, tr) |
| 1271 | painted := make(chan bool, 1) | 1271 | painted := make(chan bool, 1) | ||
| 1272 | paintDone, paintErr := boundedHandshakeOperationTracked(handshakeCtx, tr, func() error { | 1272 | paintDone, paintErr := boundedHandshakeOperationTracked(handshakeCtx, tr, func() error { | ||
| 1273 | //nolint:contextcheck // firstPaintForTransition is capability-bounded; the surrounding handshake operation owns cancellation. | 1273 | //nolint:contextcheck // firstPaintForTransition is capability-bounded; the surrounding handshake operation owns cancellation. | ||
M internal/usecase/daemon/final_acceptance_test.go +18 -12
| @@ -42,19 +42,22 @@ func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *test | |||||
| 42 | d.ptys = newFactory(t, newQuietPTY()) | 42 | d.ptys = newFactory(t, newQuietPTY()) | ||
| 43 | rc := d.attachCoordinator(sess, nil, first, true) | 43 | rc := d.attachCoordinator(sess, nil, first, true) | ||
| 44 | d.attachCoordinator(sess, nil, second, true) | 44 | d.attachCoordinator(sess, nil, second, true) | ||
| 45 | - | firstToken := sess.attachmentToken(first, first.transport()) | 45 | + | firstToken := sess.captureAttachmentCapability(first, first.transport()) |
| 46 | firstToken.lease = rc.attachmentLease(first) | 46 | firstToken.lease = rc.attachmentLease(first) | ||
| 47 | - | first.publishAttachmentCapability(firstToken) | 47 | + | first.installTestAttachmentCapability(firstToken) |
| 48 | - | secondToken := sess.attachmentToken(second, second.transport()) | 48 | + | secondToken := sess.captureAttachmentCapability(second, second.transport()) |
| 49 | secondToken.lease = rc.attachmentLease(second) | 49 | secondToken.lease = rc.attachmentLease(second) | ||
| 50 | - | second.publishAttachmentCapability(secondToken) | 50 | + | second.installTestAttachmentCapability(secondToken) |
| 51 | 51 | ||||
| 52 | firstView := first.viewSnapshot() | 52 | firstView := first.viewSnapshot() | ||
| 53 | secondView := second.viewSnapshot() | 53 | secondView := second.viewSnapshot() | ||
| 54 | first.sendMu.Lock() | 54 | first.sendMu.Lock() | ||
| 55 | firstEpoch := first.output.currentEpoch() | 55 | firstEpoch := first.output.currentEpoch() | ||
| 56 | first.sendMu.Unlock() | 56 | first.sendMu.Unlock() | ||
| 57 | - | require.True(t, d.resizeAttachmentForLease(secondToken, domain.Size{Cols: 120, Rows: 40})) | 57 | + | secondEffect, admitted := second.beginAttachmentEffect(secondToken) |
| 58 | + | require.True(t, admitted) | |||
| 59 | + | require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40})) | |||
| 60 | + | secondEffect.End() | |||
| 58 | 61 | ||||
| 59 | d.enterPalette(sess, second) | 62 | d.enterPalette(sess, second) | ||
| 60 | d.handlePaletteInput(second, []byte("CNT\r")) | 63 | d.handlePaletteInput(second, []byte("CNT\r")) | ||
| @@ -136,12 +139,12 @@ func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing | |||||
| 136 | 139 | ||||
| 137 | rc := d.attachCoordinator(sess, nil, first, true) | 140 | rc := d.attachCoordinator(sess, nil, first, true) | ||
| 138 | d.attachCoordinator(sess, nil, second, true) | 141 | d.attachCoordinator(sess, nil, second, true) | ||
| 139 | - | firstToken := sess.attachmentToken(first, first.transport()) | 142 | + | firstToken := sess.captureAttachmentCapability(first, first.transport()) |
| 140 | firstToken.lease = rc.attachmentLease(first) | 143 | firstToken.lease = rc.attachmentLease(first) | ||
| 141 | - | first.publishAttachmentCapability(firstToken) | 144 | + | first.installTestAttachmentCapability(firstToken) |
| 142 | - | secondToken := sess.attachmentToken(second, second.transport()) | 145 | + | secondToken := sess.captureAttachmentCapability(second, second.transport()) |
| 143 | secondToken.lease = rc.attachmentLease(second) | 146 | secondToken.lease = rc.attachmentLease(second) | ||
| 144 | - | second.publishAttachmentCapability(secondToken) | 147 | + | second.installTestAttachmentCapability(secondToken) |
| 145 | firstEpoch := first.output.currentEpoch() | 148 | firstEpoch := first.output.currentEpoch() | ||
| 146 | secondEpoch := second.output.currentEpoch() | 149 | secondEpoch := second.output.currentEpoch() | ||
| 147 | 150 | ||||
| @@ -152,7 +155,10 @@ func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing | |||||
| 152 | require.Greater(t, second.output.currentEpoch(), secondEpoch) | 155 | require.Greater(t, second.output.currentEpoch(), secondEpoch) | ||
| 153 | require.Equal(t, firstEpoch, first.output.currentEpoch(), "peer output reset crossed attachment boundary") | 156 | require.Equal(t, firstEpoch, first.output.currentEpoch(), "peer output reset crossed attachment boundary") | ||
| 154 | 157 | ||||
| 155 | - | require.True(t, d.resizeAttachmentForLease(secondToken, domain.Size{Cols: 120, Rows: 40})) | 158 | + | secondEffect, admitted := second.beginAttachmentEffect(secondToken) |
| 159 | + | require.True(t, admitted) | |||
| 160 | + | require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40})) | |||
| 161 | + | secondEffect.End() | |||
| 156 | require.Equal(t, domain.Size{Cols: 120, Rows: 40}, second.size) | 162 | require.Equal(t, domain.Size{Cols: 120, Rows: 40}, second.size) | ||
| 157 | require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.size, "peer resize crossed attachment boundary") | 163 | require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.size, "peer resize crossed attachment boundary") | ||
| 158 | sess.mu.Lock() | 164 | sess.mu.Lock() | ||
| @@ -183,9 +189,9 @@ func TestAcceptanceDirectAttachmentSupportsVerticalSplit(t *testing.T) { | |||||
| 183 | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | 189 | d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY()) | ||
| 184 | d.ptys = newFactory(t, newQuietPTY()) | 190 | d.ptys = newFactory(t, newQuietPTY()) | ||
| 185 | rc := d.attachCoordinator(sess, nil, ac, true) | 191 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| 186 | - | token := sess.attachmentToken(ac, ac.transport()) | 192 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 187 | token.lease = rc.attachmentLease(ac) | 193 | token.lease = rc.attachmentLease(ac) | ||
| 188 | - | ac.publishAttachmentCapability(token) | 194 | + | ac.installTestAttachmentCapability(token) |
| 189 | 195 | ||||
| 190 | d.enterPalette(sess, ac) | 196 | d.enterPalette(sess, ac) | ||
| 191 | d.handlePaletteInput(ac, []byte("SPD\r")) | 197 | d.handlePaletteInput(ac, []byte("SPD\r")) | ||
M internal/usecase/daemon/handshake_test.go +1 -1
| @@ -117,7 +117,7 @@ func TestHandshakeFirstPaintCancellationDoesNotRacePaintResult(t *testing.T) { | |||||
| 117 | paintStarted := make(chan struct{}) | 117 | paintStarted := make(chan struct{}) | ||
| 118 | releasePaint := make(chan struct{}) | 118 | releasePaint := make(chan struct{}) | ||
| 119 | paintDone := make(chan struct{}) | 119 | paintDone := make(chan struct{}) | ||
| 120 | - | d.afterAttachmentEffectAdmitted = func(attachmentConnectionToken) { | 120 | + | d.afterAttachmentEffectAdmitted = func(attachmentCapability) { |
| 121 | close(paintStarted) | 121 | close(paintStarted) | ||
| 122 | <-ctx.Done() | 122 | <-ctx.Done() | ||
| 123 | <-releasePaint | 123 | <-releasePaint | ||
M internal/usecase/daemon/input.go +24 -29
| @@ -20,11 +20,11 @@ func (d *Daemon) handleSequencedInput(sess *session, ac *attachedClient, _ uint6 | |||||
| 20 | d.handleInput(sess, ac, data) | 20 | d.handleInput(sess, ac, data) | ||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | - | func (d *Daemon) handleSequencedInputForAttachment(token attachmentConnectionToken, _ uint64, data []byte) { | 23 | + | func (d *Daemon) handleSequencedInputForAttachment(effect *attachmentEffect, _ uint64, data []byte) { |
| 24 | - | if !token.attachmentEffectCurrent() { | 24 | + | if !effect.current() { |
| 25 | return | 25 | return | ||
| 26 | } | 26 | } | ||
| 27 | - | d.handleInputForAttachment(token, data) | 27 | + | d.handleInputForAttachment(effect, data) |
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | func (d *Daemon) handleInput(_ *session, ac *attachedClient, data []byte) { | 30 | func (d *Daemon) handleInput(_ *session, ac *attachedClient, data []byte) { | ||
| @@ -40,26 +40,26 @@ func (d *Daemon) handleInput(_ *session, ac *attachedClient, data []byte) { | |||||
| 40 | ) | 40 | ) | ||
| 41 | } | 41 | } | ||
| 42 | 42 | ||||
| 43 | - | func (d *Daemon) handleInputForAttachment(token attachmentConnectionToken, data []byte) { | 43 | + | func (d *Daemon) handleInputForAttachment(effect *attachmentEffect, data []byte) { |
| 44 | - | ac := token.ac | 44 | + | ac := effect.ac |
| 45 | ac.initOverlays() | 45 | ac.initOverlays() | ||
| 46 | ac.mouseScan.Scan(data, | 46 | ac.mouseScan.Scan(data, | ||
| 47 | func(ev mouse.Event) { | 47 | func(ev mouse.Event) { | ||
| 48 | - | if token.attachmentEffectCurrent() { | 48 | + | if effect.current() { |
| 49 | d.handleMouse(ac, ev) | 49 | d.handleMouse(ac, ev) | ||
| 50 | } | 50 | } | ||
| 51 | }, | 51 | }, | ||
| 52 | func(b []byte) { | 52 | func(b []byte) { | ||
| 53 | - | if !token.attachmentEffectCurrent() { | 53 | + | if !effect.current() { |
| 54 | return | 54 | return | ||
| 55 | } | 55 | } | ||
| 56 | - | if ac.overlays.HandleInput(d, b, token.effect) { | 56 | + | if ac.overlays.HandleInput(d, b, effect) { |
| 57 | return | 57 | return | ||
| 58 | } | 58 | } | ||
| 59 | - | if !token.attachmentEffectCurrent() { | 59 | + | if !effect.current() { |
| 60 | return | 60 | return | ||
| 61 | } | 61 | } | ||
| 62 | - | ac.keys.RouteWithHandler(b, daemonKeyHandler{d: d, ac: ac, connectionToken: token}) | 62 | + | ac.keys.RouteWithHandler(b, daemonKeyHandler{d: d, ac: ac, effect: effect}) |
| 63 | }, | 63 | }, | ||
| 64 | ) | 64 | ) | ||
| 65 | } | 65 | } | ||
| @@ -371,21 +371,21 @@ func sgrOffset(raw []byte, colDelta, rowDelta int) []byte { | |||||
| 371 | } | 371 | } | ||
| 372 | 372 | ||||
| 373 | type daemonKeyHandler struct { | 373 | type daemonKeyHandler struct { | ||
| 374 | - | d *Daemon | 374 | + | d *Daemon |
| 375 | - | ac *attachedClient | 375 | + | ac *attachedClient |
| 376 | - | actions daemonActionRunner | 376 | + | actions daemonActionRunner |
| 377 | - | connectionToken attachmentConnectionToken | 377 | + | effect *attachmentEffect |
| 378 | } | 378 | } | ||
| 379 | 379 | ||||
| 380 | // acquireAttachmentEffect preserves a synchronous frame's existing admission and | 380 | // acquireAttachmentEffect preserves a synchronous frame's existing admission and | ||
| 381 | // gives delayed router callbacks a fresh ticket for the exact captured attachment. | 381 | // gives delayed router callbacks a fresh ticket for the exact captured attachment. | ||
| 382 | // Direct/headless callers without an attachment token retain their existing behavior. | 382 | // Direct/headless callers without an attachment token retain their existing behavior. | ||
| 383 | - | func (h daemonKeyHandler) acquireAttachmentEffect() (*session, *attachmentEffectTicket, bool) { | 383 | + | func (h daemonKeyHandler) acquireAttachmentEffect() (*session, *attachmentEffect, bool) { |
| 384 | - | if h.connectionToken.ac != nil { | 384 | + | if h.effect != nil && h.effect.ac != nil { |
| 385 | - | if effect := h.connectionToken.effect; effect != nil && !effect.ended.Load() { | 385 | + | if h.effect.current() { |
| 386 | - | return h.connectionToken.sess, effect, false | 386 | + | return h.effect.sess, h.effect, false |
| 387 | } | 387 | } | ||
| 388 | - | effect, admitted := h.connectionToken.ac.beginAttachmentEffect(h.connectionToken) | 388 | + | effect, admitted := h.effect.ac.beginAttachmentEffect(h.effect.capability()) |
| 389 | if h.d.afterDelayedKeyEffectAttempt != nil { | 389 | if h.d.afterDelayedKeyEffectAttempt != nil { | ||
| 390 | h.d.afterDelayedKeyEffectAttempt(admitted) | 390 | h.d.afterDelayedKeyEffectAttempt(admitted) | ||
| 391 | } | 391 | } | ||
| @@ -393,16 +393,13 @@ func (h daemonKeyHandler) acquireAttachmentEffect() (*session, *attachmentEffect | |||||
| 393 | return nil, nil, false | 393 | return nil, nil, false | ||
| 394 | } | 394 | } | ||
| 395 | if h.d.afterAttachmentEffectAdmitted != nil { | 395 | if h.d.afterAttachmentEffectAdmitted != nil { | ||
| 396 | - | token := h.connectionToken | 396 | + | h.d.afterAttachmentEffectAdmitted(effect.capability()) |
| 397 | - | token.effect = effect | |||
| 398 | - | h.d.afterAttachmentEffectAdmitted(token) | |||
| 399 | } | 397 | } | ||
| 400 | - | // Keep this defensive check even though token admission normally requires a session. | 398 | + | if effect.sess == nil { |
| 401 | - | if h.connectionToken.sess == nil { | |||
| 402 | effect.End() | 399 | effect.End() | ||
| 403 | return nil, nil, false | 400 | return nil, nil, false | ||
| 404 | } | 401 | } | ||
| 405 | - | return h.connectionToken.sess, effect, true | 402 | + | return effect.sess, effect, true |
| 406 | } | 403 | } | ||
| 407 | sess := h.ac.currentSession() | 404 | sess := h.ac.currentSession() | ||
| 408 | if sess == nil { | 405 | if sess == nil { | ||
| @@ -475,9 +472,7 @@ func (h daemonKeyHandler) Action(action keys.Action, _ []byte) { | |||||
| 475 | return | 472 | return | ||
| 476 | } | 473 | } | ||
| 477 | effect.bindActionEnd(h.d, "jump-attention") | 474 | effect.bindActionEnd(h.d, "jump-attention") | ||
| 478 | - | token := h.connectionToken | 475 | + | if err := h.d.jumpAttentionForAttachment(sess, h.ac, effect); err != nil { |
| 479 | - | token.effect = effect | |||
| 480 | - | if err := h.d.jumpAttentionForAttachment(sess, h.ac, token); err != nil { | |||
| 481 | h.d.reportError(sess, err) | 476 | h.d.reportError(sess, err) | ||
| 482 | } | 477 | } | ||
| 483 | case keys.ActionToggleFloatingPane: | 478 | case keys.ActionToggleFloatingPane: | ||
| @@ -521,7 +516,7 @@ func (h daemonKeyHandler) Action(action keys.Action, _ []byte) { | |||||
| 521 | } | 516 | } | ||
| 522 | } | 517 | } | ||
| 523 | 518 | ||||
| 524 | - | func (h daemonKeyHandler) focus(sess *session, dir layout.Direction, effect *attachmentEffectTicket) { | 519 | + | func (h daemonKeyHandler) focus(sess *session, dir layout.Direction, effect *attachmentEffect) { |
| 525 | err := h.d.focusDir(sess, h.ac, dir, effect) | 520 | err := h.d.focusDir(sess, h.ac, dir, effect) | ||
| 526 | if err != nil && !errors.Is(err, errAttachmentTransition) && !errors.Is(err, errNoNeighbor) { | 521 | if err != nil && !errors.Is(err, errAttachmentTransition) && !errors.Is(err, errNoNeighbor) { | ||
| 527 | h.d.reportError(sess, err) | 522 | h.d.reportError(sess, err) | ||
M internal/usecase/daemon/input_test.go +3 -4
| @@ -783,13 +783,12 @@ func TestPaletteBackSessionSendsClientPreviousRouteAction(t *testing.T) { | |||||
| 783 | defer releaseAll(releases) | 783 | defer releaseAll(releases) | ||
| 784 | transport := ac.transport() | 784 | transport := ac.transport() | ||
| 785 | rc := d.attachCoordinator(current, nil, ac, true) | 785 | rc := d.attachCoordinator(current, nil, ac, true) | ||
| 786 | - | token := current.attachmentToken(ac, transport) | 786 | + | token := current.captureAttachmentCapability(ac, transport) |
| 787 | token.lease = rc.attachmentLease(ac) | 787 | token.lease = rc.attachmentLease(ac) | ||
| 788 | - | ac.publishAttachmentCapability(token) | 788 | + | ac.installTestAttachmentCapability(token) |
| 789 | effect, admitted := ac.beginAttachmentEffect(token) | 789 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 790 | require.True(t, admitted) | 790 | require.True(t, admitted) | ||
| 791 | defer effect.End() | 791 | defer effect.End() | ||
| 792 | - | token.effect = effect | |||
| 793 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{ | 792 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{ | ||
| 794 | Generation: 3, | 793 | Generation: 3, | ||
| 795 | Active: ports.RouteRef{Key: 3, Generation: 3}, | 794 | Active: ports.RouteRef{Key: 3, Generation: 3}, | ||
| @@ -797,7 +796,7 @@ func TestPaletteBackSessionSendsClientPreviousRouteAction(t *testing.T) { | |||||
| 797 | Entries: []ports.RecentRouteEntry{testRouteEntry(2, 2, "recent", 2, ports.RouteKindLocal)}, | 796 | Entries: []ports.RecentRouteEntry{testRouteEntry(2, 2, "recent", 2, ports.RouteKindLocal)}, | ||
| 798 | }) | 797 | }) | ||
| 799 | 798 | ||||
| 800 | - | require.NoError(t, d.backSessionForAttachment(token)) | 799 | + | require.NoError(t, d.backSessionForAttachment(effect)) |
| 801 | frame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute) | 800 | frame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute) | ||
| 802 | action, err := ports.UnmarshalRouteNavigationAction(frame.Payload) | 801 | action, err := ports.UnmarshalRouteNavigationAction(frame.Payload) | ||
| 803 | require.NoError(t, err) | 802 | require.NoError(t, err) | ||
M internal/usecase/daemon/lifecycle_fence_test.go +6 -6
| @@ -21,7 +21,7 @@ func TestValidateAttachmentTransitionLocksSourceCoordinatorBeforeLeaseValidation | |||||
| 21 | sourceCoordinator := newRenderCoordinator(renderCoordinatorOptions{}) | 21 | sourceCoordinator := newRenderCoordinator(renderCoordinatorOptions{}) | ||
| 22 | source.installRenderCoordinator(sourceCoordinator) | 22 | source.installRenderCoordinator(sourceCoordinator) | ||
| 23 | sourceCoordinator.attach(ac) | 23 | sourceCoordinator.attach(ac) | ||
| 24 | - | token := source.attachmentToken(ac, transport) | 24 | + | token := source.captureAttachmentCapability(ac, transport) |
| 25 | require.NotNil(t, token.lease) | 25 | require.NotNil(t, token.lease) | ||
| 26 | d.sessions[source.id] = source | 26 | d.sessions[source.id] = source | ||
| 27 | d.sessions[target.id] = target | 27 | d.sessions[target.id] = target | ||
| @@ -37,7 +37,7 @@ func TestValidateAttachmentTransitionLocksSourceCoordinatorBeforeLeaseValidation | |||||
| 37 | } | 37 | } | ||
| 38 | req := attachmentTransitionRequest{ | 38 | req := attachmentTransitionRequest{ | ||
| 39 | source: source, target: target, next: ac, | 39 | source: source, target: target, next: ac, | ||
| 40 | - | expectedTransport: ac.transportSnapshot(), sourceToken: &token, | 40 | + | expectedTransport: ac.transportSnapshot(), sourceCapability: &token, |
| 41 | preflighted: true, attachmentEffectsFrozen: true, | 41 | preflighted: true, attachmentEffectsFrozen: true, | ||
| 42 | } | 42 | } | ||
| 43 | d.mu.Lock() | 43 | d.mu.Lock() | ||
| @@ -56,7 +56,7 @@ func TestValidateAttachmentTransitionLocksSourceCoordinatorBeforeLeaseValidation | |||||
| 56 | require.NotNil(t, publication) | 56 | require.NotNil(t, publication) | ||
| 57 | } | 57 | } | ||
| 58 | 58 | ||||
| 59 | - | func TestAttachmentTokenRevalidatesTheCapturedTransportIncarnation(t *testing.T) { | 59 | + | func TestAttachmentCapabilityRevalidatesTheCapturedTransportIncarnation(t *testing.T) { |
| 60 | first := &closeTrackingTransport{} | 60 | first := &closeTrackingTransport{} | ||
| 61 | second := &closeTrackingTransport{} | 61 | second := &closeTrackingTransport{} | ||
| 62 | ac := &attachedClient{tr: first} | 62 | ac := &attachedClient{tr: first} | ||
| @@ -69,12 +69,12 @@ func TestAttachmentTokenRevalidatesTheCapturedTransportIncarnation(t *testing.T) | |||||
| 69 | 69 | ||||
| 70 | captured := make(chan struct{}) | 70 | captured := make(chan struct{}) | ||
| 71 | release := make(chan struct{}) | 71 | release := make(chan struct{}) | ||
| 72 | - | ac.beforeAttachmentTokenValidation = func() { | 72 | + | ac.beforeAttachmentCapabilityValidation = func() { |
| 73 | close(captured) | 73 | close(captured) | ||
| 74 | <-release | 74 | <-release | ||
| 75 | } | 75 | } | ||
| 76 | - | result := make(chan attachmentConnectionToken, 1) | 76 | + | result := make(chan attachmentCapability, 1) |
| 77 | - | go func() { result <- sess.attachmentToken(ac, first) }() | 77 | + | go func() { result <- sess.captureAttachmentCapability(ac, first) }() |
| 78 | <-captured | 78 | <-captured | ||
| 79 | ac.replaceTransport(second) | 79 | ac.replaceTransport(second) | ||
| 80 | close(release) | 80 | close(release) | ||
M internal/usecase/daemon/move.go +13 -13
| @@ -10,21 +10,21 @@ import ( | |||||
| 10 | // immutable identity. Session names are advisory; the ID and incarnation are | 10 | // immutable identity. Session names are advisory; the ID and incarnation are | ||
| 11 | // the commit-time authority. | 11 | // the commit-time authority. | ||
| 12 | type movePaneRequest struct { | 12 | type movePaneRequest struct { | ||
| 13 | - | Attachment *attachedClient | 13 | + | Attachment *attachedClient |
| 14 | - | AttachmentToken attachmentConnectionToken | 14 | + | AttachmentCapability attachmentCapability |
| 15 | - | Source moveSessionLocator | 15 | + | Source moveSessionLocator |
| 16 | - | SourceTabID domain.TabStableID | 16 | + | SourceTabID domain.TabStableID |
| 17 | - | SourcePaneID domain.PaneStableID | 17 | + | SourcePaneID domain.PaneStableID |
| 18 | - | Destination moveSessionLocator | 18 | + | Destination moveSessionLocator |
| 19 | - | DestinationTabID domain.TabStableID | 19 | + | DestinationTabID domain.TabStableID |
| 20 | } | 20 | } | ||
| 21 | 21 | ||||
| 22 | type moveTabRequest struct { | 22 | type moveTabRequest struct { | ||
| 23 | - | Attachment *attachedClient | 23 | + | Attachment *attachedClient |
| 24 | - | AttachmentToken attachmentConnectionToken | 24 | + | AttachmentCapability attachmentCapability |
| 25 | - | Source moveSessionLocator | 25 | + | Source moveSessionLocator |
| 26 | - | SourceTabID domain.TabStableID | 26 | + | SourceTabID domain.TabStableID |
| 27 | - | Destination moveSessionLocator | 27 | + | Destination moveSessionLocator |
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | // movePane resolves, reserves, validates, and commits one pane relocation. All | 30 | // movePane resolves, reserves, validates, and commits one pane relocation. All | ||
| @@ -115,7 +115,7 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) { | |||||
| 115 | // A final source tab is retired without transferring any attachment to the | 115 | // A final source tab is retired without transferring any attachment to the | ||
| 116 | // destination. Freeze every affected source connection before publication so | 116 | // destination. Freeze every affected source connection before publication so | ||
| 117 | // teardown invalidates exact generations rather than racing input or render. | 117 | // teardown invalidates exact generations rather than racing input or render. | ||
| 118 | - | var frozen frozenAttachmentEffectGates | 118 | + | var frozen attachmentTransitionGuard |
| 119 | var sourceEffectsFrozen bool | 119 | var sourceEffectsFrozen bool | ||
| 120 | if finalSourceTab && source != destination && len(admission.sourceAttachments) != 0 { | 120 | if finalSourceTab && source != destination && len(admission.sourceAttachments) != 0 { | ||
| 121 | interrupts := make([]attachmentTransportInterrupt, 0, len(admission.sourceAttachments)) | 121 | interrupts := make([]attachmentTransportInterrupt, 0, len(admission.sourceAttachments)) | ||
M internal/usecase/daemon/move_attachments.go +0 -7
| @@ -1,11 +1,5 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | - | func moveAttachmentTokenCurrentLocked(token attachmentConnectionToken, sess *session) bool { | |||
| 4 | - | return token.sess == sess && token.ac != nil && token.generation == token.ac.connectionGeneration.Load() && | |||
| 5 | - | token.ac.currentAttachmentSession() == sess && token.ac.transportSnapshotCurrent(token.transport) && | |||
| 6 | - | attachmentRegisteredLocked(sess, token.ac) | |||
| 7 | - | } | |||
| 8 | - | ||||
| 9 | // sameMoveAttachmentsLocked reports whether the source membership is unchanged | 3 | // sameMoveAttachmentsLocked reports whether the source membership is unchanged | ||
| 10 | // since admission. Caller holds the source session lock. | 4 | // since admission. Caller holds the source session lock. | ||
| 11 | func sameMoveAttachmentsLocked(sess *session, admitted []*attachedClient) bool { | 5 | func sameMoveAttachmentsLocked(sess *session, admitted []*attachedClient) bool { | ||
| @@ -33,7 +27,6 @@ func detachMoveAttachmentsLocked(sess *session, transports map[*attachedClient]t | |||||
| 33 | retired := make([]detachedAttachmentSnapshot, 0, len(attachments)) | 27 | retired := make([]detachedAttachmentSnapshot, 0, len(attachments)) | ||
| 34 | for _, ac := range attachments { | 28 | for _, ac := range attachments { | ||
| 35 | retired = append(retired, detachedAttachmentSnapshot{ac: ac, transport: transports[ac]}) | 29 | retired = append(retired, detachedAttachmentSnapshot{ac: ac, transport: transports[ac]}) | ||
| 36 | - | ac.connectionGeneration.Add(1) | |||
| 37 | ac.setSession(nil) | 30 | ac.setSession(nil) | ||
| 38 | ac.invalidateFrozenAttachmentCapability() | 31 | ac.invalidateFrozenAttachmentCapability() | ||
| 39 | } | 32 | } | ||
M internal/usecase/daemon/move_cleanup_test.go +1 -1
| @@ -103,7 +103,7 @@ func TestMovePaneReleasesResizeFencesBeforeAttachmentCleanup(t *testing.T) { | |||||
| 103 | require.False(t, awaitTestValue(t, resizeDone, "resize commit did not drop stale owner")) | 103 | require.False(t, awaitTestValue(t, resizeDone, "resize commit did not drop stale owner")) | ||
| 104 | } | 104 | } | ||
| 105 | 105 | ||||
| 106 | - | func TestMovePaneCleanupUsesCommitPointSourceAttachmentToken(t *testing.T) { | 106 | + | func TestMovePaneCleanupUsesCapturedSourceAttachment(t *testing.T) { |
| 107 | movedPTY, releaseMoved := newBlockingPTY(t) | 107 | movedPTY, releaseMoved := newBlockingPTY(t) | ||
| 108 | remainingPTY, releaseRemaining := newBlockingPTY(t) | 108 | remainingPTY, releaseRemaining := newBlockingPTY(t) | ||
| 109 | destinationPTY, releaseDestination := newBlockingPTY(t) | 109 | destinationPTY, releaseDestination := newBlockingPTY(t) | ||
M internal/usecase/daemon/move_gate_race_test.go +2 -2
| @@ -194,11 +194,11 @@ func TestPaletteFinalCloseEndsCurrentEffectBeforeDrainingPeers(t *testing.T) { | |||||
| 194 | sess.mu.Unlock() | 194 | sess.mu.Unlock() | ||
| 195 | d.attachCoordinator(sess, nil, other, true) | 195 | d.attachCoordinator(sess, nil, other, true) | ||
| 196 | 196 | ||||
| 197 | - | currentToken := sess.attachmentToken(current, current.transport()) | 197 | + | currentToken := sess.captureAttachmentCapability(current, current.transport()) |
| 198 | currentEffect, admitted := current.beginAttachmentEffect(currentToken) | 198 | currentEffect, admitted := current.beginAttachmentEffect(currentToken) | ||
| 199 | require.True(t, admitted) | 199 | require.True(t, admitted) | ||
| 200 | t.Cleanup(currentEffect.End) | 200 | t.Cleanup(currentEffect.End) | ||
| 201 | - | otherToken := sess.attachmentToken(other, otherTransport) | 201 | + | otherToken := sess.captureAttachmentCapability(other, otherTransport) |
| 202 | otherEffect, admitted := other.beginAttachmentEffect(otherToken) | 202 | otherEffect, admitted := other.beginAttachmentEffect(otherToken) | ||
| 203 | require.True(t, admitted) | 203 | require.True(t, admitted) | ||
| 204 | t.Cleanup(otherEffect.End) | 204 | t.Cleanup(otherEffect.End) | ||
M internal/usecase/daemon/move_pane_admission.go +1 -1
| @@ -32,7 +32,7 @@ func (d *Daemon) snapshotMovePaneAdmission(req movePaneRequest, source, destinat | |||||
| 32 | if req.Attachment != nil && !attachmentRegisteredLocked(source, req.Attachment) { | 32 | if req.Attachment != nil && !attachmentRegisteredLocked(source, req.Attachment) { | ||
| 33 | return nil, errMoveStaleTarget | 33 | return nil, errMoveStaleTarget | ||
| 34 | } | 34 | } | ||
| 35 | - | if req.AttachmentToken.ac != nil && (req.AttachmentToken.ac != req.Attachment || !moveAttachmentTokenCurrentLocked(req.AttachmentToken, source)) { | 35 | + | if req.AttachmentCapability.ac != nil && (req.AttachmentCapability.ac != req.Attachment || !req.AttachmentCapability.currentInSessionLocked(source)) { |
| 36 | return nil, errMoveStaleTarget | 36 | return nil, errMoveStaleTarget | ||
| 37 | } | 37 | } | ||
| 38 | 38 | ||||
M internal/usecase/daemon/move_pane_commit.go +2 -2
| @@ -17,7 +17,7 @@ type movePaneCommit struct { | |||||
| 17 | sourceTransports map[*attachedClient]transportSnapshot | 17 | sourceTransports map[*attachedClient]transportSnapshot | ||
| 18 | sourceGeneration uint64 | 18 | sourceGeneration uint64 | ||
| 19 | destinationGeneration uint64 | 19 | destinationGeneration uint64 | ||
| 20 | - | frozenEffects frozenAttachmentEffectGates | 20 | + | frozenEffects attachmentTransitionGuard |
| 21 | err error | 21 | err error | ||
| 22 | 22 | ||||
| 23 | syncCleanup syncTimerCleanup | 23 | syncCleanup syncTimerCleanup | ||
| @@ -51,7 +51,7 @@ func (c *movePaneCommit) publishLocked(d *Daemon) bool { | |||||
| 51 | if c.req.Attachment != nil && !attachmentRegisteredLocked(c.source, c.req.Attachment) { | 51 | if c.req.Attachment != nil && !attachmentRegisteredLocked(c.source, c.req.Attachment) { | ||
| 52 | return false | 52 | return false | ||
| 53 | } | 53 | } | ||
| 54 | - | if c.req.AttachmentToken.ac != nil && (c.req.AttachmentToken.ac != c.req.Attachment || !moveAttachmentTokenCurrentLocked(c.req.AttachmentToken, c.source)) { | 54 | + | if c.req.AttachmentCapability.ac != nil && (c.req.AttachmentCapability.ac != c.req.Attachment || !c.req.AttachmentCapability.currentInSessionLocked(c.source)) { |
| 55 | return false | 55 | return false | ||
| 56 | } | 56 | } | ||
| 57 | 57 | ||||
M internal/usecase/daemon/move_pane_postcommit.go +1 -1
| @@ -29,7 +29,7 @@ type movePanePostcommitPlan struct { | |||||
| 29 | operation string | 29 | operation string | ||
| 30 | sourceAttachments []*attachedClient | 30 | sourceAttachments []*attachedClient | ||
| 31 | syncCleanup syncTimerCleanup | 31 | syncCleanup syncTimerCleanup | ||
| 32 | - | frozenEffects frozenAttachmentEffectGates | 32 | + | frozenEffects attachmentTransitionGuard |
| 33 | effectsFrozen bool | 33 | effectsFrozen bool | ||
| 34 | unlockDispatch func() | 34 | unlockDispatch func() | ||
| 35 | reservation *moveLifecycleReservation | 35 | reservation *moveLifecycleReservation | ||
M internal/usecase/daemon/move_picker.go +20 -20
| @@ -16,7 +16,7 @@ func (d *Daemon) movePickerSourceError(source moveSourceLocator) error { | |||||
| 16 | } | 16 | } | ||
| 17 | sess.mu.Lock() | 17 | sess.mu.Lock() | ||
| 18 | defer sess.mu.Unlock() | 18 | defer sess.mu.Unlock() | ||
| 19 | - | if source.AttachmentToken.ac != nil && !moveAttachmentTokenCurrentLocked(source.AttachmentToken, sess) { | 19 | + | if source.AttachmentCapability.ac != nil && !source.AttachmentCapability.currentInSessionLocked(sess) { |
| 20 | return domain.UserWarn(domain.NoticeSessionUnavailable, "Source attachment is no longer active.", errMovePaneInvalid) | 20 | return domain.UserWarn(domain.NoticeSessionUnavailable, "Source attachment is no longer active.", errMovePaneInvalid) | ||
| 21 | } | 21 | } | ||
| 22 | if source.Attachment != nil && !attachmentRegisteredLocked(sess, source.Attachment) { | 22 | if source.Attachment != nil && !attachmentRegisteredLocked(sess, source.Attachment) { | ||
| @@ -52,11 +52,11 @@ type moveSessionLocator struct { | |||||
| 52 | } | 52 | } | ||
| 53 | 53 | ||||
| 54 | type moveSourceLocator struct { | 54 | type moveSourceLocator struct { | ||
| 55 | - | Session moveSessionLocator | 55 | + | Session moveSessionLocator |
| 56 | - | TabID domain.TabStableID | 56 | + | TabID domain.TabStableID |
| 57 | - | PaneID domain.PaneStableID | 57 | + | PaneID domain.PaneStableID |
| 58 | - | Attachment *attachedClient | 58 | + | Attachment *attachedClient |
| 59 | - | AttachmentToken attachmentConnectionToken | 59 | + | AttachmentCapability attachmentCapability |
| 60 | } | 60 | } | ||
| 61 | 61 | ||||
| 62 | // enterPickerForIntent returns errNoMoveDestination only for move intents. | 62 | // enterPickerForIntent returns errNoMoveDestination only for move intents. | ||
| @@ -64,8 +64,8 @@ func (d *Daemon) enterPickerForIntent(sess *session, ac *attachedClient, intent | |||||
| 64 | if intent != pickerNavigate && !sess.capabilities().yieldsMoves() { | 64 | if intent != pickerNavigate && !sess.capabilities().yieldsMoves() { | ||
| 65 | return errSessionCannotYieldMoves | 65 | return errSessionCannotYieldMoves | ||
| 66 | } | 66 | } | ||
| 67 | - | if source.Attachment != nil && source.AttachmentToken.ac == nil { | 67 | + | if source.Attachment != nil && source.AttachmentCapability.ac == nil { |
| 68 | - | source.AttachmentToken = sess.attachmentToken(source.Attachment, source.Attachment.transport()) | 68 | + | source.AttachmentCapability = sess.captureAttachmentCapability(source.Attachment, source.Attachment.transport()) |
| 69 | } | 69 | } | ||
| 70 | model := d.newPickerModel(sess, ac, intent, source, picker.SourceFilter{}) | 70 | model := d.newPickerModel(sess, ac, intent, source, picker.SourceFilter{}) | ||
| 71 | if intent != pickerNavigate { | 71 | if intent != pickerNavigate { | ||
| @@ -97,21 +97,21 @@ func (d *Daemon) commitMovePickerSelection(intent pickerIntent, source moveSourc | |||||
| 97 | return errMovePaneInvalid | 97 | return errMovePaneInvalid | ||
| 98 | } | 98 | } | ||
| 99 | return d.movePane(movePaneRequest{ | 99 | return d.movePane(movePaneRequest{ | ||
| 100 | - | Attachment: source.Attachment, | 100 | + | Attachment: source.Attachment, |
| 101 | - | AttachmentToken: source.AttachmentToken, | 101 | + | AttachmentCapability: source.AttachmentCapability, |
| 102 | - | Source: source.Session, | 102 | + | Source: source.Session, |
| 103 | - | SourceTabID: source.TabID, | 103 | + | SourceTabID: source.TabID, |
| 104 | - | SourcePaneID: source.PaneID, | 104 | + | SourcePaneID: source.PaneID, |
| 105 | - | Destination: destination, | 105 | + | Destination: destination, |
| 106 | - | DestinationTabID: target.TabID, | 106 | + | DestinationTabID: target.TabID, |
| 107 | }) | 107 | }) | ||
| 108 | case pickerMoveTab: | 108 | case pickerMoveTab: | ||
| 109 | return d.moveTab(moveTabRequest{ | 109 | return d.moveTab(moveTabRequest{ | ||
| 110 | - | Attachment: source.Attachment, | 110 | + | Attachment: source.Attachment, |
| 111 | - | AttachmentToken: source.AttachmentToken, | 111 | + | AttachmentCapability: source.AttachmentCapability, |
| 112 | - | Source: source.Session, | 112 | + | Source: source.Session, |
| 113 | - | SourceTabID: source.TabID, | 113 | + | SourceTabID: source.TabID, |
| 114 | - | Destination: destination, | 114 | + | Destination: destination, |
| 115 | }) | 115 | }) | ||
| 116 | default: | 116 | default: | ||
| 117 | return errMovePaneInvalid | 117 | return errMovePaneInvalid | ||
M internal/usecase/daemon/move_tab.go +4 -4
| @@ -28,7 +28,7 @@ type moveTabCommit struct { | |||||
| 28 | destination *session | 28 | destination *session | ||
| 29 | admission moveTabAdmission | 29 | admission moveTabAdmission | ||
| 30 | sourceEffectsFrozen bool | 30 | sourceEffectsFrozen bool | ||
| 31 | - | frozenEffects frozenAttachmentEffectGates | 31 | + | frozenEffects attachmentTransitionGuard |
| 32 | syncCleanup syncTimerCleanup | 32 | syncCleanup syncTimerCleanup | ||
| 33 | oldTabCancel context.CancelFunc | 33 | oldTabCancel context.CancelFunc | ||
| 34 | sourceEmpty bool | 34 | sourceEmpty bool | ||
| @@ -110,7 +110,7 @@ func (d *Daemon) moveTab(req moveTabRequest) (result error) { | |||||
| 110 | d.afterMoveTabSourceSnapshot() | 110 | d.afterMoveTabSourceSnapshot() | ||
| 111 | } | 111 | } | ||
| 112 | commit := moveTabCommit{req: req, source: source, destination: destination, admission: *admission} | 112 | commit := moveTabCommit{req: req, source: source, destination: destination, admission: *admission} | ||
| 113 | - | var frozen frozenAttachmentEffectGates | 113 | + | var frozen attachmentTransitionGuard |
| 114 | if admission.finalSource && len(admission.sourceAttachments) != 0 { | 114 | if admission.finalSource && len(admission.sourceAttachments) != 0 { | ||
| 115 | interrupts := make([]attachmentTransportInterrupt, 0, len(admission.sourceAttachments)) | 115 | interrupts := make([]attachmentTransportInterrupt, 0, len(admission.sourceAttachments)) | ||
| 116 | for _, ac := range admission.sourceAttachments { | 116 | for _, ac := range admission.sourceAttachments { | ||
| @@ -190,7 +190,7 @@ func (d *Daemon) snapshotMoveTabAdmission(req moveTabRequest, source, destinatio | |||||
| 190 | if req.Attachment != nil && !attachmentRegisteredLocked(source, req.Attachment) { | 190 | if req.Attachment != nil && !attachmentRegisteredLocked(source, req.Attachment) { | ||
| 191 | return nil, errMoveStaleTarget | 191 | return nil, errMoveStaleTarget | ||
| 192 | } | 192 | } | ||
| 193 | - | if req.AttachmentToken.ac != nil && (req.AttachmentToken.ac != req.Attachment || !moveAttachmentTokenCurrentLocked(req.AttachmentToken, source)) { | 193 | + | if req.AttachmentCapability.ac != nil && (req.AttachmentCapability.ac != req.Attachment || !req.AttachmentCapability.currentInSessionLocked(source)) { |
| 194 | return nil, errMoveStaleTarget | 194 | return nil, errMoveStaleTarget | ||
| 195 | } | 195 | } | ||
| 196 | moved := findMoveTabLocked(source, req.SourceTabID) | 196 | moved := findMoveTabLocked(source, req.SourceTabID) | ||
| @@ -251,7 +251,7 @@ func (c *moveTabCommit) publishLocked(d *Daemon, fencedPanes []*pane) bool { | |||||
| 251 | if c.req.Attachment != nil && !attachmentRegisteredLocked(c.source, c.req.Attachment) { | 251 | if c.req.Attachment != nil && !attachmentRegisteredLocked(c.source, c.req.Attachment) { | ||
| 252 | return false | 252 | return false | ||
| 253 | } | 253 | } | ||
| 254 | - | if c.req.AttachmentToken.ac != nil && (c.req.AttachmentToken.ac != c.req.Attachment || !moveAttachmentTokenCurrentLocked(c.req.AttachmentToken, c.source)) { | 254 | + | if c.req.AttachmentCapability.ac != nil && (c.req.AttachmentCapability.ac != c.req.Attachment || !c.req.AttachmentCapability.currentInSessionLocked(c.source)) { |
| 255 | return false | 255 | return false | ||
| 256 | } | 256 | } | ||
| 257 | moved := c.admission.tab | 257 | moved := c.admission.tab | ||
M internal/usecase/daemon/move_tab_test.go +3 -3
| @@ -408,12 +408,12 @@ func (r *moveTabPurgeRepository) DeleteIncarnation(_ context.Context, id domain. | |||||
| 408 | sourceFenceUnlocked := r.source.layoutApplyMu.TryLock() | 408 | sourceFenceUnlocked := r.source.layoutApplyMu.TryLock() | ||
| 409 | destinationFenceUnlocked := r.destination.layoutApplyMu.TryLock() | 409 | destinationFenceUnlocked := r.destination.layoutApplyMu.TryLock() | ||
| 410 | tabFenceUnlocked := r.moved.layoutApplyMu.TryLock() | 410 | tabFenceUnlocked := r.moved.layoutApplyMu.TryLock() | ||
| 411 | - | roleUnlocked := r.client.attachmentEffects.mu.TryLock() | 411 | + | roleUnlocked := r.client.lifecycle.mu.TryLock() |
| 412 | - | roleStable := roleUnlocked && r.client.attachmentEffects.phase == attachmentEffectsStable | 412 | + | roleStable := roleUnlocked && r.client.lifecycle.phase == attachmentEffectsStable |
| 413 | r.outside = daemonUnlocked && routingUnlocked && sourceUnlocked && destinationUnlocked && tabUnlocked && paneUnlocked && | 413 | r.outside = daemonUnlocked && routingUnlocked && sourceUnlocked && destinationUnlocked && tabUnlocked && paneUnlocked && | ||
| 414 | sourceFenceUnlocked && destinationFenceUnlocked && tabFenceUnlocked && roleStable | 414 | sourceFenceUnlocked && destinationFenceUnlocked && tabFenceUnlocked && roleStable | ||
| 415 | if roleUnlocked { | 415 | if roleUnlocked { | ||
| 416 | - | r.client.attachmentEffects.mu.Unlock() | 416 | + | r.client.lifecycle.mu.Unlock() |
| 417 | } | 417 | } | ||
| 418 | if tabFenceUnlocked { | 418 | if tabFenceUnlocked { | ||
| 419 | r.moved.layoutApplyMu.Unlock() | 419 | r.moved.layoutApplyMu.Unlock() | ||
M internal/usecase/daemon/navigation_attachment_handoff_test.go +15 -15
| @@ -17,25 +17,25 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) { | |||||
| 17 | name string | 17 | name string | ||
| 18 | releasedAction string | 18 | releasedAction string | ||
| 19 | wantErr bool | 19 | wantErr bool | ||
| 20 | - | run func(*Daemon, *session, *session, *attachedClient, *attachmentEffectTicket) error | 20 | + | run func(*Daemon, *session, *session, *attachedClient, *attachmentEffect) error |
| 21 | check func(*testing.T, *Daemon, *session, *session) | 21 | check func(*testing.T, *Daemon, *session, *session) | ||
| 22 | }{ | 22 | }{ | ||
| 23 | { | 23 | { | ||
| 24 | name: "picker selection", releasedAction: "detach", | 24 | name: "picker selection", releasedAction: "detach", | ||
| 25 | - | run: func(d *Daemon, source, target *session, ac *attachedClient, effect *attachmentEffectTicket) error { | 25 | + | run: func(d *Daemon, source, target *session, ac *attachedClient, effect *attachmentEffect) error { |
| 26 | d.enterPicker(source, ac) | 26 | d.enterPicker(source, ac) | ||
| 27 | - | return d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{Session: target.id, TabIndex: 1}, sessionHandoffGuard{}, "picker-select") | 27 | + | return d.switchToTargetForAttachment(effect, picker.Target{Session: target.id, TabIndex: 1}, sessionHandoffGuard{}, "picker-select") |
| 28 | }, | 28 | }, | ||
| 29 | }, | 29 | }, | ||
| 30 | { | 30 | { | ||
| 31 | name: "palette session", releasedAction: "detach", | 31 | name: "palette session", releasedAction: "detach", | ||
| 32 | - | run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffectTicket) error { | 32 | + | run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffect) error { |
| 33 | - | return d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{Session: target.id, TabIndex: 1}, sessionHandoffGuard{}, "palette-session") | 33 | + | return d.switchToTargetForAttachment(effect, picker.Target{Session: target.id, TabIndex: 1}, sessionHandoffGuard{}, "palette-session") |
| 34 | }, | 34 | }, | ||
| 35 | }, | 35 | }, | ||
| 36 | { | 36 | { | ||
| 37 | name: "session overflow", releasedAction: "detach", | 37 | name: "session overflow", releasedAction: "detach", | ||
| 38 | - | run: func(d *Daemon, source, target *session, ac *attachedClient, effect *attachmentEffectTicket) error { | 38 | + | run: func(d *Daemon, source, target *session, ac *attachedClient, effect *attachmentEffect) error { |
| 39 | source.mu.Lock() | 39 | source.mu.Lock() | ||
| 40 | source.name = "alpha" | 40 | source.name = "alpha" | ||
| 41 | source.mu.Unlock() | 41 | source.mu.Unlock() | ||
| @@ -48,13 +48,13 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) { | |||||
| 48 | }, | 48 | }, | ||
| 49 | { | 49 | { | ||
| 50 | name: "stopped session resume", releasedAction: "detach", | 50 | name: "stopped session resume", releasedAction: "detach", | ||
| 51 | - | run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffectTicket) error { | 51 | + | run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffect) error { |
| 52 | d.mu.Lock() | 52 | d.mu.Lock() | ||
| 53 | delete(d.sessions, target.id) | 53 | delete(d.sessions, target.id) | ||
| 54 | d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 9, incarnation: domain.IncarnationID{2}, state: ports.SessionDown} | 54 | d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 9, incarnation: domain.IncarnationID{2}, state: ports.SessionDown} | ||
| 55 | d.mu.Unlock() | 55 | d.mu.Unlock() | ||
| 56 | expectedCreatedAt := int64(9) | 56 | expectedCreatedAt := int64(9) | ||
| 57 | - | return d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "stopped-session") | 57 | + | return d.switchToTargetForAttachment(effect, picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "stopped-session") |
| 58 | }, | 58 | }, | ||
| 59 | check: func(t *testing.T, d *Daemon, _, _ *session) { | 59 | check: func(t *testing.T, d *Daemon, _, _ *session) { | ||
| 60 | d.mu.Lock() | 60 | d.mu.Lock() | ||
| @@ -67,8 +67,8 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) { | |||||
| 67 | }, | 67 | }, | ||
| 68 | { | 68 | { | ||
| 69 | name: "transition prompt creation", releasedAction: "create-session", wantErr: true, | 69 | name: "transition prompt creation", releasedAction: "create-session", wantErr: true, | ||
| 70 | - | run: func(d *Daemon, _ *session, _ *session, _ *attachedClient, effect *attachmentEffectTicket) error { | 70 | + | run: func(d *Daemon, _ *session, _ *session, _ *attachedClient, effect *attachmentEffect) error { |
| 71 | - | return d.createSessionAndSwitchForAttachment(effect.connectionToken(), "created") | 71 | + | return d.createSessionAndSwitchForAttachment(effect, "created") |
| 72 | }, | 72 | }, | ||
| 73 | check: func(t *testing.T, d *Daemon, _, _ *session) { | 73 | check: func(t *testing.T, d *Daemon, _, _ *session) { | ||
| 74 | d.mu.Lock() | 74 | d.mu.Lock() | ||
| @@ -93,9 +93,9 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) { | |||||
| 93 | d.mu.Unlock() | 93 | d.mu.Unlock() | ||
| 94 | 94 | ||||
| 95 | rc := d.attachCoordinator(source, nil, old, true) | 95 | rc := d.attachCoordinator(source, nil, old, true) | ||
| 96 | - | token := source.attachmentToken(old, old.transport()) | 96 | + | token := source.captureAttachmentCapability(old, old.transport()) |
| 97 | token.lease = rc.attachmentLease(old) | 97 | token.lease = rc.attachmentLease(old) | ||
| 98 | - | old.publishAttachmentCapability(token) | 98 | + | old.installTestAttachmentCapability(token) |
| 99 | effect, admitted := old.beginAttachmentEffect(token) | 99 | effect, admitted := old.beginAttachmentEffect(token) | ||
| 100 | require.True(t, admitted) | 100 | require.True(t, admitted) | ||
| 101 | 101 | ||||
| @@ -153,9 +153,9 @@ func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing. | |||||
| 153 | d.mu.Unlock() | 153 | d.mu.Unlock() | ||
| 154 | 154 | ||||
| 155 | rc := d.attachCoordinator(source, nil, old, true) | 155 | rc := d.attachCoordinator(source, nil, old, true) | ||
| 156 | - | token := source.attachmentToken(old, old.transport()) | 156 | + | token := source.captureAttachmentCapability(old, old.transport()) |
| 157 | token.lease = rc.attachmentLease(old) | 157 | token.lease = rc.attachmentLease(old) | ||
| 158 | - | old.publishAttachmentCapability(token) | 158 | + | old.installTestAttachmentCapability(token) |
| 159 | effect, admitted := old.beginAttachmentEffect(token) | 159 | effect, admitted := old.beginAttachmentEffect(token) | ||
| 160 | require.True(t, admitted) | 160 | require.True(t, admitted) | ||
| 161 | 161 | ||||
| @@ -171,7 +171,7 @@ func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing. | |||||
| 171 | expectedCreatedAt := int64(7) | 171 | expectedCreatedAt := int64(7) | ||
| 172 | done := make(chan error, 1) | 172 | done := make(chan error, 1) | ||
| 173 | go func() { | 173 | go func() { | ||
| 174 | - | done <- d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "picker-stopped") | 174 | + | done <- d.switchToTargetForAttachment(effect, picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "picker-stopped") |
| 175 | }() | 175 | }() | ||
| 176 | select { | 176 | select { | ||
| 177 | case <-ended: | 177 | case <-ended: | ||
M internal/usecase/daemon/navigation_attachment_incarnation_test.go +8 -8
| @@ -19,9 +19,9 @@ func TestNavigationHandoffRejectsReplacedTargetLifecycleAtPublication(t *testing | |||||
| 19 | d.mu.Unlock() | 19 | d.mu.Unlock() | ||
| 20 | 20 | ||||
| 21 | rc := d.attachCoordinator(source, nil, ac, true) | 21 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 22 | - | token := source.attachmentToken(ac, ac.transport()) | 22 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 23 | token.lease = rc.attachmentLease(ac) | 23 | token.lease = rc.attachmentLease(ac) | ||
| 24 | - | ac.publishAttachmentCapability(token) | 24 | + | ac.installTestAttachmentCapability(token) |
| 25 | effect, admitted := ac.beginAttachmentEffect(token) | 25 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 26 | require.True(t, admitted) | 26 | require.True(t, admitted) | ||
| 27 | 27 | ||||
| @@ -30,7 +30,7 @@ func TestNavigationHandoffRejectsReplacedTargetLifecycleAtPublication(t *testing | |||||
| 30 | // transition's lifecycle fence is validated. | 30 | // transition's lifecycle fence is validated. | ||
| 31 | target.incarnation = domain.SessionLifecycleID{2} | 31 | target.incarnation = domain.SessionLifecycleID{2} | ||
| 32 | } | 32 | } | ||
| 33 | - | err := d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{ | 33 | + | err := d.switchToTargetForAttachment(effect, picker.Target{ |
| 34 | Session: target.id, | 34 | Session: target.id, | ||
| 35 | Incarnation: domain.SessionLifecycleID{1}, | 35 | Incarnation: domain.SessionLifecycleID{1}, | ||
| 36 | TabID: domain.TabStableID(target.tabs[1].stableID), | 36 | TabID: domain.TabStableID(target.tabs[1].stableID), | ||
| @@ -69,9 +69,9 @@ func TestNavigationHandoffDoesNotMutateAfterInitiatorIncarnationChanges(t *testi | |||||
| 69 | name: "transport incarnation rebound on same attachment", | 69 | name: "transport incarnation rebound on same attachment", | ||
| 70 | rebind: func(t *testing.T, _ *Daemon, source *session, ac *attachedClient) { | 70 | rebind: func(t *testing.T, _ *Daemon, source *session, ac *attachedClient) { | ||
| 71 | ac.replaceTransport(&closeTrackingTransport{}) | 71 | ac.replaceTransport(&closeTrackingTransport{}) | ||
| 72 | - | current := source.attachmentToken(ac, ac.transport()) | 72 | + | current := source.captureAttachmentCapability(ac, ac.transport()) |
| 73 | current.lease = source.renderCoordinator().attachmentLease(ac) | 73 | current.lease = source.renderCoordinator().attachmentLease(ac) | ||
| 74 | - | ac.publishAttachmentCapability(current) | 74 | + | ac.installTestAttachmentCapability(current) |
| 75 | }, | 75 | }, | ||
| 76 | }, | 76 | }, | ||
| 77 | } | 77 | } | ||
| @@ -88,9 +88,9 @@ func TestNavigationHandoffDoesNotMutateAfterInitiatorIncarnationChanges(t *testi | |||||
| 88 | d.mu.Unlock() | 88 | d.mu.Unlock() | ||
| 89 | 89 | ||||
| 90 | rc := d.attachCoordinator(source, nil, ac, true) | 90 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 91 | - | token := source.attachmentToken(ac, ac.transport()) | 91 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 92 | token.lease = rc.attachmentLease(ac) | 92 | token.lease = rc.attachmentLease(ac) | ||
| 93 | - | ac.publishAttachmentCapability(token) | 93 | + | ac.installTestAttachmentCapability(token) |
| 94 | effect, admitted := ac.beginAttachmentEffect(token) | 94 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 95 | require.True(t, admitted) | 95 | require.True(t, admitted) | ||
| 96 | 96 | ||||
| @@ -104,7 +104,7 @@ func TestNavigationHandoffDoesNotMutateAfterInitiatorIncarnationChanges(t *testi | |||||
| 104 | } | 104 | } | ||
| 105 | done := make(chan error, 1) | 105 | done := make(chan error, 1) | ||
| 106 | go func() { | 106 | go func() { | ||
| 107 | - | done <- d.switchToTargetForAttachment(effect.connectionToken(), picker.Target{Session: target.id, TabIndex: 1}, sessionHandoffGuard{}, "test-handoff") | 107 | + | done <- d.switchToTargetForAttachment(effect, picker.Target{Session: target.id, TabIndex: 1}, sessionHandoffGuard{}, "test-handoff") |
| 108 | }() | 108 | }() | ||
| 109 | awaitTestCompletion(t, ended, "test handoff did not release its role ticket") | 109 | awaitTestCompletion(t, ended, "test handoff did not release its role ticket") | ||
| 110 | tt.rebind(t, d, source, ac) | 110 | tt.rebind(t, d, source, ac) | ||
M internal/usecase/daemon/overlay_runtime.go +3 -3
| @@ -53,7 +53,7 @@ type overlayRuntime struct { | |||||
| 53 | promptMu sync.Mutex | 53 | promptMu sync.Mutex | ||
| 54 | prompt *promptui.Model | 54 | prompt *promptui.Model | ||
| 55 | promptSubmit func(string) error | 55 | promptSubmit func(string) error | ||
| 56 | - | promptTransitionSubmit func(string, attachmentConnectionToken) error | 56 | + | promptTransitionSubmit func(string, *attachmentEffect) error |
| 57 | promptPending []byte | 57 | promptPending []byte | ||
| 58 | 58 | ||||
| 59 | resizeMu sync.Mutex | 59 | resizeMu sync.Mutex | ||
| @@ -247,12 +247,12 @@ func (rt *overlayRuntime) clearCopyModeForPane(p *pane) bool { | |||||
| 247 | return active || prePublication | 247 | return active || prePublication | ||
| 248 | } | 248 | } | ||
| 249 | 249 | ||||
| 250 | - | func (rt *overlayRuntime) HandleInput(d *Daemon, data []byte, effects ...*attachmentEffectTicket) bool { | 250 | + | func (rt *overlayRuntime) HandleInput(d *Daemon, data []byte, effects ...*attachmentEffect) bool { |
| 251 | if rt == nil || rt.ac == nil { | 251 | if rt == nil || rt.ac == nil { | ||
| 252 | return false | 252 | return false | ||
| 253 | } | 253 | } | ||
| 254 | ac := rt.ac | 254 | ac := rt.ac | ||
| 255 | - | var effect *attachmentEffectTicket | 255 | + | var effect *attachmentEffect |
| 256 | if len(effects) != 0 { | 256 | if len(effects) != 0 { | ||
| 257 | effect = effects[0] | 257 | effect = effects[0] | ||
| 258 | } | 258 | } | ||
M internal/usecase/daemon/palette.go +22 -16
| @@ -329,7 +329,7 @@ func (d *Daemon) recordPaletteUse(code string) { | |||||
| 329 | d.paletteRecent = recent | 329 | d.paletteRecent = recent | ||
| 330 | } | 330 | } | ||
| 331 | 331 | ||||
| 332 | - | func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ...*attachmentEffectTicket) { | 332 | + | func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ...*attachmentEffect) { |
| 333 | entry := ac.currentAttachmentSession() | 333 | entry := ac.currentAttachmentSession() | ||
| 334 | if entry == nil { | 334 | if entry == nil { | ||
| 335 | return | 335 | return | ||
| @@ -350,7 +350,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ... | |||||
| 350 | var rawQuery string | 350 | var rawQuery string | ||
| 351 | changed, cancel, execute := false, false, false | 351 | changed, cancel, execute := false, false, false | ||
| 352 | var closedDiscovery remoteDiscoveryInstance | 352 | var closedDiscovery remoteDiscoveryInstance | ||
| 353 | - | var effect *attachmentEffectTicket | 353 | + | var effect *attachmentEffect |
| 354 | if len(effects) != 0 { | 354 | if len(effects) != 0 { | ||
| 355 | effect = effects[0] | 355 | effect = effects[0] | ||
| 356 | } | 356 | } | ||
| @@ -490,7 +490,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ... | |||||
| 490 | RemoteKey: &remoteKey, RemoteTarget: &remoteTarget, RemoteHost: remoteKey.Host, | 490 | RemoteKey: &remoteKey, RemoteTarget: &remoteTarget, RemoteHost: remoteKey.Host, | ||
| 491 | UnavailableReason: remoteUnavailableReason, TabIndex: -1, | 491 | UnavailableReason: remoteUnavailableReason, TabIndex: -1, | ||
| 492 | } | 492 | } | ||
| 493 | - | err := d.switchToTargetForAttachment(effect.connectionToken(), target, sessionHandoffGuard{}, "palette-remote-session") | 493 | + | err := d.switchToTargetForAttachment(effect, target, sessionHandoffGuard{}, "palette-remote-session") |
| 494 | if err == nil { | 494 | if err == nil { | ||
| 495 | d.closeExecutedPalette(ac, generation, rawQuery) | 495 | d.closeExecutedPalette(ac, generation, rawQuery) | ||
| 496 | return | 496 | return | ||
| @@ -529,7 +529,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ... | |||||
| 529 | } | 529 | } | ||
| 530 | var err error | 530 | var err error | ||
| 531 | if effect != nil { | 531 | if effect != nil { | ||
| 532 | - | err = d.switchToTargetForAttachment(effect.connectionToken(), target, sessionHandoffGuard{allowSamePeer: true}, "palette-session") | 532 | + | err = d.switchToTargetForAttachment(effect, target, sessionHandoffGuard{allowSamePeer: true}, "palette-session") |
| 533 | } else { | 533 | } else { | ||
| 534 | err = d.switchToTarget(sess, ac, target) | 534 | err = d.switchToTarget(sess, ac, target) | ||
| 535 | } | 535 | } | ||
| @@ -582,7 +582,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ... | |||||
| 582 | sess.dispatchMu.Unlock() | 582 | sess.dispatchMu.Unlock() | ||
| 583 | if attachmentHandoff { | 583 | if attachmentHandoff { | ||
| 584 | if current := ac.currentSession(); current != nil { | 584 | if current := ac.currentSession(); current != nil { | ||
| 585 | - | currentToken := current.attachmentToken(ac, ac.transport()) | 585 | + | currentToken := current.captureAttachmentCapability(ac, ac.transport()) |
| 586 | fresh, admitted := ac.beginAttachmentEffect(currentToken) | 586 | fresh, admitted := ac.beginAttachmentEffect(currentToken) | ||
| 587 | if d.closeExecutedPalette(ac, generation, rawQuery) { | 587 | if d.closeExecutedPalette(ac, generation, rawQuery) { | ||
| 588 | d.invalidateRender(current, ac, true, "palette.go") | 588 | d.invalidateRender(current, ac, true, "palette.go") | ||
| @@ -681,7 +681,7 @@ type paletteExec struct { | |||||
| 681 | ac *attachedClient | 681 | ac *attachedClient | ||
| 682 | routeSnapshot ports.RecentRouteSnapshot | 682 | routeSnapshot ports.RecentRouteSnapshot | ||
| 683 | actions daemonActionRunner | 683 | actions daemonActionRunner | ||
| 684 | - | effect *attachmentEffectTicket | 684 | + | effect *attachmentEffect |
| 685 | redrawClosedPalette bool | 685 | redrawClosedPalette bool | ||
| 686 | } | 686 | } | ||
| 687 | 687 | ||||
| @@ -719,14 +719,14 @@ func (e paletteExec) CreateSession() error { | |||||
| 719 | if entry == nil { | 719 | if entry == nil { | ||
| 720 | entry = e.sess | 720 | entry = e.sess | ||
| 721 | } | 721 | } | ||
| 722 | - | e.d.enterTransitionPrompt(entry, e.ac, " Create session ", "", func(name string, token attachmentConnectionToken) error { | 722 | + | e.d.enterTransitionPrompt(entry, e.ac, " Create session ", "", func(name string, effect *attachmentEffect) error { |
| 723 | - | if token.ac == nil { | 723 | + | if effect == nil || effect.ac == nil { |
| 724 | if e.sess == nil { | 724 | if e.sess == nil { | ||
| 725 | return errAttachmentTransition | 725 | return errAttachmentTransition | ||
| 726 | } | 726 | } | ||
| 727 | return e.d.createSessionAndSwitch(e.sess, e.ac, name) | 727 | return e.d.createSessionAndSwitch(e.sess, e.ac, name) | ||
| 728 | } | 728 | } | ||
| 729 | - | return e.d.createSessionAndSwitchForAttachment(token, name) | 729 | + | return e.d.createSessionAndSwitchForAttachment(effect, name) |
| 730 | }) | 730 | }) | ||
| 731 | return nil | 731 | return nil | ||
| 732 | } | 732 | } | ||
| @@ -735,14 +735,14 @@ func (e paletteExec) CreateSessionNamed(name string) error { | |||||
| 735 | if e.effect == nil { | 735 | if e.effect == nil { | ||
| 736 | return e.d.createSessionAndSwitch(e.sess, e.ac, name) | 736 | return e.d.createSessionAndSwitch(e.sess, e.ac, name) | ||
| 737 | } | 737 | } | ||
| 738 | - | return e.d.createSessionAndSwitchForAttachment(e.effect.connectionToken(), name) | 738 | + | return e.d.createSessionAndSwitchForAttachment(e.effect, name) |
| 739 | } | 739 | } | ||
| 740 | 740 | ||||
| 741 | func (e paletteExec) CreateEphemeralSession() error { | 741 | func (e paletteExec) CreateEphemeralSession() error { | ||
| 742 | if e.effect == nil { | 742 | if e.effect == nil { | ||
| 743 | return e.d.createEphemeralSessionAndSwitch(e.sess, e.ac) | 743 | return e.d.createEphemeralSessionAndSwitch(e.sess, e.ac) | ||
| 744 | } | 744 | } | ||
| 745 | - | return e.d.createEphemeralSessionAndSwitchForAttachment(e.effect.connectionToken()) | 745 | + | return e.d.createEphemeralSessionAndSwitchForAttachment(e.effect) |
| 746 | } | 746 | } | ||
| 747 | 747 | ||||
| 748 | func (e paletteExec) CloseTab() error { | 748 | func (e paletteExec) CloseTab() error { | ||
| @@ -839,7 +839,7 @@ func (e paletteExec) ToggleFloatingPane() error { | |||||
| 839 | 839 | ||||
| 840 | func (e paletteExec) BackSession() error { | 840 | func (e paletteExec) BackSession() error { | ||
| 841 | if e.effect != nil { | 841 | if e.effect != nil { | ||
| 842 | - | return e.d.backSessionForAttachment(e.effect.connectionToken()) | 842 | + | return e.d.backSessionForAttachment(e.effect) |
| 843 | } | 843 | } | ||
| 844 | e.d.backSession(e.sess, e.ac) | 844 | e.d.backSession(e.sess, e.ac) | ||
| 845 | return nil | 845 | return nil | ||
| @@ -847,7 +847,7 @@ func (e paletteExec) BackSession() error { | |||||
| 847 | 847 | ||||
| 848 | func (e paletteExec) Detach() error { | 848 | func (e paletteExec) Detach() error { | ||
| 849 | if e.effect != nil { | 849 | if e.effect != nil { | ||
| 850 | - | if !e.d.clientGoneForAttachment(e.effect.connectionToken(), true) { | 850 | + | if !e.d.clientGoneForAttachment(e.effect, true) { |
| 851 | return errAttachmentTransition | 851 | return errAttachmentTransition | ||
| 852 | } | 852 | } | ||
| 853 | return nil | 853 | return nil | ||
| @@ -912,7 +912,7 @@ func (e paletteExec) RenameTabTo(name string) error { | |||||
| 912 | 912 | ||||
| 913 | func (e paletteExec) OpenSessionPicker() error { | 913 | func (e paletteExec) OpenSessionPicker() error { | ||
| 914 | if e.ac != nil && e.ac.navigationCapabilities&ports.NavigationCapabilityHomePicker != 0 && e.effect != nil { | 914 | if e.ac != nil && e.ac.navigationCapabilities&ports.NavigationCapabilityHomePicker != 0 && e.effect != nil { | ||
| 915 | - | return e.d.sendNavigationActionForAttachment(e.effect.connectionToken(), ports.NavigationOpenHomePicker) | 915 | + | return e.d.sendNavigationActionForAttachment(e.effect, ports.NavigationOpenHomePicker) |
| 916 | } | 916 | } | ||
| 917 | e.d.enterPicker(e.sess, e.ac) | 917 | e.d.enterPicker(e.sess, e.ac) | ||
| 918 | return nil | 918 | return nil | ||
| @@ -957,12 +957,18 @@ func (e paletteExec) NavigateRecentRoute(action ports.RouteNavigationAction) err | |||||
| 957 | e.d.beforeRecentSessionHandoff() | 957 | e.d.beforeRecentSessionHandoff() | ||
| 958 | } | 958 | } | ||
| 959 | if e.effect != nil { | 959 | if e.effect != nil { | ||
| 960 | - | return e.d.sendRecentRouteNavigationActionForAttachment(e.effect.connectionToken(), action) | 960 | + | return e.d.sendRecentRouteNavigationActionForAttachment(e.effect, action) |
| 961 | } | 961 | } | ||
| 962 | if e.ac == nil || e.attachment == nil { | 962 | if e.ac == nil || e.attachment == nil { | ||
| 963 | return errAttachmentTransition | 963 | return errAttachmentTransition | ||
| 964 | } | 964 | } | ||
| 965 | - | return e.d.sendRecentRouteNavigationActionForAttachment(e.attachment.attachmentToken(e.ac, e.ac.transport()), action) | 965 | + | capability := e.attachment.captureAttachmentCapability(e.ac, e.ac.transport()) |
| 966 | + | effect, admitted := e.ac.beginAttachmentEffect(capability) | |||
| 967 | + | if !admitted { | |||
| 968 | + | return errAttachmentTransition | |||
| 969 | + | } | |||
| 970 | + | defer effect.End() | |||
| 971 | + | return e.d.sendRecentRouteNavigationActionForAttachment(effect, action) | |||
| 966 | } | 972 | } | ||
| 967 | 973 | ||||
| 968 | func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeui.Styles) (renderer.Frame, []renderer.Damage) { | 974 | func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeui.Styles) (renderer.Frame, []renderer.Damage) { | ||
M internal/usecase/daemon/palette_test.go +8 -9
| @@ -32,18 +32,17 @@ func testRecentRouteSnapshot() ports.RecentRouteSnapshot { | |||||
| 32 | } | 32 | } | ||
| 33 | } | 33 | } | ||
| 34 | 34 | ||||
| 35 | - | func beginRecentRoutePaletteEffect(t *testing.T, d *Daemon, sess *session, ac *attachedClient) attachmentConnectionToken { | 35 | + | func beginRecentRoutePaletteEffect(t *testing.T, d *Daemon, sess *session, ac *attachedClient) *attachmentEffect { |
| 36 | t.Helper() | 36 | t.Helper() | ||
| 37 | transport := ac.transport() | 37 | transport := ac.transport() | ||
| 38 | rc := d.attachCoordinator(sess, nil, ac, true) | 38 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| 39 | - | token := sess.attachmentToken(ac, transport) | 39 | + | token := sess.captureAttachmentCapability(ac, transport) |
| 40 | token.lease = rc.attachmentLease(ac) | 40 | token.lease = rc.attachmentLease(ac) | ||
| 41 | - | ac.publishAttachmentCapability(token) | 41 | + | ac.installTestAttachmentCapability(token) |
| 42 | effect, admitted := ac.beginAttachmentEffect(token) | 42 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 43 | require.True(t, admitted) | 43 | require.True(t, admitted) | ||
| 44 | t.Cleanup(effect.End) | 44 | t.Cleanup(effect.End) | ||
| 45 | - | token.effect = effect | 45 | + | return effect |
| 46 | - | return token | |||
| 47 | } | 46 | } | ||
| 48 | 47 | ||||
| 49 | func TestCaptureOverlayLayersPreservesPaletteDescriptionSurfaceAcrossFallbacks(t *testing.T) { | 48 | func TestCaptureOverlayLayersPreservesPaletteDescriptionSurfaceAcrossFallbacks(t *testing.T) { | ||
| @@ -355,14 +354,14 @@ func TestPaletteSelectedStoppedSessionResumesAndSwitches(t *testing.T) { | |||||
| 355 | 354 | ||||
| 356 | d.handleInput(current, ac, []byte("\x1b ")) | 355 | d.handleInput(current, ac, []byte("\x1b ")) | ||
| 357 | awaitFrame(t, sends, ports.MsgOutput) | 356 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 358 | - | generation := ac.connectionGeneration.Load() | 357 | + | generation := ac.lifecycle.generationValue() |
| 359 | d.handleInput(current, ac, []byte("stopped\r")) | 358 | d.handleInput(current, ac, []byte("stopped\r")) | ||
| 360 | 359 | ||||
| 361 | resumed := ac.currentSession() | 360 | resumed := ac.currentSession() | ||
| 362 | require.Equal(t, "stopped", resumed.name) | 361 | require.Equal(t, "stopped", resumed.name) | ||
| 363 | require.Equal(t, int64(42), resumed.createdAt) | 362 | require.Equal(t, int64(42), resumed.createdAt) | ||
| 364 | require.Equal(t, true, resumed.attachmentRegistered(ac)) | 363 | require.Equal(t, true, resumed.attachmentRegistered(ac)) | ||
| 365 | - | require.Greater(t, ac.connectionGeneration.Load(), generation, "stopped-session handoff must publish through the attachment transition") | 364 | + | require.Greater(t, ac.lifecycle.generationValue(), generation, "stopped-session handoff must publish through the attachment transition") |
| 366 | require.False(t, ac.overlays.paletteActive()) | 365 | require.False(t, ac.overlays.paletteActive()) | ||
| 367 | firstPaint := awaitFrame(t, sends, ports.MsgOutput) | 366 | firstPaint := awaitFrame(t, sends, ports.MsgOutput) | ||
| 368 | firstOutput, err := ports.UnmarshalOutput(firstPaint.Payload) | 367 | firstOutput, err := ports.UnmarshalOutput(firstPaint.Payload) | ||
| @@ -1004,7 +1003,7 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) { | |||||
| 1004 | require.True(t, ac.overlays.promptActive()) | 1003 | require.True(t, ac.overlays.promptActive()) | ||
| 1005 | require.Contains(t, string(promptOutput.Data), "Create session") | 1004 | require.Contains(t, string(promptOutput.Data), "Create session") | ||
| 1006 | 1005 | ||||
| 1007 | - | generation := ac.connectionGeneration.Load() | 1006 | + | generation := ac.lifecycle.generationValue() |
| 1008 | d.handleInput(sess, ac, []byte("scratch\r")) | 1007 | d.handleInput(sess, ac, []byte("scratch\r")) | ||
| 1009 | // The submit first paints the newly attached session while the prompt is | 1008 | // The submit first paints the newly attached session while the prompt is | ||
| 1010 | // still open, then handlePromptInput closes the prompt and repaints the | 1009 | // still open, then handlePromptInput closes the prompt and repaints the | ||
| @@ -1026,7 +1025,7 @@ func TestPaletteCNSPromptsForSessionNameThenCreatesAndSwitches(t *testing.T) { | |||||
| 1026 | require.False(t, newSess.ephemeral) | 1025 | require.False(t, newSess.ephemeral) | ||
| 1027 | require.Contains(t, newSess.snapshotAttachments(), ac) | 1026 | require.Contains(t, newSess.snapshotAttachments(), ac) | ||
| 1028 | require.Equal(t, true, newSess.attachmentRegistered(ac)) | 1027 | require.Equal(t, true, newSess.attachmentRegistered(ac)) | ||
| 1029 | - | require.Greater(t, ac.connectionGeneration.Load(), generation, "new-session handoff must publish through the attachment transition") | 1028 | + | require.Greater(t, ac.lifecycle.generationValue(), generation, "new-session handoff must publish through the attachment transition") |
| 1030 | require.Contains(t, string(finalOutput.Data), "scratch") | 1029 | require.Contains(t, string(finalOutput.Data), "scratch") | ||
| 1031 | require.NotContains(t, string(finalOutput.Data), "Create session") | 1030 | require.NotContains(t, string(finalOutput.Data), "Create session") | ||
| 1032 | } | 1031 | } | ||
M internal/usecase/daemon/pane_actions.go +3 -3
| @@ -348,7 +348,7 @@ func (d *Daemon) closePaneLocked(sess *session, tb *tab, id layout.PaneID, ac *a | |||||
| 348 | return d.closePaneLockedWithEffect(sess, tb, id, ac, repaint, nil) | 348 | return d.closePaneLockedWithEffect(sess, tb, id, ac, repaint, nil) | ||
| 349 | } | 349 | } | ||
| 350 | 350 | ||||
| 351 | - | func (d *Daemon) closePaneLockedWithEffect(sess *session, tb *tab, id layout.PaneID, ac *attachedClient, repaint bool, effect *attachmentEffectTicket) error { | 351 | + | func (d *Daemon) closePaneLockedWithEffect(sess *session, tb *tab, id layout.PaneID, ac *attachedClient, repaint bool, effect *attachmentEffect) error { |
| 352 | if tb == nil { | 352 | if tb == nil { | ||
| 353 | return layout.ErrNotFound | 353 | return layout.ErrNotFound | ||
| 354 | } | 354 | } | ||
| @@ -406,7 +406,7 @@ func (d *Daemon) closePaneLockedWithEffect(sess *session, tb *tab, id layout.Pan | |||||
| 406 | return nil | 406 | return nil | ||
| 407 | } | 407 | } | ||
| 408 | 408 | ||||
| 409 | - | func (d *Daemon) focusDir(sess *session, ac *attachedClient, dir layout.Direction, effect *attachmentEffectTicket) error { | 409 | + | func (d *Daemon) focusDir(sess *session, ac *attachedClient, dir layout.Direction, effect *attachmentEffect) error { |
| 410 | target := resolveDaemonActionTargetForAttachment(sess, ac) | 410 | target := resolveDaemonActionTargetForAttachment(sess, ac) | ||
| 411 | oldFocus := layout.PaneID("") | 411 | oldFocus := layout.PaneID("") | ||
| 412 | if target.pane != nil { | 412 | if target.pane != nil { | ||
| @@ -434,7 +434,7 @@ func (d *Daemon) focusDir(sess *session, ac *attachedClient, dir layout.Directio | |||||
| 434 | return errNoNeighbor | 434 | return errNoNeighbor | ||
| 435 | } | 435 | } | ||
| 436 | if effect != nil { | 436 | if effect != nil { | ||
| 437 | - | return d.switchToTargetForAttachment(effect.connectionToken(), sessionTarget, sessionHandoffGuard{expectedSource: target.tab}, "overflow-session") | 437 | + | return d.switchToTargetForAttachment(effect, sessionTarget, sessionHandoffGuard{expectedSource: target.tab}, "overflow-session") |
| 438 | } | 438 | } | ||
| 439 | return d.commitSessionOverflow(sess, ac, target.tab, sessionTarget) | 439 | return d.commitSessionOverflow(sess, ac, target.tab, sessionTarget) | ||
| 440 | } | 440 | } | ||
M internal/usecase/daemon/parked_route.go +85 -60
| @@ -50,9 +50,12 @@ func stopParkedRouteLeaseLocked(lease *parkedRouteLease) { | |||||
| 50 | } | 50 | } | ||
| 51 | } | 51 | } | ||
| 52 | 52 | ||||
| 53 | - | func (d *Daemon) armParkedRoute(token attachmentConnectionToken) (ports.ParkedRouteLeaseID, error) { | 53 | + | func (d *Daemon) armParkedRoute(effect *attachmentEffect) (ports.ParkedRouteLeaseID, error) { |
| 54 | - | ac := token.ac | 54 | + | if !effect.current() { |
| 55 | - | if ac == nil || !token.attachmentCurrent() || ac.navigationCapabilities&ports.NavigationCapabilityHomePicker == 0 { | 55 | + | return ports.ParkedRouteLeaseID{}, errAttachmentTransition |
| 56 | + | } | |||
| 57 | + | ac := effect.ac | |||
| 58 | + | if ac == nil || ac.navigationCapabilities&ports.NavigationCapabilityHomePicker == 0 { | |||
| 56 | return ports.ParkedRouteLeaseID{}, errAttachmentTransition | 59 | return ports.ParkedRouteLeaseID{}, errAttachmentTransition | ||
| 57 | } | 60 | } | ||
| 58 | id, err := newParkedRouteLeaseID() | 61 | id, err := newParkedRouteLeaseID() | ||
| @@ -60,7 +63,7 @@ func (d *Daemon) armParkedRoute(token attachmentConnectionToken) (ports.ParkedRo | |||||
| 60 | return ports.ParkedRouteLeaseID{}, err | 63 | return ports.ParkedRouteLeaseID{}, err | ||
| 61 | } | 64 | } | ||
| 62 | lease := &parkedRouteLease{ | 65 | lease := &parkedRouteLease{ | ||
| 63 | - | id: id, generation: token.generation, transport: token.transport, | 66 | + | id: id, generation: effect.generation, transport: effect.transport, |
| 64 | expiresAt: d.clock.Now().Add(parkedRouteLeaseTTL), | 67 | expiresAt: d.clock.Now().Add(parkedRouteLeaseTTL), | ||
| 65 | timer: d.clock.NewTimer(parkedRouteLeaseTTL), stop: make(chan struct{}), | 68 | timer: d.clock.NewTimer(parkedRouteLeaseTTL), stop: make(chan struct{}), | ||
| 66 | } | 69 | } | ||
| @@ -103,7 +106,7 @@ func (d *Daemon) expireParkedRoute(ac *attachedClient, lease *parkedRouteLease) | |||||
| 103 | ac.parkedRouteOutput.Store(false) | 106 | ac.parkedRouteOutput.Store(false) | ||
| 104 | ac.parkedRouteFullPending.Store(false) | 107 | ac.parkedRouteFullPending.Store(false) | ||
| 105 | ac.parkedRouteMu.Unlock() | 108 | ac.parkedRouteMu.Unlock() | ||
| 106 | - | if ac.connectionGeneration.Load() == lease.generation && ac.transportSnapshotCurrent(lease.transport) { | 109 | + | if ac.lifecycle.generationValue() == lease.generation && ac.transportSnapshotCurrent(lease.transport) { |
| 107 | _ = ac.closeCapturedTransport(lease.transport.transport) | 110 | _ = ac.closeCapturedTransport(lease.transport.transport) | ||
| 108 | } | 111 | } | ||
| 109 | } | 112 | } | ||
| @@ -120,8 +123,8 @@ func (ac *attachedClient) clearParkedRoute() { | |||||
| 120 | ac.parkedRouteMu.Unlock() | 123 | ac.parkedRouteMu.Unlock() | ||
| 121 | } | 124 | } | ||
| 122 | 125 | ||||
| 123 | - | func (ac *attachedClient) parkedRouteStatus(token attachmentConnectionToken, id ports.ParkedRouteLeaseID, now time.Time, suspended bool) ports.ParkedRouteStatus { | 126 | + | func (ac *attachedClient) parkedRouteStatus(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time, suspended bool) ports.ParkedRouteStatus { |
| 124 | - | if ac == nil || !token.attachmentCurrent() { | 127 | + | if ac == nil || !token.current() { |
| 125 | return ports.ParkedRouteRejected | 128 | return ports.ParkedRouteRejected | ||
| 126 | } | 129 | } | ||
| 127 | ac.parkedRouteMu.Lock() | 130 | ac.parkedRouteMu.Lock() | ||
| @@ -143,7 +146,7 @@ func (ac *attachedClient) parkedRouteStatus(token attachmentConnectionToken, id | |||||
| 143 | return 0 | 146 | return 0 | ||
| 144 | } | 147 | } | ||
| 145 | 148 | ||||
| 146 | - | func (ac *attachedClient) prepareParkedRoute(token attachmentConnectionToken, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus { | 149 | + | func (ac *attachedClient) prepareParkedRoute(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus { |
| 147 | if status := ac.parkedRouteStatus(token, id, now, false); status != 0 { | 150 | if status := ac.parkedRouteStatus(token, id, now, false); status != 0 { | ||
| 148 | return status | 151 | return status | ||
| 149 | } | 152 | } | ||
| @@ -158,7 +161,7 @@ func (ac *attachedClient) prepareParkedRoute(token attachmentConnectionToken, id | |||||
| 158 | return 0 | 161 | return 0 | ||
| 159 | } | 162 | } | ||
| 160 | 163 | ||||
| 161 | - | func (ac *attachedClient) consumeParkedRoute(token attachmentConnectionToken, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus { | 164 | + | func (ac *attachedClient) consumeParkedRoute(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus { |
| 162 | if status := ac.parkedRouteStatus(token, id, now, true); status != 0 { | 165 | if status := ac.parkedRouteStatus(token, id, now, true); status != 0 { | ||
| 163 | return status | 166 | return status | ||
| 164 | } | 167 | } | ||
| @@ -173,8 +176,8 @@ func (ac *attachedClient) consumeParkedRoute(token attachmentConnectionToken, id | |||||
| 173 | return 0 | 176 | return 0 | ||
| 174 | } | 177 | } | ||
| 175 | 178 | ||||
| 176 | - | func (ac *attachedClient) beginParkedRouteSwitch(token attachmentConnectionToken, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus { | 179 | + | func (ac *attachedClient) beginParkedRouteSwitch(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus { |
| 177 | - | if ac == nil || !token.attachmentCurrent() { | 180 | + | if ac == nil || !token.current() { |
| 178 | return ports.ParkedRouteRejected | 181 | return ports.ParkedRouteRejected | ||
| 179 | } | 182 | } | ||
| 180 | ac.parkedRouteMu.Lock() | 183 | ac.parkedRouteMu.Lock() | ||
| @@ -194,7 +197,7 @@ func (ac *attachedClient) beginParkedRouteSwitch(token attachmentConnectionToken | |||||
| 194 | return 0 | 197 | return 0 | ||
| 195 | } | 198 | } | ||
| 196 | 199 | ||||
| 197 | - | func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentConnectionToken, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus { | 200 | + | func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus { |
| 198 | if ac == nil { | 201 | if ac == nil { | ||
| 199 | return ports.ParkedRouteRejected | 202 | return ports.ParkedRouteRejected | ||
| 200 | } | 203 | } | ||
| @@ -215,8 +218,8 @@ func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentConnectionToke | |||||
| 215 | return 0 | 218 | return 0 | ||
| 216 | } | 219 | } | ||
| 217 | 220 | ||||
| 218 | - | func (ac *attachedClient) consumePublishedParkedRoute(previous, published attachmentConnectionToken, id ports.ParkedRouteLeaseID) ports.ParkedRouteStatus { | 221 | + | func (ac *attachedClient) consumePublishedParkedRoute(previous, published attachmentCapability, id ports.ParkedRouteLeaseID) ports.ParkedRouteStatus { |
| 219 | - | if ac == nil || published.ac != ac || !published.attachmentCurrent() { | 222 | + | if ac == nil || published.ac != ac || !published.current() { |
| 220 | return ports.ParkedRouteRejected | 223 | return ports.ParkedRouteRejected | ||
| 221 | } | 224 | } | ||
| 222 | ac.parkedRouteMu.Lock() | 225 | ac.parkedRouteMu.Lock() | ||
| @@ -241,12 +244,12 @@ func parkedRouteResponseFrame(response ports.ParkedRouteResponse) (ports.Frame, | |||||
| 241 | return ports.Frame{Type: ports.MsgParkedRouteResponse, Payload: payload}, nil | 244 | return ports.Frame{Type: ports.MsgParkedRouteResponse, Payload: payload}, nil | ||
| 242 | } | 245 | } | ||
| 243 | 246 | ||||
| 244 | - | func (d *Daemon) sendParkedRouteResponse(token attachmentConnectionToken, response ports.ParkedRouteResponse) error { | 247 | + | func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response ports.ParkedRouteResponse) error { |
| 245 | frame, err := parkedRouteResponseFrame(response) | 248 | frame, err := parkedRouteResponseFrame(response) | ||
| 246 | if err != nil { | 249 | if err != nil { | ||
| 247 | return err | 250 | return err | ||
| 248 | } | 251 | } | ||
| 249 | - | return token.sendControl(frame) | 252 | + | return effect.sendControl(frame) |
| 250 | } | 253 | } | ||
| 251 | 254 | ||||
| 252 | // sendParkedRouteResponseLocked preserves response -> full-output ordering. | 255 | // sendParkedRouteResponseLocked preserves response -> full-output ordering. | ||
| @@ -270,77 +273,98 @@ func (ac *attachedClient) releaseParkedOutputLocked() { | |||||
| 270 | ac.parkedRouteOutput.Store(false) | 273 | ac.parkedRouteOutput.Store(false) | ||
| 271 | } | 274 | } | ||
| 272 | 275 | ||||
| 273 | - | func (d *Daemon) parkedRouteResponseFailed(token attachmentConnectionToken) { | 276 | + | func (d *Daemon) parkedRouteResponseFailed(effect *attachmentEffect) { |
| 274 | - | if token.effect == nil || token.effect.ended.Load() { | 277 | + | if effect == nil { |
| 275 | return | 278 | return | ||
| 276 | } | 279 | } | ||
| 277 | - | launch := d.reserveAttachmentSendErrorCleanup(token, token.transport.transport) | 280 | + | capability := effect.capability() |
| 278 | - | token.effect.End() | 281 | + | launch := d.reserveAttachmentSendErrorCleanup(capability, capability.transport.transport) |
| 282 | + | effect.End() | |||
| 279 | launch() | 283 | launch() | ||
| 280 | } | 284 | } | ||
| 281 | 285 | ||||
| 282 | - | func (d *Daemon) respondParkedRoute(token attachmentConnectionToken, response ports.ParkedRouteResponse) bool { | 286 | + | func (d *Daemon) respondParkedRoute(effect *attachmentEffect, response ports.ParkedRouteResponse) bool { |
| 283 | - | if err := d.sendParkedRouteResponse(token, response); err != nil { | 287 | + | if effect == nil || effect.ac == nil { |
| 284 | - | d.parkedRouteResponseFailed(token) | 288 | + | return false |
| 289 | + | } | |||
| 290 | + | sender := effect | |||
| 291 | + | owned := false | |||
| 292 | + | if !sender.current() { | |||
| 293 | + | var admitted bool | |||
| 294 | + | sender, admitted = effect.ac.beginAttachmentEffect(effect.capability()) | |||
| 295 | + | if !admitted { | |||
| 296 | + | return false | |||
| 297 | + | } | |||
| 298 | + | owned = true | |||
| 299 | + | } | |||
| 300 | + | if owned { | |||
| 301 | + | defer sender.End() | |||
| 302 | + | } | |||
| 303 | + | if err := d.sendParkedRouteResponse(sender, response); err != nil { | |||
| 304 | + | d.parkedRouteResponseFailed(sender) | |||
| 285 | return false | 305 | return false | ||
| 286 | } | 306 | } | ||
| 287 | return true | 307 | return true | ||
| 288 | } | 308 | } | ||
| 289 | 309 | ||||
| 290 | - | func (d *Daemon) handleParkedRouteRequest(token attachmentConnectionToken, request ports.ParkedRouteRequest) { | 310 | + | func (d *Daemon) handleParkedRouteRequest(effect *attachmentEffect, request ports.ParkedRouteRequest) { |
| 291 | - | if ports.ValidateParkedRouteRequest(request) != nil || token.ac == nil { | 311 | + | if ports.ValidateParkedRouteRequest(request) != nil || !effect.current() || effect.ac == nil { |
| 292 | return | 312 | return | ||
| 293 | } | 313 | } | ||
| 314 | + | capability := effect.capability() | |||
| 294 | switch request.Action { | 315 | switch request.Action { | ||
| 295 | case ports.ParkedRoutePrepare: | 316 | case ports.ParkedRoutePrepare: | ||
| 296 | - | status := token.ac.prepareParkedRoute(token, request.LeaseID, d.clock.Now()) | 317 | + | status := effect.ac.prepareParkedRoute(capability, request.LeaseID, d.clock.Now()) |
| 297 | if status == 0 { | 318 | if status == 0 { | ||
| 298 | status = ports.ParkedRouteReady | 319 | status = ports.ParkedRouteReady | ||
| 299 | } | 320 | } | ||
| 300 | - | d.respondParkedRoute(token, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) | 321 | + | d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) |
| 301 | case ports.ParkedRouteResume: | 322 | case ports.ParkedRouteResume: | ||
| 302 | - | d.resumeParkedRoute(token, request) | 323 | + | d.resumeParkedRoute(effect, request) |
| 303 | case ports.ParkedRouteSwitch: | 324 | case ports.ParkedRouteSwitch: | ||
| 304 | - | d.switchParkedRoute(token, request) | 325 | + | d.switchParkedRoute(effect, request) |
| 305 | } | 326 | } | ||
| 306 | } | 327 | } | ||
| 307 | 328 | ||||
| 308 | - | func (d *Daemon) resumeParkedRoute(token attachmentConnectionToken, request ports.ParkedRouteRequest) { | 329 | + | func (d *Daemon) resumeParkedRoute(effect *attachmentEffect, request ports.ParkedRouteRequest) { |
| 309 | - | status := token.ac.consumeParkedRoute(token, request.LeaseID, d.clock.Now()) | 330 | + | capability := effect.capability() |
| 331 | + | status := effect.ac.consumeParkedRoute(capability, request.LeaseID, d.clock.Now()) | |||
| 310 | if status != 0 { | 332 | if status != 0 { | ||
| 311 | - | d.respondParkedRoute(token, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) | 333 | + | d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) |
| 312 | return | 334 | return | ||
| 313 | } | 335 | } | ||
| 314 | - | ac := token.ac | 336 | + | ac := effect.ac |
| 315 | ac.sendMu.Lock() | 337 | ac.sendMu.Lock() | ||
| 316 | - | err := d.sendParkedRouteResponseLocked(ac, token.transport, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: ports.ParkedRouteResumed}) | 338 | + | err := d.sendParkedRouteResponseLocked(ac, effect.transport, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: ports.ParkedRouteResumed}) |
| 317 | if err == nil { | 339 | if err == nil { | ||
| 318 | ac.releaseParkedOutputLocked() | 340 | ac.releaseParkedOutputLocked() | ||
| 319 | } | 341 | } | ||
| 320 | ac.sendMu.Unlock() | 342 | ac.sendMu.Unlock() | ||
| 321 | if err != nil { | 343 | if err != nil { | ||
| 322 | - | d.parkedRouteResponseFailed(token) | 344 | + | d.parkedRouteResponseFailed(effect) |
| 323 | return | 345 | return | ||
| 324 | } | 346 | } | ||
| 325 | - | if token.sess != nil { | 347 | + | if effect.sess != nil { |
| 326 | - | d.firstPaintWithLease(token.sess, ac, token.lease, true) | 348 | + | d.firstPaintWithLease(effect.sess, ac, effect.lease, true) |
| 327 | } | 349 | } | ||
| 328 | } | 350 | } | ||
| 329 | 351 | ||||
| 330 | - | func (d *Daemon) rejectParkedRouteSwitch(token attachmentConnectionToken, request ports.ParkedRouteRequest, fallback ports.ParkedRouteStatus) { | 352 | + | func (d *Daemon) rejectParkedRouteSwitch(effect *attachmentEffect, request ports.ParkedRouteRequest, fallback ports.ParkedRouteStatus) { |
| 331 | - | status := token.ac.rejectParkedRouteSwitch(token, request.LeaseID, d.clock.Now()) | 353 | + | capability := effect.capability() |
| 354 | + | status := effect.ac.rejectParkedRouteSwitch(capability, request.LeaseID, d.clock.Now()) | |||
| 332 | if status == 0 { | 355 | if status == 0 { | ||
| 333 | status = fallback | 356 | status = fallback | ||
| 334 | } | 357 | } | ||
| 335 | - | if d.respondParkedRoute(token, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == ports.ParkedRouteExpired { | 358 | + | if d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == ports.ParkedRouteExpired { |
| 336 | - | _ = token.ac.closeCapturedTransport(token.transport.transport) | 359 | + | _ = effect.ac.closeCapturedTransport(effect.transport.transport) |
| 337 | } | 360 | } | ||
| 338 | } | 361 | } | ||
| 339 | 362 | ||||
| 340 | - | func (d *Daemon) switchParkedRoute(token attachmentConnectionToken, request ports.ParkedRouteRequest) { | 363 | + | func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request ports.ParkedRouteRequest) { |
| 341 | - | if status := token.ac.beginParkedRouteSwitch(token, request.LeaseID, d.clock.Now()); status != 0 { | 364 | + | capability := effect.capability() |
| 342 | - | if d.respondParkedRoute(token, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == ports.ParkedRouteExpired { | 365 | + | if status := effect.ac.beginParkedRouteSwitch(capability, request.LeaseID, d.clock.Now()); status != 0 { |
| 343 | - | _ = token.ac.closeCapturedTransport(token.transport.transport) | 366 | + | if d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == ports.ParkedRouteExpired { |
| 367 | + | _ = effect.ac.closeCapturedTransport(effect.transport.transport) | |||
| 344 | } | 368 | } | ||
| 345 | return | 369 | return | ||
| 346 | } | 370 | } | ||
| @@ -350,34 +374,34 @@ func (d *Daemon) switchParkedRoute(token attachmentConnectionToken, request port | |||||
| 350 | ctx = context.Background() | 374 | ctx = context.Background() | ||
| 351 | } | 375 | } | ||
| 352 | if err := d.waitForParkedTargetRestore(ctx, target.SessionName); err != nil { | 376 | if err := d.waitForParkedTargetRestore(ctx, target.SessionName); err != nil { | ||
| 353 | - | d.rejectParkedRouteSwitch(token, request, ports.ParkedRouteStaleTarget) | 377 | + | d.rejectParkedRouteSwitch(effect, request, ports.ParkedRouteStaleTarget) |
| 354 | return | 378 | return | ||
| 355 | } | 379 | } | ||
| 356 | 380 | ||||
| 357 | - | transition, targetSession, err := d.transitionParkedRouteTarget(token, target) | 381 | + | transition, targetSession, err := d.transitionParkedRouteTarget(effect, target) |
| 358 | if err != nil { | 382 | if err != nil { | ||
| 359 | - | d.rejectParkedRouteSwitch(token, request, ports.ParkedRouteStaleTarget) | 383 | + | d.rejectParkedRouteSwitch(effect, request, ports.ParkedRouteStaleTarget) |
| 360 | return | 384 | return | ||
| 361 | } | 385 | } | ||
| 362 | - | if status := token.ac.consumePublishedParkedRoute(token, transition.published, request.LeaseID); status != 0 { | 386 | + | if status := effect.ac.consumePublishedParkedRoute(capability, transition.published, request.LeaseID); status != 0 { |
| 363 | d.abortPublishedAttachmentTransition(transition) | 387 | d.abortPublishedAttachmentTransition(transition) | ||
| 364 | return | 388 | return | ||
| 365 | } | 389 | } | ||
| 366 | - | fresh, admitted := token.ac.beginAttachmentEffect(transition.published) | 390 | + | fresh, admitted := effect.ac.beginAttachmentEffect(transition.published) |
| 367 | if !admitted { | 391 | if !admitted { | ||
| 368 | d.abortPublishedAttachmentTransition(transition) | 392 | d.abortPublishedAttachmentTransition(transition) | ||
| 369 | return | 393 | return | ||
| 370 | } | 394 | } | ||
| 371 | - | freshToken := fresh.connectionToken() | 395 | + | freshCapability := fresh.capability() |
| 372 | - | ac := token.ac | 396 | + | ac := effect.ac |
| 373 | ac.sendMu.Lock() | 397 | ac.sendMu.Lock() | ||
| 374 | - | responseErr := d.sendParkedRouteResponseLocked(ac, freshToken.transport, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: ports.ParkedRouteSwitched}) | 398 | + | responseErr := d.sendParkedRouteResponseLocked(ac, freshCapability.transport, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: ports.ParkedRouteSwitched}) |
| 375 | if responseErr == nil { | 399 | if responseErr == nil { | ||
| 376 | ac.releaseParkedOutputLocked() | 400 | ac.releaseParkedOutputLocked() | ||
| 377 | } | 401 | } | ||
| 378 | ac.sendMu.Unlock() | 402 | ac.sendMu.Unlock() | ||
| 379 | if responseErr != nil { | 403 | if responseErr != nil { | ||
| 380 | - | launch := d.reserveAttachmentSendErrorCleanup(freshToken, freshToken.transport.transport) | 404 | + | launch := d.reserveAttachmentSendErrorCleanup(freshCapability, freshCapability.transport.transport) |
| 381 | fresh.End() | 405 | fresh.End() | ||
| 382 | launch() | 406 | launch() | ||
| 383 | d.deferAttachmentTransitionCleanups(transition) | 407 | d.deferAttachmentTransitionCleanups(transition) | ||
| @@ -409,7 +433,8 @@ func (d *Daemon) waitForParkedTargetRestore(parent context.Context, name string) | |||||
| 409 | return err | 433 | return err | ||
| 410 | } | 434 | } | ||
| 411 | 435 | ||||
| 412 | - | func (d *Daemon) transitionParkedRouteTarget(token attachmentConnectionToken, target domain.RemoteSessionTarget) (attachmentTransitionResult, *session, error) { | 436 | + | func (d *Daemon) transitionParkedRouteTarget(effect *attachmentEffect, target domain.RemoteSessionTarget) (attachmentTransitionResult, *session, error) { |
| 437 | + | capability := effect.capability() | |||
| 413 | d.mu.Lock() | 438 | d.mu.Lock() | ||
| 414 | if d.closing { | 439 | if d.closing { | ||
| 415 | d.mu.Unlock() | 440 | d.mu.Unlock() | ||
| @@ -430,8 +455,8 @@ func (d *Daemon) transitionParkedRouteTarget(token attachmentConnectionToken, ta | |||||
| 430 | live.mu.Unlock() | 455 | live.mu.Unlock() | ||
| 431 | d.mu.Unlock() | 456 | d.mu.Unlock() | ||
| 432 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 457 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 433 | - | source: token.sess, target: live, next: token.ac, | 458 | + | source: effect.sess, target: live, next: effect.ac, |
| 434 | - | expectedTransport: token.transport, sourceToken: &token, action: "parked-route-switch", | 459 | + | expectedTransport: effect.transport, sourceCapability: &capability, sourceEffect: effect, action: "parked-route-switch", |
| 435 | expectedTargetLifecycle: &attachmentLifecycleFence{ | 460 | expectedTargetLifecycle: &attachmentLifecycleFence{ | ||
| 436 | name: target.SessionName, incarnation: target.LifecycleID, checkIncarnation: true, | 461 | name: target.SessionName, incarnation: target.LifecycleID, checkIncarnation: true, | ||
| 437 | tabID: tabID, tabIndex: tabIndex, checkTab: true, | 462 | tabID: tabID, tabIndex: tabIndex, checkTab: true, | ||
| @@ -455,8 +480,8 @@ func (d *Daemon) transitionParkedRouteTarget(token attachmentConnectionToken, ta | |||||
| 455 | 480 | ||||
| 456 | var created *session | 481 | var created *session | ||
| 457 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 482 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 458 | - | source: token.sess, next: token.ac, | 483 | + | source: effect.sess, next: effect.ac, |
| 459 | - | expectedTransport: token.transport, sourceToken: &token, action: "parked-route-restore", | 484 | + | expectedTransport: effect.transport, sourceCapability: &capability, sourceEffect: effect, action: "parked-route-restore", |
| 460 | activateTargetTab: true, targetTabIndex: tabIndex, ready: true, | 485 | activateTargetTab: true, targetTabIndex: tabIndex, ready: true, | ||
| 461 | createTargetLocked: func() (*session, error) { | 486 | createTargetLocked: func() (*session, error) { | ||
| 462 | current, ok := d.inactive[target.SessionName] | 487 | current, ok := d.inactive[target.SessionName] | ||
| @@ -470,7 +495,7 @@ func (d *Daemon) transitionParkedRouteTarget(token attachmentConnectionToken, ta | |||||
| 470 | env := copyEnvironment(d.baseEnv) | 495 | env := copyEnvironment(d.baseEnv) | ||
| 471 | cwd := d.dirOrHome(current.cwd) | 496 | cwd := d.dirOrHome(current.cwd) | ||
| 472 | var createErr error | 497 | var createErr error | ||
| 473 | - | created, createErr = d.resumeRemoteInactiveSessionLocked(target, cwd, token.ac.geometrySnapshot(), env, current) | 498 | + | created, createErr = d.resumeRemoteInactiveSessionLocked(target, cwd, effect.ac.geometrySnapshot(), env, current) |
| 474 | return created, createErr | 499 | return created, createErr | ||
| 475 | }, | 500 | }, | ||
| 476 | }) | 501 | }) | ||
M internal/usecase/daemon/parked_route_test.go +18 -16
| @@ -64,18 +64,17 @@ func (c *parkedRouteClock) setNow(now time.Time) { | |||||
| 64 | func armAndPrepareParkedRoute(t *testing.T, d *Daemon, source *session, ac *attachedClient, sends chan ports.Frame) ports.ParkedRouteLeaseID { | 64 | func armAndPrepareParkedRoute(t *testing.T, d *Daemon, source *session, ac *attachedClient, sends chan ports.Frame) ports.ParkedRouteLeaseID { | ||
| 65 | t.Helper() | 65 | t.Helper() | ||
| 66 | ac.navigationCapabilities |= ports.NavigationCapabilityHomePicker | 66 | ac.navigationCapabilities |= ports.NavigationCapabilityHomePicker | ||
| 67 | - | token := source.attachmentToken(ac, ac.transport()) | 67 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 68 | effect, admitted := ac.beginAttachmentEffect(token) | 68 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 69 | require.True(t, admitted) | 69 | require.True(t, admitted) | ||
| 70 | - | token.effect = effect | 70 | + | require.NoError(t, d.sendNavigationActionForAttachment(effect, ports.NavigationOpenHomePicker)) |
| 71 | - | require.NoError(t, d.sendNavigationActionForAttachment(token, ports.NavigationOpenHomePicker)) | |||
| 72 | effect.End() | 71 | effect.End() | ||
| 73 | directiveFrame := awaitFrame(t, sends, ports.MsgNavigationAction) | 72 | directiveFrame := awaitFrame(t, sends, ports.MsgNavigationAction) | ||
| 74 | directive, err := ports.UnmarshalNavigationDirective(directiveFrame.Payload) | 73 | directive, err := ports.UnmarshalNavigationDirective(directiveFrame.Payload) | ||
| 75 | require.NoError(t, err) | 74 | require.NoError(t, err) | ||
| 76 | 75 | ||||
| 77 | prepare := ports.ParkedRouteRequest{RequestID: 1, LeaseID: directive.LeaseID, Action: ports.ParkedRoutePrepare} | 76 | prepare := ports.ParkedRouteRequest{RequestID: 1, LeaseID: directive.LeaseID, Action: ports.ParkedRoutePrepare} | ||
| 78 | - | require.False(t, d.handleAttachmentClientFrame(source.attachmentToken(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(prepare)})) | 77 | + | require.False(t, d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(prepare)})) |
| 79 | readyFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | 78 | readyFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | ||
| 80 | ready, err := ports.UnmarshalParkedRouteResponse(readyFrame.Payload) | 79 | ready, err := ports.UnmarshalParkedRouteResponse(readyFrame.Payload) | ||
| 81 | require.NoError(t, err) | 80 | require.NoError(t, err) | ||
| @@ -108,7 +107,7 @@ func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) { | |||||
| 108 | SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID), | 107 | SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID), | ||
| 109 | } | 108 | } | ||
| 110 | switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &targetRequest} | 109 | switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &targetRequest} | ||
| 111 | - | require.False(t, d.handleAttachmentClientFrame(source.attachmentToken(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})) | 110 | + | require.False(t, d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})) |
| 112 | 111 | ||||
| 113 | require.Same(t, target, ac.currentAttachmentSession()) | 112 | require.Same(t, target, ac.currentAttachmentSession()) | ||
| 114 | require.False(t, ac.parkedRouteOutput.Load()) | 113 | require.False(t, ac.parkedRouteOutput.Load()) | ||
| @@ -147,7 +146,7 @@ func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing. | |||||
| 147 | SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID), | 146 | SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID), | ||
| 148 | } | 147 | } | ||
| 149 | switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &targetRequest} | 148 | switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &targetRequest} | ||
| 150 | - | d.handleAttachmentClientFrame(source.attachmentToken(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}) | 149 | + | d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}) |
| 151 | 150 | ||||
| 152 | require.Same(t, target, ac.currentAttachmentSession()) | 151 | require.Same(t, target, ac.currentAttachmentSession()) | ||
| 153 | responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | 152 | responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | ||
| @@ -185,7 +184,7 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T | |||||
| 185 | SessionName: "stopped", Stopped: true, StoppedTab: domain.NewStableTabSelector("tab-b"), | 184 | SessionName: "stopped", Stopped: true, StoppedTab: domain.NewStableTabSelector("tab-b"), | ||
| 186 | } | 185 | } | ||
| 187 | switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &target} | 186 | switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &target} | ||
| 188 | - | d.handleAttachmentClientFrame(source.attachmentToken(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}) | 187 | + | d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}) |
| 189 | 188 | ||||
| 190 | restored := ac.currentAttachmentSession() | 189 | restored := ac.currentAttachmentSession() | ||
| 191 | require.NotNil(t, restored) | 190 | require.NotNil(t, restored) | ||
| @@ -210,7 +209,7 @@ func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) { | |||||
| 210 | SessionName: "missing", LiveTabID: "tab-1", | 209 | SessionName: "missing", LiveTabID: "tab-1", | ||
| 211 | } | 210 | } | ||
| 212 | switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &missing} | 211 | switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &missing} | ||
| 213 | - | d.handleAttachmentClientFrame(source.attachmentToken(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}) | 212 | + | d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}) |
| 214 | staleFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | 213 | staleFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | ||
| 215 | stale, err := ports.UnmarshalParkedRouteResponse(staleFrame.Payload) | 214 | stale, err := ports.UnmarshalParkedRouteResponse(staleFrame.Payload) | ||
| 216 | require.NoError(t, err) | 215 | require.NoError(t, err) | ||
| @@ -219,7 +218,7 @@ func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) { | |||||
| 219 | require.True(t, ac.parkedRouteOutput.Load()) | 218 | require.True(t, ac.parkedRouteOutput.Load()) | ||
| 220 | 219 | ||||
| 221 | resume := ports.ParkedRouteRequest{RequestID: 3, LeaseID: leaseID, Action: ports.ParkedRouteResume} | 220 | resume := ports.ParkedRouteRequest{RequestID: 3, LeaseID: leaseID, Action: ports.ParkedRouteResume} | ||
| 222 | - | d.handleAttachmentClientFrame(source.attachmentToken(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)}) | 221 | + | d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)}) |
| 223 | resumedFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | 222 | resumedFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | ||
| 224 | resumed, err := ports.UnmarshalParkedRouteResponse(resumedFrame.Payload) | 223 | resumed, err := ports.UnmarshalParkedRouteResponse(resumedFrame.Payload) | ||
| 225 | require.NoError(t, err) | 224 | require.NoError(t, err) | ||
| @@ -245,7 +244,7 @@ func TestParkedRouteDoesNotReplayOneShotEffectsAfterResume(t *testing.T) { | |||||
| 245 | } | 244 | } | ||
| 246 | 245 | ||||
| 247 | resume := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteResume} | 246 | resume := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteResume} | ||
| 248 | - | d.handleAttachmentClientFrame(source.attachmentToken(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)}) | 247 | + | d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)}) |
| 249 | _ = awaitFrame(t, sends, ports.MsgParkedRouteResponse) | 248 | _ = awaitFrame(t, sends, ports.MsgParkedRouteResponse) | ||
| 250 | outputFrame := awaitFrame(t, sends, ports.MsgOutput) | 249 | outputFrame := awaitFrame(t, sends, ports.MsgOutput) | ||
| 251 | output, err := ports.UnmarshalOutput(outputFrame.Payload) | 250 | output, err := ports.UnmarshalOutput(outputFrame.Payload) | ||
| @@ -270,7 +269,7 @@ func TestParkedRouteExpiredLeaseFailsClosed(t *testing.T) { | |||||
| 270 | ac.parkedRoute.expiresAt = d.clock.Now() | 269 | ac.parkedRoute.expiresAt = d.clock.Now() | ||
| 271 | ac.parkedRouteMu.Unlock() | 270 | ac.parkedRouteMu.Unlock() | ||
| 272 | resume := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteResume} | 271 | resume := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteResume} | ||
| 273 | - | d.handleAttachmentClientFrame(source.attachmentToken(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)}) | 272 | + | d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)}) |
| 274 | responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | 273 | responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse) | ||
| 275 | response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload) | 274 | response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload) | ||
| 276 | require.NoError(t, err) | 275 | require.NoError(t, err) | ||
| @@ -288,7 +287,7 @@ func TestParkedRouteResumeRejectsSwitchInFlight(t *testing.T) { | |||||
| 288 | ac.parkedRouteMu.Lock() | 287 | ac.parkedRouteMu.Lock() | ||
| 289 | ac.parkedRoute.inFlight = true | 288 | ac.parkedRoute.inFlight = true | ||
| 290 | ac.parkedRouteMu.Unlock() | 289 | ac.parkedRouteMu.Unlock() | ||
| 291 | - | status := ac.consumeParkedRoute(source.attachmentToken(ac, ac.transport()), leaseID, d.clock.Now()) | 290 | + | status := ac.consumeParkedRoute(source.captureAttachmentCapability(ac, ac.transport()), leaseID, d.clock.Now()) |
| 292 | 291 | ||||
| 293 | require.Equal(t, ports.ParkedRouteRejected, status) | 292 | require.Equal(t, ports.ParkedRouteRejected, status) | ||
| 294 | ac.parkedRouteMu.Lock() | 293 | ac.parkedRouteMu.Lock() | ||
| @@ -335,7 +334,11 @@ func TestParkedRouteMalformedSwitchRequestsFailClosed(t *testing.T) { | |||||
| 335 | {RequestID: 2, Action: ports.ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}}, | 334 | {RequestID: 2, Action: ports.ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}}, | ||
| 336 | {RequestID: 3, LeaseID: leaseID, Action: 99}, | 335 | {RequestID: 3, LeaseID: leaseID, Action: 99}, | ||
| 337 | } { | 336 | } { | ||
| 338 | - | d.handleParkedRouteRequest(source.attachmentToken(ac, ac.transport()), request) | 337 | + | capability := source.captureAttachmentCapability(ac, ac.transport()) |
| 338 | + | effect, admitted := ac.beginAttachmentEffect(capability) | |||
| 339 | + | require.True(t, admitted) | |||
| 340 | + | d.handleParkedRouteRequest(effect, request) | |||
| 341 | + | effect.End() | |||
| 339 | } | 342 | } | ||
| 340 | require.Same(t, source, ac.currentAttachmentSession()) | 343 | require.Same(t, source, ac.currentAttachmentSession()) | ||
| 341 | select { | 344 | select { | ||
| @@ -349,14 +352,13 @@ func TestParkedRouteResponseFailureDoesNotDrainItsOwnFrameEffect(t *testing.T) { | |||||
| 349 | d, source, ac, _, releases := newManualTabSession(t, 1) | 352 | d, source, ac, _, releases := newManualTabSession(t, 1) | ||
| 350 | defer releaseAll(releases) | 353 | defer releaseAll(releases) | ||
| 351 | ac.replaceTransport(parkedRouteFailTransport{}) | 354 | ac.replaceTransport(parkedRouteFailTransport{}) | ||
| 352 | - | token := source.attachmentToken(ac, ac.transport()) | 355 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 353 | effect, admitted := ac.beginAttachmentEffect(token) | 356 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 354 | require.True(t, admitted) | 357 | require.True(t, admitted) | ||
| 355 | - | token.effect = effect | |||
| 356 | 358 | ||||
| 357 | result := make(chan bool, 1) | 359 | result := make(chan bool, 1) | ||
| 358 | go func() { | 360 | go func() { | ||
| 359 | - | result <- d.respondParkedRoute(token, ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady}) | 361 | + | result <- d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady}) |
| 360 | }() | 362 | }() | ||
| 361 | require.False(t, awaitTestValue(t, result, "parked-route send failure cleanup")) | 363 | require.False(t, awaitTestValue(t, result, "parked-route send failure cleanup")) | ||
| 362 | d.attachmentCleanupWg.Wait() | 364 | d.attachmentCleanupWg.Wait() | ||
M internal/usecase/daemon/picker.go +83 -76
| @@ -291,7 +291,7 @@ func (d *Daemon) pickerListInputState(ac *attachedClient) listInputState { | |||||
| 291 | } | 291 | } | ||
| 292 | } | 292 | } | ||
| 293 | 293 | ||||
| 294 | - | func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...*attachmentEffectTicket) { | 294 | + | func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...*attachmentEffect) { |
| 295 | sess := ac.currentAttachmentSession() | 295 | sess := ac.currentAttachmentSession() | ||
| 296 | if sess == nil { | 296 | if sess == nil { | ||
| 297 | return | 297 | return | ||
| @@ -344,7 +344,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...* | |||||
| 344 | return | 344 | return | ||
| 345 | } | 345 | } | ||
| 346 | if result.action == 'x' { | 346 | if result.action == 'x' { | ||
| 347 | - | var effect *attachmentEffectTicket | 347 | + | var effect *attachmentEffect |
| 348 | if len(effects) != 0 { | 348 | if len(effects) != 0 { | ||
| 349 | effect = effects[0] | 349 | effect = effects[0] | ||
| 350 | } | 350 | } | ||
| @@ -352,7 +352,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...* | |||||
| 352 | var err error | 352 | var err error | ||
| 353 | if effect != nil { | 353 | if effect != nil { | ||
| 354 | effect.bindActionEnd(d, "picker-delete") | 354 | effect.bindActionEnd(d, "picker-delete") | ||
| 355 | - | err = d.killPickerTargetForAttachment(target, effect.connectionToken()) | 355 | + | err = d.killPickerTargetForAttachment(target, effect) |
| 356 | } else { | 356 | } else { | ||
| 357 | err = d.killPickerTarget(target) | 357 | err = d.killPickerTarget(target) | ||
| 358 | } | 358 | } | ||
| @@ -360,8 +360,8 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...* | |||||
| 360 | d.reportAttachmentError(sess, err) | 360 | d.reportAttachmentError(sess, err) | ||
| 361 | } | 361 | } | ||
| 362 | } | 362 | } | ||
| 363 | - | if effect != nil && effect.ended.Load() { | 363 | + | if effect != nil && !effect.current() { |
| 364 | - | fresh, admitted := ac.beginAttachmentEffect(effect.token) | 364 | + | fresh, admitted := ac.beginAttachmentEffect(effect.capability()) |
| 365 | if !admitted { | 365 | if !admitted { | ||
| 366 | return | 366 | return | ||
| 367 | } | 367 | } | ||
| @@ -379,20 +379,12 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...* | |||||
| 379 | if result.exit && !committing { | 379 | if result.exit && !committing { | ||
| 380 | back := ac.startupOverlay == ports.StartupOverlaySessionPicker && ac.navigationCapabilities&ports.NavigationCapabilityBack != 0 | 380 | back := ac.startupOverlay == ports.StartupOverlaySessionPicker && ac.navigationCapabilities&ports.NavigationCapabilityBack != 0 | ||
| 381 | if back { | 381 | if back { | ||
| 382 | - | var token attachmentConnectionToken | 382 | + | if len(effects) == 0 || effects[0] == nil { |
| 383 | - | if len(effects) != 0 && effects[0] != nil { | 383 | + | d.invalidateRender(sess, ac, true, "picker.go") |
| 384 | - | token = effects[0].connectionToken() | 384 | + | return |
| 385 | - | } else { | |||
| 386 | - | var effect *attachmentEffectTicket | |||
| 387 | - | var admitted bool | |||
| 388 | - | token, effect, admitted = ac.beginCurrentAttachmentEffect(sess, ac.transport()) | |||
| 389 | - | if !admitted { | |||
| 390 | - | d.invalidateRender(sess, ac, true, "picker.go") | |||
| 391 | - | return | |||
| 392 | - | } | |||
| 393 | - | defer effect.End() | |||
| 394 | } | 385 | } | ||
| 395 | - | if err := d.sendNavigationActionForAttachment(token, ports.NavigationBack); err != nil { | 386 | + | effect := effects[0] |
| 387 | + | if err := d.sendNavigationActionForAttachment(effect, ports.NavigationBack); err != nil { | |||
| 396 | d.reportAttachmentError(sess, err) | 388 | d.reportAttachmentError(sess, err) | ||
| 397 | d.invalidateRender(sess, ac, true, "picker.go") | 389 | d.invalidateRender(sess, ac, true, "picker.go") | ||
| 398 | return | 390 | return | ||
| @@ -423,7 +415,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...* | |||||
| 423 | } | 415 | } | ||
| 424 | var err error | 416 | var err error | ||
| 425 | if len(effects) != 0 && effects[0] != nil { | 417 | if len(effects) != 0 && effects[0] != nil { | ||
| 426 | - | err = d.switchToTargetForAttachment(effects[0].connectionToken(), target, sessionHandoffGuard{closePicker: true, allowSamePeer: true}, "picker-select") | 418 | + | err = d.switchToTargetForAttachment(effects[0], target, sessionHandoffGuard{closePicker: true, allowSamePeer: true}, "picker-select") |
| 427 | } else { | 419 | } else { | ||
| 428 | d.closePicker(ac) | 420 | d.closePicker(ac) | ||
| 429 | err = d.switchToTarget(sess, ac, target) | 421 | err = d.switchToTarget(sess, ac, target) | ||
| @@ -793,8 +785,8 @@ type sessionHandoffGuard struct { | |||||
| 793 | // centralized transition. The transition releases admission at its freeze seam | 785 | // centralized transition. The transition releases admission at its freeze seam | ||
| 794 | // and revalidates the exact initiating token before changing target focus or | 786 | // and revalidates the exact initiating token before changing target focus or | ||
| 795 | // attachment ownership. | 787 | // attachment ownership. | ||
| 796 | - | func (d *Daemon) switchActiveTargetForAttachment(token attachmentConnectionToken, target picker.Target) error { | 788 | + | func (d *Daemon) switchActiveTargetForAttachment(effect *attachmentEffect, target picker.Target) error { |
| 797 | - | return d.switchActiveTargetForAttachmentGuarded(token, target, sessionHandoffGuard{}, "jump-attention") | 789 | + | return d.switchActiveTargetForAttachmentGuarded(effect, target, sessionHandoffGuard{}, "jump-attention") |
| 798 | } | 790 | } | ||
| 799 | 791 | ||||
| 800 | func pickerTargetLifecycleFence(target picker.Target) *attachmentLifecycleFence { | 792 | func pickerTargetLifecycleFence(target picker.Target) *attachmentLifecycleFence { | ||
| @@ -819,43 +811,44 @@ func pickerTargetLifecycleFence(target picker.Target) *attachmentLifecycleFence | |||||
| 819 | return fence | 811 | return fence | ||
| 820 | } | 812 | } | ||
| 821 | 813 | ||||
| 822 | - | func (d *Daemon) switchActiveTargetForAttachmentGuarded(token attachmentConnectionToken, target picker.Target, guard sessionHandoffGuard, action string) error { | 814 | + | func (d *Daemon) switchActiveTargetForAttachmentGuarded(effect *attachmentEffect, target picker.Target, guard sessionHandoffGuard, action string) error { |
| 823 | - | if token.sess == nil || token.ac == nil { | 815 | + | if !effect.current() || effect.sess == nil || effect.ac == nil { |
| 824 | return nil | 816 | return nil | ||
| 825 | } | 817 | } | ||
| 826 | d.mu.Lock() | 818 | d.mu.Lock() | ||
| 827 | targetSess := d.sessions[target.Session] | 819 | targetSess := d.sessions[target.Session] | ||
| 828 | d.mu.Unlock() | 820 | d.mu.Unlock() | ||
| 829 | if targetSess == nil { | 821 | if targetSess == nil { | ||
| 830 | - | if !token.attachmentCurrent() { | 822 | + | if !effect.current() { |
| 831 | return nil | 823 | return nil | ||
| 832 | } | 824 | } | ||
| 833 | - | d.invalidateRender(token.sess, token.ac, true, "picker.go") | 825 | + | d.invalidateRender(effect.sess, effect.ac, true, "picker.go") |
| 834 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't switch to that session", nil) | 826 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't switch to that session", nil) | ||
| 835 | } | 827 | } | ||
| 836 | - | if targetSess == token.sess { | 828 | + | if targetSess == effect.sess { |
| 837 | return nil | 829 | return nil | ||
| 838 | } | 830 | } | ||
| 839 | 831 | ||||
| 832 | + | capability := effect.capability() | |||
| 840 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 833 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 841 | - | source: token.sess, target: targetSess, next: token.ac, | 834 | + | source: effect.sess, target: targetSess, next: effect.ac, |
| 842 | 835 | ||||
| 843 | - | expectedTransport: token.transport, sourceToken: &token, action: action, | 836 | + | expectedTransport: effect.transport, sourceCapability: &capability, sourceEffect: effect, action: action, |
| 844 | expectedTargetLifecycle: pickerTargetLifecycleFence(target), | 837 | expectedTargetLifecycle: pickerTargetLifecycleFence(target), | ||
| 845 | activateTargetTab: true, targetTabIndex: target.TabIndex, copySourceEnvironment: true, ready: true, | 838 | activateTargetTab: true, targetTabIndex: target.TabIndex, copySourceEnvironment: true, ready: true, | ||
| 846 | }) | 839 | }) | ||
| 847 | if err != nil { | 840 | if err != nil { | ||
| 848 | // Losing the exact source role is a benign stale action, not a notice for | 841 | // Losing the exact source role is a benign stale action, not a notice for | ||
| 849 | // whichever attachment replaced the initiator. | 842 | // whichever attachment replaced the initiator. | ||
| 850 | - | if !token.attachmentCurrent() { | 843 | + | if !capability.current() { |
| 851 | return nil | 844 | return nil | ||
| 852 | } | 845 | } | ||
| 853 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't switch to that session", err) | 846 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't switch to that session", err) | ||
| 854 | } | 847 | } | ||
| 855 | if guard.closePicker { | 848 | if guard.closePicker { | ||
| 856 | - | fresh, admitted := token.ac.beginAttachmentEffect(transition.published) | 849 | + | fresh, admitted := effect.ac.beginAttachmentEffect(transition.published) |
| 857 | if admitted { | 850 | if admitted { | ||
| 858 | - | d.closePicker(token.ac) | 851 | + | d.closePicker(effect.ac) |
| 859 | fresh.End() | 852 | fresh.End() | ||
| 860 | } | 853 | } | ||
| 861 | } | 854 | } | ||
| @@ -876,21 +869,21 @@ func (d *Daemon) switchToTarget(from *session, ac *attachedClient, target picker | |||||
| 876 | // intent captures the exact initiating capability before admission is released; | 869 | // intent captures the exact initiating capability before admission is released; | ||
| 877 | // every active, stopped, and same-session target then uses transitionAttachment | 870 | // every active, stopped, and same-session target then uses transitionAttachment | ||
| 878 | // for frozen, atomic source-token preflight. | 871 | // for frozen, atomic source-token preflight. | ||
| 879 | - | func (d *Daemon) switchToTargetForAttachment(token attachmentConnectionToken, target picker.Target, guard sessionHandoffGuard, action string) error { | 872 | + | func (d *Daemon) switchToTargetForAttachment(effect *attachmentEffect, target picker.Target, guard sessionHandoffGuard, action string) error { |
| 880 | - | if token.sess == nil || token.ac == nil || token.effect == nil { | 873 | + | if !effect.current() || effect.sess == nil || effect.ac == nil { |
| 881 | return nil | 874 | return nil | ||
| 882 | } | 875 | } | ||
| 883 | if target.RemoteTarget != nil || target.RemoteKey != nil { | 876 | if target.RemoteTarget != nil || target.RemoteKey != nil { | ||
| 884 | - | return d.sendRemoteAttachTargetForAttachment(token, target, guard, action) | 877 | + | return d.sendRemoteAttachTargetForAttachment(effect, target, guard, action) |
| 885 | } | 878 | } | ||
| 886 | - | return d.sendLocalAttachTargetForAttachment(token, target, guard, action) | 879 | + | return d.sendLocalAttachTargetForAttachment(effect, target, guard, action) |
| 887 | } | 880 | } | ||
| 888 | 881 | ||||
| 889 | // sendLocalAttachTargetForAttachment offers an endpoint-empty, exact local | 882 | // sendLocalAttachTargetForAttachment offers an endpoint-empty, exact local | ||
| 890 | // target on the current authenticated connection. A current client confirms it | 883 | // target on the current authenticated connection. A current client confirms it | ||
| 891 | // with MsgSamePeerSwitchRequest; an interrupted or older client retains the | 884 | // with MsgSamePeerSwitchRequest; an interrupted or older client retains the | ||
| 892 | // existing close-and-redial fallback without any daemon-origin inference. | 885 | // existing close-and-redial fallback without any daemon-origin inference. | ||
| 893 | - | func (d *Daemon) sendLocalAttachTargetForAttachment(token attachmentConnectionToken, target picker.Target, guard sessionHandoffGuard, action string) error { | 886 | + | func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, target picker.Target, guard sessionHandoffGuard, action string) error { |
| 894 | d.mu.Lock() | 887 | d.mu.Lock() | ||
| 895 | var targetSess *session | 888 | var targetSess *session | ||
| 896 | var sessionName string | 889 | var sessionName string | ||
| @@ -915,13 +908,13 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(token attachmentConnectionTo | |||||
| 915 | targetSess.mu.Unlock() | 908 | targetSess.mu.Unlock() | ||
| 916 | } | 909 | } | ||
| 917 | d.mu.Unlock() | 910 | d.mu.Unlock() | ||
| 918 | - | if !matches || !token.attachmentCurrent() { | 911 | + | if !matches || !effect.current() { |
| 919 | return errAttachmentTransition | 912 | return errAttachmentTransition | ||
| 920 | } | 913 | } | ||
| 921 | if targetSess != nil && target.TabID != "" { | 914 | if targetSess != nil && target.TabID != "" { | ||
| 922 | // An explicit tab row is a direct user choice, not route memory; retain | 915 | // An explicit tab row is a direct user choice, not route memory; retain | ||
| 923 | // the established daemon-side transition so it opens exactly that tab. | 916 | // the established daemon-side transition so it opens exactly that tab. | ||
| 924 | - | return d.switchToTargetGuardedForAttachment(token.sess, token.ac, target, guard, &token, action) | 917 | + | return d.switchToTargetGuardedForAttachment(effect.sess, effect.ac, target, guard, effect, action) |
| 925 | } | 918 | } | ||
| 926 | 919 | ||||
| 927 | // An active session row with no explicit non-default tab is on the | 920 | // An active session row with no explicit non-default tab is on the | ||
| @@ -943,15 +936,15 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(token attachmentConnectionTo | |||||
| 943 | // same-peer transition keeps the attachment and its namespace; the target | 936 | // same-peer transition keeps the attachment and its namespace; the target | ||
| 944 | // scene diff deletes and replaces the source session's placements. | 937 | // scene diff deletes and replaces the source session's placements. | ||
| 945 | if !samePeerEligible { | 938 | if !samePeerEligible { | ||
| 946 | - | if err := d.cleanupAttachmentOutput(token.ac); err != nil { | 939 | + | if err := d.cleanupAttachmentOutput(effect.ac); err != nil { |
| 947 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't clean up attachment output before local handoff", err) | 940 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't clean up attachment output before local handoff", err) | ||
| 948 | } | 941 | } | ||
| 949 | } else { | 942 | } else { | ||
| 950 | - | token.ac.offerSamePeerTarget(*exactTarget) | 943 | + | effect.ac.offerSamePeerTarget(*exactTarget) |
| 951 | } | 944 | } | ||
| 952 | - | if err := token.sendControl(ports.Frame{Type: ports.MsgAttachTarget, Payload: payload}); err != nil { | 945 | + | if err := effect.sendControl(ports.Frame{Type: ports.MsgAttachTarget, Payload: payload}); err != nil { |
| 953 | if samePeerEligible { | 946 | if samePeerEligible { | ||
| 954 | - | token.ac.clearSamePeerOffer() | 947 | + | effect.ac.clearSamePeerOffer() |
| 955 | } | 948 | } | ||
| 956 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't offer local session switch", err) | 949 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't offer local session switch", err) | ||
| 957 | } | 950 | } | ||
| @@ -961,10 +954,10 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(token attachmentConnectionTo | |||||
| 961 | // Keep the source link open until the client receives the ordered cleanup | 954 | // Keep the source link open until the client receives the ordered cleanup | ||
| 962 | // and handoff frames, then let the client's close drive ordinary parking. | 955 | // and handoff frames, then let the client's close drive ordinary parking. | ||
| 963 | if guard.closePicker { | 956 | if guard.closePicker { | ||
| 964 | - | d.closePicker(token.ac) | 957 | + | d.closePicker(effect.ac) |
| 965 | } | 958 | } | ||
| 966 | - | token.effect.bindActionEnd(d, "detach") | 959 | + | effect.bindActionEnd(d, "detach") |
| 967 | - | token.effect.End() | 960 | + | effect.End() |
| 968 | } | 961 | } | ||
| 969 | return nil | 962 | return nil | ||
| 970 | } | 963 | } | ||
| @@ -973,10 +966,10 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(token attachmentConnectionTo | |||||
| 973 | // endpoint to the thin client. The daemon owns no remote session shadow: after | 966 | // endpoint to the thin client. The daemon owns no remote session shadow: after | ||
| 974 | // the target is sent, the local attachment is detached and the client opens a | 967 | // the target is sent, the local attachment is detached and the client opens a | ||
| 975 | // fresh connection to the owning daemon. | 968 | // fresh connection to the owning daemon. | ||
| 976 | - | func (d *Daemon) sendRemoteAttachTargetForAttachment(token attachmentConnectionToken, target picker.Target, guard sessionHandoffGuard, _ string) error { | 969 | + | func (d *Daemon) sendRemoteAttachTargetForAttachment(effect *attachmentEffect, target picker.Target, guard sessionHandoffGuard, _ string) error { |
| 977 | failUnavailable := func() error { | 970 | failUnavailable := func() error { | ||
| 978 | - | if token.attachmentCurrent() { | 971 | + | if effect.current() { |
| 979 | - | d.notifyRemotePickerUnavailable(token.sess, target) | 972 | + | d.notifyRemotePickerUnavailable(effect.sess, target) |
| 980 | } | 973 | } | ||
| 981 | return errAttachmentTransition | 974 | return errAttachmentTransition | ||
| 982 | } | 975 | } | ||
| @@ -999,13 +992,13 @@ func (d *Daemon) sendRemoteAttachTargetForAttachment(token attachmentConnectionT | |||||
| 999 | if payload == nil { | 992 | if payload == nil { | ||
| 1000 | return failUnavailable() | 993 | return failUnavailable() | ||
| 1001 | } | 994 | } | ||
| 1002 | - | if err := token.sendControl(ports.Frame{Type: ports.MsgAttachTarget, Payload: payload}); err != nil { | 995 | + | if err := effect.sendControl(ports.Frame{Type: ports.MsgAttachTarget, Payload: payload}); err != nil { |
| 1003 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't attach to remote session", err) | 996 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't attach to remote session", err) | ||
| 1004 | } | 997 | } | ||
| 1005 | if guard.closePicker { | 998 | if guard.closePicker { | ||
| 1006 | - | d.closePicker(token.ac) | 999 | + | d.closePicker(effect.ac) |
| 1007 | } | 1000 | } | ||
| 1008 | - | d.clientGoneForAttachment(token, false) | 1001 | + | d.clientGoneForAttachment(effect, false) |
| 1009 | return nil | 1002 | return nil | ||
| 1010 | } | 1003 | } | ||
| 1011 | 1004 | ||||
| @@ -1054,14 +1047,19 @@ func (d *Daemon) switchToTargetGuarded(from *session, ac *attachedClient, target | |||||
| 1054 | return d.switchToTargetGuardedForAttachment(from, ac, target, guard, nil, "") | 1047 | return d.switchToTargetGuardedForAttachment(from, ac, target, guard, nil, "") | ||
| 1055 | } | 1048 | } | ||
| 1056 | 1049 | ||||
| 1057 | - | func (d *Daemon) switchToTargetGuardedForAttachment(from *session, ac *attachedClient, target picker.Target, guard sessionHandoffGuard, sourceToken *attachmentConnectionToken, action string) error { | 1050 | + | func (d *Daemon) switchToTargetGuardedForAttachment(from *session, ac *attachedClient, target picker.Target, guard sessionHandoffGuard, sourceEffect *attachmentEffect, action string) error { |
| 1051 | + | var sourceCapability *attachmentCapability | |||
| 1052 | + | if sourceEffect != nil { | |||
| 1053 | + | capability := sourceEffect.capability() | |||
| 1054 | + | sourceCapability = &capability | |||
| 1055 | + | } | |||
| 1058 | if target.Name != "" { | 1056 | if target.Name != "" { | ||
| 1059 | ctx := d.serveCtx | 1057 | ctx := d.serveCtx | ||
| 1060 | if ctx == nil { | 1058 | if ctx == nil { | ||
| 1061 | ctx = context.Background() | 1059 | ctx = context.Background() | ||
| 1062 | } | 1060 | } | ||
| 1063 | if err := d.waitForTargetRestore(ctx, target.Name); err != nil { | 1061 | if err := d.waitForTargetRestore(ctx, target.Name); err != nil { | ||
| 1064 | - | if sourceToken != nil && !sourceToken.attachmentCurrent() { | 1062 | + | if sourceCapability != nil && !sourceCapability.current() { |
| 1065 | return errAttachmentTransition | 1063 | return errAttachmentTransition | ||
| 1066 | } | 1064 | } | ||
| 1067 | d.invalidateRender(from, ac, true, "picker.go") | 1065 | d.invalidateRender(from, ac, true, "picker.go") | ||
| @@ -1080,22 +1078,22 @@ func (d *Daemon) switchToTargetGuardedForAttachment(from *session, ac *attachedC | |||||
| 1080 | active, stopped, isStopped, ok := d.resolveNamedLifecycleTargetLocked(target) | 1078 | active, stopped, isStopped, ok := d.resolveNamedLifecycleTargetLocked(target) | ||
| 1081 | if ok { | 1079 | if ok { | ||
| 1082 | if isStopped { | 1080 | if isStopped { | ||
| 1083 | - | targetSess, transition, switched, cause = d.resumeStoppedAndSwitchLocked(from, ac, target, stopped, sourceToken, guard, action) | 1081 | + | targetSess, transition, switched, cause = d.resumeStoppedAndSwitchLocked(from, ac, target, stopped, sourceEffect, guard, action) |
| 1084 | } else { | 1082 | } else { | ||
| 1085 | // The snapshot ID may describe the stopped representation. The | 1083 | // The snapshot ID may describe the stopped representation. The | ||
| 1086 | // locked name/lifecycle resolver chose this active incarnation. | 1084 | // locked name/lifecycle resolver chose this active incarnation. | ||
| 1087 | resolvedTarget := target | 1085 | resolvedTarget := target | ||
| 1088 | resolvedTarget.Session = active.id | 1086 | resolvedTarget.Session = active.id | ||
| 1089 | - | targetSess, transition, switched = d.switchToActiveTargetLocked(from, ac, active, resolvedTarget, guard, sourceToken, action) | 1087 | + | targetSess, transition, switched = d.switchToActiveTargetLocked(from, ac, active, resolvedTarget, guard, sourceEffect, action) |
| 1090 | } | 1088 | } | ||
| 1091 | } | 1089 | } | ||
| 1092 | } else if active := d.sessions[target.Session]; active != nil { | 1090 | } else if active := d.sessions[target.Session]; active != nil { | ||
| 1093 | - | targetSess, transition, switched = d.switchToActiveTargetLocked(from, ac, active, target, guard, sourceToken, action) | 1091 | + | targetSess, transition, switched = d.switchToActiveTargetLocked(from, ac, active, target, guard, sourceEffect, action) |
| 1094 | } | 1092 | } | ||
| 1095 | d.mu.Unlock() | 1093 | d.mu.Unlock() | ||
| 1096 | 1094 | ||||
| 1097 | if !switched { | 1095 | if !switched { | ||
| 1098 | - | if sourceToken != nil && !sourceToken.attachmentCurrent() { | 1096 | + | if sourceCapability != nil && !sourceCapability.current() { |
| 1099 | return errAttachmentTransition | 1097 | return errAttachmentTransition | ||
| 1100 | } | 1098 | } | ||
| 1101 | if guard.expectedSource != nil { | 1099 | if guard.expectedSource != nil { | ||
| @@ -1105,7 +1103,7 @@ func (d *Daemon) switchToTargetGuardedForAttachment(from *session, ac *attachedC | |||||
| 1105 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't switch to that session", cause) | 1103 | return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't switch to that session", cause) | ||
| 1106 | } | 1104 | } | ||
| 1107 | if guard.closePicker { | 1105 | if guard.closePicker { | ||
| 1108 | - | if sourceToken != nil { | 1106 | + | if sourceEffect != nil { |
| 1109 | fresh, admitted := ac.beginAttachmentEffect(transition.published) | 1107 | fresh, admitted := ac.beginAttachmentEffect(transition.published) | ||
| 1110 | if admitted { | 1108 | if admitted { | ||
| 1111 | d.closePicker(ac) | 1109 | d.closePicker(ac) | ||
| @@ -1117,7 +1115,7 @@ func (d *Daemon) switchToTargetGuardedForAttachment(from *session, ac *attachedC | |||||
| 1117 | } | 1115 | } | ||
| 1118 | d.deferAttachmentTransitionCleanups(transition) | 1116 | d.deferAttachmentTransitionCleanups(transition) | ||
| 1119 | if targetSess == from { | 1117 | if targetSess == from { | ||
| 1120 | - | if sourceToken != nil { | 1118 | + | if sourceEffect != nil { |
| 1121 | fresh, admitted := ac.beginAttachmentEffect(transition.published) | 1119 | fresh, admitted := ac.beginAttachmentEffect(transition.published) | ||
| 1122 | if admitted { | 1120 | if admitted { | ||
| 1123 | d.activateTabAfterResizeForLease(from, from.tabForAttachment(ac), false, ac, transition.published.lease) | 1121 | d.activateTabAfterResizeForLease(from, from.tabForAttachment(ac), false, ac, transition.published.lease) | ||
| @@ -1156,12 +1154,17 @@ func (d *Daemon) resolveNamedLifecycleTargetLocked(target picker.Target) (*sessi | |||||
| 1156 | 1154 | ||||
| 1157 | // switchToActiveTargetLocked commits an active target handoff through the | 1155 | // switchToActiveTargetLocked commits an active target handoff through the | ||
| 1158 | // centralized attachment transition. Caller holds d.mu. | 1156 | // centralized attachment transition. Caller holds d.mu. | ||
| 1159 | - | func (d *Daemon) switchToActiveTargetLocked(from *session, ac *attachedClient, targetSess *session, target picker.Target, guard sessionHandoffGuard, sourceToken *attachmentConnectionToken, action string) (*session, attachmentTransitionResult, bool) { | 1157 | + | func (d *Daemon) switchToActiveTargetLocked(from *session, ac *attachedClient, targetSess *session, target picker.Target, guard sessionHandoffGuard, sourceEffect *attachmentEffect, action string) (*session, attachmentTransitionResult, bool) { |
| 1158 | + | var sourceCapability *attachmentCapability | |||
| 1159 | + | if sourceEffect != nil { | |||
| 1160 | + | capability := sourceEffect.capability() | |||
| 1161 | + | sourceCapability = &capability | |||
| 1162 | + | } | |||
| 1160 | if d.sessions[target.Session] != targetSess { | 1163 | if d.sessions[target.Session] != targetSess { | ||
| 1161 | return nil, attachmentTransitionResult{}, false | 1164 | return nil, attachmentTransitionResult{}, false | ||
| 1162 | } | 1165 | } | ||
| 1163 | if targetSess == from { | 1166 | if targetSess == from { | ||
| 1164 | - | if sourceToken == nil { | 1167 | + | if sourceEffect == nil { |
| 1165 | targetSess.mu.Lock() | 1168 | targetSess.mu.Lock() | ||
| 1166 | defer targetSess.mu.Unlock() | 1169 | defer targetSess.mu.Unlock() | ||
| 1167 | _, attached := targetSess.attachments[ac] | 1170 | _, attached := targetSess.attachments[ac] | ||
| @@ -1177,8 +1180,9 @@ func (d *Daemon) switchToActiveTargetLocked(from *session, ac *attachedClient, t | |||||
| 1177 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 1180 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 1178 | source: from, target: targetSess, next: ac, | 1181 | source: from, target: targetSess, next: ac, | ||
| 1179 | 1182 | ||||
| 1180 | - | expectedTransport: sourceToken.transport, | 1183 | + | expectedTransport: sourceCapability.transport, |
| 1181 | - | sourceToken: sourceToken, | 1184 | + | sourceCapability: sourceCapability, |
| 1185 | + | sourceEffect: sourceEffect, | |||
| 1182 | action: action, | 1186 | action: action, | ||
| 1183 | expectedTargetLifecycle: pickerTargetLifecycleFence(target), | 1187 | expectedTargetLifecycle: pickerTargetLifecycleFence(target), | ||
| 1184 | activateTargetTab: true, | 1188 | activateTargetTab: true, | ||
| @@ -1215,8 +1219,8 @@ func (d *Daemon) switchToActiveTargetLocked(from *session, ac *attachedClient, t | |||||
| 1215 | // d.mu and revalidates both registered lifecycles for publication. | 1219 | // d.mu and revalidates both registered lifecycles for publication. | ||
| 1216 | d.mu.Unlock() | 1220 | d.mu.Unlock() | ||
| 1217 | expectedTransport := ac.transportSnapshot() | 1221 | expectedTransport := ac.transportSnapshot() | ||
| 1218 | - | if sourceToken != nil { | 1222 | + | if sourceCapability != nil { |
| 1219 | - | expectedTransport = sourceToken.transport | 1223 | + | expectedTransport = sourceCapability.transport |
| 1220 | } | 1224 | } | ||
| 1221 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 1225 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 1222 | source: from, | 1226 | source: from, | ||
| @@ -1224,7 +1228,8 @@ func (d *Daemon) switchToActiveTargetLocked(from *session, ac *attachedClient, t | |||||
| 1224 | next: ac, | 1228 | next: ac, | ||
| 1225 | 1229 | ||||
| 1226 | expectedTransport: expectedTransport, | 1230 | expectedTransport: expectedTransport, | ||
| 1227 | - | sourceToken: sourceToken, | 1231 | + | sourceCapability: sourceCapability, |
| 1232 | + | sourceEffect: sourceEffect, | |||
| 1228 | action: action, | 1233 | action: action, | ||
| 1229 | expectedTargetLifecycle: pickerTargetLifecycleFence(target), | 1234 | expectedTargetLifecycle: pickerTargetLifecycleFence(target), | ||
| 1230 | expectedSourceTab: guard.expectedSource, | 1235 | expectedSourceTab: guard.expectedSource, | ||
| @@ -1244,7 +1249,7 @@ func (d *Daemon) switchToActiveTargetLocked(from *session, ac *attachedClient, t | |||||
| 1244 | // resumeStoppedAndSwitchLocked creates the stopped representation and commits | 1249 | // resumeStoppedAndSwitchLocked creates the stopped representation and commits | ||
| 1245 | // the handoff while d.mu is held. Creation failure leaves the source client | 1250 | // the handoff while d.mu is held. Creation failure leaves the source client | ||
| 1246 | // and stopped record untouched. | 1251 | // and stopped record untouched. | ||
| 1247 | - | func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, target picker.Target, stopped inactiveSession, sourceToken *attachmentConnectionToken, guard sessionHandoffGuard, action string) (*session, attachmentTransitionResult, bool, error) { | 1252 | + | func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, target picker.Target, stopped inactiveSession, sourceEffect *attachmentEffect, guard sessionHandoffGuard, action string) (*session, attachmentTransitionResult, bool, error) { |
| 1248 | if stopped.broken() { | 1253 | if stopped.broken() { | ||
| 1249 | return nil, attachmentTransitionResult{}, false, &protoErr{ports.ErrInternal, "session durable state is broken: " + target.Name} | 1254 | return nil, attachmentTransitionResult{}, false, &protoErr{ports.ErrInternal, "session durable state is broken: " + target.Name} | ||
| 1250 | } | 1255 | } | ||
| @@ -1252,12 +1257,13 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, | |||||
| 1252 | return nil, attachmentTransitionResult{}, false, errAttachmentTransition | 1257 | return nil, attachmentTransitionResult{}, false, errAttachmentTransition | ||
| 1253 | } | 1258 | } | ||
| 1254 | 1259 | ||||
| 1255 | - | if sourceToken != nil { | 1260 | + | if sourceEffect != nil { |
| 1261 | + | sourceCapability := sourceEffect.capability() | |||
| 1256 | var targetSess *session | 1262 | var targetSess *session | ||
| 1257 | d.mu.Unlock() | 1263 | d.mu.Unlock() | ||
| 1258 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 1264 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 1259 | source: from, next: ac, | 1265 | source: from, next: ac, | ||
| 1260 | - | expectedTransport: sourceToken.transport, sourceToken: sourceToken, action: action, | 1266 | + | expectedTransport: sourceCapability.transport, sourceCapability: &sourceCapability, sourceEffect: sourceEffect, action: action, |
| 1261 | expectedSourceTab: guard.expectedSource, copySourceEnvironment: true, ready: true, | 1267 | expectedSourceTab: guard.expectedSource, copySourceEnvironment: true, ready: true, | ||
| 1262 | createTargetLocked: func() (*session, error) { | 1268 | createTargetLocked: func() (*session, error) { | ||
| 1263 | current, ok := d.inactive[target.Name] | 1269 | current, ok := d.inactive[target.Name] | ||
| @@ -1359,19 +1365,20 @@ func (d *Daemon) resumeStoppedAndSwitch(from *session, ac *attachedClient, targe | |||||
| 1359 | return true | 1365 | return true | ||
| 1360 | } | 1366 | } | ||
| 1361 | 1367 | ||||
| 1362 | - | func (d *Daemon) killPickerTargetForAttachment(target picker.Target, token attachmentConnectionToken) error { | 1368 | + | func (d *Daemon) killPickerTargetForAttachment(target picker.Target, effect *attachmentEffect) error { |
| 1363 | - | if token.ac == nil { | 1369 | + | if effect == nil || effect.ac == nil { |
| 1364 | return d.killPickerTarget(target) | 1370 | return d.killPickerTarget(target) | ||
| 1365 | } | 1371 | } | ||
| 1366 | if target.RemoteKey != nil { | 1372 | if target.RemoteKey != nil { | ||
| 1367 | return nil | 1373 | return nil | ||
| 1368 | } | 1374 | } | ||
| 1375 | + | capability := effect.capability() | |||
| 1369 | if target.Stopped { | 1376 | if target.Stopped { | ||
| 1370 | - | token.effect.bindActionEnd(d, "picker-delete") | 1377 | + | effect.bindActionEnd(d, "picker-delete") |
| 1371 | - | token.effect.End() | 1378 | + | effect.End() |
| 1372 | - | frozen := freezeAttachmentEffectGates(token.ac) | 1379 | + | frozen := freezeAttachmentEffectGates(effect.ac) |
| 1373 | defer frozen.unfreeze() | 1380 | defer frozen.unfreeze() | ||
| 1374 | - | if !d.sourceAttachmentTokenCurrentFrozen(token) { | 1381 | + | if !d.sourceAttachmentCapabilityCurrentFrozen(capability) { |
| 1375 | return nil | 1382 | return nil | ||
| 1376 | } | 1383 | } | ||
| 1377 | return d.killPickerTarget(target) | 1384 | return d.killPickerTarget(target) | ||
| @@ -1382,7 +1389,7 @@ func (d *Daemon) killPickerTargetForAttachment(target picker.Target, token attac | |||||
| 1382 | if targetSess == nil { | 1389 | if targetSess == nil { | ||
| 1383 | return nil | 1390 | return nil | ||
| 1384 | } | 1391 | } | ||
| 1385 | - | return d.killSessionForAttachment(targetSess, ports.ReasonSessionKilled, true, token, "picker-delete") | 1392 | + | return d.killSessionForAttachment(targetSess, ports.ReasonSessionKilled, true, effect, "picker-delete") |
| 1386 | } | 1393 | } | ||
| 1387 | 1394 | ||||
| 1388 | func (d *Daemon) killPickerTarget(target picker.Target) error { | 1395 | func (d *Daemon) killPickerTarget(target picker.Target) error { | ||
M internal/usecase/daemon/picker_test.go +5 -6
| @@ -900,13 +900,12 @@ func TestPickerNavigationBackSendFailureKeepsPickerOpen(t *testing.T) { | |||||
| 900 | ac.overlays.pickerMu.Unlock() | 900 | ac.overlays.pickerMu.Unlock() | ||
| 901 | 901 | ||||
| 902 | rc := d.attachCoordinator(sess, nil, ac, true) | 902 | rc := d.attachCoordinator(sess, nil, ac, true) | ||
| 903 | - | token := sess.attachmentToken(ac, tr) | 903 | + | token := sess.captureAttachmentCapability(ac, tr) |
| 904 | token.lease = rc.attachmentLease(ac) | 904 | token.lease = rc.attachmentLease(ac) | ||
| 905 | - | ac.publishAttachmentCapability(token) | 905 | + | ac.installTestAttachmentCapability(token) |
| 906 | effect, admitted := ac.beginAttachmentEffect(token) | 906 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 907 | require.True(t, admitted) | 907 | require.True(t, admitted) | ||
| 908 | defer effect.End() | 908 | defer effect.End() | ||
| 909 | - | token.effect = effect | |||
| 910 | 909 | ||||
| 911 | d.handlePickerInput(ac, []byte("\r"), effect) | 910 | d.handlePickerInput(ac, []byte("\r"), effect) | ||
| 912 | require.True(t, ac.overlays.pickerActive(), "failed navigation send must leave the picker open") | 911 | require.True(t, ac.overlays.pickerActive(), "failed navigation send must leave the picker open") | ||
| @@ -1063,7 +1062,7 @@ func TestPickerSessionSwitchPublishesBeforeInFlightPaintSendCompletes(t *testing | |||||
| 1063 | case <-time.After(2 * time.Second): | 1062 | case <-time.After(2 * time.Second): | ||
| 1064 | t.Fatal("session switch waited for in-flight transport send") | 1063 | t.Fatal("session switch waited for in-flight transport send") | ||
| 1065 | } | 1064 | } | ||
| 1066 | - | require.True(t, result.published.attachmentCurrent()) | 1065 | + | require.True(t, result.published.current()) |
| 1067 | select { | 1066 | select { | ||
| 1068 | case <-handoffAtSendMu: | 1067 | case <-handoffAtSendMu: | ||
| 1069 | t.Fatal("output rebase ran during architecture publication") | 1068 | t.Fatal("output rebase ran during architecture publication") | ||
| @@ -1584,9 +1583,9 @@ func TestPickerAttachmentEffectDeleteRemovesSelectedSessionAndRefreshes(t *testi | |||||
| 1584 | require.True(t, ok) | 1583 | require.True(t, ok) | ||
| 1585 | require.Equal(t, target.id, selected.Session) | 1584 | require.Equal(t, target.id, selected.Session) | ||
| 1586 | 1585 | ||||
| 1587 | - | token := current.attachmentToken(ac, ac.transport()) | 1586 | + | token := current.captureAttachmentCapability(ac, ac.transport()) |
| 1588 | token.lease = current.renderCoordinator().attachmentLease(ac) | 1587 | token.lease = current.renderCoordinator().attachmentLease(ac) | ||
| 1589 | - | ac.publishAttachmentCapability(token) | 1588 | + | ac.installTestAttachmentCapability(token) |
| 1590 | effect, admitted := ac.beginAttachmentEffect(token) | 1589 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 1591 | require.True(t, admitted) | 1590 | require.True(t, admitted) | ||
| 1592 | d.handlePickerInput(ac, []byte("x"), effect) | 1591 | d.handlePickerInput(ac, []byte("x"), effect) | ||
M internal/usecase/daemon/prompt.go +7 -7
| @@ -23,7 +23,7 @@ func (d *Daemon) enterPrompt(sess *session, ac *attachedClient, title, initial s | |||||
| 23 | d.invalidateRender(sess, ac, true, "prompt.go") | 23 | d.invalidateRender(sess, ac, true, "prompt.go") | ||
| 24 | } | 24 | } | ||
| 25 | 25 | ||||
| 26 | - | func (d *Daemon) enterTransitionPrompt(sess *session, ac *attachedClient, title, initial string, submit func(string, attachmentConnectionToken) error) { | 26 | + | func (d *Daemon) enterTransitionPrompt(sess *session, ac *attachedClient, title, initial string, submit func(string, *attachmentEffect) error) { |
| 27 | d.closePrompt(ac) | 27 | d.closePrompt(ac) | ||
| 28 | ac.overlays.promptMu.Lock() | 28 | ac.overlays.promptMu.Lock() | ||
| 29 | ac.overlays.prompt = promptui.New(title, initial) | 29 | ac.overlays.prompt = promptui.New(title, initial) | ||
| @@ -50,7 +50,7 @@ func promptValidationError(err error) bool { | |||||
| 50 | errors.Is(err, errSessionNameInUse) | 50 | errors.Is(err, errSessionNameInUse) | ||
| 51 | } | 51 | } | ||
| 52 | 52 | ||||
| 53 | - | func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte, effects ...*attachmentEffectTicket) { | 53 | + | func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte, effects ...*attachmentEffect) { |
| 54 | sess := ac.currentAttachmentSession() | 54 | sess := ac.currentAttachmentSession() | ||
| 55 | if sess == nil { | 55 | if sess == nil { | ||
| 56 | return | 56 | return | ||
| @@ -66,7 +66,7 @@ func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte, effects ...* | |||||
| 66 | changed := false | 66 | changed := false | ||
| 67 | exit := false | 67 | exit := false | ||
| 68 | var submit func(string) error | 68 | var submit func(string) error | ||
| 69 | - | var transitionSubmit func(string, attachmentConnectionToken) error | 69 | + | var transitionSubmit func(string, *attachmentEffect) error |
| 70 | var submittedPrompt *promptui.Model | 70 | var submittedPrompt *promptui.Model | ||
| 71 | var value string | 71 | var value string | ||
| 72 | 72 | ||||
| @@ -94,11 +94,11 @@ func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte, effects ...* | |||||
| 94 | if submit != nil || transitionSubmit != nil { | 94 | if submit != nil || transitionSubmit != nil { | ||
| 95 | var err error | 95 | var err error | ||
| 96 | if transitionSubmit != nil { | 96 | if transitionSubmit != nil { | ||
| 97 | - | var token attachmentConnectionToken | 97 | + | var effect *attachmentEffect |
| 98 | - | if len(effects) != 0 && effects[0] != nil { | 98 | + | if len(effects) != 0 { |
| 99 | - | token = effects[0].connectionToken() | 99 | + | effect = effects[0] |
| 100 | } | 100 | } | ||
| 101 | - | err = transitionSubmit(value, token) | 101 | + | err = transitionSubmit(value, effect) |
| 102 | } else { | 102 | } else { | ||
| 103 | err = submit(value) | 103 | err = submit(value) | ||
| 104 | } | 104 | } | ||
M internal/usecase/daemon/recent_route_snapshot_test.go +4 -5
| @@ -64,8 +64,8 @@ func TestAttachmentStatusResolvesSubscribedRouteAttention(t *testing.T) { | |||||
| 64 | 64 | ||||
| 65 | func TestRecentRouteSnapshotRepaintsWithoutDeferredIdentity(t *testing.T) { | 65 | func TestRecentRouteSnapshotRepaintsWithoutDeferredIdentity(t *testing.T) { | ||
| 66 | d, sess, ac, sends := newManualSessionWithPTYs(t, nil) | 66 | d, sess, ac, sends := newManualSessionWithPTYs(t, nil) | ||
| 67 | - | token := sess.attachmentToken(ac, ac.transport()) | 67 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 68 | - | ac.publishAttachmentCapability(token) | 68 | + | ac.installTestAttachmentCapability(token) |
| 69 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | 69 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | ||
| 70 | 70 | ||||
| 71 | payload, err := ports.MarshalRecentRouteSnapshot(ports.RecentRouteSnapshot{Generation: 2}) | 71 | payload, err := ports.MarshalRecentRouteSnapshot(ports.RecentRouteSnapshot{Generation: 2}) | ||
| @@ -93,13 +93,12 @@ func TestPaletteRecentRouteSelectionSendsTypedClientAction(t *testing.T) { | |||||
| 93 | transport := &closeTrackingTransport{} | 93 | transport := &closeTrackingTransport{} | ||
| 94 | ac.replaceTransport(transport) | 94 | ac.replaceTransport(transport) | ||
| 95 | rc := d.attachCoordinator(source, nil, ac, true) | 95 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 96 | - | token := source.attachmentToken(ac, transport) | 96 | + | token := source.captureAttachmentCapability(ac, transport) |
| 97 | token.lease = rc.attachmentLease(ac) | 97 | token.lease = rc.attachmentLease(ac) | ||
| 98 | - | ac.publishAttachmentCapability(token) | 98 | + | ac.installTestAttachmentCapability(token) |
| 99 | effect, admitted := ac.beginAttachmentEffect(token) | 99 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 100 | require.True(t, admitted) | 100 | require.True(t, admitted) | ||
| 101 | defer effect.End() | 101 | defer effect.End() | ||
| 102 | - | token.effect = effect | |||
| 103 | 102 | ||||
| 104 | exec := paletteExec{ | 103 | exec := paletteExec{ | ||
| 105 | d: d, sess: source, attachment: source, ac: ac, | 104 | d: d, sess: source, attachment: source, ac: ac, | ||
M internal/usecase/daemon/remote_attach.go +13 -13
| @@ -102,11 +102,11 @@ func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTa | |||||
| 102 | return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, geometry, env, &expected, validate, expected.tabNames) | 102 | return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, geometry, env, &expected, validate, expected.tabNames) | ||
| 103 | } | 103 | } | ||
| 104 | 104 | ||||
| 105 | - | func (d *Daemon) sendNavigationActionForAttachment(token attachmentConnectionToken, action ports.NavigationAction) error { | 105 | + | func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, action ports.NavigationAction) error { |
| 106 | directive := ports.NavigationDirective{Action: action} | 106 | directive := ports.NavigationDirective{Action: action} | ||
| 107 | armed := false | 107 | armed := false | ||
| 108 | if action == ports.NavigationOpenHomePicker { | 108 | if action == ports.NavigationOpenHomePicker { | ||
| 109 | - | leaseID, err := d.armParkedRoute(token) | 109 | + | leaseID, err := d.armParkedRoute(effect) |
| 110 | if err != nil { | 110 | if err != nil { | ||
| 111 | return err | 111 | return err | ||
| 112 | } | 112 | } | ||
| @@ -115,7 +115,7 @@ func (d *Daemon) sendNavigationActionForAttachment(token attachmentConnectionTok | |||||
| 115 | } | 115 | } | ||
| 116 | rollback := func() { | 116 | rollback := func() { | ||
| 117 | if armed { | 117 | if armed { | ||
| 118 | - | token.ac.clearParkedRoute() | 118 | + | effect.ac.clearParkedRoute() |
| 119 | } | 119 | } | ||
| 120 | } | 120 | } | ||
| 121 | payload := ports.MarshalNavigationDirective(directive) | 121 | payload := ports.MarshalNavigationDirective(directive) | ||
| @@ -123,36 +123,36 @@ func (d *Daemon) sendNavigationActionForAttachment(token attachmentConnectionTok | |||||
| 123 | rollback() | 123 | rollback() | ||
| 124 | return errAttachmentTransition | 124 | return errAttachmentTransition | ||
| 125 | } | 125 | } | ||
| 126 | - | if err := token.sendControl(ports.Frame{Type: ports.MsgNavigationAction, Payload: payload}); err != nil { | 126 | + | if err := effect.sendControl(ports.Frame{Type: ports.MsgNavigationAction, Payload: payload}); err != nil { |
| 127 | rollback() | 127 | rollback() | ||
| 128 | return err | 128 | return err | ||
| 129 | } | 129 | } | ||
| 130 | return nil | 130 | return nil | ||
| 131 | } | 131 | } | ||
| 132 | 132 | ||||
| 133 | - | func (d *Daemon) sendRecentRouteNavigationActionForAttachment(token attachmentConnectionToken, action ports.RouteNavigationAction) error { | 133 | + | func (d *Daemon) sendRecentRouteNavigationActionForAttachment(effect *attachmentEffect, action ports.RouteNavigationAction) error { |
| 134 | payload, err := ports.MarshalRouteNavigationAction(action) | 134 | payload, err := ports.MarshalRouteNavigationAction(action) | ||
| 135 | if err != nil { | 135 | if err != nil { | ||
| 136 | return errAttachmentTransition | 136 | return errAttachmentTransition | ||
| 137 | } | 137 | } | ||
| 138 | - | return token.sendControl(ports.Frame{Type: ports.MsgNavigateRecentRoute, Payload: payload}) | 138 | + | return effect.sendControl(ports.Frame{Type: ports.MsgNavigateRecentRoute, Payload: payload}) |
| 139 | } | 139 | } | ||
| 140 | 140 | ||||
| 141 | - | func (d *Daemon) sendCommittedRouteIdentityForAttachment(token attachmentConnectionToken) error { | 141 | + | func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffect) error { |
| 142 | - | if token.sess == nil { | 142 | + | if !effect.current() || effect.sess == nil { |
| 143 | return errAttachmentTransition | 143 | return errAttachmentTransition | ||
| 144 | } | 144 | } | ||
| 145 | - | token.sess.mu.Lock() | 145 | + | effect.sess.mu.Lock() |
| 146 | identity := ports.CommittedRouteIdentity{ | 146 | identity := ports.CommittedRouteIdentity{ | ||
| 147 | - | Target: ports.ExactSessionTarget{LifecycleID: token.sess.incarnation, SessionName: token.sess.name}, | 147 | + | Target: ports.ExactSessionTarget{LifecycleID: effect.sess.incarnation, SessionName: effect.sess.name}, |
| 148 | - | Ephemeral: token.sess.ephemeral, | 148 | + | Ephemeral: effect.sess.ephemeral, |
| 149 | } | 149 | } | ||
| 150 | - | token.sess.mu.Unlock() | 150 | + | effect.sess.mu.Unlock() |
| 151 | payload, err := ports.MarshalCommittedRouteIdentity(identity) | 151 | payload, err := ports.MarshalCommittedRouteIdentity(identity) | ||
| 152 | if err != nil { | 152 | if err != nil { | ||
| 153 | return errAttachmentTransition | 153 | return errAttachmentTransition | ||
| 154 | } | 154 | } | ||
| 155 | - | return token.sendControl(ports.Frame{Type: ports.MsgCommittedRouteIdentity, Payload: payload}) | 155 | + | return effect.sendControl(ports.Frame{Type: ports.MsgCommittedRouteIdentity, Payload: payload}) |
| 156 | } | 156 | } | ||
| 157 | 157 | ||||
| 158 | func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello, routeCreated, purge bool) (*attachedClient, error) { | 158 | func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello, routeCreated, purge bool) (*attachedClient, error) { | ||
M internal/usecase/daemon/remote_attach_test.go +10 -15
| @@ -22,13 +22,12 @@ func TestLocalPickerOfferCarriesExactLifecycle(t *testing.T) { | |||||
| 22 | source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source") | 22 | source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source") | ||
| 23 | target, _, _ := addRemoteRefreshPickerOwner(t, d, "target") | 23 | target, _, _ := addRemoteRefreshPickerOwner(t, d, "target") | ||
| 24 | target.incarnation = remoteLifecycleForTest() | 24 | target.incarnation = remoteLifecycleForTest() | ||
| 25 | - | token := source.attachmentToken(ac, ac.transport()) | 25 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 26 | effect, admitted := ac.beginAttachmentEffect(token) | 26 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 27 | require.True(t, admitted) | 27 | require.True(t, admitted) | ||
| 28 | - | token.effect = effect | |||
| 29 | defer effect.End() | 28 | defer effect.End() | ||
| 30 | 29 | ||||
| 31 | - | require.NoError(t, d.switchToTargetForAttachment(token, picker.Target{Session: target.id}, sessionHandoffGuard{allowSamePeer: true}, "picker-select")) | 30 | + | require.NoError(t, d.switchToTargetForAttachment(effect, picker.Target{Session: target.id}, sessionHandoffGuard{allowSamePeer: true}, "picker-select")) |
| 32 | frame := receiveRemotePicker(t, sends, "local attach target") | 31 | frame := receiveRemotePicker(t, sends, "local attach target") | ||
| 33 | got, err := ports.UnmarshalAttachTarget(frame.Payload) | 32 | got, err := ports.UnmarshalAttachTarget(frame.Payload) | ||
| 34 | require.NoError(t, err) | 33 | require.NoError(t, err) | ||
| @@ -51,13 +50,12 @@ func TestStoppedLocalPickerHandoffWaitsForClientClose(t *testing.T) { | |||||
| 51 | graphics.mayHaveEmitted = true | 50 | graphics.mayHaveEmitted = true | ||
| 52 | ac.output.graphicsOutput = graphics | 51 | ac.output.graphicsOutput = graphics | ||
| 53 | 52 | ||||
| 54 | - | token := source.attachmentToken(ac, ac.transport()) | 53 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 55 | effect, admitted := ac.beginAttachmentEffect(token) | 54 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 56 | require.True(t, admitted) | 55 | require.True(t, admitted) | ||
| 57 | - | token.effect = effect | |||
| 58 | defer effect.End() | 56 | defer effect.End() | ||
| 59 | 57 | ||||
| 60 | - | require.NoError(t, d.switchToTargetForAttachment(token, picker.Target{ | 58 | + | require.NoError(t, d.switchToTargetForAttachment(effect, picker.Target{ |
| 61 | Name: "stopped", Incarnation: lifecycle, | 59 | Name: "stopped", Incarnation: lifecycle, | ||
| 62 | }, sessionHandoffGuard{closePicker: true, allowSamePeer: true}, "picker-select")) | 60 | }, sessionHandoffGuard{closePicker: true, allowSamePeer: true}, "picker-select")) | ||
| 63 | 61 | ||||
| @@ -85,15 +83,14 @@ func TestRemotePickerRichHandoffCarriesLifecycleTabAndPolicy(t *testing.T) { | |||||
| 85 | d.remoteCatalog.status["arch"] = remoteHostFresh | 83 | d.remoteCatalog.status["arch"] = remoteHostFresh | ||
| 86 | d.remoteCatalog.mu.Unlock() | 84 | d.remoteCatalog.mu.Unlock() | ||
| 87 | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | 85 | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | ||
| 88 | - | token := sess.attachmentToken(ac, ac.transport()) | 86 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 89 | effect, admitted := ac.beginAttachmentEffect(token) | 87 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 90 | require.True(t, admitted) | 88 | require.True(t, admitted) | ||
| 91 | - | token.effect = effect | |||
| 92 | defer effect.End() | 89 | defer effect.End() | ||
| 93 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | 90 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | ||
| 94 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | 91 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | ||
| 95 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | 92 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | ||
| 96 | - | require.NoError(t, d.sendRemoteAttachTargetForAttachment(token, target, sessionHandoffGuard{closePicker: true}, "picker-select")) | 93 | + | require.NoError(t, d.sendRemoteAttachTargetForAttachment(effect, target, sessionHandoffGuard{closePicker: true}, "picker-select")) |
| 97 | frame := receiveRemotePicker(t, sends, "rich attach target") | 94 | frame := receiveRemotePicker(t, sends, "rich attach target") | ||
| 98 | got, err := ports.UnmarshalAttachTarget(frame.Payload) | 95 | got, err := ports.UnmarshalAttachTarget(frame.Payload) | ||
| 99 | require.NoError(t, err) | 96 | require.NoError(t, err) | ||
| @@ -128,10 +125,9 @@ func TestRemotePickerRichHandoffRejectsMismatchedRouteKey(t *testing.T) { | |||||
| 128 | d.remoteCatalog.status["arch"] = remoteHostFresh | 125 | d.remoteCatalog.status["arch"] = remoteHostFresh | ||
| 129 | d.remoteCatalog.mu.Unlock() | 126 | d.remoteCatalog.mu.Unlock() | ||
| 130 | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | 127 | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | ||
| 131 | - | token := sess.attachmentToken(ac, ac.transport()) | 128 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 132 | effect, admitted := ac.beginAttachmentEffect(token) | 129 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 133 | require.True(t, admitted) | 130 | require.True(t, admitted) | ||
| 134 | - | token.effect = effect | |||
| 135 | defer effect.End() | 131 | defer effect.End() | ||
| 136 | 132 | ||||
| 137 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | 133 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | ||
| @@ -139,7 +135,7 @@ func TestRemotePickerRichHandoffRejectsMismatchedRouteKey(t *testing.T) { | |||||
| 139 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | 135 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | ||
| 140 | test.mutate(&target) | 136 | test.mutate(&target) | ||
| 141 | 137 | ||||
| 142 | - | require.ErrorIs(t, d.sendRemoteAttachTargetForAttachment(token, target, sessionHandoffGuard{}, "picker-select"), errAttachmentTransition) | 138 | + | require.ErrorIs(t, d.sendRemoteAttachTargetForAttachment(effect, target, sessionHandoffGuard{}, "picker-select"), errAttachmentTransition) |
| 143 | for { | 139 | for { | ||
| 144 | select { | 140 | select { | ||
| 145 | case frame := <-sends: | 141 | case frame := <-sends: | ||
| @@ -166,15 +162,14 @@ func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) { | |||||
| 166 | d.remoteCatalog.status["arch"] = remoteHostFresh | 162 | d.remoteCatalog.status["arch"] = remoteHostFresh | ||
| 167 | d.remoteCatalog.mu.Unlock() | 163 | d.remoteCatalog.mu.Unlock() | ||
| 168 | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | 164 | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | ||
| 169 | - | token := sess.attachmentToken(ac, ac.transport()) | 165 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 170 | effect, admitted := ac.beginAttachmentEffect(token) | 166 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 171 | require.True(t, admitted) | 167 | require.True(t, admitted) | ||
| 172 | - | token.effect = effect | |||
| 173 | defer effect.End() | 168 | defer effect.End() | ||
| 174 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | 169 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | ||
| 175 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | 170 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | ||
| 176 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | 171 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | ||
| 177 | - | require.ErrorIs(t, d.sendRemoteAttachTargetForAttachment(token, target, sessionHandoffGuard{}, "picker-select"), errAttachmentTransition) | 172 | + | require.ErrorIs(t, d.sendRemoteAttachTargetForAttachment(effect, target, sessionHandoffGuard{}, "picker-select"), errAttachmentTransition) |
| 178 | for { | 173 | for { | ||
| 179 | select { | 174 | select { | ||
| 180 | case frame := <-sends: | 175 | case frame := <-sends: | ||
M internal/usecase/daemon/remote_picker_test.go +11 -15
| @@ -686,14 +686,13 @@ func TestRemotePickerHandoffSendsTargetAndLeavesNoShadowSession(t *testing.T) { | |||||
| 686 | d.remoteCatalog.status["arch"] = remoteHostFresh | 686 | d.remoteCatalog.status["arch"] = remoteHostFresh | ||
| 687 | d.remoteCatalog.mu.Unlock() | 687 | d.remoteCatalog.mu.Unlock() | ||
| 688 | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | 688 | sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local") | ||
| 689 | - | token := sess.attachmentToken(ac, ac.transport()) | 689 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 690 | effect, admitted := ac.beginAttachmentEffect(token) | 690 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 691 | require.True(t, admitted) | 691 | require.True(t, admitted) | ||
| 692 | - | token.effect = effect | |||
| 693 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | 692 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | ||
| 694 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | 693 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | ||
| 695 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | 694 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | ||
| 696 | - | require.NoError(t, d.sendRemoteAttachTargetForAttachment(token, target, sessionHandoffGuard{closePicker: true}, "picker-select")) | 695 | + | require.NoError(t, d.sendRemoteAttachTargetForAttachment(effect, target, sessionHandoffGuard{closePicker: true}, "picker-select")) |
| 697 | 696 | ||||
| 698 | frame := receiveRemotePicker(t, sends, "attach target") | 697 | frame := receiveRemotePicker(t, sends, "attach target") | ||
| 699 | require.Equal(t, ports.MsgAttachTarget, frame.Type) | 698 | require.Equal(t, ports.MsgAttachTarget, frame.Type) | ||
| @@ -742,11 +741,10 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) { | |||||
| 742 | local.remoteCatalog.status["arch"] = remoteHostFresh | 741 | local.remoteCatalog.status["arch"] = remoteHostFresh | ||
| 743 | local.remoteCatalog.mu.Unlock() | 742 | local.remoteCatalog.mu.Unlock() | ||
| 744 | localSession, localAttachment, sends := addRemoteRefreshPickerOwner(t, local, "local") | 743 | localSession, localAttachment, sends := addRemoteRefreshPickerOwner(t, local, "local") | ||
| 745 | - | token := localSession.attachmentToken(localAttachment, localAttachment.transport()) | 744 | + | token := localSession.captureAttachmentCapability(localAttachment, localAttachment.transport()) |
| 746 | effect, admitted := localAttachment.beginAttachmentEffect(token) | 745 | effect, admitted := localAttachment.beginAttachmentEffect(token) | ||
| 747 | require.True(t, admitted) | 746 | require.True(t, admitted) | ||
| 748 | - | token.effect = effect | 747 | + | require.NoError(t, local.sendRemoteAttachTargetForAttachment(effect, selected, sessionHandoffGuard{closePicker: true}, "picker-select")) |
| 749 | - | require.NoError(t, local.sendRemoteAttachTargetForAttachment(token, selected, sessionHandoffGuard{closePicker: true}, "picker-select")) | |||
| 750 | 748 | ||||
| 751 | frame := receiveRemotePicker(t, sends, "stopped remote target") | 749 | frame := receiveRemotePicker(t, sends, "stopped remote target") | ||
| 752 | handoff, err := ports.UnmarshalAttachTarget(frame.Payload) | 750 | handoff, err := ports.UnmarshalAttachTarget(frame.Payload) | ||
| @@ -814,11 +812,10 @@ func TestRemotePickerResurrectsStoppedRemoteSessionWithoutTabMetadata(t *testing | |||||
| 814 | local.remoteCatalog.status["arch"] = remoteHostFresh | 812 | local.remoteCatalog.status["arch"] = remoteHostFresh | ||
| 815 | local.remoteCatalog.mu.Unlock() | 813 | local.remoteCatalog.mu.Unlock() | ||
| 816 | localSession, localAttachment, sends := addRemoteRefreshPickerOwner(t, local, "local") | 814 | localSession, localAttachment, sends := addRemoteRefreshPickerOwner(t, local, "local") | ||
| 817 | - | token := localSession.attachmentToken(localAttachment, localAttachment.transport()) | 815 | + | token := localSession.captureAttachmentCapability(localAttachment, localAttachment.transport()) |
| 818 | effect, admitted := localAttachment.beginAttachmentEffect(token) | 816 | effect, admitted := localAttachment.beginAttachmentEffect(token) | ||
| 819 | require.True(t, admitted) | 817 | require.True(t, admitted) | ||
| 820 | - | token.effect = effect | 818 | + | require.NoError(t, local.sendRemoteAttachTargetForAttachment(effect, selected, sessionHandoffGuard{closePicker: true}, "picker-select")) |
| 821 | - | require.NoError(t, local.sendRemoteAttachTargetForAttachment(token, selected, sessionHandoffGuard{closePicker: true}, "picker-select")) | |||
| 822 | 819 | ||||
| 823 | frame := receiveRemotePicker(t, sends, "stopped remote target without tab metadata") | 820 | frame := receiveRemotePicker(t, sends, "stopped remote target without tab metadata") | ||
| 824 | handoff, err := ports.UnmarshalAttachTarget(frame.Payload) | 821 | handoff, err := ports.UnmarshalAttachTarget(frame.Payload) | ||
| @@ -858,11 +855,11 @@ func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) { | |||||
| 858 | if tt.action == ports.NavigationOpenHomePicker { | 855 | if tt.action == ports.NavigationOpenHomePicker { | ||
| 859 | ac.navigationCapabilities = ports.NavigationCapabilityHomePicker | 856 | ac.navigationCapabilities = ports.NavigationCapabilityHomePicker | ||
| 860 | } | 857 | } | ||
| 861 | - | token := sess.attachmentToken(ac, ac.transport()) | 858 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 862 | effect, admitted := ac.beginAttachmentEffect(token) | 859 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 863 | require.True(t, admitted) | 860 | require.True(t, admitted) | ||
| 864 | - | token.effect = effect | 861 | + | defer effect.End() |
| 865 | - | require.NoError(t, d.sendNavigationActionForAttachment(token, tt.action)) | 862 | + | require.NoError(t, d.sendNavigationActionForAttachment(effect, tt.action)) |
| 866 | frame := receiveRemotePicker(t, sends, "navigation action") | 863 | frame := receiveRemotePicker(t, sends, "navigation action") | ||
| 867 | require.Equal(t, ports.MsgNavigationAction, frame.Type) | 864 | require.Equal(t, ports.MsgNavigationAction, frame.Type) | ||
| 868 | directive, err := ports.UnmarshalNavigationDirective(frame.Payload) | 865 | directive, err := ports.UnmarshalNavigationDirective(frame.Payload) | ||
| @@ -901,15 +898,14 @@ func TestRemotePickerHandoffSendFailureKeepsPickerOpen(t *testing.T) { | |||||
| 901 | 898 | ||||
| 902 | gone := make(chan struct{}) | 899 | gone := make(chan struct{}) | ||
| 903 | d.afterClientGoneDetach = func() { close(gone) } | 900 | d.afterClientGoneDetach = func() { close(gone) } | ||
| 904 | - | token := sess.attachmentToken(ac, tr) | 901 | + | token := sess.captureAttachmentCapability(ac, tr) |
| 905 | effect, admitted := ac.beginAttachmentEffect(token) | 902 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 906 | require.True(t, admitted) | 903 | require.True(t, admitted) | ||
| 907 | - | token.effect = effect | |||
| 908 | defer effect.End() | 904 | defer effect.End() | ||
| 909 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | 905 | key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"} | ||
| 910 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | 906 | remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"} | ||
| 911 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | 907 | target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"} | ||
| 912 | - | err := d.sendRemoteAttachTargetForAttachment(token, target, sessionHandoffGuard{closePicker: true}, "picker-select") | 908 | + | err := d.sendRemoteAttachTargetForAttachment(effect, target, sessionHandoffGuard{closePicker: true}, "picker-select") |
| 913 | var userErr *domain.UserError | 909 | var userErr *domain.UserError | ||
| 914 | require.ErrorAs(t, err, &userErr) | 910 | require.ErrorAs(t, err, &userErr) | ||
| 915 | require.Equal(t, "couldn't attach to remote session", userErr.Msg) | 911 | require.Equal(t, "couldn't attach to remote session", userErr.Msg) | ||
M internal/usecase/daemon/remote_preview_cache_test.go +2 -3
| @@ -457,12 +457,11 @@ func TestRemotePickerPreviewRefreshesAfterAttachmentResize(t *testing.T) { | |||||
| 457 | client.release = nil | 457 | client.release = nil | ||
| 458 | client.startedSignal = false | 458 | client.startedSignal = false | ||
| 459 | client.mu.Unlock() | 459 | client.mu.Unlock() | ||
| 460 | - | token := sess.attachmentToken(ac, ac.transport()) | 460 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 461 | effect, admitted := ac.beginAttachmentEffect(token) | 461 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 462 | require.True(t, admitted) | 462 | require.True(t, admitted) | ||
| 463 | - | token.effect = effect | |||
| 464 | defer effect.End() | 463 | defer effect.End() | ||
| 465 | - | require.True(t, d.resizeAttachmentForLease(token, domain.Size{Cols: 100, Rows: 30})) | 464 | + | require.True(t, d.resizeAttachmentForLease(effect, domain.Size{Cols: 100, Rows: 30})) |
| 466 | <-secondStarted | 465 | <-secondStarted | ||
| 467 | wantWidth, wantHeight := remotePickerPreviewSize(domain.Size{Cols: 100, Rows: 30}) | 466 | wantWidth, wantHeight := remotePickerPreviewSize(domain.Size{Cols: 100, Rows: 30}) | ||
| 468 | gotWidth, gotHeight := client.LastSize() | 467 | gotWidth, gotHeight := client.LastSize() | ||
M internal/usecase/daemon/render.go +19 -18
| @@ -294,7 +294,7 @@ func (d *Daemon) observeRuntime(kind ports.RuntimeMarkKind, bytes uint64, valid | |||||
| 294 | type runtimeMarkBatch struct { | 294 | type runtimeMarkBatch struct { | ||
| 295 | observer ports.RuntimeObserver | 295 | observer ports.RuntimeObserver | ||
| 296 | marks []ports.RuntimeMark | 296 | marks []ports.RuntimeMark | ||
| 297 | - | attachmentEffect *attachmentEffectTicket | 297 | + | attachmentEffect *attachmentEffect |
| 298 | } | 298 | } | ||
| 299 | 299 | ||||
| 300 | func (d *Daemon) newRuntimeMarkBatch() runtimeMarkBatch { | 300 | func (d *Daemon) newRuntimeMarkBatch() runtimeMarkBatch { | ||
| @@ -338,13 +338,15 @@ func (d *Daemon) paint(entry *session, ac *attachedClient, reset bool, lease *at | |||||
| 338 | sess := entry | 338 | sess := entry | ||
| 339 | local := sess != nil | 339 | local := sess != nil | ||
| 340 | marks := d.newRuntimeMarkBatch() | 340 | marks := d.newRuntimeMarkBatch() | ||
| 341 | + | var paintEffect *attachmentEffect | |||
| 341 | if lease != nil { | 342 | if lease != nil { | ||
| 342 | - | token := attachmentToken(entry, ac, ac.transport()) | 343 | + | token := captureAttachmentCapability(entry, ac, ac.transport()) |
| 343 | token.lease = lease | 344 | token.lease = lease | ||
| 344 | ticket, admitted := ac.beginAttachmentEffect(token) | 345 | ticket, admitted := ac.beginAttachmentEffect(token) | ||
| 345 | if !admitted { | 346 | if !admitted { | ||
| 346 | return paintRejected | 347 | return paintRejected | ||
| 347 | } | 348 | } | ||
| 349 | + | paintEffect = ticket | |||
| 348 | marks.attachmentEffect = ticket | 350 | marks.attachmentEffect = ticket | ||
| 349 | defer ticket.End() | 351 | defer ticket.End() | ||
| 350 | } | 352 | } | ||
| @@ -357,25 +359,24 @@ func (d *Daemon) paint(entry *session, ac *attachedClient, reset bool, lease *at | |||||
| 357 | } | 359 | } | ||
| 358 | 360 | ||||
| 359 | ac.sendMu.Lock() | 361 | ac.sendMu.Lock() | ||
| 360 | - | // sendMu is the attachment ownership boundary. Check the session's | 362 | + | if paintEffect != nil { |
| 361 | - | // published identity while holding it so a deadline captured before an | 363 | + | if !paintEffect.current() || ac.parkedRouteOutput.Load() { |
| 362 | - | // attach/replace cannot emit on either the old or new output chain. | |||
| 363 | - | entry.core().mu.Lock() | |||
| 364 | - | _, owned := entry.core().attachments[ac] | |||
| 365 | - | entry.core().mu.Unlock() | |||
| 366 | - | if !owned || ac.currentAttachmentSession() != entry || ac.parkedRouteOutput.Load() { | |||
| 367 | - | ac.sendMu.Unlock() | |||
| 368 | - | return paintRejected | |||
| 369 | - | } | |||
| 370 | - | if ac.parkedRouteFullPending.Load() { | |||
| 371 | - | reset = true | |||
| 372 | - | } | |||
| 373 | - | if lease != nil { | |||
| 374 | - | rc := attachmentRenderCoordinator(entry) | |||
| 375 | - | if rc == nil || lease.attachment != ac || !rc.leaseCurrent(lease, true) { | |||
| 376 | ac.sendMu.Unlock() | 364 | ac.sendMu.Unlock() | ||
| 377 | return paintRejected | 365 | return paintRejected | ||
| 378 | } | 366 | } | ||
| 367 | + | } else { | |||
| 368 | + | // Direct/headless rendering has no Attachment effect and retains its | |||
| 369 | + | // membership-only fixture path. | |||
| 370 | + | entry.core().mu.Lock() | |||
| 371 | + | _, owned := entry.core().attachments[ac] | |||
| 372 | + | entry.core().mu.Unlock() | |||
| 373 | + | if !owned || ac.currentAttachmentSession() != entry || ac.parkedRouteOutput.Load() { | |||
| 374 | + | ac.sendMu.Unlock() | |||
| 375 | + | return paintRejected | |||
| 376 | + | } | |||
| 377 | + | } | |||
| 378 | + | if ac.parkedRouteFullPending.Load() { | |||
| 379 | + | reset = true | |||
| 379 | } | 380 | } | ||
| 380 | // Capacity is checked before any destructive capture. Refresh the atomic | 381 | // Capacity is checked before any destructive capture. Refresh the atomic | ||
| 381 | // readiness snapshot while sendMu is held so direct test/setup mutations of | 382 | // readiness snapshot while sendMu is held so direct test/setup mutations of | ||
M internal/usecase/daemon/render_pipeline.go +16 -9
| @@ -424,16 +424,23 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe | |||||
| 424 | ActiveTabID: state.view.tabID, | 424 | ActiveTabID: state.view.tabID, | ||
| 425 | } | 425 | } | ||
| 426 | entry.core().mu.Unlock() | 426 | entry.core().mu.Unlock() | ||
| 427 | - | if !owned || state.attachment != ac || ac.currentAttachmentSession() != entry { | 427 | + | if marks.attachmentEffect != nil { |
| 428 | - | ac.sendMu.Unlock() | 428 | + | if !marks.attachmentEffect.current() || state.attachment != ac { |
| 429 | - | return false | 429 | + | ac.sendMu.Unlock() |
| 430 | - | } | 430 | + | return false |
| 431 | - | if state.lease != nil { | 431 | + | } |
| 432 | - | rc := attachmentRenderCoordinator(entry) | 432 | + | } else { |
| 433 | - | if rc == nil || state.lease.attachment != ac || !rc.leaseCurrent(state.lease, true) { | 433 | + | if !owned || state.attachment != ac || ac.currentAttachmentSession() != entry { |
| 434 | ac.sendMu.Unlock() | 434 | ac.sendMu.Unlock() | ||
| 435 | return false | 435 | return false | ||
| 436 | } | 436 | } | ||
| 437 | + | if state.lease != nil { | |||
| 438 | + | rc := attachmentRenderCoordinator(entry) | |||
| 439 | + | if rc == nil || state.lease.attachment != ac || !rc.leaseCurrent(state.lease, true) { | |||
| 440 | + | ac.sendMu.Unlock() | |||
| 441 | + | return false | |||
| 442 | + | } | |||
| 443 | + | } | |||
| 437 | } | 444 | } | ||
| 438 | endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0) | 445 | endDiff := marks.span(ports.RuntimeDiffStart, ports.RuntimeDiffEnd, 0) | ||
| 439 | prepared, err := ac.output.prepareFrame(d, state, composed.frame, composed.damage, composed.reset, composed.cursor) | 446 | prepared, err := ac.output.prepareFrame(d, state, composed.frame, composed.damage, composed.reset, composed.cursor) | ||
| @@ -581,8 +588,8 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe | |||||
| 581 | // Capture the exact admitted capability, including its coordinator | 588 | // Capture the exact admitted capability, including its coordinator | ||
| 582 | // lease, before End permits a new attachment publication. Reserve | 589 | // lease, before End permits a new attachment publication. Reserve | ||
| 583 | // cleanup accounting before End so terminal Wait cannot race a later Add. | 590 | // cleanup accounting before End so terminal Wait cannot race a later Add. | ||
| 584 | - | token := marks.attachmentEffect.connectionToken() | 591 | + | capability := marks.attachmentEffect.capability() |
| 585 | - | launchCleanup := d.reserveAttachmentSendErrorCleanup(token, sendTr) | 592 | + | launchCleanup := d.reserveAttachmentSendErrorCleanup(capability, sendTr) |
| 586 | marks.attachmentEffect.End() | 593 | marks.attachmentEffect.End() | ||
| 587 | launchCleanup() | 594 | launchCleanup() | ||
| 588 | } | 595 | } | ||
M internal/usecase/daemon/render_pipeline_test.go +4 -4
| @@ -686,12 +686,12 @@ func TestNoticeStylesFromMapsWarnToDedicatedRoleDistinctFromInfo(t *testing.T) { | |||||
| 686 | 686 | ||||
| 687 | func TestEmitFrameSkipsTransportSendWhenAttachmentEffectFenceRejects(t *testing.T) { | 687 | func TestEmitFrameSkipsTransportSendWhenAttachmentEffectFenceRejects(t *testing.T) { | ||
| 688 | d, sess, ac, sends := newManualSessionWithPTYs(t, nil) | 688 | d, sess, ac, sends := newManualSessionWithPTYs(t, nil) | ||
| 689 | - | token := sess.attachmentToken(ac, ac.transport()) | 689 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 690 | effect, admitted := ac.beginAttachmentEffect(token) | 690 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 691 | require.True(t, admitted) | 691 | require.True(t, admitted) | ||
| 692 | - | ac.attachmentEffects.mu.Lock() | 692 | + | ac.lifecycle.mu.Lock() |
| 693 | - | ac.attachmentEffects.capability.transport = transportSnapshot{} | 693 | + | ac.lifecycle.capability.transport = transportSnapshot{} |
| 694 | - | ac.attachmentEffects.mu.Unlock() | 694 | + | ac.lifecycle.mu.Unlock() |
| 695 | 695 | ||||
| 696 | state := cacheState("stale", 1) | 696 | state := cacheState("stale", 1) | ||
| 697 | state.attachment = ac | 697 | state.attachment = ac | ||
M internal/usecase/daemon/resize_publication.go +8 -14
| @@ -11,7 +11,7 @@ type resizeCommitPublication struct { | |||||
| 11 | attachment *attachedClient | 11 | attachment *attachedClient | ||
| 12 | lease *attachmentLease | 12 | lease *attachmentLease | ||
| 13 | epoch uint64 | 13 | epoch uint64 | ||
| 14 | - | connection attachmentConnectionToken | 14 | + | effect *attachmentEffect |
| 15 | coordinator *renderCoordinator | 15 | coordinator *renderCoordinator | ||
| 16 | observer ports.RuntimeObserver | 16 | observer ports.RuntimeObserver | ||
| 17 | mark ports.RuntimeMark | 17 | mark ports.RuntimeMark | ||
| @@ -24,19 +24,13 @@ func (p resizeCommitPublication) current() bool { | |||||
| 24 | if p.attachment == nil { | 24 | if p.attachment == nil { | ||
| 25 | return true | 25 | return true | ||
| 26 | } | 26 | } | ||
| 27 | - | // Attached resize publication is admitted only through a live attachment effect ticket. | 27 | + | // Attached resize publication retains the exact admitted effect; lifecycle |
| 28 | - | if p.connection.effect == nil { | 28 | + | // identity cannot change until that effect ends. |
| 29 | + | if !p.effect.current() || p.effect.sess != p.session || | |||
| 30 | + | p.effect.ac != p.attachment || p.effect.lease != p.lease || p.coordinator == nil { | |||
| 29 | return false | 31 | return false | ||
| 30 | } | 32 | } | ||
| 31 | - | if p.connection.effect.ended.Load() || p.connection.sess != p.session || | 33 | + | return p.coordinator.resizeCurrentForLease(p.epoch, p.attachment, p.lease, false) |
| 32 | - | p.connection.ac != p.attachment || p.connection.lease != p.lease || | |||
| 33 | - | p.connection.generation != p.attachment.connectionGeneration.Load() || | |||
| 34 | - | !p.attachment.transportSnapshotCurrent(p.connection.transport) || | |||
| 35 | - | p.attachment.currentSession() != p.session || p.coordinator == nil { | |||
| 36 | - | return false | |||
| 37 | - | } | |||
| 38 | - | registered := attachmentRegistered(p.session, p.attachment) | |||
| 39 | - | return registered && p.coordinator.resizeCurrentForLease(p.epoch, p.attachment, p.lease, false) | |||
| 40 | } | 34 | } | ||
| 41 | 35 | ||||
| 42 | // emit performs observer work only after every resize-owner fence and | 36 | // emit performs observer work only after every resize-owner fence and | ||
| @@ -57,7 +51,7 @@ func (p resizeCommitPublication) emit() bool { | |||||
| 57 | // reservation with pane ownership. sendMu is acquired before resize fences, so | 51 | // reservation with pane ownership. sendMu is acquired before resize fences, so | ||
| 58 | // the canonical rule never acquires an attachment lock while a pane fence is | 52 | // the canonical rule never acquires an attachment lock while a pane fence is | ||
| 59 | // held. Observer callbacks run only after both lock families are released. | 53 | // held. Observer callbacks run only after both lock families are released. | ||
| 60 | - | func (d *Daemon) publishResizeCommit(members []resizeMember, sess *session, ac *attachedClient, lease *attachmentLease, epoch uint64, ticket *attachmentEffectTicket, size domain.Size) bool { | 54 | + | func (d *Daemon) publishResizeCommit(members []resizeMember, sess *session, ac *attachedClient, lease *attachmentLease, epoch uint64, ticket *attachmentEffect, size domain.Size) bool { |
| 61 | if ac != nil && ticket == nil { | 55 | if ac != nil && ticket == nil { | ||
| 62 | return false | 56 | return false | ||
| 63 | } | 57 | } | ||
| @@ -85,7 +79,7 @@ func (d *Daemon) publishResizeCommit(members []resizeMember, sess *session, ac * | |||||
| 85 | mark: ports.NewRuntimeMark("daemon", ports.RuntimeResizeCommitted, 0, true), | 79 | mark: ports.NewRuntimeMark("daemon", ports.RuntimeResizeCommitted, 0, true), | ||
| 86 | } | 80 | } | ||
| 87 | if ticket != nil { | 81 | if ticket != nil { | ||
| 88 | - | publication.connection = ticket.connectionToken() | 82 | + | publication.effect = ticket |
| 89 | publication.coordinator = sess.renderCoordinator() | 83 | publication.coordinator = sess.renderCoordinator() | ||
| 90 | } | 84 | } | ||
| 91 | if !publication.current() { | 85 | if !publication.current() { | ||
M internal/usecase/daemon/resume.go +18 -3
| @@ -362,16 +362,32 @@ func (d *Daemon) abortResumeClaim(ac *attachedClient) bool { | |||||
| 362 | d.mu.Unlock() | 362 | d.mu.Unlock() | ||
| 363 | return false | 363 | return false | ||
| 364 | } | 364 | } | ||
| 365 | + | d.mu.Unlock() | |||
| 366 | + | ||||
| 367 | + | // A successful claim may already have published active membership. Freeze | |||
| 368 | + | // and drain without architecture locks before restoring parked identity. | |||
| 369 | + | frozen := freezeAttachmentEffectGates(ac) | |||
| 370 | + | defer frozen.unfreeze() | |||
| 371 | + | ||||
| 372 | + | d.mu.Lock() | |||
| 373 | + | if d.parked[token] != parked || !parked.claimed || ac.resumeClaimToken != token { | |||
| 374 | + | d.mu.Unlock() | |||
| 375 | + | return false | |||
| 376 | + | } | |||
| 365 | parked.claimed = false | 377 | parked.claimed = false | ||
| 366 | ac.resumeClaimToken = 0 | 378 | ac.resumeClaimToken = 0 | ||
| 367 | ac.resumeToken = token | 379 | ac.resumeToken = token | ||
| 368 | ac.parked = true | 380 | ac.parked = true | ||
| 369 | - | ac.connectionGeneration.Add(1) | |||
| 370 | sess := parked.sess | 381 | sess := parked.sess | ||
| 371 | if sess != nil { | 382 | if sess != nil { | ||
| 372 | sess.mu.Lock() | 383 | sess.mu.Lock() | ||
| 373 | sess.unregisterAttachmentLocked(ac) | 384 | sess.unregisterAttachmentLocked(ac) | ||
| 385 | + | ac.setSession(nil) | |||
| 386 | + | ac.invalidateFrozenAttachmentCapability() | |||
| 374 | sess.mu.Unlock() | 387 | sess.mu.Unlock() | ||
| 388 | + | } else { | |||
| 389 | + | ac.setSession(nil) | |||
| 390 | + | ac.invalidateFrozenAttachmentCapability() | |||
| 375 | } | 391 | } | ||
| 376 | // Recreate the parked entry and its watcher instead of restoring a timer | 392 | // Recreate the parked entry and its watcher instead of restoring a timer | ||
| 377 | // that may already have fired while the claim held the old entry. This also | 393 | // that may already have fired while the claim held the old entry. This also | ||
| @@ -390,7 +406,6 @@ func (d *Daemon) abortResumeClaim(ac *attachedClient) bool { | |||||
| 390 | } | 406 | } | ||
| 391 | d.parked[token] = rearmed | 407 | d.parked[token] = rearmed | ||
| 392 | captured := ac.transportSnapshot().transport | 408 | captured := ac.transportSnapshot().transport | ||
| 393 | - | ac.setSession(nil) | |||
| 394 | d.mu.Unlock() | 409 | d.mu.Unlock() | ||
| 395 | if sess != nil { | 410 | if sess != nil { | ||
| 396 | d.recalculateSessionGeometryAndInvalidate(sess, nil, "resume.go") | 411 | d.recalculateSessionGeometryAndInvalidate(sess, nil, "resume.go") | ||
| @@ -414,8 +429,8 @@ func (d *Daemon) retireParkedAttachmentLocked(token uint64, parked *parkedAttach | |||||
| 414 | d.discardAttachmentOutputLocked(parked.ac) | 429 | d.discardAttachmentOutputLocked(parked.ac) | ||
| 415 | parked.ac.resumeToken = 0 | 430 | parked.ac.resumeToken = 0 | ||
| 416 | parked.ac.parked = false | 431 | parked.ac.parked = false | ||
| 417 | - | parked.ac.connectionGeneration.Add(1) | |||
| 418 | parked.ac.setSession(nil) | 432 | parked.ac.setSession(nil) | ||
| 433 | + | parked.ac.invalidateRetiredAttachmentCapability() | |||
| 419 | return parkedAttachmentRetirement{ | 434 | return parkedAttachmentRetirement{ | ||
| 420 | parked: parked, | 435 | parked: parked, | ||
| 421 | pickerGeneration: parked.pickerGeneration, | 436 | pickerGeneration: parked.pickerGeneration, | ||
M internal/usecase/daemon/resume_test.go +2 -2
| @@ -1150,7 +1150,7 @@ func TestResumeCloseCapturedOldTransportDoesNotCloseReboundTransport(t *testing. | |||||
| 1150 | token := ac.resumeToken | 1150 | token := ac.resumeToken | ||
| 1151 | require.True(t, sess.detachIfCurrent(ac)) | 1151 | require.True(t, sess.detachIfCurrent(ac)) | ||
| 1152 | require.True(t, d.parkAttachment(sess, ac)) | 1152 | require.True(t, d.parkAttachment(sess, ac)) | ||
| 1153 | - | generation := ac.connectionGeneration.Load() | 1153 | + | generation := ac.lifecycle.generationValue() |
| 1154 | 1154 | ||||
| 1155 | newTr := &closeTrackingTransport{} | 1155 | newTr := &closeTrackingTransport{} | ||
| 1156 | resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), newTr, domain.Size{Cols: 80, Rows: 24}) | 1156 | resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), newTr, domain.Size{Cols: 80, Rows: 24}) | ||
| @@ -1158,7 +1158,7 @@ func TestResumeCloseCapturedOldTransportDoesNotCloseReboundTransport(t *testing. | |||||
| 1158 | require.True(t, ok) | 1158 | require.True(t, ok) | ||
| 1159 | require.Same(t, sess, resumedSess) | 1159 | require.Same(t, sess, resumedSess) | ||
| 1160 | require.Same(t, ac, resumedAC) | 1160 | require.Same(t, ac, resumedAC) | ||
| 1161 | - | require.Greater(t, ac.connectionGeneration.Load(), generation, "resume must publish active ownership through the attachment transition") | 1161 | + | require.Greater(t, ac.lifecycle.generationValue(), generation, "resume must publish active ownership through the attachment transition") |
| 1162 | 1162 | ||||
| 1163 | _ = ac.closeCapturedTransport(oldTr) | 1163 | _ = ac.closeCapturedTransport(oldTr) | ||
| 1164 | require.True(t, oldTr.Closed(), "old transport is closed") | 1164 | require.True(t, oldTr.Closed(), "old transport is closed") | ||
M internal/usecase/daemon/resume_wake_incarnation_test.go +2 -2
| @@ -88,7 +88,7 @@ func TestSameAttachmentRebindRejectsQueuedWakeAndFansOutFreshGeneration(t *testi | |||||
| 88 | }() | 88 | }() | ||
| 89 | <-wakeDispatched | 89 | <-wakeDispatched | ||
| 90 | 90 | ||||
| 91 | - | oldGeneration := ac.connectionGeneration.Load() | 91 | + | oldGeneration := ac.lifecycle.generationValue() |
| 92 | result, err := d.transitionAttachment(attachmentTransitionRequest{ | 92 | result, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 93 | source: sess, target: sess, next: ac, | 93 | source: sess, target: sess, next: ac, | ||
| 94 | 94 | ||||
| @@ -96,7 +96,7 @@ func TestSameAttachmentRebindRejectsQueuedWakeAndFansOutFreshGeneration(t *testi | |||||
| 96 | }) | 96 | }) | ||
| 97 | require.NoError(t, err) | 97 | require.NoError(t, err) | ||
| 98 | d.deferAttachmentTransitionCleanups(result) | 98 | d.deferAttachmentTransitionCleanups(result) | ||
| 99 | - | require.Greater(t, ac.connectionGeneration.Load(), oldGeneration) | 99 | + | require.Greater(t, ac.lifecycle.generationValue(), oldGeneration) |
| 100 | freshLease := rc.attachmentLease(ac) | 100 | freshLease := rc.attachmentLease(ac) | ||
| 101 | require.NotSame(t, oldLease, freshLease) | 101 | require.NotSame(t, oldLease, freshLease) | ||
| 102 | 102 | ||||
M internal/usecase/daemon/route_position_test.go +1 -1
| @@ -89,7 +89,7 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) { | |||||
| 89 | require.True(t, rc.markAttachmentReady(rc.attachmentLease(ac))) | 89 | require.True(t, rc.markAttachmentReady(rc.attachmentLease(ac))) | ||
| 90 | sess.repairAttachmentView(ac) | 90 | sess.repairAttachmentView(ac) | ||
| 91 | 91 | ||||
| 92 | - | require.True(t, d.firstPaintForTransition(sess.attachmentToken(ac, tr))) | 92 | + | require.True(t, d.firstPaintForTransition(sess.captureAttachmentCapability(ac, tr))) |
| 93 | sess.selectAttachmentTab(ac, "tab-2") | 93 | sess.selectAttachmentTab(ac, "tab-2") | ||
| 94 | d.paint(sess, ac, false, nil) | 94 | d.paint(sess, ac, false, nil) | ||
| 95 | 95 | ||||
M internal/usecase/daemon/same_peer_switch.go +28 -17
| @@ -33,27 +33,29 @@ func (ac *attachedClient) consumeSamePeerOffer(target ports.ExactSessionTarget) | |||||
| 33 | // switchSamePeerForAttachment commits one client-confirmed endpoint-empty | 33 | // switchSamePeerForAttachment commits one client-confirmed endpoint-empty | ||
| 34 | // target. The request never names an endpoint: its authority is the current | 34 | // target. The request never names an endpoint: its authority is the current | ||
| 35 | // authenticated attachment plus the target's exact lifecycle identity. | 35 | // authenticated attachment plus the target's exact lifecycle identity. | ||
| 36 | - | func (d *Daemon) switchSamePeerForAttachment(token attachmentConnectionToken, request ports.SamePeerSwitchRequest) { | 36 | + | func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request ports.SamePeerSwitchRequest) { |
| 37 | - | if err := request.Validate(); err != nil || token.sess == nil || token.ac == nil { | 37 | + | if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil { |
| 38 | return | 38 | return | ||
| 39 | } | 39 | } | ||
| 40 | - | if !token.ac.consumeSamePeerOffer(request.Target) { | 40 | + | if !effect.ac.consumeSamePeerOffer(request.Target) { |
| 41 | - | d.sendSamePeerSwitchFailure(token, request.RequestID, ports.SamePeerSwitchStaleTarget) | 41 | + | d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget) |
| 42 | return | 42 | return | ||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | target, targetTabIndex, ok := d.samePeerTarget(request) | 45 | target, targetTabIndex, ok := d.samePeerTarget(request) | ||
| 46 | - | if !ok || target == token.sess { | 46 | + | if !ok || target == effect.sess { |
| 47 | - | d.sendSamePeerSwitchFailure(token, request.RequestID, ports.SamePeerSwitchStaleTarget) | 47 | + | d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget) |
| 48 | return | 48 | return | ||
| 49 | } | 49 | } | ||
| 50 | 50 | ||||
| 51 | + | capability := effect.capability() | |||
| 51 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 52 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 52 | - | source: token.sess, | 53 | + | source: effect.sess, |
| 53 | target: target, | 54 | target: target, | ||
| 54 | - | next: token.ac, | 55 | + | next: effect.ac, |
| 55 | - | expectedTransport: token.transport, | 56 | + | expectedTransport: effect.transport, |
| 56 | - | sourceToken: &token, | 57 | + | sourceCapability: &capability, |
| 58 | + | sourceEffect: effect, | |||
| 57 | action: "same-peer-switch", | 59 | action: "same-peer-switch", | ||
| 58 | expectedTargetLifecycle: &attachmentLifecycleFence{ | 60 | expectedTargetLifecycle: &attachmentLifecycleFence{ | ||
| 59 | name: request.Target.SessionName, | 61 | name: request.Target.SessionName, | ||
| @@ -65,12 +67,12 @@ func (d *Daemon) switchSamePeerForAttachment(token attachmentConnectionToken, re | |||||
| 65 | ready: true, | 67 | ready: true, | ||
| 66 | }) | 68 | }) | ||
| 67 | if err != nil { | 69 | if err != nil { | ||
| 68 | - | d.sendSamePeerSwitchFailure(token, request.RequestID, ports.SamePeerSwitchStaleTarget) | 70 | + | d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget) |
| 69 | return | 71 | return | ||
| 70 | } | 72 | } | ||
| 71 | 73 | ||||
| 72 | - | if fresh, admitted := token.ac.beginAttachmentEffect(transition.published); admitted { | 74 | + | if fresh, admitted := effect.ac.beginAttachmentEffect(transition.published); admitted { |
| 73 | - | d.closePicker(token.ac) | 75 | + | d.closePicker(effect.ac) |
| 74 | fresh.End() | 76 | fresh.End() | ||
| 75 | } | 77 | } | ||
| 76 | d.touchMRU(target) | 78 | d.touchMRU(target) | ||
| @@ -106,15 +108,24 @@ func (d *Daemon) samePeerTarget(request ports.SamePeerSwitchRequest) (*session, | |||||
| 106 | return target, tabIndex, true | 108 | return target, tabIndex, true | ||
| 107 | } | 109 | } | ||
| 108 | 110 | ||||
| 109 | - | func (d *Daemon) sendSamePeerSwitchFailure(token attachmentConnectionToken, requestID uint64, code ports.SamePeerSwitchFailureCode) { | 111 | + | func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID uint64, code ports.SamePeerSwitchFailureCode) { |
| 110 | - | if !token.attachmentCurrent() { | 112 | + | if effect == nil || effect.ac == nil { |
| 111 | return | 113 | return | ||
| 112 | } | 114 | } | ||
| 115 | + | sender := effect | |||
| 116 | + | if !sender.current() { | |||
| 117 | + | var admitted bool | |||
| 118 | + | sender, admitted = effect.ac.beginAttachmentEffect(effect.capability()) | |||
| 119 | + | if !admitted { | |||
| 120 | + | return | |||
| 121 | + | } | |||
| 122 | + | defer sender.End() | |||
| 123 | + | } | |||
| 113 | payload, err := ports.MarshalSamePeerSwitchFailure(ports.SamePeerSwitchFailure{RequestID: requestID, Code: code}) | 124 | payload, err := ports.MarshalSamePeerSwitchFailure(ports.SamePeerSwitchFailure{RequestID: requestID, Code: code}) | ||
| 114 | if err != nil { | 125 | if err != nil { | ||
| 115 | return | 126 | return | ||
| 116 | } | 127 | } | ||
| 117 | - | if err := token.sendControl(ports.Frame{Type: ports.MsgSamePeerSwitchFailure, Payload: payload}); err != nil { | 128 | + | if err := sender.sendControl(ports.Frame{Type: ports.MsgSamePeerSwitchFailure, Payload: payload}); err != nil { |
| 118 | - | d.detachOnAttachmentSendError(token, token.transport.transport) | 129 | + | d.detachOnAttachmentSendError(sender.capability(), sender.transport.transport) |
| 119 | } | 130 | } | ||
| 120 | } | 131 | } | ||
M internal/usecase/daemon/same_peer_switch_test.go +4 -6
| @@ -70,15 +70,14 @@ func TestSamePeerSwitchTransitionsExactTargetAndPreferredTab(t *testing.T) { | |||||
| 70 | d.mu.Unlock() | 70 | d.mu.Unlock() | ||
| 71 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | 71 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | ||
| 72 | 72 | ||||
| 73 | - | token := source.attachmentToken(ac, ac.transport()) | 73 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 74 | effect, admitted := ac.beginAttachmentEffect(token) | 74 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 75 | require.True(t, admitted) | 75 | require.True(t, admitted) | ||
| 76 | - | token.effect = effect | |||
| 77 | defer effect.End() | 76 | defer effect.End() | ||
| 78 | requestTarget := ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"} | 77 | requestTarget := ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"} | ||
| 79 | ac.offerSamePeerTarget(requestTarget) | 78 | ac.offerSamePeerTarget(requestTarget) | ||
| 80 | 79 | ||||
| 81 | - | d.switchSamePeerForAttachment(token, ports.SamePeerSwitchRequest{ | 80 | + | d.switchSamePeerForAttachment(effect, ports.SamePeerSwitchRequest{ |
| 82 | RequestID: 1, | 81 | RequestID: 1, | ||
| 83 | Target: requestTarget, | 82 | Target: requestTarget, | ||
| 84 | PreferredTabID: domain.TabStableID(target.tabs[1].stableID), | 83 | PreferredTabID: domain.TabStableID(target.tabs[1].stableID), | ||
| @@ -109,13 +108,12 @@ func TestSamePeerSwitchRejectsStaleTargetWithoutMutation(t *testing.T) { | |||||
| 109 | defer releaseAll(releases) | 108 | defer releaseAll(releases) | ||
| 110 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | 109 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | ||
| 111 | 110 | ||||
| 112 | - | token := source.attachmentToken(ac, ac.transport()) | 111 | + | token := source.captureAttachmentCapability(ac, ac.transport()) |
| 113 | effect, admitted := ac.beginAttachmentEffect(token) | 112 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 114 | require.True(t, admitted) | 113 | require.True(t, admitted) | ||
| 115 | - | token.effect = effect | |||
| 116 | defer effect.End() | 114 | defer effect.End() | ||
| 117 | 115 | ||||
| 118 | - | d.switchSamePeerForAttachment(token, ports.SamePeerSwitchRequest{ | 116 | + | d.switchSamePeerForAttachment(effect, ports.SamePeerSwitchRequest{ |
| 119 | RequestID: 1, | 117 | RequestID: 1, | ||
| 120 | Target: ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{9}, SessionName: "missing"}, | 118 | Target: ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{9}, SessionName: "missing"}, | ||
| 121 | }) | 119 | }) | ||
M internal/usecase/daemon/session.go +45 -59
| @@ -511,27 +511,28 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name | |||||
| 511 | 511 | ||||
| 512 | // createSessionAndSwitchForAttachment creates no target until the initiating role | 512 | // createSessionAndSwitchForAttachment creates no target until the initiating role | ||
| 513 | // ticket has ended and the exact source capability is frozen and revalidated. | 513 | // ticket has ended and the exact source capability is frozen and revalidated. | ||
| 514 | - | func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionToken, name string) error { | 514 | + | func (d *Daemon) createSessionAndSwitchForAttachment(effect *attachmentEffect, name string) error { |
| 515 | if name == "" { | 515 | if name == "" { | ||
| 516 | return errSessionNameRequired | 516 | return errSessionNameRequired | ||
| 517 | } | 517 | } | ||
| 518 | if err := domain.ValidateSessionName(name); err != nil { | 518 | if err := domain.ValidateSessionName(name); err != nil { | ||
| 519 | return err | 519 | return err | ||
| 520 | } | 520 | } | ||
| 521 | - | if token.sess == nil || token.ac == nil || token.effect == nil { | 521 | + | if effect == nil || effect.sess == nil || effect.ac == nil { |
| 522 | return errAttachmentTransition | 522 | return errAttachmentTransition | ||
| 523 | } | 523 | } | ||
| 524 | - | token.effect.bindActionEnd(d, "create-session") | 524 | + | effect.bindActionEnd(d, "create-session") |
| 525 | - | token.effect.End() | 525 | + | effect.End() |
| 526 | + | capability := effect.capability() | |||
| 526 | 527 | ||||
| 527 | var created *session | 528 | var created *session | ||
| 528 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 529 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 529 | - | source: token.sess, next: token.ac, | 530 | + | source: capability.sess, next: capability.ac, |
| 530 | 531 | ||||
| 531 | - | expectedTransport: token.transport, sourceToken: &token, action: "create-session", | 532 | + | expectedTransport: capability.transport, sourceCapability: &capability, sourceEffect: effect, action: "create-session", |
| 532 | copySourceEnvironment: true, ready: true, | 533 | copySourceEnvironment: true, ready: true, | ||
| 533 | createTargetLocked: func() (*session, error) { | 534 | createTargetLocked: func() (*session, error) { | ||
| 534 | - | source := token.sess | 535 | + | source := capability.sess |
| 535 | if source == nil { | 536 | if source == nil { | ||
| 536 | return nil, errAttachmentTransition | 537 | return nil, errAttachmentTransition | ||
| 537 | } | 538 | } | ||
| @@ -545,7 +546,7 @@ func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionT | |||||
| 545 | cwd, env := source.cwd, copyEnvironment(source.env) | 546 | cwd, env := source.cwd, copyEnvironment(source.env) | ||
| 546 | source.mu.Unlock() | 547 | source.mu.Unlock() | ||
| 547 | var createErr error | 548 | var createErr error | ||
| 548 | - | created, createErr = d.createSessionLockedWithMode(name, false, cwd, token.ac.geometrySnapshot(), env) | 549 | + | created, createErr = d.createSessionLockedWithMode(name, false, cwd, capability.ac.geometrySnapshot(), env) |
| 549 | return created, createErr | 550 | return created, createErr | ||
| 550 | }, | 551 | }, | ||
| 551 | }) | 552 | }) | ||
| @@ -557,7 +558,7 @@ func (d *Daemon) createSessionAndSwitchForAttachment(token attachmentConnectionT | |||||
| 557 | } | 558 | } | ||
| 558 | 559 | ||||
| 559 | d.touchMRU(created) | 560 | d.touchMRU(created) | ||
| 560 | - | d.log.Info("client attached", "session", created.name, "resume", token.ac.resumeCapable) | 561 | + | d.log.Info("client attached", "session", created.name, "resume", capability.ac.resumeCapable) |
| 561 | d.deferAttachmentTransitionCleanups(transition) | 562 | d.deferAttachmentTransitionCleanups(transition) | ||
| 562 | d.firstPaintForTransition(transition.published) | 563 | d.firstPaintForTransition(transition.published) | ||
| 563 | return nil | 564 | return nil | ||
| @@ -599,29 +600,30 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie | |||||
| 599 | return nil | 600 | return nil | ||
| 600 | } | 601 | } | ||
| 601 | 602 | ||||
| 602 | - | func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(token attachmentConnectionToken) error { | 603 | + | func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(effect *attachmentEffect) error { |
| 603 | - | if token.sess == nil || token.ac == nil || token.effect == nil { | 604 | + | if effect == nil || effect.sess == nil || effect.ac == nil { |
| 604 | return errAttachmentTransition | 605 | return errAttachmentTransition | ||
| 605 | } | 606 | } | ||
| 606 | - | token.effect.bindActionEnd(d, "create-ephemeral-session") | 607 | + | effect.bindActionEnd(d, "create-ephemeral-session") |
| 607 | - | token.effect.End() | 608 | + | effect.End() |
| 609 | + | capability := effect.capability() | |||
| 608 | 610 | ||||
| 609 | var created *session | 611 | var created *session | ||
| 610 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | 612 | transition, err := d.transitionAttachment(attachmentTransitionRequest{ | ||
| 611 | - | source: token.sess, next: token.ac, | 613 | + | source: capability.sess, next: capability.ac, |
| 612 | - | expectedTransport: token.transport, sourceToken: &token, action: "create-ephemeral-session", | 614 | + | expectedTransport: capability.transport, sourceCapability: &capability, sourceEffect: effect, action: "create-ephemeral-session", |
| 613 | copySourceEnvironment: true, ready: true, | 615 | copySourceEnvironment: true, ready: true, | ||
| 614 | createTargetLocked: func() (*session, error) { | 616 | createTargetLocked: func() (*session, error) { | ||
| 615 | - | if token.sess == nil || d.closing { | 617 | + | if capability.sess == nil || d.closing { |
| 616 | return nil, errAttachmentTransition | 618 | return nil, errAttachmentTransition | ||
| 617 | } | 619 | } | ||
| 618 | - | source := token.sess | 620 | + | source := capability.sess |
| 619 | source.mu.Lock() | 621 | source.mu.Lock() | ||
| 620 | cwd, env := source.cwd, copyEnvironment(source.env) | 622 | cwd, env := source.cwd, copyEnvironment(source.env) | ||
| 621 | source.mu.Unlock() | 623 | source.mu.Unlock() | ||
| 622 | name := d.allocEphemeralNameLocked() | 624 | name := d.allocEphemeralNameLocked() | ||
| 623 | var createErr error | 625 | var createErr error | ||
| 624 | - | created, createErr = d.createSessionLockedWithMode(name, true, cwd, token.ac.geometrySnapshot(), env) | 626 | + | created, createErr = d.createSessionLockedWithMode(name, true, cwd, capability.ac.geometrySnapshot(), env) |
| 625 | return created, createErr | 627 | return created, createErr | ||
| 626 | }, | 628 | }, | ||
| 627 | }) | 629 | }) | ||
| @@ -632,7 +634,7 @@ func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(token attachmentCo | |||||
| 632 | return err | 634 | return err | ||
| 633 | } | 635 | } | ||
| 634 | d.touchMRU(created) | 636 | d.touchMRU(created) | ||
| 635 | - | d.log.Info("client attached", "session", created.name, "resume", token.ac.resumeCapable) | 637 | + | d.log.Info("client attached", "session", created.name, "resume", capability.ac.resumeCapable) |
| 636 | d.deferAttachmentTransitionCleanups(transition) | 638 | d.deferAttachmentTransitionCleanups(transition) | ||
| 637 | d.firstPaintForTransition(transition.published) | 639 | d.firstPaintForTransition(transition.published) | ||
| 638 | return nil | 640 | return nil | ||
| @@ -855,17 +857,6 @@ func (d *Daemon) startPaneGoroutines(sess *session, tb *tab, p *pane) { | |||||
| 855 | go d.readPanePTY(p) | 857 | go d.readPanePTY(p) | ||
| 856 | } | 858 | } | ||
| 857 | 859 | ||||
| 858 | - | func (s *session) detachIfCurrent(ac *attachedClient) bool { | |||
| 859 | - | s.mu.Lock() | |||
| 860 | - | defer s.mu.Unlock() | |||
| 861 | - | if !attachmentRegisteredLocked(s, ac) || ac.currentAttachmentSession() != s { | |||
| 862 | - | return false | |||
| 863 | - | } | |||
| 864 | - | s.unregisterAttachmentLocked(ac) | |||
| 865 | - | ac.setSession(nil) | |||
| 866 | - | return true | |||
| 867 | - | } | |||
| 868 | - | ||||
| 869 | // detachIfCurrent publishes terminal attachment invalidation through the attachment | 860 | // detachIfCurrent publishes terminal attachment invalidation through the attachment | ||
| 870 | // gate. Freeze/drain happens before routing or session locks are acquired. | 861 | // gate. Freeze/drain happens before routing or session locks are acquired. | ||
| 871 | func (d *Daemon) detachIfCurrent(sess *session, ac *attachedClient) bool { | 862 | func (d *Daemon) detachIfCurrent(sess *session, ac *attachedClient) bool { | ||
| @@ -892,7 +883,6 @@ func (d *Daemon) detachIfCurrentTransport(sess *session, ac *attachedClient, exp | |||||
| 892 | } | 883 | } | ||
| 893 | if current { | 884 | if current { | ||
| 894 | sess.unregisterAttachmentLocked(ac) | 885 | sess.unregisterAttachmentLocked(ac) | ||
| 895 | - | ac.connectionGeneration.Add(1) | |||
| 896 | ac.setSession(nil) | 886 | ac.setSession(nil) | ||
| 897 | ac.invalidateFrozenAttachmentCapability() | 887 | ac.invalidateFrozenAttachmentCapability() | ||
| 898 | } | 888 | } | ||
| @@ -904,11 +894,11 @@ func (d *Daemon) detachIfCurrentTransport(sess *session, ac *attachedClient, exp | |||||
| 904 | // detachIfAttachmentCurrent invalidates only the exact attachment generation and transport | 894 | // detachIfAttachmentCurrent invalidates only the exact attachment generation and transport | ||
| 905 | // whose admitted send failed. A queued stale error cannot detach a later | 895 | // whose admitted send failed. A queued stale error cannot detach a later | ||
| 906 | // attachment publication or a rebound transport on the same attachment object. | 896 | // attachment publication or a rebound transport on the same attachment object. | ||
| 907 | - | func (d *Daemon) detachIfAttachmentCurrent(token attachmentConnectionToken) bool { | 897 | + | func (d *Daemon) detachIfAttachmentCurrent(token attachmentCapability) bool { |
| 908 | return d.detachIfAttachmentCurrentUntil(token, nil) | 898 | return d.detachIfAttachmentCurrentUntil(token, nil) | ||
| 909 | } | 899 | } | ||
| 910 | 900 | ||||
| 911 | - | func (d *Daemon) detachIfAttachmentCurrentUntil(token attachmentConnectionToken, done func() <-chan struct{}) bool { | 901 | + | func (d *Daemon) detachIfAttachmentCurrentUntil(token attachmentCapability, done func() <-chan struct{}) bool { |
| 912 | if token.sess == nil || token.ac == nil || token.transport.transport == nil { | 902 | if token.sess == nil || token.ac == nil || token.transport.transport == nil { | ||
| 913 | return false | 903 | return false | ||
| 914 | } | 904 | } | ||
| @@ -938,13 +928,9 @@ func (d *Daemon) detachIfAttachmentCurrentUntil(token attachmentConnectionToken, | |||||
| 938 | if coordinator != nil { | 928 | if coordinator != nil { | ||
| 939 | coordinator.mu.Lock() | 929 | coordinator.mu.Lock() | ||
| 940 | } | 930 | } | ||
| 941 | - | req := attachmentTransitionRequest{ | 931 | + | current := coordinator != nil && token.currentInSessionAndLeaseLocked(token.sess, token.ac, coordinator) |
| 942 | - | source: token.sess, next: token.ac, expectedTransport: token.transport, | |||
| 943 | - | } | |||
| 944 | - | current := coordinator != nil && transitionSourceTokenCurrentLocked(token, token.sess, coordinator, req) | |||
| 945 | if current { | 932 | if current { | ||
| 946 | unregisterAttachmentSessionLocked(token.sess, token.ac) | 933 | unregisterAttachmentSessionLocked(token.sess, token.ac) | ||
| 947 | - | token.ac.connectionGeneration.Add(1) | |||
| 948 | token.ac.setSession(nil) | 934 | token.ac.setSession(nil) | ||
| 949 | token.ac.invalidateFrozenAttachmentCapability() | 935 | token.ac.invalidateFrozenAttachmentCapability() | ||
| 950 | } | 936 | } | ||
| @@ -1059,7 +1045,7 @@ func (d *Daemon) publishRenamedSessionRouteIdentity(sess *session) { | |||||
| 1059 | if ac.deferRouteIdentityUntilSnapshot() { | 1045 | if ac.deferRouteIdentityUntilSnapshot() { | ||
| 1060 | continue | 1046 | continue | ||
| 1061 | } | 1047 | } | ||
| 1062 | - | token := sess.attachmentToken(ac, ac.transport()) | 1048 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 1063 | if token.ac == nil { | 1049 | if token.ac == nil { | ||
| 1064 | continue | 1050 | continue | ||
| 1065 | } | 1051 | } | ||
| @@ -1067,11 +1053,11 @@ func (d *Daemon) publishRenamedSessionRouteIdentity(sess *session) { | |||||
| 1067 | if !admitted { | 1053 | if !admitted { | ||
| 1068 | continue | 1054 | continue | ||
| 1069 | } | 1055 | } | ||
| 1070 | - | token = effect.connectionToken() | 1056 | + | err := d.sendCommittedRouteIdentityForAttachment(effect) |
| 1071 | - | err := d.sendCommittedRouteIdentityForAttachment(token) | 1057 | + | capability := effect.capability() |
| 1072 | effect.End() | 1058 | effect.End() | ||
| 1073 | if err != nil { | 1059 | if err != nil { | ||
| 1074 | - | d.detachOnAttachmentSendError(token, token.transport.transport) | 1060 | + | d.detachOnAttachmentSendError(capability, capability.transport.transport) |
| 1075 | } | 1061 | } | ||
| 1076 | } | 1062 | } | ||
| 1077 | } | 1063 | } | ||
| @@ -1149,7 +1135,7 @@ func (d *Daemon) closeTabLocked(sess *session, tb *tab, repaint bool) error { | |||||
| 1149 | return d.closeTabLockedWithEffect(sess, tb, repaint, nil) | 1135 | return d.closeTabLockedWithEffect(sess, tb, repaint, nil) | ||
| 1150 | } | 1136 | } | ||
| 1151 | 1137 | ||||
| 1152 | - | func (d *Daemon) closeTabLockedWithEffect(sess *session, tb *tab, repaint bool, effect *attachmentEffectTicket) error { | 1138 | + | func (d *Daemon) closeTabLockedWithEffect(sess *session, tb *tab, repaint bool, effect *attachmentEffect) error { |
| 1153 | if sess == nil || tb == nil { | 1139 | if sess == nil || tb == nil { | ||
| 1154 | return layout.ErrNotFound | 1140 | return layout.ErrNotFound | ||
| 1155 | } | 1141 | } | ||
| @@ -1178,7 +1164,7 @@ func (d *Daemon) closeTabLockedWithEffect(sess *session, tb *tab, repaint bool, | |||||
| 1178 | var err error | 1164 | var err error | ||
| 1179 | if effect != nil { | 1165 | if effect != nil { | ||
| 1180 | effect.bindActionEnd(d, "close-tab") | 1166 | effect.bindActionEnd(d, "close-tab") | ||
| 1181 | - | err = d.killSessionForAttachment(sess, ports.ReasonSessionKilled, false, effect.connectionToken(), "close-tab") | 1167 | + | err = d.killSessionForAttachment(sess, ports.ReasonSessionKilled, false, effect, "close-tab") |
| 1182 | } else { | 1168 | } else { | ||
| 1183 | err = d.killSession(sess, ports.ReasonSessionKilled, false) | 1169 | err = d.killSession(sess, ports.ReasonSessionKilled, false) | ||
| 1184 | } | 1170 | } | ||
| @@ -1305,8 +1291,9 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||||
| 1305 | } | 1291 | } | ||
| 1306 | 1292 | ||||
| 1307 | type sessionKillAdmission struct { | 1293 | type sessionKillAdmission struct { | ||
| 1308 | - | token attachmentConnectionToken | 1294 | + | capability attachmentCapability |
| 1309 | - | action string | 1295 | + | effect *attachmentEffect |
| 1296 | + | action string | |||
| 1310 | } | 1297 | } | ||
| 1311 | 1298 | ||||
| 1312 | type sessionKillParticipants struct { | 1299 | type sessionKillParticipants struct { | ||
| @@ -1315,12 +1302,15 @@ type sessionKillParticipants struct { | |||||
| 1315 | effectGates []*attachedClient | 1302 | effectGates []*attachedClient | ||
| 1316 | interrupts []attachmentTransportInterrupt | 1303 | interrupts []attachmentTransportInterrupt | ||
| 1317 | source *session | 1304 | source *session | ||
| 1318 | - | sourceToken *attachmentConnectionToken | 1305 | + | sourceToken *attachmentCapability |
| 1319 | target *session | 1306 | target *session | ||
| 1320 | } | 1307 | } | ||
| 1321 | 1308 | ||||
| 1322 | - | func (d *Daemon) killSessionForAttachment(sess *session, reason uint8, purge bool, token attachmentConnectionToken, action string) error { | 1309 | + | func (d *Daemon) killSessionForAttachment(sess *session, reason uint8, purge bool, effect *attachmentEffect, action string) error { |
| 1323 | - | return d.killSessionWithSnapshotDeadline(sess, reason, purge, nil, &sessionKillAdmission{token: token, action: action}) | 1310 | + | if effect == nil { |
| 1311 | + | return errAttachmentTransition | |||
| 1312 | + | } | |||
| 1313 | + | return d.killSessionWithSnapshotDeadline(sess, reason, purge, nil, &sessionKillAdmission{capability: effect.capability(), effect: effect, action: action}) | |||
| 1324 | } | 1314 | } | ||
| 1325 | 1315 | ||||
| 1326 | // snapshotSessionKillParticipants captures attachment membership under the canonical | 1316 | // snapshotSessionKillParticipants captures attachment membership under the canonical | ||
| @@ -1330,7 +1320,7 @@ func (d *Daemon) killSessionForAttachment(sess *session, reason uint8, purge boo | |||||
| 1330 | func (d *Daemon) snapshotSessionKillParticipants(target *session, admission *sessionKillAdmission, requireEmpty bool) (sessionKillParticipants, bool) { | 1320 | func (d *Daemon) snapshotSessionKillParticipants(target *session, admission *sessionKillAdmission, requireEmpty bool) (sessionKillParticipants, bool) { | ||
| 1331 | snapshot := sessionKillParticipants{target: target} | 1321 | snapshot := sessionKillParticipants{target: target} | ||
| 1332 | if admission != nil { | 1322 | if admission != nil { | ||
| 1333 | - | token := admission.token | 1323 | + | token := admission.capability |
| 1334 | if token.sess == nil || token.ac == nil || token.transport.transport == nil { | 1324 | if token.sess == nil || token.ac == nil || token.transport.transport == nil { | ||
| 1335 | return sessionKillParticipants{}, false | 1325 | return sessionKillParticipants{}, false | ||
| 1336 | } | 1326 | } | ||
| @@ -1338,7 +1328,7 @@ func (d *Daemon) snapshotSessionKillParticipants(target *session, admission *ses | |||||
| 1338 | if snapshot.source == nil { | 1328 | if snapshot.source == nil { | ||
| 1339 | return sessionKillParticipants{}, false | 1329 | return sessionKillParticipants{}, false | ||
| 1340 | } | 1330 | } | ||
| 1341 | - | snapshot.sourceToken = &admission.token | 1331 | + | snapshot.sourceToken = &admission.capability |
| 1342 | } | 1332 | } | ||
| 1343 | 1333 | ||||
| 1344 | d.mu.Lock() | 1334 | d.mu.Lock() | ||
| @@ -1381,7 +1371,7 @@ func (d *Daemon) snapshotSessionKillParticipants(target *session, admission *ses | |||||
| 1381 | snapshot.effectGates = append(snapshot.effectGates, ac) | 1371 | snapshot.effectGates = append(snapshot.effectGates, ac) | ||
| 1382 | } | 1372 | } | ||
| 1383 | if admission != nil { | 1373 | if admission != nil { | ||
| 1384 | - | add(admission.token.ac) | 1374 | + | add(admission.capability.ac) |
| 1385 | } | 1375 | } | ||
| 1386 | for ac := range snapshot.attachments { | 1376 | for ac := range snapshot.attachments { | ||
| 1387 | add(ac) | 1377 | add(ac) | ||
| @@ -1440,10 +1430,7 @@ func sessionKillParticipantsCurrentLocked(snapshot sessionKillParticipants, sour | |||||
| 1440 | return true | 1430 | return true | ||
| 1441 | } | 1431 | } | ||
| 1442 | token := *snapshot.sourceToken | 1432 | token := *snapshot.sourceToken | ||
| 1443 | - | req := attachmentTransitionRequest{ | 1433 | + | return token.currentInSessionAndLeaseLocked(token.sess, token.ac, sourceCoordinator) |
| 1444 | - | source: token.sess, next: token.ac, expectedTransport: token.transport, | |||
| 1445 | - | } | |||
| 1446 | - | return transitionSourceTokenCurrentLocked(token, token.sess, sourceCoordinator, req) | |||
| 1447 | } | 1434 | } | ||
| 1448 | 1435 | ||||
| 1449 | // stopInMemoryLifecycle cancels every session producer and closes every PTY | 1436 | // stopInMemoryLifecycle cancels every session producer and closes every PTY | ||
| @@ -1570,7 +1557,7 @@ func (d *Daemon) killSessionWithSnapshotDeadlineAndCondition(sess *session, reas | |||||
| 1570 | return nil | 1557 | return nil | ||
| 1571 | } | 1558 | } | ||
| 1572 | if admission != nil { | 1559 | if admission != nil { | ||
| 1573 | - | d.endActionAttachmentEffect(admission.token.effect, admission.action) | 1560 | + | d.endActionAttachmentEffect(admission.effect, admission.action) |
| 1574 | } | 1561 | } | ||
| 1575 | action := "" | 1562 | action := "" | ||
| 1576 | if admission != nil { | 1563 | if admission != nil { | ||
| @@ -1740,7 +1727,6 @@ func (d *Daemon) killSessionWithSnapshotDeadlineAndCondition(sess *session, reas | |||||
| 1740 | return | 1727 | return | ||
| 1741 | } | 1728 | } | ||
| 1742 | attachments = append(attachments, detachedAttachmentSnapshot{ac: attached, transport: participants.transports[attached]}) | 1729 | attachments = append(attachments, detachedAttachmentSnapshot{ac: attached, transport: participants.transports[attached]}) | ||
| 1743 | - | attached.connectionGeneration.Add(1) | |||
| 1744 | attached.setSession(nil) | 1730 | attached.setSession(nil) | ||
| 1745 | attached.invalidateFrozenAttachmentCapability() | 1731 | attached.invalidateFrozenAttachmentCapability() | ||
| 1746 | } | 1732 | } | ||
| @@ -1769,7 +1755,7 @@ func (d *Daemon) killSessionWithSnapshotDeadlineAndCondition(sess *session, reas | |||||
| 1769 | d.mu.Unlock() | 1755 | d.mu.Unlock() | ||
| 1770 | d.finishParkedAttachmentRetirements(parkedRetirements) | 1756 | d.finishParkedAttachmentRetirements(parkedRetirements) | ||
| 1771 | frozen.unfreeze() | 1757 | frozen.unfreeze() | ||
| 1772 | - | frozen = frozenAttachmentEffectGates{} | 1758 | + | frozen = attachmentTransitionGuard{} |
| 1773 | d.log.Info("session closed", "session", stoppedName, "id", sess.id, "ephemeral", ephemeral, "purge", purge, "reason", reason) | 1759 | d.log.Info("session closed", "session", stoppedName, "id", sess.id, "ephemeral", ephemeral, "purge", purge, "reason", reason) | ||
| 1774 | 1760 | ||||
| 1775 | var purgeErr error | 1761 | var purgeErr error | ||
M internal/usecase/daemon/session_back.go +6 -7
| @@ -8,11 +8,11 @@ import ( | |||||
| 8 | // backSessionForAttachment delegates previous-route ownership to the client | 8 | // backSessionForAttachment delegates previous-route ownership to the client | ||
| 9 | // once it has published a complete route snapshot. There is no daemon-local | 9 | // once it has published a complete route snapshot. There is no daemon-local | ||
| 10 | // previous-session fallback after the global-history cutover. | 10 | // previous-session fallback after the global-history cutover. | ||
| 11 | - | func (d *Daemon) backSessionForAttachment(token attachmentConnectionToken) error { | 11 | + | func (d *Daemon) backSessionForAttachment(effect *attachmentEffect) error { |
| 12 | - | if d == nil || token.sess == nil || token.ac == nil { | 12 | + | if d == nil || !effect.current() || effect.sess == nil || effect.ac == nil { |
| 13 | return nil | 13 | return nil | ||
| 14 | } | 14 | } | ||
| 15 | - | snapshot := token.ac.routeSnapshotCopy() | 15 | + | snapshot := effect.ac.routeSnapshotCopy() |
| 16 | if snapshot.Generation == 0 { | 16 | if snapshot.Generation == 0 { | ||
| 17 | if d.log != nil { | 17 | if d.log != nil { | ||
| 18 | d.log.Debug("back-session skipped: attachment has no published route snapshot") | 18 | d.log.Debug("back-session skipped: attachment has no published route snapshot") | ||
| @@ -22,7 +22,7 @@ func (d *Daemon) backSessionForAttachment(token attachmentConnectionToken) error | |||||
| 22 | if snapshot.Previous.Key == 0 || snapshot.Previous.Generation == 0 { | 22 | if snapshot.Previous.Key == 0 || snapshot.Previous.Generation == 0 { | ||
| 23 | return nil | 23 | return nil | ||
| 24 | } | 24 | } | ||
| 25 | - | return d.sendRecentRouteNavigationActionForAttachment(token, ports.RouteNavigationAction{ | 25 | + | return d.sendRecentRouteNavigationActionForAttachment(effect, ports.RouteNavigationAction{ |
| 26 | SnapshotGeneration: snapshot.Generation, | 26 | SnapshotGeneration: snapshot.Generation, | ||
| 27 | Key: snapshot.Previous.Key, | 27 | Key: snapshot.Previous.Key, | ||
| 28 | Generation: snapshot.Previous.Generation, | 28 | Generation: snapshot.Previous.Generation, | ||
| @@ -33,15 +33,14 @@ func (d *Daemon) backSession(current *session, ac *attachedClient) { | |||||
| 33 | if d == nil || current == nil || ac == nil { | 33 | if d == nil || current == nil || ac == nil { | ||
| 34 | return | 34 | return | ||
| 35 | } | 35 | } | ||
| 36 | - | token := attachmentToken(current, ac, ac.transport()) | 36 | + | token := captureAttachmentCapability(current, ac, ac.transport()) |
| 37 | effect, admitted := ac.beginAttachmentEffect(token) | 37 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 38 | if !admitted { | 38 | if !admitted { | ||
| 39 | d.reportError(current, domain.UserErr(domain.NoticeSessionUnavailable, "couldn't switch to that session", errAttachmentTransition)) | 39 | d.reportError(current, domain.UserErr(domain.NoticeSessionUnavailable, "couldn't switch to that session", errAttachmentTransition)) | ||
| 40 | return | 40 | return | ||
| 41 | } | 41 | } | ||
| 42 | - | token.effect = effect | |||
| 43 | defer effect.End() | 42 | defer effect.End() | ||
| 44 | - | if err := d.backSessionForAttachment(token); err != nil { | 43 | + | if err := d.backSessionForAttachment(effect); err != nil { |
| 45 | d.reportError(current, err) | 44 | d.reportError(current, err) | ||
| 46 | } | 45 | } | ||
| 47 | } | 46 | } | ||
M internal/usecase/daemon/session_back_route_test.go +3 -4
| @@ -12,13 +12,12 @@ func TestBackSessionUsesClientPreviousRouteAfterSnapshotPublication(t *testing.T | |||||
| 12 | transport := &closeTrackingTransport{} | 12 | transport := &closeTrackingTransport{} | ||
| 13 | ac.replaceTransport(transport) | 13 | ac.replaceTransport(transport) | ||
| 14 | rc := d.attachCoordinator(source, nil, ac, true) | 14 | rc := d.attachCoordinator(source, nil, ac, true) | ||
| 15 | - | token := source.attachmentToken(ac, transport) | 15 | + | token := source.captureAttachmentCapability(ac, transport) |
| 16 | token.lease = rc.attachmentLease(ac) | 16 | token.lease = rc.attachmentLease(ac) | ||
| 17 | - | ac.publishAttachmentCapability(token) | 17 | + | ac.installTestAttachmentCapability(token) |
| 18 | effect, admitted := ac.beginAttachmentEffect(token) | 18 | effect, admitted := ac.beginAttachmentEffect(token) | ||
| 19 | require.True(t, admitted) | 19 | require.True(t, admitted) | ||
| 20 | defer effect.End() | 20 | defer effect.End() | ||
| 21 | - | token.effect = effect | |||
| 22 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{ | 21 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{ | ||
| 23 | Generation: 5, | 22 | Generation: 5, | ||
| 24 | Active: ports.RouteRef{Key: 9, Generation: 5}, | 23 | Active: ports.RouteRef{Key: 9, Generation: 5}, | ||
| @@ -26,7 +25,7 @@ func TestBackSessionUsesClientPreviousRouteAfterSnapshotPublication(t *testing.T | |||||
| 26 | Entries: []ports.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, ports.RouteKindLocal)}, | 25 | Entries: []ports.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, ports.RouteKindLocal)}, | ||
| 27 | }) | 26 | }) | ||
| 28 | 27 | ||||
| 29 | - | require.NoError(t, d.backSessionForAttachment(token)) | 28 | + | require.NoError(t, d.backSessionForAttachment(effect)) |
| 30 | frames := transport.Sends() | 29 | frames := transport.Sends() | ||
| 31 | require.NotEmpty(t, frames) | 30 | require.NotEmpty(t, frames) | ||
| 32 | var action ports.RouteNavigationAction | 31 | var action ports.RouteNavigationAction | ||
M internal/usecase/daemon/session_rename_purge_test.go +4 -4
| @@ -17,8 +17,8 @@ func TestRenamePublishesCommittedRouteIdentityToAttachedClients(t *testing.T) { | |||||
| 17 | sess.ephemeral = true | 17 | sess.ephemeral = true | ||
| 18 | sess.incarnation = domain.IncarnationID{1} | 18 | sess.incarnation = domain.IncarnationID{1} | ||
| 19 | sess.mu.Unlock() | 19 | sess.mu.Unlock() | ||
| 20 | - | token := sess.attachmentToken(ac, ac.transport()) | 20 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 21 | - | ac.publishAttachmentCapability(token) | 21 | + | ac.installTestAttachmentCapability(token) |
| 22 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | 22 | ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1}) | ||
| 23 | 23 | ||||
| 24 | require.NoError(t, d.renameSession(sess, "vps-infra")) | 24 | require.NoError(t, d.renameSession(sess, "vps-infra")) | ||
| @@ -40,8 +40,8 @@ func TestRenameDefersCommittedRouteIdentityUntilFirstRouteSnapshot(t *testing.T) | |||||
| 40 | sess.ephemeral = true | 40 | sess.ephemeral = true | ||
| 41 | sess.incarnation = domain.IncarnationID{1} | 41 | sess.incarnation = domain.IncarnationID{1} | ||
| 42 | sess.mu.Unlock() | 42 | sess.mu.Unlock() | ||
| 43 | - | token := sess.attachmentToken(ac, ac.transport()) | 43 | + | token := sess.captureAttachmentCapability(ac, ac.transport()) |
| 44 | - | ac.publishAttachmentCapability(token) | 44 | + | ac.installTestAttachmentCapability(token) |
| 45 | 45 | ||||
| 46 | require.NoError(t, d.renameSession(sess, "vps-infra")) | 46 | require.NoError(t, d.renameSession(sess, "vps-infra")) | ||
| 47 | require.Empty(t, sends, "the route ledger must be published before its identity can be updated") | 47 | require.Empty(t, sends, "the route ledger must be published before its identity can be updated") | ||
M internal/usecase/daemon/transactional_resize.go +6 -5
| @@ -703,12 +703,13 @@ func (d *Daemon) runResizeTransaction(sess *session, ac *attachedClient, lease * | |||||
| 703 | return false | 703 | return false | ||
| 704 | } | 704 | } | ||
| 705 | } | 705 | } | ||
| 706 | - | connectionGeneration := ac.connectionGeneration.Load() | 706 | + | effect, admitted := beginAttachmentLeaseEffect(sess, ac, lease) |
| 707 | - | connection := ac.transportSnapshot() | 707 | + | if !admitted { |
| 708 | + | return false | |||
| 709 | + | } | |||
| 710 | + | defer effect.End() | |||
| 708 | current := func() bool { | 711 | current := func() bool { | ||
| 709 | - | return ac.connectionGeneration.Load() == connectionGeneration && | 712 | + | return effect.current() && sess.geometryClaimTokenCurrent(ac, geometryClaim) && |
| 710 | - | ac.transportSnapshotCurrent(connection) && | |||
| 711 | - | sess.geometryClaimTokenCurrent(ac, geometryClaim) && | |||
| 712 | rc.resizeCurrentForLease(epoch, ac, lease, false) | 713 | rc.resizeCurrentForLease(epoch, ac, lease, false) | ||
| 713 | } | 714 | } | ||
| 714 | if !current() { | 715 | if !current() { | ||