Do / Until loop issues

Product: PowerShell Universal
Version: 3.8.2

Hey PSU folks,

I’m running into an issue with a Do/Until loop, where it can run infinitely if the condition is not met and the user refreshes the browser, goes back in the browser, or closes the browser.

I’m writing a tool to deploy Azure VMs. Once deployment completes, I join the VM to AD and reboot it. I need to ping test until it comes back online, then run more cmds for configurations. Outside of PSU, I’ve always used Do/Until loops for this. If the loop hangs by not meeting the condition, the user can always stop the script or close the PoSh window to stop it.

In PSU I found if the user leaves the page in any way, the loop will continue running in the background until the condition is met. Also, once the condition is met any code after that will continue to run in the background; even through the user has refreshed, closed, or otherwise left the page while the loop was running.

Restarting the dashboard will not stop the loop. The only ways I’ve found to stop the loop is to restart the PowerShellUniversal service or meet the condition. But once the condition is met it will continue running additional code which we probably don’t want it doing at this point.

How can we protect against this? The ping example I bring up here has a low chance of actually happening, b/c the condition should be met in the real world in a timely fashion after a reboot. However, I have upcoming projects to refactor several shell and GUI PoSh scripts into PSU and a lot of them create mailboxes. Doing this requires us to create the on-prem user and remote mailbox, trigger an AAD Connect sync, loop until M365 sees the user then assign a license, and then loop until EXOL provisions the mailbox and apply mailbox configurations. These tools will require 2 Do/Until loops, and b/c we’re working with M365 there’s a higher chance of something stalling the loops and causing the user to leave the page.

As I’m writing this, I’m thinking to provide the user a Cancel button for these instances, but that’s still putting trust in the users to click the Cancel button and NOT to leave the page in anyway.

The below Ping Test example will ping BogusName every 15 seconds, until it responds. There will be no response, so it will keep going. If I refresh or leave the page, the loop will continue running in the background. You can tell this by checking the log file and see it to continue to write every 15 seconds. I then add BogusName to my hosts file, to meet the condition. The loop running in the background will be satisfied and stop, but the cmd after the loop to write Ping Successful to the log file still runs; this could be problematic.

New-UDDashboard -Title 'Ping Testing' -Content {
    Show-UDModal -Content {
        New-UDHtml -Markup "Ping testing with Do/Until"

        # Dynamic progress message.
        $session:output = 'Done'

        New-UDDynamic -Id 'progress' -Content {
            if ($session:output -ne 'Done') {
                New-UDTypography -Text $session:output
                New-UDProgress
            }# if ($session:output -ne 'Done')
        }# New-UDDynamic -Id 'progress'
    
        New-UDButton -Id 'btn_ping' -Text "Ping Test" -OnClick {
            Set-UDElement -ID "btn_ping" -Attributes @{Disabled = $true}

            $logPath = 'C:\tmp\'
            $logDate = Get-Date -UFormat "%Y%m%d-%H%M%S"
            $logFile = $logPath + $logDate + "_Testing_Do-Until.txt"

            # Test pinging by DNS name, and do not continue until we can ping it.
            Add-Content -Path $logFile -Value "`r`nPinging VM every 15 seconds:"
            $ping_Seconds = 0

            do {
                $session:output = "Pinging VM every 15 seconds: $ping_Seconds Seconds ..."
                Sync-UDElement -Id 'progress'
                Add-Content -Path $logFile -Value "    * Pinging at $ping_Seconds seconds ..."

                Clear-DnsClientCache -Confirm:$false -ErrorAction SilentlyContinue
                $pingTest = Test-Connection -TargetName 'BogusName' -IPv4 -Count 1 -ErrorAction SilentlyContinue
                Start-Sleep -Seconds 15
                $ping_Seconds += 15
            }# do

            until ($pingTest.Status -eq 'Success')        
            Add-Content -Path $logFile -Value "    * Successful ping"
            
            $session:output = 'Done'
            Sync-UDElement -Id 'progress'
            
            Set-UDElement -Id "btn_ping" -Attributes @{Disabled = $false}
        }# New-UDButton
    }# Show-UDModal
}# New-UDDashboard -Content

This is a problem that I’ve been thinking about lately. It’s pretty easy to do this and then the runspace just gets stuck. I don’t necessarily want to cancel all endpoint execution when users leave a page because it’s a pretty big fundamental change to how UD works, that said, we likely need to address this internally somehow.

In the meantime, you can check to see if the user is still connected by using the following snippet.

This checks to see if the current connection ID is still actually connected to PSU. If the user closes their browser page, it will terminate the do\while loop.

                if ($Host.PrivateData.ConnectionManager.GetSessionId($ConnectionId) -eq $null)
                {
                    Write-Host "No longer connected"
                    throw "No longer connected";
                }

You could use it like this. You’ll see that once you close the browser, you’ll see “No longer connected” in the log.

            do {
                $session:output = "Pinging VM every 15 seconds: $ping_Seconds Seconds ..."
                Sync-UDElement -Id 'progress'
                Add-Content -Path $logFile -Value "    * Pinging at $ping_Seconds seconds ..."

                Clear-DnsClientCache -Confirm:$false -ErrorAction SilentlyContinue
                $pingTest = Test-Connection -TargetName 'BogusName' -IPv4 -Count 1 -ErrorAction SilentlyContinue
                Start-Sleep -Seconds 15
                $ping_Seconds += 15

                if ($Host.PrivateData.ConnectionManager.GetSessionId($ConnectionId) -eq $null)
                {
                    Write-Host "No longer connected"
                    throw "No longer connected";
                }

            }# do
2 Likes

I tested this today by closing the browser, refreshing, clicking back, and quickly clicking back then forward again. Works great in all scenarios!

Thank you once again @adam, you da man!

Hey @adam,

Recently I started having errors about “You cannot call a method on a null-valued expression” pop up when using the tool I included this work around in. I was able to figure out they were coming from $Host.PrivateData.ConnectionManager.GetSessionId($ConnectionId). Each time this runs it generates this non-terminating error, so once the tool successfully completes, I get an error message popup from UD; one for each time this command would have been run in the Do/Until loop when pinging a VM.

Further testing showed the workaround itself is no longer working; I assume due to this error. The if statement is no longer working to detect when a user is no longer connected, therefore the throw is no longer being triggered to halt the script and it just continues running infinitely in the background (back to the original problem).

I went back to the Ping Test tool and found these issues are happening with it as well. I put the Ping Test tool on another PSU server, running an older version, and still have the same troubles.

Any idea why this would have stopped working? At this point, I think I’m going to remove this from the tool and replace it with a 5-minute timeout. The timeout should work fine in this current tool I’m developing, when pinging VMs, but soon to come projects will have the need to use Do/Until to check M365 and EXOL for users and mailboxes after we create them on-prem and trigger sync. Having this working to detect when a user disconnects would be much better than a timeout when I get to doing those.

null-method

New-UDDashboard -Title 'Ping Testing' -Content {
    Show-UDModal -Content {
        New-UDHtml -Markup "Ping testing with Do/Until"

        # Dynamic progress message.
        $session:output = 'Done'

        New-UDDynamic -Id 'progress' -Content {
            if ($session:output -ne 'Done') {
                New-UDTypography -Text $session:output
                New-UDProgress
            }# if ($session:output -ne 'Done')
        }# New-UDDynamic -Id 'progress'
    
        New-UDButton -Id 'btn_ping' -Text "Ping Test" -OnClick {
            Set-UDElement -ID "btn_ping" -Attributes @{Disabled = $true}

            $logPath = 'C:\tmp\'
            $logDate = Get-Date -UFormat "%Y%m%d-%H%M%S"
            $logFile = $logPath + $logDate + "_Testing_Do-Until.txt"

            # Test pinging by DNS name, and do not continue until we can ping it.
            Add-Content -Path $logFile -Value "`r`nPinging VM every 15 seconds:"
            $ping_Seconds = 0

            do {
                $session:output = "Pinging VM every 15 seconds: $ping_Seconds Seconds ..."
                Sync-UDElement -Id 'progress'
                Add-Content -Path $logFile -Value "    * Pinging at $ping_Seconds seconds ..."

                Clear-DnsClientCache -Confirm:$false -ErrorAction SilentlyContinue
                $pingTest = Test-Connection -TargetName 'BogusName' -IPv4 -Count 1 -ErrorAction SilentlyContinue
                Start-Sleep -Seconds 15
                $ping_Seconds += 15

                if ($Host.PrivateData.ConnectionManager.GetSessionId($ConnectionId) -eq $null) {
                    Add-Content -Path $logFile -Value "No longer connected to:  PingTest"
                    throw "No longer connected to:  PingTest"
                }# if ($Host.PrivateData ...)
            }# do

            until ($pingTest.Status -eq 'Success')        
            Add-Content -Path $logFile -Value "    * Successful ping"
            
            $session:output = 'Done'
            Sync-UDElement -Id 'progress'
            
            Set-UDElement -Id "btn_ping" -Attributes @{Disabled = $false}
        }# New-UDButton
    }# Show-UDModal
}# New-UDDashboard -Content

Thanks,
Rob