diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index dd7a525..9e32b7e 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -17,6 +17,7 @@ - Negation: use `Should Not BeNullOrEmpty`, `Should Not Throw`, `Should Not Be $null` - Boolean: use `Should Be $true` or `Should Be $false` - Comparison: use `Should BeGreaterThan 0` + - Collection Null Checks: Avoid piping empty collections (like `BlockingCollection` or arrays) directly to `Should Not Be $null`. PowerShell unpacks the empty collection to nothing, causing the check to fail. Instead, check using a parenthesized boolean assertion: `($null -ne $Engine.FileQueue) | Should Be $true`. ## 4. Re-Saving UTF-8 with BOM Safely - **Avoiding Encoding Corruption**: When converting/saving a file to UTF-8 with BOM via PowerShell, ensure you read the file using `-Encoding UTF8` before writing it back. Reading it as ANSI will corrupt existing Unicode glyphs: @@ -52,3 +53,14 @@ ## 8. Artifact and Workspace File Operations - **Artifact Directory Bounds**: Always write user-facing reports, plan documents, and walk-throughs in the designated conversation brain directory `C:\Users\ajjuk\.gemini\antigravity\brain\/` and provide `ArtifactMetadata`. - **Workspace Files**: Never provide `ArtifactMetadata` when creating or modifying files inside the user's workspace directory (e.g. source files, `.vscode/settings.json`, `.cursorrules`). + +## 9. C# Inline Compilation Compatibility (PowerShell 5.1+) +- **C# 5 Syntax Limits**: When defining inline C# class definitions via `Add-Type` in module files, restrict the code syntax to C# 5 or lower. Windows PowerShell 5.1 compiles code using the .NET 4.0 C# compiler, which does not support C# 6+ features (e.g., expression-bodied properties `=>`, string interpolation `$""`, null-conditional operator `?.`). +- **Property Getters**: Use standard explicit getters instead of `=>`: + ```csharp + // Correct (C# 5): + public int ProcessedCount { get { return _processedCount; } } + + // Incorrect (C# 6): + public int ProcessedCount => _processedCount; + ``` diff --git a/public/Start-DTAuditGui.ps1 b/public/Start-DTAuditGui.ps1 new file mode 100644 index 0000000..904ee77 --- /dev/null +++ b/public/Start-DTAuditGui.ps1 @@ -0,0 +1,90 @@ +function Start-DTAuditGui { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] [string]$ScanPath, + [Parameter(Mandatory = $true)] [string]$CsvLogPath, + [int]$QueueLimit = 10000 + ) + + # 1. Instantiate the WPF window from file + Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase + $XamlPath = Join-Path $PSScriptRoot "..\src\UI\MainWindow.xaml" + [xml]$XamlContent = Get-Content -Raw -Path $XamlPath + $Reader = [System.Xml.XmlNodeReader]::new($XamlContent) + $Window = [System.Windows.Markup.XamlReader]::Load($Reader) + + # 2. Extract UI control references + $StartButton = $Window.FindName("StartButton") + $ProgressBar = $Window.FindName("AuditProgressBar") + $StatusLabel = $Window.FindName("StatusText") + $PathLabel = $Window.FindName("PathText") + + $PathLabel.Text = "Target Path: $ScanPath" + + # 3. Handle click event via asynchronous worker threads + $StartButton.Add_Click({ + $StartButton.IsEnabled = $false + + # Instantiate the shared state context using our precompiled C# engine + $Engine = [DriveTools.Core.AuditEngine]::new($QueueLimit, $CsvLogPath) + $LogicalCores = [System.Environment]::ProcessorCount + + # Spawn consumer workers to compute hashes in parallel + [System.Threading.Tasks.Task]::Run({ + $RunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $LogicalCores + 1) + $RunspacePool.Open() + $ActiveTasks = [System.Collections.Generic.List[object]]::new() + + $WorkerBlock = { param([DriveTools.Core.AuditEngine]$EngineInstance) $EngineInstance.StartConsumerWorker() } + + for ($i = 0; $i -lt $LogicalCores; $i++) { + $PS = [System.Management.Automation.PowerShell]::Create().AddScript($WorkerBlock).AddArgument($Engine) + $PS.RunspacePool = $RunspacePool + $AsyncResult = $PS.BeginInvoke() + $ActiveTasks.Add([PSCustomObject]@{ Pipeline = $PS; Result = $AsyncResult }) + } + + # File System Traversal (Producer Loop) + [System.Threading.Tasks.Task]::Run({ + try { + $Files = [System.IO.Directory]::EnumerateFiles($ScanPath, "*", [System.IO.SearchOption]::AllDirectories) + foreach ($File in $Files) { + if ([System.IO.File]::GetAttributes($File).HasFlag([System.IO.FileAttributes]::ReparsePoint)) { continue } + $Engine.FileQueue.Add($File) + } + } + finally { $Engine.FileQueue.CompleteAdding() } + }) + + # 4. Decoupled UI Progress Monitor Loop (Marshals to the UI Thread) + while (-not $Engine.FileQueue.IsCompleted) { + [System.Threading.Thread]::Sleep(250) # Throttles context switches to protect CPU frames + + $Processed = $Engine.ProcessedCount + $CurrentPath = $Engine.ActiveFile + $DisplayPath = if ($CurrentPath.Length -gt 55) { "..." + $CurrentPath.Substring($CurrentPath.Length - 52) } else { $CurrentPath } + + # Safe cross-thread invocation to avoid Thread Access Exceptions + [System.Windows.Application]::Current.Dispatcher.Invoke([Action]{ + $StatusLabel.Text = "Processing Data: $Processed files hashed..." + $PathLabel.Text = "Current Node: $DisplayPath" + }) + } + + # Clean tear-down of pipeline contexts + foreach ($Task in $ActiveTasks) { + $null = $Task.Pipeline.EndInvoke($Task.Result) + $Task.Pipeline.Dispose() + } + $RunspacePool.Close(); $RunspacePool.Dispose() + + [System.Windows.Application]::Current.Dispatcher.Invoke([Action]{ + $StatusLabel.Text = "Audit Completed Successfully!" + $ProgressBar.Value = 100 + }) + }) + }) + + # Render window frame model safely + $null = $Window.ShowDialog() +} \ No newline at end of file diff --git a/src/DriveTools.GUI.ps1 b/src/DriveTools.GUI.ps1 index 30d2eac..b5fb021 100644 --- a/src/DriveTools.GUI.ps1 +++ b/src/DriveTools.GUI.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 <# .SYNOPSIS DriveTools WPF GUI — graphical launcher for all DriveTools operations. @@ -14,22 +14,35 @@ Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase # Copy PSScriptRoot to local variable to adhere to automatic variables constraint $ScriptDir = $PSScriptRoot +# Resolve the target module execution file path cleanly to feed background worker runspaces +$ModulePathToLoad = Join-Path $ScriptDir "DriveTools.psm1" + # ── Import module if not already loaded ────────────────────────────────────── if (-not (Get-Module DriveTools)) { - $localPath = Join-Path $ScriptDir "DriveTools.psm1" $modPath = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\DriveTools\2.0\DriveTools.psm1" - if (Test-Path $localPath) { - Import-Module $localPath -Force + if (Test-Path $ModulePathToLoad) { + Import-Module $ModulePathToLoad -Force } elseif (Test-Path $modPath) { + $ModulePathToLoad = $modPath Import-Module $modPath -Force } else { [System.Windows.MessageBox]::Show( - "DriveTools module not found.`nExpected local path:`n$localPath", + "DriveTools module not found.`nExpected local path:`n$ModulePathToLoad", "DriveTools GUI", "OK", "Error") | Out-Null exit 1 } +} else { + $ModulePathToLoad = (Get-Module DriveTools).Path } +# Thread-Safe Shared Context State Capsule to bridge UI and Task threads +$Script:GuiContext = [hashtable]::Synchronized(@{ + ActivePowerShell = $null + OutputCollection = $null + CustomStartTime = $null + CustomStatusText = "" +}) + # ── XAML layout ────────────────────────────────────────────────────────────── [xml]$xaml = @' -