1package daemon
2
3import snapcodec "github.com/bnema/vev/internal/snapshotcodec"
4
5// captureSnapshotState copies history tails and recovery transcripts while holding
6// each pane lock. The returned capture contains only immutable state; encoding
7// and persistence are deliberately deferred to snapshotEncodeWorker.
8func (d *Daemon) captureSnapshotState(sess *session, generation uint64) (*snapshotCapture, bool) {
9 sess.mu.Lock()
10 capture := &snapshotCapture{
11 session: sess,
12 generation: generation,
13 name: sess.name,
14 incarnation: sess.incarnation,
15 createdAt: uint64(sess.createdAt),
16 // Snapshot format retains a deterministic first-tab default; it is not
17 // an interactive attachment selection.
18 defaultTab: 0,
19 }
20 ephemeral := sess.ephemeral
21 fallbackCwd := sess.cwd
22 tabs := append([]*tab(nil), sess.tabs...)
23 sess.mu.Unlock()
24 if ephemeral || capture.name == "" {
25 return capture, false
26 }
27
28 capture.tabs = make([]snapshotCaptureTab, 0, len(tabs))
29 for _, tb := range tabs {
30 tb.mu.Lock()
31 tabCapture := snapshotCaptureTab{
32 stableID: tb.stableID,
33 cols: uint16(max(tb.size.Cols, 0)),
34 rows: uint16(max(tb.size.Rows, 0)),
35 nextPaneID: uint64(max(tb.nextPaneID, 0)),
36 tree: tb.tree.Clone(),
37 }
38 if tb.tree != nil {
39 tabCapture.focus = tb.tree.Focus
40 }
41 panes := make([]*pane, 0, len(tb.panes))
42 for _, p := range tb.panes {
43 panes = append(panes, p)
44 }
45 tb.mu.Unlock()
46
47 tabCapture.panes = make([]snapshotCapturePane, 0, len(panes))
48 for _, p := range panes {
49 p.mu.Lock()
50 pty := p.pty
51 pid := 0
52 if pty != nil {
53 pid = pty.Pid()
54 }
55 paneCapture := snapshotCapturePane{
56 id: p.id,
57 stableID: p.stableID,
58 transcript: p.screen.RecoveryTranscriptSnapshot(),
59 }
60 paneCapture.sealed = p.history.SnapshotView()
61 paneCapture.tail = paneCapture.sealed.Tail()
62 p.mu.Unlock()
63 paneCapture.cwd = fallbackCwd
64 if d.proc != nil && pid > 0 {
65 if cwd, err := d.proc.Cwd(pid); err == nil && cwd != "" {
66 paneCapture.cwd = cwd
67 }
68 }
69 paneCapture.process = d.capturePaneProcess(pty, pid)
70 tabCapture.panes = append(tabCapture.panes, paneCapture)
71 }
72 capture.tabs = append(capture.tabs, tabCapture)
73 }
74 return capture, true
75}
76
77// captureSession remains the synchronous producer-facing trigger for callers
78// such as teardown and benchmarks; the actual encoding and Write stay async.
79func (d *Daemon) captureSession(sess *session) bool {
80 markSnapshotDirty(sess)
81 return d.scheduleSnapshot(sess)
82}
83
84func (d *Daemon) capturePaneProcess(pty interface{ ForegroundPgid() (int, error) }, shellPid int) *snapcodec.Process {
85 if d == nil || pty == nil || shellPid <= 0 || !d.hasProcessDetails() {
86 return nil
87 }
88 pgid, err := pty.ForegroundPgid()
89 if err != nil || pgid <= 0 || pgid == shellPid {
90 return nil
91 }
92 argv, err := d.proc.GroupArgv(pgid, shellPid)
93 if err != nil || len(argv) == 0 || argv[0] == "" {
94 return nil
95 }
96 strategy := detectProcessStrategy(argv)
97 return &snapcodec.Process{
98 Argv: append([]string(nil), argv...),
99 Strategy: strategy,
100 Opts: snapcodec.ProcessOpts{
101 AgentSessionID: extractAgentSessionID(strategy, argv),
102 },
103 }
104}