♻️ Refactor backup service complete.
Some checks failed
CodeQL Advanced / Analyze (go) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (c-cpp) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
CodeQL Advanced / Analyze (rust) (push) Has been cancelled
Some checks failed
CodeQL Advanced / Analyze (go) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (c-cpp) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
CodeQL Advanced / Analyze (rust) (push) Has been cancelled
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/common/helper"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitConfig "github.com/go-git/go-git/v5/config"
|
||||
@@ -61,7 +62,7 @@ type BackupService struct {
|
||||
autoBackupStop chan bool
|
||||
autoBackupWg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
cancelObserver CancelFunc
|
||||
cancelObservers []helper.CancelFunc
|
||||
}
|
||||
|
||||
// NewBackupService 创建新的备份服务实例
|
||||
@@ -74,7 +75,11 @@ func NewBackupService(configService *ConfigService, dbService *DatabaseService,
|
||||
}
|
||||
|
||||
func (s *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
s.cancelObserver = s.configService.Watch("backup.enabled", s.onBackupConfigChange)
|
||||
// 监听 backup 配置变化
|
||||
s.cancelObservers = []helper.CancelFunc{
|
||||
s.configService.Watch("backup", s.onBackupConfigChange),
|
||||
s.configService.Watch("general.dataPath", s.onDataPathChange),
|
||||
}
|
||||
if err := s.Initialize(); err != nil {
|
||||
s.logger.Error("initializing backup service: %v", err)
|
||||
}
|
||||
@@ -89,6 +94,12 @@ func (s *BackupService) onBackupConfigChange(oldValue, newValue interface{}) {
|
||||
_ = s.HandleConfigChange(&config.Backup)
|
||||
}
|
||||
|
||||
func (s *BackupService) onDataPathChange(oldValue, newValue interface{}) {
|
||||
if err := s.Reinitialize(); err != nil {
|
||||
s.logger.Error("Failed to reinitialize backup service after data path change: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize 初始化备份服务
|
||||
func (s *BackupService) Initialize() error {
|
||||
config, repoPath, err := s.getConfigAndPath()
|
||||
@@ -100,7 +111,7 @@ func (s *BackupService) Initialize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 仓库地址为空时不初始化(等待用户配置)
|
||||
// 仓库地址为空时不初始化
|
||||
if strings.TrimSpace(config.RepoURL) == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -1196,8 +1207,10 @@ func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error
|
||||
|
||||
// ServiceShutdown 服务关闭
|
||||
func (s *BackupService) ServiceShutdown() {
|
||||
if s.cancelObserver != nil {
|
||||
s.cancelObserver()
|
||||
for _, cancel := range s.cancelObservers {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
s.StopAutoBackup()
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// ObserverCallback 观察者回调函数
|
||||
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 注册配置变更监听器
|
||||
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 取消时自动清理
|
||||
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 通知指定路径的所有观察者
|
||||
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()
|
||||
}
|
||||
@@ -2,33 +2,31 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/common/helper"
|
||||
"voidraft/internal/models"
|
||||
|
||||
jsonparser "github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/providers/structs"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// ConfigService 应用配置服务
|
||||
type ConfigService struct {
|
||||
koanf *koanf.Koanf // koanf 实例
|
||||
logger *log.LogService // 日志服务
|
||||
configDir string // 配置目录
|
||||
settingsPath string // 设置文件路径
|
||||
mu sync.RWMutex // 读写锁
|
||||
fileProvider *file.File // 文件提供器,用于监听
|
||||
|
||||
observer *ConfigObserver
|
||||
koanf *koanf.Koanf
|
||||
logger *log.LogService
|
||||
configDir string
|
||||
settingsPath string
|
||||
mu sync.RWMutex
|
||||
observer *helper.ConfigObserver
|
||||
|
||||
// 配置迁移器
|
||||
configMigrator *ConfigMigrator
|
||||
@@ -36,49 +34,29 @@ type ConfigService struct {
|
||||
|
||||
// NewConfigService 创建新的配置服务实例
|
||||
func NewConfigService(logger *log.LogService) *ConfigService {
|
||||
// 获取用户主目录
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unable to get the user's home directory: %w", err))
|
||||
}
|
||||
|
||||
// 设置配置目录和设置文件路径
|
||||
configDir := filepath.Join(homeDir, ".voidraft", "config")
|
||||
settingsPath := filepath.Join(configDir, "settings.json")
|
||||
|
||||
observerService := NewConfigObserver(logger)
|
||||
|
||||
configMigrator := NewConfigMigrator(logger, configDir, "settings", settingsPath)
|
||||
|
||||
return &ConfigService{
|
||||
logger: logger,
|
||||
configDir: configDir,
|
||||
settingsPath: settingsPath,
|
||||
koanf: koanf.New("."),
|
||||
observer: observerService,
|
||||
configMigrator: configMigrator,
|
||||
observer: helper.NewConfigObserver(logger),
|
||||
configMigrator: NewConfigMigrator(logger, configDir, "settings", settingsPath),
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStartup initializes the service when the application starts
|
||||
// ServiceStartup 服务启动时初始化
|
||||
func (cs *ConfigService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
err := cs.initConfig()
|
||||
if err != nil {
|
||||
if err := cs.initConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 启动配置文件监听
|
||||
cs.startWatching()
|
||||
return nil
|
||||
}
|
||||
|
||||
// setDefaults 设置默认配置
|
||||
func (cs *ConfigService) setDefaults() error {
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
|
||||
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -87,15 +65,38 @@ func (cs *ConfigService) initConfig() error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 检查配置文件是否存在
|
||||
// 确保配置目录存在
|
||||
if err := os.MkdirAll(cs.configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
// 配置文件不存在,创建默认配置
|
||||
if _, err := os.Stat(cs.settingsPath); os.IsNotExist(err) {
|
||||
return cs.createDefaultConfig()
|
||||
}
|
||||
|
||||
// 配置文件存在,直接加载现有配置
|
||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
return err
|
||||
// 加载现有配置
|
||||
if err := cs.koanf.Load(file.Provider(cs.settingsPath), jsonparser.Parser()); err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDefaultConfig 创建默认配置
|
||||
func (cs *ConfigService) createDefaultConfig() error {
|
||||
// 重置 koanf 实例
|
||||
cs.koanf = koanf.New(".")
|
||||
|
||||
// 加载默认配置
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||
return fmt.Errorf("failed to load default config: %w", err)
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
if err := cs.writeConfigToFile(); err != nil {
|
||||
return fmt.Errorf("failed to write default config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -107,65 +108,12 @@ func (cs *ConfigService) MigrateConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
_, err := cs.configMigrator.AutoMigrate(defaultConfig, cs.koanf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDefaultConfig 创建默认配置文件
|
||||
func (cs *ConfigService) createDefaultConfig() error {
|
||||
// 确保配置目录存在
|
||||
if err := os.MkdirAll(cs.configDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cs.writeConfigToFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建文件提供器
|
||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// startWatching 启动配置文件监听
|
||||
func (cs *ConfigService) startWatching() {
|
||||
if cs.fileProvider == nil {
|
||||
return
|
||||
}
|
||||
cs.fileProvider.Watch(func(event interface{}, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cs.mu.Lock()
|
||||
oldSnapshot := cs.createConfigSnapshot()
|
||||
cs.koanf.Load(cs.fileProvider, jsonparser.Parser())
|
||||
newSnapshot := cs.createConfigSnapshot()
|
||||
cs.mu.Unlock()
|
||||
|
||||
// 检测配置变更并通知观察者
|
||||
cs.notifyChanges(oldSnapshot, newSnapshot)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// stopWatching 停止配置文件监听
|
||||
func (cs *ConfigService) stopWatching() {
|
||||
if cs.fileProvider != nil {
|
||||
cs.fileProvider.Unwatch()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetConfig 获取完整应用配置
|
||||
@@ -177,47 +125,9 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) {
|
||||
if err := cs.koanf.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Set 设置配置项
|
||||
func (cs *ConfigService) Set(key string, value interface{}) error {
|
||||
cs.mu.Lock()
|
||||
|
||||
// 获取旧值用于回滚
|
||||
oldValue := cs.koanf.Get(key)
|
||||
|
||||
// 设置值到koanf
|
||||
cs.koanf.Set(key, value)
|
||||
|
||||
// 更新时间戳
|
||||
newTimestamp := time.Now().Format(time.RFC3339)
|
||||
cs.koanf.Set("metadata.lastUpdated", newTimestamp)
|
||||
|
||||
// 将配置写回文件
|
||||
err := cs.writeConfigToFile()
|
||||
|
||||
if err != nil {
|
||||
// 写文件失败,回滚内存状态
|
||||
if oldValue != nil {
|
||||
cs.koanf.Set(key, oldValue)
|
||||
} else {
|
||||
cs.koanf.Delete(key)
|
||||
}
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
cs.mu.Unlock()
|
||||
|
||||
if cs.observer != nil {
|
||||
cs.observer.Notify(key, oldValue, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取配置项
|
||||
func (cs *ConfigService) Get(key string) interface{} {
|
||||
cs.mu.RLock()
|
||||
@@ -225,118 +135,113 @@ func (cs *ConfigService) Get(key string) interface{} {
|
||||
return cs.koanf.Get(key)
|
||||
}
|
||||
|
||||
// ResetConfig 强制重置所有配置为默认值
|
||||
// Set 设置配置项
|
||||
func (cs *ConfigService) Set(key string, value interface{}) error {
|
||||
cs.mu.Lock()
|
||||
|
||||
// 获取旧值
|
||||
oldValue := cs.koanf.Get(key)
|
||||
|
||||
// 值未变化,直接返回
|
||||
if reflect.DeepEqual(oldValue, value) {
|
||||
cs.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置新值
|
||||
err := cs.koanf.Set(key, value)
|
||||
if err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
err = cs.koanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
||||
if err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if err = cs.writeConfigToFile(); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
cs.mu.Unlock()
|
||||
|
||||
// 通知观察者
|
||||
if cs.observer != nil {
|
||||
cs.observer.Notify(key, oldValue, value)
|
||||
} else {
|
||||
cs.logger.Error("config observer is nil")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetConfig 重置所有配置为默认值
|
||||
func (cs *ConfigService) ResetConfig() error {
|
||||
cs.mu.Lock()
|
||||
|
||||
// 保存旧配置快照
|
||||
oldSnapshot := cs.createConfigSnapshot()
|
||||
oldSnapshot := cs.createSnapshot()
|
||||
|
||||
// 停止文件监听
|
||||
if cs.fileProvider != nil {
|
||||
cs.fileProvider.Unwatch()
|
||||
cs.fileProvider = nil
|
||||
}
|
||||
|
||||
// 设置默认配置
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
// 重置为默认配置
|
||||
cs.koanf = koanf.New(".")
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
return fmt.Errorf("failed to load default config: %w", err)
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
if err := cs.writeConfigToFile(); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
// 重新创建koanf实例
|
||||
cs.koanf = koanf.New(".")
|
||||
|
||||
// 重新加载默认配置到koanf
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// 重新创建文件提供器
|
||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||
|
||||
// 重新加载配置文件
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
newSnapshot := cs.createConfigSnapshot()
|
||||
newSnapshot := cs.createSnapshot()
|
||||
cs.mu.Unlock()
|
||||
|
||||
// 重新启动文件监听
|
||||
cs.startWatching()
|
||||
|
||||
// 检测配置变更并通知观察者
|
||||
// 通知配置变更
|
||||
cs.notifyChanges(oldSnapshot, newSnapshot)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeConfigToFile 将配置写回JSON文件
|
||||
// writeConfigToFile 将配置写入文件
|
||||
func (cs *ConfigService) writeConfigToFile() error {
|
||||
configBytes, err := cs.koanf.Marshal(jsonparser.Parser())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cs.settingsPath, configBytes, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return os.WriteFile(cs.settingsPath, configBytes, 0644)
|
||||
}
|
||||
|
||||
// Watch 注册配置变更监听器
|
||||
func (cs *ConfigService) Watch(path string, callback ObserverCallback) CancelFunc {
|
||||
func (cs *ConfigService) Watch(path string, callback helper.ObserverCallback) helper.CancelFunc {
|
||||
return cs.observer.Watch(path, callback)
|
||||
}
|
||||
|
||||
// WatchWithContext 使用 Context 注册监听器
|
||||
func (cs *ConfigService) WatchWithContext(ctx context.Context, path string, callback ObserverCallback) {
|
||||
func (cs *ConfigService) WatchWithContext(ctx context.Context, path string, callback helper.ObserverCallback) {
|
||||
cs.observer.WatchWithContext(ctx, path, callback)
|
||||
}
|
||||
|
||||
// createConfigSnapshot 创建当前配置的快照(调用者需确保已持有锁)
|
||||
func (cs *ConfigService) createConfigSnapshot() map[string]interface{} {
|
||||
// createSnapshotLocked 创建配置快照
|
||||
func (cs *ConfigService) createSnapshot() map[string]interface{} {
|
||||
snapshot := make(map[string]interface{})
|
||||
allKeys := cs.koanf.All()
|
||||
flattenMap("", allKeys, snapshot)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
if valueMap, ok := value.(map[string]interface{}); ok {
|
||||
// 递归处理嵌套 map
|
||||
flattenMap(fullKey, valueMap, result)
|
||||
} else {
|
||||
// 保存叶子节点
|
||||
result[fullKey] = value
|
||||
}
|
||||
for _, key := range cs.koanf.Keys() {
|
||||
snapshot[key] = cs.koanf.Get(key)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// notifyChanges 检测配置变更并通知观察者
|
||||
func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]interface{}) {
|
||||
// 检测变更
|
||||
if cs.observer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
changes := make(map[string]struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
@@ -345,14 +250,11 @@ func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]inter
|
||||
// 检查新增和修改的键
|
||||
for key, newValue := range newSnapshot {
|
||||
oldValue, exists := oldSnapshot[key]
|
||||
if !exists || !isEqual(oldValue, newValue) {
|
||||
if !exists || !reflect.DeepEqual(oldValue, newValue) {
|
||||
changes[key] = struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
}{
|
||||
OldValue: oldValue,
|
||||
NewValue: newValue,
|
||||
}
|
||||
}{oldValue, newValue}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,29 +264,18 @@ func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]inter
|
||||
changes[key] = struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
}{
|
||||
OldValue: oldValue,
|
||||
NewValue: nil,
|
||||
}
|
||||
}{oldValue, nil}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知所有变更
|
||||
if cs.observer != nil && len(changes) > 0 {
|
||||
// 批量通知
|
||||
if 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.observer != nil {
|
||||
cs.observer.Shutdown()
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ type MigrationProgress struct {
|
||||
|
||||
// MigrationService 迁移服务
|
||||
type MigrationService struct {
|
||||
logger *log.LogService
|
||||
dbService *DatabaseService
|
||||
progress atomic.Value // stores MigrationProgress
|
||||
logger *log.LogService
|
||||
dbService *DatabaseService
|
||||
configService *ConfigService
|
||||
progress atomic.Value // stores MigrationProgress
|
||||
|
||||
mu sync.Mutex
|
||||
ctx context.Context
|
||||
@@ -34,13 +35,14 @@ type MigrationService struct {
|
||||
}
|
||||
|
||||
// NewMigrationService 创建迁移服务
|
||||
func NewMigrationService(dbService *DatabaseService, logger *log.LogService) *MigrationService {
|
||||
func NewMigrationService(dbService *DatabaseService, configService *ConfigService, logger *log.LogService) *MigrationService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
ms := &MigrationService{
|
||||
logger: logger,
|
||||
dbService: dbService,
|
||||
logger: logger,
|
||||
dbService: dbService,
|
||||
configService: configService,
|
||||
}
|
||||
ms.progress.Store(MigrationProgress{})
|
||||
return ms
|
||||
@@ -94,9 +96,12 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
|
||||
if err := ms.dbService.ServiceShutdown(); err != nil {
|
||||
ms.logger.Error("Failed to close database connection", "error", err)
|
||||
}
|
||||
|
||||
// 等待文件句柄释放(Windows 特有问题)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 确保失败时恢复数据库连接
|
||||
// 确保恢复数据库连接
|
||||
defer func() {
|
||||
if ms.dbService != nil {
|
||||
if err := ms.dbService.ServiceStartup(ctx, application.ServiceOptions{}); err != nil {
|
||||
@@ -110,17 +115,59 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
|
||||
return ms.fail(err)
|
||||
}
|
||||
|
||||
// 迁移成功后,立即更新配置到新路径
|
||||
if ms.configService != nil {
|
||||
if err := ms.configService.Set("general.dataPath", dstPath); err != nil {
|
||||
return ms.fail(fmt.Errorf("migration succeeded but failed to update config: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
ms.setProgress(100)
|
||||
return nil
|
||||
}
|
||||
|
||||
// preCheck 预检查,返回是否需要迁移
|
||||
func (ms *MigrationService) preCheck(srcPath, dstPath string) (bool, error) {
|
||||
// 源目录不存在,无需迁移
|
||||
if _, err := os.Stat(srcPath); os.IsNotExist(err) {
|
||||
// 检查源目录状态
|
||||
srcStat, srcErr := os.Stat(srcPath)
|
||||
srcNotExist := os.IsNotExist(srcErr)
|
||||
|
||||
// 检查目标目录状态
|
||||
dstStat, dstErr := os.Stat(dstPath)
|
||||
dstNotExist := os.IsNotExist(dstErr)
|
||||
|
||||
// 1:源目录不存在
|
||||
if srcNotExist {
|
||||
// 如果目标目录存在且有内容,说明迁移已经完成
|
||||
if !dstNotExist && dstStat.IsDir() {
|
||||
isEmpty, err := isDirEmpty(dstPath)
|
||||
if err == nil && !isEmpty {
|
||||
ms.logger.Info("Migration already completed, source not exist but target has content", "dst", dstPath)
|
||||
return false, nil // 无需迁移
|
||||
}
|
||||
}
|
||||
// 源不存在且目标也不存在/为空,无需迁移
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 2. 源目录存在但为空
|
||||
if srcStat.IsDir() {
|
||||
srcEmpty, err := isDirEmpty(srcPath)
|
||||
if err == nil && srcEmpty {
|
||||
// 源为空,目标有内容 → 迁移已完成
|
||||
if !dstNotExist && dstStat.IsDir() {
|
||||
dstEmpty, _ := isDirEmpty(dstPath)
|
||||
if !dstEmpty {
|
||||
ms.logger.Info("Migration already completed, source is empty but target has content", "dst", dstPath)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
// 源为空,目标也为空 → 无需迁移
|
||||
ms.logger.Info("Both source and target are empty, no migration needed")
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 路径相同,无需迁移
|
||||
srcAbs, _ := filepath.Abs(srcPath)
|
||||
dstAbs, _ := filepath.Abs(dstPath)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"voidraft/internal/common/helper"
|
||||
"voidraft/internal/common/hotkey"
|
||||
"voidraft/internal/models"
|
||||
@@ -33,7 +32,7 @@ type HotkeyService struct {
|
||||
isShutdown atomic.Bool
|
||||
|
||||
// 配置观察者取消函数
|
||||
cancelObservers []CancelFunc
|
||||
cancelObservers []helper.CancelFunc
|
||||
}
|
||||
|
||||
// NewHotkeyService 创建热键服务实例
|
||||
@@ -61,7 +60,7 @@ func (hs *HotkeyService) ServiceStartup(ctx context.Context, options application
|
||||
// Initialize 初始化热键服务
|
||||
func (hs *HotkeyService) Initialize() error {
|
||||
// 注册配置监听
|
||||
hs.cancelObservers = []CancelFunc{
|
||||
hs.cancelObservers = []helper.CancelFunc{
|
||||
hs.configService.Watch("general.enableGlobalHotkey", hs.onHotkeyConfigChange),
|
||||
hs.configService.Watch("general.globalHotkey", hs.onHotkeyConfigChange),
|
||||
}
|
||||
@@ -84,11 +83,14 @@ func (hs *HotkeyService) onHotkeyConfigChange(oldValue, newValue interface{}) {
|
||||
// 重新加载配置
|
||||
config, err := hs.configService.GetConfig()
|
||||
if err != nil {
|
||||
hs.logger.Error("failed to get config", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新热键
|
||||
_ = hs.UpdateHotkey(config.General.EnableGlobalHotkey, &config.General.GlobalHotkey)
|
||||
if err := hs.UpdateHotkey(config.General.EnableGlobalHotkey, &config.General.GlobalHotkey); err != nil {
|
||||
hs.logger.Error("failed to update hotkey", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHotkey 注册全局热键
|
||||
@@ -101,22 +103,34 @@ func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error {
|
||||
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)
|
||||
}
|
||||
|
||||
hs.mu.Lock()
|
||||
// 如果已注册,异步清理旧热键
|
||||
if hs.registered.Load() {
|
||||
hs.mu.RLock()
|
||||
oldHk := hs.hk
|
||||
hs.mu.RUnlock()
|
||||
|
||||
if oldHk != nil {
|
||||
// 异步清理,不阻塞当前流程
|
||||
go func() {
|
||||
if err := oldHk.Close(); err != nil {
|
||||
hs.logger.Error("failed to close old hotkey (ignored)", "error", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的热键实例
|
||||
hs.mu.Lock()
|
||||
hs.hk = hotkey.New(mods, key)
|
||||
if err := hs.hk.Register(); err != nil {
|
||||
hs.mu.Unlock()
|
||||
hs.logger.Error("failed to register hotkey", "error", err)
|
||||
return fmt.Errorf("register hotkey: %w", err)
|
||||
}
|
||||
|
||||
@@ -141,35 +155,23 @@ func (hs *HotkeyService) UnregisterHotkey() error {
|
||||
hs.registered.Store(false)
|
||||
|
||||
// 获取热键实例的引用
|
||||
hs.mu.RLock()
|
||||
hs.mu.Lock()
|
||||
hk := hs.hk
|
||||
hs.mu.RUnlock()
|
||||
hs.hk = nil
|
||||
hs.currentHotkey = nil
|
||||
hs.mu.Unlock()
|
||||
|
||||
if hk == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 调用 Close() 确保完全清理
|
||||
_ = hk.Close()
|
||||
|
||||
// 等待监听 goroutine 退出
|
||||
done := make(chan struct{})
|
||||
// 异步清理
|
||||
go func() {
|
||||
hs.wg.Wait()
|
||||
close(done)
|
||||
if err := hk.Close(); err != nil {
|
||||
hs.logger.Error("failed to close hotkey (ignored)", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
|
||||
// 清理状态
|
||||
hs.mu.Lock()
|
||||
hs.hk = nil
|
||||
hs.currentHotkey = nil
|
||||
hs.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -321,6 +323,24 @@ func (hs *HotkeyService) showAllWindows() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetSupportedKeys 返回系统支持的快捷键列表
|
||||
func (hs *HotkeyService) GetSupportedKeys() []string {
|
||||
// 返回当前系统支持的所有键
|
||||
// 这个列表与 convertKey 方法中的 keyMap 保持一致
|
||||
return []string{
|
||||
// 字母键
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
||||
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
||||
// 数字键
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
// 功能键
|
||||
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
|
||||
// 特殊键
|
||||
"Space", "Tab", "Enter", "Escape", "Delete",
|
||||
"ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight",
|
||||
}
|
||||
}
|
||||
|
||||
// isValidHotkey 验证热键组合
|
||||
func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
|
||||
if combo == nil || combo.Key == "" {
|
||||
@@ -333,24 +353,6 @@ func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCurrentHotkey 获取当前热键
|
||||
func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
|
||||
hs.mu.RLock()
|
||||
defer hs.mu.RUnlock()
|
||||
|
||||
if hs.currentHotkey == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.HotkeyCombo{
|
||||
Ctrl: hs.currentHotkey.Ctrl,
|
||||
Shift: hs.currentHotkey.Shift,
|
||||
Alt: hs.currentHotkey.Alt,
|
||||
Win: hs.currentHotkey.Win,
|
||||
Key: hs.currentHotkey.Key,
|
||||
}
|
||||
}
|
||||
|
||||
// IsRegistered 检查是否已注册
|
||||
func (hs *HotkeyService) IsRegistered() bool {
|
||||
return hs.registered.Load()
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// TestHotkeyServiceCreation 测试服务创建
|
||||
func TestHotkeyServiceCreation(t *testing.T) {
|
||||
logger := log.New()
|
||||
configService := &ConfigService{} // Mock
|
||||
|
||||
service := NewHotkeyService(configService, logger)
|
||||
if service == nil {
|
||||
t.Fatal("Failed to create hotkey service")
|
||||
}
|
||||
|
||||
if service.logger == nil {
|
||||
t.Error("Logger should not be nil")
|
||||
}
|
||||
|
||||
if service.registered.Load() {
|
||||
t.Error("Service should not have registered hotkey initially")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyValidation 测试热键验证
|
||||
func TestHotkeyValidation(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
combo *models.HotkeyCombo
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "Nil combo",
|
||||
combo: nil,
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "Empty key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "",
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "No modifiers",
|
||||
combo: &models.HotkeyCombo{
|
||||
Key: "A",
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "Valid: Ctrl+A",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "A",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid: Ctrl+Shift+F1",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Shift: true,
|
||||
Key: "F1",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid: Alt+Space",
|
||||
combo: &models.HotkeyCombo{
|
||||
Alt: true,
|
||||
Key: "Space",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := service.isValidHotkey(tt.combo)
|
||||
if result != tt.valid {
|
||||
t.Errorf("Expected valid=%v, got %v", tt.valid, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyConversion 测试热键转换
|
||||
func TestHotkeyConversion(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
combo *models.HotkeyCombo
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid letter key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "A",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid number key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Shift: true,
|
||||
Key: "1",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid function key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Alt: true,
|
||||
Key: "F5",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "InvalidKey",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
key, mods, err := service.convertHotkey(tt.combo)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if key == 0 {
|
||||
t.Error("Key should not be 0")
|
||||
}
|
||||
|
||||
if len(mods) == 0 {
|
||||
t.Error("Should have at least one modifier")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyRegisterUnregister 测试注册和注销
|
||||
func TestHotkeyRegisterUnregister(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Shift: true,
|
||||
Key: "F10",
|
||||
}
|
||||
|
||||
// 测试注册
|
||||
err := service.RegisterHotkey(combo)
|
||||
if err != nil {
|
||||
t.Logf("Register failed (may be expected in test environment): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !service.IsRegistered() {
|
||||
t.Error("Service should be registered")
|
||||
}
|
||||
|
||||
// 验证当前热键
|
||||
current := service.GetCurrentHotkey()
|
||||
if current == nil {
|
||||
t.Error("Current hotkey should not be nil")
|
||||
}
|
||||
|
||||
if current.Key != combo.Key {
|
||||
t.Errorf("Expected key %s, got %s", combo.Key, current.Key)
|
||||
}
|
||||
|
||||
// 测试注销
|
||||
err = service.UnregisterHotkey()
|
||||
if err != nil {
|
||||
t.Fatalf("Unregister failed: %v", err)
|
||||
}
|
||||
|
||||
if service.IsRegistered() {
|
||||
t.Error("Service should not be registered after unregister")
|
||||
}
|
||||
|
||||
current = service.GetCurrentHotkey()
|
||||
if current != nil {
|
||||
t.Error("Current hotkey should be nil after unregister")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyUpdate 测试更新热键
|
||||
func TestHotkeyUpdate(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo1 := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "F11",
|
||||
}
|
||||
|
||||
// 启用热键
|
||||
err := service.UpdateHotkey(true, combo1)
|
||||
if err != nil {
|
||||
t.Logf("Update (enable) failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer service.UnregisterHotkey()
|
||||
|
||||
if !service.IsRegistered() {
|
||||
t.Error("Should be registered after enable")
|
||||
}
|
||||
|
||||
// 禁用热键
|
||||
err = service.UpdateHotkey(false, combo1)
|
||||
if err != nil {
|
||||
t.Fatalf("Update (disable) failed: %v", err)
|
||||
}
|
||||
|
||||
if service.IsRegistered() {
|
||||
t.Error("Should not be registered after disable")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyDoubleRegister 测试重复注册
|
||||
func TestHotkeyDoubleRegister(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Alt: true,
|
||||
Key: "F12",
|
||||
}
|
||||
|
||||
err := service.RegisterHotkey(combo)
|
||||
if err != nil {
|
||||
t.Skip("First registration failed")
|
||||
}
|
||||
defer service.UnregisterHotkey()
|
||||
|
||||
// 第二次注册应该先取消第一次注册,然后重新注册
|
||||
combo2 := &models.HotkeyCombo{
|
||||
Shift: true,
|
||||
Key: "F12",
|
||||
}
|
||||
|
||||
err = service.RegisterHotkey(combo2)
|
||||
if err != nil {
|
||||
t.Logf("Second registration failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证当前热键是新的
|
||||
current := service.GetCurrentHotkey()
|
||||
if current != nil && current.Shift != combo2.Shift {
|
||||
t.Error("Should have updated to new hotkey")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyConcurrentAccess 测试并发访问
|
||||
func TestHotkeyConcurrentAccess(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "G",
|
||||
}
|
||||
|
||||
const goroutines = 10
|
||||
done := make(chan bool, goroutines)
|
||||
|
||||
// 并发读取
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func() {
|
||||
for j := 0; j < 100; j++ {
|
||||
_ = service.IsRegistered()
|
||||
_ = service.GetCurrentHotkey()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// 主协程进行注册/注销操作
|
||||
go func() {
|
||||
for i := 0; i < 5; i++ {
|
||||
service.RegisterHotkey(combo)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
service.UnregisterHotkey()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待所有 goroutine 完成
|
||||
for i := 0; i < goroutines; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
t.Log("Concurrent access test completed without panics")
|
||||
}
|
||||
|
||||
// TestHotkeyServiceShutdown 测试服务关闭
|
||||
func TestHotkeyServiceShutdown(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Shift: true,
|
||||
Key: "H",
|
||||
}
|
||||
|
||||
err := service.RegisterHotkey(combo)
|
||||
if err != nil {
|
||||
t.Skip("Registration failed")
|
||||
}
|
||||
|
||||
// 测试 ServiceShutdown
|
||||
err = service.ServiceShutdown()
|
||||
if err != nil {
|
||||
t.Fatalf("ServiceShutdown failed: %v", err)
|
||||
}
|
||||
|
||||
if service.IsRegistered() {
|
||||
t.Error("Should not be registered after shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHotkeyRegistration 基准测试:热键注册
|
||||
func BenchmarkHotkeyRegistration(b *testing.B) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "B",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
service.RegisterHotkey(combo)
|
||||
service.UnregisterHotkey()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHotkeyConversion 基准测试:热键转换
|
||||
func BenchmarkHotkeyConversion(b *testing.B) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Shift: true,
|
||||
Alt: true,
|
||||
Key: "F5",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
service.convertHotkey(combo)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/wailsapp/wails/v3/pkg/services/dock"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// ServiceManager 服务管理器,负责协调各个服务
|
||||
@@ -36,7 +37,9 @@ type ServiceManager struct {
|
||||
// NewServiceManager 创建新的服务管理器实例
|
||||
func NewServiceManager() *ServiceManager {
|
||||
// 初始化日志服务
|
||||
logger := log.New()
|
||||
logger := log.NewWithConfig(&log.Config{
|
||||
LogLevel: slog.LevelDebug,
|
||||
})
|
||||
|
||||
// 初始化badge服务
|
||||
badgeService := dock.New()
|
||||
@@ -51,7 +54,7 @@ func NewServiceManager() *ServiceManager {
|
||||
databaseService := NewDatabaseService(configService, logger)
|
||||
|
||||
// 初始化迁移服务
|
||||
migrationService := NewMigrationService(databaseService, logger)
|
||||
migrationService := NewMigrationService(databaseService, configService, logger)
|
||||
|
||||
// 初始化文档服务
|
||||
documentService := NewDocumentService(databaseService, logger)
|
||||
|
||||
@@ -113,7 +113,7 @@ func (ws *WindowService) onWindowClosing(documentID int64) {
|
||||
}
|
||||
|
||||
// GetOpenWindows 获取所有打开的文档窗口
|
||||
func (ws *WindowService) GetOpenWindows() []application.Window {
|
||||
func (ws *WindowService) getOpenWindows() []application.Window {
|
||||
app := application.Get()
|
||||
return app.Window.GetAll()
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
|
||||
func (ws *WindowService) ServiceShutdown() error {
|
||||
// 从吸附服务中取消注册所有窗口
|
||||
if ws.windowSnapService != nil {
|
||||
windows := ws.GetOpenWindows()
|
||||
windows := ws.getOpenWindows()
|
||||
for _, window := range windows {
|
||||
if documentID, err := strconv.ParseInt(window.Name(), 10, 64); err == nil {
|
||||
ws.windowSnapService.UnregisterWindow(documentID)
|
||||
|
||||
@@ -91,7 +91,7 @@ type WindowSnapService struct {
|
||||
windowMoveUnhooks map[int64]func() // documentID -> 子窗口移动监听清理函数
|
||||
|
||||
// 配置观察者取消函数
|
||||
cancelObserver CancelFunc
|
||||
cancelObserver helper.CancelFunc
|
||||
}
|
||||
|
||||
// NewWindowSnapService 创建新的窗口吸附服务实例
|
||||
|
||||
Reference in New Issue
Block a user