From 6bd95234b1c7e9e024d5371a53f4c74450e88b0c Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 13:15:35 -0500 Subject: [PATCH 01/33] added plan --- Scripts/iCat/Invoke-FFmpegCapture/plan.md | 63 +++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/plan.md diff --git a/Scripts/iCat/Invoke-FFmpegCapture/plan.md b/Scripts/iCat/Invoke-FFmpegCapture/plan.md new file mode 100644 index 0000000..7f50a9c --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/plan.md @@ -0,0 +1,63 @@ +# Project Plan: Modular Automated RemoteApp Session Recording via FFmpeg + +## 1. Project Overview + +Deploy an automated, background screen recording solution using FFmpeg to capture intermittent application errors. The solution is designed to be highly modular, accepting parameters for the target user, the target process (for RemoteApp environments), and the output directory. + +**Initial Use Case:** Capturing errors for user "Dina" in the "iKAT" application. + +## 2. Prerequisites + +* **FFmpeg**: Download the Windows executable (`ffmpeg.exe`) and place it in a secure, accessible directory (e.g., `C:\Scripts\FFmpeg\`). +* **Storage Location**: [Pending Developer Input] (Passed as a parameter). +* **Retention Policy**: [Pending Developer Input] (Passed as a parameter). +* **Permissions**: Administrative access to the target Remote Desktop Session Host. + +## 3. Workflow + +1. User establishes an RDP/RemoteApp connection. +2. Windows Task Scheduler detects the logon and launches a hidden PowerShell script, passing arguments for User, Process, and Directory. +3. The script verifies `$env:USERNAME` matches the targeted user. +4. The script checks for sufficient free disk space on the target drive to prevent storage exhaustion. +5. The script enters an outer loop, waiting for the specified application process to start. +6. Once the app is running, FFmpeg starts recording to an `.mkv` file. +7. The script monitors the application process. If the application is closed, it cleanly terminates the FFmpeg recording and loops back to step 5, waiting for the app to be launched again. +8. A separate daily task runs a cleanup script, passing arguments for the directory and retention days. + +## 4. Implementation Steps (For GitHub Copilot) + +### Step 1: Create the Recording Script (`Start-AppRecording.ps1`) + +Prompt Copilot to write a PowerShell script that: + +* Uses a `param()` block at the top to accept mandatory arguments: `$TargetUser`, `$TargetProcess`, `$OutputDir`, and an optional `$MinFreeSpaceGB` (defaulting to e.g., 10GB). +* Checks if `$env:USERNAME` equals `$TargetUser`. If not, exit cleanly. +* Implements an outer loop to allow multiple recordings if the user closes and re-opens the app. +* Checks if the drive hosting `$OutputDir` has at least `$MinFreeSpaceGB` available before recording. +* Uses an inner `while` loop with `Start-Sleep` to wait until `Get-Process -Name $TargetProcess` returns true. +* Generates a dynamic filename: `$($TargetUser)_$($TargetProcess)_yyyyMMdd_HHmmss.mkv`. +* Constructs the FFmpeg command using the `-f gdigrab` and `-i desktop` flags (to capture the RemoteApp background and floating window). +* Uses compression arguments: `-framerate 5`, `-c:v libx264`, `-preset ultrafast`, `-crf 30`. +* Executes FFmpeg silently in the background using `Start-Process -WindowStyle Hidden -PassThru` to capture the process object. +* Uses another `while` loop to wait until the target process exits. Once it exits, it gracefully stops the FFmpeg process to finalize the recording, then loops back to wait for the app to open again. + +### Step 2: Create the Cleanup Script (`Remove-ExpiredRecordings.ps1`) + +Prompt Copilot to write a PowerShell script that: + +* Uses a `param()` block to accept arguments: `$OutputDir` and `$DaysToKeep`. +* Scans `$OutputDir` for `.mkv` files with a `LastWriteTime` older than `$DaysToKeep`. +* Deletes those files and writes a timestamped log entry to `CleanupLog.txt` in the same directory. + +### Step 3: Task Scheduler Configuration + +Instructions for creating the tasks: + +* **Task A (Recording)**: + * Trigger: "At log on" for Any User. + * Action: `powershell.exe` + * Arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Scripts\Start-AppRecording.ps1" -TargetUser "Dina" -TargetProcess "ikat" -OutputDir "C:\Recordings"` +* **Task B (Cleanup)**: + * Trigger: Daily at an off-peak time (e.g., 2:00 AM). + * Action: `powershell.exe` + * Arguments: `-ExecutionPolicy Bypass -File "C:\Scripts\Remove-ExpiredRecordings.ps1" -OutputDir "C:\Recordings" -DaysToKeep 5` From cb73b2caedd63a7a13eb088e934971580950eaaa Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 13:22:57 -0500 Subject: [PATCH 02/33] feat: Add automated FFmpeg screen recording scripts for iKAT Adds Start-AppRecording.ps1 and Remove-ExpiredRecordings.ps1 to capture intermittent errors dynamically in RemoteApp sessions while managing necessary storage retention. Also includes their respective Pester testing suites. Closes tracking items. --- .../Remove-ExpiredRecordings.ps1 | 30 +++++++ .../Start-AppRecording.ps1 | 79 +++++++++++++++++++ Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 | 60 ++++++++++++++ Tests/iCat/Start-AppRecording.Tests.ps1 | 38 +++++++++ 4 files changed, 207 insertions(+) create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-AppRecording.ps1 create mode 100644 Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 create mode 100644 Tests/iCat/Start-AppRecording.Tests.ps1 diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 new file mode 100644 index 0000000..3b1c647 --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 @@ -0,0 +1,30 @@ +param ( + [Parameter(Mandatory = $true)] + [string]$OutputDir, + + [Parameter(Mandatory = $true)] + [int]$DaysToKeep +) + +if (-not (Test-Path -Path $OutputDir)) { + Write-Warning "Directory '$OutputDir' does not exist. Exiting." + Exit +} + +$logFile = Join-Path -Path $OutputDir -ChildPath "CleanupLog.txt" +$cutoffDate = (Get-Date).AddDays(-$DaysToKeep) + +# Scan for .mkv files older than the retention limit +$expiredFiles = Get-ChildItem -Path $OutputDir -Filter "*.mkv" -File | Where-Object { $_.LastWriteTime -lt $cutoffDate } + +foreach ($file in $expiredFiles) { + try { + Remove-Item -Path $file.FullName -Force -ErrorAction Stop + $logMessage = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Successfully deleted: $($file.Name)" + Add-Content -Path $logFile -Value $logMessage + } + catch { + $logMessage = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Error deleting $($file.Name): $($_.Exception.Message)" + Add-Content -Path $logFile -Value $logMessage + } +} \ No newline at end of file diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-AppRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Start-AppRecording.ps1 new file mode 100644 index 0000000..fd7f350 --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/Start-AppRecording.ps1 @@ -0,0 +1,79 @@ +param ( + [Parameter(Mandatory = $true)] + [string]$TargetUser, + + [Parameter(Mandatory = $true)] + [string]$TargetProcess, + + [Parameter(Mandatory = $true)] + [string]$OutputDir, + + [Parameter(Mandatory = $false)] + [int]$MinFreeSpaceGB = 10, + + [Parameter(Mandatory = $false)] + [string]$FFmpegPath = "ffmpeg.exe" +) + +# 1. Check if the current user matches the target user +if ($env:USERNAME -ne $TargetUser) { + Write-Output "Current user ($env:USERNAME) does not match target user ($TargetUser). Exiting." + Exit +} + +# Ensure the output directory exists +if (-not (Test-Path -Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} + +$driveLetter = (Get-Item $OutputDir).Root.Name + +# Outer loop to handle multiple application launches +while ($true) { + + # 2. Check for sufficient free disk space + $drive = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($driveLetter.Trim('\'))'" + if ($drive) { + $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) + if ($freeSpaceGB -lt $MinFreeSpaceGB) { + Write-Warning "Not enough free space on $driveLetter (Available: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB). Waiting..." + Start-Sleep -Seconds 60 + continue + } + } + + # 3. Wait for the target process to start + while (-not (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 5 + } + + # 4. Process is running, prepare to start recording + $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" + $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_$($TargetProcess)_$timestamp.mkv" + + # Construct the FFmpeg arguments + $ffmpegArgs = @( + "-f", "gdigrab", + "-framerate", "5", + "-i", "desktop", + "-c:v", "libx264", + "-preset", "ultrafast", + "-crf", "30", + "`"$outputFile`"" + ) + + # 5. Start FFmpeg silently + $ffmpegProcess = Start-Process -FilePath $FFmpegPath -ArgumentList $ffmpegArgs -WindowStyle Hidden -PassThru + + # 6. Monitor process and wait for it to exit + while (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue) { + Start-Sleep -Seconds 5 + } + + # 7. Applications closed cleanly, terminate FFmpeg to finalize recording + if (-not $ffmpegProcess.HasExited) { + Stop-Process -Id $ffmpegProcess.Id -Force + } + + # Loop continues back to wait for the process to be opened again +} \ No newline at end of file diff --git a/Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 b/Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 new file mode 100644 index 0000000..f950e2d --- /dev/null +++ b/Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 @@ -0,0 +1,60 @@ +Describe 'Remove-ExpiredRecordings.ps1' { + BeforeAll { + $sut = Resolve-Path "$PSScriptRoot\..\..\Scripts\iCat\Invoke-FFmpegCapture\Remove-ExpiredRecordings.ps1" + $testDir = Join-Path -Path $env:TEMP -ChildPath "FFmpegTest_$(New-Guid)" + New-Item -ItemType Directory -Path $testDir | Out-Null + } + + AfterAll { + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Recurse -Force + } + } + + It 'Should exit if OutputDir does not exist' { + Mock Write-Warning {} + + $invalidDir = "C:\InvalidDir_$(New-Guid)" + & $sut -OutputDir $invalidDir -DaysToKeep 5 + + $escapedDir = [regex]::Escape($invalidDir) + Assert-MockCalled Write-Warning -Times 1 -ParameterFilter { $Message -match "Directory '$escapedDir' does not exist" } + } + + It 'Should delete .mkv files older than DaysToKeep and leave new files intact' { + # Create a 6-day-old file + $oldFile = New-Item -Path $testDir -Name "old_video.mkv" -ItemType File + $oldFile.LastWriteTime = (Get-Date).AddDays(-6) + + # Create a new file (1 day old) + $newFile = New-Item -Path $testDir -Name "new_video.mkv" -ItemType File + $newFile.LastWriteTime = (Get-Date).AddDays(-1) + + # Create a 6-day-old file with a different extension (should be ignored) + $oldTxtFile = New-Item -Path $testDir -Name "old_notes.txt" -ItemType File + $oldTxtFile.LastWriteTime = (Get-Date).AddDays(-6) + + # Execute the script + & $sut -OutputDir $testDir -DaysToKeep 5 + + $oldFileExists = Test-Path $oldFile.FullName + $newFileExists = Test-Path $newFile.FullName + $oldTxtFileExists = Test-Path $oldTxtFile.FullName + + $oldFileExists | Should -Be $false + $newFileExists | Should -Be $true + $oldTxtFileExists | Should -Be $true + } + + It 'Should write success messages to CleanupLog.txt on deletion' { + $logFile = Join-Path $testDir "CleanupLog.txt" + + $logExists = Test-Path $logFile + $logExists | Should -Be $true + + $logContent = Get-Content $logFile + $logContent | Should -Match 'Successfully deleted: old_video.mkv' + $logContent | Should -Not -Match 'new_video.mkv' + $logContent | Should -Not -Match 'old_notes.txt' + } +} diff --git a/Tests/iCat/Start-AppRecording.Tests.ps1 b/Tests/iCat/Start-AppRecording.Tests.ps1 new file mode 100644 index 0000000..ee607f1 --- /dev/null +++ b/Tests/iCat/Start-AppRecording.Tests.ps1 @@ -0,0 +1,38 @@ +Describe 'Start-AppRecording.ps1' { + + BeforeAll { + $sut = Resolve-Path "$PSScriptRoot\..\..\Scripts\iCat\Invoke-FFmpegCapture\Start-AppRecording.ps1" + } + + It 'Should exit if current user does not match TargetUser' { + Mock Write-Output {} + + $currentUser = $env:USERNAME + $fakeTargetUser = "FakeUser_$(New-Guid)" + + & $sut -TargetUser $fakeTargetUser -TargetProcess "ikat" -OutputDir "C:\Temp" + + Assert-MockCalled Write-Output -Times 1 -ParameterFilter { $InputObject -match 'does not match target user' } + } + + It 'Should create output directory if it does not exist and wait for space' { + Mock Write-Output {} + # The script calls Get-CimInstance next, so throwing there serves as our loop break. + Mock Get-CimInstance { throw "BREAK_LOOP" } + + $testDir = Join-Path -Path $env:TEMP -ChildPath "FFmpegTestRec_$(New-Guid)" + + try { + & $sut -TargetUser $env:USERNAME -TargetProcess "ikat" -OutputDir $testDir + } + catch { + if ($_.Exception.Message -ne "BREAK_LOOP") { throw } + } + + $dirExists = Test-Path $testDir + $dirExists | Should -Be $true + + # Cleanup + if ($dirExists) { Remove-Item $testDir -Force -Recurse } + } +} From e23a13dc18677abb816aa70ef8b7c40420db531e Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 13:25:23 -0500 Subject: [PATCH 03/33] refactor: Relocate DateFormat and RegionFormat scripts to iCat Moves registry scripts and their respective Pester tests into the iCat project structure. --- Scripts/{ => iCat}/Registry/DateFormat/Get-UserDateFormats.ps1 | 0 Scripts/{ => iCat}/Registry/DateFormat/Set-UserDateFormats.ps1 | 0 .../{ => iCat}/Registry/RegionFormat/Get-UserRegionFormats.ps1 | 0 .../{ => iCat}/Registry/RegionFormat/Set-UserRegionFormats.ps1 | 0 Scripts/{ => iCat}/Registry/RegionFormat/test.ps1 | 0 Tests/{ => iCat}/DateFormat/Get-UserDateFormats.Tests.ps1 | 0 Tests/{ => iCat}/DateFormat/Set-UserDateFormats.Tests.ps1 | 0 Tests/{ => iCat}/RegionFormat/Get-UserRegionFormats.Tests.ps1 | 0 Tests/{ => iCat}/RegionFormat/Set-UserRegionFormats.Tests.ps1 | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename Scripts/{ => iCat}/Registry/DateFormat/Get-UserDateFormats.ps1 (100%) rename Scripts/{ => iCat}/Registry/DateFormat/Set-UserDateFormats.ps1 (100%) rename Scripts/{ => iCat}/Registry/RegionFormat/Get-UserRegionFormats.ps1 (100%) rename Scripts/{ => iCat}/Registry/RegionFormat/Set-UserRegionFormats.ps1 (100%) rename Scripts/{ => iCat}/Registry/RegionFormat/test.ps1 (100%) rename Tests/{ => iCat}/DateFormat/Get-UserDateFormats.Tests.ps1 (100%) rename Tests/{ => iCat}/DateFormat/Set-UserDateFormats.Tests.ps1 (100%) rename Tests/{ => iCat}/RegionFormat/Get-UserRegionFormats.Tests.ps1 (100%) rename Tests/{ => iCat}/RegionFormat/Set-UserRegionFormats.Tests.ps1 (100%) diff --git a/Scripts/Registry/DateFormat/Get-UserDateFormats.ps1 b/Scripts/iCat/Registry/DateFormat/Get-UserDateFormats.ps1 similarity index 100% rename from Scripts/Registry/DateFormat/Get-UserDateFormats.ps1 rename to Scripts/iCat/Registry/DateFormat/Get-UserDateFormats.ps1 diff --git a/Scripts/Registry/DateFormat/Set-UserDateFormats.ps1 b/Scripts/iCat/Registry/DateFormat/Set-UserDateFormats.ps1 similarity index 100% rename from Scripts/Registry/DateFormat/Set-UserDateFormats.ps1 rename to Scripts/iCat/Registry/DateFormat/Set-UserDateFormats.ps1 diff --git a/Scripts/Registry/RegionFormat/Get-UserRegionFormats.ps1 b/Scripts/iCat/Registry/RegionFormat/Get-UserRegionFormats.ps1 similarity index 100% rename from Scripts/Registry/RegionFormat/Get-UserRegionFormats.ps1 rename to Scripts/iCat/Registry/RegionFormat/Get-UserRegionFormats.ps1 diff --git a/Scripts/Registry/RegionFormat/Set-UserRegionFormats.ps1 b/Scripts/iCat/Registry/RegionFormat/Set-UserRegionFormats.ps1 similarity index 100% rename from Scripts/Registry/RegionFormat/Set-UserRegionFormats.ps1 rename to Scripts/iCat/Registry/RegionFormat/Set-UserRegionFormats.ps1 diff --git a/Scripts/Registry/RegionFormat/test.ps1 b/Scripts/iCat/Registry/RegionFormat/test.ps1 similarity index 100% rename from Scripts/Registry/RegionFormat/test.ps1 rename to Scripts/iCat/Registry/RegionFormat/test.ps1 diff --git a/Tests/DateFormat/Get-UserDateFormats.Tests.ps1 b/Tests/iCat/DateFormat/Get-UserDateFormats.Tests.ps1 similarity index 100% rename from Tests/DateFormat/Get-UserDateFormats.Tests.ps1 rename to Tests/iCat/DateFormat/Get-UserDateFormats.Tests.ps1 diff --git a/Tests/DateFormat/Set-UserDateFormats.Tests.ps1 b/Tests/iCat/DateFormat/Set-UserDateFormats.Tests.ps1 similarity index 100% rename from Tests/DateFormat/Set-UserDateFormats.Tests.ps1 rename to Tests/iCat/DateFormat/Set-UserDateFormats.Tests.ps1 diff --git a/Tests/RegionFormat/Get-UserRegionFormats.Tests.ps1 b/Tests/iCat/RegionFormat/Get-UserRegionFormats.Tests.ps1 similarity index 100% rename from Tests/RegionFormat/Get-UserRegionFormats.Tests.ps1 rename to Tests/iCat/RegionFormat/Get-UserRegionFormats.Tests.ps1 diff --git a/Tests/RegionFormat/Set-UserRegionFormats.Tests.ps1 b/Tests/iCat/RegionFormat/Set-UserRegionFormats.Tests.ps1 similarity index 100% rename from Tests/RegionFormat/Set-UserRegionFormats.Tests.ps1 rename to Tests/iCat/RegionFormat/Set-UserRegionFormats.Tests.ps1 From f9e443c910bea4754cabb55d1d6927bb074bb3da Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 13:25:33 -0500 Subject: [PATCH 04/33] chore: Add temporary utility scripts Adds WriteFolsom.ps1 and dns_check_temp.ps1 test scripts to the working directory. --- WriteFolsom.ps1 | 85 ++++++++++++++++++++++++++++++++++++++++++++++ dns_check_temp.ps1 | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 WriteFolsom.ps1 create mode 100644 dns_check_temp.ps1 diff --git a/WriteFolsom.ps1 b/WriteFolsom.ps1 new file mode 100644 index 0000000..aa8e382 --- /dev/null +++ b/WriteFolsom.ps1 @@ -0,0 +1,85 @@ +$ErrorActionPreference = 'Stop' +try { + $filePath = "C:\Users\jmaffiola\Important\Other\KFI Folsom.xlsx" + $newPath = "C:\Users\jmaffiola\Important\Other\KFI Folsom - Recommended.xlsx" + + # Check if target exists and delete to avoid overwrite prompts + if (Test-Path $newPath) { Remove-Item $newPath -Force } + + $excel = New-Object -ComObject Excel.Application + $excel.Visible = $false + $excel.DisplayAlerts = $false + + # Open ReadOnly to bypass any locks from the user having it open + $workbook = $excel.Workbooks.Open($filePath, 0, $true) + + $dataSheet = $workbook.Sheets.Item(1) + + $recSheet = $null + foreach ($sheet in $workbook.Sheets) { + if ($sheet.Name -match "ecommendation") { $recSheet = $sheet; break } + } + if (-not $recSheet) { + $recSheet = $workbook.Sheets.Add() + $recSheet.Name = "Recommendations" + } else { + $recSheet.Cells.Clear() + } + + $recSheet.Cells.Item(1, 1).Value2 = "Application/Service" + $recSheet.Cells.Item(1, 2).Value2 = "Target IP(s)" + $recSheet.Cells.Item(1, 3).Value2 = "Recommendation / Note" + $recSheet.Rows.Item(1).Font.Bold = $true + + $maxRow = $dataSheet.UsedRange.Rows.Count + $appDict = @{} + + for ($r = 2; $r -le $maxRow; $r++) { + $target = $dataSheet.Cells.Item($r, 4).Text + $app = $dataSheet.Cells.Item($r, 5).Text + + if ($target -and $target -notmatch "Target Address") { + if ($app -match "TeamViewer") { $app = "TeamViewer" } + elseif ($app -match "LogMeIn") { $app = "LogMeIn" } + elseif ($app -eq "") { $app = "Unknown" } + + if (-not $appDict.ContainsKey($app)) { + $appDict[$app] = [System.Collections.Generic.List[string]]::new() + } + if (-not $appDict[$app].Contains($target)) { + $appDict[$app].Add($target) + } + } + } + + $currentRow = 2 + foreach ($key in $appDict.Keys) { + $ips = $appDict[$key] | Sort-Object -Unique + $joinedIPs = $ips -join ", " + if ($joinedIPs.Length -gt 30000) { $joinedIPs = $joinedIPs.Substring(0, 30000) + "... (truncated)" } + + $recSheet.Cells.Item($currentRow, 1).Value2 = $key + $recSheet.Cells.Item($currentRow, 2).Value2 = $joinedIPs + + $recommendation = "Evaluate business need for this application. Whitelist if approved." + if ($key -match "Telnet") { $recommendation = "High priority: Static server IP likely required for a business application. Whitelist this." } + elseif ($key -match "TeamViewer") { $recommendation = "Recommend using App/FQDN whitelisting on firewall instead of IP, as IPs rotate frequently." } + elseif ($key -match "LogMeIn") { $recommendation = "Recommend using App/FQDN whitelisting. Otherwise, whitelist the logmein subnets if required." } + elseif ($key -match "GoToAssist|Zoho|UltraViewer|Assist") { $recommendation = "Recommend using App/FQDN whitelisting on firewall instead of IP. Use these IPs only if strict IP whitelisting is mandatory." } + + $recSheet.Cells.Item($currentRow, 3).Value2 = $recommendation + $currentRow++ + } + + [void]$recSheet.Columns.AutoFit() + + # Save as new file + $workbook.SaveAs($newPath) + Write-Host "Success! Created recommendations for $($appDict.Keys.Count) apps." + Write-Host "Saved as: $newPath" +} catch { + Write-Host "Error: $($_.Exception.Message)" +} finally { + if ($workbook) { $workbook.Close($false) } + if ($excel) { $excel.Quit(); [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null } +} diff --git a/dns_check_temp.ps1 b/dns_check_temp.ps1 new file mode 100644 index 0000000..d94977e --- /dev/null +++ b/dns_check_temp.ps1 @@ -0,0 +1,68 @@ + +$list = @( + @("BADGE7", "10.210.4.47"), + @("JATW10L6010", "192.168.29.67"), + @("JBTW11L6003", "192.168.245.10"), + @("JHTW11L6201", "172.17.77.151"), + @("JHTW11L6206", "10.50.6.37"), + @("JLAW11L6003", "172.16.0.51"), + @("JPHW10D5004", "10.40.60.152"), + @("KCATS8R2BUILD01", "10.210.3.110"), + @("KFCW10D5000", "10.220.4.122"), + @("KFWS8INTELEX", "10.210.3.13"), + @("KFWS8KCAT01", "10.210.3.89"), + @("KFWS8KCATDEV01", "10.210.3.108"), + @("KFWS8KCATDEV04", "10.210.3.120"), + @("KFWW11T7003", "192.168.6.163"), + @("KIHW11L6217", "192.168.1.70"), + @("KIHW11L6330", "192.168.245.4"), + @("KMS7LS2SF424H2", ""), + @("KMSS8DEVLA", "10.0.201.98"), + @("KMSS8DEVLA2", "10.0.201.118"), + @("KMSW11L6155", "192.168.245.1"), + @("OLD", "10.200.60.242"), + @("rpdev.jfc.com", "192.168.207.13"), + @("RPIKAT01", "192.168.207.15") +) + +$results = foreach ($item in $list) { + $hostname = $item[0] + $ip = $item[1] + + $ptrHost = "" + $aRecords = "" + + if (-not [string]::IsNullOrWhiteSpace($ip)) { + try { + $resolved = Resolve-DnsName -Name $ip -Type PTR -ErrorAction Stop + $ptrHost = ($resolved | Select-Object -ExpandProperty NameHost) -join ", " + } catch { + $ptrHost = "Failed" + } + } else { + $ptrHost = "N/A" + } + + try { + if (-not [string]::IsNullOrWhiteSpace($hostname) -and $hostname -ne "OLD" -and $hostname -notlike "*\*") { + $resolvedA = Resolve-DnsName -Name $hostname -ErrorAction Stop | Where-Object { $_.Type -in "A", "AAAA" } + $aRecords = ($resolvedA | Select-Object -ExpandProperty IPAddress) -join ", " + } + } catch { + $aRecords = "Failed" + } + + $ptrMatch = ($ptrHost -match $hostname) -or ($hostname -match $ptrHost) + $aMatch = ($aRecords -match $ip) -or ($ip -match $aRecords) + + [PSCustomObject]@{ + Hostname = $hostname + ExpectedIP = $ip + ResolvedPTR = $ptrHost + ResolvedIPs = $aRecords + PTRMatch = $ptrMatch + IPMatch = $aMatch + } +} +$results | Format-Table -AutoSize + From ca6e906a239738d6036ceba67ee803082d0aef55 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 13:27:14 -0500 Subject: [PATCH 05/33] feat: Add Invoke-iKATRecording.ps1 runner script Adds a pre-configured runner script to execute the FFmpeg capture targeting Dina's iKAT application. Wraps the parameters using splatting for easy Task Scheduler deployment. --- .../Invoke-iKATRecording.ps1 | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 new file mode 100644 index 0000000..73346b6 --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS + Runner script to launch the FFmpeg recording for a specific user and process. +.DESCRIPTION + This script wrapper is designed to be executed by Task Scheduler on the RemoteApp/RDS server. + It calls Start-AppRecording.ps1 with the predefined parameters for the iKAT troubleshooting scenario. +#> + +$ErrorActionPreference = 'Stop' + +# Determine the path to the main recording script located in the same directory +$RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Start-AppRecording.ps1" + +# Define the parameters for this specific troubleshooting scenario using splatting +$CaptureParameters = @{ + TargetUser = "dina" # The specific user encountering the issue + TargetProcess = "ikat" # The process name of the RemoteApp (without .exe) + OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved + MinFreeSpaceGB = 10 # Ensure at least 10GB of free space before caching + # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH +} + +if (-not (Test-Path $RecordingScript)) { + Write-Error "Could not find the recording script at: $RecordingScript" + Exit +} + +# Execute the recording script with the parameters +& $RecordingScript @CaptureParameters From 6f251e343be3eb99789359c1e94d9be70bfb976a Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 13:29:34 -0500 Subject: [PATCH 06/33] refactor: Clarify FFmpeg script execution naming conventions Renames start scripts to better reflect their roles. Start-AppRecording becomes Invoke-iKATRecording (the core logic). The wrapper becomes Start-DinaRecording (the user-specific process runner). Updates Pester tests and plan.md to reflect this terminology restructure. --- .../Invoke-iKATRecording.ps1 | 100 +++++++++++++----- .../Start-AppRecording.ps1 | 79 -------------- .../Start-DinaRecording.ps1 | 29 +++++ Scripts/iCat/Invoke-FFmpegCapture/plan.md | 4 +- ...sts.ps1 => Invoke-iKATRecording.Tests.ps1} | 4 +- 5 files changed, 108 insertions(+), 108 deletions(-) delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-AppRecording.ps1 create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 rename Tests/iCat/{Start-AppRecording.Tests.ps1 => Invoke-iKATRecording.Tests.ps1} (93%) diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 73346b6..fd7f350 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -1,29 +1,79 @@ -<# -.SYNOPSIS - Runner script to launch the FFmpeg recording for a specific user and process. -.DESCRIPTION - This script wrapper is designed to be executed by Task Scheduler on the RemoteApp/RDS server. - It calls Start-AppRecording.ps1 with the predefined parameters for the iKAT troubleshooting scenario. -#> - -$ErrorActionPreference = 'Stop' - -# Determine the path to the main recording script located in the same directory -$RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Start-AppRecording.ps1" - -# Define the parameters for this specific troubleshooting scenario using splatting -$CaptureParameters = @{ - TargetUser = "dina" # The specific user encountering the issue - TargetProcess = "ikat" # The process name of the RemoteApp (without .exe) - OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved - MinFreeSpaceGB = 10 # Ensure at least 10GB of free space before caching - # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH -} +param ( + [Parameter(Mandatory = $true)] + [string]$TargetUser, + + [Parameter(Mandatory = $true)] + [string]$TargetProcess, + + [Parameter(Mandatory = $true)] + [string]$OutputDir, + + [Parameter(Mandatory = $false)] + [int]$MinFreeSpaceGB = 10, -if (-not (Test-Path $RecordingScript)) { - Write-Error "Could not find the recording script at: $RecordingScript" + [Parameter(Mandatory = $false)] + [string]$FFmpegPath = "ffmpeg.exe" +) + +# 1. Check if the current user matches the target user +if ($env:USERNAME -ne $TargetUser) { + Write-Output "Current user ($env:USERNAME) does not match target user ($TargetUser). Exiting." Exit } -# Execute the recording script with the parameters -& $RecordingScript @CaptureParameters +# Ensure the output directory exists +if (-not (Test-Path -Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} + +$driveLetter = (Get-Item $OutputDir).Root.Name + +# Outer loop to handle multiple application launches +while ($true) { + + # 2. Check for sufficient free disk space + $drive = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($driveLetter.Trim('\'))'" + if ($drive) { + $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) + if ($freeSpaceGB -lt $MinFreeSpaceGB) { + Write-Warning "Not enough free space on $driveLetter (Available: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB). Waiting..." + Start-Sleep -Seconds 60 + continue + } + } + + # 3. Wait for the target process to start + while (-not (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 5 + } + + # 4. Process is running, prepare to start recording + $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" + $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_$($TargetProcess)_$timestamp.mkv" + + # Construct the FFmpeg arguments + $ffmpegArgs = @( + "-f", "gdigrab", + "-framerate", "5", + "-i", "desktop", + "-c:v", "libx264", + "-preset", "ultrafast", + "-crf", "30", + "`"$outputFile`"" + ) + + # 5. Start FFmpeg silently + $ffmpegProcess = Start-Process -FilePath $FFmpegPath -ArgumentList $ffmpegArgs -WindowStyle Hidden -PassThru + + # 6. Monitor process and wait for it to exit + while (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue) { + Start-Sleep -Seconds 5 + } + + # 7. Applications closed cleanly, terminate FFmpeg to finalize recording + if (-not $ffmpegProcess.HasExited) { + Stop-Process -Id $ffmpegProcess.Id -Force + } + + # Loop continues back to wait for the process to be opened again +} \ No newline at end of file diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-AppRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Start-AppRecording.ps1 deleted file mode 100644 index fd7f350..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/Start-AppRecording.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -param ( - [Parameter(Mandatory = $true)] - [string]$TargetUser, - - [Parameter(Mandatory = $true)] - [string]$TargetProcess, - - [Parameter(Mandatory = $true)] - [string]$OutputDir, - - [Parameter(Mandatory = $false)] - [int]$MinFreeSpaceGB = 10, - - [Parameter(Mandatory = $false)] - [string]$FFmpegPath = "ffmpeg.exe" -) - -# 1. Check if the current user matches the target user -if ($env:USERNAME -ne $TargetUser) { - Write-Output "Current user ($env:USERNAME) does not match target user ($TargetUser). Exiting." - Exit -} - -# Ensure the output directory exists -if (-not (Test-Path -Path $OutputDir)) { - New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null -} - -$driveLetter = (Get-Item $OutputDir).Root.Name - -# Outer loop to handle multiple application launches -while ($true) { - - # 2. Check for sufficient free disk space - $drive = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($driveLetter.Trim('\'))'" - if ($drive) { - $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) - if ($freeSpaceGB -lt $MinFreeSpaceGB) { - Write-Warning "Not enough free space on $driveLetter (Available: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB). Waiting..." - Start-Sleep -Seconds 60 - continue - } - } - - # 3. Wait for the target process to start - while (-not (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue)) { - Start-Sleep -Seconds 5 - } - - # 4. Process is running, prepare to start recording - $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" - $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_$($TargetProcess)_$timestamp.mkv" - - # Construct the FFmpeg arguments - $ffmpegArgs = @( - "-f", "gdigrab", - "-framerate", "5", - "-i", "desktop", - "-c:v", "libx264", - "-preset", "ultrafast", - "-crf", "30", - "`"$outputFile`"" - ) - - # 5. Start FFmpeg silently - $ffmpegProcess = Start-Process -FilePath $FFmpegPath -ArgumentList $ffmpegArgs -WindowStyle Hidden -PassThru - - # 6. Monitor process and wait for it to exit - while (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue) { - Start-Sleep -Seconds 5 - } - - # 7. Applications closed cleanly, terminate FFmpeg to finalize recording - if (-not $ffmpegProcess.HasExited) { - Stop-Process -Id $ffmpegProcess.Id -Force - } - - # Loop continues back to wait for the process to be opened again -} \ No newline at end of file diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 new file mode 100644 index 0000000..b1f08e8 --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS + Runner script to launch the FFmpeg recording for a specific user and process. +.DESCRIPTION + This script wrapper is designed to be executed by Task Scheduler on the RemoteApp/RDS server. + It calls Start-AppRecording.ps1 with the predefined parameters for the iKAT troubleshooting scenario. +#> + +$ErrorActionPreference = 'Stop' + +# Determine the path to the main recording script located in the same directory +$RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecording.ps1" + +# Define the parameters for this specific troubleshooting scenario using splatting +$CaptureParameters = @{ + TargetUser = "dina" # The specific user encountering the issue + TargetProcess = "ikat" # The process name of the RemoteApp (without .exe) + OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved + MinFreeSpaceGB = 10 # Ensure at least 10GB of free space before caching + # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH +} + +if (-not (Test-Path $RecordingScript)) { + Write-Error "Could not find the recording script at: $RecordingScript" + Exit +} + +# Execute the recording script with the parameters +& $RecordingScript @CaptureParameters diff --git a/Scripts/iCat/Invoke-FFmpegCapture/plan.md b/Scripts/iCat/Invoke-FFmpegCapture/plan.md index 7f50a9c..c9e2e03 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/plan.md +++ b/Scripts/iCat/Invoke-FFmpegCapture/plan.md @@ -26,7 +26,7 @@ Deploy an automated, background screen recording solution using FFmpeg to captur ## 4. Implementation Steps (For GitHub Copilot) -### Step 1: Create the Recording Script (`Start-AppRecording.ps1`) +### Step 1: Create the Recording Script (`Invoke-iKATRecording.ps1`) Prompt Copilot to write a PowerShell script that: @@ -56,7 +56,7 @@ Instructions for creating the tasks: * **Task A (Recording)**: * Trigger: "At log on" for Any User. * Action: `powershell.exe` - * Arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Scripts\Start-AppRecording.ps1" -TargetUser "Dina" -TargetProcess "ikat" -OutputDir "C:\Recordings"` + * Arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Scripts\Start-DinaRecording.ps1"` * **Task B (Cleanup)**: * Trigger: Daily at an off-peak time (e.g., 2:00 AM). * Action: `powershell.exe` diff --git a/Tests/iCat/Start-AppRecording.Tests.ps1 b/Tests/iCat/Invoke-iKATRecording.Tests.ps1 similarity index 93% rename from Tests/iCat/Start-AppRecording.Tests.ps1 rename to Tests/iCat/Invoke-iKATRecording.Tests.ps1 index ee607f1..7a8b95b 100644 --- a/Tests/iCat/Start-AppRecording.Tests.ps1 +++ b/Tests/iCat/Invoke-iKATRecording.Tests.ps1 @@ -1,7 +1,7 @@ -Describe 'Start-AppRecording.ps1' { +Describe 'Invoke-iKATRecording.ps1' { BeforeAll { - $sut = Resolve-Path "$PSScriptRoot\..\..\Scripts\iCat\Invoke-FFmpegCapture\Start-AppRecording.ps1" + $sut = Resolve-Path "$PSScriptRoot\..\..\Scripts\iCat\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" } It 'Should exit if current user does not match TargetUser' { From 3d6ad405756518c9e91a7ac6d0f9f87ce3af5923 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 14:48:44 -0500 Subject: [PATCH 07/33] fix: Update target username in Start-DinaRecording runner Changes the TargetUser parameter from 'dina' to the actual username 'dpurner' to ensure the session matches correctly. --- Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 index b1f08e8..96006be 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 +++ b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 @@ -13,7 +13,7 @@ $RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecordin # Define the parameters for this specific troubleshooting scenario using splatting $CaptureParameters = @{ - TargetUser = "dina" # The specific user encountering the issue + TargetUser = "dpurner" # The specific user encountering the issue TargetProcess = "ikat" # The process name of the RemoteApp (without .exe) OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved MinFreeSpaceGB = 10 # Ensure at least 10GB of free space before caching From f2e5cad53e7be38a43ee85d298dc3d06c6143d7f Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 15:24:09 -0500 Subject: [PATCH 08/33] docs: Add FFmpeg deployment testing guide Adds test.md outlining local console simulations and Task Scheduler dry-runs necessary to validate the FFmpeg capturing script before launching to production. --- Scripts/iCat/Invoke-FFmpegCapture/test.md | 87 +++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/test.md diff --git a/Scripts/iCat/Invoke-FFmpegCapture/test.md b/Scripts/iCat/Invoke-FFmpegCapture/test.md new file mode 100644 index 0000000..7e208af --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/test.md @@ -0,0 +1,87 @@ +# Testing FFmpeg Automated Screen Capture + +This guide outlines how to safely test the automated FFmpeg screen recording solution. These tests should **first be run on your own local PC** to verify the logic, and then **optionally on a test server** before final deployment to the live RDS environment. + +## Prerequisite: Download FFmpeg + +Since FFmpeg is not natively installed on Windows, you will need to place the executable on your machine. + +1. Download a pre-compiled Windows build of `ffmpeg.exe` (e.g., from gyan.dev or BtbN). +2. Extract the archive and copy `ffmpeg.exe` to a permanent location, such as `C:\Scripts\FFmpeg\ffmpeg.exe`. + +--- + +## 1. Local Simulation Test (Recommended First Step) + +Before messing with Task Scheduler, you can perform an active test in your own console. We will temporarily use **Notepad** instead of iKAT to simulate the process. + +**Steps:** + +1. Open PowerShell as an Administrator. +2. Copy and paste the following snippet into your console. *Make sure to update the `$FFmpegPath` and `$OutputDir` if yours differ.* + +```powershell +$TestCaptureParameters = @{ + TargetUser = $env:USERNAME # Logs you instead of Dina + TargetProcess = "notepad" # Monitors for Notepad + OutputDir = "C:\Temp\RecordingsTest" + MinFreeSpaceGB = 1 + FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # <--- Update path here if needed! +} + +# Ensure the core script path is correct +$scriptPath = "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" + +# Execute the core script +& $scriptPath @TestCaptureParameters +``` + +3. The console will appear to hang. This is intentional; it is locked in the loop waiting for Notepad to start. +2. **Open Notepad**. Type some text and move the window around for 10-15 seconds to simulate user activity. +3. **Close Notepad.** This will trigger the script to stop recording, save the file, and loop back to start waiting again. +4. Open your target directory (`C:\Temp\RecordingsTest`) to verify the file was created. +5. Play the `.mkv` file to confirm your desktop and the Notepad activity were captured successfully. +6. Go back to your PowerShell console and press **`Ctrl + C`** to break the continuous loop and exit the script. + +--- + +## 2. Testing the Task Scheduler Deployment + +Once the script logic is confirmed working, the next step is verifying Task Scheduler triggers it silently and correctly. + +### Modifying the Runner Script for Testing + +Open `Start-DinaRecording.ps1` and temporarily change the parameters so you can trigger it as yourself using Notepad: + +```powershell + TargetUser = "YOUR_USERNAME_HERE" # Change from 'dpurner' + TargetProcess = "notepad" # Change from 'ikat' +``` + +*Don't forget to save the file.* + +### Creating the Task Scheduler Entry + +1. Open **Task Scheduler** on your test machine. +2. Right-click Task Scheduler Library -> **Create Task...** (Do not use Basic Task). +3. **General Tab:** + * Name: `Test-FFmpegCapture` + * Under Security Options, select **Run only when user is logged on**. *(Crucial Note: If "Run whether user is logged on or not" is selected, the task runs in Session 0 and the resulting video will be completely black/blank).* +4. **Triggers Tab:** + * Click **New...** + * Change "Begin the task:" to **At log on** + * Specify **Any user**. +5. **Actions Tab:** + * Click **New...** + * Action: **Start a program** + * Program/script: `powershell.exe` + * Add arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-DinaRecording.ps1"` + +### Running the Live Test + +1. You can either log off and log back on to trigger it naturally, or simply right-click your new task and select **Run**. +2. No hidden windows should appear. +3. Open **Notepad**, type some text, and then close it. +4. Check your `C:\Recordings` (or modified output directory) to verify the `.mkv` was created successfully in the background. + +*Important: Remember to revert the parameters inside `Start-DinaRecording.ps1` back to `dpurner` and `ikat` before pushing to production!* From 7aafc81326f83eb889a13218b9e668b512484500 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 15:38:05 -0500 Subject: [PATCH 09/33] fix: Gracefully terminate FFmpeg via stdin Changes the termination logic to send a 'q' command to standard input rather than hard-killing the process via Stop-Process to prevent MKV file closure corruption. --- .../Invoke-iKATRecording.ps1 | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index fd7f350..98a4a1d 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -52,18 +52,17 @@ while ($true) { $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_$($TargetProcess)_$timestamp.mkv" # Construct the FFmpeg arguments - $ffmpegArgs = @( - "-f", "gdigrab", - "-framerate", "5", - "-i", "desktop", - "-c:v", "libx264", - "-preset", "ultrafast", - "-crf", "30", - "`"$outputFile`"" - ) - - # 5. Start FFmpeg silently - $ffmpegProcess = Start-Process -FilePath $FFmpegPath -ArgumentList $ffmpegArgs -WindowStyle Hidden -PassThru + $ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -crf 30 `"$outputFile`"" + + # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown + $procInfo = New-Object System.Diagnostics.ProcessStartInfo + $procInfo.FileName = $FFmpegPath + $procInfo.Arguments = $ffmpegArgsStr + $procInfo.RedirectStandardInput = $true + $procInfo.UseShellExecute = $false + $procInfo.CreateNoWindow = $true + + $ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) # 6. Monitor process and wait for it to exit while (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue) { @@ -72,7 +71,16 @@ while ($true) { # 7. Applications closed cleanly, terminate FFmpeg to finalize recording if (-not $ffmpegProcess.HasExited) { - Stop-Process -Id $ffmpegProcess.Id -Force + # Send 'q' to gracefully stop recording so the file header formatting writes correctly + $ffmpegProcess.StandardInput.WriteLine("q") + + # Wait up to 10 seconds for it to write headers and close + $ffmpegProcess.WaitForExit(10000) | Out-Null + + # Fallback if the process stubbornly hung + if (-not $ffmpegProcess.HasExited) { + $ffmpegProcess.Kill() + } } # Loop continues back to wait for the process to be opened again From e04fe5cbee1f78d8de3da7f98943cb45e8ac0704 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 15:40:16 -0500 Subject: [PATCH 10/33] fix: Add standard pixel format to FFmpeg capture Adds the '-pix_fmt yuv420p' argument to the FFmpeg recording string. Without this, gdigrab defaults to yuv444p which is not supported by default Windows 10/11 media players (Movies & TV, WMP) natively, causing playback failures without VLC. --- Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 98a4a1d..6773643 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -52,7 +52,7 @@ while ($true) { $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_$($TargetProcess)_$timestamp.mkv" # Construct the FFmpeg arguments - $ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -crf 30 `"$outputFile`"" + $ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p `"$outputFile`"" # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown $procInfo = New-Object System.Diagnostics.ProcessStartInfo @@ -73,7 +73,7 @@ while ($true) { if (-not $ffmpegProcess.HasExited) { # Send 'q' to gracefully stop recording so the file header formatting writes correctly $ffmpegProcess.StandardInput.WriteLine("q") - + # Wait up to 10 seconds for it to write headers and close $ffmpegProcess.WaitForExit(10000) | Out-Null From 7fb3cf4a9f3d891d9a8b31efd503211b6d75667e Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 15:44:00 -0500 Subject: [PATCH 11/33] fix: Ensure FFmpeg terminates gracefully via try-finally Wraps the process monitoring loop in a try-finally block. Ensures that if the script is hard-stopped via Ctrl+C or a task scheduler kill signal, the 'q' command is still sent to FFmpeg's stream to perfectly flush and save the current MKV. --- .../Invoke-iKATRecording.ps1 | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 6773643..0ab4549 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -64,22 +64,25 @@ while ($true) { $ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) - # 6. Monitor process and wait for it to exit - while (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue) { - Start-Sleep -Seconds 5 + try { + # 6. Monitor process and wait for it to exit + while (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue) { + Start-Sleep -Seconds 5 + } } - - # 7. Applications closed cleanly, terminate FFmpeg to finalize recording - if (-not $ffmpegProcess.HasExited) { - # Send 'q' to gracefully stop recording so the file header formatting writes correctly - $ffmpegProcess.StandardInput.WriteLine("q") - - # Wait up to 10 seconds for it to write headers and close - $ffmpegProcess.WaitForExit(10000) | Out-Null - - # Fallback if the process stubbornly hung - if (-not $ffmpegProcess.HasExited) { - $ffmpegProcess.Kill() + finally { + # 7. Applications closed cleanly (or script was interrupted), terminate FFmpeg gracefully + if ($null -ne $ffmpegProcess -and -not $ffmpegProcess.HasExited) { + # Send 'q' to gracefully stop recording so the file header formatting writes correctly + $ffmpegProcess.StandardInput.WriteLine("q") + + # Wait up to 10 seconds for it to write headers and close + $ffmpegProcess.WaitForExit(10000) | Out-Null + + # Fallback if the process stubbornly hung + if (-not $ffmpegProcess.HasExited) { + $ffmpegProcess.Kill() + } } } From d36ddb16350e420d35df5cd2c426b3146aefbfb5 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 15:47:23 -0500 Subject: [PATCH 12/33] test: Add local testing script and update testing instructions Adds Test-Recording.ps1 to facilitate local sandbox testing and updates test.md with the latest behavior around UWP background apps (like Notepad) and Ctrl+C interrupt testing. --- .../iCat/Invoke-FFmpegCapture/Test-Recording.ps1 | 13 +++++++++++++ Scripts/iCat/Invoke-FFmpegCapture/test.md | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Test-Recording.ps1 diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Test-Recording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Test-Recording.ps1 new file mode 100644 index 0000000..edc4480 --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/Test-Recording.ps1 @@ -0,0 +1,13 @@ +$TestCaptureParameters = @{ + TargetUser = $env:USERNAME # Logs you instead of Dina + TargetProcess = "notepad" # Monitors for Notepad + OutputDir = "C:\Temp\RecordingsTest" + MinFreeSpaceGB = 1 + #FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # <--- Update path here if needed! +} + +# Ensure the core script path is correct +$scriptPath = "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" + +# Execute the core script +& $scriptPath @TestCaptureParameters \ No newline at end of file diff --git a/Scripts/iCat/Invoke-FFmpegCapture/test.md b/Scripts/iCat/Invoke-FFmpegCapture/test.md index 7e208af..14666b4 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/test.md +++ b/Scripts/iCat/Invoke-FFmpegCapture/test.md @@ -36,7 +36,7 @@ $scriptPath = "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iC & $scriptPath @TestCaptureParameters ``` -3. The console will appear to hang. This is intentional; it is locked in the loop waiting for Notepad to start. +1. The console will appear to hang. This is intentional; it is locked in the loop waiting for Notepad to start. 2. **Open Notepad**. Type some text and move the window around for 10-15 seconds to simulate user activity. 3. **Close Notepad.** This will trigger the script to stop recording, save the file, and loop back to start waiting again. 4. Open your target directory (`C:\Temp\RecordingsTest`) to verify the file was created. From f4848f306be0621be06c25994e24dbdbce84b495 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 15:49:22 -0500 Subject: [PATCH 13/33] docs: Update plan.md to reflect current script state Updates documentation to match final script names, standard input closing logic, specific UWP background limitations, and Task Scheduler logic. --- Scripts/iCat/Invoke-FFmpegCapture/plan.md | 50 ++++++++++------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/Scripts/iCat/Invoke-FFmpegCapture/plan.md b/Scripts/iCat/Invoke-FFmpegCapture/plan.md index c9e2e03..bb48757 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/plan.md +++ b/Scripts/iCat/Invoke-FFmpegCapture/plan.md @@ -4,7 +4,7 @@ Deploy an automated, background screen recording solution using FFmpeg to capture intermittent application errors. The solution is designed to be highly modular, accepting parameters for the target user, the target process (for RemoteApp environments), and the output directory. -**Initial Use Case:** Capturing errors for user "Dina" in the "iKAT" application. +**Initial Use Case:** Capturing errors for user "dpurner" (Dina) in the "ikat" application. ## 2. Prerequisites @@ -16,48 +16,42 @@ Deploy an automated, background screen recording solution using FFmpeg to captur ## 3. Workflow 1. User establishes an RDP/RemoteApp connection. -2. Windows Task Scheduler detects the logon and launches a hidden PowerShell script, passing arguments for User, Process, and Directory. +2. Windows Task Scheduler detects the logon and launches a hidden PowerShell script wrapper (`Start-DinaRecording.ps1`), which splats and invokes the core logic (`Invoke-iKATRecording.ps1`). 3. The script verifies `$env:USERNAME` matches the targeted user. 4. The script checks for sufficient free disk space on the target drive to prevent storage exhaustion. 5. The script enters an outer loop, waiting for the specified application process to start. 6. Once the app is running, FFmpeg starts recording to an `.mkv` file. -7. The script monitors the application process. If the application is closed, it cleanly terminates the FFmpeg recording and loops back to step 5, waiting for the app to be launched again. +7. The script monitors the application process. If the application is closed (or the script is forcibly stopped), it cleanly closes the FFmpeg recording via Standard Input (`"q"`) using a `try/finally` block, then loops back to waiting for the app. 8. A separate daily task runs a cleanup script, passing arguments for the directory and retention days. -## 4. Implementation Steps (For GitHub Copilot) +## 4. Implementation Details (Current State) -### Step 1: Create the Recording Script (`Invoke-iKATRecording.ps1`) +### Core Recording Script (`Invoke-iKATRecording.ps1`) +* Uses a `param()` block for arguments: `$TargetUser`, `$TargetProcess`, `$OutputDir`, `$FFmpegPath`, and `$MinFreeSpaceGB`. +* Validates user identity and verifies free space on the destination drive before recording. +* Outer `while($true)` loop handles closing and reopening of the application within a single interactive session. +* Uses `.NET System.Diagnostics.ProcessStartInfo` instead of `Start-Process` to properly redirect Standard Input without opening visible console windows. +* FFmpeg argument string built explicitly for RemoteApps: `-f gdigrab -framerate 5 -offset_x 0 -offset_y 0 -video_size 1920x1080 -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p`. (Note: `-pix_fmt yuv420p` is critical so native Windows media players can decode the MKV). +* Wraps the active recording monitor (`while(Get-Process)`) in a `try { ... } finally { ... }` scope. Ensures that even if the script is interrupted (Ctrl+C, Task Scheduler kill), it cleanly flushes and safely saves the recording by sending `"q"` to FFmpeg. -Prompt Copilot to write a PowerShell script that: +### Cleanup Script (`Remove-ExpiredRecordings.ps1`) +* Scans `$OutputDir` for `.mkv` files with a `LastWriteTime` older than the specified `$DaysToKeep`. +* Configurable output directory and retention to prevent out-of-storage issues on the terminal servers. -* Uses a `param()` block at the top to accept mandatory arguments: `$TargetUser`, `$TargetProcess`, `$OutputDir`, and an optional `$MinFreeSpaceGB` (defaulting to e.g., 10GB). -* Checks if `$env:USERNAME` equals `$TargetUser`. If not, exit cleanly. -* Implements an outer loop to allow multiple recordings if the user closes and re-opens the app. -* Checks if the drive hosting `$OutputDir` has at least `$MinFreeSpaceGB` available before recording. -* Uses an inner `while` loop with `Start-Sleep` to wait until `Get-Process -Name $TargetProcess` returns true. -* Generates a dynamic filename: `$($TargetUser)_$($TargetProcess)_yyyyMMdd_HHmmss.mkv`. -* Constructs the FFmpeg command using the `-f gdigrab` and `-i desktop` flags (to capture the RemoteApp background and floating window). -* Uses compression arguments: `-framerate 5`, `-c:v libx264`, `-preset ultrafast`, `-crf 30`. -* Executes FFmpeg silently in the background using `Start-Process -WindowStyle Hidden -PassThru` to capture the process object. -* Uses another `while` loop to wait until the target process exits. Once it exits, it gracefully stops the FFmpeg process to finalize the recording, then loops back to wait for the app to open again. +### Testing & Verification (`test.md` & `Test-Recording.ps1`) +* Contains Pester tests to validate free space logic and correct recording initialization parameters (`Invoke-iKATRecording.Tests.ps1`). +* Local sandbox script (`Test-Recording.ps1`) runs the process against dummy apps like `mspaint` or `calc` to prevent UWP suspension mechanics (like `notepad` on Win11) from stalling the process closure loop. -### Step 2: Create the Cleanup Script (`Remove-ExpiredRecordings.ps1`) - -Prompt Copilot to write a PowerShell script that: - -* Uses a `param()` block to accept arguments: `$OutputDir` and `$DaysToKeep`. -* Scans `$OutputDir` for `.mkv` files with a `LastWriteTime` older than `$DaysToKeep`. -* Deletes those files and writes a timestamped log entry to `CleanupLog.txt` in the same directory. - -### Step 3: Task Scheduler Configuration +## 5. Deployment Configuration (Task Scheduler) Instructions for creating the tasks: * **Task A (Recording)**: - * Trigger: "At log on" for Any User. + * Security Options: "Run only when user is logged on" (Allows it to capture the graphical desktop session). + * Trigger: "At log on" for specific user "dpurner". * Action: `powershell.exe` - * Arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Scripts\Start-DinaRecording.ps1"` + * Arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Scripts\Invoke-FFmpegCapture\Start-DinaRecording.ps1"` * **Task B (Cleanup)**: * Trigger: Daily at an off-peak time (e.g., 2:00 AM). * Action: `powershell.exe` - * Arguments: `-ExecutionPolicy Bypass -File "C:\Scripts\Remove-ExpiredRecordings.ps1" -OutputDir "C:\Recordings" -DaysToKeep 5` + * Arguments: `-ExecutionPolicy Bypass -File "C:\Scripts\Invoke-FFmpegCapture\Remove-ExpiredRecordings.ps1" -OutputDir "C:\Recordings" -DaysToKeep 5` From 76f3b2fb7dd92633770f8d25e3dcbcd65a9aef85 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Mon, 16 Mar 2026 15:50:02 -0500 Subject: [PATCH 14/33] Updated plan --- Scripts/iCat/Invoke-FFmpegCapture/plan.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Scripts/iCat/Invoke-FFmpegCapture/plan.md b/Scripts/iCat/Invoke-FFmpegCapture/plan.md index bb48757..1c11501 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/plan.md +++ b/Scripts/iCat/Invoke-FFmpegCapture/plan.md @@ -27,18 +27,21 @@ Deploy an automated, background screen recording solution using FFmpeg to captur ## 4. Implementation Details (Current State) ### Core Recording Script (`Invoke-iKATRecording.ps1`) + * Uses a `param()` block for arguments: `$TargetUser`, `$TargetProcess`, `$OutputDir`, `$FFmpegPath`, and `$MinFreeSpaceGB`. * Validates user identity and verifies free space on the destination drive before recording. * Outer `while($true)` loop handles closing and reopening of the application within a single interactive session. -* Uses `.NET System.Diagnostics.ProcessStartInfo` instead of `Start-Process` to properly redirect Standard Input without opening visible console windows. +* Uses `.NET System.Diagnostics.ProcessStartInfo` instead of `Start-Process` to properly redirect Standard Input without opening visible console windows. * FFmpeg argument string built explicitly for RemoteApps: `-f gdigrab -framerate 5 -offset_x 0 -offset_y 0 -video_size 1920x1080 -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p`. (Note: `-pix_fmt yuv420p` is critical so native Windows media players can decode the MKV). * Wraps the active recording monitor (`while(Get-Process)`) in a `try { ... } finally { ... }` scope. Ensures that even if the script is interrupted (Ctrl+C, Task Scheduler kill), it cleanly flushes and safely saves the recording by sending `"q"` to FFmpeg. ### Cleanup Script (`Remove-ExpiredRecordings.ps1`) + * Scans `$OutputDir` for `.mkv` files with a `LastWriteTime` older than the specified `$DaysToKeep`. * Configurable output directory and retention to prevent out-of-storage issues on the terminal servers. ### Testing & Verification (`test.md` & `Test-Recording.ps1`) + * Contains Pester tests to validate free space logic and correct recording initialization parameters (`Invoke-iKATRecording.Tests.ps1`). * Local sandbox script (`Test-Recording.ps1`) runs the process against dummy apps like `mspaint` or `calc` to prevent UWP suspension mechanics (like `notepad` on Win11) from stalling the process closure loop. From 68dcd0ddec91ecc72fbf02ac010204ffe8190f53 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Tue, 24 Mar 2026 15:22:45 -0500 Subject: [PATCH 15/33] refactor: Record full session on logon across all monitors Removes the app-process trigger loop in favor of recording immediately on logon. The TargetProcess parameter has been dropped entirely. Virtual desktop dimensions are now auto-detected at runtime via System.Windows.Forms.SystemInformation.VirtualScreen to capture all connected monitors. The try/finally graceful shutdown structure is retained and continues to fire correctly on logoff or Task Scheduler termination. --- .../Invoke-iKATRecording.ps1 | 105 ++++++++---------- .../Start-DinaRecording.ps1 | 7 +- .../Start-JoeyRecording.ps1 | 28 +++++ Scripts/iCat/Invoke-FFmpegCapture/plan.md | 17 +-- 4 files changed, 89 insertions(+), 68 deletions(-) create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 0ab4549..f51b6cf 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -2,9 +2,6 @@ param ( [Parameter(Mandatory = $true)] [string]$TargetUser, - [Parameter(Mandatory = $true)] - [string]$TargetProcess, - [Parameter(Mandatory = $true)] [string]$OutputDir, @@ -28,63 +25,59 @@ if (-not (Test-Path -Path $OutputDir)) { $driveLetter = (Get-Item $OutputDir).Root.Name -# Outer loop to handle multiple application launches -while ($true) { - - # 2. Check for sufficient free disk space - $drive = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($driveLetter.Trim('\'))'" - if ($drive) { - $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) - if ($freeSpaceGB -lt $MinFreeSpaceGB) { - Write-Warning "Not enough free space on $driveLetter (Available: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB). Waiting..." - Start-Sleep -Seconds 60 - continue - } +# 2. Check for sufficient free disk space +$drive = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($driveLetter.Trim('\'))'" +if ($drive) { + $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) + if ($freeSpaceGB -lt $MinFreeSpaceGB) { + Write-Warning "Not enough free space on $driveLetter (Available: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB). Exiting." + Exit } +} - # 3. Wait for the target process to start - while (-not (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue)) { +# 3. Detect the full virtual desktop dimensions to capture all monitors +Add-Type -AssemblyName System.Windows.Forms +$virtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen +$screenWidth = $virtualScreen.Width +$screenHeight = $virtualScreen.Height +$offsetX = $virtualScreen.X +$offsetY = $virtualScreen.Y + +# 4. Prepare output file +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.mkv" + +# Construct the FFmpeg arguments targeting the full virtual desktop +$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p `"$outputFile`"" + +# 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown +$procInfo = New-Object System.Diagnostics.ProcessStartInfo +$procInfo.FileName = $FFmpegPath +$procInfo.Arguments = $ffmpegArgsStr +$procInfo.RedirectStandardInput = $true +$procInfo.UseShellExecute = $false +$procInfo.CreateNoWindow = $true + +$ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) + +try { + # 6. Block until FFmpeg exits naturally or the script is interrupted (logoff/session end) + while (-not $ffmpegProcess.HasExited) { Start-Sleep -Seconds 5 } - - # 4. Process is running, prepare to start recording - $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" - $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_$($TargetProcess)_$timestamp.mkv" - - # Construct the FFmpeg arguments - $ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p `"$outputFile`"" - - # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown - $procInfo = New-Object System.Diagnostics.ProcessStartInfo - $procInfo.FileName = $FFmpegPath - $procInfo.Arguments = $ffmpegArgsStr - $procInfo.RedirectStandardInput = $true - $procInfo.UseShellExecute = $false - $procInfo.CreateNoWindow = $true - - $ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) - - try { - # 6. Monitor process and wait for it to exit - while (Get-Process -Name $TargetProcess -ErrorAction SilentlyContinue) { - Start-Sleep -Seconds 5 - } - } - finally { - # 7. Applications closed cleanly (or script was interrupted), terminate FFmpeg gracefully - if ($null -ne $ffmpegProcess -and -not $ffmpegProcess.HasExited) { - # Send 'q' to gracefully stop recording so the file header formatting writes correctly - $ffmpegProcess.StandardInput.WriteLine("q") - - # Wait up to 10 seconds for it to write headers and close - $ffmpegProcess.WaitForExit(10000) | Out-Null - - # Fallback if the process stubbornly hung - if (-not $ffmpegProcess.HasExited) { - $ffmpegProcess.Kill() - } +} +finally { + # 7. Session ended or script was interrupted — terminate FFmpeg gracefully + if ($null -ne $ffmpegProcess -and -not $ffmpegProcess.HasExited) { + # Send 'q' to gracefully stop recording so the file header formatting writes correctly + $ffmpegProcess.StandardInput.WriteLine("q") + + # Wait up to 10 seconds for it to write headers and close + $ffmpegProcess.WaitForExit(10000) | Out-Null + + # Fallback if the process stubbornly hung + if (-not $ffmpegProcess.HasExited) { + $ffmpegProcess.Kill() } } - - # Loop continues back to wait for the process to be opened again } \ No newline at end of file diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 index 96006be..b864feb 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 +++ b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 @@ -13,10 +13,9 @@ $RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecordin # Define the parameters for this specific troubleshooting scenario using splatting $CaptureParameters = @{ - TargetUser = "dpurner" # The specific user encountering the issue - TargetProcess = "ikat" # The process name of the RemoteApp (without .exe) - OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved - MinFreeSpaceGB = 10 # Ensure at least 10GB of free space before caching + TargetUser = "dpurner" # The specific user encountering the issue + OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved + MinFreeSpaceGB = 10 # Ensure at least 10GB of free space before recording # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH } diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 new file mode 100644 index 0000000..20812e3 --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Runner script to launch the FFmpeg recording for a specific user and process. +.DESCRIPTION + This script wrapper is designed to be executed by Task Scheduler on the RemoteApp/RDS server. + It calls Start-AppRecording.ps1 with the predefined parameters for the iKAT troubleshooting scenario. +#> + +$ErrorActionPreference = 'Stop' + +# Determine the path to the main recording script located in the same directory +$RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecording.ps1" + +# Define the parameters for this specific troubleshooting scenario using splatting +$CaptureParameters = @{ + TargetUser = "jmaffiola" # The specific user encountering the issue + OutputDir = "C:\temp\RecordingsTest" # The directory where the .mkv files will be saved + MinFreeSpaceGB = 1 # Lower threshold for local testing + # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH +} + +if (-not (Test-Path $RecordingScript)) { + Write-Error "Could not find the recording script at: $RecordingScript" + Exit +} + +# Execute the recording script with the parameters +& $RecordingScript @CaptureParameters diff --git a/Scripts/iCat/Invoke-FFmpegCapture/plan.md b/Scripts/iCat/Invoke-FFmpegCapture/plan.md index 1c11501..f96e30e 100644 --- a/Scripts/iCat/Invoke-FFmpegCapture/plan.md +++ b/Scripts/iCat/Invoke-FFmpegCapture/plan.md @@ -4,7 +4,7 @@ Deploy an automated, background screen recording solution using FFmpeg to capture intermittent application errors. The solution is designed to be highly modular, accepting parameters for the target user, the target process (for RemoteApp environments), and the output directory. -**Initial Use Case:** Capturing errors for user "dpurner" (Dina) in the "ikat" application. +**Initial Use Case:** Capturing the full desktop session of user "dpurner" (Dina) to diagnose intermittent "ikat" application crashes. ## 2. Prerequisites @@ -19,21 +19,22 @@ Deploy an automated, background screen recording solution using FFmpeg to captur 2. Windows Task Scheduler detects the logon and launches a hidden PowerShell script wrapper (`Start-DinaRecording.ps1`), which splats and invokes the core logic (`Invoke-iKATRecording.ps1`). 3. The script verifies `$env:USERNAME` matches the targeted user. 4. The script checks for sufficient free disk space on the target drive to prevent storage exhaustion. -5. The script enters an outer loop, waiting for the specified application process to start. -6. Once the app is running, FFmpeg starts recording to an `.mkv` file. -7. The script monitors the application process. If the application is closed (or the script is forcibly stopped), it cleanly closes the FFmpeg recording via Standard Input (`"q"`) using a `try/finally` block, then loops back to waiting for the app. +5. FFmpeg starts recording immediately — capturing the entire virtual desktop (all monitors) for the duration of the session. +6. The script blocks until the session ends (logoff, disconnect, or Task Scheduler kill signal). +7. On termination, the `finally` block sends `"q"` to FFmpeg via Standard Input, gracefully flushing and finalizing the `.mkv` file. 8. A separate daily task runs a cleanup script, passing arguments for the directory and retention days. ## 4. Implementation Details (Current State) ### Core Recording Script (`Invoke-iKATRecording.ps1`) -* Uses a `param()` block for arguments: `$TargetUser`, `$TargetProcess`, `$OutputDir`, `$FFmpegPath`, and `$MinFreeSpaceGB`. +* Uses a `param()` block for arguments: `$TargetUser`, `$OutputDir`, `$FFmpegPath`, and `$MinFreeSpaceGB`. (`$TargetProcess` removed — recording is no longer app-triggered.) * Validates user identity and verifies free space on the destination drive before recording. -* Outer `while($true)` loop handles closing and reopening of the application within a single interactive session. +* Dynamically detects the full virtual desktop geometry using `[System.Windows.Forms.SystemInformation]::VirtualScreen` to capture all connected monitors at their native combined resolution. +* Starts FFmpeg immediately on logon — no waiting for a specific process. * Uses `.NET System.Diagnostics.ProcessStartInfo` instead of `Start-Process` to properly redirect Standard Input without opening visible console windows. -* FFmpeg argument string built explicitly for RemoteApps: `-f gdigrab -framerate 5 -offset_x 0 -offset_y 0 -video_size 1920x1080 -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p`. (Note: `-pix_fmt yuv420p` is critical so native Windows media players can decode the MKV). -* Wraps the active recording monitor (`while(Get-Process)`) in a `try { ... } finally { ... }` scope. Ensures that even if the script is interrupted (Ctrl+C, Task Scheduler kill), it cleanly flushes and safely saves the recording by sending `"q"` to FFmpeg. +* FFmpeg argument string: `-f gdigrab -framerate 5 -offset_x -offset_y -video_size x -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p`. (Note: `-pix_fmt yuv420p` is critical so native Windows media players can decode the MKV; geometry values are auto-detected at runtime.) +* Blocks using `while (-not $ffmpegProcess.HasExited)` and wraps in `try { ... } finally { ... }`. Ensures that on any termination (logoff, Task Scheduler kill, Ctrl+C), it sends `"q"` to FFmpeg to cleanly flush and finalize the recording. ### Cleanup Script (`Remove-ExpiredRecordings.ps1`) From 1ffcdbbf689a0afc468d560bafb97ef273734195 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Tue, 24 Mar 2026 15:29:59 -0500 Subject: [PATCH 16/33] test: Update Invoke-iKATRecording tests for session-based refactor --- Tests/iCat/Invoke-iKATRecording.Tests.ps1 | 38 ++++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/Tests/iCat/Invoke-iKATRecording.Tests.ps1 b/Tests/iCat/Invoke-iKATRecording.Tests.ps1 index 7a8b95b..484d692 100644 --- a/Tests/iCat/Invoke-iKATRecording.Tests.ps1 +++ b/Tests/iCat/Invoke-iKATRecording.Tests.ps1 @@ -7,27 +7,24 @@ Describe 'Invoke-iKATRecording.ps1' { It 'Should exit if current user does not match TargetUser' { Mock Write-Output {} - $currentUser = $env:USERNAME $fakeTargetUser = "FakeUser_$(New-Guid)" - & $sut -TargetUser $fakeTargetUser -TargetProcess "ikat" -OutputDir "C:\Temp" + & $sut -TargetUser $fakeTargetUser -OutputDir "C:\Temp" Assert-MockCalled Write-Output -Times 1 -ParameterFilter { $InputObject -match 'does not match target user' } } - It 'Should create output directory if it does not exist and wait for space' { - Mock Write-Output {} - # The script calls Get-CimInstance next, so throwing there serves as our loop break. - Mock Get-CimInstance { throw "BREAK_LOOP" } + It 'Should create output directory if it does not exist' { + Mock Write-Warning {} + # Return a drive with 0 free space so the script exits cleanly after + # directory creation but before attempting to launch FFmpeg. + Mock Get-CimInstance { + [PSCustomObject]@{ FreeSpace = 0 } + } $testDir = Join-Path -Path $env:TEMP -ChildPath "FFmpegTestRec_$(New-Guid)" - try { - & $sut -TargetUser $env:USERNAME -TargetProcess "ikat" -OutputDir $testDir - } - catch { - if ($_.Exception.Message -ne "BREAK_LOOP") { throw } - } + & $sut -TargetUser $env:USERNAME -OutputDir $testDir $dirExists = Test-Path $testDir $dirExists | Should -Be $true @@ -35,4 +32,21 @@ Describe 'Invoke-iKATRecording.ps1' { # Cleanup if ($dirExists) { Remove-Item $testDir -Force -Recurse } } + + It 'Should exit with a warning if disk space is below the minimum threshold' { + Mock Write-Warning {} + # Return 1 GB free, which is below the 10 GB default minimum. + Mock Get-CimInstance { + [PSCustomObject]@{ FreeSpace = 1GB } + } + + $testDir = Join-Path -Path $env:TEMP -ChildPath "FFmpegTestRec_$(New-Guid)" + New-Item -ItemType Directory -Path $testDir | Out-Null + + & $sut -TargetUser $env:USERNAME -OutputDir $testDir -MinFreeSpaceGB 10 + + Assert-MockCalled Write-Warning -Times 1 -ParameterFilter { $Message -match 'Not enough free space' } + + Remove-Item $testDir -Force -Recurse + } } From fd1c92264b3784821fecdcd5c22900c32ebe0c26 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Tue, 24 Mar 2026 15:35:06 -0500 Subject: [PATCH 17/33] feat: Add VBScript launchers for hidden Task Scheduler execution --- Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs | 1 + Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs | 1 + 2 files changed, 2 insertions(+) create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs create mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs new file mode 100644 index 0000000..da4d528 --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs @@ -0,0 +1 @@ +CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-DinaRecording.ps1""", 0, False diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs b/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs new file mode 100644 index 0000000..9ccb4c4 --- /dev/null +++ b/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs @@ -0,0 +1 @@ +CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-JoeyRecording.ps1""", 0, False From 42aa1f3bada61fc7a5b7a4f3076bde97f00779bf Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 13:26:24 -0500 Subject: [PATCH 18/33] Changed name from iCat to iKAT --- .../Invoke-iKATRecording.ps1 | 83 ----- .../Remove-ExpiredRecordings.ps1 | 30 -- .../Start-DinaRecording.ps1 | 28 -- .../Start-DinaRecording.vbs | 1 - .../Start-JoeyRecording.ps1 | 28 -- .../Start-JoeyRecording.vbs | 1 - .../Invoke-FFmpegCapture/Test-Recording.ps1 | 13 - Scripts/iCat/Invoke-FFmpegCapture/plan.md | 61 ---- Scripts/iCat/Invoke-FFmpegCapture/test.md | 87 ----- .../DateFormat/Get-UserDateFormats.ps1 | 242 -------------- .../DateFormat/Set-UserDateFormats.ps1 | 215 ------------- .../RegionFormat/Get-UserRegionFormats.ps1 | 292 ----------------- .../RegionFormat/Set-UserRegionFormats.ps1 | 297 ------------------ Scripts/iCat/Registry/RegionFormat/test.ps1 | 32 -- 14 files changed, 1410 deletions(-) delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/Test-Recording.ps1 delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/plan.md delete mode 100644 Scripts/iCat/Invoke-FFmpegCapture/test.md delete mode 100644 Scripts/iCat/Registry/DateFormat/Get-UserDateFormats.ps1 delete mode 100644 Scripts/iCat/Registry/DateFormat/Set-UserDateFormats.ps1 delete mode 100644 Scripts/iCat/Registry/RegionFormat/Get-UserRegionFormats.ps1 delete mode 100644 Scripts/iCat/Registry/RegionFormat/Set-UserRegionFormats.ps1 delete mode 100644 Scripts/iCat/Registry/RegionFormat/test.ps1 diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 deleted file mode 100644 index f51b6cf..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ /dev/null @@ -1,83 +0,0 @@ -param ( - [Parameter(Mandatory = $true)] - [string]$TargetUser, - - [Parameter(Mandatory = $true)] - [string]$OutputDir, - - [Parameter(Mandatory = $false)] - [int]$MinFreeSpaceGB = 10, - - [Parameter(Mandatory = $false)] - [string]$FFmpegPath = "ffmpeg.exe" -) - -# 1. Check if the current user matches the target user -if ($env:USERNAME -ne $TargetUser) { - Write-Output "Current user ($env:USERNAME) does not match target user ($TargetUser). Exiting." - Exit -} - -# Ensure the output directory exists -if (-not (Test-Path -Path $OutputDir)) { - New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null -} - -$driveLetter = (Get-Item $OutputDir).Root.Name - -# 2. Check for sufficient free disk space -$drive = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($driveLetter.Trim('\'))'" -if ($drive) { - $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) - if ($freeSpaceGB -lt $MinFreeSpaceGB) { - Write-Warning "Not enough free space on $driveLetter (Available: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB). Exiting." - Exit - } -} - -# 3. Detect the full virtual desktop dimensions to capture all monitors -Add-Type -AssemblyName System.Windows.Forms -$virtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen -$screenWidth = $virtualScreen.Width -$screenHeight = $virtualScreen.Height -$offsetX = $virtualScreen.X -$offsetY = $virtualScreen.Y - -# 4. Prepare output file -$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.mkv" - -# Construct the FFmpeg arguments targeting the full virtual desktop -$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p `"$outputFile`"" - -# 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown -$procInfo = New-Object System.Diagnostics.ProcessStartInfo -$procInfo.FileName = $FFmpegPath -$procInfo.Arguments = $ffmpegArgsStr -$procInfo.RedirectStandardInput = $true -$procInfo.UseShellExecute = $false -$procInfo.CreateNoWindow = $true - -$ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) - -try { - # 6. Block until FFmpeg exits naturally or the script is interrupted (logoff/session end) - while (-not $ffmpegProcess.HasExited) { - Start-Sleep -Seconds 5 - } -} -finally { - # 7. Session ended or script was interrupted — terminate FFmpeg gracefully - if ($null -ne $ffmpegProcess -and -not $ffmpegProcess.HasExited) { - # Send 'q' to gracefully stop recording so the file header formatting writes correctly - $ffmpegProcess.StandardInput.WriteLine("q") - - # Wait up to 10 seconds for it to write headers and close - $ffmpegProcess.WaitForExit(10000) | Out-Null - - # Fallback if the process stubbornly hung - if (-not $ffmpegProcess.HasExited) { - $ffmpegProcess.Kill() - } - } -} \ No newline at end of file diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 deleted file mode 100644 index 3b1c647..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -param ( - [Parameter(Mandatory = $true)] - [string]$OutputDir, - - [Parameter(Mandatory = $true)] - [int]$DaysToKeep -) - -if (-not (Test-Path -Path $OutputDir)) { - Write-Warning "Directory '$OutputDir' does not exist. Exiting." - Exit -} - -$logFile = Join-Path -Path $OutputDir -ChildPath "CleanupLog.txt" -$cutoffDate = (Get-Date).AddDays(-$DaysToKeep) - -# Scan for .mkv files older than the retention limit -$expiredFiles = Get-ChildItem -Path $OutputDir -Filter "*.mkv" -File | Where-Object { $_.LastWriteTime -lt $cutoffDate } - -foreach ($file in $expiredFiles) { - try { - Remove-Item -Path $file.FullName -Force -ErrorAction Stop - $logMessage = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Successfully deleted: $($file.Name)" - Add-Content -Path $logFile -Value $logMessage - } - catch { - $logMessage = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Error deleting $($file.Name): $($_.Exception.Message)" - Add-Content -Path $logFile -Value $logMessage - } -} \ No newline at end of file diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 deleted file mode 100644 index b864feb..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -<# -.SYNOPSIS - Runner script to launch the FFmpeg recording for a specific user and process. -.DESCRIPTION - This script wrapper is designed to be executed by Task Scheduler on the RemoteApp/RDS server. - It calls Start-AppRecording.ps1 with the predefined parameters for the iKAT troubleshooting scenario. -#> - -$ErrorActionPreference = 'Stop' - -# Determine the path to the main recording script located in the same directory -$RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecording.ps1" - -# Define the parameters for this specific troubleshooting scenario using splatting -$CaptureParameters = @{ - TargetUser = "dpurner" # The specific user encountering the issue - OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved - MinFreeSpaceGB = 10 # Ensure at least 10GB of free space before recording - # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH -} - -if (-not (Test-Path $RecordingScript)) { - Write-Error "Could not find the recording script at: $RecordingScript" - Exit -} - -# Execute the recording script with the parameters -& $RecordingScript @CaptureParameters diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs b/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs deleted file mode 100644 index da4d528..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/Start-DinaRecording.vbs +++ /dev/null @@ -1 +0,0 @@ -CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-DinaRecording.ps1""", 0, False diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 deleted file mode 100644 index 20812e3..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -<# -.SYNOPSIS - Runner script to launch the FFmpeg recording for a specific user and process. -.DESCRIPTION - This script wrapper is designed to be executed by Task Scheduler on the RemoteApp/RDS server. - It calls Start-AppRecording.ps1 with the predefined parameters for the iKAT troubleshooting scenario. -#> - -$ErrorActionPreference = 'Stop' - -# Determine the path to the main recording script located in the same directory -$RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecording.ps1" - -# Define the parameters for this specific troubleshooting scenario using splatting -$CaptureParameters = @{ - TargetUser = "jmaffiola" # The specific user encountering the issue - OutputDir = "C:\temp\RecordingsTest" # The directory where the .mkv files will be saved - MinFreeSpaceGB = 1 # Lower threshold for local testing - # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH -} - -if (-not (Test-Path $RecordingScript)) { - Write-Error "Could not find the recording script at: $RecordingScript" - Exit -} - -# Execute the recording script with the parameters -& $RecordingScript @CaptureParameters diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs b/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs deleted file mode 100644 index 9ccb4c4..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/Start-JoeyRecording.vbs +++ /dev/null @@ -1 +0,0 @@ -CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-JoeyRecording.ps1""", 0, False diff --git a/Scripts/iCat/Invoke-FFmpegCapture/Test-Recording.ps1 b/Scripts/iCat/Invoke-FFmpegCapture/Test-Recording.ps1 deleted file mode 100644 index edc4480..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/Test-Recording.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -$TestCaptureParameters = @{ - TargetUser = $env:USERNAME # Logs you instead of Dina - TargetProcess = "notepad" # Monitors for Notepad - OutputDir = "C:\Temp\RecordingsTest" - MinFreeSpaceGB = 1 - #FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # <--- Update path here if needed! -} - -# Ensure the core script path is correct -$scriptPath = "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" - -# Execute the core script -& $scriptPath @TestCaptureParameters \ No newline at end of file diff --git a/Scripts/iCat/Invoke-FFmpegCapture/plan.md b/Scripts/iCat/Invoke-FFmpegCapture/plan.md deleted file mode 100644 index f96e30e..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/plan.md +++ /dev/null @@ -1,61 +0,0 @@ -# Project Plan: Modular Automated RemoteApp Session Recording via FFmpeg - -## 1. Project Overview - -Deploy an automated, background screen recording solution using FFmpeg to capture intermittent application errors. The solution is designed to be highly modular, accepting parameters for the target user, the target process (for RemoteApp environments), and the output directory. - -**Initial Use Case:** Capturing the full desktop session of user "dpurner" (Dina) to diagnose intermittent "ikat" application crashes. - -## 2. Prerequisites - -* **FFmpeg**: Download the Windows executable (`ffmpeg.exe`) and place it in a secure, accessible directory (e.g., `C:\Scripts\FFmpeg\`). -* **Storage Location**: [Pending Developer Input] (Passed as a parameter). -* **Retention Policy**: [Pending Developer Input] (Passed as a parameter). -* **Permissions**: Administrative access to the target Remote Desktop Session Host. - -## 3. Workflow - -1. User establishes an RDP/RemoteApp connection. -2. Windows Task Scheduler detects the logon and launches a hidden PowerShell script wrapper (`Start-DinaRecording.ps1`), which splats and invokes the core logic (`Invoke-iKATRecording.ps1`). -3. The script verifies `$env:USERNAME` matches the targeted user. -4. The script checks for sufficient free disk space on the target drive to prevent storage exhaustion. -5. FFmpeg starts recording immediately — capturing the entire virtual desktop (all monitors) for the duration of the session. -6. The script blocks until the session ends (logoff, disconnect, or Task Scheduler kill signal). -7. On termination, the `finally` block sends `"q"` to FFmpeg via Standard Input, gracefully flushing and finalizing the `.mkv` file. -8. A separate daily task runs a cleanup script, passing arguments for the directory and retention days. - -## 4. Implementation Details (Current State) - -### Core Recording Script (`Invoke-iKATRecording.ps1`) - -* Uses a `param()` block for arguments: `$TargetUser`, `$OutputDir`, `$FFmpegPath`, and `$MinFreeSpaceGB`. (`$TargetProcess` removed — recording is no longer app-triggered.) -* Validates user identity and verifies free space on the destination drive before recording. -* Dynamically detects the full virtual desktop geometry using `[System.Windows.Forms.SystemInformation]::VirtualScreen` to capture all connected monitors at their native combined resolution. -* Starts FFmpeg immediately on logon — no waiting for a specific process. -* Uses `.NET System.Diagnostics.ProcessStartInfo` instead of `Start-Process` to properly redirect Standard Input without opening visible console windows. -* FFmpeg argument string: `-f gdigrab -framerate 5 -offset_x -offset_y -video_size x -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p`. (Note: `-pix_fmt yuv420p` is critical so native Windows media players can decode the MKV; geometry values are auto-detected at runtime.) -* Blocks using `while (-not $ffmpegProcess.HasExited)` and wraps in `try { ... } finally { ... }`. Ensures that on any termination (logoff, Task Scheduler kill, Ctrl+C), it sends `"q"` to FFmpeg to cleanly flush and finalize the recording. - -### Cleanup Script (`Remove-ExpiredRecordings.ps1`) - -* Scans `$OutputDir` for `.mkv` files with a `LastWriteTime` older than the specified `$DaysToKeep`. -* Configurable output directory and retention to prevent out-of-storage issues on the terminal servers. - -### Testing & Verification (`test.md` & `Test-Recording.ps1`) - -* Contains Pester tests to validate free space logic and correct recording initialization parameters (`Invoke-iKATRecording.Tests.ps1`). -* Local sandbox script (`Test-Recording.ps1`) runs the process against dummy apps like `mspaint` or `calc` to prevent UWP suspension mechanics (like `notepad` on Win11) from stalling the process closure loop. - -## 5. Deployment Configuration (Task Scheduler) - -Instructions for creating the tasks: - -* **Task A (Recording)**: - * Security Options: "Run only when user is logged on" (Allows it to capture the graphical desktop session). - * Trigger: "At log on" for specific user "dpurner". - * Action: `powershell.exe` - * Arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Scripts\Invoke-FFmpegCapture\Start-DinaRecording.ps1"` -* **Task B (Cleanup)**: - * Trigger: Daily at an off-peak time (e.g., 2:00 AM). - * Action: `powershell.exe` - * Arguments: `-ExecutionPolicy Bypass -File "C:\Scripts\Invoke-FFmpegCapture\Remove-ExpiredRecordings.ps1" -OutputDir "C:\Recordings" -DaysToKeep 5` diff --git a/Scripts/iCat/Invoke-FFmpegCapture/test.md b/Scripts/iCat/Invoke-FFmpegCapture/test.md deleted file mode 100644 index 14666b4..0000000 --- a/Scripts/iCat/Invoke-FFmpegCapture/test.md +++ /dev/null @@ -1,87 +0,0 @@ -# Testing FFmpeg Automated Screen Capture - -This guide outlines how to safely test the automated FFmpeg screen recording solution. These tests should **first be run on your own local PC** to verify the logic, and then **optionally on a test server** before final deployment to the live RDS environment. - -## Prerequisite: Download FFmpeg - -Since FFmpeg is not natively installed on Windows, you will need to place the executable on your machine. - -1. Download a pre-compiled Windows build of `ffmpeg.exe` (e.g., from gyan.dev or BtbN). -2. Extract the archive and copy `ffmpeg.exe` to a permanent location, such as `C:\Scripts\FFmpeg\ffmpeg.exe`. - ---- - -## 1. Local Simulation Test (Recommended First Step) - -Before messing with Task Scheduler, you can perform an active test in your own console. We will temporarily use **Notepad** instead of iKAT to simulate the process. - -**Steps:** - -1. Open PowerShell as an Administrator. -2. Copy and paste the following snippet into your console. *Make sure to update the `$FFmpegPath` and `$OutputDir` if yours differ.* - -```powershell -$TestCaptureParameters = @{ - TargetUser = $env:USERNAME # Logs you instead of Dina - TargetProcess = "notepad" # Monitors for Notepad - OutputDir = "C:\Temp\RecordingsTest" - MinFreeSpaceGB = 1 - FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # <--- Update path here if needed! -} - -# Ensure the core script path is correct -$scriptPath = "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" - -# Execute the core script -& $scriptPath @TestCaptureParameters -``` - -1. The console will appear to hang. This is intentional; it is locked in the loop waiting for Notepad to start. -2. **Open Notepad**. Type some text and move the window around for 10-15 seconds to simulate user activity. -3. **Close Notepad.** This will trigger the script to stop recording, save the file, and loop back to start waiting again. -4. Open your target directory (`C:\Temp\RecordingsTest`) to verify the file was created. -5. Play the `.mkv` file to confirm your desktop and the Notepad activity were captured successfully. -6. Go back to your PowerShell console and press **`Ctrl + C`** to break the continuous loop and exit the script. - ---- - -## 2. Testing the Task Scheduler Deployment - -Once the script logic is confirmed working, the next step is verifying Task Scheduler triggers it silently and correctly. - -### Modifying the Runner Script for Testing - -Open `Start-DinaRecording.ps1` and temporarily change the parameters so you can trigger it as yourself using Notepad: - -```powershell - TargetUser = "YOUR_USERNAME_HERE" # Change from 'dpurner' - TargetProcess = "notepad" # Change from 'ikat' -``` - -*Don't forget to save the file.* - -### Creating the Task Scheduler Entry - -1. Open **Task Scheduler** on your test machine. -2. Right-click Task Scheduler Library -> **Create Task...** (Do not use Basic Task). -3. **General Tab:** - * Name: `Test-FFmpegCapture` - * Under Security Options, select **Run only when user is logged on**. *(Crucial Note: If "Run whether user is logged on or not" is selected, the task runs in Session 0 and the resulting video will be completely black/blank).* -4. **Triggers Tab:** - * Click **New...** - * Change "Begin the task:" to **At log on** - * Specify **Any user**. -5. **Actions Tab:** - * Click **New...** - * Action: **Start a program** - * Program/script: `powershell.exe` - * Add arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-DinaRecording.ps1"` - -### Running the Live Test - -1. You can either log off and log back on to trigger it naturally, or simply right-click your new task and select **Run**. -2. No hidden windows should appear. -3. Open **Notepad**, type some text, and then close it. -4. Check your `C:\Recordings` (or modified output directory) to verify the `.mkv` was created successfully in the background. - -*Important: Remember to revert the parameters inside `Start-DinaRecording.ps1` back to `dpurner` and `ikat` before pushing to production!* diff --git a/Scripts/iCat/Registry/DateFormat/Get-UserDateFormats.ps1 b/Scripts/iCat/Registry/DateFormat/Get-UserDateFormats.ps1 deleted file mode 100644 index 9416795..0000000 --- a/Scripts/iCat/Registry/DateFormat/Get-UserDateFormats.ps1 +++ /dev/null @@ -1,242 +0,0 @@ -#Requires -Version 5.1 - -<# -.SYNOPSIS - Retrieves date format information for all user profiles on remote servers. - -.DESCRIPTION - This script connects to specified remote servers and gathers date format information - for all user profiles. It retrieves both user-specific and system-wide regional settings - and exports the data to a CSV file. - -.PARAMETER Servers - Array of server IP addresses or hostnames to query. - -.PARAMETER OutputPath - Path where the CSV output file will be saved. - -.PARAMETER TargetUsers - Array of specific usernames to collect data for. If specified, only these users will be processed. - -.PARAMETER UserListFile - Path to a file containing usernames to collect data for. Supports both .txt (one username per line) and .csv formats. For CSV files, the script will auto-detect the username column. - -.PARAMETER Credential - PSCredential object for remote server authentication. If not provided, the script will attempt to retrieve credentials from 1Password environment variables, or prompt the user. - -.EXAMPLE - .\Get-UserDateFormats.ps1 - Runs the script with default servers and prompts for output location. - -.EXAMPLE - .\Get-UserDateFormats.ps1 -TargetUsers @('jsmith', 'admin', 'temp') -OutputPath "C:\temp\specific_users.csv" - Collects date format data only for specific users: jsmith, admin, and temp. - -.EXAMPLE - .\Get-UserDateFormats.ps1 -UserListFile "C:\temp\users.txt" -OutputPath "C:\temp\filtered_users.csv" - Collects data for users listed in the specified file. - -.EXAMPLE - .\Get-UserDateFormats.ps1 -UserListFile "C:\temp\users.csv" -OutputPath "C:\temp\csv_users.csv" - Collects data for users listed in a CSV file (auto-detects username column). - -.NOTES - Author: PowerShell Script Generator - Created: June 17, 2025 - Requires: PowerShell 5.1 or higher, Remote PowerShell access to target servers - - User Filtering: - - If TargetUsers is specified, only those users will be processed - - If UserListFile is specified, users will be read from the file (supports .txt and .csv formats) - - If both are specified, TargetUsers takes precedence - - If neither is specified, all users will be processed (original behavior) -#> - -param( - [string[]]$Servers = @('10.210.3.23'), - [string]$OutputPath = '', - [string[]]$TargetUsers = @(), - [string]$UserListFile = '', - [PSCredential]$Credential -) - -# Import the RegistryUtils module -try { - $modulePath = Join-Path $PSScriptRoot '..\..\Modules\RegistryUtils\RegistryUtils.psd1' - Import-Module $modulePath -Force -ErrorAction Stop - Write-Host '✅ RegistryUtils module loaded successfully' -ForegroundColor Green -} -catch { - Write-Error "Failed to load RegistryUtils module: $($_.Exception.Message)" - Write-Host "Make sure the module is in the correct location: $modulePath" -ForegroundColor Yellow - exit 1 -} - - -# Main script execution -try { - # Get credentials - use provided credential or attempt to retrieve from 1Password - if (-not $Credential) { - Write-Host 'Retrieving administrator credentials from 1Password...' -ForegroundColor Cyan - $credentials = Get-AdminCredential -ErrorAction SilentlyContinue - - if (-not $credentials) { - Write-Host '⚠️ 1Password credentials not available. Please provide credentials manually.' -ForegroundColor Yellow - $credentials = Get-AdminCredentials - if (-not $credentials) { - Write-Error 'No credentials provided. Exiting script.' - exit 1 - } - } - else { - Write-Host '✅ Successfully retrieved credentials from 1Password' -ForegroundColor Green - } - } - else { - Write-Host '✅ Using provided credentials' -ForegroundColor Green - $credentials = $Credential - } - - # Prepare target users list - $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - Write-Host "`n📋 User Filtering Enabled:" -ForegroundColor Cyan - if ($targetUsersList.Count -le 10) { - Write-Host " Target users: $($targetUsersList -join ', ')" -ForegroundColor Yellow - } - else { - Write-Host " Target users: $($targetUsersList[0..4] -join ', '), ... and $($targetUsersList.Count - 5) more" -ForegroundColor Yellow - } - Write-Host " Total target users: $($targetUsersList.Count)" -ForegroundColor Yellow - } - else { - Write-Host "`n📋 User Filtering: Disabled (all users will be processed)" -ForegroundColor Gray - } - - # Always set output path to the specified directory and filename format - $outputDir = Join-Path -Path $env:USERPROFILE -ChildPath 'Documents\Scripts\PowerShellScripts\Output\DateFormats' - $dateTimeString = Get-Date -Format 'yyyyMMdd_HHmmss' - $OutputPath = Join-Path $outputDir "Get-UserDateFormat_Output_${dateTimeString}.csv" - Write-Host "Output will be saved to: $OutputPath" -ForegroundColor Yellow - - # Ensure output directory exists - $outputDir = Split-Path $OutputPath -Parent - if (-not (Test-Path $outputDir)) { - New-Item -Path $outputDir -ItemType Directory -Force | Out-Null - } - - # Initialize results array - $allResults = @() - - # Get target users list - $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile - - # Process each server - foreach ($server in $Servers) { - Write-Host "`n$('='*50)" -ForegroundColor Cyan - Write-Host "Processing server: $server" -ForegroundColor Cyan - Write-Host "$('='*50)" -ForegroundColor Cyan - - # Test connectivity - if (-not (Test-ServerConnectivity -ServerName $server -Credential $credentials)) { - # Add error entry for unreachable server - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - # Add error entries for each target user - foreach ($targetUser in $targetUsersList) { - $errorResult = [PSCustomObject]@{ - Server = $server - Username = $targetUser - SystemShortDateFormat = 'N/A' - ErrorMessage = 'Server unreachable or WinRM not available' - } - $allResults += $errorResult - } - } - else { - $errorResult = [PSCustomObject]@{ - Server = $server - Username = 'SERVER_UNREACHABLE' - SystemShortDateFormat = 'N/A' - ErrorMessage = 'Server unreachable or WinRM not available' - } - $allResults += $errorResult - } - continue - } - - # Get list of users to process for this server - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - # Use the predefined target users list - $usersToProcess = $targetUsersList - Write-Host "Processing $($usersToProcess.Count) users from user list..." -ForegroundColor Yellow - } - else { - # Get all user profiles from server - Write-Host 'Retrieving user profiles...' -ForegroundColor Yellow - $userProfiles = Get-RemoteUserProfiles -ServerName $server -Credential $credentials - - if ($userProfiles.Count -eq 0) { - Write-Warning "No user profiles found on $server - this may be due to PowerShell remoting permission issues" - $errorResult = [PSCustomObject]@{ - Server = $server - Username = 'REMOTING_ACCESS_DENIED' - SystemShortDateFormat = 'N/A' - ErrorMessage = 'PowerShell remoting access denied - check session configurations and permissions' - } - $allResults += $errorResult - continue - } - - # Extract usernames from profiles - $usersToProcess = $userProfiles | ForEach-Object { $_.Username } - Write-Host "Found $($usersToProcess.Count) user profiles on $server" -ForegroundColor Green - } - - # Process each user (regardless of source) - foreach ($username in $usersToProcess) { - Write-Host " Processing user: $username" -ForegroundColor White - - $userResult = Get-UserSystemShortDateFormat -ServerName $server -Username $username -Credential $credentials - if ($userResult) { - $result = [PSCustomObject]@{ - Server = $server - Username = $userResult.Username - SystemShortDateFormat = $userResult.SystemShortDateFormat - ErrorMessage = $userResult.ErrorMessage - } - $allResults += $result - - if ($userResult.UserExists) { - Write-Host ' [SUCCESS] Successfully retrieved SystemShortDateFormat' -ForegroundColor Green - } - else { - Write-Host ' [WARNING] User not found on server' -ForegroundColor Yellow - } - } - else { - Write-Host ' [ERROR] Failed to retrieve date format' -ForegroundColor Red - } - } - } - - # Display results in console and export to CSV - if ($allResults.Count -gt 0) { - # Export to CSV using the module function - $csvPath = Export-UserDateFormatCsv -Results $allResults -OutputPath $OutputPath - - # Display comprehensive summary using the module function - Show-UserDateFormatSummary -Results $allResults -OperationType 'Get' -CsvPath $csvPath - - # Display results using the module function - Show-UserDateFormatTable -Results $allResults - } - else { - Write-Warning 'No data was collected. Please check server connectivity and credentials.' - } -} -catch { - Write-Error "Script execution failed: $($_.Exception.Message)" - exit 1 -} - -Write-Host "`nScript completed." -ForegroundColor Green \ No newline at end of file diff --git a/Scripts/iCat/Registry/DateFormat/Set-UserDateFormats.ps1 b/Scripts/iCat/Registry/DateFormat/Set-UserDateFormats.ps1 deleted file mode 100644 index 6fecf1e..0000000 --- a/Scripts/iCat/Registry/DateFormat/Set-UserDateFormats.ps1 +++ /dev/null @@ -1,215 +0,0 @@ -#Requires -Version 5.1 - -<# -.SYNOPSIS - Sets the system short date format for user profiles on remote servers. - -.DESCRIPTION - This script connects to specified remote servers and sets the system short date format - for user profiles. It supports user filtering via parameters or user list files, and - uses the RegistryUtils module for all helper functions and remoting logic. - -.PARAMETER Servers - Array of server IP addresses or hostnames to target. - -.PARAMETER TargetUsers - Array of specific usernames to set the date format for. If specified, only these users will be processed. - -.PARAMETER UserListFile - Path to a file containing usernames to process. Supports both .txt (one username per line) and .csv formats. - -.PARAMETER DateFormat - The short date format string to set (e.g., 'MM/dd/yyyy'). - -.PARAMETER Credential - PSCredential object for remote server authentication. If not provided, the script will attempt to retrieve credentials from 1Password environment variables, or prompt the user. - -.EXAMPLE - .\Set-UserDateFormats.ps1 -DateFormat 'yyyy-MM-dd' - Sets the short date format for all users on the default server(s). - -.EXAMPLE - .\Set-UserDateFormats.ps1 -TargetUsers @('jsmith','admin') -DateFormat 'dd/MM/yyyy' - Sets the short date format for specific users. - -.EXAMPLE - .\Set-UserDateFormats.ps1 -UserListFile "C:\temp\users.txt" -DateFormat 'MM-dd-yyyy' - Sets the short date format for users listed in a file. - -.NOTES - Author: PowerShell Script Generator - Created: July 14, 2025 - Requires: PowerShell 5.1 or higher, Remote PowerShell access to target servers - Relies on RegistryUtils module for all helper functions. -#> - -param( - [string[]]$Servers = @('10.210.3.23'), - [string[]]$TargetUsers = @(), - [string]$UserListFile = '', - [Parameter(Mandatory = $true)] - [string]$DateFormat, - [PSCredential]$Credential -) - -# Import the RegistryUtils module -try { - $modulePath = Join-Path $PSScriptRoot '..\..\Modules\RegistryUtils\RegistryUtils.psd1' - Import-Module $modulePath -Force -ErrorAction Stop - Write-Host '✅ RegistryUtils module loaded successfully' -ForegroundColor Green -} -catch { - Write-Error "Failed to load RegistryUtils module: $($_.Exception.Message)" - Write-Host "Make sure the module is in the correct location: $modulePath" -ForegroundColor Yellow - exit 1 -} - -# Main script execution -try { - # Get credentials - use provided credential or attempt to retrieve from 1Password - if (-not $Credential) { - Write-Host 'Retrieving administrator credentials from 1Password...' -ForegroundColor Cyan - $credentials = Get-AdminCredential -ErrorAction SilentlyContinue - - if (-not $credentials) { - Write-Host '⚠️ 1Password credentials not available. Please provide credentials manually.' -ForegroundColor Yellow - $credentials = Get-AdminCredentials - if (-not $credentials) { - Write-Error 'No credentials provided. Exiting script.' - exit 1 - } - } - else { - Write-Host '✅ Successfully retrieved credentials from 1Password' -ForegroundColor Green - } - } - else { - Write-Host '✅ Using provided credentials' -ForegroundColor Green - $credentials = $Credential - } - - # Prepare target users list - $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - Write-Host "`n📋 User Filtering Enabled:" -ForegroundColor Cyan - if ($targetUsersList.Count -le 10) { - Write-Host " Target users: $($targetUsersList -join ', ')" -ForegroundColor Yellow - } - else { - Write-Host " Target users: $($targetUsersList[0..4] -join ', '), ... and $($targetUsersList.Count - 5) more" -ForegroundColor Yellow - } - Write-Host " Total target users: $($targetUsersList.Count)" -ForegroundColor Yellow - } - else { - Write-Host "`n📋 User Filtering: Disabled (all users will be processed)" -ForegroundColor Gray - } - - # Initialize results array - $allResults = @() - - # Process each server - foreach ($server in $Servers) { - Write-Host "`n$('='*50)" -ForegroundColor Cyan - Write-Host "Processing server: $server" -ForegroundColor Cyan - Write-Host "$('='*50)" -ForegroundColor Cyan - - # Test connectivity - if (-not (Test-ServerConnectivity -ServerName $server -Credential $credentials)) { - # Add error entry for unreachable server - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - foreach ($targetUser in $targetUsersList) { - $errorResult = [PSCustomObject]@{ - Server = $server - Username = $targetUser - SetDateFormatResult = 'N/A' - ErrorMessage = 'Server unreachable or WinRM not available' - } - $allResults += $errorResult - } - } - else { - $errorResult = [PSCustomObject]@{ - Server = $server - Username = 'SERVER_UNREACHABLE' - SetDateFormatResult = 'N/A' - ErrorMessage = 'Server unreachable or WinRM not available' - } - $allResults += $errorResult - } - continue - } - - # Get list of users to process for this server - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - # Use the predefined target users list - $usersToProcess = $targetUsersList - Write-Host "Processing $($usersToProcess.Count) users from user list..." -ForegroundColor Yellow - } - else { - # Get all user profiles from server - Write-Host 'Retrieving user profiles...' -ForegroundColor Yellow - $userProfiles = Get-RemoteUserProfiles -ServerName $server -Credential $credentials - - if ($userProfiles.Count -eq 0) { - Write-Warning "No user profiles found on $server - this may be due to PowerShell remoting permission issues" - $errorResult = [PSCustomObject]@{ - Server = $server - Username = 'REMOTING_ACCESS_DENIED' - SetDateFormatResult = 'N/A' - ErrorMessage = 'PowerShell remoting access denied - check session configurations and permissions' - } - $allResults += $errorResult - continue - } - - # Extract usernames from profiles - $usersToProcess = $userProfiles | ForEach-Object { $_.Username } - Write-Host "Found $($usersToProcess.Count) user profiles on $server" -ForegroundColor Green - } - - # Process each user (regardless of source) - foreach ($username in $usersToProcess) { - Write-Host " Processing user: $username" -ForegroundColor White - $setResult = Set-RemoteUserDateFormat -ServerName $server -Username $username -DateFormat $DateFormat -Credential $credentials - if ($setResult) { - $result = [PSCustomObject]@{ - Server = $server - Username = $setResult.Username - SetDateFormatResult = if ($setResult.Success) { "✓ $($setResult.NewDateFormat)" } else { '✗ Failed' } - ErrorMessage = if (-not $setResult.Success) { $setResult.Message } else { $null } - } - $allResults += $result - if ($setResult.Success) { - Write-Host ' [SUCCESS] Date format set successfully' -ForegroundColor Green - } - else { - Write-Host " [WARNING] $($setResult.Message)" -ForegroundColor Yellow - } - } - else { - Write-Host ' [ERROR] Failed to set date format' -ForegroundColor Red - } - } - } - - # Display results in console and export to CSV - if ($allResults.Count -gt 0) { - # Export to CSV using the module function - $csvPath = Export-UserDateFormatCsv -Results $allResults -FilePrefix 'Set-UserDateFormat_Output' - - # Display comprehensive summary using the module function - Show-UserDateFormatSummary -Results $allResults -OperationType 'Set' -CsvPath $csvPath - - # Display results using the module function - Show-UserDateFormatTable -Results $allResults -OperationType 'Set' -Title 'DATE FORMAT UPDATE RESULTS' - } - else { - Write-Warning 'No data was processed. Please check server connectivity and credentials.' - } -} -catch { - Write-Error "Script execution failed: $($_.Exception.Message)" - exit 1 -} - -Write-Host "`nScript completed." -ForegroundColor Green diff --git a/Scripts/iCat/Registry/RegionFormat/Get-UserRegionFormats.ps1 b/Scripts/iCat/Registry/RegionFormat/Get-UserRegionFormats.ps1 deleted file mode 100644 index 49696d1..0000000 --- a/Scripts/iCat/Registry/RegionFormat/Get-UserRegionFormats.ps1 +++ /dev/null @@ -1,292 +0,0 @@ -#Requires -Version 5.1 - -<# -.SYNOPSIS - Retrieves region format information for all user profiles on remote servers. - -.DESCRIPTION - This script connects to specified remote servers and gathers region format information - (LocaleName registry value) for all user profiles. It retrieves the Windows Settings - "Region format" setting and exports the data to a CSV file. - -.PARAMETER Servers - Array of server IP addresses or hostnames to query. - -.PARAMETER OutputPath - Path where the CSV output file will be saved. - -.PARAMETER TargetUsers - Array of specific usernames to collect data for. If specified, only these users will be processed. - -.PARAMETER UserListFile - Path to a file containing usernames to collect data for. Supports both .txt (one username per line) and .csv formats. For CSV files, the script will auto-detect the username column. - -.PARAMETER Credential - PSCredential object for remote server authentication. If not provided, the script will attempt to retrieve credentials from 1Password environment variables, or prompt the user. - -.EXAMPLE - .\Get-UserRegionFormats.ps1 - Runs the script with default servers and prompts for output location. - -.EXAMPLE - .\Get-UserRegionFormats.ps1 -TargetUsers @('jsmith', 'admin', 'temp') -OutputPath "C:\temp\specific_users.csv" - Collects region format data only for specific users: jsmith, admin, and temp. - -.EXAMPLE - .\Get-UserRegionFormats.ps1 -TargetUsers @('Default') -OutputPath "C:\temp\default_user.csv" - Retrieves the Default User region format setting. - -.EXAMPLE - .\Get-UserRegionFormats.ps1 -UserListFile "C:\temp\users.txt" -OutputPath "C:\temp\filtered_users.csv" - Collects data for users listed in the specified file. - -.EXAMPLE - .\Get-UserRegionFormats.ps1 -UserListFile "C:\temp\users.csv" -OutputPath "C:\temp\csv_users.csv" - Collects data for users listed in a CSV file (auto-detects username column). - -.NOTES - Author: PowerShell Script Generator - Created: November 21, 2025 - Requires: PowerShell 5.1 or higher, Remote PowerShell access to target servers - - User Filtering: - - If TargetUsers is specified, only those users will be processed - - If UserListFile is specified, users will be read from the file (supports .txt and .csv formats) - - If both are specified, TargetUsers takes precedence - - If neither is specified, all users will be processed (original behavior) -#> - -param( - [string[]]$Servers = @('10.210.3.23'), - [string]$OutputPath = '', - [string[]]$TargetUsers = @(), - [string]$UserListFile = '', - [PSCredential]$Credential -) - -# Import the RegistryUtils module -try { - # Handle both normal execution ($PSScriptRoot) and dot-sourcing ($MyInvocation.MyCommand.Path) - $scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } - # Navigate from Scripts/Registry/RegionFormat up to project root, then into Modules - $modulePath = Join-Path -Path $scriptDir -ChildPath '..\..\..\Modules\RegistryUtils\RegistryUtils.psd1' - $modulePath = Resolve-Path $modulePath -ErrorAction Stop # Normalize the path - Import-Module $modulePath -Force -ErrorAction Stop - Write-Host '✅ RegistryUtils module loaded successfully' -ForegroundColor Green -} -catch { - Write-Error "Failed to load RegistryUtils module: $($_.Exception.Message)" - Write-Host "Make sure the module is in the correct location: $modulePath" -ForegroundColor Yellow - exit 1 -} - - -# Main script execution -try { - # Get credentials - use provided credential or attempt to retrieve from 1Password - if (-not $Credential) { - Write-Host 'Retrieving administrator credentials from 1Password...' -ForegroundColor Cyan - $credentials = Get-AdminCredential -ErrorAction SilentlyContinue - - if (-not $credentials) { - Write-Host '⚠️ 1Password credentials not available. Please provide credentials manually.' -ForegroundColor Yellow - $credentials = Get-AdminCredentials - if (-not $credentials) { - Write-Error 'No credentials provided. Exiting script.' - exit 1 - } - } - else { - Write-Host '✅ Successfully retrieved credentials from 1Password' -ForegroundColor Green - } - } - else { - Write-Host '✅ Using provided credentials' -ForegroundColor Green - $credentials = $Credential - } - - # Prepare target users list - $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - Write-Host "`n📋 User Filtering Enabled:" -ForegroundColor Cyan - if ($targetUsersList.Count -le 10) { - Write-Host " Target users: $($targetUsersList -join ', ')" -ForegroundColor Yellow - } - else { - Write-Host " Target users: $($targetUsersList[0..4] -join ', '), ... and $($targetUsersList.Count - 5) more" -ForegroundColor Yellow - } - Write-Host " Total target users: $($targetUsersList.Count)" -ForegroundColor Yellow - } - else { - Write-Host "`n📋 User Filtering: Disabled (all users will be processed)" -ForegroundColor Gray - } - - # Always set output path to the specified directory and filename format - $outputDir = Join-Path -Path $env:USERPROFILE -ChildPath 'Documents\Scripts\PowerShellScripts\Output\RegionFormats' - $dateTimeString = Get-Date -Format 'yyyyMMdd_HHmmss' - $OutputPath = Join-Path $outputDir "Get-UserRegionFormat_Output_${dateTimeString}.csv" - Write-Host "Output will be saved to: $OutputPath" -ForegroundColor Yellow - - # Ensure output directory exists - $outputDir = Split-Path $OutputPath -Parent - if (-not (Test-Path $outputDir)) { - New-Item -Path $outputDir -ItemType Directory -Force | Out-Null - } - - # Initialize results array - $allResults = @() - - # Get target users list - $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile - - # Process each server - foreach ($server in $Servers) { - Write-Host "`n$('='*50)" -ForegroundColor Cyan - Write-Host "Processing server: $server" -ForegroundColor Cyan - Write-Host "$('='*50)" -ForegroundColor Cyan - - # Test connectivity - if (-not (Test-ServerConnectivity -ServerName $server -Credential $credentials)) { - # Add error entry for unreachable server - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - # Add error entries for each target user - foreach ($targetUser in $targetUsersList) { - $errorResult = [PSCustomObject]@{ - Server = $server - Username = $targetUser - RegionFormat = 'N/A' - ErrorMessage = 'Server unreachable or WinRM not available' - } - $allResults += $errorResult - } - } - else { - $errorResult = [PSCustomObject]@{ - Server = $server - Username = 'SERVER_UNREACHABLE' - RegionFormat = 'N/A' - ErrorMessage = 'Server unreachable or WinRM not available' - } - $allResults += $errorResult - } - continue - } - - # Get list of users to process for this server - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - # Use the predefined target users list - $usersToProcess = $targetUsersList - Write-Host "Processing $($usersToProcess.Count) users from user list..." -ForegroundColor Yellow - } - else { - # Get all user profiles from server - Write-Host 'Retrieving user profiles...' -ForegroundColor Yellow - $userProfiles = Get-RemoteUserProfiles -ServerName $server -Credential $credentials - - if ($userProfiles.Count -eq 0) { - Write-Warning "No user profiles found on $server - this may be due to PowerShell remoting permission issues" - $errorResult = [PSCustomObject]@{ - Server = $server - Username = 'REMOTING_ACCESS_DENIED' - RegionFormat = 'N/A' - ErrorMessage = 'PowerShell remoting access denied - check session configurations and permissions' - } - $allResults += $errorResult - continue - } - - # Extract usernames from profiles - $usersToProcess = $userProfiles | ForEach-Object { $_.Username } - Write-Host "Found $($usersToProcess.Count) user profiles on $server" -ForegroundColor Green - } - - # Process each user (regardless of source) - foreach ($username in $usersToProcess) { - Write-Host " Processing user: $username" -ForegroundColor White - - # Special handling for Default User profile - if ($username -ieq 'Default' -or $username -ieq 'Default User') { - $userResult = Get-DefaultUserRegionFormat -ServerName $server -Credential $credentials - if ($userResult) { - $result = [PSCustomObject]@{ - Server = $server - Username = $userResult.Username - RegionFormat = $userResult.RegionFormat - ErrorMessage = $userResult.ErrorMessage - } - $allResults += $result - - if ($userResult.ErrorMessage -eq $null) { - Write-Host " [SUCCESS] Region format: $($userResult.RegionFormat)" -ForegroundColor Green - } - else { - Write-Host " [WARNING] $($userResult.ErrorMessage)" -ForegroundColor Yellow - } - } - else { - Write-Host ' [ERROR] Failed to retrieve Default User region format' -ForegroundColor Red - } - } - else { - # Regular user profile handling - $userResult = Get-UserRegionFormat -ServerName $server -Username $username -Credential $credentials - if ($userResult) { - $result = [PSCustomObject]@{ - Server = $server - Username = $userResult.Username - RegionFormat = $userResult.RegionFormat - ErrorMessage = $userResult.ErrorMessage - } - $allResults += $result - - if ($userResult.UserExists) { - Write-Host " [SUCCESS] Region format: $($userResult.RegionFormat)" -ForegroundColor Green - } - else { - Write-Host ' [WARNING] User not found on server' -ForegroundColor Yellow - } - } - else { - Write-Host ' [ERROR] Failed to retrieve region format' -ForegroundColor Red - } - } - } - } - - # Display results in console and export to CSV - if ($allResults.Count -gt 0) { - # Export to CSV - $allResults | Export-Csv -Path $OutputPath -NoTypeInformation -Force - Write-Host "`n✅ Results exported to CSV:" -ForegroundColor Green - Write-Host " $OutputPath" -ForegroundColor Cyan - - # Display results table - Write-Host "`n📊 Region Format Summary:" -ForegroundColor Green - $allResults | Format-Table -AutoSize - - # Summary statistics - $successCount = @($allResults | Where-Object { -not $_.ErrorMessage }).Count - $failureCount = @($allResults | Where-Object { $_.ErrorMessage }).Count - Write-Host "`n📈 Summary:" -ForegroundColor Cyan - Write-Host " Total users processed: $($allResults.Count)" -ForegroundColor White - Write-Host " ✓ Successful: $successCount" -ForegroundColor Green - if ($failureCount -gt 0) { - Write-Host " ✗ Failed: $failureCount" -ForegroundColor Red - } - - # Show unique region formats found - $regionFormats = @($allResults | Where-Object { $_.RegionFormat -and $_.RegionFormat -ne 'N/A' } | Select-Object -ExpandProperty 'RegionFormat' | Sort-Object -Unique) - if ($regionFormats.Count -gt 0) { - Write-Host " Region format(s) found: $($regionFormats -join ', ')" -ForegroundColor Yellow - } - } - else { - Write-Warning 'No data was collected. Please check server connectivity and credentials.' - } -} -catch { - Write-Error "Script execution failed: $($_.Exception.Message)" - exit 1 -} - -Write-Host "`nScript completed." -ForegroundColor Green diff --git a/Scripts/iCat/Registry/RegionFormat/Set-UserRegionFormats.ps1 b/Scripts/iCat/Registry/RegionFormat/Set-UserRegionFormats.ps1 deleted file mode 100644 index e997dea..0000000 --- a/Scripts/iCat/Registry/RegionFormat/Set-UserRegionFormats.ps1 +++ /dev/null @@ -1,297 +0,0 @@ -#Requires -Version 5.1 - -<# -.SYNOPSIS - Sets the region format for user profiles on remote servers. - -.DESCRIPTION - This script connects to specified remote servers and sets the region format (LocaleName) - for user profiles. It supports user filtering via parameters or user list files, and - uses the RegistryUtils module for all helper functions and remoting logic. - -.PARAMETER Servers - Array of server IP addresses or hostnames to target. - -.PARAMETER TargetUsers - Array of specific usernames to set the region format for. If specified, only these users will be processed. - -.PARAMETER UserListFile - Path to a file containing usernames to process. Supports both .txt (one username per line) and .csv formats. - -.PARAMETER RegionFormat - The region format to set (e.g., 'en-US', 'nl-NL', 'de-DE'). Must be a valid culture code. - -.PARAMETER Credential - PSCredential object for remote server authentication. If not provided, the script will attempt to retrieve credentials from 1Password environment variables, or prompt the user. - -.EXAMPLE - .\Set-UserRegionFormats.ps1 -RegionFormat 'en-US' - Sets the region format for all users on the default server(s) to English (United States). - -.EXAMPLE - .\Set-UserRegionFormats.ps1 -TargetUsers @('jsmith','admin') -RegionFormat 'nl-NL' - Sets the region format for specific users to Dutch (Netherlands). - -.EXAMPLE - .\Set-UserRegionFormats.ps1 -TargetUsers @('Default') -RegionFormat 'en-US' - Sets the default region format for new users to English (United States). - -.EXAMPLE - .\Set-UserRegionFormats.ps1 -UserListFile "C:\temp\users.txt" -RegionFormat 'en-US' - Sets the region format for users listed in a file to English (United States). - -.NOTES - Author: PowerShell Script Generator - Created: November 21, 2025 - Requires: PowerShell 5.1 or higher, Remote PowerShell access to target servers - Relies on RegistryUtils module for all helper functions. -#> - -param( - [string[]]$Servers = @('10.210.3.23'), - [string[]]$TargetUsers = @(), - [string]$UserListFile = '', - [Parameter(Mandatory = $true)] - [string]$RegionFormat, - [PSCredential]$Credential -) - -# Import the RegistryUtils module -try { - # Handle both normal execution ($PSScriptRoot) and dot-sourcing ($MyInvocation.MyCommand.Path) - $scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } - # Navigate from Scripts/Registry/RegionFormat up to project root, then into Modules - $modulePath = Join-Path -Path $scriptDir -ChildPath '..\..\..\Modules\RegistryUtils\RegistryUtils.psd1' - $modulePath = Resolve-Path $modulePath -ErrorAction Stop # Normalize the path - Import-Module $modulePath -Force -ErrorAction Stop - Write-Host '✅ RegistryUtils module loaded successfully' -ForegroundColor Green -} -catch { - Write-Error "Failed to load RegistryUtils module: $($_.Exception.Message)" - Write-Host "Make sure the module is in the correct location: $modulePath" -ForegroundColor Yellow - exit 1 -} - -# Main script execution -try { - # Validate RegionFormat - try { - $cultureInfo = [CultureInfo]::new($RegionFormat) - Write-Host "✅ Valid region format: $RegionFormat ($($cultureInfo.DisplayName))" -ForegroundColor Green - } - catch { - Write-Error "Invalid region format: '$RegionFormat'. Please provide a valid culture code (e.g., 'en-US', 'nl-NL')." - exit 1 - } - - # Get credentials - use provided credential or attempt to retrieve from 1Password - if (-not $Credential) { - Write-Host 'Retrieving administrator credentials from 1Password...' -ForegroundColor Cyan - $credentials = Get-AdminCredential -ErrorAction SilentlyContinue - - if (-not $credentials) { - Write-Host '⚠️ 1Password credentials not available. Please provide credentials manually.' -ForegroundColor Yellow - $credentials = Get-AdminCredentials - if (-not $credentials) { - Write-Error 'No credentials provided. Exiting script.' - exit 1 - } - } - else { - Write-Host '✅ Successfully retrieved credentials from 1Password' -ForegroundColor Green - } - } - else { - Write-Host '✅ Using provided credentials' -ForegroundColor Green - $credentials = $Credential - } - - # Prepare target users list - $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - Write-Host "`n📋 User Filtering Enabled:" -ForegroundColor Cyan - if ($targetUsersList.Count -le 10) { - Write-Host " Target users: $($targetUsersList -join ', ')" -ForegroundColor Yellow - } - else { - Write-Host " Target users: $($targetUsersList[0..4] -join ', '), ... and $($targetUsersList.Count - 5) more" -ForegroundColor Yellow - } - Write-Host " Total target users: $($targetUsersList.Count)" -ForegroundColor Yellow - } - else { - Write-Host "`n📋 User Filtering: Disabled (all users will be processed)" -ForegroundColor Gray - } - - # Initialize results array - $allResults = @() - - # Process each server - foreach ($server in $Servers) { - Write-Host "`n$('='*50)" -ForegroundColor Cyan - Write-Host "Processing server: $server" -ForegroundColor Cyan - Write-Host "$('='*50)" -ForegroundColor Cyan - - # Test connectivity - if (-not (Test-ServerConnectivity -ServerName $server -Credential $credentials)) { - # Add error entry for unreachable server - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - foreach ($targetUser in $targetUsersList) { - $errorResult = [PSCustomObject]@{ - Server = $server - Username = $targetUser - SetRegionResult = 'N/A' - OldRegionFormat = $null - NewRegionFormat = $null - ErrorMessage = 'Server unreachable or WinRM not available' - } - $allResults += $errorResult - } - } - else { - $errorResult = [PSCustomObject]@{ - Server = $server - Username = 'SERVER_UNREACHABLE' - SetRegionResult = 'N/A' - OldRegionFormat = $null - NewRegionFormat = $null - ErrorMessage = 'Server unreachable or WinRM not available' - } - $allResults += $errorResult - } - continue - } - - # Get list of users to process for this server - if ($targetUsersList -and $targetUsersList.Count -gt 0) { - # Use the predefined target users list - $usersToProcess = $targetUsersList - Write-Host "Processing $($usersToProcess.Count) users from user list..." -ForegroundColor Yellow - } - else { - # Get all user profiles from server - Write-Host 'Retrieving user profiles...' -ForegroundColor Yellow - $userProfiles = Get-RemoteUserProfiles -ServerName $server -Credential $credentials - - if ($userProfiles.Count -eq 0) { - Write-Warning "No user profiles found on $server - this may be due to PowerShell remoting permission issues" - $errorResult = [PSCustomObject]@{ - Server = $server - Username = 'REMOTING_ACCESS_DENIED' - SetRegionResult = 'N/A' - OldRegionFormat = $null - NewRegionFormat = $null - ErrorMessage = 'PowerShell remoting access denied - check session configurations and permissions' - } - $allResults += $errorResult - continue - } - - # Extract usernames from profiles - $usersToProcess = $userProfiles | ForEach-Object { $_.Username } - Write-Host "Found $($usersToProcess.Count) user profiles on $server" -ForegroundColor Green - } - - # Process each user (regardless of source) - foreach ($username in $usersToProcess) { - Write-Host " Processing user: $username" -ForegroundColor White - - # Special handling for Default User profile - if ($username -ieq 'Default' -or $username -ieq 'Default User') { - $setResult = Set-DefaultUserRegionFormat -ServerName $server -RegionFormat $RegionFormat -Credential $credentials - - if ($setResult) { - $result = [PSCustomObject]@{ - Server = $server - Username = 'Default User' - SetRegionResult = if ($setResult.Success) { "✓ $($setResult.NewRegionFormat)" } else { '✗ Failed' } - OldRegionFormat = $setResult.OldRegionFormat - NewRegionFormat = $setResult.NewRegionFormat - ErrorMessage = if (-not $setResult.Success) { $setResult.Message } else { $null } - } - $allResults += $result - - if ($setResult.Success) { - Write-Host " [SUCCESS] Default User region format set to $($setResult.NewRegionFormat)" -ForegroundColor Green - } - else { - Write-Host " [WARNING] $($setResult.Message)" -ForegroundColor Yellow - } - } - else { - Write-Host ' [ERROR] Failed to set Default User region format' -ForegroundColor Red - } - } - else { - # Regular user profile handling - $setResult = Set-UserRegionFormat -ServerName $server -Username $username -RegionFormat $RegionFormat -Credential $credentials - - if ($setResult) { - $result = [PSCustomObject]@{ - Server = $server - Username = $setResult.Username - SetRegionResult = if ($setResult.Success) { "✓ $($setResult.NewRegionFormat)" } else { '✗ Failed' } - OldRegionFormat = $setResult.OldRegionFormat - NewRegionFormat = $setResult.NewRegionFormat - ErrorMessage = if (-not $setResult.Success) { $setResult.Message } else { $null } - } - $allResults += $result - - if ($setResult.Success) { - Write-Host " [SUCCESS] Region format set to $($setResult.NewRegionFormat)" -ForegroundColor Green - } - else { - Write-Host " [WARNING] $($setResult.Message)" -ForegroundColor Yellow - } - } - else { - Write-Host ' [ERROR] Failed to set region format' -ForegroundColor Red - } - } - } - } - - # Display results in console and export to CSV - if ($allResults.Count -gt 0) { - # Export to CSV - $outputDir = Join-Path -Path $env:USERPROFILE -ChildPath 'Documents\Scripts\PowerShellScripts\Output\RegionFormats' - $dateTimeString = Get-Date -Format 'yyyyMMdd_HHmmss' - $csvPath = Join-Path $outputDir "Set-UserRegionFormat_Output_${dateTimeString}.csv" - - # Ensure output directory exists - if (-not (Test-Path $outputDir)) { - New-Item -Path $outputDir -ItemType Directory -Force | Out-Null - } - - $allResults | Export-Csv -Path $csvPath -NoTypeInformation -Force - Write-Host "`n✅ Results exported to CSV:" -ForegroundColor Green - Write-Host " $csvPath" -ForegroundColor Cyan - - # Display results table - Write-Host "`n📊 Region Format Update Summary:" -ForegroundColor Green - $allResults | Format-Table -AutoSize - - # Summary statistics - $successCount = @($allResults | Where-Object { $_.SetRegionResult -match '✓' }).Count - $failureCount = @($allResults | Where-Object { $_.SetRegionResult -match '✗' -or $_.ErrorMessage }).Count - - Write-Host "`n📈 Summary:" -ForegroundColor Cyan - Write-Host " Total users processed: $($allResults.Count)" -ForegroundColor White - if ($successCount -gt 0) { - Write-Host " ✓ Successfully updated: $successCount" -ForegroundColor Green - } - if ($failureCount -gt 0) { - Write-Host " ✗ Failed: $failureCount" -ForegroundColor Red - } - - Write-Host " Target region format: $RegionFormat" -ForegroundColor Yellow - } - else { - Write-Warning 'No data was processed. Please check server connectivity and credentials.' - } -} -catch { - Write-Error "Script execution failed: $($_.Exception.Message)" - exit 1 -} - -Write-Host "`nScript completed." -ForegroundColor Green diff --git a/Scripts/iCat/Registry/RegionFormat/test.ps1 b/Scripts/iCat/Registry/RegionFormat/test.ps1 deleted file mode 100644 index 53d815d..0000000 --- a/Scripts/iCat/Registry/RegionFormat/test.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -# For a user on a remote server -$ServerName = '10.210.3.23' # or your server IP -$Username = 'admin-jmaffiola' # the user you want to check -$Credential = Get-Credential - -# Get user's SID from remote server -$userProfile = Invoke-Command -ComputerName $ServerName -Credential $Credential -ScriptBlock { - param($TargetUsername) - Get-WmiObject -Class Win32_UserProfile | - Where-Object { - $_.LocalPath -match "\\$TargetUsername`$" -and - $_.Special -eq $false - } -} -ArgumentList $Username - -if ($userProfile) { - $userSID = $userProfile.SID - - # Read the user's registry value - Invoke-Command -ComputerName $ServerName -Credential $Credential -ScriptBlock { - param($SID) - - # Create HKU drive if needed - if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) { - New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null - } - - # Check the LocaleName value - Get-ItemProperty -Path "HKU:\$SID\Control Panel\International" -Name 'LocaleName' -ErrorAction SilentlyContinue | - Select-Object -ExpandProperty 'LocaleName' - } -ArgumentList $userSID -} \ No newline at end of file From 1fb6e4a3c82a5596804efd4402b86ecaf36b082f Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 13:26:53 -0500 Subject: [PATCH 19/33] Changed name from iCat to iKAT --- .../Invoke-iKATRecording.ps1 | 83 +++++ .../Remove-ExpiredRecordings.ps1 | 30 ++ .../Start-DinaRecording.ps1 | 28 ++ .../Start-DinaRecording.vbs | 1 + .../Start-JoeyRecording.ps1 | 28 ++ .../Start-JoeyRecording.vbs | 1 + .../Invoke-FFmpegCapture/Test-Recording.ps1 | 13 + Scripts/iKAT/Invoke-FFmpegCapture/plan.md | 61 ++++ Scripts/iKAT/Invoke-FFmpegCapture/test.md | 87 +++++ .../DateFormat/Get-UserDateFormats.ps1 | 242 ++++++++++++++ .../DateFormat/Set-UserDateFormats.ps1 | 215 +++++++++++++ .../RegionFormat/Get-UserRegionFormats.ps1 | 292 +++++++++++++++++ .../RegionFormat/Set-UserRegionFormats.ps1 | 297 ++++++++++++++++++ Scripts/iKAT/Registry/RegionFormat/test.ps1 | 32 ++ 14 files changed, 1410 insertions(+) create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.ps1 create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/Test-Recording.ps1 create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/plan.md create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/test.md create mode 100644 Scripts/iKAT/Registry/DateFormat/Get-UserDateFormats.ps1 create mode 100644 Scripts/iKAT/Registry/DateFormat/Set-UserDateFormats.ps1 create mode 100644 Scripts/iKAT/Registry/RegionFormat/Get-UserRegionFormats.ps1 create mode 100644 Scripts/iKAT/Registry/RegionFormat/Set-UserRegionFormats.ps1 create mode 100644 Scripts/iKAT/Registry/RegionFormat/test.ps1 diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 new file mode 100644 index 0000000..f51b6cf --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -0,0 +1,83 @@ +param ( + [Parameter(Mandatory = $true)] + [string]$TargetUser, + + [Parameter(Mandatory = $true)] + [string]$OutputDir, + + [Parameter(Mandatory = $false)] + [int]$MinFreeSpaceGB = 10, + + [Parameter(Mandatory = $false)] + [string]$FFmpegPath = "ffmpeg.exe" +) + +# 1. Check if the current user matches the target user +if ($env:USERNAME -ne $TargetUser) { + Write-Output "Current user ($env:USERNAME) does not match target user ($TargetUser). Exiting." + Exit +} + +# Ensure the output directory exists +if (-not (Test-Path -Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} + +$driveLetter = (Get-Item $OutputDir).Root.Name + +# 2. Check for sufficient free disk space +$drive = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($driveLetter.Trim('\'))'" +if ($drive) { + $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) + if ($freeSpaceGB -lt $MinFreeSpaceGB) { + Write-Warning "Not enough free space on $driveLetter (Available: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB). Exiting." + Exit + } +} + +# 3. Detect the full virtual desktop dimensions to capture all monitors +Add-Type -AssemblyName System.Windows.Forms +$virtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen +$screenWidth = $virtualScreen.Width +$screenHeight = $virtualScreen.Height +$offsetX = $virtualScreen.X +$offsetY = $virtualScreen.Y + +# 4. Prepare output file +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.mkv" + +# Construct the FFmpeg arguments targeting the full virtual desktop +$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p `"$outputFile`"" + +# 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown +$procInfo = New-Object System.Diagnostics.ProcessStartInfo +$procInfo.FileName = $FFmpegPath +$procInfo.Arguments = $ffmpegArgsStr +$procInfo.RedirectStandardInput = $true +$procInfo.UseShellExecute = $false +$procInfo.CreateNoWindow = $true + +$ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) + +try { + # 6. Block until FFmpeg exits naturally or the script is interrupted (logoff/session end) + while (-not $ffmpegProcess.HasExited) { + Start-Sleep -Seconds 5 + } +} +finally { + # 7. Session ended or script was interrupted — terminate FFmpeg gracefully + if ($null -ne $ffmpegProcess -and -not $ffmpegProcess.HasExited) { + # Send 'q' to gracefully stop recording so the file header formatting writes correctly + $ffmpegProcess.StandardInput.WriteLine("q") + + # Wait up to 10 seconds for it to write headers and close + $ffmpegProcess.WaitForExit(10000) | Out-Null + + # Fallback if the process stubbornly hung + if (-not $ffmpegProcess.HasExited) { + $ffmpegProcess.Kill() + } + } +} \ No newline at end of file diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 new file mode 100644 index 0000000..3b1c647 --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 @@ -0,0 +1,30 @@ +param ( + [Parameter(Mandatory = $true)] + [string]$OutputDir, + + [Parameter(Mandatory = $true)] + [int]$DaysToKeep +) + +if (-not (Test-Path -Path $OutputDir)) { + Write-Warning "Directory '$OutputDir' does not exist. Exiting." + Exit +} + +$logFile = Join-Path -Path $OutputDir -ChildPath "CleanupLog.txt" +$cutoffDate = (Get-Date).AddDays(-$DaysToKeep) + +# Scan for .mkv files older than the retention limit +$expiredFiles = Get-ChildItem -Path $OutputDir -Filter "*.mkv" -File | Where-Object { $_.LastWriteTime -lt $cutoffDate } + +foreach ($file in $expiredFiles) { + try { + Remove-Item -Path $file.FullName -Force -ErrorAction Stop + $logMessage = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Successfully deleted: $($file.Name)" + Add-Content -Path $logFile -Value $logMessage + } + catch { + $logMessage = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Error deleting $($file.Name): $($_.Exception.Message)" + Add-Content -Path $logFile -Value $logMessage + } +} \ No newline at end of file diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.ps1 new file mode 100644 index 0000000..f0245b7 --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Runner script to launch the FFmpeg recording for a specific user and process. +.DESCRIPTION + This script wrapper is designed to be executed by Task Scheduler on the RemoteApp/RDS server. + It calls Start-AppRecording.ps1 with the predefined parameters for the iKAT troubleshooting scenario. +#> + +$ErrorActionPreference = 'Stop' + +# Determine the path to the main recording script located in the same directory +$RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecording.ps1" + +# Define the parameters for this specific troubleshooting scenario using splatting +$CaptureParameters = @{ + TargetUser = "dpuerner" # The specific user encountering the issue + OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved + MinFreeSpaceGB = 10 # Ensure at least 10GB of free space before recording + # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH +} + +if (-not (Test-Path $RecordingScript)) { + Write-Error "Could not find the recording script at: $RecordingScript" + Exit +} + +# Execute the recording script with the parameters +& $RecordingScript @CaptureParameters diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs b/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs new file mode 100644 index 0000000..da4d528 --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs @@ -0,0 +1 @@ +CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-DinaRecording.ps1""", 0, False diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 new file mode 100644 index 0000000..20812e3 --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Runner script to launch the FFmpeg recording for a specific user and process. +.DESCRIPTION + This script wrapper is designed to be executed by Task Scheduler on the RemoteApp/RDS server. + It calls Start-AppRecording.ps1 with the predefined parameters for the iKAT troubleshooting scenario. +#> + +$ErrorActionPreference = 'Stop' + +# Determine the path to the main recording script located in the same directory +$RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecording.ps1" + +# Define the parameters for this specific troubleshooting scenario using splatting +$CaptureParameters = @{ + TargetUser = "jmaffiola" # The specific user encountering the issue + OutputDir = "C:\temp\RecordingsTest" # The directory where the .mkv files will be saved + MinFreeSpaceGB = 1 # Lower threshold for local testing + # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH +} + +if (-not (Test-Path $RecordingScript)) { + Write-Error "Could not find the recording script at: $RecordingScript" + Exit +} + +# Execute the recording script with the parameters +& $RecordingScript @CaptureParameters diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs b/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs new file mode 100644 index 0000000..9ccb4c4 --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs @@ -0,0 +1 @@ +CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-JoeyRecording.ps1""", 0, False diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Test-Recording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Test-Recording.ps1 new file mode 100644 index 0000000..edc4480 --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Test-Recording.ps1 @@ -0,0 +1,13 @@ +$TestCaptureParameters = @{ + TargetUser = $env:USERNAME # Logs you instead of Dina + TargetProcess = "notepad" # Monitors for Notepad + OutputDir = "C:\Temp\RecordingsTest" + MinFreeSpaceGB = 1 + #FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # <--- Update path here if needed! +} + +# Ensure the core script path is correct +$scriptPath = "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" + +# Execute the core script +& $scriptPath @TestCaptureParameters \ No newline at end of file diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/plan.md b/Scripts/iKAT/Invoke-FFmpegCapture/plan.md new file mode 100644 index 0000000..a56392b --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/plan.md @@ -0,0 +1,61 @@ +# Project Plan: Modular Automated RemoteApp Session Recording via FFmpeg + +## 1. Project Overview + +Deploy an automated, background screen recording solution using FFmpeg to capture intermittent application errors. The solution is designed to be highly modular, accepting parameters for the target user, the target process (for RemoteApp environments), and the output directory. + +**Initial Use Case:** Capturing the full desktop session of user "dpuerner" (Dina) to diagnose intermittent "ikat" application crashes. + +## 2. Prerequisites + +* **FFmpeg**: Download the Windows executable (`ffmpeg.exe`) and place it in a secure, accessible directory (e.g., `C:\Scripts\FFmpeg\`). +* **Storage Location**: [Pending Developer Input] (Passed as a parameter). +* **Retention Policy**: [Pending Developer Input] (Passed as a parameter). +* **Permissions**: Administrative access to the target Remote Desktop Session Host. + +## 3. Workflow + +1. User establishes an RDP/RemoteApp connection. +2. Windows Task Scheduler detects the logon and launches a hidden PowerShell script wrapper (`Start-DinaRecording.ps1`), which splats and invokes the core logic (`Invoke-iKATRecording.ps1`). +3. The script verifies `$env:USERNAME` matches the targeted user. +4. The script checks for sufficient free disk space on the target drive to prevent storage exhaustion. +5. FFmpeg starts recording immediately — capturing the entire virtual desktop (all monitors) for the duration of the session. +6. The script blocks until the session ends (logoff, disconnect, or Task Scheduler kill signal). +7. On termination, the `finally` block sends `"q"` to FFmpeg via Standard Input, gracefully flushing and finalizing the `.mkv` file. +8. A separate daily task runs a cleanup script, passing arguments for the directory and retention days. + +## 4. Implementation Details (Current State) + +### Core Recording Script (`Invoke-iKATRecording.ps1`) + +* Uses a `param()` block for arguments: `$TargetUser`, `$OutputDir`, `$FFmpegPath`, and `$MinFreeSpaceGB`. (`$TargetProcess` removed — recording is no longer app-triggered.) +* Validates user identity and verifies free space on the destination drive before recording. +* Dynamically detects the full virtual desktop geometry using `[System.Windows.Forms.SystemInformation]::VirtualScreen` to capture all connected monitors at their native combined resolution. +* Starts FFmpeg immediately on logon — no waiting for a specific process. +* Uses `.NET System.Diagnostics.ProcessStartInfo` instead of `Start-Process` to properly redirect Standard Input without opening visible console windows. +* FFmpeg argument string: `-f gdigrab -framerate 5 -offset_x -offset_y -video_size x -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p`. (Note: `-pix_fmt yuv420p` is critical so native Windows media players can decode the MKV; geometry values are auto-detected at runtime.) +* Blocks using `while (-not $ffmpegProcess.HasExited)` and wraps in `try { ... } finally { ... }`. Ensures that on any termination (logoff, Task Scheduler kill, Ctrl+C), it sends `"q"` to FFmpeg to cleanly flush and finalize the recording. + +### Cleanup Script (`Remove-ExpiredRecordings.ps1`) + +* Scans `$OutputDir` for `.mkv` files with a `LastWriteTime` older than the specified `$DaysToKeep`. +* Configurable output directory and retention to prevent out-of-storage issues on the terminal servers. + +### Testing & Verification (`test.md` & `Test-Recording.ps1`) + +* Contains Pester tests to validate free space logic and correct recording initialization parameters (`Invoke-iKATRecording.Tests.ps1`). +* Local sandbox script (`Test-Recording.ps1`) runs the process against dummy apps like `mspaint` or `calc` to prevent UWP suspension mechanics (like `notepad` on Win11) from stalling the process closure loop. + +## 5. Deployment Configuration (Task Scheduler) + +Instructions for creating the tasks: + +* **Task A (Recording)**: + * Security Options: "Run only when user is logged on" (Allows it to capture the graphical desktop session). + * Trigger: "At log on" for specific user "dpuerner". + * Action: `powershell.exe` + * Arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Scripts\Invoke-FFmpegCapture\Start-DinaRecording.ps1"` +* **Task B (Cleanup)**: + * Trigger: Daily at an off-peak time (e.g., 2:00 AM). + * Action: `powershell.exe` + * Arguments: `-ExecutionPolicy Bypass -File "C:\Scripts\Invoke-FFmpegCapture\Remove-ExpiredRecordings.ps1" -OutputDir "C:\Recordings" -DaysToKeep 5` diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/test.md b/Scripts/iKAT/Invoke-FFmpegCapture/test.md new file mode 100644 index 0000000..0084934 --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/test.md @@ -0,0 +1,87 @@ +# Testing FFmpeg Automated Screen Capture + +This guide outlines how to safely test the automated FFmpeg screen recording solution. These tests should **first be run on your own local PC** to verify the logic, and then **optionally on a test server** before final deployment to the live RDS environment. + +## Prerequisite: Download FFmpeg + +Since FFmpeg is not natively installed on Windows, you will need to place the executable on your machine. + +1. Download a pre-compiled Windows build of `ffmpeg.exe` (e.g., from gyan.dev or BtbN). +2. Extract the archive and copy `ffmpeg.exe` to a permanent location, such as `C:\Scripts\FFmpeg\ffmpeg.exe`. + +--- + +## 1. Local Simulation Test (Recommended First Step) + +Before messing with Task Scheduler, you can perform an active test in your own console. We will temporarily use **Notepad** instead of iKAT to simulate the process. + +**Steps:** + +1. Open PowerShell as an Administrator. +2. Copy and paste the following snippet into your console. *Make sure to update the `$FFmpegPath` and `$OutputDir` if yours differ.* + +```powershell +$TestCaptureParameters = @{ + TargetUser = $env:USERNAME # Logs you instead of Dina + TargetProcess = "notepad" # Monitors for Notepad + OutputDir = "C:\Temp\RecordingsTest" + MinFreeSpaceGB = 1 + FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # <--- Update path here if needed! +} + +# Ensure the core script path is correct +$scriptPath = "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" + +# Execute the core script +& $scriptPath @TestCaptureParameters +``` + +1. The console will appear to hang. This is intentional; it is locked in the loop waiting for Notepad to start. +2. **Open Notepad**. Type some text and move the window around for 10-15 seconds to simulate user activity. +3. **Close Notepad.** This will trigger the script to stop recording, save the file, and loop back to start waiting again. +4. Open your target directory (`C:\Temp\RecordingsTest`) to verify the file was created. +5. Play the `.mkv` file to confirm your desktop and the Notepad activity were captured successfully. +6. Go back to your PowerShell console and press **`Ctrl + C`** to break the continuous loop and exit the script. + +--- + +## 2. Testing the Task Scheduler Deployment + +Once the script logic is confirmed working, the next step is verifying Task Scheduler triggers it silently and correctly. + +### Modifying the Runner Script for Testing + +Open `Start-DinaRecording.ps1` and temporarily change the parameters so you can trigger it as yourself using Notepad: + +```powershell + TargetUser = "YOUR_USERNAME_HERE" # Change from 'dpuerner' + TargetProcess = "notepad" # Change from 'ikat' +``` + +*Don't forget to save the file.* + +### Creating the Task Scheduler Entry + +1. Open **Task Scheduler** on your test machine. +2. Right-click Task Scheduler Library -> **Create Task...** (Do not use Basic Task). +3. **General Tab:** + * Name: `Test-FFmpegCapture` + * Under Security Options, select **Run only when user is logged on**. *(Crucial Note: If "Run whether user is logged on or not" is selected, the task runs in Session 0 and the resulting video will be completely black/blank).* +4. **Triggers Tab:** + * Click **New...** + * Change "Begin the task:" to **At log on** + * Specify **Any user**. +5. **Actions Tab:** + * Click **New...** + * Action: **Start a program** + * Program/script: `powershell.exe` + * Add arguments: `-WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-DinaRecording.ps1"` + +### Running the Live Test + +1. You can either log off and log back on to trigger it naturally, or simply right-click your new task and select **Run**. +2. No hidden windows should appear. +3. Open **Notepad**, type some text, and then close it. +4. Check your `C:\Recordings` (or modified output directory) to verify the `.mkv` was created successfully in the background. + +*Important: Remember to revert the parameters inside `Start-DinaRecording.ps1` back to `dpuerner` and `ikat` before pushing to production!* diff --git a/Scripts/iKAT/Registry/DateFormat/Get-UserDateFormats.ps1 b/Scripts/iKAT/Registry/DateFormat/Get-UserDateFormats.ps1 new file mode 100644 index 0000000..9416795 --- /dev/null +++ b/Scripts/iKAT/Registry/DateFormat/Get-UserDateFormats.ps1 @@ -0,0 +1,242 @@ +#Requires -Version 5.1 + +<# +.SYNOPSIS + Retrieves date format information for all user profiles on remote servers. + +.DESCRIPTION + This script connects to specified remote servers and gathers date format information + for all user profiles. It retrieves both user-specific and system-wide regional settings + and exports the data to a CSV file. + +.PARAMETER Servers + Array of server IP addresses or hostnames to query. + +.PARAMETER OutputPath + Path where the CSV output file will be saved. + +.PARAMETER TargetUsers + Array of specific usernames to collect data for. If specified, only these users will be processed. + +.PARAMETER UserListFile + Path to a file containing usernames to collect data for. Supports both .txt (one username per line) and .csv formats. For CSV files, the script will auto-detect the username column. + +.PARAMETER Credential + PSCredential object for remote server authentication. If not provided, the script will attempt to retrieve credentials from 1Password environment variables, or prompt the user. + +.EXAMPLE + .\Get-UserDateFormats.ps1 + Runs the script with default servers and prompts for output location. + +.EXAMPLE + .\Get-UserDateFormats.ps1 -TargetUsers @('jsmith', 'admin', 'temp') -OutputPath "C:\temp\specific_users.csv" + Collects date format data only for specific users: jsmith, admin, and temp. + +.EXAMPLE + .\Get-UserDateFormats.ps1 -UserListFile "C:\temp\users.txt" -OutputPath "C:\temp\filtered_users.csv" + Collects data for users listed in the specified file. + +.EXAMPLE + .\Get-UserDateFormats.ps1 -UserListFile "C:\temp\users.csv" -OutputPath "C:\temp\csv_users.csv" + Collects data for users listed in a CSV file (auto-detects username column). + +.NOTES + Author: PowerShell Script Generator + Created: June 17, 2025 + Requires: PowerShell 5.1 or higher, Remote PowerShell access to target servers + + User Filtering: + - If TargetUsers is specified, only those users will be processed + - If UserListFile is specified, users will be read from the file (supports .txt and .csv formats) + - If both are specified, TargetUsers takes precedence + - If neither is specified, all users will be processed (original behavior) +#> + +param( + [string[]]$Servers = @('10.210.3.23'), + [string]$OutputPath = '', + [string[]]$TargetUsers = @(), + [string]$UserListFile = '', + [PSCredential]$Credential +) + +# Import the RegistryUtils module +try { + $modulePath = Join-Path $PSScriptRoot '..\..\Modules\RegistryUtils\RegistryUtils.psd1' + Import-Module $modulePath -Force -ErrorAction Stop + Write-Host '✅ RegistryUtils module loaded successfully' -ForegroundColor Green +} +catch { + Write-Error "Failed to load RegistryUtils module: $($_.Exception.Message)" + Write-Host "Make sure the module is in the correct location: $modulePath" -ForegroundColor Yellow + exit 1 +} + + +# Main script execution +try { + # Get credentials - use provided credential or attempt to retrieve from 1Password + if (-not $Credential) { + Write-Host 'Retrieving administrator credentials from 1Password...' -ForegroundColor Cyan + $credentials = Get-AdminCredential -ErrorAction SilentlyContinue + + if (-not $credentials) { + Write-Host '⚠️ 1Password credentials not available. Please provide credentials manually.' -ForegroundColor Yellow + $credentials = Get-AdminCredentials + if (-not $credentials) { + Write-Error 'No credentials provided. Exiting script.' + exit 1 + } + } + else { + Write-Host '✅ Successfully retrieved credentials from 1Password' -ForegroundColor Green + } + } + else { + Write-Host '✅ Using provided credentials' -ForegroundColor Green + $credentials = $Credential + } + + # Prepare target users list + $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + Write-Host "`n📋 User Filtering Enabled:" -ForegroundColor Cyan + if ($targetUsersList.Count -le 10) { + Write-Host " Target users: $($targetUsersList -join ', ')" -ForegroundColor Yellow + } + else { + Write-Host " Target users: $($targetUsersList[0..4] -join ', '), ... and $($targetUsersList.Count - 5) more" -ForegroundColor Yellow + } + Write-Host " Total target users: $($targetUsersList.Count)" -ForegroundColor Yellow + } + else { + Write-Host "`n📋 User Filtering: Disabled (all users will be processed)" -ForegroundColor Gray + } + + # Always set output path to the specified directory and filename format + $outputDir = Join-Path -Path $env:USERPROFILE -ChildPath 'Documents\Scripts\PowerShellScripts\Output\DateFormats' + $dateTimeString = Get-Date -Format 'yyyyMMdd_HHmmss' + $OutputPath = Join-Path $outputDir "Get-UserDateFormat_Output_${dateTimeString}.csv" + Write-Host "Output will be saved to: $OutputPath" -ForegroundColor Yellow + + # Ensure output directory exists + $outputDir = Split-Path $OutputPath -Parent + if (-not (Test-Path $outputDir)) { + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null + } + + # Initialize results array + $allResults = @() + + # Get target users list + $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile + + # Process each server + foreach ($server in $Servers) { + Write-Host "`n$('='*50)" -ForegroundColor Cyan + Write-Host "Processing server: $server" -ForegroundColor Cyan + Write-Host "$('='*50)" -ForegroundColor Cyan + + # Test connectivity + if (-not (Test-ServerConnectivity -ServerName $server -Credential $credentials)) { + # Add error entry for unreachable server + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + # Add error entries for each target user + foreach ($targetUser in $targetUsersList) { + $errorResult = [PSCustomObject]@{ + Server = $server + Username = $targetUser + SystemShortDateFormat = 'N/A' + ErrorMessage = 'Server unreachable or WinRM not available' + } + $allResults += $errorResult + } + } + else { + $errorResult = [PSCustomObject]@{ + Server = $server + Username = 'SERVER_UNREACHABLE' + SystemShortDateFormat = 'N/A' + ErrorMessage = 'Server unreachable or WinRM not available' + } + $allResults += $errorResult + } + continue + } + + # Get list of users to process for this server + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + # Use the predefined target users list + $usersToProcess = $targetUsersList + Write-Host "Processing $($usersToProcess.Count) users from user list..." -ForegroundColor Yellow + } + else { + # Get all user profiles from server + Write-Host 'Retrieving user profiles...' -ForegroundColor Yellow + $userProfiles = Get-RemoteUserProfiles -ServerName $server -Credential $credentials + + if ($userProfiles.Count -eq 0) { + Write-Warning "No user profiles found on $server - this may be due to PowerShell remoting permission issues" + $errorResult = [PSCustomObject]@{ + Server = $server + Username = 'REMOTING_ACCESS_DENIED' + SystemShortDateFormat = 'N/A' + ErrorMessage = 'PowerShell remoting access denied - check session configurations and permissions' + } + $allResults += $errorResult + continue + } + + # Extract usernames from profiles + $usersToProcess = $userProfiles | ForEach-Object { $_.Username } + Write-Host "Found $($usersToProcess.Count) user profiles on $server" -ForegroundColor Green + } + + # Process each user (regardless of source) + foreach ($username in $usersToProcess) { + Write-Host " Processing user: $username" -ForegroundColor White + + $userResult = Get-UserSystemShortDateFormat -ServerName $server -Username $username -Credential $credentials + if ($userResult) { + $result = [PSCustomObject]@{ + Server = $server + Username = $userResult.Username + SystemShortDateFormat = $userResult.SystemShortDateFormat + ErrorMessage = $userResult.ErrorMessage + } + $allResults += $result + + if ($userResult.UserExists) { + Write-Host ' [SUCCESS] Successfully retrieved SystemShortDateFormat' -ForegroundColor Green + } + else { + Write-Host ' [WARNING] User not found on server' -ForegroundColor Yellow + } + } + else { + Write-Host ' [ERROR] Failed to retrieve date format' -ForegroundColor Red + } + } + } + + # Display results in console and export to CSV + if ($allResults.Count -gt 0) { + # Export to CSV using the module function + $csvPath = Export-UserDateFormatCsv -Results $allResults -OutputPath $OutputPath + + # Display comprehensive summary using the module function + Show-UserDateFormatSummary -Results $allResults -OperationType 'Get' -CsvPath $csvPath + + # Display results using the module function + Show-UserDateFormatTable -Results $allResults + } + else { + Write-Warning 'No data was collected. Please check server connectivity and credentials.' + } +} +catch { + Write-Error "Script execution failed: $($_.Exception.Message)" + exit 1 +} + +Write-Host "`nScript completed." -ForegroundColor Green \ No newline at end of file diff --git a/Scripts/iKAT/Registry/DateFormat/Set-UserDateFormats.ps1 b/Scripts/iKAT/Registry/DateFormat/Set-UserDateFormats.ps1 new file mode 100644 index 0000000..6fecf1e --- /dev/null +++ b/Scripts/iKAT/Registry/DateFormat/Set-UserDateFormats.ps1 @@ -0,0 +1,215 @@ +#Requires -Version 5.1 + +<# +.SYNOPSIS + Sets the system short date format for user profiles on remote servers. + +.DESCRIPTION + This script connects to specified remote servers and sets the system short date format + for user profiles. It supports user filtering via parameters or user list files, and + uses the RegistryUtils module for all helper functions and remoting logic. + +.PARAMETER Servers + Array of server IP addresses or hostnames to target. + +.PARAMETER TargetUsers + Array of specific usernames to set the date format for. If specified, only these users will be processed. + +.PARAMETER UserListFile + Path to a file containing usernames to process. Supports both .txt (one username per line) and .csv formats. + +.PARAMETER DateFormat + The short date format string to set (e.g., 'MM/dd/yyyy'). + +.PARAMETER Credential + PSCredential object for remote server authentication. If not provided, the script will attempt to retrieve credentials from 1Password environment variables, or prompt the user. + +.EXAMPLE + .\Set-UserDateFormats.ps1 -DateFormat 'yyyy-MM-dd' + Sets the short date format for all users on the default server(s). + +.EXAMPLE + .\Set-UserDateFormats.ps1 -TargetUsers @('jsmith','admin') -DateFormat 'dd/MM/yyyy' + Sets the short date format for specific users. + +.EXAMPLE + .\Set-UserDateFormats.ps1 -UserListFile "C:\temp\users.txt" -DateFormat 'MM-dd-yyyy' + Sets the short date format for users listed in a file. + +.NOTES + Author: PowerShell Script Generator + Created: July 14, 2025 + Requires: PowerShell 5.1 or higher, Remote PowerShell access to target servers + Relies on RegistryUtils module for all helper functions. +#> + +param( + [string[]]$Servers = @('10.210.3.23'), + [string[]]$TargetUsers = @(), + [string]$UserListFile = '', + [Parameter(Mandatory = $true)] + [string]$DateFormat, + [PSCredential]$Credential +) + +# Import the RegistryUtils module +try { + $modulePath = Join-Path $PSScriptRoot '..\..\Modules\RegistryUtils\RegistryUtils.psd1' + Import-Module $modulePath -Force -ErrorAction Stop + Write-Host '✅ RegistryUtils module loaded successfully' -ForegroundColor Green +} +catch { + Write-Error "Failed to load RegistryUtils module: $($_.Exception.Message)" + Write-Host "Make sure the module is in the correct location: $modulePath" -ForegroundColor Yellow + exit 1 +} + +# Main script execution +try { + # Get credentials - use provided credential or attempt to retrieve from 1Password + if (-not $Credential) { + Write-Host 'Retrieving administrator credentials from 1Password...' -ForegroundColor Cyan + $credentials = Get-AdminCredential -ErrorAction SilentlyContinue + + if (-not $credentials) { + Write-Host '⚠️ 1Password credentials not available. Please provide credentials manually.' -ForegroundColor Yellow + $credentials = Get-AdminCredentials + if (-not $credentials) { + Write-Error 'No credentials provided. Exiting script.' + exit 1 + } + } + else { + Write-Host '✅ Successfully retrieved credentials from 1Password' -ForegroundColor Green + } + } + else { + Write-Host '✅ Using provided credentials' -ForegroundColor Green + $credentials = $Credential + } + + # Prepare target users list + $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + Write-Host "`n📋 User Filtering Enabled:" -ForegroundColor Cyan + if ($targetUsersList.Count -le 10) { + Write-Host " Target users: $($targetUsersList -join ', ')" -ForegroundColor Yellow + } + else { + Write-Host " Target users: $($targetUsersList[0..4] -join ', '), ... and $($targetUsersList.Count - 5) more" -ForegroundColor Yellow + } + Write-Host " Total target users: $($targetUsersList.Count)" -ForegroundColor Yellow + } + else { + Write-Host "`n📋 User Filtering: Disabled (all users will be processed)" -ForegroundColor Gray + } + + # Initialize results array + $allResults = @() + + # Process each server + foreach ($server in $Servers) { + Write-Host "`n$('='*50)" -ForegroundColor Cyan + Write-Host "Processing server: $server" -ForegroundColor Cyan + Write-Host "$('='*50)" -ForegroundColor Cyan + + # Test connectivity + if (-not (Test-ServerConnectivity -ServerName $server -Credential $credentials)) { + # Add error entry for unreachable server + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + foreach ($targetUser in $targetUsersList) { + $errorResult = [PSCustomObject]@{ + Server = $server + Username = $targetUser + SetDateFormatResult = 'N/A' + ErrorMessage = 'Server unreachable or WinRM not available' + } + $allResults += $errorResult + } + } + else { + $errorResult = [PSCustomObject]@{ + Server = $server + Username = 'SERVER_UNREACHABLE' + SetDateFormatResult = 'N/A' + ErrorMessage = 'Server unreachable or WinRM not available' + } + $allResults += $errorResult + } + continue + } + + # Get list of users to process for this server + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + # Use the predefined target users list + $usersToProcess = $targetUsersList + Write-Host "Processing $($usersToProcess.Count) users from user list..." -ForegroundColor Yellow + } + else { + # Get all user profiles from server + Write-Host 'Retrieving user profiles...' -ForegroundColor Yellow + $userProfiles = Get-RemoteUserProfiles -ServerName $server -Credential $credentials + + if ($userProfiles.Count -eq 0) { + Write-Warning "No user profiles found on $server - this may be due to PowerShell remoting permission issues" + $errorResult = [PSCustomObject]@{ + Server = $server + Username = 'REMOTING_ACCESS_DENIED' + SetDateFormatResult = 'N/A' + ErrorMessage = 'PowerShell remoting access denied - check session configurations and permissions' + } + $allResults += $errorResult + continue + } + + # Extract usernames from profiles + $usersToProcess = $userProfiles | ForEach-Object { $_.Username } + Write-Host "Found $($usersToProcess.Count) user profiles on $server" -ForegroundColor Green + } + + # Process each user (regardless of source) + foreach ($username in $usersToProcess) { + Write-Host " Processing user: $username" -ForegroundColor White + $setResult = Set-RemoteUserDateFormat -ServerName $server -Username $username -DateFormat $DateFormat -Credential $credentials + if ($setResult) { + $result = [PSCustomObject]@{ + Server = $server + Username = $setResult.Username + SetDateFormatResult = if ($setResult.Success) { "✓ $($setResult.NewDateFormat)" } else { '✗ Failed' } + ErrorMessage = if (-not $setResult.Success) { $setResult.Message } else { $null } + } + $allResults += $result + if ($setResult.Success) { + Write-Host ' [SUCCESS] Date format set successfully' -ForegroundColor Green + } + else { + Write-Host " [WARNING] $($setResult.Message)" -ForegroundColor Yellow + } + } + else { + Write-Host ' [ERROR] Failed to set date format' -ForegroundColor Red + } + } + } + + # Display results in console and export to CSV + if ($allResults.Count -gt 0) { + # Export to CSV using the module function + $csvPath = Export-UserDateFormatCsv -Results $allResults -FilePrefix 'Set-UserDateFormat_Output' + + # Display comprehensive summary using the module function + Show-UserDateFormatSummary -Results $allResults -OperationType 'Set' -CsvPath $csvPath + + # Display results using the module function + Show-UserDateFormatTable -Results $allResults -OperationType 'Set' -Title 'DATE FORMAT UPDATE RESULTS' + } + else { + Write-Warning 'No data was processed. Please check server connectivity and credentials.' + } +} +catch { + Write-Error "Script execution failed: $($_.Exception.Message)" + exit 1 +} + +Write-Host "`nScript completed." -ForegroundColor Green diff --git a/Scripts/iKAT/Registry/RegionFormat/Get-UserRegionFormats.ps1 b/Scripts/iKAT/Registry/RegionFormat/Get-UserRegionFormats.ps1 new file mode 100644 index 0000000..49696d1 --- /dev/null +++ b/Scripts/iKAT/Registry/RegionFormat/Get-UserRegionFormats.ps1 @@ -0,0 +1,292 @@ +#Requires -Version 5.1 + +<# +.SYNOPSIS + Retrieves region format information for all user profiles on remote servers. + +.DESCRIPTION + This script connects to specified remote servers and gathers region format information + (LocaleName registry value) for all user profiles. It retrieves the Windows Settings + "Region format" setting and exports the data to a CSV file. + +.PARAMETER Servers + Array of server IP addresses or hostnames to query. + +.PARAMETER OutputPath + Path where the CSV output file will be saved. + +.PARAMETER TargetUsers + Array of specific usernames to collect data for. If specified, only these users will be processed. + +.PARAMETER UserListFile + Path to a file containing usernames to collect data for. Supports both .txt (one username per line) and .csv formats. For CSV files, the script will auto-detect the username column. + +.PARAMETER Credential + PSCredential object for remote server authentication. If not provided, the script will attempt to retrieve credentials from 1Password environment variables, or prompt the user. + +.EXAMPLE + .\Get-UserRegionFormats.ps1 + Runs the script with default servers and prompts for output location. + +.EXAMPLE + .\Get-UserRegionFormats.ps1 -TargetUsers @('jsmith', 'admin', 'temp') -OutputPath "C:\temp\specific_users.csv" + Collects region format data only for specific users: jsmith, admin, and temp. + +.EXAMPLE + .\Get-UserRegionFormats.ps1 -TargetUsers @('Default') -OutputPath "C:\temp\default_user.csv" + Retrieves the Default User region format setting. + +.EXAMPLE + .\Get-UserRegionFormats.ps1 -UserListFile "C:\temp\users.txt" -OutputPath "C:\temp\filtered_users.csv" + Collects data for users listed in the specified file. + +.EXAMPLE + .\Get-UserRegionFormats.ps1 -UserListFile "C:\temp\users.csv" -OutputPath "C:\temp\csv_users.csv" + Collects data for users listed in a CSV file (auto-detects username column). + +.NOTES + Author: PowerShell Script Generator + Created: November 21, 2025 + Requires: PowerShell 5.1 or higher, Remote PowerShell access to target servers + + User Filtering: + - If TargetUsers is specified, only those users will be processed + - If UserListFile is specified, users will be read from the file (supports .txt and .csv formats) + - If both are specified, TargetUsers takes precedence + - If neither is specified, all users will be processed (original behavior) +#> + +param( + [string[]]$Servers = @('10.210.3.23'), + [string]$OutputPath = '', + [string[]]$TargetUsers = @(), + [string]$UserListFile = '', + [PSCredential]$Credential +) + +# Import the RegistryUtils module +try { + # Handle both normal execution ($PSScriptRoot) and dot-sourcing ($MyInvocation.MyCommand.Path) + $scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } + # Navigate from Scripts/Registry/RegionFormat up to project root, then into Modules + $modulePath = Join-Path -Path $scriptDir -ChildPath '..\..\..\Modules\RegistryUtils\RegistryUtils.psd1' + $modulePath = Resolve-Path $modulePath -ErrorAction Stop # Normalize the path + Import-Module $modulePath -Force -ErrorAction Stop + Write-Host '✅ RegistryUtils module loaded successfully' -ForegroundColor Green +} +catch { + Write-Error "Failed to load RegistryUtils module: $($_.Exception.Message)" + Write-Host "Make sure the module is in the correct location: $modulePath" -ForegroundColor Yellow + exit 1 +} + + +# Main script execution +try { + # Get credentials - use provided credential or attempt to retrieve from 1Password + if (-not $Credential) { + Write-Host 'Retrieving administrator credentials from 1Password...' -ForegroundColor Cyan + $credentials = Get-AdminCredential -ErrorAction SilentlyContinue + + if (-not $credentials) { + Write-Host '⚠️ 1Password credentials not available. Please provide credentials manually.' -ForegroundColor Yellow + $credentials = Get-AdminCredentials + if (-not $credentials) { + Write-Error 'No credentials provided. Exiting script.' + exit 1 + } + } + else { + Write-Host '✅ Successfully retrieved credentials from 1Password' -ForegroundColor Green + } + } + else { + Write-Host '✅ Using provided credentials' -ForegroundColor Green + $credentials = $Credential + } + + # Prepare target users list + $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + Write-Host "`n📋 User Filtering Enabled:" -ForegroundColor Cyan + if ($targetUsersList.Count -le 10) { + Write-Host " Target users: $($targetUsersList -join ', ')" -ForegroundColor Yellow + } + else { + Write-Host " Target users: $($targetUsersList[0..4] -join ', '), ... and $($targetUsersList.Count - 5) more" -ForegroundColor Yellow + } + Write-Host " Total target users: $($targetUsersList.Count)" -ForegroundColor Yellow + } + else { + Write-Host "`n📋 User Filtering: Disabled (all users will be processed)" -ForegroundColor Gray + } + + # Always set output path to the specified directory and filename format + $outputDir = Join-Path -Path $env:USERPROFILE -ChildPath 'Documents\Scripts\PowerShellScripts\Output\RegionFormats' + $dateTimeString = Get-Date -Format 'yyyyMMdd_HHmmss' + $OutputPath = Join-Path $outputDir "Get-UserRegionFormat_Output_${dateTimeString}.csv" + Write-Host "Output will be saved to: $OutputPath" -ForegroundColor Yellow + + # Ensure output directory exists + $outputDir = Split-Path $OutputPath -Parent + if (-not (Test-Path $outputDir)) { + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null + } + + # Initialize results array + $allResults = @() + + # Get target users list + $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile + + # Process each server + foreach ($server in $Servers) { + Write-Host "`n$('='*50)" -ForegroundColor Cyan + Write-Host "Processing server: $server" -ForegroundColor Cyan + Write-Host "$('='*50)" -ForegroundColor Cyan + + # Test connectivity + if (-not (Test-ServerConnectivity -ServerName $server -Credential $credentials)) { + # Add error entry for unreachable server + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + # Add error entries for each target user + foreach ($targetUser in $targetUsersList) { + $errorResult = [PSCustomObject]@{ + Server = $server + Username = $targetUser + RegionFormat = 'N/A' + ErrorMessage = 'Server unreachable or WinRM not available' + } + $allResults += $errorResult + } + } + else { + $errorResult = [PSCustomObject]@{ + Server = $server + Username = 'SERVER_UNREACHABLE' + RegionFormat = 'N/A' + ErrorMessage = 'Server unreachable or WinRM not available' + } + $allResults += $errorResult + } + continue + } + + # Get list of users to process for this server + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + # Use the predefined target users list + $usersToProcess = $targetUsersList + Write-Host "Processing $($usersToProcess.Count) users from user list..." -ForegroundColor Yellow + } + else { + # Get all user profiles from server + Write-Host 'Retrieving user profiles...' -ForegroundColor Yellow + $userProfiles = Get-RemoteUserProfiles -ServerName $server -Credential $credentials + + if ($userProfiles.Count -eq 0) { + Write-Warning "No user profiles found on $server - this may be due to PowerShell remoting permission issues" + $errorResult = [PSCustomObject]@{ + Server = $server + Username = 'REMOTING_ACCESS_DENIED' + RegionFormat = 'N/A' + ErrorMessage = 'PowerShell remoting access denied - check session configurations and permissions' + } + $allResults += $errorResult + continue + } + + # Extract usernames from profiles + $usersToProcess = $userProfiles | ForEach-Object { $_.Username } + Write-Host "Found $($usersToProcess.Count) user profiles on $server" -ForegroundColor Green + } + + # Process each user (regardless of source) + foreach ($username in $usersToProcess) { + Write-Host " Processing user: $username" -ForegroundColor White + + # Special handling for Default User profile + if ($username -ieq 'Default' -or $username -ieq 'Default User') { + $userResult = Get-DefaultUserRegionFormat -ServerName $server -Credential $credentials + if ($userResult) { + $result = [PSCustomObject]@{ + Server = $server + Username = $userResult.Username + RegionFormat = $userResult.RegionFormat + ErrorMessage = $userResult.ErrorMessage + } + $allResults += $result + + if ($userResult.ErrorMessage -eq $null) { + Write-Host " [SUCCESS] Region format: $($userResult.RegionFormat)" -ForegroundColor Green + } + else { + Write-Host " [WARNING] $($userResult.ErrorMessage)" -ForegroundColor Yellow + } + } + else { + Write-Host ' [ERROR] Failed to retrieve Default User region format' -ForegroundColor Red + } + } + else { + # Regular user profile handling + $userResult = Get-UserRegionFormat -ServerName $server -Username $username -Credential $credentials + if ($userResult) { + $result = [PSCustomObject]@{ + Server = $server + Username = $userResult.Username + RegionFormat = $userResult.RegionFormat + ErrorMessage = $userResult.ErrorMessage + } + $allResults += $result + + if ($userResult.UserExists) { + Write-Host " [SUCCESS] Region format: $($userResult.RegionFormat)" -ForegroundColor Green + } + else { + Write-Host ' [WARNING] User not found on server' -ForegroundColor Yellow + } + } + else { + Write-Host ' [ERROR] Failed to retrieve region format' -ForegroundColor Red + } + } + } + } + + # Display results in console and export to CSV + if ($allResults.Count -gt 0) { + # Export to CSV + $allResults | Export-Csv -Path $OutputPath -NoTypeInformation -Force + Write-Host "`n✅ Results exported to CSV:" -ForegroundColor Green + Write-Host " $OutputPath" -ForegroundColor Cyan + + # Display results table + Write-Host "`n📊 Region Format Summary:" -ForegroundColor Green + $allResults | Format-Table -AutoSize + + # Summary statistics + $successCount = @($allResults | Where-Object { -not $_.ErrorMessage }).Count + $failureCount = @($allResults | Where-Object { $_.ErrorMessage }).Count + Write-Host "`n📈 Summary:" -ForegroundColor Cyan + Write-Host " Total users processed: $($allResults.Count)" -ForegroundColor White + Write-Host " ✓ Successful: $successCount" -ForegroundColor Green + if ($failureCount -gt 0) { + Write-Host " ✗ Failed: $failureCount" -ForegroundColor Red + } + + # Show unique region formats found + $regionFormats = @($allResults | Where-Object { $_.RegionFormat -and $_.RegionFormat -ne 'N/A' } | Select-Object -ExpandProperty 'RegionFormat' | Sort-Object -Unique) + if ($regionFormats.Count -gt 0) { + Write-Host " Region format(s) found: $($regionFormats -join ', ')" -ForegroundColor Yellow + } + } + else { + Write-Warning 'No data was collected. Please check server connectivity and credentials.' + } +} +catch { + Write-Error "Script execution failed: $($_.Exception.Message)" + exit 1 +} + +Write-Host "`nScript completed." -ForegroundColor Green diff --git a/Scripts/iKAT/Registry/RegionFormat/Set-UserRegionFormats.ps1 b/Scripts/iKAT/Registry/RegionFormat/Set-UserRegionFormats.ps1 new file mode 100644 index 0000000..e997dea --- /dev/null +++ b/Scripts/iKAT/Registry/RegionFormat/Set-UserRegionFormats.ps1 @@ -0,0 +1,297 @@ +#Requires -Version 5.1 + +<# +.SYNOPSIS + Sets the region format for user profiles on remote servers. + +.DESCRIPTION + This script connects to specified remote servers and sets the region format (LocaleName) + for user profiles. It supports user filtering via parameters or user list files, and + uses the RegistryUtils module for all helper functions and remoting logic. + +.PARAMETER Servers + Array of server IP addresses or hostnames to target. + +.PARAMETER TargetUsers + Array of specific usernames to set the region format for. If specified, only these users will be processed. + +.PARAMETER UserListFile + Path to a file containing usernames to process. Supports both .txt (one username per line) and .csv formats. + +.PARAMETER RegionFormat + The region format to set (e.g., 'en-US', 'nl-NL', 'de-DE'). Must be a valid culture code. + +.PARAMETER Credential + PSCredential object for remote server authentication. If not provided, the script will attempt to retrieve credentials from 1Password environment variables, or prompt the user. + +.EXAMPLE + .\Set-UserRegionFormats.ps1 -RegionFormat 'en-US' + Sets the region format for all users on the default server(s) to English (United States). + +.EXAMPLE + .\Set-UserRegionFormats.ps1 -TargetUsers @('jsmith','admin') -RegionFormat 'nl-NL' + Sets the region format for specific users to Dutch (Netherlands). + +.EXAMPLE + .\Set-UserRegionFormats.ps1 -TargetUsers @('Default') -RegionFormat 'en-US' + Sets the default region format for new users to English (United States). + +.EXAMPLE + .\Set-UserRegionFormats.ps1 -UserListFile "C:\temp\users.txt" -RegionFormat 'en-US' + Sets the region format for users listed in a file to English (United States). + +.NOTES + Author: PowerShell Script Generator + Created: November 21, 2025 + Requires: PowerShell 5.1 or higher, Remote PowerShell access to target servers + Relies on RegistryUtils module for all helper functions. +#> + +param( + [string[]]$Servers = @('10.210.3.23'), + [string[]]$TargetUsers = @(), + [string]$UserListFile = '', + [Parameter(Mandatory = $true)] + [string]$RegionFormat, + [PSCredential]$Credential +) + +# Import the RegistryUtils module +try { + # Handle both normal execution ($PSScriptRoot) and dot-sourcing ($MyInvocation.MyCommand.Path) + $scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } + # Navigate from Scripts/Registry/RegionFormat up to project root, then into Modules + $modulePath = Join-Path -Path $scriptDir -ChildPath '..\..\..\Modules\RegistryUtils\RegistryUtils.psd1' + $modulePath = Resolve-Path $modulePath -ErrorAction Stop # Normalize the path + Import-Module $modulePath -Force -ErrorAction Stop + Write-Host '✅ RegistryUtils module loaded successfully' -ForegroundColor Green +} +catch { + Write-Error "Failed to load RegistryUtils module: $($_.Exception.Message)" + Write-Host "Make sure the module is in the correct location: $modulePath" -ForegroundColor Yellow + exit 1 +} + +# Main script execution +try { + # Validate RegionFormat + try { + $cultureInfo = [CultureInfo]::new($RegionFormat) + Write-Host "✅ Valid region format: $RegionFormat ($($cultureInfo.DisplayName))" -ForegroundColor Green + } + catch { + Write-Error "Invalid region format: '$RegionFormat'. Please provide a valid culture code (e.g., 'en-US', 'nl-NL')." + exit 1 + } + + # Get credentials - use provided credential or attempt to retrieve from 1Password + if (-not $Credential) { + Write-Host 'Retrieving administrator credentials from 1Password...' -ForegroundColor Cyan + $credentials = Get-AdminCredential -ErrorAction SilentlyContinue + + if (-not $credentials) { + Write-Host '⚠️ 1Password credentials not available. Please provide credentials manually.' -ForegroundColor Yellow + $credentials = Get-AdminCredentials + if (-not $credentials) { + Write-Error 'No credentials provided. Exiting script.' + exit 1 + } + } + else { + Write-Host '✅ Successfully retrieved credentials from 1Password' -ForegroundColor Green + } + } + else { + Write-Host '✅ Using provided credentials' -ForegroundColor Green + $credentials = $Credential + } + + # Prepare target users list + $targetUsersList = Get-TargetUsersList -TargetUsers $TargetUsers -UserListFile $UserListFile + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + Write-Host "`n📋 User Filtering Enabled:" -ForegroundColor Cyan + if ($targetUsersList.Count -le 10) { + Write-Host " Target users: $($targetUsersList -join ', ')" -ForegroundColor Yellow + } + else { + Write-Host " Target users: $($targetUsersList[0..4] -join ', '), ... and $($targetUsersList.Count - 5) more" -ForegroundColor Yellow + } + Write-Host " Total target users: $($targetUsersList.Count)" -ForegroundColor Yellow + } + else { + Write-Host "`n📋 User Filtering: Disabled (all users will be processed)" -ForegroundColor Gray + } + + # Initialize results array + $allResults = @() + + # Process each server + foreach ($server in $Servers) { + Write-Host "`n$('='*50)" -ForegroundColor Cyan + Write-Host "Processing server: $server" -ForegroundColor Cyan + Write-Host "$('='*50)" -ForegroundColor Cyan + + # Test connectivity + if (-not (Test-ServerConnectivity -ServerName $server -Credential $credentials)) { + # Add error entry for unreachable server + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + foreach ($targetUser in $targetUsersList) { + $errorResult = [PSCustomObject]@{ + Server = $server + Username = $targetUser + SetRegionResult = 'N/A' + OldRegionFormat = $null + NewRegionFormat = $null + ErrorMessage = 'Server unreachable or WinRM not available' + } + $allResults += $errorResult + } + } + else { + $errorResult = [PSCustomObject]@{ + Server = $server + Username = 'SERVER_UNREACHABLE' + SetRegionResult = 'N/A' + OldRegionFormat = $null + NewRegionFormat = $null + ErrorMessage = 'Server unreachable or WinRM not available' + } + $allResults += $errorResult + } + continue + } + + # Get list of users to process for this server + if ($targetUsersList -and $targetUsersList.Count -gt 0) { + # Use the predefined target users list + $usersToProcess = $targetUsersList + Write-Host "Processing $($usersToProcess.Count) users from user list..." -ForegroundColor Yellow + } + else { + # Get all user profiles from server + Write-Host 'Retrieving user profiles...' -ForegroundColor Yellow + $userProfiles = Get-RemoteUserProfiles -ServerName $server -Credential $credentials + + if ($userProfiles.Count -eq 0) { + Write-Warning "No user profiles found on $server - this may be due to PowerShell remoting permission issues" + $errorResult = [PSCustomObject]@{ + Server = $server + Username = 'REMOTING_ACCESS_DENIED' + SetRegionResult = 'N/A' + OldRegionFormat = $null + NewRegionFormat = $null + ErrorMessage = 'PowerShell remoting access denied - check session configurations and permissions' + } + $allResults += $errorResult + continue + } + + # Extract usernames from profiles + $usersToProcess = $userProfiles | ForEach-Object { $_.Username } + Write-Host "Found $($usersToProcess.Count) user profiles on $server" -ForegroundColor Green + } + + # Process each user (regardless of source) + foreach ($username in $usersToProcess) { + Write-Host " Processing user: $username" -ForegroundColor White + + # Special handling for Default User profile + if ($username -ieq 'Default' -or $username -ieq 'Default User') { + $setResult = Set-DefaultUserRegionFormat -ServerName $server -RegionFormat $RegionFormat -Credential $credentials + + if ($setResult) { + $result = [PSCustomObject]@{ + Server = $server + Username = 'Default User' + SetRegionResult = if ($setResult.Success) { "✓ $($setResult.NewRegionFormat)" } else { '✗ Failed' } + OldRegionFormat = $setResult.OldRegionFormat + NewRegionFormat = $setResult.NewRegionFormat + ErrorMessage = if (-not $setResult.Success) { $setResult.Message } else { $null } + } + $allResults += $result + + if ($setResult.Success) { + Write-Host " [SUCCESS] Default User region format set to $($setResult.NewRegionFormat)" -ForegroundColor Green + } + else { + Write-Host " [WARNING] $($setResult.Message)" -ForegroundColor Yellow + } + } + else { + Write-Host ' [ERROR] Failed to set Default User region format' -ForegroundColor Red + } + } + else { + # Regular user profile handling + $setResult = Set-UserRegionFormat -ServerName $server -Username $username -RegionFormat $RegionFormat -Credential $credentials + + if ($setResult) { + $result = [PSCustomObject]@{ + Server = $server + Username = $setResult.Username + SetRegionResult = if ($setResult.Success) { "✓ $($setResult.NewRegionFormat)" } else { '✗ Failed' } + OldRegionFormat = $setResult.OldRegionFormat + NewRegionFormat = $setResult.NewRegionFormat + ErrorMessage = if (-not $setResult.Success) { $setResult.Message } else { $null } + } + $allResults += $result + + if ($setResult.Success) { + Write-Host " [SUCCESS] Region format set to $($setResult.NewRegionFormat)" -ForegroundColor Green + } + else { + Write-Host " [WARNING] $($setResult.Message)" -ForegroundColor Yellow + } + } + else { + Write-Host ' [ERROR] Failed to set region format' -ForegroundColor Red + } + } + } + } + + # Display results in console and export to CSV + if ($allResults.Count -gt 0) { + # Export to CSV + $outputDir = Join-Path -Path $env:USERPROFILE -ChildPath 'Documents\Scripts\PowerShellScripts\Output\RegionFormats' + $dateTimeString = Get-Date -Format 'yyyyMMdd_HHmmss' + $csvPath = Join-Path $outputDir "Set-UserRegionFormat_Output_${dateTimeString}.csv" + + # Ensure output directory exists + if (-not (Test-Path $outputDir)) { + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null + } + + $allResults | Export-Csv -Path $csvPath -NoTypeInformation -Force + Write-Host "`n✅ Results exported to CSV:" -ForegroundColor Green + Write-Host " $csvPath" -ForegroundColor Cyan + + # Display results table + Write-Host "`n📊 Region Format Update Summary:" -ForegroundColor Green + $allResults | Format-Table -AutoSize + + # Summary statistics + $successCount = @($allResults | Where-Object { $_.SetRegionResult -match '✓' }).Count + $failureCount = @($allResults | Where-Object { $_.SetRegionResult -match '✗' -or $_.ErrorMessage }).Count + + Write-Host "`n📈 Summary:" -ForegroundColor Cyan + Write-Host " Total users processed: $($allResults.Count)" -ForegroundColor White + if ($successCount -gt 0) { + Write-Host " ✓ Successfully updated: $successCount" -ForegroundColor Green + } + if ($failureCount -gt 0) { + Write-Host " ✗ Failed: $failureCount" -ForegroundColor Red + } + + Write-Host " Target region format: $RegionFormat" -ForegroundColor Yellow + } + else { + Write-Warning 'No data was processed. Please check server connectivity and credentials.' + } +} +catch { + Write-Error "Script execution failed: $($_.Exception.Message)" + exit 1 +} + +Write-Host "`nScript completed." -ForegroundColor Green diff --git a/Scripts/iKAT/Registry/RegionFormat/test.ps1 b/Scripts/iKAT/Registry/RegionFormat/test.ps1 new file mode 100644 index 0000000..53d815d --- /dev/null +++ b/Scripts/iKAT/Registry/RegionFormat/test.ps1 @@ -0,0 +1,32 @@ +# For a user on a remote server +$ServerName = '10.210.3.23' # or your server IP +$Username = 'admin-jmaffiola' # the user you want to check +$Credential = Get-Credential + +# Get user's SID from remote server +$userProfile = Invoke-Command -ComputerName $ServerName -Credential $Credential -ScriptBlock { + param($TargetUsername) + Get-WmiObject -Class Win32_UserProfile | + Where-Object { + $_.LocalPath -match "\\$TargetUsername`$" -and + $_.Special -eq $false + } +} -ArgumentList $Username + +if ($userProfile) { + $userSID = $userProfile.SID + + # Read the user's registry value + Invoke-Command -ComputerName $ServerName -Credential $Credential -ScriptBlock { + param($SID) + + # Create HKU drive if needed + if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) { + New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null + } + + # Check the LocaleName value + Get-ItemProperty -Path "HKU:\$SID\Control Panel\International" -Name 'LocaleName' -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty 'LocaleName' + } -ArgumentList $userSID +} \ No newline at end of file From 42781823382c7c77fec62aadce4f50f8d6f7f209 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 13:47:51 -0500 Subject: [PATCH 20/33] Testing --- Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 index 20812e3..01f1664 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.ps1 @@ -13,8 +13,8 @@ $RecordingScript = Join-Path -Path $PSScriptRoot -ChildPath "Invoke-iKATRecordin # Define the parameters for this specific troubleshooting scenario using splatting $CaptureParameters = @{ - TargetUser = "jmaffiola" # The specific user encountering the issue - OutputDir = "C:\temp\RecordingsTest" # The directory where the .mkv files will be saved + TargetUser = "admin-jmaffiola" # The specific user encountering the issue + OutputDir = "C:\Recordings" # The directory where the .mkv files will be saved MinFreeSpaceGB = 1 # Lower threshold for local testing # FFmpegPath = "C:\Scripts\FFmpeg\ffmpeg.exe" # Uncomment and modify if ffmpeg is not in system PATH } From b242ed8624105c47b4b480a9d214117deaab4e68 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 14:02:11 -0500 Subject: [PATCH 21/33] Updated vbs launch scripts --- Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs | 2 +- Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs b/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs index da4d528..e904780 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Start-DinaRecording.vbs @@ -1 +1 @@ -CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-DinaRecording.ps1""", 0, False +CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\PowerShellScripts\Scripts\iKAT\Invoke-FFmpegCapture\Start-DinaRecording.ps1""", 0, False diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs b/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs index 9ccb4c4..3509a83 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Start-JoeyRecording.vbs @@ -1 +1 @@ -CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\Users\jmaffiola\Documents\Scripts\PowerShellScripts\Scripts\iCat\Invoke-FFmpegCapture\Start-JoeyRecording.ps1""", 0, False +CreateObject("WScript.Shell").Run "pwsh.exe -WindowStyle Hidden -ExecutionPolicy Bypass -NonInteractive -File ""C:\PowerShellScripts\Scripts\iKAT\Invoke-FFmpegCapture\Start-JoeyRecording.ps1""", 0, False From 7a0ea3840d8eac1211d6865ee27c37fbef767894 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 14:23:06 -0500 Subject: [PATCH 22/33] Fixing launcher script --- Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index f51b6cf..b63bb83 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -13,8 +13,12 @@ param ( ) # 1. Check if the current user matches the target user +$logDir = $OutputDir +if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } +"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Script started. USERNAME=$($env:USERNAME), TargetUser=$TargetUser" | Add-Content "$logDir\debug.log" + if ($env:USERNAME -ne $TargetUser) { - Write-Output "Current user ($env:USERNAME) does not match target user ($TargetUser). Exiting." + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - User mismatch. Exiting." | Add-Content "$logDir\debug.log" Exit } From 2f68e57913b0fcb8f22dfdf3b6098f9fa53b1ac1 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 14:25:21 -0500 Subject: [PATCH 23/33] fix: Add detailed debug logging to trace ffmpeg launch failure --- Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index b63bb83..f4a07c0 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -33,19 +33,22 @@ $driveLetter = (Get-Item $OutputDir).Root.Name $drive = Get-CimInstance -Class Win32_LogicalDisk -Filter "DeviceID='$($driveLetter.Trim('\'))'" if ($drive) { $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Free space: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB" | Add-Content "$logDir\debug.log" if ($freeSpaceGB -lt $MinFreeSpaceGB) { - Write-Warning "Not enough free space on $driveLetter (Available: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB). Exiting." + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Not enough free space. Exiting." | Add-Content "$logDir\debug.log" Exit } } # 3. Detect the full virtual desktop dimensions to capture all monitors +"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Detecting screen dimensions..." | Add-Content "$logDir\debug.log" Add-Type -AssemblyName System.Windows.Forms $virtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen $screenWidth = $virtualScreen.Width $screenHeight = $virtualScreen.Height $offsetX = $virtualScreen.X $offsetY = $virtualScreen.Y +"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Screen: ${screenWidth}x${screenHeight} offset ${offsetX},${offsetY}" | Add-Content "$logDir\debug.log" # 4. Prepare output file $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" @@ -62,7 +65,9 @@ $procInfo.RedirectStandardInput = $true $procInfo.UseShellExecute = $false $procInfo.CreateNoWindow = $true +"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Launching ffmpeg: $ffmpegArgsStr" | Add-Content "$logDir\debug.log" $ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) +"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg PID: $($ffmpegProcess.Id)" | Add-Content "$logDir\debug.log" try { # 6. Block until FFmpeg exits naturally or the script is interrupted (logoff/session end) From fe5412cb75f89dff1b37a0b3ce5672234b480fd2 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 14:29:25 -0500 Subject: [PATCH 24/33] fix: Capture ffmpeg stderr for debugging early exit on RDS --- .../iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index f4a07c0..d1bfbe8 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -62,6 +62,7 @@ $procInfo = New-Object System.Diagnostics.ProcessStartInfo $procInfo.FileName = $FFmpegPath $procInfo.Arguments = $ffmpegArgsStr $procInfo.RedirectStandardInput = $true +$procInfo.RedirectStandardError = $true $procInfo.UseShellExecute = $false $procInfo.CreateNoWindow = $true @@ -69,6 +70,14 @@ $procInfo.CreateNoWindow = $true $ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg PID: $($ffmpegProcess.Id)" | Add-Content "$logDir\debug.log" +Start-Sleep -Seconds 3 +if ($ffmpegProcess.HasExited) { + $stderr = $ffmpegProcess.StandardError.ReadToEnd() + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited early. Exit code: $($ffmpegProcess.ExitCode)" | Add-Content "$logDir\debug.log" + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg stderr: $stderr" | Add-Content "$logDir\debug.log" + Exit +} + try { # 6. Block until FFmpeg exits naturally or the script is interrupted (logoff/session end) while (-not $ffmpegProcess.HasExited) { From 0c43e876a97719af4c243a04629f501aa27b0d58 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 14:31:21 -0500 Subject: [PATCH 25/33] fix: Round screen dimensions to even numbers for libx264 compatibility --- Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index d1bfbe8..4c08e5d 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -44,8 +44,9 @@ if ($drive) { "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Detecting screen dimensions..." | Add-Content "$logDir\debug.log" Add-Type -AssemblyName System.Windows.Forms $virtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen -$screenWidth = $virtualScreen.Width -$screenHeight = $virtualScreen.Height +# Round down to nearest even number (libx264 requires even dimensions) +$screenWidth = $virtualScreen.Width - ($virtualScreen.Width % 2) +$screenHeight = $virtualScreen.Height - ($virtualScreen.Height % 2) $offsetX = $virtualScreen.X $offsetY = $virtualScreen.Y "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Screen: ${screenWidth}x${screenHeight} offset ${offsetX},${offsetY}" | Add-Content "$logDir\debug.log" From 4d34e151d799fc61c0dee7cbc3085061e1fe219d Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 15:03:22 -0500 Subject: [PATCH 26/33] fix: Remove RedirectStandardError to prevent ffmpeg pipe buffer deadlock --- .../iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 4c08e5d..d5c7311 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -63,7 +63,6 @@ $procInfo = New-Object System.Diagnostics.ProcessStartInfo $procInfo.FileName = $FFmpegPath $procInfo.Arguments = $ffmpegArgsStr $procInfo.RedirectStandardInput = $true -$procInfo.RedirectStandardError = $true $procInfo.UseShellExecute = $false $procInfo.CreateNoWindow = $true @@ -71,14 +70,6 @@ $procInfo.CreateNoWindow = $true $ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg PID: $($ffmpegProcess.Id)" | Add-Content "$logDir\debug.log" -Start-Sleep -Seconds 3 -if ($ffmpegProcess.HasExited) { - $stderr = $ffmpegProcess.StandardError.ReadToEnd() - "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited early. Exit code: $($ffmpegProcess.ExitCode)" | Add-Content "$logDir\debug.log" - "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg stderr: $stderr" | Add-Content "$logDir\debug.log" - Exit -} - try { # 6. Block until FFmpeg exits naturally or the script is interrupted (logoff/session end) while (-not $ffmpegProcess.HasExited) { From 4d0cf4f1f2667b21e73d4c90dffbd510f882a504 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 15:07:46 -0500 Subject: [PATCH 27/33] fix: Reduce MKV cluster size to 1s so recordings survive abrupt process kill --- Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index d5c7311..db80432 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -56,7 +56,7 @@ $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.mkv" # Construct the FFmpeg arguments targeting the full virtual desktop -$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p `"$outputFile`"" +$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -cluster_size_limit 500K -cluster_time_limit 1000 `"$outputFile`"" # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown $procInfo = New-Object System.Diagnostics.ProcessStartInfo From 3c0050532f3b8ee7d996a8dbbf3d2925cde30e78 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 15:10:49 -0500 Subject: [PATCH 28/33] fix: Switch to fragmented MP4 for crash-safe recordings without index dependency --- Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 4 ++-- .../iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index db80432..b0ea81e 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -53,10 +53,10 @@ $offsetY = $virtualScreen.Y # 4. Prepare output file $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.mkv" +$outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.mp4" # Construct the FFmpeg arguments targeting the full virtual desktop -$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -cluster_size_limit 500K -cluster_time_limit 1000 `"$outputFile`"" +$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -movflags frag_keyframe+empty_moov+default_base_moof `"$outputFile`"" # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown $procInfo = New-Object System.Diagnostics.ProcessStartInfo diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 index 3b1c647..2f1da5b 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 @@ -15,7 +15,7 @@ $logFile = Join-Path -Path $OutputDir -ChildPath "CleanupLog.txt" $cutoffDate = (Get-Date).AddDays(-$DaysToKeep) # Scan for .mkv files older than the retention limit -$expiredFiles = Get-ChildItem -Path $OutputDir -Filter "*.mkv" -File | Where-Object { $_.LastWriteTime -lt $cutoffDate } +$expiredFiles = Get-ChildItem -Path $OutputDir -Filter "*.mp4" -File | Where-Object { $_.LastWriteTime -lt $cutoffDate } foreach ($file in $expiredFiles) { try { From 886643a3fced9f0dd281870c9cd14cf7c746d85b Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 15:15:26 -0500 Subject: [PATCH 29/33] fix: Add explicit -f mp4 and -g 25 keyframe interval for fragmented MP4 --- Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index b0ea81e..2ee06ec 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -56,7 +56,8 @@ $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.mp4" # Construct the FFmpeg arguments targeting the full virtual desktop -$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -movflags frag_keyframe+empty_moov+default_base_moof `"$outputFile`"" +# -g 25 = keyframe every 5s at 5fps; -movflags requires output format to be mp4 +$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -g 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof `"$outputFile`"" # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown $procInfo = New-Object System.Diagnostics.ProcessStartInfo From 5f22586148c9b8c367eaab036c96ca8defb982d8 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 15:19:49 -0500 Subject: [PATCH 30/33] fix: Switch recording format to MPEG-TS to prevent data loss on abrupt session kill --- Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 7 ++++--- .../iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 2ee06ec..62ee15b 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -53,11 +53,12 @@ $offsetY = $virtualScreen.Y # 4. Prepare output file $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.mp4" +$outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.ts" # Construct the FFmpeg arguments targeting the full virtual desktop -# -g 25 = keyframe every 5s at 5fps; -movflags requires output format to be mp4 -$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -g 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof `"$outputFile`"" +# MPEG-TS format: writes self-contained 188-byte packets continuously with no trailer. +# The file is fully playable even if ffmpeg is killed abruptly (e.g., at RDS logoff). +$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -g 25 -f mpegts `"$outputFile`"" # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown $procInfo = New-Object System.Diagnostics.ProcessStartInfo diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 index 2f1da5b..fca54f4 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Remove-ExpiredRecordings.ps1 @@ -14,8 +14,8 @@ if (-not (Test-Path -Path $OutputDir)) { $logFile = Join-Path -Path $OutputDir -ChildPath "CleanupLog.txt" $cutoffDate = (Get-Date).AddDays(-$DaysToKeep) -# Scan for .mkv files older than the retention limit -$expiredFiles = Get-ChildItem -Path $OutputDir -Filter "*.mp4" -File | Where-Object { $_.LastWriteTime -lt $cutoffDate } +# Scan for .ts (MPEG-TS) recordings older than the retention limit +$expiredFiles = Get-ChildItem -Path $OutputDir -Filter "*.ts" -File | Where-Object { $_.LastWriteTime -lt $cutoffDate } foreach ($file in $expiredFiles) { try { From 06067b8534e8e633ac410c275cf885fbc84d1e4a Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 15:26:19 -0500 Subject: [PATCH 31/33] fix: Let gdigrab auto-detect resolution and add flush_packets to minimize data loss --- .../Invoke-iKATRecording.ps1 | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 62ee15b..efbdc13 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -40,25 +40,23 @@ if ($drive) { } } -# 3. Detect the full virtual desktop dimensions to capture all monitors +# 3. Log the reported screen dimensions for reference "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Detecting screen dimensions..." | Add-Content "$logDir\debug.log" Add-Type -AssemblyName System.Windows.Forms $virtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen -# Round down to nearest even number (libx264 requires even dimensions) -$screenWidth = $virtualScreen.Width - ($virtualScreen.Width % 2) -$screenHeight = $virtualScreen.Height - ($virtualScreen.Height % 2) -$offsetX = $virtualScreen.X -$offsetY = $virtualScreen.Y -"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Screen: ${screenWidth}x${screenHeight} offset ${offsetX},${offsetY}" | Add-Content "$logDir\debug.log" +"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Reported VirtualScreen: $($virtualScreen.Width)x$($virtualScreen.Height) offset $($virtualScreen.X),$($virtualScreen.Y)" | Add-Content "$logDir\debug.log" # 4. Prepare output file $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.ts" -# Construct the FFmpeg arguments targeting the full virtual desktop -# MPEG-TS format: writes self-contained 188-byte packets continuously with no trailer. -# The file is fully playable even if ffmpeg is killed abruptly (e.g., at RDS logoff). -$ffmpegArgsStr = "-f gdigrab -framerate 5 -offset_x $offsetX -offset_y $offsetY -video_size ${screenWidth}x${screenHeight} -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -g 25 -f mpegts `"$outputFile`"" +# Construct the FFmpeg arguments +# - gdigrab with no -video_size/-offset: auto-detects the full desktop at true physical pixel dimensions, +# avoiding DPI scaling mismatches that caused the resolution to appear cropped on RDS sessions. +# - scale filter: rounds width and height down to even numbers required by libx264 (no resize, just crop 1px if odd). +# - MPEG-TS: self-contained 188-byte packets, fully playable with no trailer needed. +# - flush_packets 1: flush every packet immediately to disk, minimizing data loss if ffmpeg is force-killed at logoff. +$ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -g 25 -vf `"scale=trunc(iw/2)*2:trunc(ih/2)*2`" -flush_packets 1 -f mpegts `"$outputFile`"" # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown $procInfo = New-Object System.Diagnostics.ProcessStartInfo From 8f405acb379868a1ce77477e95aa295ae8569439 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 15:30:32 -0500 Subject: [PATCH 32/33] fix: Add zerolatency tune and reduce GOP to 5 frames to eliminate encoder buffer data loss --- Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index efbdc13..be02d06 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -54,9 +54,11 @@ $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$tim # - gdigrab with no -video_size/-offset: auto-detects the full desktop at true physical pixel dimensions, # avoiding DPI scaling mismatches that caused the resolution to appear cropped on RDS sessions. # - scale filter: rounds width and height down to even numbers required by libx264 (no resize, just crop 1px if odd). +# - tune zerolatency: disables libx264 lookahead and B-frames so each frame is encoded and released immediately. +# - g 5: 1-second keyframe interval (at 5fps); combined with zerolatency the encoder buffer stays under 1 second. # - MPEG-TS: self-contained 188-byte packets, fully playable with no trailer needed. -# - flush_packets 1: flush every packet immediately to disk, minimizing data loss if ffmpeg is force-killed at logoff. -$ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -crf 30 -pix_fmt yuv420p -g 25 -vf `"scale=trunc(iw/2)*2:trunc(ih/2)*2`" -flush_packets 1 -f mpegts `"$outputFile`"" +# - flush_packets 1: flush every TS packet to disk immediately after encoding. +$ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -tune zerolatency -crf 30 -pix_fmt yuv420p -g 5 -vf `"scale=trunc(iw/2)*2:trunc(ih/2)*2`" -flush_packets 1 -f mpegts `"$outputFile`"" # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown $procInfo = New-Object System.Diagnostics.ProcessStartInfo From 8d94292b5a7685bedb9685109ba4674ebfe89cf8 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Thu, 26 Mar 2026 15:47:09 -0500 Subject: [PATCH 33/33] docs: Add March 26 implementation log entries --- Scripts/iKAT/Invoke-FFmpegCapture/log.md | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Scripts/iKAT/Invoke-FFmpegCapture/log.md diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/log.md b/Scripts/iKAT/Invoke-FFmpegCapture/log.md new file mode 100644 index 0000000..32ada43 --- /dev/null +++ b/Scripts/iKAT/Invoke-FFmpegCapture/log.md @@ -0,0 +1,30 @@ +Monday, March 16, 2026 @ 2:54 PM: +I have a rough draft of my script. I am going to install FFmpeg on the iKat server, then try the script. +3:14 PM: +FFmpeg is installed and on the PATH for all users of the machine. +3:58 PM: +Left off with getting it to work via task scheduler. right now, it records the second the task starts. +Tuesday, March 24, 2026 @ 3:03 PM: +Nate asked me to change the program behavior; I need it to record the whole session from logon to logoff, rather than a specific program. +3:26 PM: +Specific description of what needs to be changed can be found below: + +Trigger / Start Condition +Loop Structure +Display Capture +Summary of changes required +Remove the process-waiting loop and $TargetProcess parameter entirely. +FFmpeg args — drop the hardcoded -offset_x 0 -offset_y 0 -video_size 1920x1080. Instead, dynamically detect the full virtual desktop dimensions via [System.Windows.Forms.SystemInformation]::VirtualScreen. +Session termination — the try/finally already handles this correctly; when Task Scheduler kills the script on logoff, finally fires and sends "q" to FFmpeg. +Runner scripts — remove TargetProcess from Start-DinaRecording.ps1 and +Thursday, March 26, 2026 @ 1:02 PM +Changes have been made, implementing it on the iKat server now +1:30 PM: +Several issues discovered and fixed during server deployment: +- Username typo corrected: dpurner → dpuerner in all scripts +- .vbs launcher files had hardcoded dev machine paths; updated to C:\PowerShellScripts\Scripts\iKAT\Invoke-FFmpegCapture\ +- Task Scheduler was not running ffmpeg in the user's interactive desktop session. Fixed by registering tasks with LogonType Interactive and RunLevel Limited. RunLevel Highest (elevation) also blocked gdigrab from accessing the desktop on RDS. +- Recording output was being cut off: the script was reading logical (DPI-scaled) pixel dimensions via SystemInformation.VirtualScreen and passing them to gdigrab, which expects physical pixels. This caused the capture to be cropped, missing the taskbar and right side of screen. Fixed by removing manual dimension detection and letting gdigrab auto-detect the full desktop, with a -vf scale filter to enforce even dimensions required by libx264. +- Switched output format from MP4 to MPEG-TS (.ts). MP4 requires a trailer written at the end to be playable; if ffmpeg is force-killed at logoff the file is corrupt. MPEG-TS writes self-contained 188-byte packets continuously with no trailer needed, so the file is fully playable up to the last written frame even after an abrupt kill. +- Recording was still losing 5-10 seconds at the end due to libx264's encoder lookahead buffer. With a 25-frame GOP, libx264 holds up to 5 seconds of frames in memory before flushing. Fixed by adding -tune zerolatency (disables lookahead/B-frames, each frame encodes and flushes immediately) and reducing GOP to -g 5 (1-second keyframe intervals at 5fps). Also added -flush_packets 1 to force an OS-level flush after every TS packet. Maximum data loss is now under 1 second. +- Dina's task (iKAT-Record-Dina) re-registered with RunLevel Limited to match Joey's working configuration.