Generalize USMT Dash and add a containerized demo mode
Remove all environment-specific data and make the dashboard adoptable anywhere, then add a demo that runs the real PowerShell services. - Strip cached device data, internal knowledge-base articles and screenshots, server/site names and the old project notes; replace the knowledge base with generic sample articles and diagrams - Move every environment value into Config/usmt-dash.psd1 - Route all infrastructure access in the worker and Get-Software.ps1 through a provider (Providers/SCCM.ps1), with a config-driven asset inventory view instead of a hard-coded custom class - Add Providers/Demo.ps1: a deterministic simulated fleet whose migrations progress in real time, and Demo/New-DemoData.ps1 to seed it - Server: -Demo makes the API read-only; add get-config and get-logs; serve static files only from web/, Documentation/ and an allowlist of Data/ files with a path-traversal guard; gzip large responses - Mirror both services' console output to Data/Logs via a Write-Host proxy, and add a terminal panel to the dashboard that streams it - Demo sandbox (web/demo/demo.js): visitor changes to tags, articles and images are kept in the visitor's browser and echoed in the terminals; nothing a visitor does reaches the server - Vendor marked, highlight.js and html2pdf.js instead of loading CDNs - Dockerfile and compose file for hosting the demo (Coolify-ready)
This commit is contained in:
commit
1c531bc2ab
56 files changed
+10232
No files matched your search
@@ -0,0 +1,448 @@
|
||||
# =========================================================================================
|
||||
# SCCM / ConfigMgr provider
|
||||
#
|
||||
# Talks to real infrastructure: the ConfigMgr PowerShell module (computer associations),
|
||||
# the site database over SQL (inventory + installed software), Active Directory (OU and
|
||||
# group membership), DNS/ICMP (online checks) and the devices' admin shares over SMB
|
||||
# (free disk space and USMT progress logs).
|
||||
#
|
||||
# Requirements: Windows PowerShell 5.1, the ConfigMgr console (ConfigurationManager
|
||||
# module), read access to the site database, and admin-share access to the devices.
|
||||
#
|
||||
# Every provider implements the same functions:
|
||||
# Initialize-Provider, Open-ProviderCycle, Close-ProviderCycle, pingDevice,
|
||||
# Resolve-DeviceIP, Get-DiskSpace, Get-LogInfo, Get-DeviceAssociations,
|
||||
# Get-DeviceInventory, Get-InstalledSoftware
|
||||
# =========================================================================================
|
||||
|
||||
$script:SccmCfg = $config.SCCM
|
||||
|
||||
function Initialize-Provider {
|
||||
$ModulePath = Join-Path (Split-Path $env:SMS_ADMIN_UI_PATH) "ConfigurationManager.psd1"
|
||||
Import-Module $ModulePath -ErrorAction Stop
|
||||
}
|
||||
|
||||
function Open-ProviderCycle {
|
||||
# Flush DNS cache at the start of every cycle so stale A records and cached
|
||||
# negative (NXDOMAIN) results don't cause false timeouts -- especially for
|
||||
# devices freshly plugged into the network that were looked up before they registered.
|
||||
if ($config.Worker.FlushDnsEachCycle) {
|
||||
try {
|
||||
$null = ipconfig /flushdns
|
||||
if ($v) { Write-Host "DNS cache flushed." -ForegroundColor DarkGray }
|
||||
} catch { Write-Host "DNS flush failed: $($_.Exception.Message)" -ForegroundColor DarkYellow }
|
||||
}
|
||||
|
||||
$SiteCode = $script:SccmCfg.SiteCode
|
||||
$session = @{ Sql = $null; PreviousLocation = Get-Location }
|
||||
|
||||
$session.Sql = New-Object System.Data.SqlClient.SqlConnection("Server=$($script:SccmCfg.SqlServer);Database=$($script:SccmCfg.Database);Integrated Security=True;")
|
||||
try { $session.Sql.Open() } catch { Write-Host "Warning: Could not open Master SQL Connection" -ForegroundColor Red }
|
||||
|
||||
if (-not (Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue)) {
|
||||
if ($v) { Write-Host "Connecting to SCCM Drive ($SiteCode)..." -ForegroundColor DarkGray }
|
||||
New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $script:SccmCfg.ProviderMachineName -Scope Global | Out-Null
|
||||
}
|
||||
Set-Location "$($SiteCode):"
|
||||
return $session
|
||||
}
|
||||
|
||||
function Close-ProviderCycle {
|
||||
param($Session)
|
||||
if ($Session.Sql) {
|
||||
if ($Session.Sql.State -eq 'Open') { $Session.Sql.Close() }
|
||||
$Session.Sql.Dispose()
|
||||
}
|
||||
Set-Location $Session.PreviousLocation
|
||||
}
|
||||
|
||||
function pingDevice {
|
||||
param ([string]$computer)
|
||||
if ([string]::IsNullOrWhiteSpace($computer) -or $computer -eq "N/A") { return "Not Found" }
|
||||
|
||||
if ($v) { Write-Host " > Fast Ping: $computer... " -ForegroundColor DarkGray -NoNewline }
|
||||
|
||||
try {
|
||||
# Use Resolve-DnsName instead of GetHostAddressesAsync: the latter reads the
|
||||
# Windows DNS client cache, which can hold STALE A records (wrong IP) long after
|
||||
# DNS itself has the current record. Resolve-DnsName queries more directly and
|
||||
# returns the live record. -DnsOnly skips the local cache; we still take all A records.
|
||||
$allIps = @()
|
||||
try {
|
||||
$dnsRecs = Resolve-DnsName -Name $computer -Type A -DnsOnly -ErrorAction Stop | Where-Object { $_.Type -eq 'A' }
|
||||
$allIps = @($dnsRecs | ForEach-Object { $_.IPAddress } | Select-Object -Unique)
|
||||
} catch {
|
||||
# Resolve-DnsName failed (no record / DNS error) -- fall back to .NET resolver.
|
||||
try {
|
||||
$task = [System.Net.Dns]::GetHostAddressesAsync($computer)
|
||||
if ($task.Wait(1000)) {
|
||||
$allIps = @($task.Result | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | ForEach-Object { $_.IPAddressToString } | Select-Object -Unique)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if ($allIps.Count -eq 0) {
|
||||
if ($v) { Write-Host "Not Found (No IP)." -ForegroundColor Red }
|
||||
return "Not Found"
|
||||
}
|
||||
|
||||
# --- Ping every resolved IP; online if ANY responds ---
|
||||
# Handles duplicate/stale DNS records by testing all candidates.
|
||||
$p = $null
|
||||
$liveIp = $null
|
||||
try {
|
||||
$p = [System.Net.NetworkInformation.Ping]::new()
|
||||
foreach ($ip in $allIps) {
|
||||
foreach ($attempt in 1..2) {
|
||||
$res = $p.Send($ip, 1000)
|
||||
if ($res.Status -eq 'Success') { $liveIp = $ip; break }
|
||||
}
|
||||
if ($liveIp) { break }
|
||||
}
|
||||
} finally {
|
||||
if ($null -ne $p) { $p.Dispose() }
|
||||
}
|
||||
|
||||
if (-not $liveIp) {
|
||||
if ($v) { Write-Host "Timed Out." -ForegroundColor DarkYellow }
|
||||
return "Timed Out"
|
||||
}
|
||||
|
||||
# --- Reverse DNS validation on the IP that actually responded ---
|
||||
# Only flag DNS Issue if reverse DNS SUCCEEDS but names a DIFFERENT machine.
|
||||
# A missing PTR record is common and is NOT an issue -- treat as online.
|
||||
try {
|
||||
$reverseHost = [System.Net.Dns]::GetHostEntry($liveIp).HostName
|
||||
$reverseShort = ($reverseHost -split '\.')[0].ToUpper()
|
||||
if ($reverseShort -ne $computer.ToUpper()) {
|
||||
if ($v) { Write-Host "DNS Issue ($liveIp resolves back to $reverseShort)." -ForegroundColor Red }
|
||||
return "DNS Issue"
|
||||
}
|
||||
} catch {
|
||||
# No PTR record -- not a conflict.
|
||||
}
|
||||
# --------------------------------
|
||||
|
||||
if ($v) { Write-Host "Online" -ForegroundColor Green }
|
||||
return "Online"
|
||||
} catch {
|
||||
if ($v) { Write-Host "Not Found (DNS)." -ForegroundColor Red }
|
||||
return "Not Found"
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-DeviceIP {
|
||||
param([string]$computerName)
|
||||
try {
|
||||
$ipRecord = [System.Net.Dns]::GetHostAddresses($computerName) | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1
|
||||
if ($ipRecord) { return $ipRecord.IPAddressToString }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-DiskSpace {
|
||||
param([string]$computer)
|
||||
if ($v) { Write-Host " > Checking C: Drive Storage (SMB Method)... " -ForegroundColor DarkGray -NoNewline }
|
||||
|
||||
try {
|
||||
# Using SMB (Port 445) completely bypasses WMI/DCOM hangs
|
||||
$fso = New-Object -ComObject Scripting.FileSystemObject
|
||||
$uncPath = "\\$computer\$($script:SccmCfg.AdminShare)"
|
||||
|
||||
# Check if the path exists to avoid a long COM hang
|
||||
if ([System.IO.Directory]::Exists($uncPath)) {
|
||||
$drive = $fso.GetDrive($uncPath)
|
||||
$total = [math]::Round($drive.TotalSize / 1GB, 1)
|
||||
$free = [math]::Round($drive.FreeSpace / 1GB, 1)
|
||||
|
||||
if ($v) { Write-Host "${free}GB Free out of ${total}GB" -ForegroundColor Green }
|
||||
return @{ Total = "$total GB"; Free = "$free GB" }
|
||||
} else {
|
||||
if ($v) { Write-Host "$($script:SccmCfg.AdminShare) Share Unreachable" -ForegroundColor Red }
|
||||
}
|
||||
} catch {
|
||||
if ($v) { Write-Host "SMB Access Error" -ForegroundColor Red }
|
||||
}
|
||||
|
||||
return @{ Total = "N/A"; Free = "N/A" }
|
||||
}
|
||||
|
||||
function Get-LogInfo {
|
||||
param ([string]$computer, [string]$logName)
|
||||
|
||||
$basePaths = @($script:SccmCfg.LogFolders | ForEach-Object { "\\$computer\$($script:SccmCfg.AdminShare)\$_" })
|
||||
$currentDrive = Get-Location
|
||||
Set-Location "C:\"
|
||||
$result = New-LogInfoResult
|
||||
|
||||
if ($v) { Write-Host " > Scraping logs for '$logName'..." -ForegroundColor DarkGray }
|
||||
|
||||
foreach ($folder in $basePaths) {
|
||||
if ($v) { Write-Host " Checking $folder... " -ForegroundColor DarkGray -NoNewline }
|
||||
try {
|
||||
if ([System.IO.Directory]::Exists($folder)) {
|
||||
$files = [System.IO.Directory]::GetFiles($folder, "$logName*")
|
||||
if ($files.Count -gt 0) {
|
||||
$realPath = $files[0]
|
||||
if ($v) { Write-Host "Found!" -ForegroundColor Green }
|
||||
|
||||
$result.LastModified = (Get-Item $realPath).LastWriteTime
|
||||
$content = Get-Content -Path $realPath -Tail 1 -ErrorAction SilentlyContinue
|
||||
if (-not $content) { $content = Get-Content -Path $realPath -Tail 1 -Encoding Unicode -ErrorAction SilentlyContinue }
|
||||
|
||||
if ($content) {
|
||||
Set-LogInfoLine $result $content.Trim()
|
||||
$result.Found = $true; $result.Path = $realPath
|
||||
}
|
||||
break
|
||||
} else { if ($v) { Write-Host "Empty/Not there." -ForegroundColor DarkGray } }
|
||||
} else { if ($v) { Write-Host "Path doesn't exist." -ForegroundColor DarkGray } }
|
||||
} catch {
|
||||
if ($v) { Write-Host "Access Denied / Error." -ForegroundColor Red }
|
||||
$result.Line = "Access Denied"
|
||||
}
|
||||
}
|
||||
Set-Location $currentDrive
|
||||
return $result
|
||||
}
|
||||
|
||||
function Get-DeviceAssociations {
|
||||
param([string]$SourceComputer)
|
||||
return Get-CMComputerAssociation -SourceComputer $SourceComputer -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Inventory: AD (OU + groups) and the site database (hardware, users, asset-info class)
|
||||
# Returns only the fields it could find; the worker fills the rest with "N/A".
|
||||
# -----------------------------------------------------------------------------------------
|
||||
function Test-SqlName([string]$name) { return ($name -match '^[A-Za-z0-9_]+$') }
|
||||
|
||||
function Read-AssetInventoryRow {
|
||||
# Applies the configured column map + room-type flags from a data reader to $res
|
||||
param($reader, $res)
|
||||
$ai = $script:SccmCfg.AssetInventory
|
||||
foreach ($field in $ai.Columns.Keys) {
|
||||
$val = $reader["AI_$field"]
|
||||
if ($val -isnot [DBNull] -and "$val") { $res[$field] = "$val" }
|
||||
}
|
||||
$rTypes = @()
|
||||
for ($i = 0; $i -lt $ai.RoomTypes.Count; $i++) {
|
||||
if ("$($reader["RT_$i"])" -eq "1") { $rTypes += $ai.RoomTypes[$i].Label }
|
||||
}
|
||||
return $rTypes
|
||||
}
|
||||
|
||||
function Get-AssetInventorySelect {
|
||||
param([string]$alias)
|
||||
$ai = $script:SccmCfg.AssetInventory
|
||||
$parts = @()
|
||||
foreach ($field in $ai.Columns.Keys) {
|
||||
$col = $ai.Columns[$field]
|
||||
if ((Test-SqlName $col) -and (Test-SqlName $field)) { $parts += "$alias.[$col] AS [AI_$field]" }
|
||||
}
|
||||
for ($i = 0; $i -lt $ai.RoomTypes.Count; $i++) {
|
||||
$col = $ai.RoomTypes[$i].Column
|
||||
if (Test-SqlName $col) { $parts += "$alias.[$col] AS [RT_$i]" }
|
||||
}
|
||||
return ($parts -join ", ")
|
||||
}
|
||||
|
||||
function Get-DeviceInventory {
|
||||
param([string]$computerName, $Session)
|
||||
$res = @{}
|
||||
$SqlConn = $Session.Sql
|
||||
|
||||
$searcher = $null
|
||||
try {
|
||||
$searcher = [adsisearcher]"(&(objectCategory=computer)(name=$computerName))"
|
||||
$searcher.PropertiesToLoad.Add("distinguishedname") | Out-Null
|
||||
$searcher.PropertiesToLoad.Add("memberof") | Out-Null
|
||||
$adResult = $searcher.FindOne()
|
||||
|
||||
if ($adResult) {
|
||||
$dn = $adResult.Properties["distinguishedname"][0]
|
||||
if ($dn) {
|
||||
$ouParts = @($dn -split ',' | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_.Substring(3) })
|
||||
[array]::Reverse($ouParts)
|
||||
$res.OU = if ($ouParts.Count -gt 0) { $ouParts -join '/' } else { $dn }
|
||||
}
|
||||
|
||||
$groupDNs = $adResult.Properties["memberof"]
|
||||
if ($groupDNs) {
|
||||
$groups = $groupDNs | ForEach-Object { ($_ -split ',')[0].Substring(3) }
|
||||
$res.Groups = $groups -join "<br>"
|
||||
}
|
||||
}
|
||||
} catch {} finally {
|
||||
if ($null -ne $searcher) { $searcher.Dispose() }
|
||||
}
|
||||
|
||||
$ai = $script:SccmCfg.AssetInventory
|
||||
$useAsset = $ai.Enabled -and (Test-SqlName $ai.View)
|
||||
|
||||
try {
|
||||
if ($SqlConn -and $SqlConn.State -eq 'Open') {
|
||||
$cmd = $SqlConn.CreateCommand()
|
||||
|
||||
$assetSelect = ""
|
||||
$assetJoin = ""
|
||||
if ($useAsset) {
|
||||
$assetSelect = ", ai.ResourceID AS AI_ResourceID, " + (Get-AssetInventorySelect "ai")
|
||||
$assetJoin = "LEFT JOIN $($ai.View) ai ON sys.ResourceID = ai.ResourceID"
|
||||
}
|
||||
|
||||
$cmd.CommandText = @"
|
||||
SELECT TOP 1
|
||||
sys.User_Name0, sys.Client0, sys.Active0, cs.Model0, mem.TotalPhysicalMemory0, mac.MAC_Addresses0, ip.IP_Addresses0
|
||||
$assetSelect
|
||||
FROM v_R_System sys
|
||||
$assetJoin
|
||||
LEFT JOIN v_GS_COMPUTER_SYSTEM cs ON sys.ResourceID = cs.ResourceID
|
||||
LEFT JOIN v_GS_X86_PC_MEMORY mem ON sys.ResourceID = mem.ResourceID
|
||||
LEFT JOIN (SELECT ResourceID, MAX(MAC_Addresses0) as MAC_Addresses0 FROM v_RA_System_MACAddresses GROUP BY ResourceID) mac ON sys.ResourceID = mac.ResourceID
|
||||
LEFT JOIN (SELECT ResourceID, MAX(IP_Addresses0) as IP_Addresses0 FROM v_RA_System_IPAddresses GROUP BY ResourceID) ip ON sys.ResourceID = ip.ResourceID
|
||||
WHERE sys.Name0 = @CompName
|
||||
"@
|
||||
|
||||
$cmd.Parameters.AddWithValue("@CompName", $computerName) | Out-Null
|
||||
$reader = $cmd.ExecuteReader()
|
||||
$assetExists = $false
|
||||
|
||||
if ($reader.Read()) {
|
||||
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"]) {
|
||||
$res.RAM = "$([math]::Round([long]$reader["TotalPhysicalMemory0"] / 1048576, 1)) GB"
|
||||
}
|
||||
if ($reader["IP_Addresses0"] -isnot [DBNull] -and $reader["IP_Addresses0"]) {
|
||||
$res.InventoryIP = $reader["IP_Addresses0"]
|
||||
}
|
||||
if ($reader["User_Name0"] -isnot [DBNull] -and $reader["User_Name0"]) { $res.LastUser = $reader["User_Name0"] }
|
||||
|
||||
$cStat = if ("$($reader["Client0"])" -eq "1") { "Installed" } else { "Not Installed" }
|
||||
$aStat = if ("$($reader["Active0"])" -eq "1") { "Active" } else { "Inactive" }
|
||||
$res.ClientStatus = "$cStat / $aStat"
|
||||
|
||||
if ($useAsset) {
|
||||
# Check if the asset-info record actually exists
|
||||
if ($reader["AI_ResourceID"] -isnot [DBNull]) { $assetExists = $true }
|
||||
$rTypes = Read-AssetInventoryRow $reader $res
|
||||
$res.RoomType = if ($rTypes.Count -gt 0) { $rTypes -join ", " } else { "Standard" }
|
||||
}
|
||||
}
|
||||
$reader.Dispose()
|
||||
|
||||
# --- Fallback asset view (Only if the primary row was completely missing) ---
|
||||
if ($useAsset -and -not $assetExists -and (Test-SqlName $ai.FallbackView)) {
|
||||
if ($v) { Write-Host " -> Not in $($ai.View), trying $($ai.FallbackView) fallback..." -ForegroundColor DarkYellow }
|
||||
|
||||
$fallbackCmd = $SqlConn.CreateCommand()
|
||||
$fallbackCmd.CommandText = @"
|
||||
SELECT TOP 1 $(Get-AssetInventorySelect "fb")
|
||||
FROM v_R_System sys
|
||||
INNER JOIN $($ai.FallbackView) fb ON sys.ResourceID = fb.ResourceID
|
||||
WHERE sys.Name0 = @CompName
|
||||
"@
|
||||
$fallbackCmd.Parameters.AddWithValue("@CompName", $computerName) | Out-Null
|
||||
$fbReader = $fallbackCmd.ExecuteReader()
|
||||
|
||||
if ($fbReader.Read()) {
|
||||
$rTypes = Read-AssetInventoryRow $fbReader $res
|
||||
if ($rTypes.Count -gt 0) { $res.RoomType = $rTypes -join ", " }
|
||||
}
|
||||
$fbReader.Dispose()
|
||||
$fallbackCmd.Dispose()
|
||||
}
|
||||
# ------------------------------------------
|
||||
|
||||
$cmd.CommandText = "SELECT UniqueUserName FROM v_UserMachineRelationship WHERE MachineResourceName = @CompName2"
|
||||
$cmd.Parameters.AddWithValue("@CompName2", $computerName) | Out-Null
|
||||
$reader2 = $cmd.ExecuteReader()
|
||||
$uda = @()
|
||||
while ($reader2.Read()) {
|
||||
$uName = $reader2["UniqueUserName"].ToString()
|
||||
if ($uName -match '^[^\\]+\\(.*)') { $uName = $Matches[1] } # strip DOMAIN\
|
||||
$uda += $uName
|
||||
}
|
||||
if ($uda.Count -gt 0) { $res.SCCMPrimaryUsers = $uda -join "<br>" }
|
||||
$reader2.Dispose()
|
||||
$cmd.Dispose()
|
||||
}
|
||||
} catch { }
|
||||
|
||||
# --- Physical Disk Query ---
|
||||
try {
|
||||
if ($SqlConn -and $SqlConn.State -eq 'Open') {
|
||||
$diskCmd = $SqlConn.CreateCommand()
|
||||
$diskCmd.CommandText = @"
|
||||
SELECT TOP 1 pd.Model0, pd.SerialNumber0, pd.Size0
|
||||
FROM v_R_System sys
|
||||
LEFT JOIN v_GS_PHYSICAL_DISK pd ON sys.ResourceID = pd.ResourceID
|
||||
WHERE sys.Name0 = @DiskCompName AND pd.MediaType0 = 4
|
||||
ORDER BY pd.Size0 DESC
|
||||
"@
|
||||
$diskCmd.Parameters.AddWithValue("@DiskCompName", $computerName) | Out-Null
|
||||
$diskReader = $diskCmd.ExecuteReader()
|
||||
if ($diskReader.Read()) {
|
||||
$diskModel = if ($diskReader["Model0"] -isnot [DBNull]) { $diskReader["Model0"] } else { "N/A" }
|
||||
$diskSerial = if ($diskReader["SerialNumber0"] -isnot [DBNull]) { $diskReader["SerialNumber0"] } else { "N/A" }
|
||||
$diskSizeGB = if ($diskReader["Size0"] -isnot [DBNull] -and $diskReader["Size0"]) { "$([math]::Round([long]$diskReader["Size0"] / 1GB)) GB" } else { "N/A" }
|
||||
$res.Disk = "Size: $diskSizeGB<br>Model: $diskModel<br>S/N: $diskSerial"
|
||||
}
|
||||
$diskReader.Dispose()
|
||||
$diskCmd.Dispose()
|
||||
}
|
||||
} catch {}
|
||||
# ---------------------------
|
||||
|
||||
return $res
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Installed software (used by Get-Software.ps1)
|
||||
# -----------------------------------------------------------------------------------------
|
||||
function Get-InstalledSoftware {
|
||||
param([string]$ComputerName)
|
||||
|
||||
$sql = @"
|
||||
SELECT DISTINCT
|
||||
ARPDisplayName0 AS Name,
|
||||
ProductVersion0 AS Version,
|
||||
Publisher0 AS Publisher,
|
||||
InstallSource0 AS Source,
|
||||
InstallDate0 AS InstallDate
|
||||
FROM v_GS_INSTALLED_SOFTWARE SW
|
||||
INNER JOIN v_R_System SYS ON SW.ResourceID = SYS.ResourceID
|
||||
WHERE SYS.Name0 = @CompName
|
||||
AND ARPDisplayName0 IS NOT NULL
|
||||
ORDER BY ARPDisplayName0
|
||||
"@
|
||||
|
||||
$rows = @()
|
||||
$conn = New-Object System.Data.SqlClient.SqlConnection
|
||||
$conn.ConnectionString = "Server=$($script:SccmCfg.SqlServer);Database=$($script:SccmCfg.Database);Integrated Security=True;"
|
||||
$conn.Open()
|
||||
try {
|
||||
$cmd = $conn.CreateCommand()
|
||||
$cmd.CommandText = $sql
|
||||
$cmd.CommandTimeout = 30
|
||||
$cmd.Parameters.AddWithValue("@CompName", $ComputerName) | Out-Null
|
||||
|
||||
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter $cmd
|
||||
$dataset = New-Object System.Data.DataSet
|
||||
$null = $adapter.Fill($dataset)
|
||||
|
||||
if ($dataset.Tables.Count -gt 0) {
|
||||
foreach ($row in $dataset.Tables[0].Rows) {
|
||||
$rows += [PSCustomObject]@{
|
||||
Name = "$($row.Name)"
|
||||
Version = $row.Version
|
||||
Publisher = $row.Publisher
|
||||
Source = $row.Source
|
||||
InstallDate = if ($row.InstallDate -isnot [DBNull]) { $row.InstallDate } else { "" }
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally { $conn.Close() }
|
||||
return $rows
|
||||
}
|
||||
Reference in new issue
Block a user