I use this script to check to see which CloudPCs are online and responding to ping and to restart any that are not. This can be useful if you need to deploy software or configurations to all CloudPCs and you want to make sure they are all only to receive your deployment.
You must have CloudPC Administrator and MgGraph permissions in your Azure tenant for this to work. I usually run this from a PowerShell 7 tab in Terminal and pre-authenticate to my
Tab Command Line: PowerShell-7.5.4-win-x64\pwsh.exe -NoExit -Command Connect-MgGraph -TenantId “<YourTenantID>” -NoWelcome
Modify the $NamePrefix as needed to match your CloudPC naming standards.
###########################################################################################################
#
# Script Name: Ping-Reboot-W365CloudPCs.ps1
#
# Description: Connects to Graph and queries all CloudPCs and reoirts online/offline status.
# Reboots any computers not responding to ping.
#
# Version History: 1.0 - Initial Script
#
# Command Line: .\Ping-Reboot-W365CloudPCs.ps1 -PingOnly
# .\Ping-Reboot-W365CloudPCs.ps1
#
###########################################################################################################
param(
# If set, we only ping + generate OFFLINE.TXT and do NOT reboot anything
[switch]$PingOnly,
# Filter: only Cloud PCs whose managedDeviceName starts with this prefix
[string]$NamePrefix = "W365",
# Ping target becomes <managedDeviceName>.<DnsSuffix>
[string]$DnsSuffix = "<YourDomain.com>",
# Offline list output (one device short name per line)
[string]$OfflineFile = ".\OFFLINE.TXT",
# Ping timeout per device (seconds)
[int]$PingCount = 1,
[int]$PingTimeoutSeconds = 2,
# Parallel throttle (only used in PS7+ parallel branch)
[int]$Throttle = 40,
# Only used if NOT already connected to Graph
[switch]$UseDeviceCode,
[switch]$DisableWam
)
$ErrorActionPreference="Stop"
function Write-Section {
param([string]$Text)
Write-Host ""
Write-Host "==== $Text ====" -ForegroundColor Cyan
}
# Initialize-Graph Function
function Initialize-Graph {
param(
[switch]$UseDeviceCode,
[switch]$DisableWam
)
# Ensure MgGraph request cmdlet exists before trying to call it
# (If cmdlets aren't loaded, Get-MgContext/Invoke-MgGraphRequest won't exist)
if (-not (Get-Command Invoke-MgGraphRequest -ErrorAction SilentlyContinue)) {
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
}
# If a context exists, we assume you're already connected and do nothing
$ctx = $null;
try {
$ctx = Get-MgContext
}
catch{}
# Connect only when no active account/tenant is present
if (-not $ctx -or -not $ctx.Account -or -not $ctx.TenantId) {
# Optional: force-disable WAM auth flow if your environment requires it
if ($DisableWam) {
$env:MSGRAPH_USE_WAM = "false"
}
# Connect with CloudPC.ReadWrite.All since reboot is a write operation
if ($UseDeviceCode) {
Connect-MgGraph -Scopes "CloudPC.ReadWrite.All" -UseDeviceCode | Out-Null
}
else {
Connect-MgGraph -Scopes "CloudPC.ReadWrite.All" | Out-Null
}
}
}
# Get-AllCloudPCs Function
function Get-AllCloudPcs {
# Pull Cloud PCs from Graph beta endpoint using paging via @odata.nextLink
$uri="https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs"
$pcs=@()
while ($uri) {
$r=Invoke-MgGraphRequest -Method GET -Uri $uri
# Some pages may be empty; guard to avoid null issues
if ($r -and $r.value) {
$pcs += @($r.value)
}
# If no nextLink, this becomes $null and loop ends
$uri=$r.'@odata.nextLink'
}
return $pcs
}
# Get-PingTargets Function
function Get-PingTargets {
param(
[object[]]$CloudPcs,
[string]$NamePrefix,
[string]$DnsSuffix
)
# Build a simple list of targets to ping:
# Short = managedDeviceName, Target = FQDN used for ping, Id = CloudPC id used for reboot
$targets=@()
foreach ($pc in @($CloudPcs)) {
$sn=[string]$pc.managedDeviceName
# Skip objects without a managedDeviceName
if (-not $sn) { continue }
# Prefix filter (case-insensitive)
if ($sn.StartsWith($NamePrefix,[System.StringComparison]::OrdinalIgnoreCase)) {
$targets += [pscustomobject]@{
Short=$sn
Target=("$sn.$DnsSuffix".TrimEnd('.')) # protects if suffix is empty or ends with dot
Id=$pc.id
}
}
}
return $targets
}
# Invoke-PingTargets Function
function Invoke-PingTargets {
param(
[object[]]$Targets,
[int]$PingTimeoutSeconds,
[int]$Throttle
)
# Ping each target and return objects with Online=$true/$false.
# In PS7+, use ForEach-Object -Parallel for faster runs on large sets.
if ($PSVersionTable.PSVersion.Major -ge 7) {
# NOTE: this is the built-in PS7 parallel loop mechanism
$ping = $Targets | ForEach-Object -Parallel {
[pscustomobject]@{
# what gets returned
Short=$_.Short; Target=$_.Target; Id=$_.Id
Online=(Test-Connection -ComputerName $_.Target -Count 1 -TimeoutSeconds $using:PingTimeoutSeconds -Quiet -ErrorAction SilentlyContinue)
}
} -ThrottleLimit $Throttle
return @($ping)
}
# PS5.1 fallback (sequential)
$ping=@()
foreach ($t in @($Targets)) {
$ping += [pscustomobject]@{
# what gets returned
Short=$t.Short; Target=$t.Target; Id=$t.Id
Online=(Test-Connection -ComputerName $t.Target -Count $PingCount -Quiet -ErrorAction SilentlyContinue)
}
}
return $ping
}
# Show-PingResults Function
function Show-PingResults {
param([object[]]$PingResults)
# Optional alphabetical sort (disabled by default)
# $sorted = @($PingResults | Sort-Object Short)
# Use original order unless sorting is re-enabled above
$list = if ($sorted) { $sorted } else { $PingResults }
foreach ($r in @($list)) {
if ($r.Online) {
Write-Host ("ONLINE: {0} ({1})" -f $r.Short, $r.Target) #-ForegroundColor Green
}
else {
Write-Warning ("OFFLINE (ping failed): {0} ({1})" -f $r.Short, $r.Target)
}
}
}
# Get-OfflineList Function
function Get-OfflineList {
param([object[]]$PingResults)
# Build unique offline short-name list for OFFLINE.TXT
$offline=@()
foreach ($r in @($PingResults)) {
if (-not $r.Online) {
$offline += $r.Short
}
}
$offline = @($offline | Sort-Object -Unique)
return $offline
}
# Get-OfflinePingRows Function
function Get-OfflinePingRows {
param([object[]]$PingResults)
# Keep the full rows for offline devices (includes Id for reboot)
$rows=@()
foreach ($r in @($PingResults)) {
if (-not $r.Online) {
$rows += $r
}
}
return $rows
}
# Restart-CloudPc Function
function Restart-CloudPc {
param([string]$Id)
# Calls the Graph Cloud PC reboot action:
# POST /deviceManagement/virtualEndpoint/cloudPCs/{id}/reboot
$u="https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs/$Id/reboot"
# Retry a few times because Graph can return 409 conflict if the Cloud PC is busy
for ($a=1;$a -le 5;$a++) {
try {
Invoke-MgGraphRequest -Method POST -Uri $u | Out-Null
return $true
}
catch {
# Handle the common "busy/conflict" cases with a small backoff
if ($_.Exception.Message -match '409|conflict') {
Start-Sleep -Seconds ([math]::Min(120,5*$a))
continue
}
# Any other error: bubble up to the caller
throw
}
}
# Ran out of retries
return $false
}
# ----------------------------------------------------------------------
# MAIN SCRIPT
# ----------------------------------------------------------------------
Write-Host "Starting Script Execution..." -ForegroundColor Cyan
# 1) Ensure Graph cmdlets exist + connect only if needed
Write-Host "Checking graph connection, if none found, prompt for login." -ForegroundColor White
Initialize-Graph -UseDeviceCode:$UseDeviceCode -DisableWam:$DisableWam
# 2) Pull full Cloud PC inventory and filter to target prefix
Write-Section "Fetch Cloud PC Inventory"
Write-Host "Fetching Cloud PC inventory (paged)..." -ForegroundColor Cyan
$pcs = Get-AllCloudPcs
$targets = Get-PingTargets -CloudPcs $pcs -NamePrefix $NamePrefix -DnsSuffix $DnsSuffix
if ($targets) {
#Write-Host ("Filtered to {0} Cloud PCs where managedDeviceName starts with '{1}'." -f $w365Pcs.Count, $NamePrefix) -ForegroundColor Cyan
Write-Host ("Filtered to {0} Cloud PCs total where managedDeviceName starts with '$NamePrefix'." -f $pcs.Count) -ForegroundColor Cyan
}
else {
Write-Warning "No Cloud PCs matched prefix '$NamePrefix'"
return
}
# 3) Ping all targets
Write-Section "Ping Cloud PCs & Generate OFFLINE.TXT"
Write-Host ("Pinging using suffix: .{0}" -f $DnsSuffix) -ForegroundColor Cyan
Write-Host ("PingCount={0}, TimeoutSeconds={1}, Parallel={2}, Throttle={3}" -f $PingCount, $PingTimeoutSeconds, (-not $NoParallelPing), $Throttle) -ForegroundColor Cyan
$ping = Invoke-PingTargets -Targets $targets -PingTimeoutSeconds $PingTimeoutSeconds -Throttle $Throttle
# 4) Display per-device ping results (ONLINE/OFFLINE)
Write-Host ""
Show-PingResults -PingResults $ping
# 5) Write OFFLINE.TXT for any ping failures
$offline = Get-OfflineList -PingResults $ping
$offline | Set-Content $OfflineFile
Write-Host ("OFFLINE saved: {0} ({1} systems)" -f (Resolve-Path $OfflineFile), $offline.Count) -ForegroundColor Yellow
# 6) If PingOnly, stop here (no reboot actions)
if ($PingOnly) {
Write-Host "PingOnly enabled - skipping reboots." -ForegroundColor Yellow
$totalPinged = $ping.Count
$onlineCount = ($ping | Where-Object { $_.Online }).Count
$offlineCount = ($ping | Where-Object { -not $_.Online }).Count
Write-Host "PING SUMMARY : TOTAL: $totalPinged ONLINE: $onlineCount OFFLINE: $offlineCount" -ForegroundColor DarkCyan
return
}
else {
Write-Host "Rebooting all offline Cloud PCs..." -ForegroundColor White
}
# 7 Set Summary counters
$totalPinged = $ping.Count
$onlineCount = ($ping | Where-Object { $_.Online }).Count
$offlineCount = ($ping | Where-Object { -not $_.Online }).Count
$rebootAttempted = 0
$rebootSuccess = 0
$rebootFailed = 0
# 8) Reboot Cloud PCs that failed ping (best-effort, logs to console)
$offlineRows = Get-OfflinePingRows -PingResults $ping
foreach ($row in $offlineRows) {
$rebootAttempted++
try {
$ok = Restart-CloudPc -Id $row.Id
if ($ok) {
$rebootSuccess++
Write-Host ("{0}: REBOOT SENT" -f $row.Short) -ForegroundColor Cyan
}
else {
$rebootFailed++
Write-Host ("{0}: RETRY LIMIT" -f $row.Short) -ForegroundColor Yellow
}
}
catch {
$rebootFailed++
Write-Warning ("{0}: REBOOT FAILED - {1}" -f $row.Short, $_.Exception.Message)
}
}
# Display Summary and Finish
Write-Host ""
if ($PingOnly) {
Write-Host "PING SUMMARY : TOTAL: $totalPinged ONLINE: $onlineCount OFFLINE: $offlineCount" -ForegroundColor DarkCyan
}
else {
Write-Host "PING SUMMARY : TOTAL: $totalPinged ONLINE: $onlineCount OFFLINE: $offlineCount" -ForegroundColor DarkCyan
Write-Host "REBOOT SUMMARY : ATTEMPTED: $rebootAttempted SUCCEEDED: $rebootSuccess FAILED: $rebootFailed" -ForegroundColor DarkCyan
}
Write-Host "Finished script execution." -ForegroundColor Cyan