Presence, AD/SCCM/DNS reasons, monitors, snapshots, new-device priority
Worker: pings every tracked device on a presence sweep (hourly in business hours plus midnight) and records last online; inventory reports whether a device was found in AD and SCCM; monitors from WmiMonitorID; snapshots of the last reachable state replace "Cached"; newly tracked devices are swept and published first; totals count source devices only; per-type timing in Worker-State. Front end: details dialog shows presence, reasons instead of N/A, "Waiting for worker...", monitors and snapshot times, and refreshes in place; Custom View gains Days Offline, Last Online, Found In and monitor columns; manual pair changes show immediately; sync dialog shows timing by type. Custom View gets a large scrollbar; "Exactly ticked" filter.
This commit is contained in:
1 parent
50c2a18a68
commit
7868948bfc
10 files changed
+1230
-356
No files matched your search
+484
-217
@@ -22,10 +22,17 @@ $completedPath = Join-Path $dataDir "Completed.json"
|
||||
$statePath = Join-Path $dataDir "Worker-State.json"
|
||||
$softwareCachePath = Join-Path $dataDir "Software-Cache.json"
|
||||
$advancedCachePath = Join-Path $dataDir "Advanced-Cache.json"
|
||||
$presencePath = Join-Path $dataDir "Presence.json"
|
||||
$snapshotPath = Join-Path $dataDir "Snapshots.json"
|
||||
$getSoftwareScript = Join-Path $originalLocation "Get-Software.ps1"
|
||||
$diskSkipList = @($config.Worker.DiskSkipList) # Devices with broken SMB that hang Directory.Exists
|
||||
$scanLogName = if ($config.SCCM.ScanStateLog) { $config.SCCM.ScanStateLog } else { "scanstateprogress" }
|
||||
$loadLogName = if ($config.SCCM.LoadStateLog) { $config.SCCM.LoadStateLog } else { "loadstateprogress" }
|
||||
# How often every tracked device (cached and destination devices included) is pinged to keep
|
||||
# "last online" current. Cycles only run in business hours plus the midnight pitstop, so this
|
||||
# works out to an hourly presence sweep during the day and one at midnight.
|
||||
$presenceMinutes = if ($config.IsDemo) { $config.Demo.PresenceSweepMinutes } else { $config.Worker.PresenceSweepMinutes }
|
||||
if (-not $presenceMinutes) { $presenceMinutes = 60 }
|
||||
|
||||
if ($config.Logging.Enabled) {
|
||||
$cmdLine = "PS $originalLocation> ./USMT-Worker.ps1" + $(if ($config.IsDemo) { " -Demo" } else { "" }) + $(if ($v) { " -v" } else { "" })
|
||||
@@ -42,6 +49,8 @@ if (Test-Path $softwareCachePath) {
|
||||
$globalLastUpdated = "Never"
|
||||
$globalNextRun = "Calculating..."
|
||||
$globalLastDuration = "N/A"
|
||||
$global:LastBreakdown = $null
|
||||
$global:LastPresenceSweep = [datetime]::MinValue
|
||||
|
||||
# --- FUNCTIONS ---
|
||||
function Update-WorkerState {
|
||||
@@ -54,7 +63,8 @@ function Update-WorkerState {
|
||||
LastUpdated = (Get-Date).ToString("MM/dd/yy hh:mm tt")
|
||||
NextCycle = $nextRun
|
||||
LastDuration = $duration
|
||||
} | ConvertTo-Json -Compress
|
||||
LastCycle = $global:LastBreakdown
|
||||
} | ConvertTo-Json -Compress -Depth 5
|
||||
|
||||
[System.IO.File]::WriteAllText($statePath, $json, [System.Text.Encoding]::UTF8)
|
||||
} catch {}
|
||||
@@ -79,10 +89,176 @@ function Set-LogInfoLine {
|
||||
if ($rawLine -match '^\d{1,2}\s+[a-zA-Z]{3}\s+\d{4},\s+\d{2}:\d{2}:\d{2}\s+[+-]\d{2}:\d{2},\s+(\d{2}:\d{2}:\d{2})') { $result.Elapsed = $Matches[1] }
|
||||
}
|
||||
|
||||
function Get-IsoNow { return (Get-Date).ToString("o") }
|
||||
|
||||
function ConvertTo-Hashtable($obj) {
|
||||
$h = @{}
|
||||
if ($obj -is [hashtable]) { foreach ($k in $obj.Keys) { $h[$k] = $obj[$k] } }
|
||||
elseif ($obj) { foreach ($p in $obj.psobject.properties) { $h[$p.Name] = $p.Value } }
|
||||
return $h
|
||||
}
|
||||
|
||||
# Reads a JSON object of objects into a hashtable of hashtables
|
||||
function Read-JsonTable([string]$path) {
|
||||
$table = @{}
|
||||
if (Test-Path $path) {
|
||||
try {
|
||||
$raw = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
|
||||
foreach ($p in $raw.psobject.properties) { $table[$p.Name] = ConvertTo-Hashtable $p.Value }
|
||||
} catch {}
|
||||
}
|
||||
return $table
|
||||
}
|
||||
|
||||
function Write-JsonFile([string]$path, $obj, [int]$depth = 6) {
|
||||
$json = ConvertTo-Json -InputObject $obj -Depth $depth -Compress
|
||||
[System.IO.File]::WriteAllText($path, $json, [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
|
||||
# --- PROVIDER (data source adapter) ---
|
||||
# Supplies: Initialize-Provider, Open-ProviderCycle, Close-ProviderCycle, pingDevice,
|
||||
# Resolve-DeviceIP, Get-DiskSpace, Get-LogInfo, Get-DeviceAssociations, Get-DeviceInventory
|
||||
# and optionally Get-DeviceLastSeen (backfills "last online" for devices tracked for the first time)
|
||||
. (Join-Path $originalLocation "Providers/$providerName.ps1")
|
||||
$hasLastSeen = [bool](Get-Command Get-DeviceLastSeen -ErrorAction SilentlyContinue)
|
||||
|
||||
# --- PRESENCE (last online) ---
|
||||
# Every ping the worker makes is recorded here: when the device was last checked, what the
|
||||
# ping said, and when it last answered. Kept across restarts in Presence.json.
|
||||
$global:Presence = Read-JsonTable $presencePath
|
||||
$global:PingedThisCycle = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
|
||||
function Set-Presence {
|
||||
param([string]$name, [string]$status, $Session)
|
||||
if ([string]::IsNullOrWhiteSpace($name) -or $name -eq "N/A") { return }
|
||||
$key = $name.Trim().ToUpper()
|
||||
$now = Get-IsoNow
|
||||
$entry = $global:Presence[$key]
|
||||
if (-not $entry) {
|
||||
$entry = @{ FirstChecked = $now; LastOnline = $null }
|
||||
# First time we see this device: ask the data source when it was last active
|
||||
if ($status -ne "Online" -and $hasLastSeen) {
|
||||
try {
|
||||
$seen = Get-DeviceLastSeen $key -Session $Session
|
||||
if ($seen) { $entry.LastOnline = ([datetime]$seen).ToString("o"); $entry.LastOnlineSource = "Inventory" }
|
||||
} catch {}
|
||||
}
|
||||
$global:Presence[$key] = $entry
|
||||
}
|
||||
$entry.LastChecked = $now
|
||||
$entry.LastPing = $status
|
||||
$entry.InDNS = ($status -ne "Not Found")
|
||||
if ($status -eq "Online") { $entry.LastOnline = $now; $entry.LastOnlineSource = "Ping" }
|
||||
$null = $global:PingedThisCycle.Add($key)
|
||||
}
|
||||
|
||||
# pingDevice + presence bookkeeping
|
||||
function Test-Device {
|
||||
param([string]$name, $Session)
|
||||
$status = pingDevice $name
|
||||
Set-Presence $name $status -Session $Session
|
||||
return $status
|
||||
}
|
||||
|
||||
function Get-PresenceValue([string]$name, [string]$field) {
|
||||
if ([string]::IsNullOrWhiteSpace($name) -or $name -eq "N/A") { return $null }
|
||||
$e = $global:Presence[$name.ToUpper()]
|
||||
if ($e) { return $e[$field] }
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- SNAPSHOTS ---
|
||||
# The last state seen while a device was reachable: free space, the last scanstate /
|
||||
# loadstate line, and when each was read. Shown (with its timestamp) instead of "Cached" for
|
||||
# completed migrations, and instead of blanks while a device is offline.
|
||||
$global:Snapshots = Read-JsonTable $snapshotPath
|
||||
|
||||
function Update-Snapshot {
|
||||
param([string]$sName, [string]$dName, $row, [bool]$srcLive, [bool]$dstLive)
|
||||
$key = $sName.ToUpper()
|
||||
$snap = $global:Snapshots[$key]
|
||||
if (-not $snap) { $snap = @{}; $global:Snapshots[$key] = $snap }
|
||||
$now = Get-IsoNow
|
||||
if ($srcLive) {
|
||||
$snap.Src = @{
|
||||
At = $now; Total = $row."Source Total Space"; Free = $row."Source Free Space"
|
||||
Line = $row."Source Scanstate Line"; File = $row."Source Scanstate File"; Elapsed = $row."Source Scanstate Elapsed"
|
||||
}
|
||||
}
|
||||
if ($dstLive -and $dName -and $dName -ne "N/A") {
|
||||
$snap.Dst = @{
|
||||
At = $now; Device = $dName.ToUpper()
|
||||
Line = $row."Destination Loadstate Line"; File = $row."Destination Loadstate File"; Elapsed = $row."Destination Loadstate Elapsed"
|
||||
}
|
||||
}
|
||||
if ($srcLive -or $dstLive) { $snap.Status = $row."Backup Status"; $snap.At = $now }
|
||||
}
|
||||
|
||||
# Fills the source / destination parts of a row from the snapshot. Returns what was used.
|
||||
function Use-Snapshot {
|
||||
param([string]$sName, [string]$dName, $row, [bool]$src, [bool]$dst)
|
||||
$snap = $global:Snapshots[$sName.ToUpper()]
|
||||
if (-not $snap) { return }
|
||||
$s = $snap.Src; $d = $snap.Dst
|
||||
if ($src -and $s) {
|
||||
$s = ConvertTo-Hashtable $s
|
||||
$row."Source Total Space" = $s.Total; $row."Source Free Space" = $s.Free
|
||||
$row."Source Scanstate Line" = $s.Line; $row."Source Scanstate File" = $s.File; $row."Source Scanstate Elapsed" = $s.Elapsed
|
||||
$row."Source Snapshot" = $s.At
|
||||
$row."Source Space Checked" = $s.At
|
||||
}
|
||||
if ($dst -and $d) {
|
||||
$d = ConvertTo-Hashtable $d
|
||||
if (-not $dName -or $dName -eq "N/A" -or $d.Device -eq $dName.ToUpper()) {
|
||||
$row."Destination Loadstate Line" = $d.Line; $row."Destination Loadstate File" = $d.File; $row."Destination Loadstate Elapsed" = $d.Elapsed
|
||||
$row."Destination Snapshot" = $d.At
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Every row has the same columns; these are filled in as the device is processed
|
||||
function New-DeviceRow {
|
||||
param([string]$sName, [string]$dName, [string]$tags, [string]$pairing)
|
||||
return [ordered]@{
|
||||
"Source Device"=$sName; "Destination Device"=$dName; "Tags"=$tags; "Pairing"=$pairing;
|
||||
"Source Total Space"="N/A"; "Source Free Space"="N/A";
|
||||
"USMT Status"="N/A"; "Backup Status"="N/A";
|
||||
"Source Online"="N/A"; "Destination Online"="N/A";
|
||||
"Source Scanstate Line"="N/A"; "Destination Loadstate Line"="N/A";
|
||||
"Source Scanstate File"="N/A"; "Destination Loadstate File"="N/A";
|
||||
"Source Scanstate Elapsed"="N/A"; "Destination Loadstate Elapsed"="N/A";
|
||||
"Source Checked"=$null; "Destination Checked"=$null; "Source Space Checked"=$null;
|
||||
"Source Snapshot"=$null; "Destination Snapshot"=$null
|
||||
}
|
||||
}
|
||||
|
||||
# Advanced-Cache.json = inventory per device plus its presence (last online / last check)
|
||||
function Save-AdvancedCache {
|
||||
$out = @{}
|
||||
foreach ($key in @($global:AdvancedCache.Keys)) {
|
||||
$entry = ConvertTo-Hashtable $global:AdvancedCache[$key]
|
||||
$pr = $global:Presence[$key.ToUpper()]
|
||||
if ($pr) {
|
||||
foreach ($f in @("LastOnline", "LastOnlineSource", "LastChecked", "LastPing", "FirstChecked", "InDNS")) { $entry[$f] = $pr[$f] }
|
||||
}
|
||||
$out[$key] = $entry
|
||||
}
|
||||
Write-JsonFile $advancedCachePath $out 5
|
||||
Write-JsonFile $presencePath $global:Presence 4
|
||||
}
|
||||
|
||||
# Cached / skipped rows show the latest presence-sweep result instead of a placeholder
|
||||
function Set-RowPresence {
|
||||
param($row, [switch]$Source, [switch]$Destination)
|
||||
if ($Source) {
|
||||
$ping = Get-PresenceValue $row."Source Device" "LastPing"
|
||||
if ($ping) { $row."Source Online" = $ping; $row."Source Checked" = Get-PresenceValue $row."Source Device" "LastChecked" }
|
||||
}
|
||||
if ($Destination) {
|
||||
$ping = Get-PresenceValue $row."Destination Device" "LastPing"
|
||||
if ($ping) { $row."Destination Online" = $ping; $row."Destination Checked" = Get-PresenceValue $row."Destination Device" "LastChecked" }
|
||||
}
|
||||
}
|
||||
|
||||
$global:AdvancedCache = @{}
|
||||
$global:AdvancedCacheUpdated = $false
|
||||
@@ -98,7 +274,7 @@ if (Test-Path $advancedCachePath) {
|
||||
function Get-AdvancedData {
|
||||
param ([string]$computerName, [switch]$IsDest, [string]$OnlineStatus, [switch]$ForceUpdate, $Session)
|
||||
|
||||
$res = @{ IP="N/A"; MAC="N/A"; OU="N/A"; Groups="N/A"; Updated="N/A"; Location="N/A"; Vanity="N/A"; Department="N/A"; Building="N/A"; Mount="N/A"; PrimaryUser="N/A"; LastUser="N/A"; SCCMPrimaryUsers="N/A"; Model="N/A"; RAM="N/A"; Disk="N/A"; ClientStatus="N/A"; PhoneExt="N/A"; RoomType="Standard"; InfoBy="N/A"; Notes="N/A" }
|
||||
$res = @{ IP="N/A"; MAC="N/A"; OU="N/A"; Groups="N/A"; Updated="N/A"; Location="N/A"; Vanity="N/A"; Department="N/A"; Building="N/A"; Mount="N/A"; PrimaryUser="N/A"; LastUser="N/A"; SCCMPrimaryUsers="N/A"; Model="N/A"; RAM="N/A"; Disk="N/A"; ClientStatus="N/A"; PhoneExt="N/A"; RoomType="Standard"; InfoBy="N/A"; Notes="N/A"; MonitorCount="N/A"; Monitors="N/A"; InAD=$null; InSCCM=$null; Fetched=$null }
|
||||
if ([string]::IsNullOrWhiteSpace($computerName) -or $computerName -eq "N/A") { return $res }
|
||||
|
||||
$needsUpdate = $true
|
||||
@@ -107,6 +283,9 @@ function Get-AdvancedData {
|
||||
$cachedRes = $global:AdvancedCache[$computerName]
|
||||
$cachedKeys = if ($cachedRes -is [hashtable]) { $cachedRes.Keys } else { $cachedRes.psobject.properties.Name }
|
||||
foreach ($key in @($cachedKeys)) { $res[$key] = $cachedRes.$key }
|
||||
# A device that wasn't in AD or SCCM yet (e.g. a replacement not imaged yet) is looked
|
||||
# up again every cycle until it shows up, instead of waiting for the nightly refresh
|
||||
if ($res.InAD -eq $false -or $res.InSCCM -eq $false -or -not $res.Fetched) { $needsUpdate = $true }
|
||||
}
|
||||
|
||||
# Live IP from DNS for devices that answered the ping (inventory IP is the fallback)
|
||||
@@ -125,6 +304,13 @@ function Get-AdvancedData {
|
||||
if ($inventory[$key]) { $res[$key] = $inventory[$key] }
|
||||
}
|
||||
if ($null -eq $dnsIp -and $inventory.InventoryIP) { $res.IP = $inventory.InventoryIP }
|
||||
$res.InAD = $inventory.InAD
|
||||
$res.InSCCM = $inventory.InSCCM
|
||||
$res.Fetched = Get-IsoNow
|
||||
if ($v -and ($res.InAD -eq $false -or $res.InSCCM -eq $false)) {
|
||||
$missing = @(); if ($res.InAD -eq $false) { $missing += "AD" }; if ($res.InSCCM -eq $false) { $missing += "SCCM" }
|
||||
Write-Host " -> $computerName not found in: $($missing -join ', ')" -ForegroundColor DarkYellow
|
||||
}
|
||||
|
||||
$global:AdvancedCache[$computerName] = $res
|
||||
$global:AdvancedCacheUpdated = $true
|
||||
@@ -180,7 +366,7 @@ while ($true) {
|
||||
$cycleStart = Get-Date
|
||||
Write-Host "`n--- Cycle Start: $($cycleStart.ToString('hh:mm:ss tt')) ---" -ForegroundColor White
|
||||
|
||||
Update-WorkerState "Preparing..." 0 0 "N/A" "Calculating..."
|
||||
Update-WorkerState "Preparing..." 0 0 $globalLastDuration "Calculating..."
|
||||
|
||||
$session = Open-ProviderCycle
|
||||
|
||||
@@ -242,10 +428,38 @@ while ($true) {
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
$uniqueDevices = $deviceTags.Keys | Sort-Object
|
||||
$totalDevices = @($uniqueDevices).Count
|
||||
$uniqueDevices = @($deviceTags.Keys | Sort-Object)
|
||||
|
||||
Update-WorkerState "Software Check..." 0 $totalDevices "N/A" "Calculating..."
|
||||
# Totals count SOURCE devices only: a manual pair's destination is part of its source's row
|
||||
$manualDestSet = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($d in $manualPairs.Values) { if (-not $manualPairs.ContainsKey($d)) { $null = $manualDestSet.Add($d) } }
|
||||
|
||||
# Last cycle's rows: devices with no data yet (newly tracked) are swept first, and the
|
||||
# dashboard gets them as soon as they're done instead of at the end of the cycle
|
||||
$previousRows = @{}
|
||||
if (Test-Path $jsonPath) {
|
||||
try {
|
||||
$prev = [System.IO.File]::ReadAllText($jsonPath, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
|
||||
foreach ($r in @($prev.Devices)) { if ($r."Source Device") { $previousRows[$r."Source Device".ToUpper()] = $r } }
|
||||
} catch {}
|
||||
}
|
||||
$newSources = [System.Collections.Generic.List[string]]::new()
|
||||
$knownSources = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($dev in $uniqueDevices) {
|
||||
if ($manualDestSet.Contains($dev)) { continue }
|
||||
$prevRow = $previousRows[$dev]
|
||||
$isNew = (-not $prevRow) -or (-not $global:AdvancedCache.ContainsKey($dev)) -or ($manualPairs.ContainsKey($dev) -and "$($prevRow."Destination Device")" -ne $manualPairs[$dev])
|
||||
if ($isNew) { $newSources.Add($dev) } else { $knownSources.Add($dev) }
|
||||
}
|
||||
$sourceDevices = @($newSources) + @($knownSources)
|
||||
$totalDevices = $sourceDevices.Count
|
||||
$global:PingedThisCycle.Clear()
|
||||
|
||||
$timing = [ordered]@{}
|
||||
foreach ($k in @("SCCM", "Cached", "NoAssoc", "Manual", "Skipped")) { $timing[$k] = @{ Count = 0; Ms = 0 } }
|
||||
$phaseWatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
|
||||
Update-WorkerState "Software Check..." 0 $totalDevices $globalLastDuration "Calculating..."
|
||||
Write-Host "`n[Checking Software Cache Engine...]" -ForegroundColor Cyan
|
||||
$runFullSoftwareSweep = $false
|
||||
$currentDate = (Get-Date).ToString("yyyy-MM-dd")
|
||||
@@ -265,7 +479,9 @@ while ($true) {
|
||||
$cachedNames = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
if ($cacheObj) { foreach ($p in $cacheObj.psobject.properties) { $null = $cachedNames.Add($p.Name) } }
|
||||
|
||||
foreach ($dev in $uniqueDevices) {
|
||||
# New devices first here too, so their software is ready when their row is
|
||||
$softwareOrder = @($newSources) + @($uniqueDevices | Where-Object { -not $newSources.Contains($_) })
|
||||
foreach ($dev in $softwareOrder) {
|
||||
if ($runFullSoftwareSweep -or -not $cachedNames.Contains($dev)) {
|
||||
if ($v) { Write-Host "Fetching background software for $dev..." -ForegroundColor DarkCyan }
|
||||
Update-SoftwareCacheEntry $dev
|
||||
@@ -273,15 +489,21 @@ while ($true) {
|
||||
}
|
||||
}
|
||||
Write-Host "[Software Cache Synchronized]`n" -ForegroundColor Green
|
||||
$softwareSeconds = $phaseWatch.Elapsed.TotalSeconds
|
||||
$phaseWatch.Restart()
|
||||
|
||||
$deviceTable = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
$processedSources = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
$currentDeviceCount = 0
|
||||
|
||||
Write-Host "--- Commencing Data Sweep ($totalDevices Devices) ---" -ForegroundColor White
|
||||
if ($newSources.Count -gt 0) { Write-Host "--- Commencing Data Sweep ($totalDevices Devices, $($newSources.Count) new first) ---" -ForegroundColor White }
|
||||
else { Write-Host "--- Commencing Data Sweep ($totalDevices Devices) ---" -ForegroundColor White }
|
||||
|
||||
foreach ($deviceName in $uniqueDevices) {
|
||||
foreach ($deviceName in $sourceDevices) {
|
||||
$currentDeviceCount++
|
||||
Update-WorkerState "Updating Devices..." $currentDeviceCount $totalDevices "N/A" "Calculating..."
|
||||
$deviceWatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$category = "SCCM"
|
||||
Update-WorkerState "Updating Devices..." $currentDeviceCount $totalDevices $globalLastDuration "Calculating..."
|
||||
Write-Host "[$currentDeviceCount/$totalDevices] Processing: $deviceName" -ForegroundColor Yellow
|
||||
|
||||
$tags = $deviceTags[$deviceName] -join ", "
|
||||
@@ -291,57 +513,39 @@ while ($true) {
|
||||
$deviceTagList = @($deviceTags[$deviceName])
|
||||
$allTagsSkipped = ($deviceTagList.Count -gt 0) -and @($deviceTagList | Where-Object { $skipUSMTTags.Contains($_) }).Count -gt 0
|
||||
if ($allTagsSkipped) {
|
||||
$category = "Skipped"
|
||||
if ($v) { Write-Host " -> [SKIP USMT] Tag marked Skip USMT. Caching Advanced/Software only." -ForegroundColor DarkMagenta }
|
||||
$null = Get-AdvancedData $deviceName -OnlineStatus "Skipped" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$deviceTable.Add([PSCustomObject]@{
|
||||
"Source Device"=$deviceName; "Destination Device"="N/A";
|
||||
"Tags"=$tags; "Source Total Space"="N/A"; "Source Free Space"="N/A";
|
||||
"USMT Status"="Skipped"; "Backup Status"="Skipped";
|
||||
"Source Online"="Skipped"; "Destination Online"="-";
|
||||
"Source Scanstate Line"="N/A"; "Destination Loadstate Line"="N/A";
|
||||
"Source Scanstate File"="N/A"; "Destination Loadstate File"="N/A"
|
||||
"Source Scanstate Elapsed"="N/A"; "Destination Loadstate Elapsed"="N/A";
|
||||
})
|
||||
continue
|
||||
$row = New-DeviceRow $deviceName "N/A" $tags "Skipped"
|
||||
$row."USMT Status" = "Skipped"; $row."Backup Status" = "Skipped"
|
||||
$row."Source Online" = "Skipped"; $row."Destination Online" = "-"
|
||||
$deviceTable.Add([PSCustomObject]$row)
|
||||
}
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
if ($manualPairs.ContainsKey($deviceName)) {
|
||||
elseif ($manualPairs.ContainsKey($deviceName)) {
|
||||
$category = "Manual"
|
||||
$dName = $manualPairs[$deviceName]
|
||||
$sName = $deviceName
|
||||
if ($v) { Write-Host " -> [MANUAL MATCH] Hardware exception detected. Bypassing SCCM..." -ForegroundColor DarkYellow }
|
||||
|
||||
$pingSrc = pingDevice $sName
|
||||
$pingDst = pingDevice $dName
|
||||
$row = New-DeviceRow $sName $dName $tags "Manual"
|
||||
$row."Source Online" = Test-Device $sName -Session $session
|
||||
$row."Destination Online" = Test-Device $dName -Session $session
|
||||
$row."Source Checked" = Get-IsoNow; $row."Destination Checked" = $row."Source Checked"
|
||||
|
||||
$null = Get-AdvancedData $sName -OnlineStatus $pingSrc -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $dName -IsDest -OnlineStatus $pingDst -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
|
||||
$deviceTable.Add([PSCustomObject]@{
|
||||
"Source Device"=$sName; "Destination Device"=$dName;
|
||||
"Tags"=$tags; "Source Total Space"="N/A"; "Source Free Space"="N/A";
|
||||
"USMT Status"="N/A"; "Backup Status"="N/A";
|
||||
"Source Online"=$pingSrc; "Destination Online"=$pingDst;
|
||||
"Source Scanstate Line"="N/A"; "Destination Loadstate Line"="N/A";
|
||||
"Source Scanstate File"="N/A"; "Destination Loadstate File"="N/A"
|
||||
"Source Scanstate Elapsed"="N/A"; "Destination Loadstate Elapsed"="N/A";
|
||||
})
|
||||
continue # Move on to the next device
|
||||
$null = Get-AdvancedData $sName -OnlineStatus $row."Source Online" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $dName -IsDest -OnlineStatus $row."Destination Online" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$deviceTable.Add([PSCustomObject]$row)
|
||||
}
|
||||
else {
|
||||
if ($v) { Write-Host " -> Querying SCCM for associations..." -ForegroundColor DarkGray }
|
||||
$associations = Get-DeviceAssociations -SourceComputer $deviceName
|
||||
|
||||
# Prevent the destination machine from being swept a second time as a standalone source
|
||||
if ($manualPairs.Values -contains $deviceName -and -not $manualPairs.ContainsKey($deviceName)) {
|
||||
if ($v) { Write-Host " -> [MANUAL DEST] Device handled by pair logic. Skipping independent sweep." -ForegroundColor DarkGray }
|
||||
continue
|
||||
}
|
||||
|
||||
if ($v) { Write-Host " -> Querying SCCM for associations..." -ForegroundColor DarkGray }
|
||||
$associations = Get-DeviceAssociations -SourceComputer $deviceName
|
||||
|
||||
if (-not $associations) {
|
||||
$cached = $completedList | Where-Object { $_."Source Device".ToUpper() -eq $deviceName } | Select-Object -First 1
|
||||
if ($cached) {
|
||||
if ($v) { Write-Host " -> [CACHE HIT] But association was removed from SCCM." -ForegroundColor DarkYellow }
|
||||
if (-not $associations) {
|
||||
$category = "NoAssoc"
|
||||
$cached = $completedList | Where-Object { $_."Source Device".ToUpper() -eq $deviceName } | Select-Object -First 1
|
||||
if ($cached) {
|
||||
$category = "Cached"
|
||||
if ($v) { Write-Host " -> [CACHE HIT] But association was removed from SCCM." -ForegroundColor DarkYellow }
|
||||
|
||||
# Backfill dest software - association is gone so the assoc loop will never fetch it
|
||||
$cachedDest = "$($cached."Destination Device")"
|
||||
@@ -353,179 +557,223 @@ while ($true) {
|
||||
}
|
||||
}
|
||||
|
||||
$null = Get-AdvancedData $cached."Source Device" -OnlineStatus "Cached" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $cached."Destination Device" -IsDest -OnlineStatus "Cached" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $cached."Source Device" -OnlineStatus "Cached" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $cached."Destination Device" -IsDest -OnlineStatus "Cached" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
|
||||
$deviceTable.Add([PSCustomObject]@{
|
||||
"Source Device"=$cached."Source Device"; "Destination Device"=$cached."Destination Device";
|
||||
"Tags"=$tags; "Source Total Space"="Cached"; "Source Free Space"="Cached";
|
||||
"USMT Status"="Not Associated"; "Backup Status"="Completed";
|
||||
"Source Online"="Cached"; "Destination Online"="Cached";
|
||||
"Source Scanstate Line"="Cached"; "Destination Loadstate Line"="Cached";
|
||||
"Source Scanstate File"="Cached"; "Destination Loadstate File"="Cached"
|
||||
"Source Scanstate Elapsed"="Cached"; "Destination Loadstate Elapsed"="Cached";
|
||||
})
|
||||
continue
|
||||
}
|
||||
$row = New-DeviceRow $cached."Source Device" $cached."Destination Device" $tags "Cached"
|
||||
foreach ($k in @("Source Total Space", "Source Free Space", "Source Online", "Destination Online", "Source Scanstate Line", "Destination Loadstate Line", "Source Scanstate File", "Destination Loadstate File", "Source Scanstate Elapsed", "Destination Loadstate Elapsed")) { $row[$k] = "Cached" }
|
||||
$row."USMT Status" = "Not Associated"; $row."Backup Status" = "Completed"
|
||||
Use-Snapshot $row."Source Device" $row."Destination Device" $row $true $true
|
||||
Set-RowPresence $row -Source -Destination
|
||||
$deviceTable.Add([PSCustomObject]$row)
|
||||
}
|
||||
else {
|
||||
if ($v) { Write-Host " -> No SCCM association found. Sweeping source device anyway..." -ForegroundColor DarkGray }
|
||||
|
||||
if ($v) { Write-Host " -> No SCCM association found. Sweeping source device anyway..." -ForegroundColor DarkGray }
|
||||
$row = New-DeviceRow $deviceName "N/A" $tags "None"
|
||||
$pingSrc = Test-Device $deviceName -Session $session
|
||||
$row."Source Online" = $pingSrc; $row."Destination Online" = "-"
|
||||
$row."Source Checked" = Get-IsoNow
|
||||
$row."USMT Status" = "Not Associated"; $row."Backup Status" = "Not Associated"
|
||||
$logSrc = @{ IsSuccess = $false; Found = $false; Line = "Unreachable ($pingSrc)"; Path = "Unreachable"; LastModified = $null; Elapsed = "N/A" }
|
||||
|
||||
$pingSrc = pingDevice $deviceName
|
||||
$logSrc = @{ IsSuccess = $false; Found = $false; Line = "Unreachable ($pingSrc)"; Path = "Unreachable"; LastModified = $null }
|
||||
$srcTotal = "N/A"
|
||||
$srcFree = "N/A"
|
||||
if ($pingSrc -eq "Online") {
|
||||
# --- DISK SKIP LIST CHECK (No-Association path) ---
|
||||
if ($diskSkipList -notcontains $deviceName) {
|
||||
$diskInfo = Get-DiskSpace $deviceName
|
||||
$row."Source Total Space" = $diskInfo.Total
|
||||
$row."Source Free Space" = $diskInfo.Free
|
||||
if ($diskInfo.Free -ne "N/A") { $row."Source Space Checked" = Get-IsoNow }
|
||||
} else {
|
||||
if ($v) { Write-Host " > Disk check skipped for $deviceName (on diskSkipList)" -ForegroundColor DarkYellow }
|
||||
}
|
||||
if ($diskSkipList -notcontains $deviceName -and $row."Source Free Space" -ne "N/A") { $logSrc = Get-LogInfo $deviceName $scanLogName }
|
||||
}
|
||||
$row."Source Scanstate Line" = $logSrc.Line
|
||||
$row."Source Scanstate File" = $logSrc.Path
|
||||
$row."Source Scanstate Elapsed" = $logSrc.Elapsed
|
||||
|
||||
if ($pingSrc -eq "Online") {
|
||||
# --- DISK SKIP LIST CHECK (No-Association path) ---
|
||||
if ($diskSkipList -notcontains $deviceName) {
|
||||
$diskInfo = Get-DiskSpace $deviceName
|
||||
$srcTotal = $diskInfo.Total
|
||||
$srcFree = $diskInfo.Free
|
||||
} else {
|
||||
if ($v) { Write-Host " > Disk check skipped for $deviceName (on diskSkipList)" -ForegroundColor DarkYellow }
|
||||
$srcLive = ($pingSrc -eq "Online" -and $row."Source Free Space" -ne "N/A")
|
||||
if ($srcLive) { Update-Snapshot $deviceName "N/A" $row $true $false }
|
||||
else { Use-Snapshot $deviceName "N/A" $row $true $false }
|
||||
|
||||
$null = Get-AdvancedData $deviceName -OnlineStatus $pingSrc -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$deviceTable.Add([PSCustomObject]$row)
|
||||
}
|
||||
if ($diskSkipList -notcontains $deviceName -and $srcFree -ne "N/A") { $logSrc = Get-LogInfo $deviceName $scanLogName }
|
||||
}
|
||||
else {
|
||||
foreach ($assoc in $associations) {
|
||||
$assoc.Get()
|
||||
$dName = if ($assoc.DestinationComputer) { $assoc.DestinationComputer } else { $assoc.RestoreName }
|
||||
$sName = if ($assoc.SourceComputer) { $assoc.SourceComputer } else { $deviceName }
|
||||
|
||||
$null = Get-AdvancedData $deviceName -OnlineStatus $pingSrc -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
if (-not [string]::IsNullOrWhiteSpace($dName) -and $dName -ne "N/A") {
|
||||
if ($runFullSoftwareSweep -or -not $cachedNames.Contains($dName)) {
|
||||
if ($v) { Write-Host " -> [CACHE MISS] Fetching software for Dest Device $dName..." -ForegroundColor DarkCyan }
|
||||
Update-SoftwareCacheEntry $dName
|
||||
$null = $cachedNames.Add($dName)
|
||||
}
|
||||
}
|
||||
|
||||
$deviceTable.Add([PSCustomObject]@{
|
||||
"Source Device"=$deviceName; "Destination Device"="N/A";
|
||||
"Tags"=$tags; "Source Total Space"=$srcTotal; "Source Free Space"=$srcFree;
|
||||
"USMT Status"="Not Associated"; "Backup Status"="Not Associated";
|
||||
"Source Online"=$pingSrc; "Destination Online"="-";
|
||||
"Source Scanstate Line"=$logSrc.Line; "Destination Loadstate Line"="N/A";
|
||||
"Source Scanstate File"=$logSrc.Path; "Destination Loadstate File"="N/A"
|
||||
"Source Scanstate Elapsed"=$logSrc.Elapsed; "Destination Loadstate Elapsed"="N/A";
|
||||
})
|
||||
continue
|
||||
$usmtStatusRaw = $assoc.MigrationStatus
|
||||
$usmtStatusStr = switch ($usmtStatusRaw) { 0 {"Not Started"} 1 {"In Progress"} 2 {"Completed"} default {"Unknown"} }
|
||||
|
||||
if ($v) { Write-Host " -> SCCM DB Status: $usmtStatusStr (Raw: $usmtStatusRaw)" -ForegroundColor DarkGray }
|
||||
|
||||
$cached = $completedList | Where-Object { $_."Source Device".ToUpper() -eq $sName.ToUpper() -and $_."Destination Device".ToUpper() -eq $dName.ToUpper() }
|
||||
|
||||
if ($cached -and $usmtStatusRaw -eq 0) {
|
||||
if ($v) { Write-Host " -> [CACHE RESET] SCCM Status reverted to Not Started. Resetting..." -ForegroundColor DarkYellow }
|
||||
$completedList = @($completedList | Where-Object { $_."Source Device".ToUpper() -ne $sName.ToUpper() -or $_."Destination Device".ToUpper() -ne $dName.ToUpper() })
|
||||
$jsonCompleted = ConvertTo-Json -InputObject @($completedList) -Depth 5
|
||||
[System.IO.File]::WriteAllText($completedPath, $jsonCompleted, [System.Text.Encoding]::UTF8)
|
||||
$cached = $null
|
||||
}
|
||||
|
||||
if ($cached) {
|
||||
$category = "Cached"
|
||||
if ($v) { Write-Host " -> [CACHE HIT] Fast-tracking row and bypassing network sweeps." -ForegroundColor Green }
|
||||
$null = Get-AdvancedData $sName -OnlineStatus "Cached" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $dName -IsDest -OnlineStatus "Cached" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
|
||||
$row = New-DeviceRow $sName $dName $tags "Cached"
|
||||
foreach ($k in @("Source Total Space", "Source Free Space", "Source Online", "Destination Online", "Source Scanstate Line", "Destination Loadstate Line", "Source Scanstate File", "Destination Loadstate File", "Source Scanstate Elapsed", "Destination Loadstate Elapsed")) { $row[$k] = "Cached" }
|
||||
$row."USMT Status" = $usmtStatusStr; $row."Backup Status" = "Completed"
|
||||
# Last state seen before it completed (with its timestamp) instead of "Cached"
|
||||
Use-Snapshot $sName $dName $row $true $true
|
||||
Set-RowPresence $row -Source -Destination
|
||||
if ($v -and $row."Source Snapshot") { Write-Host " > Showing snapshot from $(([datetime]$row."Source Snapshot").ToString('MM/dd hh:mm tt'))" -ForegroundColor DarkGray }
|
||||
$deviceTable.Add([PSCustomObject]$row)
|
||||
continue
|
||||
}
|
||||
|
||||
$row = New-DeviceRow $sName $dName $tags "SCCM"
|
||||
$row."USMT Status" = $usmtStatusStr; $row."Backup Status" = "Checking..."
|
||||
|
||||
if ($v) { Write-Host " -> [SWEEP SOURCE] $sName" -ForegroundColor DarkCyan }
|
||||
$pingSrc = Test-Device $sName -Session $session
|
||||
$row."Source Online" = $pingSrc
|
||||
$row."Source Checked" = Get-IsoNow
|
||||
$logSrc = @{ IsSuccess = $false; Found = $false; Line = "Unreachable ($pingSrc)"; Path = "Unreachable"; LastModified = $null; Elapsed = "N/A" }
|
||||
|
||||
if ($pingSrc -eq "Online") {
|
||||
# --- DISK SKIP LIST CHECK (Association path) ---
|
||||
if ($diskSkipList -notcontains $sName) {
|
||||
$diskInfo = Get-DiskSpace $sName
|
||||
$row."Source Total Space" = $diskInfo.Total
|
||||
$row."Source Free Space" = $diskInfo.Free
|
||||
if ($diskInfo.Free -ne "N/A") { $row."Source Space Checked" = Get-IsoNow }
|
||||
} else {
|
||||
if ($v) { Write-Host " > Disk check skipped for $sName (on diskSkipList)" -ForegroundColor DarkYellow }
|
||||
}
|
||||
if ($diskSkipList -notcontains $sName -and $row."Source Free Space" -ne "N/A") { $logSrc = Get-LogInfo $sName $scanLogName }
|
||||
}
|
||||
$row."Source Scanstate Line" = $logSrc.Line
|
||||
$row."Source Scanstate File" = $logSrc.Path
|
||||
$row."Source Scanstate Elapsed" = $logSrc.Elapsed
|
||||
|
||||
if ($v) { Write-Host " -> [SWEEP DESTINATION] $dName" -ForegroundColor DarkCyan }
|
||||
$pingDst = Test-Device $dName -Session $session
|
||||
$row."Destination Online" = $pingDst
|
||||
$row."Destination Checked" = Get-IsoNow
|
||||
$logDst = @{ IsSuccess = $false; Found = $false; Line = "Unreachable ($pingDst)"; Path = "Unreachable"; LastModified = $null; Elapsed = "N/A" }
|
||||
|
||||
if ($pingDst -eq "Online") { $logDst = Get-LogInfo $dName $loadLogName }
|
||||
$row."Destination Loadstate Line" = $logDst.Line
|
||||
$row."Destination Loadstate File" = $logDst.Path
|
||||
$row."Destination Loadstate Elapsed" = $logDst.Elapsed
|
||||
|
||||
if ($v) { Write-Host " -> Calculating Master Status..." -ForegroundColor DarkGray }
|
||||
$timeThreshold = (Get-Date).AddHours(-1 * $config.Worker.ActiveLogHours)
|
||||
|
||||
$isSrcLogActive = ($logSrc.Found -and $logSrc.Path -match "SMSTSLog" -and $logSrc.LastModified -gt $timeThreshold)
|
||||
$isDstLogActive = ($logDst.Found -and $logDst.Path -match "SMSTSLog" -and $logDst.LastModified -gt $timeThreshold)
|
||||
|
||||
if ($logDst.IsSuccess -or $usmtStatusRaw -eq 2) { $row."Backup Status" = "Completed" }
|
||||
elseif ($usmtStatusRaw -eq 1 -and $isDstLogActive) { $row."Backup Status" = "Restore In Progress" }
|
||||
elseif ($usmtStatusRaw -eq 1) { $row."Backup Status" = "Backup Completed" }
|
||||
elseif ($usmtStatusRaw -eq 0 -and $isSrcLogActive) { $row."Backup Status" = "Backup In Progress" }
|
||||
else { $row."Backup Status" = "Not Started" }
|
||||
|
||||
# Remember what we could read; fill what we couldn't from the last snapshot
|
||||
$srcLive = ($pingSrc -eq "Online" -and $row."Source Free Space" -ne "N/A")
|
||||
$dstLive = ($pingDst -eq "Online" -and $logDst.Found)
|
||||
Update-Snapshot $sName $dName $row $srcLive $dstLive
|
||||
Use-Snapshot $sName $dName $row (-not $srcLive) (-not $dstLive -and $pingDst -ne "Online")
|
||||
|
||||
if ($v) { Write-Host " -> Fetching Advanced SQL/AD Data..." -ForegroundColor DarkGray }
|
||||
$null = Get-AdvancedData $sName -OnlineStatus $pingSrc -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $dName -IsDest -OnlineStatus $pingDst -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
|
||||
$finalRow = [PSCustomObject]$row
|
||||
Write-Host " - Status: $($finalRow."Backup Status")" -ForegroundColor Cyan
|
||||
$deviceTable.Add($finalRow)
|
||||
|
||||
if ($finalRow."Backup Status" -eq "Completed") {
|
||||
$completedList += [PSCustomObject]@{ "Source Device"=$sName; "Destination Device"=$dName }
|
||||
$jsonCompleted = ConvertTo-Json -InputObject @($completedList) -Depth 5
|
||||
[System.IO.File]::WriteAllText($completedPath, $jsonCompleted, [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($assoc in $associations) {
|
||||
$assoc.Get()
|
||||
$dName = if ($assoc.DestinationComputer) { $assoc.DestinationComputer } else { $assoc.RestoreName }
|
||||
$sName = if ($assoc.SourceComputer) { $assoc.SourceComputer } else { $deviceName }
|
||||
$null = $processedSources.Add($deviceName)
|
||||
$timing[$category].Count++
|
||||
$timing[$category].Ms += $deviceWatch.Elapsed.TotalMilliseconds
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($dName) -and $dName -ne "N/A") {
|
||||
if ($runFullSoftwareSweep -or -not $cachedNames.Contains($dName)) {
|
||||
if ($v) { Write-Host " -> [CACHE MISS] Fetching software for Dest Device $dName..." -ForegroundColor DarkCyan }
|
||||
Update-SoftwareCacheEntry $dName
|
||||
$null = $cachedNames.Add($dName)
|
||||
}
|
||||
}
|
||||
|
||||
$usmtStatusRaw = $assoc.MigrationStatus
|
||||
$usmtStatusStr = switch ($usmtStatusRaw) { 0 {"Not Started"} 1 {"In Progress"} 2 {"Completed"} default {"Unknown"} }
|
||||
|
||||
if ($v) { Write-Host " -> SCCM DB Status: $usmtStatusStr (Raw: $usmtStatusRaw)" -ForegroundColor DarkGray }
|
||||
|
||||
$cached = $completedList | Where-Object { $_."Source Device".ToUpper() -eq $sName.ToUpper() -and $_."Destination Device".ToUpper() -eq $dName.ToUpper() }
|
||||
|
||||
if ($cached -and $usmtStatusRaw -eq 0) {
|
||||
if ($v) { Write-Host " -> [CACHE RESET] SCCM Status reverted to Not Started. Resetting..." -ForegroundColor DarkYellow }
|
||||
$completedList = @($completedList | Where-Object { $_."Source Device".ToUpper() -ne $sName.ToUpper() -or $_."Destination Device".ToUpper() -ne $dName.ToUpper() })
|
||||
$jsonCompleted = ConvertTo-Json -InputObject @($completedList) -Depth 5
|
||||
[System.IO.File]::WriteAllText($completedPath, $jsonCompleted, [System.Text.Encoding]::UTF8)
|
||||
$cached = $null
|
||||
}
|
||||
|
||||
if ($cached) {
|
||||
if ($v) { Write-Host " -> [CACHE HIT] Fast-tracking row and bypassing network sweeps." -ForegroundColor Green }
|
||||
$null = Get-AdvancedData $sName -OnlineStatus "Cached" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $dName -IsDest -OnlineStatus "Cached" -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
|
||||
$deviceTable.Add([PSCustomObject]@{
|
||||
"Source Device"=$sName; "Destination Device"=$dName;
|
||||
"Tags"=$tags; "Source Total Space"="Cached"; "Source Free Space"="Cached";
|
||||
"USMT Status"=$usmtStatusStr; "Backup Status"="Completed";
|
||||
"Source Online"="Cached"; "Destination Online"="Cached";
|
||||
"Source Scanstate Line"="Cached"; "Destination Loadstate Line"="Cached";
|
||||
"Source Scanstate File"="Cached"; "Destination Loadstate File"="Cached"
|
||||
"Source Scanstate Elapsed"="Cached"; "Destination Loadstate Elapsed"="Cached";
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
$row = [ordered]@{
|
||||
"Source Device"=$sName; "Destination Device"=$dName; "Tags"=$tags;
|
||||
"Source Total Space"="N/A"; "Source Free Space"="N/A";
|
||||
"USMT Status"=$usmtStatusStr; "Backup Status"="Checking...";
|
||||
"Source Online"="N/A"; "Destination Online"="N/A";
|
||||
"Source Scanstate Line"="N/A"; "Destination Loadstate Line"="N/A";
|
||||
"Source Scanstate File"="N/A"; "Destination Loadstate File"="N/A"
|
||||
"Source Scanstate Elapsed"="N/A"; "Destination Loadstate Elapsed"="N/A";
|
||||
}
|
||||
|
||||
if ($v) { Write-Host " -> [SWEEP SOURCE] $sName" -ForegroundColor DarkCyan }
|
||||
$pingSrc = pingDevice $sName
|
||||
$row."Source Online" = $pingSrc
|
||||
$logSrc = @{ IsSuccess = $false; Found = $false; Line = "Unreachable ($pingSrc)"; Path = "Unreachable"; LastModified = $null }
|
||||
|
||||
if ($pingSrc -eq "Online") {
|
||||
# --- DISK SKIP LIST CHECK (Association path) ---
|
||||
if ($diskSkipList -notcontains $sName) {
|
||||
$diskInfo = Get-DiskSpace $sName
|
||||
$row."Source Total Space" = $diskInfo.Total
|
||||
$row."Source Free Space" = $diskInfo.Free
|
||||
} else {
|
||||
if ($v) { Write-Host " > Disk check skipped for $sName (on diskSkipList)" -ForegroundColor DarkYellow }
|
||||
}
|
||||
if ($diskSkipList -notcontains $sName -and $row."Source Free Space" -ne "N/A") { $logSrc = Get-LogInfo $sName $scanLogName }
|
||||
}
|
||||
$row."Source Scanstate Line" = $logSrc.Line
|
||||
$row."Source Scanstate File" = $logSrc.Path
|
||||
$row."Source Scanstate Elapsed" = $logSrc.Elapsed
|
||||
|
||||
if ($v) { Write-Host " -> [SWEEP DESTINATION] $dName" -ForegroundColor DarkCyan }
|
||||
$pingDst = pingDevice $dName
|
||||
$row."Destination Online" = $pingDst
|
||||
$logDst = @{ IsSuccess = $false; Found = $false; Line = "Unreachable ($pingDst)"; Path = "Unreachable"; LastModified = $null }
|
||||
|
||||
if ($pingDst -eq "Online") { $logDst = Get-LogInfo $dName $loadLogName }
|
||||
$row."Destination Loadstate Line" = $logDst.Line
|
||||
$row."Destination Loadstate File" = $logDst.Path
|
||||
$row."Destination Loadstate Elapsed" = $logDst.Elapsed
|
||||
|
||||
if ($v) { Write-Host " -> Calculating Master Status..." -ForegroundColor DarkGray }
|
||||
$timeThreshold = (Get-Date).AddHours(-1 * $config.Worker.ActiveLogHours)
|
||||
|
||||
$isSrcLogActive = ($logSrc.Found -and $logSrc.Path -match "SMSTSLog" -and $logSrc.LastModified -gt $timeThreshold)
|
||||
$isDstLogActive = ($logDst.Found -and $logDst.Path -match "SMSTSLog" -and $logDst.LastModified -gt $timeThreshold)
|
||||
|
||||
if ($logDst.IsSuccess -or $usmtStatusRaw -eq 2) { $row."Backup Status" = "Completed" }
|
||||
elseif ($usmtStatusRaw -eq 1 -and $isDstLogActive) { $row."Backup Status" = "Restore In Progress" }
|
||||
elseif ($usmtStatusRaw -eq 1) { $row."Backup Status" = "Backup Completed" }
|
||||
elseif ($usmtStatusRaw -eq 0 -and $isSrcLogActive) { $row."Backup Status" = "Backup In Progress" }
|
||||
else { $row."Backup Status" = "Not Started" }
|
||||
|
||||
if ($v) { Write-Host " -> Fetching Advanced SQL/AD Data..." -ForegroundColor DarkGray }
|
||||
$null = Get-AdvancedData $sName -OnlineStatus $pingSrc -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
$null = Get-AdvancedData $dName -IsDest -OnlineStatus $pingDst -ForceUpdate:$runFullSoftwareSweep -Session $session
|
||||
|
||||
$finalRow = [PSCustomObject]$row
|
||||
Write-Host " - Status: $($finalRow."Backup Status")" -ForegroundColor Cyan
|
||||
$deviceTable.Add($finalRow)
|
||||
|
||||
if ($finalRow."Backup Status" -eq "Completed") {
|
||||
$completedList += [PSCustomObject]@{ "Source Device"=$sName; "Destination Device"=$dName }
|
||||
$jsonCompleted = ConvertTo-Json -InputObject @($completedList) -Depth 5
|
||||
[System.IO.File]::WriteAllText($completedPath, $jsonCompleted, [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
# Newly tracked devices are done: publish them now rather than at the end of the cycle
|
||||
if ($newSources.Count -gt 0 -and $knownSources.Count -gt 0 -and $currentDeviceCount -eq $newSources.Count) {
|
||||
Write-Host " -> [PRIORITY] $($newSources.Count) new device(s) swept. Publishing early..." -ForegroundColor Green
|
||||
$interim = [System.Collections.Generic.List[object]]::new()
|
||||
foreach ($r in $deviceTable) { $interim.Add($r) }
|
||||
foreach ($dev in $knownSources) { if ($previousRows.ContainsKey($dev)) { $interim.Add($previousRows[$dev]) } }
|
||||
Save-AdvancedCache
|
||||
Write-JsonFile $jsonPath ([PSCustomObject]@{ LastUpdated = (Get-Date).ToString("MM/dd/yy hh:mm tt"); Devices = $interim })
|
||||
}
|
||||
}
|
||||
$sweepSeconds = $phaseWatch.Elapsed.TotalSeconds
|
||||
$phaseWatch.Restart()
|
||||
|
||||
# --- PRESENCE SWEEP: ping everything the sweep didn't (cached pairs, skipped tags...) ---
|
||||
$presenceCount = 0
|
||||
$presenceDue = ((Get-Date) - $global:LastPresenceSweep).TotalMinutes -ge ($presenceMinutes - 0.5) -or $runFullSoftwareSweep
|
||||
if ($presenceDue) {
|
||||
$everyone = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($r in $deviceTable) {
|
||||
foreach ($n in @($r."Source Device", $r."Destination Device")) { if ($n -and $n -ne "N/A") { $null = $everyone.Add($n) } }
|
||||
}
|
||||
$toPing = @($everyone | Where-Object { -not $global:PingedThisCycle.Contains($_) } | Sort-Object)
|
||||
Write-Host "`n[Presence Sweep: $($toPing.Count) device(s) not pinged this cycle]" -ForegroundColor Cyan
|
||||
$i = 0
|
||||
foreach ($n in $toPing) {
|
||||
$i++
|
||||
Update-WorkerState "Presence Sweep..." $i $toPing.Count $globalLastDuration "Calculating..."
|
||||
$null = Test-Device $n -Session $session
|
||||
}
|
||||
# Cached / skipped rows pick up the fresh result
|
||||
foreach ($r in $deviceTable) {
|
||||
if ($r.Pairing -eq "Cached") { Set-RowPresence $r -Source -Destination }
|
||||
}
|
||||
$presenceCount = $toPing.Count
|
||||
$global:LastPresenceSweep = Get-Date
|
||||
Write-Host "[Presence Updated]" -ForegroundColor Green
|
||||
}
|
||||
$presenceSeconds = $phaseWatch.Elapsed.TotalSeconds
|
||||
$phaseWatch.Restart()
|
||||
|
||||
Close-ProviderCycle $session
|
||||
|
||||
$activeDevs = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($row in $deviceTable) {
|
||||
if ($row."Source Device" -and $row."Source Device" -ne "N/A") { $null = $activeDevs.Add($row."Source Device") }
|
||||
if ($row."Destination Device" -and $row."Destination Device" -ne "N/A") { $null = $activeDevs.Add($row."Destination Device") }
|
||||
}
|
||||
|
||||
if ($v) { Write-Host " -> Pruning and Compressing Software Cache..." -ForegroundColor DarkGray }
|
||||
if (Test-Path $softwareCachePath) {
|
||||
try {
|
||||
$rawCache = [System.IO.File]::ReadAllText($softwareCachePath, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
|
||||
$activeDevs = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($row in $deviceTable) {
|
||||
if ($row."Source Device" -and $row."Source Device" -ne "N/A") { $null = $activeDevs.Add($row."Source Device") }
|
||||
if ($row."Destination Device" -and $row."Destination Device" -ne "N/A") { $null = $activeDevs.Add($row."Destination Device") }
|
||||
}
|
||||
|
||||
$pruned = $false
|
||||
foreach ($p in @($rawCache.psobject.properties.Name)) {
|
||||
if (-not $activeDevs.Contains($p)) {
|
||||
@@ -537,28 +785,27 @@ while ($true) {
|
||||
$jsonSoftCache = $rawCache | ConvertTo-Json -Depth 6 -Compress
|
||||
[System.IO.File]::WriteAllText($softwareCachePath, $jsonSoftCache, [System.Text.Encoding]::UTF8)
|
||||
if ($pruned -and $v) { Write-Host " -> Removed untracked devices from cache." -ForegroundColor Green }
|
||||
|
||||
if ($global:AdvancedCacheUpdated -or $pruned -or $runFullSoftwareSweep) {
|
||||
if ($v) { Write-Host " -> Pruning and Saving Advanced Info Cache..." -ForegroundColor DarkGray }
|
||||
$keysToRemove = @()
|
||||
foreach ($key in $global:AdvancedCache.Keys) {
|
||||
if (-not $activeDevs.Contains($key)) { $keysToRemove += $key }
|
||||
}
|
||||
foreach ($key in $keysToRemove) { $global:AdvancedCache.Remove($key) }
|
||||
|
||||
$jsonAdvCache = $global:AdvancedCache | ConvertTo-Json -Depth 5 -Compress
|
||||
[System.IO.File]::WriteAllText($advancedCachePath, $jsonAdvCache, [System.Text.Encoding]::UTF8)
|
||||
$global:AdvancedCacheUpdated = $false
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# Presence changes every cycle, so the advanced cache (which carries it) is saved every cycle
|
||||
try {
|
||||
if ($v) { Write-Host " -> Pruning and Saving Advanced Info Cache..." -ForegroundColor DarkGray }
|
||||
foreach ($key in @($global:AdvancedCache.Keys)) { if (-not $activeDevs.Contains($key)) { $global:AdvancedCache.Remove($key) } }
|
||||
foreach ($key in @($global:Presence.Keys)) { if (-not $activeDevs.Contains($key)) { $global:Presence.Remove($key) } }
|
||||
foreach ($key in @($global:Snapshots.Keys)) { if (-not $activeDevs.Contains($key)) { $global:Snapshots.Remove($key) } }
|
||||
Save-AdvancedCache
|
||||
Write-JsonFile $snapshotPath $global:Snapshots 4
|
||||
$global:AdvancedCacheUpdated = $false
|
||||
} catch { Write-Host "Could not save caches: $($_.Exception.GetType().Name)" -ForegroundColor Red }
|
||||
|
||||
if ($v) { Write-Host " -> Writing payload to Data.json..." -ForegroundColor DarkGray }
|
||||
$globalLastUpdated = (Get-Date).ToString("MM/dd/yy hh:mm tt")
|
||||
$exportPayload = [PSCustomObject]@{ LastUpdated = $globalLastUpdated; Devices = $deviceTable }
|
||||
|
||||
$jsonFinal = $exportPayload | ConvertTo-Json -Depth 6
|
||||
[System.IO.File]::WriteAllText($jsonPath, $jsonFinal, [System.Text.Encoding]::UTF8)
|
||||
$saveSeconds = $phaseWatch.Elapsed.TotalSeconds
|
||||
|
||||
$cycleEnd = Get-Date
|
||||
$cycleTime = $cycleEnd - $cycleStart
|
||||
@@ -566,6 +813,26 @@ while ($true) {
|
||||
|
||||
Write-Host "`nCycle Complete in $globalLastDuration." -ForegroundColor Gray
|
||||
|
||||
# How the time was spent, per kind of row (shown in the dashboard's sync dialog)
|
||||
$labels = [ordered]@{ SCCM = "SCCM associations"; Manual = "Manual pairs"; Cached = "Completed (snapshot)"; NoAssoc = "No association"; Skipped = "Skip USMT tags" }
|
||||
$cats = @()
|
||||
foreach ($k in $labels.Keys) {
|
||||
$t = $timing[$k]
|
||||
if ($t.Count -eq 0) { continue }
|
||||
$cats += [PSCustomObject]@{ Key = $k; Label = $labels[$k]; Count = $t.Count; Seconds = [math]::Round($t.Ms / 1000, 1); AvgMs = [int]($t.Ms / $t.Count) }
|
||||
if ($v) { Write-Host (" {0,-22} {1,4} device(s) {2,7:N1}s (avg {3} ms)" -f $labels[$k], $t.Count, ($t.Ms / 1000), [int]($t.Ms / $t.Count)) -ForegroundColor DarkGray }
|
||||
}
|
||||
$global:LastBreakdown = [PSCustomObject]@{
|
||||
Finished = Get-IsoNow
|
||||
Seconds = [math]::Round($cycleTime.TotalSeconds, 1)
|
||||
Sources = $totalDevices
|
||||
NewFirst = $newSources.Count
|
||||
FullRefresh = $runFullSoftwareSweep
|
||||
Categories = $cats
|
||||
Phases = [PSCustomObject]@{ Software = [math]::Round($softwareSeconds, 1); Sweep = [math]::Round($sweepSeconds, 1); Presence = [math]::Round($presenceSeconds, 1); Save = [math]::Round($saveSeconds, 1) }
|
||||
PresenceSweep = [PSCustomObject]@{ Ran = $presenceDue; Pinged = $presenceCount; Last = $(if ($global:LastPresenceSweep -gt [datetime]::MinValue) { $global:LastPresenceSweep.ToString("o") } else { $null }); EveryMinutes = $presenceMinutes }
|
||||
}
|
||||
|
||||
$nextRun = Get-NextRunTime (Get-Date)
|
||||
$nextRunStr = $nextRun.ToString("MM/dd/yy hh:mm tt")
|
||||
|
||||
|
||||
Reference in new issue
Block a user