Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -17,6 +17,8 @@ internal partial class ApplicationImpl
/// <inheritdoc/>
public event EventHandler<EventArgs<bool>>? InitializedChanged;

internal SynchronizationContext SynchronizationContext;
Comment thread
BDisp marked this conversation as resolved.
Outdated

Comment thread
BDisp marked this conversation as resolved.
Outdated
/// <inheritdoc/>
public IApplication Init (string? driverName = null)
{
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);

Comment thread
tig marked this conversation as resolved.
// 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 { })
{
throw error;
}
Comment thread
BDisp marked this conversation as resolved.
}
}
116 changes: 116 additions & 0 deletions Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,122 @@ 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);

int mainThreadId2 = app2.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
Comment thread
BDisp marked this conversation as resolved.
int? callbackThreadId2 = null;
SynchronizationContext? synchronizationContext2 = null;
Comment thread
BDisp marked this conversation as resolved.

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