private/Wait-MsalEssentialsTask.ps1
|
<#
.SYNOPSIS Starts an MSAL async call and blocks until it completes, without deadlocking on a UI thread. .DESCRIPTION PowerShell can't await, so MSAL's async methods are waited on with GetAwaiter().GetResult(). That blocks the calling thread. When the calling thread has a SynchronizationContext (the Dispatcher of a WPF window, the message loop of a WinForms form), MSAL captures it and posts continuations back to it. That thread is blocked in GetResult(), so the continuation never runs and everything freezes. The console host has no SynchronizationContext, which is why this only shows up when the module is used from a UI. In MSAL this happens in the system browser sign-in, while it waits for the loopback listener. The first sign-in in a process escapes it, because its instance discovery request moves the rest of the call to the thread pool. MSAL caches that metadata for the process, so the second Connect-MsalEssentials (after Disconnect-MsalEssentials) is the one that freezes. This clears the SynchronizationContext while the task is started and waited on, so MSAL's continuations run on the thread pool instead, and restores it afterwards. The task must be created inside -ScriptBlock: MSAL captures the context when the call starts, so a task created before this function runs would already be bound to the UI thread. .PARAMETER ScriptBlock A script block that starts the MSAL call and returns its task, for example { $request.ExecuteAsync() }. .EXAMPLE $accounts = Wait-MsalEssentialsTask -ScriptBlock { $clientApplication.GetAccountsAsync() } #> function Wait-MsalEssentialsTask { [CmdletBinding()] param ( [Parameter(Mandatory)] [scriptblock] $ScriptBlock ) $previousContext = [System.Threading.SynchronizationContext]::Current try { [System.Threading.SynchronizationContext]::SetSynchronizationContext($null) return (& $ScriptBlock).GetAwaiter().GetResult() } finally { [System.Threading.SynchronizationContext]::SetSynchronizationContext($previousContext) } } |