diff --git a/frontend/bindings/voidraft/internal/services/fileservice.ts b/frontend/bindings/voidraft/internal/services/fileservice.ts index 5b129a6..df39b65 100644 --- a/frontend/bindings/voidraft/internal/services/fileservice.ts +++ b/frontend/bindings/voidraft/internal/services/fileservice.ts @@ -2,7 +2,7 @@ // This file is automatically generated. DO NOT EDIT /** - * FileService 提供原子化文件操作 + * FileService 提供文件操作 * @module */ @@ -10,6 +10,10 @@ // @ts-ignore: Unused imports import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as time$0 from "../../../time/models.js"; + /** * DeleteFile 删除文件 */ @@ -34,6 +38,14 @@ export function FileExists(filePath: string): Promise & { cancel(): voi return $resultPromise; } +/** + * GetFileModTime 获取文件的修改时间 + */ +export function GetFileModTime(filePath: string): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2240854203, filePath) as any; + return $resultPromise; +} + /** * LoadJSON 从文件加载JSON数据 */ @@ -43,9 +55,19 @@ export function LoadJSON(filePath: string, target: any): Promise & { cance } /** - * SaveJSON 原子化保存JSON数据到文件 + * SaveJSON 保存JSON数据到文件 */ export function SaveJSON(filePath: string, data: any): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(3646933935, filePath, data) as any; return $resultPromise; } + +/** + * SaveJSONWithCheck 保存JSON数据到文件,带并发检查 + * expectedModTime是期望的文件修改时间,如果为零值则不检查 + * onConflict是冲突处理函数,如果文件已被修改且此函数不为nil,则调用此函数合并数据 + */ +export function SaveJSONWithCheck(filePath: string, data: any, expectedModTime: time$0.Time, onConflict: any): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(4074237977, filePath, data, expectedModTime, onConflict) as any; + return $resultPromise; +} diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 05a42d3..4c942c5 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -1,11 +1,13 @@ package services import ( + "encoding/json" "fmt" "log" "os" "path/filepath" - "sync" + "strconv" + "strings" "time" "voidraft/internal/models" ) @@ -16,8 +18,8 @@ type ConfigService struct { configPath string rootDir string homePath string - mutex sync.RWMutex - config *models.AppConfig // 缓存最新配置 + config *models.AppConfig + lastModTime time.Time // 上次读取配置文件的修改时间 } // NewConfigService 创建新的配置服务实例 @@ -40,16 +42,16 @@ func NewConfigService(fileService *FileService) *ConfigService { rootDir: defaultConfig.Paths.RootDir, configPath: defaultConfig.Paths.ConfigPath, homePath: homePath, - config: defaultConfig, // 初始化缓存配置 + config: defaultConfig, } - // 初始化配置目录和文件(非锁定方式) + // 初始化配置目录和文件 service.initializeConfig() return service } -// initializeConfig 初始化配置目录和文件,避免死锁 +// initializeConfig 初始化配置目录和文件 func (cs *ConfigService) initializeConfig() { // 确保配置目录存在 dirPath := filepath.Join(cs.homePath, cs.rootDir) @@ -78,149 +80,251 @@ func (cs *ConfigService) initializeConfig() { log.Printf("Config file not found, creating default config") // 创建默认配置文件 defaultConfig := models.NewDefaultAppConfig() - if err := cs.saveAppConfigInitial(defaultConfig); err != nil { + if err := cs.saveAppConfig(defaultConfig); err != nil { log.Printf("Failed to save default config: %v", err) } } else { // 加载现有配置 log.Printf("Loading existing config file") - existingConfig, err := cs.loadAppConfigInitial() + existingConfig, modTime, err := cs.loadAppConfigWithModTime() if err != nil { log.Printf("Failed to load existing config, using default: %v", err) } else { cs.config = existingConfig + cs.lastModTime = modTime } } } -// saveAppConfigInitial 初始化时保存配置,不使用互斥锁 -func (cs *ConfigService) saveAppConfigInitial(config *models.AppConfig) error { - // 更新配置元数据 - config.Metadata.LastUpdated = time.Now() - - // 保存到文件 - configPath := cs.GetFullConfigPath() - log.Printf("saveAppConfigInitial: Saving to %s", configPath) - - if err := cs.fileService.SaveJSON(configPath, config); err != nil { - return fmt.Errorf("failed to save initial config file: %w", err) - } - - // 更新内存中的配置 - cs.config = config - return nil -} - -// loadAppConfigInitial 初始化时加载配置,不使用互斥锁 -func (cs *ConfigService) loadAppConfigInitial() (*models.AppConfig, error) { - config := &models.AppConfig{} - configPath := cs.GetFullConfigPath() - - if err := cs.fileService.LoadJSON(configPath, config); err != nil { - return nil, fmt.Errorf("failed to load initial config file: %w", err) - } - - return config, nil -} - -// initConfigDir 确保目录存在,如不存在则创建 -func (cs *ConfigService) initConfigDir() error { - configDir := filepath.Join(cs.homePath, cs.rootDir) - return cs.fileService.EnsureDir(configDir) -} - // GetFullConfigPath 获取完整的配置文件路径 func (cs *ConfigService) GetFullConfigPath() string { return filepath.Join(cs.homePath, cs.rootDir, cs.configPath) } +// mergeConfigs 合并两个配置,实现智能合并策略 +func (cs *ConfigService) mergeConfigs(currentConfig *models.AppConfig, existingData []byte) (*models.AppConfig, error) { + // 解析磁盘配置 + var diskConfig models.AppConfig + if err := json.Unmarshal(existingData, &diskConfig); err != nil { + return nil, fmt.Errorf("failed to parse existing config: %w", err) + } + + // 创建合并后的配置(基于磁盘配置) + mergedConfig := diskConfig + + // ===== 1. 编辑器配置合并策略 ===== + // 编辑器配置是用户最直接的操作,总是保留当前配置 + log.Printf("Merge: Preserving current editor config") + mergedConfig.Editor = currentConfig.Editor + + // ===== 2. 路径配置合并策略 ===== + // 如果当前实例已经修改了根目录(非默认值),保留当前配置 + defaultConfig := models.NewDefaultAppConfig() + + if currentConfig.Paths.RootDir != defaultConfig.Paths.RootDir && + currentConfig.Paths.RootDir != diskConfig.Paths.RootDir { + log.Printf("Merge: Using custom root directory from current config: %s", currentConfig.Paths.RootDir) + mergedConfig.Paths.RootDir = currentConfig.Paths.RootDir + } else { + log.Printf("Merge: Using root directory from disk config: %s", diskConfig.Paths.RootDir) + } + + // 对配置路径采用相同策略 + if currentConfig.Paths.ConfigPath != defaultConfig.Paths.ConfigPath && + currentConfig.Paths.ConfigPath != diskConfig.Paths.ConfigPath { + log.Printf("Merge: Using custom config path from current config: %s", currentConfig.Paths.ConfigPath) + mergedConfig.Paths.ConfigPath = currentConfig.Paths.ConfigPath + } else { + log.Printf("Merge: Using config path from disk config: %s", diskConfig.Paths.ConfigPath) + } + + // ===== 3. 元数据合并策略 ===== + // 版本号使用较高的版本 + if compareVersions(currentConfig.Metadata.Version, diskConfig.Metadata.Version) > 0 { + log.Printf("Merge: Using higher version from current config: %s", currentConfig.Metadata.Version) + mergedConfig.Metadata.Version = currentConfig.Metadata.Version + } else { + log.Printf("Merge: Using version from disk config: %s", diskConfig.Metadata.Version) + } + + // 更新时间总是使用当前时间 + mergedConfig.Metadata.LastUpdated = time.Now() + + // ===== 4. 其他配置项 ===== + + log.Printf("Merge: Completed using comprehensive merge strategy") + return &mergedConfig, nil +} + +// compareVersions 比较两个版本号 +// 返回: 1 如果v1>v2, 0 如果v1=v2, -1 如果v1 parts2[i] { + return 1 + } else if parts1[i] < parts2[i] { + return -1 + } + continue + } + + // 数值比较 + if num1 > num2 { + return 1 + } else if num1 < num2 { + return -1 + } + } + + // 所有部分都相等 + return 0 +} + +// loadAppConfigWithModTime 从文件加载配置,同时返回文件修改时间 +func (cs *ConfigService) loadAppConfigWithModTime() (*models.AppConfig, time.Time, error) { + config := &models.AppConfig{} + configPath := cs.GetFullConfigPath() + + // 先获取文件修改时间 + modTime, err := cs.fileService.GetFileModTime(configPath) + if err != nil { + return nil, time.Time{}, fmt.Errorf("failed to get config file modification time: %w", err) + } + + // 然后加载文件内容 + if err := cs.fileService.LoadJSON(configPath, config); err != nil { + return nil, time.Time{}, fmt.Errorf("failed to load config file: %w", err) + } + + return config, modTime, nil +} + +// handleConfigConflict 处理配置冲突的回调函数 +func (cs *ConfigService) handleConfigConflict(existingData []byte) (interface{}, error) { + // 如果没有当前配置,无法合并 + if cs.config == nil { + return nil, fmt.Errorf("no current config to merge") + } + + // 合并配置 + mergedConfig, err := cs.mergeConfigs(cs.config, existingData) + if err != nil { + return nil, err + } + + // 更新内存中的配置 + cs.config = mergedConfig + + return mergedConfig, nil +} + +// 内部方法:从文件加载配置 +func (cs *ConfigService) loadAppConfig() (*models.AppConfig, error) { + config, modTime, err := cs.loadAppConfigWithModTime() + if err != nil { + return nil, err + } + + // 更新最后修改时间 + cs.lastModTime = modTime + return config, nil +} + +// 内部方法:保存配置到文件 +func (cs *ConfigService) saveAppConfig(config *models.AppConfig) error { + // 更新配置元数据 + config.Metadata.LastUpdated = time.Now() + + // 保存到文件,使用乐观并发控制 + configPath := cs.GetFullConfigPath() + log.Printf("saveAppConfig: Saving to %s with last mod time: %s", + configPath, cs.lastModTime.Format(time.RFC3339)) + + err := cs.fileService.SaveJSONWithCheck( + configPath, + config, + cs.lastModTime, + cs.handleConfigConflict, + ) + + if err != nil { + return fmt.Errorf("failed to save config file: %w", err) + } + + // 更新内存中的配置和最后修改时间 + cs.config = config + + // 获取最新的修改时间 + newModTime, err := cs.fileService.GetFileModTime(configPath) + if err == nil { + cs.lastModTime = newModTime + } + + return nil +} + // GetAppConfig 获取应用配置 func (cs *ConfigService) GetAppConfig() (*models.AppConfig, error) { - cs.mutex.RLock() - defer cs.mutex.RUnlock() - // 返回内存中的配置副本 if cs.config != nil { return cs.config, nil } // 从文件加载 - config := &models.AppConfig{} - configPath := cs.GetFullConfigPath() - - log.Printf("GetAppConfig: Loading from %s", configPath) - - // 如果配置文件存在,则加载 - if cs.fileService.FileExists(configPath) { - if err := cs.fileService.LoadJSON(configPath, config); err != nil { - log.Printf("GetAppConfig: Failed to load config: %v", err) - return nil, fmt.Errorf("failed to load config file: %w", err) - } - log.Printf("GetAppConfig: Successfully loaded config") - // 更新内存中的配置 - cs.config = config - } else { - // 文件不存在,使用默认配置 - log.Printf("GetAppConfig: Config file not found, using default") - config = models.NewDefaultAppConfig() + config, err := cs.loadAppConfig() + if err != nil { + log.Printf("GetAppConfig: Failed to load config: %v", err) + // 使用默认配置 + defaultConfig := models.NewDefaultAppConfig() + cs.config = defaultConfig // 保存默认配置到文件 - if err := cs.SaveAppConfig(config); err != nil { + if err := cs.saveAppConfig(defaultConfig); err != nil { log.Printf("GetAppConfig: Failed to save default config: %v", err) - return nil, fmt.Errorf("failed to save default config: %w", err) } + + return defaultConfig, nil } + // 更新内存中的配置 + cs.config = config return config, nil } // SaveAppConfig 保存应用配置 func (cs *ConfigService) SaveAppConfig(config *models.AppConfig) error { - cs.mutex.Lock() - defer cs.mutex.Unlock() - - // 更新配置元数据 - config.Metadata.LastUpdated = time.Now() - - // 保存到文件 - configPath := cs.GetFullConfigPath() - log.Printf("SaveAppConfig: Saving to %s", configPath) - - if err := cs.fileService.SaveJSON(configPath, config); err != nil { - log.Printf("SaveAppConfig: Failed to save config: %v", err) - return fmt.Errorf("failed to save config file: %w", err) - } - - // 更新内存中的配置 - cs.config = config - log.Printf("SaveAppConfig: Successfully saved config") - - return nil + return cs.saveAppConfig(config) } // UpdateEditorConfig 更新编辑器配置 func (cs *ConfigService) UpdateEditorConfig(editorConfig models.EditorConfig) error { - cs.mutex.Lock() - defer cs.mutex.Unlock() - // 如果内存中已有配置,直接更新 if cs.config != nil { log.Printf("UpdateEditorConfig: Updating in-memory editor config: %+v", editorConfig) cs.config.Editor = editorConfig - - // 保存到文件 - configPath := cs.GetFullConfigPath() - if err := cs.fileService.SaveJSON(configPath, cs.config); err != nil { - log.Printf("UpdateEditorConfig: Failed to save config: %v", err) - return fmt.Errorf("failed to save config file: %w", err) - } - - log.Printf("UpdateEditorConfig: Successfully saved updated config") - return nil + return cs.saveAppConfig(cs.config) } // 没有内存中的配置,需要先加载 - config, err := cs.loadAppConfigInitial() + config, err := cs.loadAppConfig() if err != nil { log.Printf("UpdateEditorConfig: Failed to load config: %v", err) // 使用默认配置 @@ -230,35 +334,19 @@ func (cs *ConfigService) UpdateEditorConfig(editorConfig models.EditorConfig) er // 更新编辑器配置 config.Editor = editorConfig - // 更新配置元数据 - config.Metadata.LastUpdated = time.Now() - // 保存到文件 - configPath := cs.GetFullConfigPath() - if err := cs.fileService.SaveJSON(configPath, config); err != nil { - log.Printf("UpdateEditorConfig: Failed to save config: %v", err) - return fmt.Errorf("failed to save config file: %w", err) - } - - // 更新内存中的配置 - cs.config = config - log.Printf("UpdateEditorConfig: Successfully saved config with updated editor settings") - - return nil + return cs.saveAppConfig(config) } // GetEditorConfig 获取编辑器配置 func (cs *ConfigService) GetEditorConfig() (models.EditorConfig, error) { - cs.mutex.RLock() - defer cs.mutex.RUnlock() - // 如果内存中已有配置,直接返回 if cs.config != nil { return cs.config.Editor, nil } // 否则从文件加载 - config, err := cs.loadAppConfigInitial() + config, err := cs.loadAppConfig() if err != nil { log.Printf("GetEditorConfig: Failed to load config: %v", err) // 使用默认配置 @@ -275,24 +363,18 @@ func (cs *ConfigService) GetEditorConfig() (models.EditorConfig, error) { // ResetToDefault 重置为默认配置 func (cs *ConfigService) ResetToDefault() error { - cs.mutex.Lock() - defer cs.mutex.Unlock() - // 创建默认配置 defaultConfig := models.NewDefaultAppConfig() log.Printf("ResetToDefault: Resetting to default config") // 保存到文件 - configPath := cs.GetFullConfigPath() - if err := cs.fileService.SaveJSON(configPath, defaultConfig); err != nil { + err := cs.saveAppConfig(defaultConfig) + if err != nil { log.Printf("ResetToDefault: Failed to save default config: %v", err) return fmt.Errorf("failed to save default config: %w", err) } - // 更新内存中的配置 - cs.config = defaultConfig log.Printf("ResetToDefault: Successfully reset to default config") - return nil } diff --git a/internal/services/file_service.go b/internal/services/file_service.go index fbe3823..87c6467 100644 --- a/internal/services/file_service.go +++ b/internal/services/file_service.go @@ -6,13 +6,11 @@ import ( "log" "os" "path/filepath" - "sync" + "time" ) -// FileService 提供原子化文件操作 -type FileService struct { - mutex sync.Mutex -} +// FileService 提供文件操作 +type FileService struct{} // NewFileService 创建新的文件服务实例 func NewFileService() *FileService { @@ -21,14 +19,6 @@ func NewFileService() *FileService { // EnsureDir 确保目录存在,如不存在则创建 func (fs *FileService) EnsureDir(dirPath string) error { - fs.mutex.Lock() - defer fs.mutex.Unlock() - - return fs.ensureDirNoLock(dirPath) -} - -// ensureDirNoLock 无锁版本的EnsureDir,仅供内部使用 -func (fs *FileService) ensureDirNoLock(dirPath string) error { log.Printf("EnsureDir: Checking directory: %s", dirPath) if _, err := os.Stat(dirPath); os.IsNotExist(err) { @@ -45,52 +35,114 @@ func (fs *FileService) ensureDirNoLock(dirPath string) error { return nil } -// SaveJSON 原子化保存JSON数据到文件 -func (fs *FileService) SaveJSON(filePath string, data interface{}) error { - fs.mutex.Lock() - defer fs.mutex.Unlock() +// GetFileModTime 获取文件的修改时间 +func (fs *FileService) GetFileModTime(filePath string) (time.Time, error) { + log.Printf("GetFileModTime: Getting modification time for file: %s", filePath) - log.Printf("SaveJSON: Saving to file: %s", filePath) + fileInfo, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + log.Printf("GetFileModTime: File does not exist: %s", filePath) + return time.Time{}, fmt.Errorf("file does not exist: %w", err) + } + log.Printf("GetFileModTime: Failed to get file info: %v", err) + return time.Time{}, fmt.Errorf("failed to get file info: %w", err) + } - // 确保目录存在 - 使用无锁版本以避免死锁 + modTime := fileInfo.ModTime() + log.Printf("GetFileModTime: File modification time: %s", modTime.Format(time.RFC3339)) + return modTime, nil +} + +// SaveJSONWithCheck 保存JSON数据到文件,带并发检查 +// expectedModTime是期望的文件修改时间,如果为零值则不检查 +// onConflict是冲突处理函数,如果文件已被修改且此函数不为nil,则调用此函数合并数据 +func (fs *FileService) SaveJSONWithCheck(filePath string, data interface{}, expectedModTime time.Time, onConflict func(existingData []byte) (interface{}, error)) error { + log.Printf("SaveJSONWithCheck: Saving to file with concurrency check: %s", filePath) + + // 检查文件是否存在且已被修改 + if !expectedModTime.IsZero() && fs.FileExists(filePath) { + currentModTime, err := fs.GetFileModTime(filePath) + if err == nil { + // 如果文件修改时间与预期不符,说明文件已被其他进程修改 + if !currentModTime.Equal(expectedModTime) { + log.Printf("SaveJSONWithCheck: File has been modified since last read. Expected: %s, Current: %s", + expectedModTime.Format(time.RFC3339), currentModTime.Format(time.RFC3339)) + + // 如果提供了冲突处理函数,尝试解决冲突 + if onConflict != nil { + log.Printf("SaveJSONWithCheck: Attempting to resolve conflict") + + // 读取当前文件内容 + existingData, err := os.ReadFile(filePath) + if err != nil { + log.Printf("SaveJSONWithCheck: Failed to read existing file: %v", err) + return fmt.Errorf("failed to read existing file for conflict resolution: %w", err) + } + + // 调用冲突处理函数合并数据 + mergedData, err := onConflict(existingData) + if err != nil { + log.Printf("SaveJSONWithCheck: Conflict resolution failed: %v", err) + return fmt.Errorf("conflict resolution failed: %w", err) + } + + // 使用合并后的数据继续保存 + data = mergedData + log.Printf("SaveJSONWithCheck: Conflict resolved, proceeding with merged data") + } else { + // 没有提供冲突处理函数,返回错误 + return fmt.Errorf("concurrent modification detected") + } + } + } else { + log.Printf("SaveJSONWithCheck: Could not check file modification time: %v", err) + // 继续执行,不中断操作 + } + } + + // 确保目录存在 dir := filepath.Dir(filePath) - if err := fs.ensureDirNoLock(dir); err != nil { - log.Printf("SaveJSON: Failed to create directory: %v", err) + if err := fs.EnsureDir(dir); err != nil { + log.Printf("SaveJSONWithCheck: Failed to create directory: %v", err) return fmt.Errorf("failed to create directory: %w", err) } // 将数据编码为JSON jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { - log.Printf("SaveJSON: Failed to encode JSON: %v", err) + log.Printf("SaveJSONWithCheck: Failed to encode JSON: %v", err) return fmt.Errorf("failed to encode JSON: %w", err) } // 先写入临时文件 tempFile := filePath + ".tmp" - log.Printf("SaveJSON: Writing to temporary file: %s", tempFile) + log.Printf("SaveJSONWithCheck: Writing to temporary file: %s", tempFile) if err := os.WriteFile(tempFile, jsonData, 0644); err != nil { - log.Printf("SaveJSON: Failed to write temporary file: %v", err) + log.Printf("SaveJSONWithCheck: Failed to write temporary file: %v", err) return fmt.Errorf("failed to write temporary file: %w", err) } // 原子替换原文件 - log.Printf("SaveJSON: Replacing original file with temporary file") + log.Printf("SaveJSONWithCheck: Replacing original file with temporary file") if err := os.Rename(tempFile, filePath); err != nil { os.Remove(tempFile) // 清理临时文件 - log.Printf("SaveJSON: Failed to replace file: %v", err) + log.Printf("SaveJSONWithCheck: Failed to replace file: %v", err) return fmt.Errorf("failed to replace file: %w", err) } - log.Printf("SaveJSON: File saved successfully: %s", filePath) + log.Printf("SaveJSONWithCheck: File saved successfully: %s", filePath) return nil } +// SaveJSON 保存JSON数据到文件 +func (fs *FileService) SaveJSON(filePath string, data interface{}) error { + // 调用带并发检查的版本,但不提供冲突解决函数(保持原有行为) + return fs.SaveJSONWithCheck(filePath, data, time.Time{}, nil) +} + // LoadJSON 从文件加载JSON数据 func (fs *FileService) LoadJSON(filePath string, target interface{}) error { - fs.mutex.Lock() - defer fs.mutex.Unlock() - log.Printf("LoadJSON: Loading from file: %s", filePath) // 检查文件是否存在 @@ -126,9 +178,6 @@ func (fs *FileService) FileExists(filePath string) bool { // DeleteFile 删除文件 func (fs *FileService) DeleteFile(filePath string) error { - fs.mutex.Lock() - defer fs.mutex.Unlock() - log.Printf("DeleteFile: Deleting file: %s", filePath) if !fs.FileExists(filePath) {