using System; using System.Globalization; using System.IO; using System.Text.RegularExpressions; using Newtonsoft.Json; namespace Lskj.AgentBridge { /// /// Publishes the local ERP bridge discovery document as one durable, /// atomic filesystem transition. Readers therefore observe either the /// previous complete generation or the new complete generation. /// public static class BridgeDiscoveryPublisher { internal const int MaximumDocumentBytes = 4096; private static readonly Regex SafePipeName = new Regex( "^[A-Za-z0-9_.-]{1,128}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex SafeBridgeInstanceId = new Regex( "^[a-f0-9]{32}$", RegexOptions.Compiled | RegexOptions.CultureInvariant); public static string Publish( string directory, string pipeName, int processId, DateTime processStartedAtUtc, string bridgeInstanceId) { if (string.IsNullOrWhiteSpace(directory) || !Path.IsPathRooted(directory)) throw new ArgumentException("发现目录必须是绝对路径。", "directory"); if (string.IsNullOrWhiteSpace(pipeName) || !SafePipeName.IsMatch(pipeName)) throw new ArgumentException("命名管道名称无效。", "pipeName"); if (processId <= 0) throw new ArgumentOutOfRangeException("processId"); if (string.IsNullOrWhiteSpace(bridgeInstanceId) || !SafeBridgeInstanceId.IsMatch(bridgeInstanceId)) throw new ArgumentException("ERP 桥实例标识无效。", "bridgeInstanceId"); string expectedPipeName = "lserp.agent." + processId.ToString(CultureInfo.InvariantCulture) + "." + bridgeInstanceId; if (!string.Equals(pipeName, expectedPipeName, StringComparison.Ordinal)) throw new ArgumentException( "命名管道名称必须绑定 ERP 进程号和桥实例。", "pipeName"); if (processStartedAtUtc.Kind != DateTimeKind.Utc) throw new ArgumentException("ERP 进程启动时间必须是 UTC。", "processStartedAtUtc"); string fullDirectory = Path.GetFullPath(directory); Directory.CreateDirectory(fullDirectory); string destination = Path.Combine( fullDirectory, "agentbridge-" + processId.ToString(CultureInfo.InvariantCulture) + ".json"); string temporary = Path.Combine( fullDirectory, ".agentbridge-" + processId.ToString(CultureInfo.InvariantCulture) + "-" + Guid.NewGuid().ToString("N") + ".tmp"); string json = JsonConvert.SerializeObject(new { protocolVersion = AgentBridgeRuntime.ProtocolVersion, pipeName = pipeName, processId = processId, startedAtUtc = processStartedAtUtc.ToString("o", CultureInfo.InvariantCulture), bridgeInstanceId = bridgeInstanceId }); byte[] body = BridgeUtf8Codec.Encode(json); if (body.Length == 0 || body.Length > MaximumDocumentBytes) throw new InvalidDataException("ERP 桥发现文档长度无效。"); try { using (FileStream stream = new FileStream( temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { stream.Write(body, 0, body.Length); stream.Flush(true); } if (File.Exists(destination)) File.Replace(temporary, destination, null, true); else File.Move(temporary, destination); return destination; } finally { try { if (File.Exists(temporary)) File.Delete(temporary); } catch (IOException) { } catch (UnauthorizedAccessException) { } } } } }