Skip to content

Commit 5160742

Browse files
BDispCopilot
andauthored
Fixes #5579. SynchronizationContext is not correctly implemented in v2 (#5588)
* Fixes #5579. SynchronizationContext is not correctly implemented in v2 * Enforces that apps can be crested before call Run and the Begin method guarantee set the sync context for the current running app. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Fix nullable warning * Reset callbackCalled before reusing it --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 8975fec commit 5160742

5 files changed

Lines changed: 202 additions & 44 deletions

File tree

Terminal.Gui/App/ApplicationImpl.Lifecycle.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ internal partial class ApplicationImpl
1414
/// <inheritdoc/>
1515
public bool Initialized { get; set; }
1616

17+
internal SynchronizationContext? SynchronizationContext { get; private set; }
18+
1719
/// <inheritdoc/>
1820
public event EventHandler<EventArgs<bool>>? InitializedChanged;
1921

@@ -79,7 +81,8 @@ public IApplication Init (string? driverName = null)
7981
RaiseInitializedChanged (this, new EventArgs<bool> (true));
8082
SubscribeDriverEvents ();
8183

82-
SynchronizationContext.SetSynchronizationContext (new SynchronizationContext ());
84+
SynchronizationContext = new MainLoopSyncContext (this);
85+
SynchronizationContext.SetSynchronizationContext (SynchronizationContext);
8386

8487
_result = null;
8588

Terminal.Gui/App/ApplicationImpl.Run.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ public void Invoke (Action action)
136136
// Set the application reference in the runnable
137137
runnable.SetApp (this);
138138

139+
// Set the synchronization context to MainLoopSyncContext for this application
140+
SynchronizationContext.SetSynchronizationContext (SynchronizationContext);
141+
139142
// Ensure the mouse is ungrabbed
140143
Mouse.UngrabMouse ();
141144

Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ public async Task StartInputTaskAsync (IApplication? app)
7171
Task waitForSemaphore = _startupSemaphore.WaitAsync ();
7272

7373
// Wait for either the semaphore to be released or the input task to crash.
74-
// ReSharper disable once UseConfigureAwaitFalse
75-
Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask);
74+
Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask).ConfigureAwait (false);
7675

7776
// Check if the task was the input task and if it has failed.
7877
if (completedTask == _inputTask)
Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,78 @@
1-
#nullable disable
21
namespace Terminal.Gui.App;
32

4-
///// <summary>
5-
///// provides the sync context set while executing code in Terminal.Gui, to let
6-
///// users use async/await on their code
7-
///// </summary>
8-
//internal sealed class MainLoopSyncContext : SynchronizationContext
9-
//{
10-
// public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); }
11-
12-
// public override void Post (SendOrPostCallback d, object state)
13-
// {
14-
// // Queue the task using the modern architecture
15-
// ApplicationImpl.Instance.Invoke (() => { d (state); });
16-
// }
17-
18-
// //_mainLoop.Driver.Wakeup ();
19-
// public override void Send (SendOrPostCallback d, object state)
20-
// {
21-
// if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId)
22-
// {
23-
// d (state);
24-
// }
25-
// else
26-
// {
27-
// var wasExecuted = false;
28-
29-
// ApplicationImpl.Instance.Invoke (
30-
// () =>
31-
// {
32-
// d (state);
33-
// wasExecuted = true;
34-
// }
35-
// );
36-
37-
// while (!wasExecuted)
38-
// {
39-
// Thread.Sleep (15);
40-
// }
41-
// }
42-
// }
43-
//}
3+
/// <summary>
4+
/// Provides the sync context set while executing code in Terminal.Gui, to let
5+
/// users use async/await on their code
6+
/// </summary>
7+
internal sealed class MainLoopSyncContext : SynchronizationContext
8+
{
9+
private readonly IApplication _app;
10+
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="MainLoopSyncContext"/> class.
13+
/// </summary>
14+
/// <param name="app">The application instance that owns the main loop.</param>
15+
public MainLoopSyncContext (IApplication app) => _app = app;
16+
17+
/// <inheritdoc/>
18+
public override SynchronizationContext CreateCopy () => new MainLoopSyncContext (_app);
19+
20+
/// <inheritdoc/>
21+
public override void Post (SendOrPostCallback d, object? state)
22+
{
23+
ArgumentNullException.ThrowIfNull (d);
24+
25+
// Queue the task using the modern architecture
26+
_app.Invoke (() => d (state));
27+
}
28+
29+
/// <inheritdoc/>
30+
public override void Send (SendOrPostCallback d, object? state)
31+
{
32+
ArgumentNullException.ThrowIfNull (d);
33+
34+
if (_app.MainThreadId == Thread.CurrentThread.ManagedThreadId)
35+
{
36+
d (state);
37+
38+
return;
39+
}
40+
41+
object gate = new ();
42+
bool wasExecuted = false;
43+
Exception? error = null;
44+
45+
_app.Invoke (() =>
46+
{
47+
try
48+
{
49+
d (state);
50+
}
51+
catch (Exception ex)
52+
{
53+
error = ex;
54+
}
55+
finally
56+
{
57+
lock (gate)
58+
{
59+
wasExecuted = true;
60+
Monitor.Pulse (gate);
61+
}
62+
}
63+
});
64+
65+
lock (gate)
66+
{
67+
while (!wasExecuted)
68+
{
69+
Monitor.Wait (gate);
70+
}
71+
}
72+
73+
if (error is { })
74+
{
75+
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (error).Throw ();
76+
}
77+
}
78+
}

Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,124 @@ public void Dispose_Resets_SyncContext ()
5353
Assert.Null (SynchronizationContext.Current);
5454
}
5555

56+
[Fact]
57+
public void Init_Posts_To_The_App_Main_Thread ()
58+
{
59+
IApplication app = Application.Create ();
60+
app.Init (DriverRegistry.Names.ANSI);
61+
62+
int mainThreadId = app.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
63+
int? callbackThreadId = null;
64+
ManualResetEventSlim callbackCalled = new (false);
65+
66+
SynchronizationContext.Current!.Post (_ =>
67+
{
68+
callbackThreadId = Thread.CurrentThread.ManagedThreadId;
69+
callbackCalled.Set ();
70+
},
71+
null);
72+
73+
app.AddTimeout (TimeSpan.FromMilliseconds (100),
74+
() =>
75+
{
76+
app.RequestStop ();
77+
78+
return false;
79+
});
80+
81+
app.Run (new Window ());
82+
83+
Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
84+
Assert.Equal (mainThreadId, callbackThreadId);
85+
86+
app.Dispose ();
87+
}
88+
89+
[Fact]
90+
public void Init_Posts_To_The_App_Instance_Main_Thread ()
91+
{
92+
IApplication app1 = Application.Create ();
93+
app1.Init (DriverRegistry.Names.ANSI);
94+
95+
IApplication app2 = Application.Create ();
96+
app2.Init (DriverRegistry.Names.ANSI);
97+
98+
int mainThreadId1 = app1.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
99+
int? callbackThreadId1 = null;
100+
SynchronizationContext? synchronizationContext1 = null;
101+
102+
ManualResetEventSlim callbackCalled = new (false);
103+
104+
app1.AddTimeout (TimeSpan.FromMilliseconds (100),
105+
() =>
106+
{
107+
if (synchronizationContext1 is null)
108+
{
109+
synchronizationContext1 = SynchronizationContext.Current;
110+
synchronizationContext1?.Post (_ =>
111+
{
112+
callbackThreadId1 = Thread.CurrentThread.ManagedThreadId;
113+
callbackCalled.Set ();
114+
},
115+
null);
116+
117+
return true;
118+
}
119+
else
120+
{
121+
app1.RequestStop ();
122+
123+
return false;
124+
}
125+
});
126+
127+
app1.Run (new Window ());
128+
129+
Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
130+
Assert.Equal (mainThreadId1, callbackThreadId1);
131+
Assert.Equal (synchronizationContext1, SynchronizationContext.Current);
132+
133+
callbackCalled.Reset ();
134+
135+
int mainThreadId2 = app2.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
136+
int? callbackThreadId2 = null;
137+
SynchronizationContext? synchronizationContext2 = null;
138+
139+
app2.AddTimeout (TimeSpan.FromMilliseconds (100),
140+
() =>
141+
{
142+
if (synchronizationContext2 is null)
143+
{
144+
synchronizationContext2 = SynchronizationContext.Current;
145+
synchronizationContext2?.Post (_ =>
146+
{
147+
callbackThreadId2 = Thread.CurrentThread.ManagedThreadId;
148+
callbackCalled.Set ();
149+
},
150+
null);
151+
152+
return true;
153+
}
154+
else
155+
{
156+
app2.RequestStop ();
157+
158+
return false;
159+
}
160+
});
161+
162+
app2.Run (new Window ());
163+
164+
Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
165+
Assert.Equal (mainThreadId2, callbackThreadId2);
166+
Assert.NotEqual (synchronizationContext1, SynchronizationContext.Current);
167+
Assert.Equal (synchronizationContext2, SynchronizationContext.Current);
168+
Assert.NotEqual (synchronizationContext1, synchronizationContext2);
169+
170+
app1.Dispose ();
171+
app2.Dispose ();
172+
}
173+
56174
[Fact]
57175
public void Dispose_Alone_Does_Nothing ()
58176
{

0 commit comments

Comments
 (0)