// Copyright (C) 2015, The Duplicati Team // http://www.duplicati.com, info@duplicati.com // // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation; either version 2.1 of the // License, or (at your option) any later version. // // This library 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 // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 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 } }