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;
}
}
}
}