• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

DemoBytom / DemoEngine / 22265994988

21 Feb 2026 10:57PM UTC coverage: 32.674% (-0.4%) from 33.107%
22265994988

push

coveralls.net

DemoBytom
Merge remote-tracking branch 'origin/main' into feature/ValueResults

1222 of 3740 relevant lines covered (32.67%)

0.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.68
/src/Demo.Engine.Core/Features/StaThread/StaThreadService.cs
1
// Copyright © Michał Dembski and contributors.
2
// Distributed under MIT license. See LICENSE file in the root for more information.
3

4
using System.Runtime.InteropServices;
5
using System.Threading.Channels;
6
using Demo.Engine.Core.Interfaces;
7
using Demo.Engine.Core.Interfaces.Platform;
8
using Demo.Engine.Core.Interfaces.Rendering;
9
using Microsoft.Extensions.Hosting;
10

11
namespace Demo.Engine.Core.Features.StaThread;
12

13
internal sealed class StaThreadService
14
    : IStaThreadService,
15
      IDisposable
16
{
17
    private readonly IHostApplicationLifetime _hostApplicationLifetime;
18
    private readonly ChannelReader<StaThreadRequests> _channelReader;
19
    private readonly IMainLoopLifetime _mainLoopLifetime;
20
    private bool _disposedValue;
21

22
    public Task ExecutingTask { get; }
1✔
23
    public bool IsRunning { get; private set; }
1✔
24

25
    public StaThreadService(
1✔
26
        IHostApplicationLifetime hostApplicationLifetime,
1✔
27
        IRenderingEngine renderingEngine,
1✔
28
        IOSMessageHandler osMessageHandler,
1✔
29
        ChannelReader<StaThreadRequests> channelReader,
1✔
30
        IMainLoopLifetime mainLoopLifetime)
1✔
31
    {
32
        _hostApplicationLifetime = hostApplicationLifetime;
1✔
33
        _channelReader = channelReader;
1✔
34
        _mainLoopLifetime = mainLoopLifetime;
1✔
35
        IsRunning = true;
1✔
36
        ExecutingTask = RunSTAThread(
1✔
37
            renderingEngine,
1✔
38
            osMessageHandler);
1✔
39
    }
1✔
40

41
    private Task RunSTAThread(
42
        IRenderingEngine renderingEngine,
43
        IOSMessageHandler osMessageHandler)
44
    {
45
        var tcs = new TaskCompletionSource();
1✔
46
        var thread = new Thread(()
1✔
47
            =>
1✔
48
        {
1✔
49
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(
1✔
50
                _hostApplicationLifetime.ApplicationStopping,
1✔
51
                _mainLoopLifetime.Token);
1✔
52

1✔
53
            try
1✔
54
            {
1✔
55

1✔
56
                SingleThreadedSynchronizationContextChannel.Await(async ()
1✔
57
                    => await STAThread(
1✔
58
                        renderingEngine: renderingEngine,
1✔
59
                        osMessageHandler: osMessageHandler,
1✔
60
                        cancellationToken: cts.Token));
1✔
61

1✔
62
                FinishRunning(tcs);
63
            }
64
            catch (OperationCanceledException)
1✔
65
            {
1✔
66
                FinishRunning(tcs);
1✔
67
            }
1✔
68
            catch (Exception ex)
69
            {
1✔
70
                FinishRunning(tcs, ex);
71
            }
72
        });
1✔
73
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
74
        {
75
            //Can only by set on the Windows machine. Doesn't work on Linux/MacOS
76
            thread.SetApartmentState(ApartmentState.STA);
1✔
77
            thread.Name = "Main STA thread";
1✔
78
        }
79
        else
80
        {
81
            thread.Name = "Main thread";
82
        }
83

84
        thread.Start();
1✔
85

86
        return tcs.Task;
1✔
87

88
        void FinishRunning(
89
            TaskCompletionSource tcs,
90
            Exception? exception = null)
91
        {
92
            /* This should be called BEFORE tcs.SetResult/tcs.SetException!
93
             * Otherwise _mainLoopLifetime.Cancel() gets called after the returned tcs.Task completes,
94
             * leading to dispoes exception on mainLoopLifetime, that's already disposed upstream! */
95
            IsRunning = false;
1✔
96
            _mainLoopLifetime.Cancel();
1✔
97

98
            if (exception is null)
99
            {
100
                tcs.SetResult();
1✔
101
            }
102
            else
103
            {
104
                tcs.SetException(exception);
105
            }
106
        }
107
    }
108

109
    private async Task STAThread(
110
        IRenderingEngine renderingEngine,
111
        IOSMessageHandler osMessageHandler,
112
        CancellationToken cancellationToken)
113
    {
114
        var doEventsOk = true;
1✔
115

116
        await foreach (var staAction in _channelReader
117
            .ReadAllAsync(cancellationToken)
1✔
118
            .WithCancellation(cancellationToken))
1✔
119
        {
120
            switch (staAction)
121
            {
122
                case StaThreadRequests.DoEventsOkRequest doEventsOkRequest:
123
                    doEventsOk &= doEventsOkRequest.Invoke(renderingEngine, osMessageHandler);
124
                    break;
125

126
                default:
127
                    _ = staAction.Invoke(renderingEngine, osMessageHandler);
1✔
128
                    break;
129
            }
130

131
            if (!doEventsOk || !IsRunning || cancellationToken.IsCancellationRequested)
132
            {
133
                break;
134
            }
135
        }
136
    }
137

138
    private void Dispose(bool disposing)
139
    {
140
        if (!_disposedValue)
1✔
141
        {
142
            if (disposing)
1✔
143
            {
144
            }
145

146
            _disposedValue = true;
1✔
147
        }
148
    }
1✔
149

150
    public void Dispose()
151
    {
152
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
153
        Dispose(disposing: true);
1✔
154
        GC.SuppressFinalize(this);
1✔
155
    }
1✔
156

157
    private sealed class SingleThreadedSynchronizationContextChannel
158
        : SynchronizationContext
159
    {
160
        private readonly Channel<(SendOrPostCallback d, object? state)> _channel =
2✔
161
            Channel.CreateUnbounded<(SendOrPostCallback d, object? state)>(
2✔
162
                new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
2✔
163

164
        public override void Post(SendOrPostCallback d, object? state)
165
            => _channel.Writer.TryWrite((d, state));
2✔
166

167
        public override void Send(SendOrPostCallback d, object? state)
168
            => throw new InvalidOperationException("Synchronous operations are not supported!");
×
169

170
        public static void Await(Func<Task> taskInvoker)
171
        {
172
            var originalContext = Current;
2✔
173
            try
174
            {
175
                var context = new SingleThreadedSynchronizationContextChannel();
2✔
176
                SetSynchronizationContext(context);
2✔
177

178
                Task task;
179
                try
180
                {
181
                    task = taskInvoker.Invoke();
2✔
182
                }
2✔
183
                catch (Exception ex)
×
184
                {
185
                    // If the invoker throws synchronously, complete the channel so the pump can exit.
186
                    context._channel.Writer.Complete(ex);
×
187
                    throw;
×
188
                }
189

190
                _ = task.ContinueWith(t
2✔
191
                    => context._channel.Writer.Complete(t.Exception),
2✔
192
                    TaskScheduler.Default);
2✔
193

194
                // Pump loop: block synchronously until items are available or the writer completes.
195
                while (context._channel.Reader.WaitToReadAsync().Preserve().GetAwaiter().GetResult())
2✔
196
                {
197
                    while (context._channel.Reader.TryRead(out var work))
2✔
198
                    {
199
                        work.d.Invoke(work.state);
2✔
200
                    }
2✔
201
                }
202

203
                task.GetAwaiter().GetResult();
2✔
204
            }
×
205
            finally
206
            {
207
                SetSynchronizationContext(originalContext);
2✔
208
            }
2✔
209
        }
×
210
    }
211
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc