Files

96 lines
3.7 KiB
PowerShell

# Cancel affordance for the status overlay.
#
# The app is effectively single-threaded: a long wait (device-code sign-in, a
# browser round trip) runs a loop on the UI thread and keeps the window alive by
# pumping messages (Invoke-UIPump). Because that pump already runs inside both
# login wait loops, a button on the status overlay DOES get its click dispatched -
# which is what makes a Cancel button possible at all without a threading rewrite.
#
# This file owns only the state and the contract. It deliberately holds no UI
# types (R12): the button lives in each backend, and its click handler calls
# Request-StatusCancel by NAME - which also satisfies the Avalonia rule that an
# event handler may not rely on captured locals.
#
# Two separate things are recorded:
#
# the ACTION what actually stops the pending operation (cancel a
# CancellationTokenSource, stop an HttpListener). Supplied by the
# caller that knows how, via Write-Status -OnCancel.
# the REQUEST a sticky flag the waiting loop polls, so it can break out
# promptly instead of running to its timeout.
#
# Both matter. Without the action the operation keeps running invisibly to its
# timeout; without the flag the loop would not notice for up to its poll interval
# (or at all, if the underlying wait is not cancellable).
$script:StatusCancelAction = $null
$script:StatusCancelRequested = $false
# Arm the overlay's cancel button. Called from Write-Status -OnCancel; also
# callable directly by code that wants the action armed without touching the
# status text.
function Set-StatusCancelAction
{
param([scriptblock]$Action)
$script:StatusCancelAction = $Action
# Arming starts a NEW cancellable operation, so a request left over from a
# previous one must not immediately cancel it.
$script:StatusCancelRequested = $false
}
# Disarm. Called when the status overlay is cleared, and from the finally block of
# whatever armed it, so a stale action can never fire against a finished operation.
function Clear-StatusCancelAction
{
$script:StatusCancelAction = $null
$script:StatusCancelRequested = $false
}
# True once the user has asked to cancel. Waiting loops poll this.
function Test-StatusCancelRequested
{
return ($script:StatusCancelRequested -eq $true)
}
# Sleep for up to -Seconds, in slices, pumping the UI between them so the overlay's
# Cancel button stays clickable, and return as soon as cancel is requested.
# Returns $true when the wait ended because of a cancel.
#
# A plain Start-Sleep of several seconds blocks the single UI thread outright: no
# repaint, no click dispatch, so a Cancel button would be dead for the whole nap.
# Any polling loop that wants to be cancellable has to wait THIS way.
function Wait-StatusCancel
{
param([int]$Seconds = 1, [int]$SliceMilliseconds = 100)
if($Seconds -le 0) { return (Test-StatusCancelRequested) }
$deadline = [DateTime]::UtcNow.AddSeconds($Seconds)
while([DateTime]::UtcNow -lt $deadline)
{
if(Test-StatusCancelRequested) { return $true }
Invoke-UIPump
Start-Sleep -Milliseconds $SliceMilliseconds
}
return (Test-StatusCancelRequested)
}
# Invoked by the overlay's Cancel button. Sets the flag first so the flag is
# observable even if the action throws, then runs the action.
function Request-StatusCancel
{
$script:StatusCancelRequested = $true
$action = $script:StatusCancelAction
if(-not $action) { return }
# One shot: a second click must not run the action again (stopping an already
# stopped listener throws).
$script:StatusCancelAction = $null
Write-Log "Cancel requested from the status window"
try { & $action }
catch { Write-LogError "Status cancel action failed" $_.Exception }
}