| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "sync" |
| 6 | |
| 7 | "reasonix/internal/agent" |
| 8 | ) |
| 9 | |
| 10 | // returnCurrentMirror refreshes the binding after pushLocked while sendMu |
| 11 | // fences the generation used by both the reverse reservation and mirror-end. |
| 12 | func (m *cliTakeoverManager) returnCurrentMirror(expectedPath string, retire func(*cliTakeoverBinding) error) error { |
| 13 | return m.returnMirrorTransaction(expectedPath, false, false, retire) |
| 14 | } |
| 15 | |
| 16 | // returnMirrorTransaction is the single owner for retiring an active mirror. |
| 17 | // returnMu prevents Activate/session switches, sendMu fences re-adoption, and |
| 18 | // the binding is re-read after pushLocked so the reverse reservation and |
| 19 | // mirror-end always use one current generation. Snapshot is requested for |
| 20 | // reclaim/exit, after returnMu excludes a different session but before the |
| 21 | // sender lock is held across disk I/O and event fan-out. |
| 22 | func (m *cliTakeoverManager) returnMirrorTransaction(expectedPath string, allowReclaim, snapshot bool, retire func(*cliTakeoverBinding) error) error { |
| 23 | if m == nil || retire == nil { |
| 24 | return fmt.Errorf("takeover return transaction unavailable") |
| 25 | } |
| 26 | expectedPath = agent.CanonicalSessionPath(expectedPath) |
| 27 | m.returnMu.Lock() |
| 28 | defer m.returnMu.Unlock() |
| 29 | if !allowReclaim && m.reclaiming.Load() { |
| 30 | return fmt.Errorf("the remote side is reclaiming the current session") |
| 31 | } |
| 32 | current, ctrl, _, _ := m.snapshot() |
| 33 | if current == nil || m.returned.Load() { |
| 34 | return nil |
| 35 | } |
| 36 | if expectedPath != "" && agent.CanonicalSessionPath(current.path) != expectedPath { |
| 37 | return fmt.Errorf("current takeover mirror changed during session switch") |
| 38 | } |
| 39 | if snapshot && ctrl != nil { |
| 40 | if err := ctrl.Snapshot(); err != nil { |
| 41 | return err |
| 42 | } |
| 43 | } |
| 44 | m.sendMu.Lock() |
| 45 | unlockError := func(err error) error { m.sendMu.Unlock(); return err } |
| 46 | current, _, _, _ = m.snapshot() |
| 47 | if current == nil || m.returned.Load() || expectedPath != "" && agent.CanonicalSessionPath(current.path) != expectedPath { |
| 48 | return unlockError(fmt.Errorf("current takeover mirror changed during session switch")) |
| 49 | } |
| 50 | if !m.pushLocked(false) { |
| 51 | return unlockError(fmt.Errorf("current takeover mirror stopped during session switch")) |
| 52 | } |
| 53 | current, _, _, _ = m.snapshot() |
| 54 | if current == nil || m.returned.Load() || expectedPath != "" && agent.CanonicalSessionPath(current.path) != expectedPath { |
| 55 | return unlockError(fmt.Errorf("current takeover mirror changed during session switch")) |
| 56 | } |
| 57 | if !allowReclaim && m.reclaiming.Load() { |
| 58 | return unlockError(fmt.Errorf("the remote side is reclaiming the current session")) |
| 59 | } |
| 60 | if err := retire(current); err != nil { |
| 61 | return unlockError(err) |
| 62 | } |
| 63 | m.returned.Store(true) |
| 64 | m.mirrorEndLocked(current) |
| 65 | m.mu.Lock() |
| 66 | started, stop, done := m.started, m.stop, m.done |
| 67 | m.binding = nil |
| 68 | if allowReclaim { |
| 69 | m.yielded = current |
| 70 | } |
| 71 | m.queue.Reset() |
| 72 | m.revision++ |
| 73 | m.failures = 0 |
| 74 | m.mu.Unlock() |
| 75 | m.sendMu.Unlock() |
| 76 | if started { |
| 77 | m.stopOnce.Do(func() { close(stop) }) |
| 78 | <-done |
| 79 | } |
| 80 | m.mu.Lock() |
| 81 | m.started, m.wake, m.stop, m.done = false, nil, nil, nil |
| 82 | m.stopOnce = sync.Once{} |
| 83 | m.reclaiming.Store(false) |
| 84 | if !allowReclaim { |
| 85 | // A session switch is followed by Activate for a different mirror (or |
| 86 | // continues without one). Terminal reclaim/Close keeps returned true so |
| 87 | // the outer TUI/headless lifecycle can observe that ownership was yielded. |
| 88 | m.returned.Store(false) |
| 89 | } |
| 90 | m.mu.Unlock() |
| 91 | return nil |
| 92 | } |
| 93 |