Skip to content

Commit d712f57

Browse files
committed
fix: elevation flow + protocol dual-hive delete + recycle fallback (v1.1.2)
Based on the first real cleanup run (51 OK / 106 failed): - Auto-elevation: when confirmed selection contains admin-required items and the session is not elevated, offer to open an Administrator window that re-scans and cleans exactly the selected items (clean-resume with a saved ID list). Declining cleans only what is possible and reports the skipped count - no more walls of "Access is denied". - Protocols: HKCR is a merged view of HKCU+HKLM Classes; deleting through it failed midway ("subkey does not exist"). Now deletes from both real hives with per-hive .reg backups. - Recycle Bin fallback: when the VisualBasic recycle API throws (e.g. "not supported" for ProgramData files), fall back to copy-to-backup + delete so the operation stays undoable. - Suppress system cmdlet progress bars (Remove-NetFirewallRule etc.) that painted large cyan blocks over the UI.
1 parent 05b2d31 commit d712f57

3 files changed

Lines changed: 137 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## [1.1.2] - 2026-07-06
4+
5+
### Thêm
6+
- **Tự động nâng quyền Administrator**: khi xác nhận dọn mà có mục cần admin, script hỏi "mở cửa sổ Admin?" - đồng ý thì lưu danh sách mục đã chọn, mở cửa sổ Administrator (UAC), quét lại nhanh và dọn đúng các mục đó (`-Action clean-resume`). Chọn n thì chỉ dọn phần làm được, không còn cảnh hàng trăm dòng "Access is denied".
7+
8+
### Sửa
9+
- **Protocols xóa đúng hive**: HKCR là view gộp HKCU+HKLM Classes - xóa qua HKCR gây lỗi "subkey does not exist" giữa chừng; giờ xóa thẳng ở cả hai hive thật (backup .reg từng cái).
10+
- **Recycle Bin fallback**: khi API Recycle của VisualBasic báo "not supported" (hay gặp với file trong ProgramData), tự chuyển sang copy-vào-backup rồi xóa - vẫn hoàn tác được.
11+
- **Tắt progress bar hệ thống** (`$ProgressPreference = SilentlyContinue`): các cmdlet như Remove-NetFirewallRule không còn vẽ khối progress xanh đè lên giao diện.
12+
313
## [1.1.1] - 2026-07-06
414

515
### Sửa

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.1
1+
1.1.2

WinTrash.ps1

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,17 @@ param(
3939
[string]$Language,
4040
[ValidateSet('User', 'Developer')]
4141
[string]$Role,
42-
[ValidateSet('scan', 'clean', 'downloads', 'devscan', 'install-devradar', 'install-claudefy', 'restore', 'temp', 'schedule')]
42+
[ValidateSet('scan', 'clean', 'downloads', 'devscan', 'install-devradar', 'install-claudefy', 'restore', 'temp', 'schedule', 'clean-resume')]
4343
[string]$Action
4444
)
4545

4646
$ErrorActionPreference = 'Continue'
47+
# Tắt progress bar mặc định của các cmdlet hệ thống (Get/Remove-NetFirewallRule,
48+
# Invoke-WebRequest...) - chúng vẽ khối xanh to chèn lên giao diện của mình
49+
$ProgressPreference = 'SilentlyContinue'
4750
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
4851

49-
$script:WinTrashVersion = [version]'1.1.1'
52+
$script:WinTrashVersion = [version]'1.1.2'
5053
$script:UpdateRawBase = 'https://raw.githubusercontent.com/hasoftware/WinTrash/main'
5154

5255
# ════════════════════════════ I18N ════════════════════════════
@@ -92,6 +95,10 @@ $i18n = @{
9295
UpdateFound = 'Có phiên bản mới {0} (bạn đang dùng {1}). Cập nhật ngay? [y/N]'
9396
UpdateDone = 'Cập nhật thành công - đang khởi động lại...'
9497
UpdateFail = 'Không cập nhật được: {0} - tiếp tục dùng phiên bản hiện tại.'
98+
ElevateAsk = '{0}/{1} mục cần quyền Administrator. Mở cửa sổ Admin để dọn TOÀN BỘ? [y = mở Admin / n = chỉ dọn phần làm được]'
99+
ElevateLaunched = 'Đã mở cửa sổ Administrator - phần dọn tiếp tục ở đó (quét lại nhanh rồi tự dọn đúng các mục bạn đã chọn).'
100+
SkippedAdmin = 'Đã bỏ qua {0} mục cần Administrator.'
101+
ResumeNothing = 'Không còn mục nào khớp danh sách đã chọn (có thể đã được dọn).'
95102
PickerTitle = 'CHỌN CÁC MỤC MUỐN DỌN (chưa xóa gì cho tới khi bạn xác nhận)'
96103
NothingFound= 'Không phát hiện mục nào có thể dọn. Máy sạch!'
97104
NothingSel = 'Không chọn mục nào - không làm gì cả.'
@@ -151,6 +158,10 @@ $i18n = @{
151158
UpdateFound = 'New version {0} available (you have {1}). Update now? [y/N]'
152159
UpdateDone = 'Updated successfully - restarting...'
153160
UpdateFail = 'Update failed: {0} - continuing with current version.'
161+
ElevateAsk = '{0}/{1} items require Administrator. Open an Admin window to clean EVERYTHING? [y = elevate / n = clean what is possible now]'
162+
ElevateLaunched = 'Administrator window opened - cleanup continues there (quick re-scan, then cleans exactly what you picked).'
163+
SkippedAdmin = 'Skipped {0} items that require Administrator.'
164+
ResumeNothing = 'No items match the saved selection (they may already be cleaned).'
154165
PickerTitle = 'SELECT ITEMS TO CLEAN (nothing is deleted until you confirm)'
155166
NothingFound= 'Nothing cleanable found. Your machine is clean!'
156167
NothingSel = 'Nothing selected - no action taken.'
@@ -210,6 +221,10 @@ $i18n = @{
210221
UpdateFound = '发现新版本 {0}(当前 {1})。立即更新?[y/N]'
211222
UpdateDone = '更新成功 - 正在重新启动...'
212223
UpdateFail = '更新失败:{0} - 继续使用当前版本。'
224+
ElevateAsk = '{0}/{1} 项需要管理员权限。打开管理员窗口清理全部?[y = 提权 / n = 仅清理当前可行的]'
225+
ElevateLaunched = '已打开管理员窗口 - 清理将在那里继续(快速重扫后清理您选择的项目)。'
226+
SkippedAdmin = '已跳过 {0} 个需要管理员权限的项目。'
227+
ResumeNothing = '没有与已保存选择匹配的项目(可能已被清理)。'
213228
PickerTitle = '选择要清理的项目(确认前不会删除任何内容)'
214229
NothingFound= '未发现可清理的项目。您的电脑很干净!'
215230
NothingSel = '未选择任何项目 - 不执行任何操作。'
@@ -269,6 +284,10 @@ $i18n = @{
269284
UpdateFound = 'Доступна новая версия {0} (у вас {1}). Обновить сейчас? [y/N]'
270285
UpdateDone = 'Обновление выполнено - перезапуск...'
271286
UpdateFail = 'Ошибка обновления: {0} - продолжаем с текущей версией.'
287+
ElevateAsk = '{0}/{1} пунктов требуют прав администратора. Открыть окно администратора для полной очистки? [y / n = очистить возможное]'
288+
ElevateLaunched = 'Окно администратора открыто - очистка продолжится там (пересканирование, затем очистка выбранного).'
289+
SkippedAdmin = 'Пропущено пунктов, требующих администратора: {0}.'
290+
ResumeNothing = 'Нет пунктов, соответствующих сохранённому выбору (возможно, уже очищены).'
272291
PickerTitle = 'ВЫБЕРИТЕ ПУНКТЫ ДЛЯ ОЧИСТКИ (ничего не удаляется до подтверждения)'
273292
NothingFound= 'Ничего для очистки не найдено. Ваш компьютер чист!'
274293
NothingSel = 'Ничего не выбрано - действий не выполнено.'
@@ -1283,9 +1302,11 @@ function Invoke-ScanProtocols {
12831302
if ([string]::IsNullOrWhiteSpace($command)) { continue }
12841303
$exe = Resolve-CommandPath -CommandLine $command
12851304
if ($exe -and $exe -match '^[A-Za-z]:\\' -and (Test-ExeMissing -ExePath $exe)) {
1305+
# HKCR là VIEW GỘP của HKCU+HKLM Classes - phải xóa ở hive thật,
1306+
# xóa qua HKCR sẽ vỡ giữa chừng ("subkey does not exist")
12861307
Add-Finding -Category 'Protocols' -Name "$name`://" -Target $exe `
12871308
-Detail 'Protocol handler trỏ exe đã mất' `
1288-
-RemoveKind 'RegKey' -RemoveData @{ PSPath = "Registry::HKEY_CLASSES_ROOT\$name" }
1309+
-RemoveKind 'ProtocolKey' -RemoveData @{ Name = $name }
12891310
}
12901311
} finally { $cmdKey.Close() }
12911312
} finally { $key.Close() }
@@ -1377,6 +1398,25 @@ function Invoke-ScanDevTrash {
13771398
}
13781399
}
13791400

1401+
function Test-FindingNeedsAdmin {
1402+
# Ước lượng mục này có cần Administrator để xóa không (để cảnh báo TRƯỚC khi làm)
1403+
param($Finding)
1404+
switch ($Finding.RemoveKind) {
1405+
'Service' { return $true }
1406+
'Firewall' { return $true }
1407+
'DefenderPath' { return $true }
1408+
'DefenderProcess' { return $true }
1409+
'Task' { return ($Finding.RemoveData.TaskPath -eq '\') } # task gốc thường của hệ thống
1410+
'PathEntry' { return ($Finding.RemoveData.Scope -eq 'Machine') }
1411+
'RegValue' { return ([string]$Finding.RemoveData.PSPath -match '^(HKLM:|Registry::HKEY_LOCAL_MACHINE)') }
1412+
'RegKey' { return ([string]$Finding.RemoveData.PSPath -match '^(HKLM:|Registry::HKEY_LOCAL_MACHINE)') }
1413+
'ProtocolKey' { return (Test-Path ("HKLM:\Software\Classes\{0}" -f $Finding.RemoveData.Name)) }
1414+
'RecycleDir' { return ([string]$Finding.RemoveData.Path -match '^[A-Za-z]:\\(ProgramData|Program Files)') }
1415+
'RecycleFile' { return ([string]$Finding.RemoveData.Path -match '^[A-Za-z]:\\(ProgramData|Program Files)') }
1416+
default { return $false }
1417+
}
1418+
}
1419+
13801420
# ════════════════════════ REMOVAL ENGINE ════════════════════════
13811421

13821422
function Remove-SelectedFindings {
@@ -1406,11 +1446,47 @@ function Remove-SelectedFindings {
14061446
$deferOrSkip = $true # xử lý gộp ở cuối
14071447
}
14081448
'RecycleDir' {
1409-
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory($f.RemoveData.Path, 'OnlyErrorDialogs', 'SendToRecycleBin')
1449+
try {
1450+
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory($f.RemoveData.Path, 'OnlyErrorDialogs', 'SendToRecycleBin')
1451+
} catch {
1452+
# API Recycle Bin dở chứng (vd: "not supported") -> chuyển cả thư mục
1453+
# vào thư mục backup của lần dọn này (vẫn hoàn tác được)
1454+
$destDir = Join-Path $backupDir ("dir_{0}_{1}" -f $idx, (Split-Path $f.RemoveData.Path -Leaf))
1455+
Move-Item -LiteralPath $f.RemoveData.Path -Destination $destDir -Force -ErrorAction Stop
1456+
}
14101457
if (Test-Path -LiteralPath $f.RemoveData.Path) { throw 'Thư mục vẫn còn (bị khóa?)' }
14111458
}
14121459
'RecycleFile' {
1413-
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($f.RemoveData.Path, 'OnlyErrorDialogs', 'SendToRecycleBin')
1460+
try {
1461+
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($f.RemoveData.Path, 'OnlyErrorDialogs', 'SendToRecycleBin')
1462+
} catch {
1463+
# Fallback: copy file vào backup rồi xóa thẳng (fix "not supported" ở ProgramData)
1464+
$destFile = Join-Path $backupDir ("file_{0}_{1}" -f $idx, (Split-Path $f.RemoveData.Path -Leaf))
1465+
Copy-Item -LiteralPath $f.RemoveData.Path -Destination $destFile -Force -ErrorAction Stop
1466+
Remove-Item -LiteralPath $f.RemoveData.Path -Force -ErrorAction Stop
1467+
}
1468+
}
1469+
'ProtocolKey' {
1470+
# Xóa ở CẢ HAI hive thật (HKCU + HKLM Software\Classes), backup từng cái
1471+
$protoName = $f.RemoveData.Name
1472+
$safeProto = ($protoName -replace '[^\w\.-]', '_')
1473+
$deletedAny = $false
1474+
$protoErrors = [System.Collections.Generic.List[string]]::new()
1475+
foreach ($hivePair in @(
1476+
@{ PS = "HKCU:\Software\Classes\$protoName"; Reg = "HKEY_CURRENT_USER\Software\Classes\$protoName" },
1477+
@{ PS = "HKLM:\Software\Classes\$protoName"; Reg = "HKEY_LOCAL_MACHINE\Software\Classes\$protoName" })) {
1478+
if (-not (Test-Path -Path $hivePair.PS)) { continue }
1479+
& reg.exe export $hivePair.Reg (Join-Path $backupDir "protocol_$idx`_$safeProto.reg") /y | Out-Null
1480+
try {
1481+
Remove-Item -Path $hivePair.PS -Recurse -Force -ErrorAction Stop
1482+
$deletedAny = $true
1483+
} catch { $protoErrors.Add($_.Exception.Message) }
1484+
}
1485+
if ($protoErrors.Count -gt 0) {
1486+
if ($deletedAny) { throw ('đã xóa 1 hive, hive còn lại lỗi: ' + ($protoErrors -join '; ')) }
1487+
throw ($protoErrors -join '; ')
1488+
}
1489+
if (-not $deletedAny) { throw 'không tìm thấy key ở hive nào (đã bị xóa trước đó?)' }
14141490
}
14151491
'RegValue' {
14161492
$regExe = ConvertTo-RegExePath -PSPath $f.RemoveData.PSPath
@@ -1664,6 +1740,48 @@ function Invoke-FlowClean {
16641740
$selected = @($selectedIdx | ForEach-Object { $removable[$_] })
16651741
$answer = Read-Host ($L.ConfirmDel -f $selected.Count)
16661742
if ($answer -notmatch '^[yY]') { Write-Host $L.NothingSel -ForegroundColor Yellow; return }
1743+
1744+
# Chưa phải admin mà có mục cần admin -> đề nghị mở cửa sổ Administrator
1745+
# (tránh cảnh cả trăm dòng lỗi "Access is denied" như trước)
1746+
if (-not (Test-IsAdmin)) {
1747+
$adminNeeded = @($selected | Where-Object { Test-FindingNeedsAdmin -Finding $_ })
1748+
if ($adminNeeded.Count -gt 0 -and (Test-Interactive)) {
1749+
$elevAnswer = Read-Host ($L.ElevateAsk -f $adminNeeded.Count, $selected.Count)
1750+
if ($elevAnswer -match '^[yY]') {
1751+
# Lưu danh sách ID đã chọn -> cửa sổ admin quét lại và dọn đúng các mục này
1752+
$pendingDir = Join-Path $PSScriptRoot 'WinTrashBackups'
1753+
if (-not (Test-Path -LiteralPath $pendingDir)) { New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null }
1754+
$ids = @($selected | ForEach-Object { Get-FindingId $_ })
1755+
ConvertTo-Json $ids | Set-Content -LiteralPath (Join-Path $pendingDir 'pending-clean.json') -Encoding UTF8
1756+
$argStr = '-NoProfile -ExecutionPolicy Bypass -File "{0}" -Language {1} -Action clean-resume' -f $PSCommandPath, $script:Language
1757+
Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList $argStr
1758+
Write-Host $L.ElevateLaunched -ForegroundColor Green
1759+
return
1760+
}
1761+
# Chọn n: chỉ dọn phần làm được ở quyền thường
1762+
$selected = @($selected | Where-Object { -not (Test-FindingNeedsAdmin -Finding $_) })
1763+
Write-Host ($L.SkippedAdmin -f $adminNeeded.Count) -ForegroundColor Yellow
1764+
if ($selected.Count -eq 0) { return }
1765+
}
1766+
}
1767+
Remove-SelectedFindings -Selected $selected -L $L
1768+
}
1769+
1770+
function Invoke-FlowCleanResume {
1771+
# Chạy trong cửa sổ Administrator: đọc danh sách ID đã chọn, quét lại, dọn đúng các mục đó
1772+
param([hashtable]$L)
1773+
$pendingFile = Join-Path $PSScriptRoot 'WinTrashBackups\pending-clean.json'
1774+
if (-not (Test-Path -LiteralPath $pendingFile)) { Write-Host $L.ResumeNothing -ForegroundColor Yellow; return }
1775+
$ids = @(Get-Content -LiteralPath $pendingFile -Raw | ConvertFrom-Json)
1776+
Remove-Item -LiteralPath $pendingFile -Force -ErrorAction SilentlyContinue
1777+
Invoke-AllScans -L $L
1778+
if (@($ids | Where-Object { $_ -like 'DevTrash|*' }).Count -gt 0) {
1779+
$spinnerHandle = Start-ScanSpinner -Text 'DevTrash...'
1780+
Invoke-ScanDevTrash
1781+
Stop-ScanSpinner -Handle $spinnerHandle
1782+
}
1783+
$selected = @($script:findings | Where-Object { $_.RemoveKind -ne 'None' -and $ids -contains (Get-FindingId $_) })
1784+
if ($selected.Count -eq 0) { Write-Host $L.ResumeNothing -ForegroundColor Yellow; return }
16671785
Remove-SelectedFindings -Selected $selected -L $L
16681786
}
16691787

@@ -1983,6 +2101,7 @@ function Invoke-OneAction {
19832101
'devscan' { Invoke-FlowClean -L $L -DevOnly }
19842102
'install-devradar' { Invoke-FlowInstall -L $L -Package 'devradar' }
19852103
'install-claudefy' { Invoke-FlowInstall -L $L -Package 'claudefy' }
2104+
'clean-resume' { Invoke-FlowCleanResume -L $L }
19862105
}
19872106
}
19882107

@@ -1995,6 +2114,8 @@ if ($Action) {
19952114
Show-Banner -Tagline $tagline
19962115
Show-Spinner -Label $L.Init
19972116
Invoke-OneAction -L $L -Key $Action
2117+
# Cửa sổ admin mở riêng cho clean-resume: giữ lại để người dùng đọc kết quả
2118+
if ($Action -eq 'clean-resume' -and (Test-Interactive)) { Read-Host $L.PressEnter | Out-Null }
19982119
return
19992120
}
20002121

0 commit comments

Comments
 (0)