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
+42
-3
@@ -58,6 +58,7 @@ $script:DemoModels = @{
|
||||
}
|
||||
$script:DemoSsdModels = @("KXG50ZNV256G TOSHIBA", "SAMSUNG MZVLB256HBHQ-000H1", "SK hynix PC611 HFS256GD9TNG", "Micron 2450 NVMe 512GB", "SAMSUNG MZVL2512HCJQ-00BH1", "KBG50ZNS512G KIOXIA")
|
||||
$script:DemoMounts = @("Desktop Bundle", "Monitor Arm", "Under Desk", "Wall Mount", "Desktop Bundle", "Monitor Arm")
|
||||
$script:DemoMonitors = @("HP E24 G5", "HP E27 G5", "HP P24h G5", "DELL P2422H", "DELL P2723DE", "LEN T24i-30", "LEN T27h-30", "LG 27UK850")
|
||||
$script:DemoNotes = @("N/A", "N/A", "N/A", "N/A", "Front counter", "Dual monitors", "Desk 3", "Shared with night shift", "Badge printer attached", "Label printer attached")
|
||||
|
||||
# Specialty apps: one or more ARP entries each (multi-entry apps exercise the "suite" rules)
|
||||
@@ -202,6 +203,11 @@ function Get-DemoProfile([string]$name) {
|
||||
$p.SrcFree = if ((Get-DemoRand $s 'lowdisk') -lt 0.08) { 4.0 + 12 * (Get-DemoRand $s 'freeL') } else { 30.0 + 150 * (Get-DemoRand $s 'free') }
|
||||
$p.Apps = @($p.Department.Apps | Where-Object { (Get-DemoRand $s "app$_") -lt 0.65 })
|
||||
if ($p.Apps.Count -eq 0) { $p.Apps = @($p.Department.Apps[0]) }
|
||||
# A few old devices have been unplugged / powered off for days (not while they're mid-migration)
|
||||
$p.OfflineDays = if ((Get-DemoRand $s 'longoff') -lt 0.07) { Get-DemoInt $s 'offdays' 1 21 } else { 0 }
|
||||
# Monitors at the desk move to the replacement, so both share the source's set
|
||||
$monCount = if ($p.Laptop) { Get-DemoPick $s 'mons' @(0, 1, 1, 2) } else { Get-DemoPick $s 'mons' @(1, 2, 2, 2, 3) }
|
||||
$p.Monitors = @(1..3 | Select-Object -First $monCount | ForEach-Object { Get-DemoPick $s "mon$(if ((Get-DemoRand $s 'monmatch') -lt 0.7) { 1 } else { $_ })" $script:DemoMonitors })
|
||||
|
||||
# Facts specific to this physical device
|
||||
$n = $key
|
||||
@@ -214,6 +220,8 @@ function Get-DemoProfile([string]$name) {
|
||||
$p.DiskModel = Get-DemoPick $n 'ssd' $script:DemoSsdModels
|
||||
$p.DiskSerial = (1..8 | ForEach-Object { "{0:X4}" -f (Get-DemoInt $n "dsn$_" 0 65535) }) -join '_'
|
||||
$p.Inactive = (Get-DemoRand $n 'inactive') -lt 0.04
|
||||
$p.NoSccm = (-not $p.IsDest) -and (Get-DemoRand $n 'nosccm') -lt 0.025 # broken client, never reported
|
||||
$p.NoAd = (-not $p.IsDest) -and (Get-DemoRand $n 'noad') -lt 0.012 # computer object deleted
|
||||
}
|
||||
$script:DemoProfiles[$key] = $p
|
||||
return $p
|
||||
@@ -247,8 +255,9 @@ function Get-DemoNetworkState([string]$computer) {
|
||||
if ($fault -lt 0.03) { return "DNS Issue" }
|
||||
|
||||
$stage = (Get-DemoStage $p).Name
|
||||
if (-not $p.IsDest -and $p.OfflineDays -gt 0 -and $stage -in @('NotStarted', 'Completed')) { return "Timed Out" }
|
||||
if ($p.IsDest) {
|
||||
if ($stage -eq 'NotStarted' -and (Get-DemoRand $p.Name 'staged') -lt 0.5) { return "Not Found" } # not imaged yet
|
||||
if (Test-DemoNotImaged $p) { return "Not Found" } # not imaged yet: no DNS record
|
||||
if ($stage -eq 'RestoreInProgress') { return "Online" }
|
||||
} else {
|
||||
if ($stage -eq 'BackupInProgress') { return "Online" }
|
||||
@@ -261,6 +270,11 @@ function Get-DemoNetworkState([string]$computer) {
|
||||
return "Timed Out"
|
||||
}
|
||||
|
||||
# A replacement that hasn't been imaged yet isn't in DNS, AD or SCCM
|
||||
function Test-DemoNotImaged([hashtable]$p) {
|
||||
return $p.IsDest -and (Get-DemoStage $p).Name -eq 'NotStarted' -and (Get-DemoRand $p.Name 'staged') -lt 0.5
|
||||
}
|
||||
|
||||
function Start-DemoLatency([int]$scale = 1) {
|
||||
$ms = [int]$script:DemoCfg.DelayMsPerDevice * $scale
|
||||
if ($ms -gt 0) { Start-Sleep -Milliseconds $ms }
|
||||
@@ -412,7 +426,9 @@ function Get-DeviceAssociations {
|
||||
function Get-DeviceInventory {
|
||||
param([string]$computerName, $Session)
|
||||
$p = Get-DemoProfile $computerName
|
||||
if (-not $p.Valid) { return @{} }
|
||||
if (-not $p.Valid) { return @{ InAD = $false; InSCCM = $false } } # a typo: nothing knows it
|
||||
if (Test-DemoNotImaged $p) { return @{ InAD = $false; InSCCM = $false } }
|
||||
$flags = @{ InAD = -not $p.NoAd; InSCCM = -not $p.NoSccm }
|
||||
|
||||
$vis = if ($p.Public) { "Public" } else { "Private" }
|
||||
$form = if ($p.Laptop) { "Laptops" } else { "Desktops" }
|
||||
@@ -432,7 +448,12 @@ function Get-DeviceInventory {
|
||||
$updated = (Get-Date).AddDays(-1 * (Get-DemoInt $p.Source 'upd' 20 1400))
|
||||
if ($p.IsDest) { $updated = (Get-Date).AddDays(-1 * (Get-DemoInt $p.Name 'upd' 1 30)) }
|
||||
|
||||
return @{
|
||||
$monitors = @($p.Monitors)
|
||||
$inv = @{
|
||||
InAD = $true
|
||||
InSCCM = $true
|
||||
MonitorCount = "$($monitors.Count)"
|
||||
Monitors = if ($monitors.Count) { $monitors -join "<br>" } else { "None" }
|
||||
OU = "Company Devices/$vis/$form"
|
||||
Groups = $groups -join "<br>"
|
||||
MAC = $p.Mac
|
||||
@@ -455,6 +476,24 @@ function Get-DeviceInventory {
|
||||
SCCMPrimaryUsers = $uda
|
||||
Disk = "Size: $($p.DiskSize) GB<br>Model: $($p.DiskModel)<br>S/N: $($p.DiskSerial)."
|
||||
}
|
||||
# What a missing AD object / SCCM record would take away
|
||||
if (-not $flags.InAD) { $inv.InAD = $false; foreach ($k in @("OU", "Groups")) { $inv.Remove($k) } }
|
||||
if (-not $flags.InSCCM) {
|
||||
$inv.InSCCM = $false
|
||||
foreach ($k in @("MAC", "Model", "RAM", "InventoryIP", "LastUser", "ClientStatus", "Updated", "Location", "Vanity", "Department", "Building", "Mount", "PrimaryUser", "PhoneExt", "InfoBy", "Notes", "RoomType", "SCCMPrimaryUsers", "Disk", "MonitorCount", "Monitors")) { $inv.Remove($k) }
|
||||
}
|
||||
return $inv
|
||||
}
|
||||
|
||||
# Client's last activity, used the first time a device is tracked and doesn't answer
|
||||
function Get-DeviceLastSeen {
|
||||
param([string]$computerName, $Session)
|
||||
$p = Get-DemoProfile $computerName
|
||||
if (-not $p.Valid -or (Test-DemoNotImaged $p) -or $p.NoSccm) { return $null }
|
||||
if (-not $p.IsDest -and $p.OfflineDays -gt 0) {
|
||||
return (Get-Date).AddDays(-$p.OfflineDays).AddHours(-(Get-DemoInt $p.Name 'offhrs' 0 9))
|
||||
}
|
||||
return (Get-Date).AddMinutes(-(Get-DemoInt $p.Name "seen$([math]::Floor((Get-DemoNow) / 600))" 5 55))
|
||||
}
|
||||
|
||||
function Get-InstalledSoftware {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# Initialize-Provider, Open-ProviderCycle, Close-ProviderCycle, pingDevice,
|
||||
# Resolve-DeviceIP, Get-DiskSpace, Get-LogInfo, Get-DeviceAssociations,
|
||||
# Get-DeviceInventory, Get-InstalledSoftware
|
||||
# and optionally Get-DeviceLastSeen.
|
||||
# =========================================================================================
|
||||
|
||||
$script:SccmCfg = $config.SCCM
|
||||
@@ -254,10 +255,13 @@ function Get-DeviceInventory {
|
||||
|
||||
$searcher = $null
|
||||
try {
|
||||
# Only letters, digits and - _ . can reach the LDAP filter
|
||||
if ($computerName -notmatch '^[A-Za-z0-9._-]+$') { throw "invalid name" }
|
||||
$searcher = [adsisearcher]"(&(objectCategory=computer)(name=$computerName))"
|
||||
$searcher.PropertiesToLoad.Add("distinguishedname") | Out-Null
|
||||
$searcher.PropertiesToLoad.Add("memberof") | Out-Null
|
||||
$adResult = $searcher.FindOne()
|
||||
$res.InAD = [bool]$adResult # the lookup worked, so "no result" really means "not in AD"
|
||||
|
||||
if ($adResult) {
|
||||
$dn = $adResult.Properties["distinguishedname"][0]
|
||||
@@ -308,7 +312,9 @@ function Get-DeviceInventory {
|
||||
$reader = $cmd.ExecuteReader()
|
||||
$assetExists = $false
|
||||
|
||||
$res.InSCCM = $false
|
||||
if ($reader.Read()) {
|
||||
$res.InSCCM = $true
|
||||
if ($reader["MAC_Addresses0"] -isnot [DBNull] -and $reader["MAC_Addresses0"]) { $res.MAC = $reader["MAC_Addresses0"] }
|
||||
if ($reader["Model0"] -isnot [DBNull] -and $reader["Model0"]) { $res.Model = $reader["Model0"] }
|
||||
if ($reader["TotalPhysicalMemory0"] -isnot [DBNull] -and $reader["TotalPhysicalMemory0"]) {
|
||||
@@ -395,9 +401,63 @@ function Get-DeviceInventory {
|
||||
} catch {}
|
||||
# ---------------------------
|
||||
|
||||
# --- Monitors (WmiMonitorID hardware inventory) ---
|
||||
$mon = $script:SccmCfg.Monitors
|
||||
if ($mon -and $mon.Enabled -and (Test-SqlName $mon.View) -and (Test-SqlName $mon.NameColumn) -and (Test-SqlName $mon.ManufacturerColumn)) {
|
||||
try {
|
||||
if ($SqlConn -and $SqlConn.State -eq 'Open') {
|
||||
$monCmd = $SqlConn.CreateCommand()
|
||||
$monCmd.CommandText = @"
|
||||
SELECT m.[$($mon.NameColumn)] AS MonName, m.[$($mon.ManufacturerColumn)] AS MonMfr
|
||||
FROM v_R_System sys
|
||||
INNER JOIN $($mon.View) m ON sys.ResourceID = m.ResourceID
|
||||
WHERE sys.Name0 = @MonCompName
|
||||
"@
|
||||
$monCmd.Parameters.AddWithValue("@MonCompName", $computerName) | Out-Null
|
||||
$monReader = $monCmd.ExecuteReader()
|
||||
$names = @()
|
||||
while ($monReader.Read()) {
|
||||
$name = ConvertFrom-WmiCharCodes $monReader["MonName"]
|
||||
$mfr = ConvertFrom-WmiCharCodes $monReader["MonMfr"]
|
||||
if ($mon.InternalPattern -and $mfr -match $mon.InternalPattern -and -not $name) { continue } # laptop panel
|
||||
$names += $(if ($name) { $name } elseif ($mfr) { "$mfr (unnamed)" } else { "Unknown monitor" })
|
||||
}
|
||||
$monReader.Dispose(); $monCmd.Dispose()
|
||||
$res.MonitorCount = "$($names.Count)"
|
||||
$res.Monitors = if ($names.Count -gt 0) { $names -join "<br>" } else { "None" }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return $res
|
||||
}
|
||||
|
||||
# WmiMonitorID strings are uint16 arrays; SCCM inventories them as "72,80,32,69,50,52,0,0"
|
||||
function ConvertFrom-WmiCharCodes($value) {
|
||||
if ($null -eq $value -or $value -is [DBNull]) { return "" }
|
||||
$text = "$value".Trim()
|
||||
if ($text -match '^\d+(\s*,\s*\d+)*$') {
|
||||
$text = -join ($text -split '\s*,\s*' | Where-Object { [int]$_ -gt 0 } | ForEach-Object { [char][int]$_ })
|
||||
}
|
||||
return $text.Trim()
|
||||
}
|
||||
|
||||
# When a device is tracked for the first time and doesn't answer a ping, "last online" is
|
||||
# backfilled from the client's last activity (heartbeat / policy / status messages).
|
||||
function Get-DeviceLastSeen {
|
||||
param([string]$computerName, $Session)
|
||||
$SqlConn = $Session.Sql
|
||||
if (-not $SqlConn -or $SqlConn.State -ne 'Open') { return $null }
|
||||
$cmd = $SqlConn.CreateCommand()
|
||||
try {
|
||||
$cmd.CommandText = "SELECT TOP 1 ch.LastActiveTime FROM v_R_System sys INNER JOIN v_CH_ClientSummary ch ON sys.ResourceID = ch.ResourceID WHERE sys.Name0 = @SeenName"
|
||||
$cmd.Parameters.AddWithValue("@SeenName", $computerName) | Out-Null
|
||||
$val = $cmd.ExecuteScalar()
|
||||
if ($val -and $val -isnot [DBNull]) { return ([datetime]$val).ToLocalTime() }
|
||||
} catch {} finally { $cmd.Dispose() }
|
||||
return $null
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Installed software (used by Get-Software.ps1)
|
||||
# -----------------------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user