diff --git a/Implab/Component.cs b/Implab/Component.cs new file mode 100644 --- /dev/null +++ b/Implab/Component.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace Implab { + /// + /// Компоненты являются состовными объектами, имеют детерминированный период жизни, автоматически освобождают ресурсы входящие в них. + /// + /// Компонента управляет временем жизни включенных в нее компонент + public class Component: Disposable { + LinkedList m_components = new LinkedList(); + + /// + /// Коллекция компонент, из которых состоит текущая компонента. + /// + public ICollection Components { + get { + AssertNotDisposed(); + return m_components; + } + } + + /// + /// Освобождает компоненты, входящие в состав текущей компоненты. + /// + /// Признак того, что происходит освобождение ресурсов. + protected override void Dispose(bool disposing) { + if (disposing) { + foreach (var item in m_components) + item.Dispose(); + m_components.Clear(); + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/Implab/Diagnostics/ConsoleTraceListener.cs b/Implab/Diagnostics/ConsoleTraceListener.cs --- a/Implab/Diagnostics/ConsoleTraceListener.cs +++ b/Implab/Diagnostics/ConsoleTraceListener.cs @@ -4,29 +4,16 @@ using System.Linq; using System.Text; namespace Implab.Diagnostics { - public class ConsoleTraceListener { + public class ConsoleTraceListener: TextListenerBase { static readonly object _consoleLock = new object(); - public void Subscribe() { - LogChannel.Default.Events += Default_Events; - } - - public void Unsubscribe() { - LogChannel.Default.Events -= Default_Events; - } + protected override void WriteEntry(TraceContext context, EventText text) { + var msg = new StringBuilder(); - void Default_Events(object sender, ValueEventArgs e) { - LogEvent((TraceContext)sender, e.Value); - } - - void LogEvent(TraceContext context, TraceEvent evt) { - var msg = new StringBuilder(); - for (int i = 0; i < context.CurrentOperation.Level; i++) + for (int i = 0; i < text.indent; i++) msg.Append(" "); - msg.Append(evt.EventType); - msg.AppendFormat("[{0}]: ",context.ThreadId); - msg.Append(evt.Message); + msg.AppendFormat("[{0}]: {1}", context.ThreadId, text.content); lock (_consoleLock) { Console.ForegroundColor = (ConsoleColor)(context.ThreadId % 15 + 1); diff --git a/Implab/Diagnostics/EventText.cs b/Implab/Diagnostics/EventText.cs new file mode 100644 --- /dev/null +++ b/Implab/Diagnostics/EventText.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Implab.Diagnostics { + public struct EventText { + public int indent; + + public string content; + } +} diff --git a/Implab/Diagnostics/IEventTextFormatter.cs b/Implab/Diagnostics/IEventTextFormatter.cs new file mode 100644 --- /dev/null +++ b/Implab/Diagnostics/IEventTextFormatter.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Implab.Diagnostics { + public interface IEventTextFormatter { + EventText Format(TraceContext context, TEvent data); + } +} diff --git a/Implab/Diagnostics/TextFileListener.cs b/Implab/Diagnostics/TextFileListener.cs new file mode 100644 --- /dev/null +++ b/Implab/Diagnostics/TextFileListener.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Implab.Diagnostics { + public class TextFileListener: TextListenerBase { + readonly TextWriter m_textWriter; + + public TextFileListener(string fileName) { + m_textWriter = File.CreateText(fileName); + + m_textWriter.WriteLine("LOG {0}", DateTime.Now); + Register(this); + } + + protected override void WriteEntry(TraceContext context, EventText text) { + var msg = new StringBuilder(); + for (int i = 0; i < text.indent; i++) + msg.Append(" "); + msg.AppendFormat("[{0}]: {1}", context.ThreadId, text.content); + + lock (m_textWriter) { + if (!IsDisposed) { + m_textWriter.WriteLine(msg.ToString()); + m_textWriter.Flush(); + } + } + } + + + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (disposing) { + lock (m_textWriter) { + Safe.Dispose(m_textWriter); + } + } + } + + + } +} diff --git a/Implab/Diagnostics/TextListenerBase.cs b/Implab/Diagnostics/TextListenerBase.cs new file mode 100644 --- /dev/null +++ b/Implab/Diagnostics/TextListenerBase.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Implab.Diagnostics { + public abstract class TextListenerBase : ServiceLocator, IEventTextFormatter, IEventTextFormatter { + + readonly Dictionary m_subscriptions = new Dictionary(); + + protected TextListenerBase() { + Register(this); + } + + public void Subscribe(Type eventType) { + if (eventType == null) + throw new ArgumentNullException("eventType"); + GetType().GetMethod("Subscribe", new Type[0]).MakeGenericMethod(eventType).Invoke(this, null); + } + + public void Subscribe() { + Subscribe(LogChannel.Default); + } + + public void Subscribe(LogChannel channel) { + if (channel == null) + throw new ArgumentNullException("channel"); + + lock (m_subscriptions) { + AssertNotDisposed(); + + var formatter = GetService>(); + + EventHandler> handler = (sender, args) => { + WriteEntry((TraceContext)sender, formatter.Format((TraceContext)sender, args.Value)); + }; + + if (m_subscriptions.ContainsKey(channel)) + return; + + channel.Events += handler; + + Action unsubscribe = () => { + channel.Events -= handler; + }; + + m_subscriptions.Add(channel, unsubscribe); + } + } + + public void Unsubscribe(LogChannel channel) { + if (channel == null) + throw new ArgumentNullException("channel"); + + lock (m_subscriptions) { + Action subscription; + if (m_subscriptions.TryGetValue(channel, out subscription)) { + subscription(); + m_subscriptions.Remove(channel); + } + } + } + + public void UnsubscribeAll() { + lock (m_subscriptions) { + foreach (var subscription in m_subscriptions.Values) + subscription(); + m_subscriptions.Clear(); + } + } + + protected abstract void WriteEntry(TraceContext context, EventText text); + + public EventText Format(TraceContext context, object data) { + return new EventText { + indent = context.CurrentOperation.Level, + content = data.ToString() + }; + } + + public EventText Format(TraceContext context, TraceEvent data) { + var level = context.CurrentOperation.Level; + if (data.EventType == TraceEventType.OperationCompleted || data.EventType == TraceEventType.OperationStarted) + level--; + + return new EventText { + indent = level, + content = data.ToString() + }; + } + + protected override void Dispose(bool disposing) { + if (disposing) { + UnsubscribeAll(); + } + base.Dispose(disposing); + } + } +} diff --git a/Implab/Diagnostics/TraceContext.cs b/Implab/Diagnostics/TraceContext.cs --- a/Implab/Diagnostics/TraceContext.cs +++ b/Implab/Diagnostics/TraceContext.cs @@ -6,17 +6,23 @@ using System.Threading; using System.Threading.Tasks; namespace Implab.Diagnostics { + /// + /// Контекст трассировки, привязывается к потоку и содержит в себе информацию о стеке логических операций. + /// + /// + /// Контекст трассировки передается слушателям событий для определения места, где возникло событие. + /// public class TraceContext { LogicalOperation m_currentOperation; - readonly LogicalOperation m_traceBound; + readonly LogicalOperation m_bound; readonly int m_threadId; - readonly TraceContext m_parent; - - readonly static object _consoleLock = new object(); [ThreadStatic] static TraceContext _current; + /// + /// Текущий контекст трассировки для потока, создается астоматически при первом обращении. + /// public static TraceContext Current { get { if (_current == null) @@ -29,64 +35,124 @@ namespace Implab.Diagnostics { if (context == null) throw new ArgumentNullException("context"); - m_parent = context; m_currentOperation = context.CurrentOperation; - m_traceBound = context.CurrentOperation; + m_bound = context.CurrentOperation; m_threadId = Thread.CurrentThread.ManagedThreadId; - - LogEvent(TraceEventType.Transfer, "FORK {0}", context.ThreadId); } TraceContext() { m_currentOperation = new LogicalOperation(); - m_traceBound = m_currentOperation; + m_bound = m_currentOperation; m_threadId = Thread.CurrentThread.ManagedThreadId; } + /// + /// При необходимости копирует состояние контекста трассивровки в текущий поток. + /// + /// Исходный контекст трассировки, который передается. + /// + /// + /// Копирование происходит за счет создания нового контекста трассировки и заполнением его + /// состояния из переданного контекста. При этом копируется стек операций, однако в новом + /// контексте ранее начатые логические операции не могут быть завершены. + /// + /// + /// Если передача состояния состоялась, то вызывается событие трассировки . + /// + /// public static void Transfer(TraceContext from) { - _current = from == null ? new TraceContext() : new TraceContext(from); - } - - public TraceContext ParentContext { - get { - return m_parent; + if (_current == from) + return; + if (from != null) { + var context = new TraceContext(from); + context.LogEvent(TraceEventType.Transfer, "[{0}]-->[{1}]",from.ThreadId, context.ThreadId); + _current = context; + } else { + _current = new TraceContext(); } } + /// + /// Создает постоянную копию текущего контекста, данную копию можно хранить и использовать для передачи через + /// + /// Копия текущего контекста трассировки или null если таковой не был создан. + public static TraceContext Snapshot() { + return _current == null ? null : new TraceContext(_current); + } + + /// + /// Выполняет переданное действие в указанном контексте трассировки, по окончании восстанавливает предыдущий контекст трассировки потока. + /// + /// + public void Invoke(Action action) { + if (action == null) + throw new ArgumentNullException("action"); + var old = _current; + Transfer(this); + try { + action(); + } finally { + _current = old; + } + } + + /// + /// Текущая логическая операция. + /// public LogicalOperation CurrentOperation { get { return m_currentOperation; } } - public LogicalOperation TraceBound { + /// + /// Операция ниже которой нельзя опускаться в стеке логических операций, т.е. она не может быть завершена в текущем контексте. + /// + public LogicalOperation BoundOperation { get { - return m_traceBound; + return m_bound; } } + /// + /// Поток, в котором создан контекст трассировки. + /// public int ThreadId { get { return m_threadId; } } + /// + /// Начинает безымянную логическую операцию. + /// public void StartLogicalOperation() { StartLogicalOperation(null); } + /// + /// Начинает логическую операцию с указанным именем. Созданная операция будет добвалена в стек логических операций контекста, затем будет создано соответсвующее событие. + /// + /// Имя начинаемой операции. public void StartLogicalOperation(string name) { - LogEvent(TraceEventType.OperationStarted, "{0}", name); m_currentOperation = new LogicalOperation(name, m_currentOperation); + LogEvent(TraceEventType.OperationStarted, name); } + /// + /// Заканчивает логическую операцию начатую в текущем контексте. Операции, начатые в других контекстах не могут быть закончены в текущем контексте. + /// + /// + /// При вызове данного метода создается событие журнала трассировки, либо о завершении операции, либо об ошибки, поскольку данная операция + /// начата в другом контексте. + /// public void EndLogicalOperation() { - if (m_traceBound == m_currentOperation) { + if (m_bound == m_currentOperation) { LogEvent(TraceEventType.Error, "Trying to end the operation which isn't belongs to current trace"); } else { var op = m_currentOperation; + LogEvent(TraceEventType.OperationCompleted, "{0} {1} ms", op.Name, op.Duration); m_currentOperation = m_currentOperation.Parent; - LogEvent(TraceEventType.OperationCompleted, "{0} {1} ms", op.Name, op.Duration); } } diff --git a/Implab/Diagnostics/TraceEvent.cs b/Implab/Diagnostics/TraceEvent.cs --- a/Implab/Diagnostics/TraceEvent.cs +++ b/Implab/Diagnostics/TraceEvent.cs @@ -20,6 +20,10 @@ namespace Implab.Diagnostics { Message = message; } + public override string ToString() { + return String.Format("{0}: {1}", EventType, Message); + } + public static TraceEvent Create(TraceEventType type, string format, params object[] args) { return new TraceEvent(type, String.Format(format, args)); } diff --git a/Implab/Diagnostics/TraceLog.cs b/Implab/Diagnostics/TraceLog.cs --- a/Implab/Diagnostics/TraceLog.cs +++ b/Implab/Diagnostics/TraceLog.cs @@ -7,15 +7,12 @@ using System.Threading.Tasks; namespace Implab.Diagnostics { /// - /// Класс для публикации событий выполнения программы, события публикуются через + /// Класс для публикации событий выполнения программы, события публикуются через . + /// Журнал трассировки отражает логический ход выполнения программы и существует всегда, поскольку тесно связан с + /// контекстом трассировки. /// public static class TraceLog { [Conditional("TRACE")] - public static void Transfer(TraceContext from) { - TraceContext.Transfer(from); - } - - [Conditional("TRACE")] public static void StartLogicalOperation() { TraceContext.Current.StartLogicalOperation(); } diff --git a/Implab/Disposable.cs b/Implab/Disposable.cs new file mode 100644 --- /dev/null +++ b/Implab/Disposable.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Web; + +namespace Implab { + public class Disposable : IDisposable { + + bool m_disposed; + + public event EventHandler Disposed; + + public bool IsDisposed { + get { return m_disposed; } + } + + protected void AssertNotDisposed() { + if (m_disposed) + throw new ObjectDisposedException(this.ToString()); + } + + protected virtual void Dispose(bool disposing) { + if (disposing && !m_disposed) { + m_disposed = true; + + EventHandler temp = Disposed; + if (temp != null) + temp(this,EventArgs.Empty); + } + } + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void ReportObjectLeaks() { + Trace.TraceWarning("The object is marked as disposable but isn't disposed properly: {0}", this); + } + + ~Disposable() { + Dispose(false); + ReportObjectLeaks(); + } + } +} \ No newline at end of file diff --git a/Implab/IServiceLocator.cs b/Implab/IServiceLocator.cs new file mode 100644 --- /dev/null +++ b/Implab/IServiceLocator.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Implab { + public interface IServiceLocator: IServiceProvider { + T GetService(); + bool TryGetService(out T service); + } +} diff --git a/Implab/Implab.csproj b/Implab/Implab.csproj --- a/Implab/Implab.csproj +++ b/Implab/Implab.csproj @@ -32,24 +32,32 @@ + + + + + + + + diff --git a/Implab/Parallels/AsyncPool.cs b/Implab/Parallels/AsyncPool.cs --- a/Implab/Parallels/AsyncPool.cs +++ b/Implab/Parallels/AsyncPool.cs @@ -14,10 +14,10 @@ namespace Implab.Parallels { public static Promise Invoke(Func func) { var p = new Promise(); - var caller = TraceContext.Current; + var caller = TraceContext.Snapshot(); ThreadPool.QueueUserWorkItem(param => { - TraceLog.Transfer(caller); + TraceContext.Transfer(caller); try { p.Resolve(func()); } catch(Exception e) { @@ -31,10 +31,10 @@ namespace Implab.Parallels { public static Promise InvokeNewThread(Func func) { var p = new Promise(); - var caller = TraceContext.Current; + var caller = TraceContext.Snapshot(); var worker = new Thread(() => { - TraceLog.Transfer(caller); + TraceContext.Transfer(caller); try { p.Resolve(func()); } catch (Exception e) { diff --git a/Implab/Parallels/WorkerPool.cs b/Implab/Parallels/WorkerPool.cs --- a/Implab/Parallels/WorkerPool.cs +++ b/Implab/Parallels/WorkerPool.cs @@ -42,15 +42,16 @@ namespace Implab.Parallels { var promise = new Promise(); - var caller = TraceContext.Current; + var caller = TraceContext.Snapshot(); EnqueueTask(delegate() { - TraceLog.Transfer(caller); - try { - promise.Resolve(task()); - } catch (Exception e) { - promise.Reject(e); - } + caller.Invoke(delegate() { + try { + promise.Resolve(task()); + } catch (Exception e) { + promise.Reject(e); + } + }); }); return promise; diff --git a/Implab/ServiceLocator.cs b/Implab/ServiceLocator.cs new file mode 100644 --- /dev/null +++ b/Implab/ServiceLocator.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace Implab { + /// + /// Коллекция сервисов, позволяет регистрировать и получать сервисы. + /// + public class ServiceLocator: Component, IServiceLocator, IServiceProvider { + // запись об сервисе + struct ServiceEntry { + public object service; // сервис + public bool shared; // признак того, что сервис НЕ нужно освобождать + public Func activator; // активатор сервиса при первом обращении + public List associated; // ссылки на текущую запись + public Type origin; // ссылка на оригинальную запись о сервисе + } + + // словарь существующих сервисов + Dictionary m_services = new Dictionary(); + + /// + /// Получает объект предоставляющий сервис . + /// + /// Тип запрашиваемого сервиса + /// Объект, реализующий сервис + /// Сервис не зарегистрирован + public T GetService() { + return (T)GetService(typeof(T)); + } + + + /// + /// Пытается получить указанный сервис, в случае, если компонента не предоставляет требуемый сервис + /// не возникает исключений. + /// + /// Тип требуемого сервиса. + /// Объект реализующий сервис, или default(T) если такового нет. + /// true - сервис найден, false - сервис не зарегистрирован. + public bool TryGetService(out T service) { + AssertNotDisposed(); + + try { + service = GetService(); + return true; + } catch(KeyNotFoundException) { + service = default(T); + return false; + } + } + + /// + /// Получает объект предоставляющий сервис + /// + /// Тип запрашиваемого сервиса + /// Объект, реализующий сервис + /// Сервис не зарегистрирован + public object GetService(Type serviceType) { + if (serviceType == null) + throw new ArgumentNullException("serviceType"); + AssertNotDisposed(); + + ServiceEntry se; + if (!m_services.TryGetValue(serviceType, out se)) { + // ищем ближайщий объект, реализующий нужный сервис + Type pt = null; + foreach (var t in m_services.Keys) + if (serviceType.IsAssignableFrom(t) && (pt == null || t.IsAssignableFrom(pt))) + pt = t; + if (pt == null) + throw new ApplicationException(String.Format("{0} doesn't provide {1} service",this,serviceType)); + + var pe = m_services[pt]; + + // найденная запись может ссылаться на оригинальную запись с сервисом + if(pe.origin != null) { + pt = pe.origin; + pe = m_services[pt]; + } + + // добавляем список с обратными ссылками + if (pe.associated == null) + pe.associated = new List(); + + pe.associated.Add(serviceType); + + // обновляем родительскую запись + m_services[pt] = pe; + + // создаем запись со ссылкой + se = new ServiceEntry { + service = pe.service, + origin = pt, + shared = true // предотвращаем множественные попытки освобождения + }; + + m_services[serviceType] = se; + } + + // запись содержит в себе информацию о сервисе + if (se.service != null) + return se.service; + + // текущая запись является ссылкой + if (se.origin != null) { + se.service = GetService(se.origin); + m_services[serviceType] = se; + return se.service; + } + + // текущая запись не является ссылкой и не имеет информации о сервисе + // она должна сожержать информацию об активации + if (se.activator != null) { + se.service = se.activator(); + + m_services[serviceType] = se; + + return se.service; + } + + throw new Exception("Unable to create a service instance"); + } + + /// + /// Регистрирует фабрику для активации сервиса по первому требованию. + /// + /// Тип регистрируемого сервиса. + /// Фабрика для создания/получения объекта, предоставляющего сервис. + /// Указанный сервис уже зарегистрирован. + /// При освобождении сервис-локатора, сервисы полученные в результате активации также будут освобождены. + public void Register(Func activator) { + if (activator == null) + throw new ArgumentNullException("activator"); + + AssertNotDisposed(); + + Unregister(typeof(T)); + + m_services[typeof(T)] = new ServiceEntry { + activator = () => activator() as object + }; + } + + /// + /// Регистрирует объект, предоставляющий сервис. + /// + /// Тип регистрируемого сервиса. + /// Объект, предоставляющий сервис. + /// Указанный сервис уже зарегистрирован. + /// Сервис-локатором не управляет временем жизни объекта для зарегистрированного сервиса. + public void Register(T service) { + Register(service, true); + } + + /// + /// Регистрирует объект, предоставляющий сервис. + /// + /// Тип регистрируемого сервиса. + /// Объект, предоставляющий сервис. + /// Признак того, что объект является разделяемым и сервис-локатор не должен его освобождать. + /// Указанный сервис уже зарегистрирован. + public void Register(T service, bool shared) { + if (service == null) + throw new ArgumentNullException("service"); + + AssertNotDisposed(); + + Unregister(typeof(T)); + + m_services[typeof(T)] = new ServiceEntry { service = service, shared = shared }; + } + + public void Unregister(Type serviceType) { + if (serviceType == null) + throw new ArgumentNullException("serviceType"); + + AssertNotDisposed(); + + ServiceEntry se; + if (m_services.TryGetValue(serviceType, out se)) { + // освобождаем ресурсы + if (se.service != null && !se.shared) + ((IDisposable)se.service).Dispose(); + m_services.Remove(serviceType); + + // убираем связанные записи + if (se.associated != null) + foreach (var item in se.associated) + m_services.Remove(item); + } + } + + /// + /// Освобождает зарегистрированные сервисы (которые требуется освобоить). + /// + /// Призанак того, что нужно освободить ресурсы. + protected override void Dispose(bool disposing) { + if (disposing) { + + foreach (var entry in m_services.Values) + if (!entry.shared && entry.service is IDisposable) + ((IDisposable)entry.service).Dispose(); + + } + base.Dispose(disposing); + } + } +} \ No newline at end of file