chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
 
namespace OpenTap
{
    /// <summary>
    /// Options used to define the behavior of a <see cref="OpenTap.Session"/>
    /// </summary>
    [Flags]
    public enum SessionOptions
    {
        /// <summary>
        /// No special behavior is applied. Starting a session like this, is the same as just starting a TapThread.
        /// </summary>
        None = 0,
        /// <summary>
        /// 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.
        /// </summary>
        OverlayComponentSettings = 1,
        /// <summary> Log messages written in Sessions that redirect logging only go to LogListeners that are added in that session. </summary>
        RedirectLogging = 2,
        ///// <summary>
        ///// 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. <see cref="TapThread.Abort()"/> and <see cref="ThreadHierarchyLocal{T}"/>.
        ///// </summary>
        //ThreadHierarchyRoot = 4,
    }
 
    internal interface ISessionLocal
    {
        bool AutoDispose { get; }
    }
 
    /// <summary>
    /// Used to hold a value that is specific to a session.
    /// </summary>
    public class SessionLocal<T> : ISessionLocal
    {
        /// <summary>
        /// Automatically dispose the value when all threads in the session has completed.
        /// Only has any effect if T is IDisposable
        /// </summary>
        public bool AutoDispose { get; }
 
        /// <summary> Session specific value. </summary>
        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;
        }
 
 
        /// <summary>
        /// Used to hold a value that is specific to a session.
        /// Initializes a session local with a root/default value.
        /// </summary>
        /// <param name="rootValue">Default value set at the root session.</param>
        /// <param name="autoDispose">True to automatically dispose the value when all threads in the session has completed. Only has any effect if T is IDisposable.</param>
        public SessionLocal(T rootValue, bool autoDispose = true) : this(autoDispose)
        {
            if(Equals(rootValue, default(T)) == false)
                Session.RootSession.sessionLocals[this] = rootValue;
        }
 
        /// <summary>
        /// Used to hold a value that is specific to a session.
        /// Initializes a session local without a root/default value.
        /// </summary>
        /// <param name="autoDispose">True to automatically dispose the value when all threads in the session has completed. Only has any effect if T is IDisposable.</param>
        public SessionLocal(bool autoDispose = true)
        {
            AutoDispose = autoDispose;
        }
    }
 
    /// <summary> 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.
    /// </summary>
    public class Session : IDisposable
    {
        static readonly ThreadField<Session> sessionTField = new ThreadField<Session>(ThreadFieldMode.Cached);
        
        /// <summary> The default/root session. This session is active when no other session is. </summary>
        public static readonly Session RootSession;
 
        TapThread threadContext;
        internal readonly ConcurrentDictionary<ISessionLocal, object> sessionLocals = new ConcurrentDictionary<ISessionLocal, object>();
 
        /// <summary> The parent session of the current session. This marks the session that started it.</summary>
        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();
        }
 
        /// <summary>
        /// Gets the currently active session.
        /// </summary>
        public static Session Current => sessionTField.Value ?? RootSession;
 
        /// <summary>
        /// Gets the session ID for this session.
        /// </summary>
        public Guid Id { get; }
 
        /// <summary>
        /// Gets the flags used to create/start this session.
        /// </summary>
        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<IDisposable> disposables = new Stack<IDisposable>();
        
        /// <summary> Disposes the session. </summary>
        public void Dispose()
        {
            var exceptions = new List<Exception>();
            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);
            }
        }
 
        /// <summary> Creates a new session in the current <see cref="TapThread"/> context. The session lasts until the TapTread ends, or Dispose is called on the returned Session object.</summary>
        /// <param name="options">Flags selected from the SessionOptions enum to customize the behavior of the session.</param>
        /// <param name="id">Option to specify the ID of the Session</param>
        /// <returns> A disposable Session object. </returns>
        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;
        }
 
        /// <summary> Creates a new session in the current <see cref="TapThread"/> context. The session lasts until the TapTread ends, or Dispose is called on the returned Session object.</summary>
        /// <param name="options">Flags selected from the SessionOptions enum to customize the behavior of the session.</param>
        /// <returns> A disposable Session object. </returns>
        public static Session Create(SessionOptions options) => Create(options, null);
 
 
        /// <summary>
        /// Creates a new session, and runs the specified action in the context of that session. When the acion completes, the session is Disposed automatically.
        /// </summary>
        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}");
        }
 
        /// <summary>
        /// Synchronously runs the specified action in the context of the given session
        /// </summary>
        /// <param name="action">The action to run.</param>
        /// <returns>The session in which the action is run</returns>
        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;
            }
        }
    }
}