using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace OpenTap { /// /// Options used to define the behavior of a /// [Flags] public enum SessionOptions { /// /// No special behavior is applied. Starting a session like this, is the same as just starting a TapThread. /// None = 0, /// /// Component settings are cloned for the sake of this session. Instrument, DUT etc instances are cloned. /// When this is used, test plans should be reloaded in the new context. This causes resources to be serialized. /// OverlayComponentSettings = 1, /// Log messages written in Sessions that redirect logging only go to LogListeners that are added in that session. RedirectLogging = 2, ///// ///// When this option is specified, the thread context will not be a child of the calling context. ///// Instead the session will be the root of a new separate context. ///// This will affect the behavior of e.g. and . ///// //ThreadHierarchyRoot = 4, } internal interface ISessionLocal { bool AutoDispose { get; } } /// /// Used to hold a value that is specific to a session. /// public class SessionLocal : ISessionLocal { /// /// Automatically dispose the value when all threads in the session has completed. /// Only has any effect if T is IDisposable /// public bool AutoDispose { get; } /// Session specific value. public T Value { get { for (var session = Session.Current; session != null; session = session.Parent) { if (session.sessionLocals.TryGetValue(this, out object val)) return (T) val; if (session == session.Parent) throw new InvalidOperationException("This should not be possible"); } return default; } set => Session.Current.sessionLocals[this] = value; } /// /// Used to hold a value that is specific to a session. /// Initializes a session local with a root/default value. /// /// Default value set at the root session. /// True to automatically dispose the value when all threads in the session has completed. Only has any effect if T is IDisposable. public SessionLocal(T rootValue, bool autoDispose = true) : this(autoDispose) { if(Equals(rootValue, default(T)) == false) Session.RootSession.sessionLocals[this] = rootValue; } /// /// Used to hold a value that is specific to a session. /// Initializes a session local without a root/default value. /// /// True to automatically dispose the value when all threads in the session has completed. Only has any effect if T is IDisposable. public SessionLocal(bool autoDispose = true) { AutoDispose = autoDispose; } } /// A session represents a collection of data associated with running and configuring test plans: /// - Logging /// - Settings /// - Resources /// - ... /// When a new session is created, it overrides the existing values for these items. /// public class Session : IDisposable { static readonly ThreadField sessionTField = new ThreadField(ThreadFieldMode.Cached); /// The default/root session. This session is active when no other session is. public static readonly Session RootSession; TapThread threadContext; internal readonly ConcurrentDictionary sessionLocals = new ConcurrentDictionary(); /// The parent session of the current session. This marks the session that started it. public readonly Session Parent; static Session() { // TapThread needs a RootSession to start, so the first time, the TapThread cannot be set. RootSession = new Session(Guid.NewGuid(), SessionOptions.None, true); RootSession.threadContext = TapThread.Current; } internal void DisposeSessionLocals() { foreach (var item in sessionLocals) { if(item.Key.AutoDispose && item.Value is IDisposable disp) { disp.Dispose(); } } sessionLocals.Clear(); } /// /// Gets the currently active session. /// public static Session Current => sessionTField.Value ?? RootSession; /// /// Gets the session ID for this session. /// public Guid Id { get; } /// /// Gets the flags used to create/start this session. /// public SessionOptions Options { get; } Session(Guid id, SessionOptions options, bool rootSession = false) { Id = id; Options = options; if (!rootSession) { threadContext = TapThread.Current; Parent = Current; } } static TraceSource _log; // lazily loaded to prevent a circular dependency between Session and LogContext. static TraceSource log => _log ?? (_log = Log.CreateSource(nameof(Session))); readonly Stack disposables = new Stack(); /// Disposes the session. public void Dispose() { var exceptions = new List(); while (disposables.Count > 0) { try { var item = disposables.Pop(); item.Dispose(); } catch(Exception e) { exceptions.Add(e); } } foreach (var ex in exceptions) { log.Error("Caught error while disposing session: {0}", ex.Message); log.Debug(ex); } } /// Creates a new session in the current context. The session lasts until the TapTread ends, or Dispose is called on the returned Session object. /// Flags selected from the SessionOptions enum to customize the behavior of the session. /// Option to specify the ID of the Session /// A disposable Session object. public static Session Create(SessionOptions options = SessionOptions.OverlayComponentSettings | SessionOptions.RedirectLogging, Guid? id = null) { var session = new Session(id.HasValue ? id.Value : Guid.NewGuid(), options); session.disposables.Push(TapThread.UsingThreadContext(session.DisposeSessionLocals)); session.Activate(); return session; } /// Creates a new session in the current context. The session lasts until the TapTread ends, or Dispose is called on the returned Session object. /// Flags selected from the SessionOptions enum to customize the behavior of the session. /// A disposable Session object. public static Session Create(SessionOptions options) => Create(options, null); /// /// Creates a new session, and runs the specified action in the context of that session. When the acion completes, the session is Disposed automatically. /// public static void Start(Action action, SessionOptions options = SessionOptions.OverlayComponentSettings | SessionOptions.RedirectLogging) { var session = new Session(Guid.NewGuid(), options); TapThread.Start(() => { try { session.Activate(); sessionTField.Value = session; action(); } finally { session.Dispose(); } }, session.DisposeSessionLocals, $"SessionRootThread-{session.Id}"); } /// /// Synchronously runs the specified action in the context of the given session /// /// The action to run. /// The session in which the action is run public void RunInSession(Action action) { TapThread.WithNewContext(action, this.threadContext); } void Activate() { try { sessionTField.Value = this; if (Options.HasFlag(SessionOptions.OverlayComponentSettings)) ComponentSettings.BeginSession(); if (Options.HasFlag(SessionOptions.RedirectLogging)) Log.WithNewContext(); } catch { Dispose(); throw; } } } }