Skip to content

Commit 1e7f955

Browse files
authored
fix: make meta updates atomic to prevent cursor position races (#11)
Concurrent LoadMeta β†’ modify β†’ SaveMeta cycles could overwrite each other's changes. When captureOutput cleanup saved session state, it could clobber cursor positions written by handleRead, causing reads to restart from position 0. - Add UpdateMeta to OutputStorage interface (atomic read-modify-write under lock) - Implement in MemoryStorage (mutates in-place under lock) and FileStorage (load-mutate-save under lock) - Replace all LoadMeta/SaveMeta pairs in server.go with UpdateMeta calls - Affects: captureOutput cleanup, handleRead, handleReadTUI, handleStop, handleResize
1 parent 3b720d1 commit 1e7f955

File tree

4 files changed

+51
-25
lines changed

4 files changed

+51
-25
lines changed

β€Žinternal/daemon/server.goβ€Ž

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -499,11 +499,10 @@ func (s *Server) captureOutput(name string, h *sessionHandle) {
499499
now := time.Now()
500500
h.stoppedAt = &now
501501

502-
if meta, err := s.storage.LoadMeta(name); err == nil {
502+
s.storage.UpdateMeta(name, func(meta *SessionMeta) {
503503
meta.State = StateStopped
504504
meta.StoppedAt = &now
505-
s.storage.SaveMeta(name, meta)
506-
}
505+
})
507506
}()
508507

509508
buf := make([]byte, ReadBufferSize)
@@ -622,15 +621,16 @@ func (s *Server) handleRead(req Request) Response {
622621
result = string(output)
623622
}
624623

625-
if req.Cursor != "" {
626-
if meta.Cursors == nil {
627-
meta.Cursors = make(map[string]int64)
624+
storage.UpdateMeta(req.Name, func(m *SessionMeta) {
625+
if req.Cursor != "" {
626+
if m.Cursors == nil {
627+
m.Cursors = make(map[string]int64)
628+
}
629+
m.Cursors[req.Cursor] = totalLen
630+
} else {
631+
m.ReadPos = totalLen
628632
}
629-
meta.Cursors[req.Cursor] = totalLen
630-
} else {
631-
meta.ReadPos = totalLen
632-
}
633-
storage.SaveMeta(req.Name, meta)
633+
})
634634
default:
635635
output, err := storage.ReadAll(req.Name)
636636
if err != nil {
@@ -682,15 +682,16 @@ func (s *Server) handleReadTUI(req Request, h *sessionHandle, screen *vterm.Scre
682682
result = screen.Render()
683683
}
684684

685-
if req.Cursor != "" {
686-
if meta.Cursors == nil {
687-
meta.Cursors = make(map[string]int64)
685+
s.storage.UpdateMeta(req.Name, func(m *SessionMeta) {
686+
if req.Cursor != "" {
687+
if m.Cursors == nil {
688+
m.Cursors = make(map[string]int64)
689+
}
690+
m.Cursors[req.Cursor] = currentVersion
691+
} else {
692+
m.ReadPos = currentVersion
688693
}
689-
meta.Cursors[req.Cursor] = currentVersion
690-
} else {
691-
meta.ReadPos = currentVersion
692-
}
693-
s.storage.SaveMeta(req.Name, meta)
694+
})
694695
default:
695696
result = screen.Render()
696697
}
@@ -924,11 +925,10 @@ func (s *Server) handleStop(req Request) Response {
924925
now := time.Now()
925926
h.stoppedAt = &now
926927

927-
if meta, err := s.storage.LoadMeta(req.Name); err == nil {
928+
s.storage.UpdateMeta(req.Name, func(meta *SessionMeta) {
928929
meta.State = StateStopped
929930
meta.StoppedAt = &now
930-
s.storage.SaveMeta(req.Name, meta)
931-
}
931+
})
932932

933933
return Response{Success: true}
934934
}
@@ -1188,9 +1188,10 @@ func (s *Server) handleResize(req Request) Response {
11881188
}
11891189
s.mu.Unlock()
11901190

1191-
meta.Cols = cols
1192-
meta.Rows = rows
1193-
if err := storage.SaveMeta(req.Name, meta); err != nil {
1191+
if err := storage.UpdateMeta(req.Name, func(m *SessionMeta) {
1192+
m.Cols = cols
1193+
m.Rows = rows
1194+
}); err != nil {
11941195
return Response{Success: false, Error: fmt.Sprintf("save meta: %v", err)}
11951196
}
11961197

β€Žinternal/daemon/storage.goβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ type OutputStorage interface {
3636

3737
LoadMeta(session string) (*SessionMeta, error)
3838
SaveMeta(session string, meta *SessionMeta) error
39+
UpdateMeta(session string, fn func(meta *SessionMeta)) error
3940
ListSessions() ([]string, error)
4041
}

β€Žinternal/daemon/storage_file.goβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ func (s *FileStorage) saveMetaLocked(session string, meta *SessionMeta) error {
207207
return nil
208208
}
209209

210+
func (s *FileStorage) UpdateMeta(session string, fn func(meta *SessionMeta)) error {
211+
s.mu.Lock()
212+
defer s.mu.Unlock()
213+
214+
meta, err := s.loadMetaLocked(session)
215+
if err != nil {
216+
return err
217+
}
218+
fn(meta)
219+
return s.saveMetaLocked(session, meta)
220+
}
221+
210222
func (s *FileStorage) ListSessions() ([]string, error) {
211223
s.mu.RLock()
212224
defer s.mu.RUnlock()

β€Žinternal/daemon/storage_memory.goβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,18 @@ func (s *MemoryStorage) SaveMeta(session string, meta *SessionMeta) error {
162162
return nil
163163
}
164164

165+
func (s *MemoryStorage) UpdateMeta(session string, fn func(meta *SessionMeta)) error {
166+
s.mu.Lock()
167+
defer s.mu.Unlock()
168+
169+
meta, exists := s.metas[session]
170+
if !exists {
171+
return fmt.Errorf("session %q not found", session)
172+
}
173+
fn(meta)
174+
return nil
175+
}
176+
165177
func (s *MemoryStorage) ListSessions() ([]string, error) {
166178
s.mu.RLock()
167179
defer s.mu.RUnlock()

0 commit comments

Comments
Β (0)