param( [string]$ComputerName, [string]$ConfigPath, [switch]$Demo ) if ([string]::IsNullOrWhiteSpace($ComputerName)) { return "[]" } $scriptDir = $PSScriptRoot $dataDir = Join-Path $scriptDir "Data" if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir | Out-Null } $cacheFile = Join-Path $dataDir "Software-Cache.json" $blacklistFile = Join-Path $dataDir "Blacklisted-Apps.txt" if (-not (Test-Path $cacheFile)) { Set-Content -Path $cacheFile -Value "{}" -Encoding UTF8 } if (-not (Test-Path $blacklistFile)) { Set-Content -Path $blacklistFile -Value "" -Encoding UTF8 } . (Join-Path $scriptDir "Lib/Common.ps1") $config = Import-DashConfig -Path $ConfigPath -Demo:$Demo $cacheHours = $config.Worker.SoftwareCacheHours # --- 1. CHECK THE CACHE (24-Hour Timer Only) --- $needsUpdate = $true if (Test-Path $cacheFile) { try { $rawCache = Read-SharedText $cacheFile if (-not [string]::IsNullOrWhiteSpace($rawCache)) { $cacheData = $rawCache | ConvertFrom-Json $propExists = $false foreach ($p in $cacheData.psobject.properties) { if ($p.Name -eq $ComputerName) { $propExists = $true; break } } if ($propExists) { $deviceCache = $cacheData.$ComputerName if ($null -ne $deviceCache.Timestamp -and $null -ne $deviceCache.Data) { $cacheTime = [datetime]$deviceCache.Timestamp # Logic Check: Is cache still fresh? if ($cacheTime -gt (Get-Date).AddHours(-1 * $cacheHours)) { $needsUpdate = $false $outData = @($deviceCache.Data) return (ConvertTo-Json -InputObject $outData -Depth 5 -Compress) } } } } } catch { # If cache is corrupt, proceed to the provider } } if (-not $needsUpdate) { return } # --- 2. LOAD BLACKLIST --- $blacklist = @() if (Test-Path $blacklistFile) { $blacklist = Get-Content $blacklistFile | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() } } # --- 3. PROVIDER QUERY (SCCM SQL in Live mode, simulated fleet in Demo mode) --- $providerName = if ($config.IsDemo) { "Demo" } else { $config.Provider } $v = $false . (Join-Path $scriptDir "Providers/$providerName.ps1") $results = [System.Collections.Generic.List[PSCustomObject]]::new() try { foreach ($row in (Get-InstalledSoftware -ComputerName $ComputerName)) { $appName = "$($row.Name)".Trim() if ([string]::IsNullOrWhiteSpace($appName) -or $appName -eq "--") { continue } $isSpecialty = $true foreach ($badApp in $blacklist) { if ($appName -like $badApp) { $isSpecialty = $false break } } $results.Add([PSCustomObject]@{ Name = $appName Version = $row.Version Publisher = $row.Publisher Source = $row.Source InstallDate = $row.InstallDate IsSpecialty = $isSpecialty }) } } catch {} # --- 4. ATOMIC CACHE SAVE --- try { $cacheEntry = [PSCustomObject]@{ Timestamp = (Get-Date).ToString("o") Data = @($results) } for ($i = 0; $i -lt 10; $i++) { try { $fs = [System.IO.File]::Open($cacheFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) $sr = New-Object System.IO.StreamReader($fs) $rawCache = $sr.ReadToEnd() $latestCache = [PSCustomObject]@{} if (-not [string]::IsNullOrWhiteSpace($rawCache)) { try { $latestCache = $rawCache | ConvertFrom-Json } catch {} } $propExists = $false foreach ($p in $latestCache.psobject.properties) { if ($p.Name -eq $ComputerName) { $propExists = $true; break } } if ($propExists) { $latestCache.$ComputerName = $cacheEntry } else { $latestCache | Add-Member -MemberType NoteProperty -Name $ComputerName -Value $cacheEntry -Force } $fs.SetLength(0) $sw = New-Object System.IO.StreamWriter($fs, [System.Text.Encoding]::UTF8) $sw.Write(($latestCache | ConvertTo-Json -Depth 6 -Compress)) $sw.Flush() $sw.Close() $sr.Close() $fs.Close() break } catch { Start-Sleep -Milliseconds 300 } } } catch {} $outData = @($results) return (ConvertTo-Json -InputObject $outData -Depth 5 -Compress)