From cf9acdf7866240b9211a4495bacac8849e0e3852 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 19 Feb 2026 16:37:26 +0000 Subject: [PATCH] v1.0.1! - Due to friend suffering issues with the SFC command, added a watchdog to detect if a command hangs for >45s and pops up a Window asking the user if they'd like to retry the command, skip the command or just outright cancel the command. - Moved Smark Disk Check and moved it to its own section with better output about your disk's help. --- WinRepair.ps1 | 352 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 253 insertions(+), 99 deletions(-) diff --git a/WinRepair.ps1 b/WinRepair.ps1 index 080c91c..21fa1f9 100644 --- a/WinRepair.ps1 +++ b/WinRepair.ps1 @@ -179,12 +179,6 @@ $script:RepairSteps = @( Command = "sfc /scannow" Phase = "System Integrity" }, - @{ - Name = "CHKDSK - Disk Scan" - Description = "Scan C: drive for filesystem errors" - Command = "chkdsk C: /scan" - Phase = "Disk Health" - }, @{ Name = "Clear Temporary Files" Description = "Remove temp files to free disk space" @@ -468,44 +462,53 @@ $splitContainer.Panel2.Controls.Add($logHeaderPanel) # ═══════════════════════════════════════════════════════════════════════════════ $buttonPanel = New-Object System.Windows.Forms.Panel $buttonPanel.Dock = [System.Windows.Forms.DockStyle]::Bottom -$buttonPanel.Height = 60 +$buttonPanel.Height = 70 $buttonPanel.Width = $form.ClientSize.Width # Crucial for Right Anchor to work correctly $buttonPanel.BackColor = $script:Colors.CardBg $buttonPanel.Padding = New-Object System.Windows.Forms.Padding(16, 10, 16, 10) +$form.Controls.Add($buttonPanel) -# Bottom border (between progress bar and buttons) +# Top border for button panel $bottomBorder = New-Object System.Windows.Forms.Panel -$bottomBorder.Dock = [System.Windows.Forms.DockStyle]::Bottom -$bottomBorder.Height = 1 +$bottomBorder.Size = New-Object System.Drawing.Size($form.ClientSize.Width, 1) +$bottomBorder.Location = New-Object System.Drawing.Point(0, 0) +$bottomBorder.Dock = [System.Windows.Forms.DockStyle]::Top $bottomBorder.BackColor = $script:Colors.Border -# Run System Repair button -$script:btnRun = New-StyledButton -Text "Run System Repair" -X 16 -Y 10 -Width 200 -Height 38 ` +# Run System Repair button (Left) +$script:btnRun = New-StyledButton -Text "Run Repair" -X 16 -Y 12 -Width 160 -Height 45 ` -BgColor $script:Colors.ButtonBg -FgColor ([System.Drawing.Color]::White) $buttonPanel.Controls.Add($script:btnRun) -# Network Reset button -$script:btnNetwork = New-StyledButton -Text "Network Reset" -X 226 -Y 10 -Width 160 -Height 38 ` - -BgColor $script:Colors.CardBgAlt -FgColor $script:Colors.Text -$script:btnNetwork.FlatAppearance.BorderColor = $script:Colors.Border -$buttonPanel.Controls.Add($script:btnNetwork) - -# Cancel button (starts disabled with muted style, turns red when active) -$script:btnCancel = New-StyledButton -Text "Cancel" -X 396 -Y 10 -Width 100 -Height 38 ` +# Cancel button (Left, next to Run) +$script:btnCancel = New-StyledButton -Text "Cancel" -X 186 -Y 12 -Width 90 -Height 45 ` -BgColor $script:Colors.CardBgAlt -FgColor $script:Colors.TextSecondary $script:btnCancel.FlatAppearance.BorderColor = $script:Colors.Border $script:btnCancel.Enabled = $false $buttonPanel.Controls.Add($script:btnCancel) -# Export Log button (anchored far right) -$script:btnExport = New-StyledButton -Text "Export Log" -X 616 -Y 10 -Width 110 -Height 38 ` +# Network Reset button (Left, next to Cancel) +$script:btnNetwork = New-StyledButton -Text "Net Reset" -X 286 -Y 12 -Width 110 -Height 45 ` + -BgColor $script:Colors.CardBgAlt -FgColor $script:Colors.Text +$script:btnNetwork.FlatAppearance.BorderColor = $script:Colors.Border +$buttonPanel.Controls.Add($script:btnNetwork) + +# Disk Check Button (Left, next to Network) +$script:btnDisk = New-StyledButton -Text "Disk Check" -X 406 -Y 12 -Width 100 -Height 45 ` + -BgColor $script:Colors.CardBgAlt -FgColor $script:Colors.Text +$script:btnDisk.FlatAppearance.BorderColor = $script:Colors.Border +$script:btnDisk.Add_Click({ Invoke-DiskCheck }) +$buttonPanel.Controls.Add($script:btnDisk) + +# Export Log button (Anchored Right) +$script:btnExport = New-StyledButton -Text "Export Log" -X ($buttonPanel.Width - 16 - 100) -Y 12 -Width 100 -Height 45 ` -BgColor $script:Colors.CardBgAlt -FgColor $script:Colors.Text $script:btnExport.FlatAppearance.BorderColor = $script:Colors.Border $script:btnExport.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right $buttonPanel.Controls.Add($script:btnExport) -# Advanced button (anchored right, next to Export Log) -$script:btnAdvanced = New-StyledButton -Text "Advanced" -X 506 -Y 10 -Width 100 -Height 38 ` +# Advanced button (Anchored Right, left of Export) +$script:btnAdvanced = New-StyledButton -Text "Advanced" -X ($buttonPanel.Width - 16 - 100 - 10 - 90) -Y 12 -Width 90 -Height 45 ` -BgColor $script:Colors.CardBgAlt -FgColor $script:Colors.Accent $script:btnAdvanced.FlatAppearance.BorderColor = $script:Colors.Accent $script:btnAdvanced.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right @@ -544,6 +547,91 @@ function Update-RunButtonText { } } +# ═══════════════════════════════════════════════════════════════════════════════ +# DISK CHECK FUNCTION (Run separately via dedicated button) +# ═══════════════════════════════════════════════════════════════════════════════ +function Invoke-DiskCheck { + if ($script:IsRunning) { return } + $script:IsRunning = $true + $script:btnRun.Enabled = $false + $script:btnNetwork.Enabled = $false + $script:btnDisk.Enabled = $false + $form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor + + try { + Write-Log "Starting Disk Health Scan (chkdsk C: /scan)..." -Level "CMD" + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = "cmd.exe" + $psi.Arguments = "/c chkdsk C: /scan" # Force standard output for parsing + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.CreateNoWindow = $true + $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $psi + $process.Start() | Out-Null + $script:CurrentProcess = $process + + $output = New-Object System.Text.StringBuilder + + while (-not $process.HasExited) { + $line = $process.StandardOutput.ReadLine() + if ($line) { + Write-Log $line -Level "INFO" + $output.AppendLine($line) | Out-Null + } + [System.Windows.Forms.Application]::DoEvents() + } + + # Capture remainder + $remain = $process.StandardOutput.ReadToEnd() + if ($remain) { + Write-Log $remain -Level "INFO" + $output.AppendLine($remain) | Out-Null + } + + $fullOutput = $output.ToString() + $script:CurrentProcess = $null + + if ($fullOutput -match "Windows has scanned the file system and found no problems") { + Write-Log "Disk Scan Complete: No errors found." -Level "SUCCESS" + [System.Windows.Forms.MessageBox]::Show( + "Great news! Your disk appears to be healthy.`n`nNo corruption was found.", + "Disk Health - Healthy", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Information + ) + } elseif ($fullOutput -match "found corruption" -or $fullOutput -match "errors found") { + Write-Log "WARNING: Disk corruption detected!" -Level "ERROR" + [System.Windows.Forms.MessageBox]::Show( + "Corruption was detected on your disk!`n`nPlease schedule a full repair by running 'chkdsk C: /f /r' in an Administrator Command Prompt and rebooting.", + "Disk Health - Corruption Found", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Error + ) + } else { + Write-Log "Disk Scan Complete." -Level "INFO" + [System.Windows.Forms.MessageBox]::Show( + "Disk Scan finished. Check the log for details.", + "Disk Health - Complete", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Information + ) + } + + } catch { + Write-Log "Disk Scan Failed: $($_.Exception.Message)" -Level "ERROR" + [System.Windows.Forms.MessageBox]::Show("Failed to run Disk Scan.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) + } finally { + $script:IsRunning = $false + $script:btnRun.Enabled = $true + $script:btnNetwork.Enabled = $true + $script:btnDisk.Enabled = $true + $form.Cursor = [System.Windows.Forms.Cursors]::Default + } +} + # Build advanced panel content $advYPos = 10 @@ -755,8 +843,20 @@ function Write-Log { default { $script:Colors.Text } } + $consoleColor = switch ($Level) { + "CMD" { "Cyan" } + "SUCCESS" { "Green" } + "ERROR" { "Red" } + "WARN" { "Yellow" } + "STEP" { "Cyan" } + default { "White" } + } + $fullMsg = "$timestamp $prefix $Message" $script:LogBuilder.AppendLine($fullMsg) | Out-Null + + # Echo to console for Debug Mode visibility + Write-Host $fullMsg -ForegroundColor $consoleColor $script:txtLog.SelectionStart = $script:txtLog.TextLength $script:txtLog.SelectionLength = 0 @@ -776,100 +876,153 @@ function Set-StepStatus { } } +# ═══════════════════════════════════════════════════════════════════════════════ +# RUN A COMMAND IN HIDDEN PROCESS WITH REAL-TIME OUTPUT # ═══════════════════════════════════════════════════════════════════════════════ # RUN A COMMAND IN HIDDEN PROCESS WITH REAL-TIME OUTPUT # ═══════════════════════════════════════════════════════════════════════════════ function Invoke-RepairCommand { param([string]$Command, [int]$StepIndex) - Write-Log "Executing: $Command" -Level "CMD" - Set-StepStatus -Index $StepIndex -Status "running" - $script:lblStatus.Text = "Running - $($script:RepairSteps[$StepIndex].Name)" - [System.Windows.Forms.Application]::DoEvents() - - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = "cmd.exe" - $psi.Arguments = "/c $Command 2>&1" - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $false - $psi.CreateNoWindow = $true - $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden - - try { - $process = New-Object System.Diagnostics.Process - $process.StartInfo = $psi + $retryStep = $false + + do { + $retryStep = $false + Write-Log "Executing: $Command" -Level "CMD" - $process.Start() | Out-Null - $script:CurrentProcess = $process - - # Use ReadLineAsync to poll output without blocking UI and avoiding - # the thread-safety crashes of OutputDataReceived events in PowerShell. - $reader = $process.StandardOutput - $readTask = $reader.ReadLineAsync() + # Add friendly feedback for slow-starting SFC + if ($Command -match "sfc /scannow") { + Write-Log "Initializing System File Checker (this may take up to 30-60 seconds)..." -Level "INFO" + } - while (-not $process.HasExited -or ($readTask -ne $null -and $readTask.IsCompleted)) { - # Process output - if ($readTask -ne $null -and $readTask.IsCompleted) { - try { - $line = $readTask.Result - if ($line -ne $null) { - # Sanitize output: Remove control chars (like backspaces/nulls) but keep tabs - $cleanLine = $line -replace "[\x00-\x08\x0B-\x1F\x7F]", "" - if ($cleanLine.Trim() -ne "") { Write-Log $cleanLine.Trim() } - $readTask = $reader.ReadLineAsync() - } else { - $readTask = $null # End of stream + Set-StepStatus -Index $StepIndex -Status "running" + $script:lblStatus.Text = "Running - $($script:RepairSteps[$StepIndex].Name)" + [System.Windows.Forms.Application]::DoEvents() + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = "cmd.exe" + $psi.Arguments = "/c $Command 2>&1" + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $false + $psi.CreateNoWindow = $true + $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden + + try { + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $psi + + $process.Start() | Out-Null + $script:CurrentProcess = $process + $lastActivity = Get-Date + + # Use ReadLineAsync to poll output without blocking UI and avoiding + # the thread-safety crashes of OutputDataReceived events in PowerShell. + $reader = $process.StandardOutput + $readTask = $reader.ReadLineAsync() + + while (-not $process.HasExited -or ($readTask -ne $null -and $readTask.IsCompleted)) { + # Process output + if ($readTask -ne $null -and $readTask.IsCompleted) { + try { + $line = $readTask.Result + if ($line -ne $null) { + $lastActivity = Get-Date # Reset timeout on activity + # Sanitize output: Remove control chars (like backspaces/nulls) but keep tabs + $cleanLine = $line -replace "[\x00-\x08\x0B-\x1F\x7F]", "" + if ($cleanLine.Trim() -ne "") { Write-Log $cleanLine.Trim() } + $readTask = $reader.ReadLineAsync() + } else { + $readTask = $null # End of stream + } + } catch { + $readTask = $null + } + } + + # Timeout Check (45 Seconds of silence) + if (((Get-Date) - $lastActivity).TotalSeconds -gt 45) { + $result = [System.Windows.Forms.MessageBox]::Show( + "The current command appears to be stuck (no output for 45s).`n`nDo you want to retry the step, skip it, or cancel the repair?", + "Command Timeout Warning", + [System.Windows.Forms.MessageBoxButtons]::AbortRetryIgnore, + [System.Windows.Forms.MessageBoxIcon]::Warning + ) + + # Abort = Cancel, Retry = Retry, Ignore = Skip + if ($result -eq [System.Windows.Forms.DialogResult]::Abort) { + try { if (-not $process.HasExited) { $process.Kill() } } catch {} + Write-Log "Operation cancelled by user (Timeout)" -Level "WARN" + Set-StepStatus -Index $StepIndex -Status "error" + $script:CurrentProcess = $null + try { $process.Dispose() } catch {} + return $false + } + elseif ($result -eq [System.Windows.Forms.DialogResult]::Retry) { + try { if (-not $process.HasExited) { $process.Kill() } } catch {} + Write-Log "Timeout detected. Retrying step..." -Level "WARN" + $script:CurrentProcess = $null + try { $process.Dispose() } catch {} + $retryStep = $true + break # Break inner loop to restart do-while + } + elseif ($result -eq [System.Windows.Forms.DialogResult]::Ignore) { + try { if (-not $process.HasExited) { $process.Kill() } } catch {} + Write-Log "Timeout detected. Skipping step..." -Level "WARN" + Set-StepStatus -Index $StepIndex -Status "success" # Treat as success to continue? Or error? Usually skip implies 'ignore error' + $script:CurrentProcess = $null + try { $process.Dispose() } catch {} + return $true + } + } + + if ($script:CancelRequested) { + try { if (-not $process.HasExited) { $process.Kill() } } catch {} + Write-Log "Operation cancelled by user" -Level "WARN" + Set-StepStatus -Index $StepIndex -Status "error" + $script:CurrentProcess = $null + try { $process.Dispose() } catch {} + return $false + } + + [System.Windows.Forms.Application]::DoEvents() + Start-Sleep -Milliseconds 50 + } + + if ($retryStep) { continue } # fast track to retry + + # Drain any remaining output after exit + if ($readTask -ne $null) { + # process has exited, so just read to end synchronously + $remaining = $reader.ReadToEnd() + if ($remaining) { + foreach ($l in ($remaining -split "`n")) { + if ($l.Trim()) { Write-Log $l.Trim() } } - } catch { - $readTask = $null } } - if ($script:CancelRequested) { - try { if (-not $process.HasExited) { $process.Kill() } } catch {} - Write-Log "Operation cancelled by user" -Level "WARN" + $exitCode = $process.ExitCode + $script:CurrentProcess = $null + try { $process.Dispose() } catch {} + + if ($exitCode -eq 0) { + Write-Log "$($script:RepairSteps[$StepIndex].Name) completed successfully" -Level "SUCCESS" + Set-StepStatus -Index $StepIndex -Status "success" + return $true + } else { + Write-Log "$($script:RepairSteps[$StepIndex].Name) exited with code $exitCode" -Level "ERROR" Set-StepStatus -Index $StepIndex -Status "error" - $script:CurrentProcess = $null - try { $process.Dispose() } catch {} return $false } - - [System.Windows.Forms.Application]::DoEvents() - Start-Sleep -Milliseconds 50 } - - # Drain any remaining output after exit - if ($readTask -ne $null) { - # process has exited, so just read to end synchronously (it should be buffered) - $remaining = $reader.ReadToEnd() - if ($remaining) { - foreach ($l in ($remaining -split "`n")) { - if ($l.Trim()) { Write-Log $l.Trim() } - } - } - } - - $exitCode = $process.ExitCode - $script:CurrentProcess = $null - try { $process.Dispose() } catch {} - - if ($exitCode -eq 0) { - Write-Log "$($script:RepairSteps[$StepIndex].Name) completed successfully" -Level "SUCCESS" - Set-StepStatus -Index $StepIndex -Status "success" - return $true - } else { - Write-Log "$($script:RepairSteps[$StepIndex].Name) exited with code $exitCode" -Level "ERROR" + catch { + Write-Log "Failed: $($_.Exception.Message)" -Level "ERROR" Set-StepStatus -Index $StepIndex -Status "error" + $script:CurrentProcess = $null return $false } - } - catch { - Write-Log "Failed: $($_.Exception.Message)" -Level "ERROR" - Set-StepStatus -Index $StepIndex -Status "error" - $script:CurrentProcess = $null - return $false - } + } while ($retryStep) } # ═══════════════════════════════════════════════════════════════════════════════ @@ -1395,3 +1548,4 @@ $form.Add_Shown({ }) [System.Windows.Forms.Application]::Run($form) +exit 0