Files
skrueckenandCursor 2dbf0918de Add local JSON daemon and route TUI/CLI through it.
A per-DataDir background process owns ticket JSON, history, and sync so
multiple TUI instances stay consistent; MCP token APIs are ready for phase 2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 16:32:13 +01:00

92 lines
1.9 KiB
Go

package daemon
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
)
const (
sockName = "daemon.sock"
pidName = "daemon.pid"
logName = "daemon.log"
)
// Paths holds filesystem locations for one DataDir's daemon instance.
type Paths struct {
RuntimeDir string
Socket string
PIDFile string
LogFile string
}
// PathsForDataDir returns runtime paths unique to dataDir.
func PathsForDataDir(dataDir string) (Paths, error) {
dataDir = filepath.Clean(dataDir)
sum := sha256.Sum256([]byte(dataDir))
id := hex.EncodeToString(sum[:8])
dir, err := mkdirRuntime(id, dataDir)
if err != nil {
return Paths{}, err
}
return Paths{
RuntimeDir: dir,
Socket: filepath.Join(dir, sockName),
PIDFile: filepath.Join(dir, pidName),
LogFile: filepath.Join(dir, logName),
}, nil
}
func mkdirRuntime(id, dataDir string) (string, error) {
candidates := []string{}
if r := strings.TrimSpace(os.Getenv("XDG_RUNTIME_DIR")); r != "" {
candidates = append(candidates, filepath.Join(r, "task-master-"+id))
}
candidates = append(candidates,
filepath.Join(os.TempDir(), fmt.Sprintf("task-master-%d-%s", os.Getuid(), id)),
filepath.Join(dataDir, ".runtime"),
)
var last error
for _, dir := range candidates {
if err := os.MkdirAll(dir, 0o700); err != nil {
last = err
continue
}
return dir, nil
}
if last == nil {
last = fmt.Errorf("no runtime dir candidate")
}
return "", fmt.Errorf("create runtime dir: %w", last)
}
func writePID(path string, pid int) error {
return os.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0o600)
}
func readPID(path string) (int, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, err
}
s := strings.TrimSpace(string(data))
if s == "" {
return 0, fmt.Errorf("empty pid file")
}
return strconv.Atoi(s)
}
func processAlive(pid int) bool {
if pid <= 0 {
return false
}
err := syscall.Kill(pid, 0)
return err == nil
}