using System; using System.IO; using System.IO.MemoryMappedFiles; using System.Text; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Stability; /// /// Ring-buffer of the last N IPC operations, written into a memory-mapped file. On a /// hard crash the Proxy-side supervisor reads the MMF after the corpse is gone to see /// what was in flight at the moment the Host died. Single-writer (the Host), multi-reader /// (the supervisor) — the file format is identical to the Galaxy Tier-C /// PostMortemMmf so a single reader tool can work both. /// /// /// File layout: /// /// [16-byte header: magic(4) | version(4) | capacity(4) | writeIndex(4)] /// [capacity × 256-byte entries: each is [8-byte utcUnixMs | 8-byte opKind | 240-byte UTF-8 message]] /// /// Magic is 'OFPC' (0x4F46_5043) to distinguish a FOCAS file from the Galaxy MMF. /// public sealed class PostMortemMmf : IDisposable { private const int Magic = 0x4F465043; // 'OFPC' private const int Version = 1; private const int HeaderBytes = 16; public const int EntryBytes = 256; private const int MessageOffset = 16; private const int MessageCapacity = EntryBytes - MessageOffset; public int Capacity { get; } public string Path { get; } private readonly MemoryMappedFile _mmf; private readonly MemoryMappedViewAccessor _accessor; private readonly object _writeGate = new(); public PostMortemMmf(string path, int capacity = 1000) { if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); Capacity = capacity; Path = path; var fileBytes = HeaderBytes + capacity * EntryBytes; Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)!); var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); fs.SetLength(fileBytes); _mmf = MemoryMappedFile.CreateFromFile(fs, null, fileBytes, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, leaveOpen: false); _accessor = _mmf.CreateViewAccessor(0, fileBytes, MemoryMappedFileAccess.ReadWrite); if (_accessor.ReadInt32(0) != Magic) { _accessor.Write(0, Magic); _accessor.Write(4, Version); _accessor.Write(8, capacity); _accessor.Write(12, 0); } } public void Write(long opKind, string message) { lock (_writeGate) { var idx = _accessor.ReadInt32(12); var offset = HeaderBytes + idx * EntryBytes; _accessor.Write(offset + 0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); _accessor.Write(offset + 8, opKind); var msgBytes = Encoding.UTF8.GetBytes(message ?? string.Empty); var copy = Math.Min(msgBytes.Length, MessageCapacity - 1); _accessor.WriteArray(offset + MessageOffset, msgBytes, 0, copy); _accessor.Write(offset + MessageOffset + copy, (byte)0); var next = (idx + 1) % Capacity; _accessor.Write(12, next); } } public PostMortemEntry[] ReadAll() { var magic = _accessor.ReadInt32(0); if (magic != Magic) return new PostMortemEntry[0]; var capacity = _accessor.ReadInt32(8); var writeIndex = _accessor.ReadInt32(12); var entries = new PostMortemEntry[capacity]; var count = 0; for (var i = 0; i < capacity; i++) { var slot = (writeIndex + i) % capacity; var offset = HeaderBytes + slot * EntryBytes; var ts = _accessor.ReadInt64(offset + 0); if (ts == 0) continue; var op = _accessor.ReadInt64(offset + 8); var msgBuf = new byte[MessageCapacity]; _accessor.ReadArray(offset + MessageOffset, msgBuf, 0, MessageCapacity); var nulTerm = Array.IndexOf(msgBuf, 0); var msg = Encoding.UTF8.GetString(msgBuf, 0, nulTerm < 0 ? MessageCapacity : nulTerm); entries[count++] = new PostMortemEntry(ts, op, msg); } Array.Resize(ref entries, count); return entries; } public void Dispose() { _accessor.Dispose(); _mmf.Dispose(); } } public readonly struct PostMortemEntry { public long UtcUnixMs { get; } public long OpKind { get; } public string Message { get; } public PostMortemEntry(long utcUnixMs, long opKind, string message) { UtcUnixMs = utcUnixMs; OpKind = opKind; Message = message; } }