/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System; using System.IO; using System.IO.MemoryMappedFiles; using System.Text; using System.Collections.Generic; namespace ProtonVPN.Vpn.WireGuard; public class RingLogger { private bool _started; private readonly struct UnixTimestamp { private readonly long _ns; public UnixTimestamp(long ns) { _ns = ns; } public bool IsEmpty => _ns == 0; public override string ToString() { return DateTimeOffset.FromUnixTimeSeconds(_ns / 1000000000).LocalDateTime .ToString("yyyy'-'MM'-'dd HH':'mm':'ss'.'") + (_ns % 1000000000 + "00000").Substring(0, 6); } } private readonly struct Line { private const int MAX_LINE_LENGTH = 512; private const int OFFSET_TIME_NS = 0; private const int OFFSET_LINE = 8; private readonly MemoryMappedViewAccessor _view; private readonly int _start; public Line(MemoryMappedViewAccessor view, uint index) { (_view, _start) = (view, (int)(Log.HeaderBytes + index * Bytes)); } public static int Bytes => MAX_LINE_LENGTH + OFFSET_LINE; public UnixTimestamp Timestamp => new(_view.ReadInt64(_start + OFFSET_TIME_NS)); private string? Text { get { byte[] textBytes = new byte[MAX_LINE_LENGTH]; _view.ReadArray(_start + OFFSET_LINE, textBytes, 0, textBytes.Length); int nullByte = Array.IndexOf(textBytes, 0); if (nullByte <= 0) { return null; } return Encoding.UTF8.GetString(textBytes, 0, nullByte); } } public override string? ToString() { UnixTimestamp time = Timestamp; if (time.IsEmpty) { return null; } string? text = Text; if (text == null) { return null; } return $"{time}: {text}"; } } private struct Log { private const uint MAX_LINES = 2048; private const int OFFSET_NEXT_INDEX = 4; private const int OFFSET_LINES = 8; private readonly MemoryMappedViewAccessor _view; public Log(MemoryMappedViewAccessor view) { _view = view; } public static int HeaderBytes => OFFSET_LINES; public static int Bytes => (int)(HeaderBytes + Line.Bytes * MAX_LINES); public uint NextIndex => _view.ReadUInt32(OFFSET_NEXT_INDEX); public uint LineCount => MAX_LINES; public Line this[uint i] => new(_view, i % MAX_LINES); } private Log _log; private MemoryMappedFile? _memoryMappedFile; private MemoryMappedViewAccessor? _memoryMappedViewAccessor; private readonly string _filename; public RingLogger(string filename) { _filename = filename; } public static readonly uint CursorAll = uint.MaxValue; public void Start() { DeleteFile(); FileStream file = File.Open(_filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete); file.SetLength(Log.Bytes); _memoryMappedFile = MemoryMappedFile.CreateFromFile(file, null, 0, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, false); _memoryMappedViewAccessor = _memoryMappedFile.CreateViewAccessor(0, Log.Bytes, MemoryMappedFileAccess.ReadWrite); _log = new Log(_memoryMappedViewAccessor); _started = true; } public void Stop() { _started = false; _memoryMappedViewAccessor?.Dispose(); _memoryMappedFile?.Dispose(); } private void DeleteFile() { if (File.Exists(_filename)) { try { File.Delete(_filename); } catch (Exception) { // ignored } } } public List FollowFromCursor(ref uint cursor) { if (!_started) { return []; } List lines = new((int)_log.LineCount); uint i = cursor; bool all = cursor == CursorAll; if (all) { i = _log.NextIndex; } for (uint l = 0; l < _log.LineCount; ++l, ++i) { if (!all && i % _log.LineCount == _log.NextIndex % _log.LineCount) { break; } Line entry = _log[i]; if (entry.Timestamp.IsEmpty) { if (all) { continue; } break; } cursor = (i + 1) % _log.LineCount; string? entryString = entry.ToString(); if (!string.IsNullOrEmpty(entryString)) { lines.Add(entryString); } } return lines; } }