Files
clawgo/pkg/configops/configops.go
2026-03-10 00:33:23 +08:00

191 lines
4.5 KiB
Go

package configops
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/YspCoder/clawgo/pkg/config"
)
func LoadConfigAsMap(path string) (map[string]interface{}, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
defaultCfg := config.DefaultConfig()
defData, mErr := json.Marshal(defaultCfg)
if mErr != nil {
return nil, mErr
}
var cfgMap map[string]interface{}
if uErr := json.Unmarshal(defData, &cfgMap); uErr != nil {
return nil, uErr
}
return cfgMap, nil
}
return nil, err
}
var cfgMap map[string]interface{}
if err := json.Unmarshal(data, &cfgMap); err != nil {
return nil, err
}
return cfgMap, nil
}
func NormalizeConfigPath(path string) string {
p := strings.TrimSpace(path)
p = strings.Trim(p, ".")
parts := strings.Split(p, ".")
for i, part := range parts {
if part == "enable" {
parts[i] = "enabled"
}
}
return strings.Join(parts, ".")
}
func ParseConfigValue(raw string) interface{} {
v := strings.TrimSpace(raw)
lv := strings.ToLower(v)
if lv == "true" {
return true
}
if lv == "false" {
return false
}
if lv == "null" {
return nil
}
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
return i
}
if f, err := strconv.ParseFloat(v, 64); err == nil && strings.Contains(v, ".") {
return f
}
if len(v) >= 2 && ((v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'')) {
return v[1 : len(v)-1]
}
return v
}
func SetMapValueByPath(root map[string]interface{}, path string, value interface{}) error {
if path == "" {
return fmt.Errorf("path is empty")
}
parts := strings.Split(path, ".")
cur := root
for i := 0; i < len(parts)-1; i++ {
key := parts[i]
if key == "" {
return fmt.Errorf("invalid path: %s", path)
}
next, ok := cur[key]
if !ok {
child := map[string]interface{}{}
cur[key] = child
cur = child
continue
}
child, ok := next.(map[string]interface{})
if !ok {
return fmt.Errorf("path segment is not object: %s", key)
}
cur = child
}
last := parts[len(parts)-1]
if last == "" {
return fmt.Errorf("invalid path: %s", path)
}
cur[last] = value
return nil
}
func GetMapValueByPath(root map[string]interface{}, path string) (interface{}, bool) {
if path == "" {
return nil, false
}
parts := strings.Split(path, ".")
var cur interface{} = root
for _, key := range parts {
obj, ok := cur.(map[string]interface{})
if !ok {
return nil, false
}
next, ok := obj[key]
if !ok {
return nil, false
}
cur = next
}
return cur, true
}
func WriteConfigAtomicWithBackup(configPath string, data []byte) (string, error) {
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return "", err
}
backupPath := configPath + ".bak"
if oldData, err := os.ReadFile(configPath); err == nil {
if err := os.WriteFile(backupPath, oldData, 0644); err != nil {
return "", fmt.Errorf("write backup failed: %w", err)
}
} else if !os.IsNotExist(err) {
return "", fmt.Errorf("read existing config failed: %w", err)
}
tmpPath := configPath + ".tmp"
if err := os.WriteFile(tmpPath, data, 0644); err != nil {
return "", fmt.Errorf("write temp config failed: %w", err)
}
if err := os.Rename(tmpPath, configPath); err != nil {
_ = os.Remove(tmpPath)
return "", fmt.Errorf("atomic replace config failed: %w", err)
}
return backupPath, nil
}
func RollbackConfigFromBackup(configPath, backupPath string) error {
backupData, err := os.ReadFile(backupPath)
if err != nil {
return fmt.Errorf("read backup failed: %w", err)
}
tmpPath := configPath + ".rollback.tmp"
if err := os.WriteFile(tmpPath, backupData, 0644); err != nil {
return fmt.Errorf("write rollback temp failed: %w", err)
}
if err := os.Rename(tmpPath, configPath); err != nil {
_ = os.Remove(tmpPath)
return fmt.Errorf("rollback replace failed: %w", err)
}
return nil
}
func TriggerGatewayReload(configPath string, notRunningErr error) (bool, error) {
pidPath := filepath.Join(filepath.Dir(configPath), "gateway.pid")
data, err := os.ReadFile(pidPath)
if err != nil {
return false, fmt.Errorf("%w (pid file not found: %s)", notRunningErr, pidPath)
}
pidStr := strings.TrimSpace(string(data))
pid, err := strconv.Atoi(pidStr)
if err != nil || pid <= 0 {
return true, fmt.Errorf("invalid gateway pid: %q", pidStr)
}
proc, err := os.FindProcess(pid)
if err != nil {
return true, fmt.Errorf("find process failed: %w", err)
}
if err := proc.Signal(syscall.SIGHUP); err != nil {
return true, fmt.Errorf("send SIGHUP failed: %w", err)
}
return true, nil
}