♻️ Refactor configuration change notification service
This commit is contained in:
@@ -12,10 +12,9 @@ void wakeupMainThread(void) {
|
||||
}
|
||||
|
||||
// The following three lines of code must run on the main thread.
|
||||
// For GUI applications (Wails, Cocoa), the framework handles this automatically.
|
||||
// For CLI applications, see README for manual event loop setup.
|
||||
// It must handle it using golang.design/x/mainthread.
|
||||
//
|
||||
// Inspired from: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m
|
||||
// inspired from here: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m
|
||||
void os_main(void) {
|
||||
[NSApplication sharedApplication];
|
||||
[NSApp disableRelaunchOnLogin];
|
||||
|
||||
@@ -36,6 +36,9 @@ type BackupService struct {
|
||||
isInitialized bool
|
||||
autoBackupTicker *time.Ticker
|
||||
autoBackupStop chan bool
|
||||
|
||||
// 配置观察者取消函数
|
||||
cancelObserver CancelFunc
|
||||
}
|
||||
|
||||
// NewBackupService 创建新的备份服务实例
|
||||
@@ -48,12 +51,25 @@ func NewBackupService(configService *ConfigService, dbService *DatabaseService,
|
||||
}
|
||||
|
||||
func (ds *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
ds.cancelObserver = ds.configService.Watch("backup", ds.onBackupConfigChange)
|
||||
|
||||
if err := ds.Initialize(); err != nil {
|
||||
return fmt.Errorf("initializing backup service: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onBackupConfigChange 备份配置变更回调
|
||||
func (ds *BackupService) onBackupConfigChange(oldValue, newValue interface{}) {
|
||||
// 重新加载配置
|
||||
config, err := ds.configService.GetConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 处理配置变更
|
||||
_ = ds.HandleConfigChange(&config.Backup)
|
||||
}
|
||||
|
||||
// Initialize 初始化备份服务
|
||||
func (s *BackupService) Initialize() error {
|
||||
config, repoPath, err := s.getConfigAndPath()
|
||||
@@ -393,5 +409,9 @@ func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error
|
||||
|
||||
// ServiceShutdown 服务关闭时的清理工作
|
||||
func (s *BackupService) ServiceShutdown() {
|
||||
// 取消配置观察者
|
||||
if s.cancelObserver != nil {
|
||||
s.cancelObserver()
|
||||
}
|
||||
s.StopAutoBackup()
|
||||
}
|
||||
|
||||
@@ -1,483 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// ConfigChangeType 配置变更类型
|
||||
type ConfigChangeType string
|
||||
|
||||
const (
|
||||
// ConfigChangeTypeHotkey 热键配置变更
|
||||
ConfigChangeTypeHotkey ConfigChangeType = "hotkey"
|
||||
// ConfigChangeTypeDataPath 数据路径配置变更
|
||||
ConfigChangeTypeDataPath ConfigChangeType = "datapath"
|
||||
// ConfigChangeTypeBackup 备份配置变更
|
||||
ConfigChangeTypeBackup ConfigChangeType = "backup"
|
||||
// ConfigChangeTypeWindowSnap 窗口吸附配置变更
|
||||
ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap"
|
||||
)
|
||||
|
||||
// ConfigChangeCallback 配置变更回调函数类型
|
||||
type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error
|
||||
|
||||
// ConfigListener 配置监听器
|
||||
type ConfigListener struct {
|
||||
ID string // 监听器唯一ID
|
||||
Name string // 监听器名称
|
||||
ChangeType ConfigChangeType // 监听的配置变更类型
|
||||
Callback ConfigChangeCallback // 回调函数(现在包含新旧配置)
|
||||
DebounceDelay time.Duration // 防抖延迟时间
|
||||
GetConfigFunc func(*koanf.Koanf) *models.AppConfig // 获取相关配置的函数
|
||||
|
||||
// 内部状态
|
||||
mu sync.RWMutex // 监听器状态锁
|
||||
timer *time.Timer // 防抖定时器
|
||||
lastConfigHash string // 上次配置的哈希值,用于变更检测
|
||||
lastConfig *models.AppConfig // 上次的配置副本
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// ConfigNotificationService 配置通知服务
|
||||
type ConfigNotificationService struct {
|
||||
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
|
||||
mu sync.RWMutex // 监听器map的读写锁
|
||||
logger *log.LogService // 日志服务
|
||||
koanf *koanf.Koanf // koanf实例
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewConfigNotificationService 创建配置通知服务
|
||||
func NewConfigNotificationService(k *koanf.Koanf, logger *log.LogService) *ConfigNotificationService {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &ConfigNotificationService{
|
||||
listeners: make(map[ConfigChangeType][]*ConfigListener),
|
||||
logger: logger,
|
||||
koanf: k,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterListener 注册配置监听器
|
||||
func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error {
|
||||
// 生成唯一ID如果没有提供
|
||||
if listener.ID == "" {
|
||||
listener.ID = fmt.Sprintf("%s_%d", listener.Name, time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// 初始化新监听器
|
||||
listener.ctx, listener.cancel = context.WithCancel(cns.ctx)
|
||||
if err := cns.initializeListenerState(listener); err != nil {
|
||||
listener.cancel()
|
||||
return fmt.Errorf("failed to initialize listener state: %w", err)
|
||||
}
|
||||
|
||||
// 添加到监听器列表
|
||||
cns.mu.Lock()
|
||||
cns.listeners[listener.ChangeType] = append(cns.listeners[listener.ChangeType], listener)
|
||||
cns.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeListenerState 初始化监听器状态
|
||||
func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigListener) error {
|
||||
if listener.GetConfigFunc == nil {
|
||||
return fmt.Errorf("GetConfigFunc is required")
|
||||
}
|
||||
|
||||
if config := listener.GetConfigFunc(cns.koanf); config != nil {
|
||||
listener.mu.Lock()
|
||||
listener.lastConfig = deepCopyConfigReflect(config)
|
||||
listener.lastConfigHash = computeConfigHash(config)
|
||||
listener.mu.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterListener 注销指定ID的配置监听器
|
||||
func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType, listenerID string) {
|
||||
cns.mu.Lock()
|
||||
defer cns.mu.Unlock()
|
||||
|
||||
listeners := cns.listeners[changeType]
|
||||
for i, listener := range listeners {
|
||||
if listener.ID == listenerID {
|
||||
// 取消监听器
|
||||
listener.cancel()
|
||||
// 从切片中移除
|
||||
cns.listeners[changeType] = append(listeners[:i], listeners[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果该类型没有监听器了,删除整个条目
|
||||
if len(cns.listeners[changeType]) == 0 {
|
||||
delete(cns.listeners, changeType)
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterAllListeners 注销指定类型的所有监听器
|
||||
func (cns *ConfigNotificationService) UnregisterAllListeners(changeType ConfigChangeType) {
|
||||
cns.mu.Lock()
|
||||
defer cns.mu.Unlock()
|
||||
|
||||
if listeners, exists := cns.listeners[changeType]; exists {
|
||||
for _, listener := range listeners {
|
||||
listener.cancel()
|
||||
}
|
||||
delete(cns.listeners, changeType)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckConfigChanges 检查配置变更并通知相关监听器
|
||||
func (cns *ConfigNotificationService) CheckConfigChanges() {
|
||||
cns.mu.RLock()
|
||||
allListeners := make(map[ConfigChangeType][]*ConfigListener)
|
||||
for changeType, listeners := range cns.listeners {
|
||||
// 创建监听器切片的副本以避免并发访问问题
|
||||
listenersCopy := make([]*ConfigListener, len(listeners))
|
||||
copy(listenersCopy, listeners)
|
||||
allListeners[changeType] = listenersCopy
|
||||
}
|
||||
cns.mu.RUnlock()
|
||||
|
||||
// 检查所有监听器
|
||||
for _, listeners := range allListeners {
|
||||
for _, listener := range listeners {
|
||||
cns.checkAndNotify(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndNotify 检查配置变更并通知
|
||||
func (cns *ConfigNotificationService) checkAndNotify(listener *ConfigListener) {
|
||||
if listener.GetConfigFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
currentConfig := listener.GetConfigFunc(cns.koanf)
|
||||
|
||||
listener.mu.RLock()
|
||||
lastHash := listener.lastConfigHash
|
||||
lastConfig := listener.lastConfig
|
||||
listener.mu.RUnlock()
|
||||
|
||||
var hasChanges bool
|
||||
var currentHash string
|
||||
|
||||
if currentConfig != nil {
|
||||
currentHash = computeConfigHash(currentConfig)
|
||||
hasChanges = currentHash != lastHash
|
||||
} else {
|
||||
hasChanges = lastConfig != nil
|
||||
}
|
||||
|
||||
if hasChanges {
|
||||
listener.mu.Lock()
|
||||
listener.lastConfig = deepCopyConfigReflect(currentConfig)
|
||||
listener.lastConfigHash = currentHash
|
||||
listener.mu.Unlock()
|
||||
|
||||
cns.debounceNotify(listener, lastConfig, currentConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// computeConfigHash 计算配置的哈希值
|
||||
func computeConfigHash(config *models.AppConfig) string {
|
||||
if config == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%p", config)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(jsonBytes)
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
|
||||
// deepCopyConfigReflect 使用反射实现高效深拷贝
|
||||
func deepCopyConfigReflect(src *models.AppConfig) *models.AppConfig {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用反射进行深拷贝
|
||||
srcValue := reflect.ValueOf(src).Elem()
|
||||
dstValue := reflect.New(srcValue.Type()).Elem()
|
||||
|
||||
deepCopyValue(srcValue, dstValue)
|
||||
|
||||
return dstValue.Addr().Interface().(*models.AppConfig)
|
||||
}
|
||||
|
||||
// deepCopyValue 递归深拷贝reflect.Value
|
||||
func deepCopyValue(src, dst reflect.Value) {
|
||||
switch src.Kind() {
|
||||
case reflect.Ptr:
|
||||
if src.IsNil() {
|
||||
return
|
||||
}
|
||||
dst.Set(reflect.New(src.Elem().Type()))
|
||||
deepCopyValue(src.Elem(), dst.Elem())
|
||||
|
||||
case reflect.Struct:
|
||||
for i := 0; i < src.NumField(); i++ {
|
||||
if dst.Field(i).CanSet() {
|
||||
deepCopyValue(src.Field(i), dst.Field(i))
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
if src.IsNil() {
|
||||
return
|
||||
}
|
||||
dst.Set(reflect.MakeSlice(src.Type(), src.Len(), src.Cap()))
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
deepCopyValue(src.Index(i), dst.Index(i))
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
if src.IsNil() {
|
||||
return
|
||||
}
|
||||
dst.Set(reflect.MakeMap(src.Type()))
|
||||
for _, key := range src.MapKeys() {
|
||||
srcValue := src.MapIndex(key)
|
||||
dstValue := reflect.New(srcValue.Type()).Elem()
|
||||
deepCopyValue(srcValue, dstValue)
|
||||
dst.SetMapIndex(key, dstValue)
|
||||
}
|
||||
|
||||
case reflect.Interface:
|
||||
if src.IsNil() {
|
||||
return
|
||||
}
|
||||
srcValue := src.Elem()
|
||||
dstValue := reflect.New(srcValue.Type()).Elem()
|
||||
deepCopyValue(srcValue, dstValue)
|
||||
dst.Set(dstValue)
|
||||
|
||||
case reflect.Array:
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
deepCopyValue(src.Index(i), dst.Index(i))
|
||||
}
|
||||
|
||||
default:
|
||||
// 对于基本类型和string,直接赋值
|
||||
if dst.CanSet() {
|
||||
dst.Set(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// debounceNotify 防抖通知
|
||||
func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) {
|
||||
listener.mu.Lock()
|
||||
defer listener.mu.Unlock()
|
||||
|
||||
// 取消之前的定时器
|
||||
if listener.timer != nil {
|
||||
listener.timer.Stop()
|
||||
}
|
||||
|
||||
// 创建配置副本,避免在闭包中持有原始引用
|
||||
oldConfigCopy := deepCopyConfigReflect(oldConfig)
|
||||
newConfigCopy := deepCopyConfigReflect(newConfig)
|
||||
|
||||
changeType := listener.ChangeType
|
||||
|
||||
listener.timer = time.AfterFunc(listener.DebounceDelay, func() {
|
||||
cns.executeCallback(listener.ctx, changeType, listener.Callback, oldConfigCopy, newConfigCopy)
|
||||
})
|
||||
}
|
||||
|
||||
// executeCallback 执行回调函数
|
||||
func (cns *ConfigNotificationService) executeCallback(
|
||||
ctx context.Context,
|
||||
changeType ConfigChangeType,
|
||||
callback ConfigChangeCallback,
|
||||
oldConfig, newConfig *models.AppConfig,
|
||||
) {
|
||||
cns.wg.Add(1)
|
||||
go func() {
|
||||
defer cns.wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
cns.logger.Error("config callback panic", "error", r)
|
||||
}
|
||||
}()
|
||||
|
||||
callbackCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- callback(changeType, oldConfig, newConfig)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-callbackCtx.Done():
|
||||
cns.logger.Error("config callback timeout")
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
cns.logger.Error("config callback error", "error", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Cleanup 清理所有监听器
|
||||
func (cns *ConfigNotificationService) Cleanup() {
|
||||
cns.cancel() // 取消所有context
|
||||
|
||||
cns.mu.Lock()
|
||||
for changeType, listeners := range cns.listeners {
|
||||
for _, listener := range listeners {
|
||||
listener.cancel()
|
||||
}
|
||||
delete(cns.listeners, changeType)
|
||||
}
|
||||
cns.mu.Unlock()
|
||||
|
||||
cns.wg.Wait() // 等待所有协程完成
|
||||
}
|
||||
|
||||
// GetListeners 获取指定类型的所有监听器
|
||||
func (cns *ConfigNotificationService) GetListeners(changeType ConfigChangeType) []*ConfigListener {
|
||||
cns.mu.RLock()
|
||||
defer cns.mu.RUnlock()
|
||||
|
||||
listeners := cns.listeners[changeType]
|
||||
result := make([]*ConfigListener, len(listeners))
|
||||
copy(result, listeners)
|
||||
return result
|
||||
}
|
||||
|
||||
// CreateHotkeyListener 创建热键配置监听器
|
||||
func CreateHotkeyListener(name string, callback func(enable bool, hotkey *models.HotkeyCombo) error) *ConfigListener {
|
||||
return &ConfigListener{
|
||||
Name: name,
|
||||
ChangeType: ConfigChangeTypeHotkey,
|
||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||
if newConfig != nil {
|
||||
return callback(newConfig.General.EnableGlobalHotkey, &newConfig.General.GlobalHotkey)
|
||||
}
|
||||
// 使用默认配置
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
return callback(defaultConfig.General.EnableGlobalHotkey, &defaultConfig.General.GlobalHotkey)
|
||||
},
|
||||
DebounceDelay: 200 * time.Millisecond,
|
||||
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
|
||||
var config models.AppConfig
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &config
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDataPathListener 创建数据路径配置监听器
|
||||
func CreateDataPathListener(name string, callback func() error) *ConfigListener {
|
||||
return &ConfigListener{
|
||||
Name: name,
|
||||
ChangeType: ConfigChangeTypeDataPath,
|
||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||
var oldPath, newPath string
|
||||
|
||||
if oldConfig != nil {
|
||||
oldPath = oldConfig.General.DataPath
|
||||
}
|
||||
|
||||
if newConfig != nil {
|
||||
newPath = newConfig.General.DataPath
|
||||
} else {
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
newPath = defaultConfig.General.DataPath
|
||||
}
|
||||
|
||||
if oldPath != newPath {
|
||||
return callback()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DebounceDelay: 100 * time.Millisecond,
|
||||
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
|
||||
var config models.AppConfig
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &config
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateBackupConfigListener 创建备份配置监听器
|
||||
func CreateBackupConfigListener(name string, callback func(config *models.GitBackupConfig) error) *ConfigListener {
|
||||
return &ConfigListener{
|
||||
Name: name,
|
||||
ChangeType: ConfigChangeTypeBackup,
|
||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||
if newConfig == nil {
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
return callback(&defaultConfig.Backup)
|
||||
}
|
||||
return callback(&newConfig.Backup)
|
||||
},
|
||||
DebounceDelay: 200 * time.Millisecond,
|
||||
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
|
||||
var config models.AppConfig
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &config
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWindowSnapConfigListener 创建窗口吸附配置监听器
|
||||
func CreateWindowSnapConfigListener(name string, callback func(enabled bool) error) *ConfigListener {
|
||||
return &ConfigListener{
|
||||
Name: name,
|
||||
ChangeType: ConfigChangeTypeWindowSnap,
|
||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||
if newConfig == nil {
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
return callback(defaultConfig.General.EnableWindowSnap)
|
||||
}
|
||||
return callback(newConfig.General.EnableWindowSnap)
|
||||
},
|
||||
DebounceDelay: 200 * time.Millisecond,
|
||||
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
|
||||
var config models.AppConfig
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &config
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (cns *ConfigNotificationService) ServiceShutdown() error {
|
||||
cns.Cleanup()
|
||||
return nil
|
||||
}
|
||||
243
internal/services/config_observer.go
Normal file
243
internal/services/config_observer.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// ObserverCallback 观察者回调函数
|
||||
// 参数:
|
||||
// - oldValue: 配置变更前的值
|
||||
// - newValue: 配置变更后的值
|
||||
type ObserverCallback func(oldValue, newValue interface{})
|
||||
|
||||
// CancelFunc 取消订阅函数
|
||||
// 调用此函数可以取消对配置的监听
|
||||
type CancelFunc func()
|
||||
|
||||
// observer 内部观察者结构
|
||||
type observer struct {
|
||||
id string // 唯一ID
|
||||
path string // 监听的配置路径
|
||||
callback ObserverCallback // 回调函数
|
||||
}
|
||||
|
||||
// ConfigObserver 配置观察者系统
|
||||
// 提供轻量级的配置变更监听机制
|
||||
type ConfigObserver struct {
|
||||
observers map[string][]*observer // 路径 -> 观察者列表
|
||||
observerMu sync.RWMutex // 观察者锁
|
||||
nextObserverID atomic.Uint64 // 观察者ID生成器
|
||||
workerPool chan struct{} // Goroutine 池,限制并发数
|
||||
logger *log.LogService // 日志服务
|
||||
ctx context.Context // 全局 context
|
||||
cancel context.CancelFunc // 取消函数
|
||||
wg sync.WaitGroup // 等待组,用于优雅关闭
|
||||
}
|
||||
|
||||
// NewConfigObserver 创建新的配置观察者系统
|
||||
func NewConfigObserver(logger *log.LogService) *ConfigObserver {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &ConfigObserver{
|
||||
observers: make(map[string][]*observer),
|
||||
workerPool: make(chan struct{}, 100), // 限制最多100个并发回调
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Watch 注册配置变更监听器
|
||||
// 参数:
|
||||
// - path: 配置路径,如 "general.enableGlobalHotkey"
|
||||
// - callback: 变更回调函数,接收旧值和新值
|
||||
//
|
||||
// 返回:
|
||||
// - CancelFunc: 取消监听的函数,务必在不需要时调用以避免内存泄漏
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// cancel := observer.Watch("general.hotkey", func(old, new interface{}) {
|
||||
// fmt.Printf("配置从 %v 变更为 %v\n", old, new)
|
||||
// })
|
||||
// defer cancel() // 确保清理
|
||||
func (co *ConfigObserver) Watch(path string, callback ObserverCallback) CancelFunc {
|
||||
// 生成唯一ID
|
||||
id := fmt.Sprintf("obs_%d", co.nextObserverID.Add(1))
|
||||
|
||||
obs := &observer{
|
||||
id: id,
|
||||
path: path,
|
||||
callback: callback,
|
||||
}
|
||||
|
||||
// 添加到观察者列表
|
||||
co.observerMu.Lock()
|
||||
co.observers[path] = append(co.observers[path], obs)
|
||||
co.observerMu.Unlock()
|
||||
|
||||
// 返回取消函数
|
||||
return func() {
|
||||
co.removeObserver(path, id)
|
||||
}
|
||||
}
|
||||
|
||||
// WatchWithContext 使用 Context 注册监听器,Context 取消时自动清理
|
||||
// 参数:
|
||||
// - ctx: Context,取消时自动移除观察者
|
||||
// - path: 配置路径
|
||||
// - callback: 变更回调函数
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// defer cancel()
|
||||
// observer.WatchWithContext(ctx, "general.hotkey", callback)
|
||||
// // Context 取消时自动清理
|
||||
func (co *ConfigObserver) WatchWithContext(ctx context.Context, path string, callback ObserverCallback) {
|
||||
cancel := co.Watch(path, callback)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
case <-co.ctx.Done():
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// removeObserver 移除观察者
|
||||
func (co *ConfigObserver) removeObserver(path, id string) {
|
||||
co.observerMu.Lock()
|
||||
defer co.observerMu.Unlock()
|
||||
|
||||
observers := co.observers[path]
|
||||
for i, obs := range observers {
|
||||
if obs.id == id {
|
||||
// 从切片中移除
|
||||
co.observers[path] = append(observers[:i], observers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有观察者了,删除整个条目
|
||||
if len(co.observers[path]) == 0 {
|
||||
delete(co.observers, path)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify 通知指定路径的所有观察者
|
||||
// 参数:
|
||||
// - path: 配置路径
|
||||
// - oldValue: 旧值
|
||||
// - newValue: 新值
|
||||
//
|
||||
// 注意:此方法会在独立的 goroutine 中异步执行回调,不会阻塞调用者
|
||||
func (co *ConfigObserver) Notify(path string, oldValue, newValue interface{}) {
|
||||
// 获取该路径的所有观察者(拷贝以避免并发问题)
|
||||
co.observerMu.RLock()
|
||||
observers := co.observers[path]
|
||||
if len(observers) == 0 {
|
||||
co.observerMu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 拷贝观察者列表
|
||||
callbacks := make([]ObserverCallback, len(observers))
|
||||
for i, obs := range observers {
|
||||
callbacks[i] = obs.callback
|
||||
}
|
||||
co.observerMu.RUnlock()
|
||||
|
||||
// 在独立 goroutine 中执行回调
|
||||
for _, callback := range callbacks {
|
||||
co.executeCallback(callback, oldValue, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyAll 通知所有匹配前缀的观察者
|
||||
func (co *ConfigObserver) NotifyAll(changes map[string]struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
}) {
|
||||
for path, change := range changes {
|
||||
co.Notify(path, change.OldValue, change.NewValue)
|
||||
}
|
||||
}
|
||||
|
||||
// executeCallback 执行回调函数
|
||||
func (co *ConfigObserver) executeCallback(callback ObserverCallback, oldValue, newValue interface{}) {
|
||||
co.wg.Add(1)
|
||||
|
||||
// 获取 worker(限制并发数)
|
||||
select {
|
||||
case co.workerPool <- struct{}{}:
|
||||
// 成功获取 worker
|
||||
case <-co.ctx.Done():
|
||||
// 系统正在关闭
|
||||
co.wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer co.wg.Done()
|
||||
defer func() { <-co.workerPool }() // 释放 worker
|
||||
|
||||
// Panic 恢复
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
|
||||
// 创建带超时的 context
|
||||
ctx, cancel := context.WithTimeout(co.ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 在 channel 中执行回调,以便可以超时控制
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
callback(oldValue, newValue)
|
||||
}()
|
||||
|
||||
// 等待完成或超时
|
||||
select {
|
||||
case <-done:
|
||||
// 正常完成
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Clear 清空所有观察者
|
||||
func (co *ConfigObserver) Clear() {
|
||||
co.observerMu.Lock()
|
||||
co.observers = make(map[string][]*observer)
|
||||
co.observerMu.Unlock()
|
||||
|
||||
}
|
||||
|
||||
// Shutdown 关闭观察者系统
|
||||
// 等待所有正在执行的回调完成
|
||||
func (co *ConfigObserver) Shutdown() {
|
||||
// 取消 context
|
||||
co.cancel()
|
||||
|
||||
// 等待所有回调完成
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
co.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
}
|
||||
// 清空所有观察者
|
||||
co.Clear()
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
@@ -25,36 +27,12 @@ type ConfigService struct {
|
||||
mu sync.RWMutex // 读写锁
|
||||
fileProvider *file.File // 文件提供器,用于监听
|
||||
|
||||
// 配置通知服务
|
||||
notificationService *ConfigNotificationService
|
||||
observer *ConfigObserver
|
||||
|
||||
// 配置迁移器
|
||||
configMigrator *ConfigMigrator
|
||||
}
|
||||
|
||||
// ConfigError 配置错误
|
||||
type ConfigError struct {
|
||||
Operation string // 操作名称
|
||||
Err error // 原始错误
|
||||
}
|
||||
|
||||
// Error 实现error接口
|
||||
func (e *ConfigError) Error() string {
|
||||
return fmt.Sprintf("config error during %s: %v", e.Operation, e.Err)
|
||||
}
|
||||
|
||||
// Unwrap 获取原始错误
|
||||
func (e *ConfigError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Is 实现错误匹配
|
||||
func (e *ConfigError) Is(target error) bool {
|
||||
var configError *ConfigError
|
||||
ok := errors.As(target, &configError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// NewConfigService 创建新的配置服务实例
|
||||
func NewConfigService(logger *log.LogService) *ConfigService {
|
||||
// 获取用户主目录
|
||||
@@ -74,8 +52,8 @@ func NewConfigService(logger *log.LogService) *ConfigService {
|
||||
koanf: koanf.New("."),
|
||||
}
|
||||
|
||||
// 初始化配置通知服务
|
||||
cs.notificationService = NewConfigNotificationService(cs.koanf, logger)
|
||||
// 初始化配置观察者系统
|
||||
cs.observer = NewConfigObserver(logger)
|
||||
|
||||
// 初始化配置迁移器
|
||||
cs.configMigrator = NewConfigMigrator(logger, configDir, "settings", settingsPath)
|
||||
@@ -93,7 +71,7 @@ func (cs *ConfigService) setDefaults() error {
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
|
||||
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||
return &ConfigError{Operation: "load_defaults", Err: err}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -112,7 +90,7 @@ func (cs *ConfigService) initConfig() error {
|
||||
// 配置文件存在,直接加载现有配置
|
||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
return &ConfigError{Operation: "load_config_file", Err: err}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -145,7 +123,7 @@ func (cs *ConfigService) MigrateConfig() error {
|
||||
func (cs *ConfigService) createDefaultConfig() error {
|
||||
// 确保配置目录存在
|
||||
if err := os.MkdirAll(cs.configDir, 0755); err != nil {
|
||||
return &ConfigError{Operation: "create_config_dir", Err: err}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
@@ -160,7 +138,7 @@ func (cs *ConfigService) createDefaultConfig() error {
|
||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
return &ConfigError{Operation: "load_config_file", Err: err}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -170,21 +148,20 @@ func (cs *ConfigService) startWatching() {
|
||||
if cs.fileProvider == nil {
|
||||
return
|
||||
}
|
||||
err := cs.fileProvider.Watch(func(event interface{}, err error) {
|
||||
cs.fileProvider.Watch(func(event interface{}, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cs.koanf.Load(cs.fileProvider, jsonparser.Parser())
|
||||
|
||||
// 使用配置通知服务检查所有已注册的配置变更
|
||||
if cs.notificationService != nil {
|
||||
cs.notificationService.CheckConfigChanges()
|
||||
}
|
||||
cs.mu.Lock()
|
||||
oldSnapshot := cs.createConfigSnapshot()
|
||||
cs.koanf.Load(cs.fileProvider, jsonparser.Parser())
|
||||
cs.mu.Unlock()
|
||||
|
||||
// 检测配置变更并通知观察者
|
||||
cs.detectAndNotifyChanges(oldSnapshot)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
cs.logger.Error("Failed to setup config file watcher", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// stopWatching 停止配置文件监听
|
||||
@@ -201,7 +178,7 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) {
|
||||
|
||||
var config models.AppConfig
|
||||
if err := cs.koanf.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
||||
return nil, &ConfigError{Operation: "unmarshal_config", Err: err}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
@@ -210,7 +187,9 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) {
|
||||
// Set 设置配置项
|
||||
func (cs *ConfigService) Set(key string, value interface{}) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 获取旧值
|
||||
oldValue := cs.koanf.Get(key)
|
||||
|
||||
// 设置值到koanf
|
||||
cs.koanf.Set(key, value)
|
||||
@@ -219,7 +198,18 @@ func (cs *ConfigService) Set(key string, value interface{}) error {
|
||||
cs.koanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
||||
|
||||
// 将配置写回文件
|
||||
return cs.writeConfigToFile()
|
||||
err := cs.writeConfigToFile()
|
||||
cs.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cs.observer != nil {
|
||||
cs.observer.Notify(key, oldValue, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取配置项
|
||||
@@ -232,7 +222,9 @@ func (cs *ConfigService) Get(key string) interface{} {
|
||||
// ResetConfig 强制重置所有配置为默认值
|
||||
func (cs *ConfigService) ResetConfig() error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 保存旧配置快照
|
||||
oldSnapshot := cs.createConfigSnapshot()
|
||||
|
||||
// 停止文件监听
|
||||
if cs.fileProvider != nil {
|
||||
@@ -242,12 +234,14 @@ func (cs *ConfigService) ResetConfig() error {
|
||||
|
||||
// 设置默认配置
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
return &ConfigError{Operation: "reset_set_defaults", Err: err}
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
if err := cs.writeConfigToFile(); err != nil {
|
||||
return &ConfigError{Operation: "reset_write_config", Err: err}
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// 重新创建koanf实例
|
||||
@@ -255,7 +249,8 @@ func (cs *ConfigService) ResetConfig() error {
|
||||
|
||||
// 重新加载默认配置到koanf
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
return &ConfigError{Operation: "reset_reload_defaults", Err: err}
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// 重新创建文件提供器
|
||||
@@ -263,16 +258,17 @@ func (cs *ConfigService) ResetConfig() error {
|
||||
|
||||
// 重新加载配置文件
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
return &ConfigError{Operation: "reset_reload_config", Err: err}
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
cs.mu.Unlock()
|
||||
|
||||
// 重新启动文件监听
|
||||
cs.startWatching()
|
||||
|
||||
// 手动触发配置变更检查,确保通知系统能感知到变更
|
||||
if cs.notificationService != nil {
|
||||
cs.notificationService.CheckConfigChanges()
|
||||
}
|
||||
// 检测配置变更并通知观察者
|
||||
cs.detectAndNotifyChanges(oldSnapshot)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -281,71 +277,116 @@ func (cs *ConfigService) ResetConfig() error {
|
||||
func (cs *ConfigService) writeConfigToFile() error {
|
||||
configBytes, err := cs.koanf.Marshal(jsonparser.Parser())
|
||||
if err != nil {
|
||||
return &ConfigError{Operation: "marshal_config", Err: err}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cs.settingsPath, configBytes, 0644); err != nil {
|
||||
return &ConfigError{Operation: "write_config_file", Err: err}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetHotkeyChangeCallback 设置热键配置变更回调
|
||||
func (cs *ConfigService) SetHotkeyChangeCallback(callback func(enable bool, hotkey *models.HotkeyCombo) error) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 创建热键监听器并注册
|
||||
hotkeyListener := CreateHotkeyListener("DefaultHotkeyListener", callback)
|
||||
return cs.notificationService.RegisterListener(hotkeyListener)
|
||||
// Watch 注册配置变更监听器
|
||||
func (cs *ConfigService) Watch(path string, callback ObserverCallback) CancelFunc {
|
||||
return cs.observer.Watch(path, callback)
|
||||
}
|
||||
|
||||
// SetDataPathChangeCallback 设置数据路径配置变更回调
|
||||
func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 创建数据路径监听器并注册
|
||||
dataPathListener := CreateDataPathListener("DefaultDataPathListener", callback)
|
||||
return cs.notificationService.RegisterListener(dataPathListener)
|
||||
// WatchWithContext 使用 Context 注册监听器
|
||||
func (cs *ConfigService) WatchWithContext(ctx context.Context, path string, callback ObserverCallback) {
|
||||
cs.observer.WatchWithContext(ctx, path, callback)
|
||||
}
|
||||
|
||||
// SetBackupConfigChangeCallback 设置备份配置变更回调
|
||||
func (cs *ConfigService) SetBackupConfigChangeCallback(callback func(config *models.GitBackupConfig) error) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
// createConfigSnapshot 创建当前配置的快照
|
||||
func (cs *ConfigService) createConfigSnapshot() map[string]interface{} {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
snapshot := make(map[string]interface{})
|
||||
allKeys := cs.koanf.All()
|
||||
|
||||
// 创建备份配置监听器并注册
|
||||
backupListener := CreateBackupConfigListener("DefaultBackupConfigListener", callback)
|
||||
return cs.notificationService.RegisterListener(backupListener)
|
||||
// 递归展平配置
|
||||
flattenMap("", allKeys, snapshot)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
|
||||
func (cs *ConfigService) SetWindowSnapConfigChangeCallback(callback func(enabled bool) error) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
// flattenMap 递归展平嵌套的 map(使用 strings.Builder 优化字符串拼接)
|
||||
func flattenMap(prefix string, data map[string]interface{}, result map[string]interface{}) {
|
||||
var builder strings.Builder
|
||||
for key, value := range data {
|
||||
builder.Reset()
|
||||
if prefix != "" {
|
||||
builder.WriteString(prefix)
|
||||
builder.WriteString(".")
|
||||
}
|
||||
builder.WriteString(key)
|
||||
fullKey := builder.String()
|
||||
|
||||
// 创建窗口吸附配置监听器并注册
|
||||
windowSnapListener := CreateWindowSnapConfigListener("DefaultWindowSnapConfigListener", callback)
|
||||
return cs.notificationService.RegisterListener(windowSnapListener)
|
||||
if valueMap, ok := value.(map[string]interface{}); ok {
|
||||
// 递归处理嵌套 map
|
||||
flattenMap(fullKey, valueMap, result)
|
||||
} else {
|
||||
// 保存叶子节点
|
||||
result[fullKey] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detectAndNotifyChanges 检测配置变更并通知观察者
|
||||
func (cs *ConfigService) detectAndNotifyChanges(oldSnapshot map[string]interface{}) {
|
||||
// 创建新快照
|
||||
newSnapshot := cs.createConfigSnapshot()
|
||||
|
||||
// 检测变更
|
||||
changes := make(map[string]struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
})
|
||||
|
||||
// 检查新增和修改的键
|
||||
for key, newValue := range newSnapshot {
|
||||
oldValue, exists := oldSnapshot[key]
|
||||
if !exists || !isEqual(oldValue, newValue) {
|
||||
changes[key] = struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
}{
|
||||
OldValue: oldValue,
|
||||
NewValue: newValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查删除的键
|
||||
for key, oldValue := range oldSnapshot {
|
||||
if _, exists := newSnapshot[key]; !exists {
|
||||
changes[key] = struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
}{
|
||||
OldValue: oldValue,
|
||||
NewValue: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知所有变更
|
||||
if cs.observer != nil && len(changes) > 0 {
|
||||
cs.observer.NotifyAll(changes)
|
||||
}
|
||||
}
|
||||
|
||||
// isEqual 值相等比较
|
||||
func isEqual(a, b interface{}) bool {
|
||||
aJSON, _ := json.Marshal(a)
|
||||
bJSON, _ := json.Marshal(b)
|
||||
return string(aJSON) == string(bJSON)
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (cs *ConfigService) ServiceShutdown() error {
|
||||
cs.stopWatching()
|
||||
if cs.notificationService != nil {
|
||||
cs.notificationService.Cleanup()
|
||||
if cs.observer != nil {
|
||||
cs.observer.Shutdown()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigDir 获取配置目录
|
||||
func (cs *ConfigService) GetConfigDir() string {
|
||||
return cs.configDir
|
||||
}
|
||||
|
||||
// GetSettingsPath 获取设置文件路径
|
||||
func (cs *ConfigService) GetSettingsPath() string {
|
||||
return cs.settingsPath
|
||||
}
|
||||
|
||||
@@ -96,6 +96,9 @@ type DatabaseService struct {
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
tableModels []TableModel // 注册的表模型
|
||||
|
||||
// 配置观察者取消函数
|
||||
cancelObserver CancelFunc
|
||||
}
|
||||
|
||||
// NewDatabaseService creates a new database service
|
||||
@@ -130,9 +133,29 @@ func (ds *DatabaseService) registerAllModels() {
|
||||
// ServiceStartup initializes the service when the application starts
|
||||
func (ds *DatabaseService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
ds.ctx = ctx
|
||||
|
||||
ds.cancelObserver = ds.configService.Watch("general.dataPath", ds.onDataPathChange)
|
||||
|
||||
return ds.initDatabase()
|
||||
}
|
||||
|
||||
// onDataPathChange 数据路径配置变更回调
|
||||
func (ds *DatabaseService) onDataPathChange(oldValue, newValue interface{}) {
|
||||
oldPath := ""
|
||||
newPath := ""
|
||||
|
||||
if oldValue != nil {
|
||||
oldPath = fmt.Sprintf("%v", oldValue)
|
||||
}
|
||||
if newValue != nil {
|
||||
newPath = fmt.Sprintf("%v", newValue)
|
||||
}
|
||||
|
||||
if oldPath != newPath {
|
||||
_ = ds.OnDataPathChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// initDatabase initializes the SQLite database
|
||||
func (ds *DatabaseService) initDatabase() error {
|
||||
dbPath, err := ds.getDatabasePath()
|
||||
@@ -369,6 +392,11 @@ func getSQLTypeAndDefault(t reflect.Type) (string, string) {
|
||||
|
||||
// ServiceShutdown shuts down the service when the application closes
|
||||
func (ds *DatabaseService) ServiceShutdown() error {
|
||||
// 取消配置观察者
|
||||
if ds.cancelObserver != nil {
|
||||
ds.cancelObserver()
|
||||
}
|
||||
|
||||
if ds.db != nil {
|
||||
return ds.db.Close()
|
||||
}
|
||||
|
||||
@@ -25,13 +25,14 @@ type HotkeyService struct {
|
||||
app *application.App
|
||||
registered atomic.Bool
|
||||
|
||||
hk *hotkey.Hotkey
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
hk *hotkey.Hotkey
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
isShutdown atomic.Bool
|
||||
|
||||
// 防抖相关
|
||||
lastTriggerTime atomic.Int64 // 上次触发时间(Unix 纳秒)
|
||||
debounceInterval time.Duration // 防抖间隔
|
||||
// 配置观察者取消函数
|
||||
cancelObservers []CancelFunc
|
||||
}
|
||||
|
||||
// NewHotkeyService 创建热键服务实例
|
||||
@@ -40,12 +41,13 @@ func NewHotkeyService(configService *ConfigService, logger *log.LogService) *Hot
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &HotkeyService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
windowHelper: NewWindowHelper(),
|
||||
stopChan: make(chan struct{}),
|
||||
debounceInterval: 100 * time.Millisecond,
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
windowHelper: NewWindowHelper(),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,50 +59,69 @@ func (hs *HotkeyService) ServiceStartup(ctx context.Context, options application
|
||||
|
||||
// Initialize 初始化热键服务
|
||||
func (hs *HotkeyService) Initialize() error {
|
||||
// 注册配置监听
|
||||
hs.cancelObservers = []CancelFunc{
|
||||
hs.configService.Watch("general.enableGlobalHotkey", hs.onHotkeyConfigChange),
|
||||
hs.configService.Watch("general.globalHotkey", hs.onHotkeyConfigChange),
|
||||
}
|
||||
|
||||
// 加载初始配置
|
||||
config, err := hs.configService.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
if config.General.EnableGlobalHotkey {
|
||||
if err := hs.RegisterHotkey(&config.General.GlobalHotkey); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = hs.RegisterHotkey(&config.General.GlobalHotkey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// onHotkeyConfigChange 热键配置变更回调
|
||||
func (hs *HotkeyService) onHotkeyConfigChange(oldValue, newValue interface{}) {
|
||||
// 重新加载配置
|
||||
config, err := hs.configService.GetConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新热键
|
||||
_ = hs.UpdateHotkey(config.General.EnableGlobalHotkey, &config.General.GlobalHotkey)
|
||||
}
|
||||
|
||||
// RegisterHotkey 注册全局热键
|
||||
func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error {
|
||||
if hs.isShutdown.Load() {
|
||||
return errors.New("service is shutdown")
|
||||
}
|
||||
|
||||
if !hs.isValidHotkey(combo) {
|
||||
return errors.New("invalid hotkey combination")
|
||||
}
|
||||
|
||||
// 如果已注册,先取消
|
||||
if hs.registered.Load() {
|
||||
_ = hs.UnregisterHotkey()
|
||||
}
|
||||
|
||||
// 转换为 hotkey 库的格式
|
||||
key, mods, err := hs.convertHotkey(combo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert hotkey: %w", err)
|
||||
}
|
||||
|
||||
// 创建新热键(在锁外创建,避免持锁时间过长)
|
||||
newHk := hotkey.New(mods, key)
|
||||
if err := newHk.Register(); err != nil {
|
||||
hs.mu.Lock()
|
||||
// 创建新的热键实例
|
||||
hs.hk = hotkey.New(mods, key)
|
||||
if err := hs.hk.Register(); err != nil {
|
||||
hs.mu.Unlock()
|
||||
return fmt.Errorf("register hotkey: %w", err)
|
||||
}
|
||||
|
||||
hs.mu.Lock()
|
||||
defer hs.mu.Unlock()
|
||||
|
||||
// 如果已注册,先取消旧热键
|
||||
if hs.registered.Load() {
|
||||
hs.unregisterInternal()
|
||||
}
|
||||
|
||||
// 设置新热键
|
||||
hs.hk = newHk
|
||||
hs.registered.Store(true)
|
||||
hs.currentHotkey = combo
|
||||
hs.mu.Unlock()
|
||||
|
||||
// 启动监听 goroutine
|
||||
hs.wg.Add(1)
|
||||
@@ -111,36 +132,42 @@ func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error {
|
||||
|
||||
// UnregisterHotkey 取消注册全局热键
|
||||
func (hs *HotkeyService) UnregisterHotkey() error {
|
||||
hs.mu.Lock()
|
||||
defer hs.mu.Unlock()
|
||||
return hs.unregisterInternal()
|
||||
}
|
||||
|
||||
// unregisterInternal 内部取消注册
|
||||
func (hs *HotkeyService) unregisterInternal() error {
|
||||
if !hs.registered.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 停止监听
|
||||
close(hs.stopChan)
|
||||
|
||||
// 取消注册热键
|
||||
if hs.hk != nil {
|
||||
if err := hs.hk.Unregister(); err != nil {
|
||||
hs.logger.Error("failed to unregister hotkey", "error", err)
|
||||
}
|
||||
hs.hk = nil
|
||||
}
|
||||
|
||||
// 等待 goroutine 结束
|
||||
hs.wg.Wait()
|
||||
|
||||
hs.currentHotkey = nil
|
||||
// 先标记为未注册
|
||||
hs.registered.Store(false)
|
||||
|
||||
// 重新创建 stopChan
|
||||
hs.stopChan = make(chan struct{})
|
||||
// 获取热键实例的引用
|
||||
hs.mu.RLock()
|
||||
hk := hs.hk
|
||||
hs.mu.RUnlock()
|
||||
|
||||
if hk == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 调用 Close() 确保完全清理
|
||||
_ = hk.Close()
|
||||
|
||||
// 等待监听 goroutine 退出
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
hs.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
|
||||
// 清理状态
|
||||
hs.mu.Lock()
|
||||
hs.hk = nil
|
||||
hs.currentHotkey = nil
|
||||
hs.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -157,26 +184,28 @@ func (hs *HotkeyService) UpdateHotkey(enable bool, combo *models.HotkeyCombo) er
|
||||
func (hs *HotkeyService) listenHotkey() {
|
||||
defer hs.wg.Done()
|
||||
|
||||
// 缓存 channel 引用,避免每次循环都访问 hs.hk
|
||||
// 获取热键实例和通道
|
||||
hs.mu.RLock()
|
||||
keydownChan := hs.hk.Keydown()
|
||||
hk := hs.hk
|
||||
hs.mu.RUnlock()
|
||||
|
||||
if hk == nil {
|
||||
return
|
||||
}
|
||||
|
||||
keydownChan := hk.Keydown()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-hs.stopChan:
|
||||
case <-hs.ctx.Done():
|
||||
return
|
||||
case <-keydownChan:
|
||||
now := time.Now().UnixNano()
|
||||
lastTrigger := hs.lastTriggerTime.Load()
|
||||
|
||||
// 如果距离上次触发时间小于防抖间隔,忽略此次触发
|
||||
if lastTrigger > 0 && time.Duration(now-lastTrigger) < hs.debounceInterval {
|
||||
continue
|
||||
case _, ok := <-keydownChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if hs.registered.Load() {
|
||||
hs.toggleWindow()
|
||||
}
|
||||
// 更新最后触发时间
|
||||
hs.lastTriggerTime.Store(now)
|
||||
hs.toggleWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,5 +357,18 @@ func (hs *HotkeyService) IsRegistered() bool {
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (hs *HotkeyService) ServiceShutdown() error {
|
||||
hs.isShutdown.Store(true)
|
||||
|
||||
// 取消配置观察者
|
||||
for _, cancel := range hs.cancelObservers {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// 取消 context
|
||||
hs.cancel()
|
||||
|
||||
// 取消注册热键
|
||||
return hs.UnregisterHotkey()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/dock"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
@@ -103,38 +101,6 @@ func NewServiceManager() *ServiceManager {
|
||||
// 初始化测试服务(开发环境使用)
|
||||
testService := NewTestService(badgeService, notificationService, logger)
|
||||
|
||||
// 使用新的配置通知系统设置热键配置变更监听
|
||||
err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||
return hotkeyService.UpdateHotkey(enable, hotkey)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 设置数据路径变更监听,处理配置重置和路径变更
|
||||
err = configService.SetDataPathChangeCallback(func() error {
|
||||
return databaseService.OnDataPathChanged()
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 设置备份配置变更监听,处理备份配置变更
|
||||
err = configService.SetBackupConfigChangeCallback(func(config *models.GitBackupConfig) error {
|
||||
return backupService.HandleConfigChange(config)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 设置窗口吸附配置变更回调
|
||||
err = configService.SetWindowSnapConfigChangeCallback(func(enabled bool) error {
|
||||
return windowSnapService.OnWindowSnapConfigChanged(enabled)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &ServiceManager{
|
||||
configService: configService,
|
||||
databaseService: databaseService,
|
||||
|
||||
@@ -53,6 +53,9 @@ type WindowSnapService struct {
|
||||
// 事件监听器清理函数
|
||||
mainMoveUnhook func() // 主窗口移动监听清理函数
|
||||
windowMoveUnhooks map[int64]func() // documentID -> 子窗口移动监听清理函数
|
||||
|
||||
// 配置观察者取消函数
|
||||
cancelObserver CancelFunc
|
||||
}
|
||||
|
||||
// NewWindowSnapService 创建新的窗口吸附服务实例
|
||||
@@ -69,7 +72,7 @@ func NewWindowSnapService(logger *log.LogService, configService *ConfigService)
|
||||
snapEnabled = config.General.EnableWindowSnap
|
||||
}
|
||||
|
||||
return &WindowSnapService{
|
||||
wss := &WindowSnapService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
windowHelper: NewWindowHelper(),
|
||||
@@ -83,6 +86,23 @@ func NewWindowSnapService(logger *log.LogService, configService *ConfigService)
|
||||
isUpdatingPosition: make(map[int64]bool),
|
||||
windowMoveUnhooks: make(map[int64]func()),
|
||||
}
|
||||
|
||||
// 注册窗口吸附配置监听
|
||||
wss.cancelObserver = configService.Watch("general.enableWindowSnap", wss.onWindowSnapConfigChange)
|
||||
|
||||
return wss
|
||||
}
|
||||
|
||||
// onWindowSnapConfigChange 窗口吸附配置变更回调
|
||||
func (wss *WindowSnapService) onWindowSnapConfigChange(oldValue, newValue interface{}) {
|
||||
enabled := false
|
||||
if newValue != nil {
|
||||
if val, ok := newValue.(bool); ok {
|
||||
enabled = val
|
||||
}
|
||||
}
|
||||
|
||||
_ = wss.OnWindowSnapConfigChanged(enabled)
|
||||
}
|
||||
|
||||
// RegisterWindow 注册需要吸附管理的窗口
|
||||
@@ -194,7 +214,6 @@ func (wss *WindowSnapService) GetCurrentThreshold() int {
|
||||
// OnWindowSnapConfigChanged 处理窗口吸附配置变更
|
||||
func (wss *WindowSnapService) OnWindowSnapConfigChanged(enabled bool) error {
|
||||
wss.SetSnapEnabled(enabled)
|
||||
// 阈值现在是自适应的,无需手动设置
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -625,6 +644,10 @@ func (wss *WindowSnapService) Cleanup() {
|
||||
|
||||
// ServiceShutdown 实现服务关闭接口
|
||||
func (wss *WindowSnapService) ServiceShutdown() error {
|
||||
// 取消配置观察者
|
||||
if wss.cancelObserver != nil {
|
||||
wss.cancelObserver()
|
||||
}
|
||||
wss.Cleanup()
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user