Skip to content

Commit 0a2161a

Browse files
committed
Add util MacMainThreadScheduler, and switch to main thread before call into NativeInterop interactive API
1 parent e50a97b commit 0a2161a

File tree

5 files changed

+252
-20
lines changed

5 files changed

+252
-20
lines changed

src/client/Microsoft.Identity.Client.Broker/RuntimeBroker.cs

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,34 @@ public async Task<MsalTokenResponse> AcquireTokenInteractiveAsync(
186186
{
187187
if (readAccountResult.IsSuccess)
188188
{
189-
using (var result = await s_lazyCore.Value.AcquireTokenInteractivelyAsync(
190-
_parentHandle,
191-
authParams,
192-
authenticationRequestParameters.CorrelationId.ToString("D"),
193-
readAccountResult.Account,
194-
cancellationToken).ConfigureAwait(false))
189+
if (DesktopOsHelper.IsMac())
195190
{
191+
AuthResult result = null;
192+
await MacMainThreadScheduler.Instance.RunOnMainThreadAsync(async () =>
193+
{
194+
result = await s_lazyCore.Value.AcquireTokenInteractivelyAsync(
195+
_parentHandle,
196+
authParams,
197+
authenticationRequestParameters.CorrelationId.ToString("D"),
198+
readAccountResult.Account,
199+
cancellationToken).ConfigureAwait(false);
200+
}).ConfigureAwait(false);
196201
var errorMessage = "Could not acquire token interactively.";
197202
msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage);
198203
}
204+
else // Non macOS
205+
{
206+
using (var result = await s_lazyCore.Value.AcquireTokenInteractivelyAsync(
207+
_parentHandle,
208+
authParams,
209+
authenticationRequestParameters.CorrelationId.ToString("D"),
210+
readAccountResult.Account,
211+
cancellationToken).ConfigureAwait(false))
212+
{
213+
var errorMessage = "Could not acquire token interactively.";
214+
msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage);
215+
}
216+
}
199217
}
200218
else
201219
{
@@ -238,16 +256,34 @@ private async Task<MsalTokenResponse> SignInInteractivelyAsync(
238256
string loginHint = authenticationRequestParameters.LoginHint ?? authenticationRequestParameters?.Account?.Username;
239257
_logger?.Verbose(() => "[RuntimeBroker] AcquireTokenInteractive - login hint provided? " + !string.IsNullOrEmpty(loginHint));
240258

241-
using (var result = await s_lazyCore.Value.SignInInteractivelyAsync(
242-
_parentHandle,
243-
authParams,
244-
authenticationRequestParameters.CorrelationId.ToString("D"),
245-
loginHint,
246-
cancellationToken).ConfigureAwait(false))
259+
if (DesktopOsHelper.IsMac())
247260
{
261+
AuthResult result = null;
262+
await MacMainThreadScheduler.Instance.RunOnMainThreadAsync(async () =>
263+
{
264+
result = await s_lazyCore.Value.SignInInteractivelyAsync(
265+
_parentHandle,
266+
authParams,
267+
authenticationRequestParameters.CorrelationId.ToString("D"),
268+
loginHint,
269+
cancellationToken).ConfigureAwait(false);
270+
}).ConfigureAwait(false);
248271
var errorMessage = "Could not sign in interactively.";
249272
msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage);
250273
}
274+
else // Non macOS
275+
{
276+
using (var result = await s_lazyCore.Value.SignInInteractivelyAsync(
277+
_parentHandle,
278+
authParams,
279+
authenticationRequestParameters.CorrelationId.ToString("D"),
280+
loginHint,
281+
cancellationToken).ConfigureAwait(true))
282+
{
283+
var errorMessage = "Could not sign in interactively.";
284+
msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage);
285+
}
286+
}
251287
}
252288

253289
return msalTokenResponse;
@@ -268,14 +304,31 @@ private async Task<MsalTokenResponse> AcquireTokenInteractiveDefaultUserAsync(
268304
_brokerOptions,
269305
_logger))
270306
{
271-
using (NativeInterop.AuthResult result = await s_lazyCore.Value.SignInAsync(
307+
if (DesktopOsHelper.IsMac())
308+
{
309+
AuthResult result = null;
310+
await MacMainThreadScheduler.Instance.RunOnMainThreadAsync(async () =>
311+
{
312+
result = await s_lazyCore.Value.SignInAsync(
313+
_parentHandle,
314+
authParams,
315+
authenticationRequestParameters.CorrelationId.ToString("D"),
316+
cancellationToken).ConfigureAwait(false);
317+
}).ConfigureAwait(false);
318+
var errorMessage = "Could not sign in interactively with the default OS account.";
319+
msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage);
320+
}
321+
else // Non macOS
322+
{
323+
using (NativeInterop.AuthResult result = await s_lazyCore.Value.SignInAsync(
272324
_parentHandle,
273325
authParams,
274326
authenticationRequestParameters.CorrelationId.ToString("D"),
275327
cancellationToken).ConfigureAwait(false))
276-
{
277-
var errorMessage = "Could not sign in interactively with the default OS account.";
278-
msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage);
328+
{
329+
var errorMessage = "Could not sign in interactively with the default OS account.";
330+
msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage);
331+
}
279332
}
280333
}
281334

src/client/Microsoft.Identity.Client/Internal/Requests/Interactive/InteractiveRequest.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.Identity.Client.Instance;
1010
using Microsoft.Identity.Client.Internal.Broker;
1111
using Microsoft.Identity.Client.OAuth2;
12+
using Microsoft.Identity.Client.PlatformsCommon.Shared;
1213
using Microsoft.Identity.Client.UI;
1314

1415
namespace Microsoft.Identity.Client.Internal.Requests
@@ -63,8 +64,46 @@ protected override async Task<AuthenticationResult> ExecuteAsync(
6364
{
6465
await ResolveAuthorityAsync().ConfigureAwait(false);
6566
cancellationToken.ThrowIfCancellationRequested();
66-
MsalTokenResponse tokenResponse = await GetTokenResponseAsync(cancellationToken)
67-
.ConfigureAwait(false);
67+
MsalTokenResponse tokenResponse = null;
68+
if (DesktopOsHelper.IsMac() && ServiceBundle.Config.IsBrokerEnabled)
69+
{
70+
var macMainThreadScheduler = MacMainThreadScheduler.Instance;
71+
if (!macMainThreadScheduler.IsCurrentlyOnMainThread)
72+
{
73+
throw new MsalClientException(
74+
MsalError.MacBrokerRequiresMainThread,
75+
"Interactive requests with mac broker enabled must be executed on the main thread. " +
76+
"Use MacMainThreadScheduler.Instance.RunOnMainThreadAsync to run the request on the main thread.");
77+
}
78+
bool messageLoopStarted = macMainThreadScheduler.IsRunning;
79+
var tcs = new TaskCompletionSource<MsalTokenResponse>();
80+
_ = Task.Run(async () =>
81+
{
82+
try
83+
{
84+
MsalTokenResponse response = await GetTokenResponseAsync(cancellationToken).ConfigureAwait(false);
85+
tcs.SetResult(response);
86+
}
87+
catch (Exception ex)
88+
{
89+
_logger.Error($"Error in background GetTokenResponseAsync: {ex}");
90+
tcs.SetException(ex);
91+
}
92+
finally
93+
{
94+
if (!messageLoopStarted)
95+
macMainThreadScheduler.Stop();
96+
}
97+
});
98+
if (!messageLoopStarted)
99+
macMainThreadScheduler.StartMessageLoop();
100+
tokenResponse = await tcs.Task.ConfigureAwait(false);
101+
}
102+
else
103+
{
104+
tokenResponse = await GetTokenResponseAsync(cancellationToken)
105+
.ConfigureAwait(false);
106+
}
68107
return await CacheTokenResponseAndCreateAuthenticationResultAsync(tokenResponse)
69108
.ConfigureAwait(false);
70109
}

src/client/Microsoft.Identity.Client/MsalError.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,11 @@ public static class MsalError
10021002
/// </summary>
10031003
public const string WamUiThread = "wam_ui_thread_only";
10041004

1005+
/// <summary>
1006+
/// When calling AcquireTokenInteractive with the mac broker, the call must be made from the main thread.
1007+
/// </summary>
1008+
public const string MacBrokerRequiresMainThread = "mac_broker_main_thread_only";
1009+
10051010
/// <summary>
10061011
/// The Windows broker (WAM) is only supported in conjunction with "work and school" accounts
10071012
/// and with Microsoft accounts.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
/// <summary>
10+
/// MacMainThreadScheduler is a utility class that allows scheduling actions to be executed on the main thread.
11+
/// </summary>
12+
public sealed class MacMainThreadScheduler
13+
{
14+
private readonly ConcurrentQueue<(Action Action, TaskCompletionSource<bool> Completion, bool IsAsyncAction)> _mainThreadActions;
15+
16+
private volatile bool _workerFinished;
17+
private volatile bool _isRunning;
18+
19+
// Singleton mode
20+
private static readonly Lazy<MacMainThreadScheduler> _instance = new Lazy<MacMainThreadScheduler>(() => new MacMainThreadScheduler());
21+
22+
/// <summary>
23+
/// Gets the singleton instance of MacMainThreadScheduler
24+
/// </summary>
25+
public static MacMainThreadScheduler Instance => _instance.Value;
26+
27+
/// <summary>
28+
/// Private constructor for MacMainThreadScheduler (singleton pattern)
29+
/// </summary>
30+
private MacMainThreadScheduler()
31+
{
32+
_mainThreadActions = new ConcurrentQueue<(Action, TaskCompletionSource<bool>, bool)>();
33+
_workerFinished = false;
34+
_isRunning = false;
35+
}
36+
37+
/// <summary>
38+
/// Check if the current thread is the main thread.
39+
/// </summary>
40+
public bool IsCurrentlyOnMainThread => Environment.CurrentManagedThreadId == 1; // Main thread id is always 1 on amcOS.
41+
42+
/// <summary>
43+
/// Check if the message loop is currently running.
44+
/// </summary>
45+
public bool IsRunning => _isRunning;
46+
47+
/// <summary>
48+
/// Stop the main thread message loop
49+
/// </summary>
50+
public void Stop()
51+
{
52+
_workerFinished = true;
53+
}
54+
55+
/// <summary>
56+
/// Run on the main thread asynchronously.
57+
/// </summary>
58+
/// <param name="asyncAction">action</param>
59+
/// <returns>FinishedTask</returns>
60+
public Task RunOnMainThreadAsync(Func<Task> asyncAction)
61+
{
62+
if (asyncAction == null)
63+
throw new ArgumentNullException(nameof(asyncAction));
64+
65+
var tcs = new TaskCompletionSource<bool>();
66+
Action wrapper = () =>
67+
{
68+
try
69+
{
70+
asyncAction().ContinueWith(task =>
71+
{
72+
if (task.IsFaulted && task.Exception != null)
73+
{
74+
tcs.TrySetException(task.Exception.InnerExceptions);
75+
}
76+
else
77+
{
78+
tcs.TrySetResult(true);
79+
}
80+
}, TaskScheduler.Default);
81+
}
82+
catch (Exception ex)
83+
{
84+
tcs.TrySetException(ex);
85+
}
86+
};
87+
88+
_mainThreadActions.Enqueue((wrapper, tcs, true));
89+
return tcs.Task;
90+
}
91+
92+
/// <summary>
93+
/// Start the message loop on the main thread to process actions
94+
/// </summary>
95+
public void StartMessageLoop()
96+
{
97+
if (!IsCurrentlyOnMainThread)
98+
throw new InvalidOperationException("Message loop must be started on the main thread.");
99+
100+
if (_isRunning)
101+
throw new InvalidOperationException("StartMessageLoop already running.");
102+
103+
_isRunning = true;
104+
_workerFinished = false;
105+
try
106+
{
107+
while (!_workerFinished)
108+
{
109+
while (_mainThreadActions.TryDequeue(out var actionItem))
110+
{
111+
try
112+
{
113+
actionItem.Action();
114+
if (!actionItem.IsAsyncAction)
115+
{
116+
actionItem.Completion.TrySetResult(true);
117+
}
118+
}
119+
catch (Exception ex)
120+
{
121+
actionItem.Completion.TrySetException(ex);
122+
}
123+
}
124+
Thread.Sleep(10);
125+
}
126+
}
127+
finally
128+
{
129+
_isRunning = false;
130+
}
131+
}
132+
133+
}
134+

tests/devapps/MacConsoleAppWithBroker/MacConsoleAppWithBroker.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,15 @@ await RunOnMainThreadAsync(async () =>
169169
// Make an HTTP request to switch to a background thread
170170
await SwitchToBackgroundThreadViaHttpRequest().ConfigureAwait(false);
171171

172-
// Second interactive call on main thread
172+
interactiveBuilder.WithAccount(account);
173+
// Second interactive call with account on main thread
173174
await RunOnMainThreadAsync(async () =>
174175
{
175176
try
176177
{
177178
// Execute the authentication request on the main thread
178179
result = await interactiveBuilder.ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
179-
Console.WriteLine("Second interactive authentication completed successfully.");
180+
Console.WriteLine("Second interactive authentication with account completed successfully.");
180181
}
181182
catch (Exception ex)
182183
{

0 commit comments

Comments
 (0)