Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ internal partial class ApplicationImpl
/// <inheritdoc/>
public bool Initialized { get; set; }

internal SynchronizationContext? SynchronizationContext { get; private set; }

/// <inheritdoc/>
public event EventHandler<EventArgs<bool>>? InitializedChanged;

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

SynchronizationContext.SetSynchronizationContext (new SynchronizationContext ());
SynchronizationContext = new MainLoopSyncContext (this);
SynchronizationContext.SetSynchronizationContext (SynchronizationContext);

_result = null;

Expand Down
3 changes: 3 additions & 0 deletions Terminal.Gui/App/ApplicationImpl.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ public void Invoke (Action action)
// Set the application reference in the runnable
runnable.SetApp (this);

// Set the synchronization context to MainLoopSyncContext for this application
SynchronizationContext.SetSynchronizationContext (SynchronizationContext);

// Ensure the mouse is ungrabbed
Mouse.UngrabMouse ();

Expand Down
3 changes: 1 addition & 2 deletions Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ public async Task StartInputTaskAsync (IApplication? app)
Task waitForSemaphore = _startupSemaphore.WaitAsync ();

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

// Check if the task was the input task and if it has failed.
if (completedTask == _inputTask)
Expand Down
117 changes: 76 additions & 41 deletions Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,78 @@
#nullable disable
namespace Terminal.Gui.App;

///// <summary>
///// provides the sync context set while executing code in Terminal.Gui, to let
///// users use async/await on their code
///// </summary>
//internal sealed class MainLoopSyncContext : SynchronizationContext
//{
// public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); }

// public override void Post (SendOrPostCallback d, object state)
// {
// // Queue the task using the modern architecture
// ApplicationImpl.Instance.Invoke (() => { d (state); });
// }

// //_mainLoop.Driver.Wakeup ();
// public override void Send (SendOrPostCallback d, object state)
// {
// if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId)
// {
// d (state);
// }
// else
// {
// var wasExecuted = false;

// ApplicationImpl.Instance.Invoke (
// () =>
// {
// d (state);
// wasExecuted = true;
// }
// );

// while (!wasExecuted)
// {
// Thread.Sleep (15);
// }
// }
// }
//}
/// <summary>
/// Provides the sync context set while executing code in Terminal.Gui, to let
/// users use async/await on their code
/// </summary>
internal sealed class MainLoopSyncContext : SynchronizationContext
{
private readonly IApplication _app;

/// <summary>
/// Initializes a new instance of the <see cref="MainLoopSyncContext"/> class.
/// </summary>
/// <param name="app">The application instance that owns the main loop.</param>
public MainLoopSyncContext (IApplication app) => _app = app;

/// <inheritdoc/>
public override SynchronizationContext CreateCopy () => new MainLoopSyncContext (_app);

/// <inheritdoc/>
public override void Post (SendOrPostCallback d, object? state)
{
ArgumentNullException.ThrowIfNull (d);

// Queue the task using the modern architecture
_app.Invoke (() => d (state));
}

/// <inheritdoc/>
public override void Send (SendOrPostCallback d, object? state)
{
ArgumentNullException.ThrowIfNull (d);

if (_app.MainThreadId == Thread.CurrentThread.ManagedThreadId)
{
d (state);

return;
}

object gate = new ();
bool wasExecuted = false;
Exception? error = null;

_app.Invoke (() =>
{
try
{
d (state);
}
catch (Exception ex)
{
error = ex;
}
finally
{
lock (gate)
{
wasExecuted = true;
Monitor.Pulse (gate);
}
}
});

lock (gate)
{
while (!wasExecuted)
{
Monitor.Wait (gate);
}
}

if (error is { })
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (error).Throw ();
}
}
}
118 changes: 118 additions & 0 deletions Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,124 @@ public void Dispose_Resets_SyncContext ()
Assert.Null (SynchronizationContext.Current);
}

[Fact]
public void Init_Posts_To_The_App_Main_Thread ()
{
IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);

int mainThreadId = app.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
int? callbackThreadId = null;
ManualResetEventSlim callbackCalled = new (false);

SynchronizationContext.Current!.Post (_ =>
{
callbackThreadId = Thread.CurrentThread.ManagedThreadId;
callbackCalled.Set ();
},
null);

app.AddTimeout (TimeSpan.FromMilliseconds (100),
() =>
{
app.RequestStop ();

return false;
});

app.Run (new Window ());

Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
Assert.Equal (mainThreadId, callbackThreadId);

app.Dispose ();
}

[Fact]
public void Init_Posts_To_The_App_Instance_Main_Thread ()
{
IApplication app1 = Application.Create ();
app1.Init (DriverRegistry.Names.ANSI);

IApplication app2 = Application.Create ();
app2.Init (DriverRegistry.Names.ANSI);

int mainThreadId1 = app1.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
int? callbackThreadId1 = null;
SynchronizationContext? synchronizationContext1 = null;

ManualResetEventSlim callbackCalled = new (false);

app1.AddTimeout (TimeSpan.FromMilliseconds (100),
() =>
{
if (synchronizationContext1 is null)
{
synchronizationContext1 = SynchronizationContext.Current;
synchronizationContext1?.Post (_ =>
{
callbackThreadId1 = Thread.CurrentThread.ManagedThreadId;
callbackCalled.Set ();
},
null);

return true;
}
else
{
app1.RequestStop ();

return false;
}
});

app1.Run (new Window ());

Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
Assert.Equal (mainThreadId1, callbackThreadId1);
Assert.Equal (synchronizationContext1, SynchronizationContext.Current);

callbackCalled.Reset ();

int mainThreadId2 = app2.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
int? callbackThreadId2 = null;
SynchronizationContext? synchronizationContext2 = null;

app2.AddTimeout (TimeSpan.FromMilliseconds (100),
() =>
{
if (synchronizationContext2 is null)
{
synchronizationContext2 = SynchronizationContext.Current;
synchronizationContext2?.Post (_ =>
{
callbackThreadId2 = Thread.CurrentThread.ManagedThreadId;
callbackCalled.Set ();
},
null);

return true;
}
else
{
app2.RequestStop ();

return false;
}
});

app2.Run (new Window ());

Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
Assert.Equal (mainThreadId2, callbackThreadId2);
Assert.NotEqual (synchronizationContext1, SynchronizationContext.Current);
Assert.Equal (synchronizationContext2, SynchronizationContext.Current);
Assert.NotEqual (synchronizationContext1, synchronizationContext2);

app1.Dispose ();
app2.Dispose ();
}

[Fact]
public void Dispose_Alone_Does_Nothing ()
{
Expand Down
Loading