// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Linq; using Duplicati.Library.Logging; using System.Collections.Generic; using Duplicati.Library.Interface; namespace Duplicati.Server { /// /// Class that handles logging from the server, /// and provides an entry point for the runner /// to redirect log output to a file /// public class LogWriteHandler : ILogDestination, IDisposable { /// /// The number of messages to keep when inactive /// private const int INACTIVE_SIZE = 30; /// /// The number of messages to keep when active /// private const int ACTIVE_SIZE = 5000; /// /// The context key used for conveying the backup ID /// public const string LOG_EXTRA_BACKUPID = "BackupID"; /// /// The context key used for conveying the task ID /// public const string LOG_EXTRA_TASKID = "TaskID"; /// /// Represents a single log event /// public struct LogEntry { /// /// A unique ID that sequentially increments /// private static long _id; /// /// The time the message was logged /// public readonly DateTime When; /// /// The ID assigned to the message /// public readonly long ID; /// /// The logged message /// public readonly string Message; /// /// The log tag /// public readonly string Tag; /// /// The message ID /// public readonly string MessageID; /// /// The message ID /// public readonly string ExceptionID; /// /// The message type /// public readonly LogMessageType Type; /// /// Exception data attached to the message /// public readonly Exception Exception; /// /// The backup ID, if any /// public readonly string BackupID; /// /// The task ID, if any /// public readonly string TaskID; /// /// Initializes a new instance of the struct. /// /// The log entry to store public LogEntry(Duplicati.Library.Logging.LogEntry entry) { this.ID = System.Threading.Interlocked.Increment(ref _id); this.When = entry.When; this.Message = entry.FormattedMessage; this.Type = entry.Level; this.Exception = entry.Exception; this.Tag = entry.FilterTag; this.MessageID = entry.Id; this.BackupID = entry[LOG_EXTRA_BACKUPID]; this.TaskID = entry[LOG_EXTRA_TASKID]; if (entry.Exception == null) this.ExceptionID = null; else if (entry.Exception is UserInformationException exception) this.ExceptionID = exception.HelpID; else this.ExceptionID = entry.Exception.GetType().FullName; } } /// /// Basic implementation of a ring-buffer /// private class RingBuffer : IEnumerable { private readonly T[] m_buffer; private int m_head; private int m_tail; private int m_length; private int m_key; private readonly object m_lock = new object(); public RingBuffer(int size, IEnumerable initial = null) { m_buffer = new T[size]; if (initial != null) foreach (var t in initial) this.Enqueue(t); } public int Length { get { return m_length; } } public void Enqueue(T item) { lock (m_lock) { m_key++; m_buffer[m_head] = item; m_head = (m_head + 1) % m_buffer.Length; if (m_length == m_buffer.Length) m_tail = (m_tail + 1) % m_buffer.Length; else m_length++; } } #region IEnumerable implementation public IEnumerator GetEnumerator() { var k = m_key; for (var i = 0; i < m_length; i++) if (m_key != k) throw new InvalidOperationException("Buffer was modified while reading"); else yield return m_buffer[(m_tail + i) % m_buffer.Length]; } #endregion #region IEnumerable implementation System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion public T[] FlatArray(Func filter = null) { lock (m_lock) if (filter == null) return this.ToArray(); else return this.Where(filter).ToArray(); } public int Size { get { return m_buffer.Length; } } } private readonly DateTime[] m_timeouts; private readonly object m_lock = new object(); private volatile bool m_anytimeouts = false; private RingBuffer m_buffer; private ILogDestination m_serverfile; private LogMessageType m_serverloglevel; private LogMessageType m_logLevel; public LogWriteHandler() { var fields = Enum.GetValues(typeof(LogMessageType)); m_timeouts = new DateTime[fields.Length]; m_buffer = new RingBuffer(INACTIVE_SIZE); } public void RenewTimeout(LogMessageType type) { lock (m_lock) { m_timeouts[(int)type] = DateTime.Now.AddSeconds(30); m_anytimeouts = true; if (m_buffer == null || m_buffer.Size == INACTIVE_SIZE) m_buffer = new RingBuffer(ACTIVE_SIZE, m_buffer); } } public void SetServerFile(string path, LogMessageType level) { var dir = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)); if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir); m_serverfile = new StreamLogDestination(path); m_serverloglevel = level; UpdateLogLevel(); } public LogEntry[] AfterTime(DateTime offset, LogMessageType level) { RenewTimeout(level); UpdateLogLevel(); offset = offset.ToUniversalTime(); lock (m_lock) { if (m_buffer == null) return new LogEntry[0]; return m_buffer.FlatArray((x) => x.When > offset && x.Type >= level); } } public LogEntry[] AfterID(long id, LogMessageType level, int pagesize) { RenewTimeout(level); UpdateLogLevel(); lock (m_lock) { if (m_buffer == null) return new LogEntry[0]; var buffer = m_buffer.FlatArray((x) => x.ID > id && x.Type >= level); // Return the newest entries if (buffer.Length > pagesize) { var index = buffer.Length - pagesize; return buffer.Skip(index).Take(pagesize).ToArray(); } else { return buffer; } } } private int[] GetActiveTimeouts() { var i = 0; return (from n in m_timeouts let ix = i++ where n > DateTime.Now select ix).ToArray(); } private void UpdateLogLevel() { m_logLevel = (LogMessageType)(GetActiveTimeouts().Union(new int[] { (int)m_serverloglevel }).Min()); } #region ILog implementation public void WriteMessage(Duplicati.Library.Logging.LogEntry entry) { if (entry.Level < m_logLevel) return; if (m_serverfile != null && entry.Level >= m_serverloglevel) try { m_serverfile.WriteMessage(entry); } catch { } lock (m_lock) { if (m_anytimeouts) { var q = GetActiveTimeouts(); if (q.Length == 0) { UpdateLogLevel(); m_anytimeouts = false; if (m_buffer == null || m_buffer.Size != INACTIVE_SIZE) m_buffer = new RingBuffer(INACTIVE_SIZE, m_buffer); } } if (m_buffer != null) m_buffer.Enqueue(new LogEntry(entry)); } } #endregion #region IDisposable implementation public void Dispose() { if (m_serverfile != null) { var sf = m_serverfile; m_serverfile = null; if (sf is IDisposable disposable) disposable.Dispose(); } } #endregion } }