返回 DeepSeek-Reasonix
chat_tui_shutdown.go
根目录 / internal / cli / chat_tui_shutdown.go
1 // chat_tui_shutdown.go — the one exit every quit gesture and signal funnels into.
2 package cli
3
4 import (
5 "fmt"
6 "os"
7 "sync/atomic"
8
9 tea "charm.land/bubbletea/v2"
10
11 "reasonix/internal/i18n"
12 )
13
14 const (
15 tuiShutdownPending uint32 = iota
16 tuiShutdownCompleted
17 tuiShutdownFallbackClaimed
18 )
19
20 // tuiShutdownCompletion arbitrates the boundary between a completed graceful
21 // exit and the watchdog fallback. Exactly one side wins, so a timer callback
22 // cannot turn a snapshot that already completed into ErrProgramKilled.
23 type tuiShutdownCompletion struct {
24 state atomic.Uint32
25 done chan struct{}
26 }
27
28 func newTUIShutdownCompletion() *tuiShutdownCompletion {
29 return &tuiShutdownCompletion{done: make(chan struct{})}
30 }
31
32 func (c *tuiShutdownCompletion) complete() {
33 if c != nil && c.state.CompareAndSwap(tuiShutdownPending, tuiShutdownCompleted) {
34 if c.done != nil {
35 close(c.done)
36 }
37 }
38 }
39
40 func (c *tuiShutdownCompletion) claimFallback() bool {
41 return c != nil && c.state.CompareAndSwap(tuiShutdownPending, tuiShutdownFallbackClaimed)
42 }
43
44 // shutdownAndQuit persists what the controller holds beyond the last snapshot
45 // and leaves. The shutdown variant writes a recovery branch when another
46 // process keeps the active session's compatibility lock for the bounded wait.
47 func (m chatTUI) shutdownAndQuit(msg tuiShutdownMsg) (tea.Model, tea.Cmd) {
48 if !msg.userInitiated && m.takeover != nil && m.takeover.Reclaiming() {
49 // The manager goroutine owns the in-flight reclaim's final snapshot and
50 // its return, so the TUI must not race it with a second snapshot. The
51 // request is deferred, not dropped: completeSessionReclaim honors it.
52 if msg.completion != nil {
53 msg.completion.complete()
54 }
55 m.shutdownAfterReclaim = true
56 return m, nil
57 }
58 if msg.completion != nil {
59 defer msg.completion.complete()
60 }
61 // Snapshot only while this process still writes the session. A reclaimed
62 // or returned session belongs to the remote side again, and its runtime
63 // binding is already released.
64 if m.ctrl != nil && !m.sessionDetached() {
65 m.shutdownErr = m.ctrl.SnapshotForShutdown()
66 m.followSessionLease()
67 }
68 return m, tea.Quit
69 }
70
71 // reportShutdownFailure runs after Bubble Tea restores the terminal, when a
72 // final-save failure can be printed without corrupting the alt screen.
73 func reportShutdownFailure(err error) {
74 if err != nil {
75 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
76 }
77 }
78
78 lines GO