Files
chenhg5-cc-connect/core/dir_history.go
zed 65ff498174 fix: add concurrency safety, error logging, and Windows path hardening
P0 — Feishu platform concurrency safety (platform/feishu/feishu.go):
- Add sync.RWMutex to Platform struct
- Protect botOpenID, handler, cancel, server fields with getter helpers
- Fix TOCTOU race in Stop() by capturing cancel/server under lock

P2 — Log silently discarded errors (5 locations):
- core/engine.go: cron EnableJob/DisableJob, outgoing rate limit wait
- platform/telegram/telegram.go: invalid thread ID parsing
- agent/claudecode/session.go: stdin close, SIGTERM, force kill failures
- agent/claudecode/claude_usage.go: ptmx close, process kill failures

P4 — Windows path hardening (2 locations):
- core/message.go: normalize backslashes with filepath.ToSlash +
  strings.ReplaceAll for cross-platform defense in depth
- core/dir_history.go: tighten MkdirAll permissions from 0755 to 0700

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:21:34 +08:00

168 lines
3.5 KiB
Go

package core
import (
"encoding/json"
"log/slog"
"os"
"path/filepath"
"sync"
)
const (
defaultDirHistorySize = 10
dirHistoryFileName = "dir_history.json"
)
// DirHistory manages directory switch history per project.
type DirHistory struct {
mu sync.RWMutex
storePath string
entries map[string][]string // project name -> dir list (most recent first)
maxSize int
}
// NewDirHistory creates a new DirHistory with the given data directory.
func NewDirHistory(dataDir string) *DirHistory {
dh := &DirHistory{
storePath: filepath.Join(dataDir, dirHistoryFileName),
entries: make(map[string][]string),
maxSize: defaultDirHistorySize,
}
dh.load()
return dh
}
// Add adds a directory to the history for the given project.
// If the directory already exists, it's moved to the front.
func (dh *DirHistory) Add(project, dir string) {
if dir == "" {
return
}
dh.mu.Lock()
defer dh.mu.Unlock()
entries := dh.entries[project]
// Remove if exists
for i, d := range entries {
if d == dir {
entries = append(entries[:i], entries[i+1:]...)
break
}
}
// Add to front
entries = append([]string{dir}, entries...)
// Trim to max size
if len(entries) > dh.maxSize {
entries = entries[:dh.maxSize]
}
dh.entries[project] = entries
dh.saveLocked()
}
// List returns the history for the given project.
func (dh *DirHistory) List(project string) []string {
dh.mu.RLock()
defer dh.mu.RUnlock()
entries := dh.entries[project]
if entries == nil {
return nil
}
// Return a copy
result := make([]string, len(entries))
copy(result, entries)
return result
}
// Get returns the directory at the given index (1-based) for the project.
// Returns empty string if index is out of range.
func (dh *DirHistory) Get(project string, index int) string {
dh.mu.RLock()
defer dh.mu.RUnlock()
entries := dh.entries[project]
if index < 1 || index > len(entries) {
return ""
}
return entries[index-1]
}
// Previous returns the previous directory (index 2, since index 1 is current).
func (dh *DirHistory) Previous(project string) string {
return dh.Get(project, 2)
}
// Contains checks if a directory is in the history for the given project.
func (dh *DirHistory) Contains(project, dir string) bool {
dh.mu.RLock()
defer dh.mu.RUnlock()
entries := dh.entries[project]
for _, d := range entries {
if d == dir {
return true
}
}
return false
}
// SetMaxSize sets the maximum history size.
func (dh *DirHistory) SetMaxSize(size int) {
if size < 1 {
size = 1
}
dh.mu.Lock()
defer dh.mu.Unlock()
dh.maxSize = size
}
func (dh *DirHistory) load() {
if dh.storePath == "" {
return
}
data, err := os.ReadFile(dh.storePath)
if err != nil {
if !os.IsNotExist(err) {
slog.Error("dir_history: failed to read", "path", dh.storePath, "error", err)
}
return
}
var entries map[string][]string
if err := json.Unmarshal(data, &entries); err != nil {
slog.Error("dir_history: failed to unmarshal", "path", dh.storePath, "error", err)
return
}
if entries != nil {
dh.entries = entries
}
}
func (dh *DirHistory) saveLocked() {
if dh.storePath == "" {
return
}
data, err := json.MarshalIndent(dh.entries, "", " ")
if err != nil {
slog.Error("dir_history: failed to marshal", "error", err)
return
}
if err := os.MkdirAll(filepath.Dir(dh.storePath), 0700); err != nil {
slog.Error("dir_history: failed to create dir", "error", err)
return
}
if err := AtomicWriteFile(dh.storePath, data, 0644); err != nil {
slog.Error("dir_history: failed to write", "path", dh.storePath, "error", err)
}
}