Files
cc-connect/daemon/manager.go
T
chenhg5andClaude Opus 4.6 9a291eb0c3 feat: add daemon mode for running as background service
Add support for running cc-connect as a daemon managed by OS init system:
- Linux: systemd user service
- macOS: launchd LaunchAgent

New CLI commands:
- cc-connect daemon install/uninstall
- cc-connect daemon start/stop/restart/status
- cc-connect daemon logs [-f] [-n N] [--log-file PATH]

Features:
- Auto log rotation with configurable max size
- Cross-platform support (systemd/launchd)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:18:55 +08:00

133 lines
2.7 KiB
Go

package daemon
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
const (
DefaultLogMaxSize = 10 * 1024 * 1024 // 10 MB
ServiceName = "cc-connect"
)
type Config struct {
BinaryPath string
WorkDir string
LogFile string
LogMaxSize int64
EnvPATH string // capture user's PATH so agents are accessible
}
type Status struct {
Installed bool
Running bool
PID int
Platform string // "systemd", "launchd", "schtasks"
}
type Manager interface {
Install(cfg Config) error
Uninstall() error
Start() error
Stop() error
Restart() error
Status() (*Status, error)
Platform() string
}
// NewManager returns a platform-specific daemon manager.
func NewManager() (Manager, error) {
return newPlatformManager()
}
func DefaultLogFile() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".cc-connect", "logs", "cc-connect.log")
}
func DefaultDataDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".cc-connect")
}
// ── Metadata ────────────────────────────────────────────────
// Stored at ~/.cc-connect/daemon.json so that `logs`, `status`,
// etc. can locate the log file without parsing service definitions.
type Meta struct {
LogFile string `json:"log_file"`
LogMaxSize int64 `json:"log_max_size"`
WorkDir string `json:"work_dir"`
BinaryPath string `json:"binary_path"`
InstalledAt string `json:"installed_at"`
}
func metaPath() string {
return filepath.Join(DefaultDataDir(), "daemon.json")
}
func SaveMeta(m *Meta) error {
if err := os.MkdirAll(filepath.Dir(metaPath()), 0755); err != nil {
return err
}
data, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
return os.WriteFile(metaPath(), data, 0644)
}
func LoadMeta() (*Meta, error) {
data, err := os.ReadFile(metaPath())
if err != nil {
return nil, err
}
var m Meta
if err := json.Unmarshal(data, &m); err != nil {
return nil, err
}
return &m, nil
}
func RemoveMeta() {
os.Remove(metaPath())
}
func NowISO() string {
return time.Now().Format(time.RFC3339)
}
func Resolve(cfg *Config) error {
if cfg.BinaryPath == "" {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("cannot detect binary path: %w", err)
}
real, err := filepath.EvalSymlinks(exe)
if err == nil {
exe = real
}
cfg.BinaryPath = exe
}
if cfg.WorkDir == "" {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("cannot detect working directory: %w", err)
}
cfg.WorkDir = wd
}
if cfg.LogFile == "" {
cfg.LogFile = DefaultLogFile()
}
if cfg.LogMaxSize <= 0 {
cfg.LogMaxSize = DefaultLogMaxSize
}
if cfg.EnvPATH == "" {
cfg.EnvPATH = os.Getenv("PATH")
}
return nil
}