| | |
| | | using System; |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.IO; |
| | | using System.Linq; |
| | | using System.Text; |
| | | using System.Text.Json; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | using WIDESEAWCS_S7Simulator.Core.Enums; |
| | |
| | | namespace WIDESEAWCS_S7Simulator.Core.Persistence |
| | | { |
| | | /// <summary> |
| | | /// 文件持久化服务实现 |
| | | /// 将实例配置和内存数据保存到本地JSON文件 |
| | | /// 鏂囦欢鎸佷箙鍖栨湇鍔″疄鐜? |
| | | /// 灏嗗疄渚嬮厤缃拰鍐呭瓨鏁版嵁淇濆瓨鍒版湰鍦癑SON鏂囦欢 |
| | | /// </summary> |
| | | public class FilePersistenceService : IPersistenceService |
| | | { |
| | | /// <summary> |
| | | /// 数据目录路径 |
| | | /// 鏁版嵁鐩綍璺緞 |
| | | /// </summary> |
| | | private readonly string _dataPath; |
| | | |
| | | /// <summary> |
| | | /// JSON序列化选项 |
| | | /// JSON搴忓垪鍖栭€夐」锛堢嚎绋嬪畨鍏級 |
| | | /// </summary> |
| | | private readonly JsonSerializerOptions _jsonOptions; |
| | | private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions |
| | | { |
| | | WriteIndented = true, |
| | | PropertyNamingPolicy = JsonNamingPolicy.CamelCase |
| | | }; |
| | | |
| | | /// <summary> |
| | | /// 构造函数 |
| | | /// 鏂囦欢鎿嶄綔閿侊紙绾跨▼瀹夊叏锛? |
| | | /// </summary> |
| | | /// <param name="dataPath">数据目录路径</param> |
| | | private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1); |
| | | |
| | | /// <summary> |
| | | /// 鏋勯€犲嚱鏁? |
| | | /// </summary> |
| | | /// <param name="dataPath">鏁版嵁鐩綍璺緞</param> |
| | | public FilePersistenceService(string dataPath = "Data") |
| | | { |
| | | _dataPath = dataPath; |
| | | _jsonOptions = new JsonSerializerOptions |
| | | { |
| | | WriteIndented = true, |
| | | PropertyNamingPolicy = JsonNamingPolicy.CamelCase |
| | | }; |
| | | // 杞崲涓虹粷瀵硅矾寰勶紙鍩轰簬褰撳墠宸ヤ綔鐩綍锛? |
| | | _dataPath = Path.GetFullPath(dataPath); |
| | | |
| | | // 确保数据目录存在 |
| | | if (!Directory.Exists(_dataPath)) |
| | | try |
| | | { |
| | | Directory.CreateDirectory(_dataPath); |
| | | // 纭繚鏁版嵁鐩綍瀛樺湪 |
| | | if (!Directory.Exists(_dataPath)) |
| | | { |
| | | Directory.CreateDirectory(_dataPath); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"Error creating data directory '{_dataPath}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存实例配置 |
| | | /// 淇濆瓨瀹炰緥閰嶇疆 |
| | | /// </summary> |
| | | public async Task SaveInstanceConfigAsync(InstanceConfig config) |
| | | { |
| | | if (config == null) |
| | | throw new ArgumentNullException(nameof(config)); |
| | | |
| | | var instanceDir = GetInstanceDirectory(config.Id); |
| | | if (!Directory.Exists(instanceDir)) |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | Directory.CreateDirectory(instanceDir); |
| | | var instanceDir = GetInstanceDirectory(config.Id); |
| | | if (!Directory.Exists(instanceDir)) |
| | | { |
| | | Directory.CreateDirectory(instanceDir); |
| | | } |
| | | |
| | | var configPath = Path.Combine(instanceDir, "config.json"); |
| | | var model = ToDataModel(config); |
| | | |
| | | var json = JsonSerializer.Serialize(model, _jsonOptions); |
| | | await WriteFileAtomicAsync(configPath, json); |
| | | } |
| | | |
| | | var configPath = Path.Combine(instanceDir, "config.json"); |
| | | var model = ToDataModel(config); |
| | | |
| | | var json = JsonSerializer.Serialize(model, _jsonOptions); |
| | | await File.WriteAllTextAsync(configPath, json); |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"Error saving instance config for '{config.Id}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载实例配置 |
| | | /// 鍔犺浇瀹炰緥閰嶇疆 |
| | | /// </summary> |
| | | public async Task<InstanceConfig> LoadInstanceConfigAsync(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json"); |
| | | if (!File.Exists(configPath)) |
| | | throw new FileNotFoundException($"实例配置文件不存在: {configPath}"); |
| | | ValidateInstanceId(instanceId); |
| | | |
| | | var json = await File.ReadAllTextAsync(configPath); |
| | | var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions); |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json"); |
| | | if (!File.Exists(configPath)) |
| | | throw new FileNotFoundException($"瀹炰緥閰嶇疆鏂囦欢涓嶅瓨鍦? {configPath}"); |
| | | |
| | | if (model == null) |
| | | throw new InvalidOperationException("无法反序列化实例配置"); |
| | | var json = await File.ReadAllTextAsync(configPath); |
| | | var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions); |
| | | |
| | | return ToEntity(model); |
| | | if (model == null) |
| | | throw new InvalidOperationException("鏃犳硶鍙嶅簭鍒楀寲瀹炰緥閰嶇疆"); |
| | | |
| | | return ToEntity(model); |
| | | } |
| | | catch (Exception ex) when (!(ex is FileNotFoundException || ex is InvalidOperationException || ex is ArgumentException)) |
| | | { |
| | | Console.WriteLine($"Error loading instance config for '{instanceId}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载所有实例配置 |
| | | /// 鍔犺浇鎵€鏈夊疄渚嬮厤缃? |
| | | /// </summary> |
| | | public async Task<List<InstanceConfig>> LoadAllInstanceConfigsAsync() |
| | | { |
| | | var configs = new List<InstanceConfig>(); |
| | | |
| | | if (!Directory.Exists(_dataPath)) |
| | | return configs; |
| | | |
| | | var instanceDirs = Directory.GetDirectories(_dataPath) |
| | | .Where(d => Path.GetFileName(d).StartsWith("instance-")) |
| | | .ToList(); |
| | | |
| | | foreach (var dir in instanceDirs) |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | var configPath = Path.Combine(dir, "config.json"); |
| | | if (File.Exists(configPath)) |
| | | var configs = new List<InstanceConfig>(); |
| | | |
| | | if (!Directory.Exists(_dataPath)) |
| | | return configs; |
| | | |
| | | var instanceDirs = Directory.GetDirectories(_dataPath) |
| | | .Where(d => Path.GetFileName(d).StartsWith("instance-")) |
| | | .ToList(); |
| | | |
| | | foreach (var dir in instanceDirs) |
| | | { |
| | | try |
| | | var configPath = Path.Combine(dir, "config.json"); |
| | | if (File.Exists(configPath)) |
| | | { |
| | | var json = await File.ReadAllTextAsync(configPath); |
| | | var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions); |
| | | if (model != null) |
| | | try |
| | | { |
| | | configs.Add(ToEntity(model)); |
| | | var json = await File.ReadAllTextAsync(configPath); |
| | | var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions); |
| | | if (model != null) |
| | | { |
| | | configs.Add(ToEntity(model)); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"Error loading instance config from '{configPath}': {ex.Message}"); |
| | | // 璺宠繃鏃犳硶鍔犺浇鐨勯厤缃枃浠? |
| | | continue; |
| | | } |
| | | } |
| | | catch (Exception) |
| | | { |
| | | // 跳过无法加载的配置文件 |
| | | continue; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return configs; |
| | | return configs; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 删除实例配置 |
| | | /// 鍒犻櫎瀹炰緥閰嶇疆 |
| | | /// </summary> |
| | | public Task DeleteInstanceConfigAsync(string instanceId) |
| | | public async Task DeleteInstanceConfigAsync(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | var instanceDir = GetInstanceDirectory(instanceId); |
| | | if (Directory.Exists(instanceDir)) |
| | | ValidateInstanceId(instanceId); |
| | | |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | Directory.Delete(instanceDir, recursive: true); |
| | | var instanceDir = GetInstanceDirectory(instanceId); |
| | | if (Directory.Exists(instanceDir)) |
| | | { |
| | | Directory.Delete(instanceDir, recursive: true); |
| | | } |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"Error deleting instance '{instanceId}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存内存数据 |
| | | /// 淇濆瓨鍐呭瓨鏁版嵁 |
| | | /// </summary> |
| | | public async Task SaveMemoryDataAsync(string instanceId, IMemoryStore memoryStore) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | ValidateInstanceId(instanceId); |
| | | |
| | | if (memoryStore == null) |
| | | throw new ArgumentNullException(nameof(memoryStore)); |
| | | |
| | | var instanceDir = GetInstanceDirectory(instanceId); |
| | | if (!Directory.Exists(instanceDir)) |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | Directory.CreateDirectory(instanceDir); |
| | | var instanceDir = GetInstanceDirectory(instanceId); |
| | | if (!Directory.Exists(instanceDir)) |
| | | { |
| | | Directory.CreateDirectory(instanceDir); |
| | | } |
| | | |
| | | var memoryPath = Path.Combine(instanceDir, "memory.json"); |
| | | var exportedData = memoryStore.Export(); |
| | | |
| | | // 灏嗗瓧鑺傛暟缁勮浆鎹负Base64瀛楃涓蹭互渚縅SON搴忓垪鍖? |
| | | var memoryDataModel = new MemoryDataModel |
| | | { |
| | | MemoryData = exportedData.ToDictionary( |
| | | kvp => kvp.Key, |
| | | kvp => Convert.ToBase64String(kvp.Value) |
| | | ) |
| | | }; |
| | | |
| | | var json = JsonSerializer.Serialize(memoryDataModel, _jsonOptions); |
| | | await WriteFileAtomicAsync(memoryPath, json); |
| | | } |
| | | |
| | | var memoryPath = Path.Combine(instanceDir, "memory.json"); |
| | | var exportedData = memoryStore.Export(); |
| | | |
| | | // 将字节数组转换为Base64字符串以便JSON序列化 |
| | | var memoryDataModel = new MemoryDataModel |
| | | catch (Exception ex) |
| | | { |
| | | MemoryData = exportedData.ToDictionary( |
| | | kvp => kvp.Key, |
| | | kvp => Convert.ToBase64String(kvp.Value) |
| | | ) |
| | | }; |
| | | |
| | | var json = JsonSerializer.Serialize(memoryDataModel, _jsonOptions); |
| | | await File.WriteAllTextAsync(memoryPath, json); |
| | | Console.WriteLine($"Error saving memory data for instance '{instanceId}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载内存数据 |
| | | /// 鍔犺浇鍐呭瓨鏁版嵁 |
| | | /// </summary> |
| | | public async Task LoadMemoryDataAsync(string instanceId, IMemoryStore memoryStore) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("实例ID不能为空", nameof(instanceId)); |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | ValidateInstanceId(instanceId); |
| | | |
| | | if (memoryStore == null) |
| | | throw new ArgumentNullException(nameof(memoryStore)); |
| | | |
| | | var memoryPath = Path.Combine(GetInstanceDirectory(instanceId), "memory.json"); |
| | | if (!File.Exists(memoryPath)) |
| | | return; // 内存文件不存在,跳过加载 |
| | | |
| | | var json = await File.ReadAllTextAsync(memoryPath); |
| | | var memoryDataModel = JsonSerializer.Deserialize<MemoryDataModel>(json, _jsonOptions); |
| | | |
| | | if (memoryDataModel?.MemoryData == null) |
| | | return; |
| | | |
| | | // 将Base64字符串转换回字节数组 |
| | | var importedData = new Dictionary<string, byte[]>(); |
| | | foreach (var kvp in memoryDataModel.MemoryData) |
| | | await _fileLock.WaitAsync(); |
| | | try |
| | | { |
| | | try |
| | | { |
| | | importedData[kvp.Key] = Convert.FromBase64String(kvp.Value); |
| | | } |
| | | catch (FormatException) |
| | | { |
| | | // 跳过无效的Base64数据 |
| | | continue; |
| | | } |
| | | } |
| | | var memoryPath = Path.Combine(GetInstanceDirectory(instanceId), "memory.json"); |
| | | if (!File.Exists(memoryPath)) |
| | | return; // 鍐呭瓨鏂囦欢涓嶅瓨鍦紝璺宠繃鍔犺浇 |
| | | |
| | | memoryStore.Import(importedData); |
| | | var json = await File.ReadAllTextAsync(memoryPath); |
| | | var memoryDataModel = JsonSerializer.Deserialize<MemoryDataModel>(json, _jsonOptions); |
| | | |
| | | if (memoryDataModel?.MemoryData == null) |
| | | return; |
| | | |
| | | // 灏咮ase64瀛楃涓茶浆鎹㈠洖瀛楄妭鏁扮粍 |
| | | var importedData = new Dictionary<string, byte[]>(); |
| | | foreach (var kvp in memoryDataModel.MemoryData) |
| | | { |
| | | try |
| | | { |
| | | importedData[kvp.Key] = Convert.FromBase64String(kvp.Value); |
| | | } |
| | | catch (FormatException ex) |
| | | { |
| | | Console.WriteLine($"Warning: Invalid Base64 data for memory region '{kvp.Key}' in instance '{instanceId}': {ex.Message}"); |
| | | // 璺宠繃鏃犳晥鐨凚ase64鏁版嵁 |
| | | continue; |
| | | } |
| | | } |
| | | |
| | | memoryStore.Import(importedData); |
| | | } |
| | | catch (Exception ex) when (!(ex is FileNotFoundException)) |
| | | { |
| | | Console.WriteLine($"Error loading memory data for instance '{instanceId}': {ex.Message}"); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | _fileLock.Release(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取实例目录路径 |
| | | /// 鑾峰彇瀹炰緥鐩綍璺緞 |
| | | /// </summary> |
| | | private string GetInstanceDirectory(string instanceId) |
| | | { |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 将实体转换为数据模型 |
| | | /// 楠岃瘉瀹炰緥ID锛堥槻姝㈣矾寰勯亶鍘嗘敾鍑伙級 |
| | | /// </summary> |
| | | private void ValidateInstanceId(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId)); |
| | | |
| | | // 妫€鏌ヨ矾寰勯亶鍘嗗瓧绗? |
| | | if (instanceId.Contains("..") || instanceId.Contains("/") || instanceId.Contains("\\")) |
| | | throw new ArgumentException("瀹炰緥ID鍖呭惈闈炴硶瀛楃", nameof(instanceId)); |
| | | |
| | | // 妫€鏌ユ棤鏁堣矾寰勫瓧绗? |
| | | var invalidChars = Path.GetInvalidFileNameChars(); |
| | | if (instanceId.IndexOfAny(invalidChars) >= 0) |
| | | throw new ArgumentException("瀹炰緥ID鍖呭惈闈炴硶瀛楃", nameof(instanceId)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 鍐欏叆鏂囦欢锛堢洿鎺ュ啓鍏ワ紝绠€鍖栫増鏈級 |
| | | /// </summary> |
| | | private async Task WriteFileAtomicAsync(string filePath, string content) |
| | | { |
| | | // 纭繚鐩爣鏂囦欢鐨勭埗鐩綍瀛樺湪 |
| | | var directory = Path.GetDirectoryName(filePath); |
| | | if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) |
| | | { |
| | | Directory.CreateDirectory(directory); |
| | | } |
| | | |
| | | // 鐩存帴鍐欏叆鏂囦欢 |
| | | await File.WriteAllTextAsync(filePath, content, Encoding.UTF8); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 灏嗗疄浣撹浆鎹负鏁版嵁妯″瀷 |
| | | /// </summary> |
| | | private InstanceDataModel ToDataModel(InstanceConfig config) |
| | | { |
| | |
| | | Port = config.Port, |
| | | ActivationKey = config.ActivationKey, |
| | | AutoStart = config.AutoStart, |
| | | ProtocolTemplateId = config.ProtocolTemplateId, |
| | | MemoryConfig = new MemoryRegionConfigModel |
| | | { |
| | | MRegionSize = config.MemoryConfig.MRegionSize, |
| | | DBBlockCount = config.MemoryConfig.DBBlockCount, |
| | | DBBlockNumbers = config.MemoryConfig.DBBlockNumbers.ToList(), |
| | | DBBlockSize = config.MemoryConfig.DBBlockSize, |
| | | IRegionSize = config.MemoryConfig.IRegionSize, |
| | | QRegionSize = config.MemoryConfig.QRegionSize, |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 将数据模型转换为实体 |
| | | /// 灏嗘暟鎹ā鍨嬭浆鎹负瀹炰綋 |
| | | /// </summary> |
| | | private InstanceConfig ToEntity(InstanceDataModel model) |
| | | { |
| | |
| | | Port = model.Port, |
| | | ActivationKey = model.ActivationKey, |
| | | AutoStart = model.AutoStart, |
| | | ProtocolTemplateId = model.ProtocolTemplateId, |
| | | MemoryConfig = new MemoryRegionConfig |
| | | { |
| | | MRegionSize = model.MemoryConfig.MRegionSize, |
| | | DBBlockCount = model.MemoryConfig.DBBlockCount, |
| | | DBBlockNumbers = model.MemoryConfig.DBBlockNumbers?.ToList() ?? new List<int>(), |
| | | DBBlockSize = model.MemoryConfig.DBBlockSize, |
| | | IRegionSize = model.MemoryConfig.IRegionSize, |
| | | QRegionSize = model.MemoryConfig.QRegionSize, |
| | |
| | | } |
| | | } |
| | | } |
| | | |