简介:TotalUninstaller.zip 是一款面向 Visual Studio 2015 用户的卸载辅助工具,核心目标是在常规卸载不彻底时,帮助清理与 VS2015 相关的安装配置和残留组件,适合需要重装或升级到新版 VS、又遇到旧版命令提示符或工具项残留的开发者参考使用。压缩包共 20 个文件,约 1.68MB,包含 6 个 XML 配置/描述文件、4 个 DLL 组件、3 个 PDB 调试符号文件,以及 EXE 强制卸载程序、BIN 数据文件、DOCX 说明文档和 CONFIG 配置文件,整体以 Setup.ForcedUninstall 相关组件和配置管理支持库为主。现有约 555 人学习或下载。需要留意的是,作者明确提示该工具并非万能,实际卸载 VS2015 后仍可能在 C 盘发现旧版工具命令提示符等残留,需要手工删除,使用前应做好备份并谨慎评估。其价值在于提供一套可执行的强制卸载入口、配置管理与日志/符号文件,便于排查卸载过程、理解组件构成并辅助完成清理,但不建议将其视为完全自动化的彻底卸载方案。
1. 一个 VS2015 卸载卡了 40 分钟之后,我把 TotalUninstaller 拆了个底朝天
控制面板里点卸载 VS2015,进度条停在某个组件上四十分钟不动,任务管理器里安装器进程 CPU 归零——这种场面做开发机维护的都遇到过。强行结束进程的后果更难受:重装时安装器提示「已安装该产品的另一个版本」,卸载又卸不掉,进退两难。TotalUninstaller.zip 就是冲这个局面来的:它把卸载器没干完的活拆成清单和脚本,按目录、注册表、服务、包缓存四条线逐条补。这套东西适合需要重装、换版本或者纯腾磁盘空间的开发机,也适合一次要清几台机器的运维。不适合想「双击一下全清空」的人,它本质是一份需要你过目的清单,不是一键工具。
2. 拆包看家底:TotalUninstaller 里的四类文件和它们的执行顺序
拿到压缩包第一件事不是解压到桌面然后双击 exe。这类清理包的破坏力和它的便利性成正比,我一般的做法是先解到一块独立目录,用只读方式把结构摸一遍,确认没有会自作主张改系统的地方,再决定怎么用。这一节讲清楚包里通常有哪些东西、各自负责什么、以什么顺序执行才是安全的。
2.1 先解到隔离目录,只读勘察
选一个非系统盘、非 VS 安装盘的路径,比如D:\CleanupWork\TU,解压后先别运行任何脚本。用下面这段 PowerShell 把目录树和文件类型列出来,重点看有没有.bat、.cmd、.ps1、.reg、.inf这几类会真正改动系统的文件。
# 只读勘察:列出解压目录下所有脚本与注册表文件,不执行任何内容 $root = 'D:\CleanupWork\TU' Get-ChildItem $root -Recurse -File | Where-Object { $_.Extension -in '.bat','.cmd','.ps1','.reg','.inf','.exe','.vbs' } | Select-Object @{n='相对路径';e={$_.FullName.Substring($root.Length+1)}}, @{n='大小KB';e={[math]::Round($_.Length/1KB,1)}}, LastWriteTime | Sort-Object 相对路径 | Format-Table -AutoSize这段脚本只做枚举,不碰系统状态。-Recurse -File保证递归到子目录且排除目录项,Where-Object用扩展名白名单过滤,避免把几百个说明文档也刷出来。LastWriteTime值得看一眼:如果脚本的时间戳集中在某个较早的年份,说明这个包可能没跟上后续版本的目录结构,清理清单里的路径需要你自己核对一遍。
勘察阶段还要确认一件事——包里是「清单驱动」还是「硬编码扫描」。判断方法很简单:打开那个主脚本,看它删路径的方式。如果里面是一长串rd /s /q "C:\..."或者一整个.reg文件的删除键,那是清单驱动,可控;如果出现for /r C:\ %i in (*visual*studio*) do这类按关键词全盘搜的逻辑,直接放下,不要用。
2.2 四类文件的职责与执行顺序
一个完整的卸载清理包,文件通常分成四层,职责和执行顺序不能乱:
| 层次 | 典型文件 | 职责 | 执行时机 |
|---|---|---|---|
| 入口层 | run.bat、main.ps1 | 提权、写日志、按顺序调用下层 | 最先 |
| 清单层 | dirs.txt、services.txt、products.txt | 目标路径、服务名、产品码列表 | 入口层读取 |
| 动作层 | clean_dirs.ps1、del_services.bat、*.reg | 真正执行删除 | 清单确认之后 |
| 记录层 | logs\、backup\ | 逐条记录改了什么、备份原值 | 与动作层同步 |
顺序上有硬性约束:先停服务和进程,再删目录,最后动注册表。反过来做会翻车——服务还挂在某个 DLL 上,目录删不掉,脚本报一堆「拒绝访问」,你以为清完了,实际上文件还在,注册表却被抹了,下次重装直接卡在「文件已存在但产品未注册」的中间态。
我习惯把动作拆成「先干跑、再实删」两轮。很多清理包本身带了 dry-run 开关,写在入口脚本的参数里,比如-WhatIf或者--dry-run;如果没有,就自己把动作脚本里的删除语句临时注释掉,让它只打印路径。干跑一轮看输出,比事后拿数据恢复工具捞文件靠谱得多。
2.3 为什么清单驱动才敢用在 VS2015 上
VS2015 这类开发套件的卸载难点不在主程序,而在于它拖家带口装了一大堆共享组件:运行库、SDK、本地数据库实例、诊断服务、模拟器镜像、包缓存。这些组件被多个产品共用,用「关键词扫盘」的方式清理,几乎必然误伤——把共享的运行库目录删了,本机另一个版本的开发环境当场起不来,这就是典型的后悔药没处买。
清单驱动的逻辑是反过来的:只删明确列出来的路径,清单里没有的一律不动。代价是清单可能不全,残留清不干净;收益是误删风险可控。我更愿意接受「第一次没清干净,跑第二轮补」,而不是「一次清太狠,重装系统」。所以拿到这个包,第一件事是打开dirs.txt之类的清单,逐行看一眼有没有你不认识的路径——不认识的就先注释掉。
清单里通常还会有一份产品码列表,形如{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}。这是 MSI 安装数据库里的产品标识,清理注册表时按它精确匹配比按名称模糊匹配安全得多。VS2015 相关的产品码不止一个,主产品、各语言包、更新补丁各占一条,清单里列全了才谈得上「干净」。
提示:勘察阶段不要运行任何
.exe。有些清理包会把提权逻辑封装成可执行文件,双击即提权,你连它改了什么都不知道。
3. 动手前的三道保险:快照、备份、进程隔离
清理脚本再温和,也是在系统层做减法。做过一次误删就知道,删除类操作的容错窗口极短——目录进了回收站还能捞,注册表删错了就是当场报错。这一章的三件事看着繁琐,实际每件都能在十分钟内跑完,出问题时能省下半天。
3.1 采集清理前快照
快照的目的不是备份,是「对照」。清完之后你需要一个客观依据回答「到底清掉了什么、有没有清过头」。快照至少覆盖三类对象:服务列表、目标目录的存在性与体积、注册表相关键的项数。
# 清理前快照:服务、目录体积、注册表键数,统一落到带时间戳的目录 $stamp = Get-Date -Format 'yyyyMMdd_HHmm' $out = "D:\CleanupWork\snapshot\$stamp" New-Item -ItemType Directory -Path $out -Force | Out-Null # 1) 服务清单 Get-Service | Sort-Object Name | Select-Object Name, DisplayName, Status, StartType | Export-Csv "$out\services_before.csv" -NoTypeInformation -Encoding UTF8 # 2) 待清理目录的体积与文件数 $dirs = Get-Content 'D:\CleanupWork\TU\dirs.txt' | Where-Object { $_ -and -not $_.TrimStart().StartsWith('#') } $dirs | ForEach-Object { if (Test-Path $_) { $items = Get-ChildItem $_ -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum [pscustomobject]@{ Path = $_ Files = $items.Count MB = [math]::Round($items.Sum / 1MB, 1) } } } | Export-Csv "$out\dirs_before.csv" -NoTypeInformation -Encoding UTF8 # 3) 注册表关键键的子项数量 $keys = @( 'HKLM:\SOFTWARE\Classes\Installer\Products', 'HKLM:\SOFTWARE\WOW6432Node\Classes\Installer\Products' ) $keys | ForEach-Object { if (Test-Path $_) { [pscustomobject]@{ Key = $_ Count = (Get-ChildItem $_ -ErrorAction SilentlyContinue).Count } } } | Export-Csv "$out\reg_before.csv" -NoTypeInformation -Encoding UTF8$stamp用时间戳命名目录,跑第二次不会覆盖第一次的结果,回滚对比时能看出每一步的增量。Export-Csv统一加-Encoding UTF8,否则中文服务名在某些系统区域设置下会变成乱码,后面 diff 对不上。目录体积那段用-Force是为了把隐藏目录也算进去——VS 的配置目录里隐藏项不少,漏算会低估占用。-ErrorAction SilentlyContinue是必要的,清单里总有几条路径在你这台机器上不存在,不能因为一条不存在就中断整个循环。
3.2 定点备份:注册表段与关键目录
全盘备份不现实,也没必要。真正需要留底的是两类:注册表里即将被删除的键,和体积不大但重建困难的配置目录。
# 注册表定点备份:导出为 .reg,文件名带阶段前缀 $reg = 'D:\CleanupWork\backup\reg' New-Item -ItemType Directory -Path $reg -Force | Out-Null $targets = @( 'HKLM\SOFTWARE\Classes\Installer\Products', 'HKLM\SOFTWARE\WOW6432Node\VisualStudio\14.0', 'HKCU\SOFTWARE\VisualStudio\14.0' ) $i = 0 foreach ($t in $targets) { $i++ $file = Join-Path $reg ('{0:d2}_{1}.reg' -f $i, ($t -replace '[\\:]','_')) & reg.exe export $t $file /y | Out-Null if (Test-Path $file) { Write-Host "OK $file" -ForegroundColor Green } else { Write-Host "MISS $t" -ForegroundColor Yellow } }这里调的是系统自带的reg.exe而不是 PowerShell 的New-Item遍历,原因是reg export会把整个子树连同值类型一次性写进标准.reg文件,将来双击就能原样导回,兼容性最好。-replace '[\\:]','_'是把路径里的反斜杠和冒号换成下划线做文件名,Windows 文件名不允许出现这两个字符。/y表示覆盖同名文件不询问。最后那段判断很有用:某个键在你这台机器上不存在时reg export直接失败,脚本不会中断,但你得知道哪一条没备上。
目录备份用robocopy比Copy-Item稳,它天生跳过占用文件、支持断点、能处理超长路径:
# 配置目录镜像备份,/MIR 保证与源一致,/R:1 失败只重试一次 robocopy "$env:LOCALAPPDATA\VisualStudio\14.0" ` "D:\CleanupWork\backup\vs14_local" ` /MIR /R:1 /W:1 /NFL /NDL /NP/MIR是镜像模式,目标目录会与源完全同步,所以目标目录必须是空的或者专门建的,别指向已有备份目录。/R:1 /W:1把重试次数和等待时间压到最低,清理场景下不需要死磕被占用的文件,跳过就行,脚本跑完看日志里有没有*EXTRA或失败项即可。/NFL /NDL关掉文件级和目录级的逐条输出,不然几万行日志没法看。
3.3 进程与服务隔离:先停再删
删除失败的头号原因是文件被占用。VS2015 的后台进程比想象的多:诊断收集服务、包缓存维护进程、语言服务宿主,还有各路扩展留下的常驻进程。动手前把这些清干净,后面所有删除动作的失败率会下降一大截。
# 停进程 + 停服务,只针对清单内的名字,不做模糊匹配 $procs = @('devenv','VSIXInstaller','ServiceHub.*','vshost32','vshost64') foreach ($p in $procs) { Get-Process -Name $p -ErrorAction SilentlyContinue | ForEach-Object { Write-Host ("KILL {0} (PID {1})" -f $_.Name, $_.Id) Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } } # 服务按清单停,记录停止前的启动类型,方便回滚 Get-Content 'D:\CleanupWork\TU\services.txt' | Where-Object { $_ -and -not $_.TrimStart().StartsWith('#') } | ForEach-Object { $svc = Get-Service -Name $_ -ErrorAction SilentlyContinue if ($svc -and $svc.Status -ne 'Stopped') { Write-Host ("STOP {0} [{1}]" -f $svc.Name, $svc.StartType) Stop-Service -Name $svc.Name -Force -ErrorAction SilentlyContinue } }进程名里的ServiceHub.*用了通配符,这是 PowerShellGet-Process -Name支持的写法,能一次匹配同一前缀的多个进程。服务段的关键在Write-Host那行——把StartType打印出来记进日志,将来要恢复时知道该恢复成自动还是手动,很多清理脚本只删不管,恢复时全靠猜。
隔离还包含网络层面的考虑。清理过程中脚本可能会触发某些组件的在线校验或者自动修复,把网络断开(或者至少关掉相关进程的自动更新)能避免「删了一半又被拉回来」的诡异现象。这一步不是必须的,但在状态混乱的机器上能省掉不少排查时间。
4. 清理 VS2015 残留的四条主线:目录、注册表、服务、包缓存
准备工作做完,进入真正的清理环节。四条线之间有依赖关系,顺序错了会出现「删了目录但注册表还在,安装器认为产品未损坏」这种中间态。下面按我实际使用的顺序写,每条线都给出可直接跑的片段和参数说明。
4.1 目录线:用户级先行,机器级后置
目录清理的原则是「先删当前用户能删的,再处理需要提权的」。用户级目录删除不需要管理员权限,失败率低,先跑一遍能快速看到效果,也能提前暴露清单路径写错的问题。
# 目录清理:白名单驱动,先统计后删除,删除失败逐条记录 $log = 'D:\CleanupWork\logs\dirs_clean.log' $dirs = Get-Content 'D:\CleanupWork\TU\dirs.txt' | Where-Object { $_ -and -not $_.TrimStart().StartsWith('#') } foreach ($d in $dirs) { $path = [Environment]::ExpandEnvironmentVariables($d.Trim()) if (-not (Test-Path $path)) { Add-Content $log "SKIP $path (不存在)" continue } $stat = Get-ChildItem $path -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum Add-Content $log ("DEL {0} {1} files / {2:N1} MB" -f $path, $stat.Count, ($stat.Sum/1MB)) try { Remove-Item $path -Recurse -Force -ErrorAction Stop Add-Content $log "OK $path" } catch { Add-Content $log ("FAIL {0} -> {1}" -f $path, $_.Exception.Message) } }[Environment]::ExpandEnvironmentVariables是整个片段里最关键的一行。清单文件里的路径通常写成%LOCALAPPDATA%\...这种形式,PowerShell 的Test-Path不认百分号变量,不展开就会全部判成「不存在」,脚本跑得飞快却什么都没删——这是最容易被忽略的一种假成功。-ErrorAction Stop配合try/catch把每条失败原因记下来,而不是让SilentlyContinue把错误吞掉。跑完之后打开日志,FAIL的行数就是你需要人工介入的数量。
机器级目录(Program Files下的安装根目录、ProgramData下的共享配置)在这一轮之后单独处理,需要管理员权限。这里有个取舍:安装根目录删掉之后,如果注册表没清干净,安装器会进入一种「产品已注册但文件缺失」的修复状态,重装时反复提示修复。所以机器级目录我一般放在注册表清理之后再做,顺序反了就得靠/force强制重装来兜底。
4.2 注册表线:Installer 产品库与配置键分开处理
注册表这块分两个完全不同的区域,处理方式不能一样。
第一个区域是 MSI 的产品数据库,位置在HKLM\SOFTWARE\Classes\Installer\Products和对应的WOW6432Node分支下。每个已安装产品在这里有一个以产品码压缩形式命名的子键,记录着安装路径、源路径、补丁信息。VS2015 卸载不干净,十有八九是这个区域里还留着条目。清理方式是按产品码精确匹配删除,不能按名称搜——同一个名字下可能挂着多个语言版本。
# 按产品码清理 Installer 产品库,先列出再删除 $products = @( '{11111111-2222-3333-4444-555555555555}' # 示例:替换为清单里的真实产品码 ) $roots = @( 'HKLM:\SOFTWARE\Classes\Installer\Products', 'HKLM:\SOFTWARE\WOW6432Node\Classes\Installer\Products' ) foreach ($root in $roots) { Get-ChildItem $root -ErrorAction SilentlyContinue | ForEach-Object { $packed = $_.PSChildName $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue # ProductName 存在时才比对,避免空值误判 if ($props.ProductName) { Write-Host ("{0} {1}" -f $packed, $props.ProductName) } } }产品码在注册表里是「压缩」存储的,目录名不是标准 GUID 格式,所以不能直接拿清单里的 GUID 去做Test-Path。可行的做法是先按ProductName属性筛出候选,再对照dirs.txt或产品码清单确认。上面这段先只打印不删,就是为了让你看清哪个压缩键对应哪个显示名,确认无误后再把Remove-Item加上去。这一步属于「宁可慢一轮」的操作,产品库误删会导致本机其他 MSI 程序无法卸载和修复。
第二个区域是配置键,比如HKLM\SOFTWARE\WOW6432Node\VisualStudio\14.0和用户侧的HKCU\SOFTWARE\VisualStudio\14.0。这些键存的是 IDE 的个性化设置、扩展注册信息、窗口布局,删掉不影响系统,最多丢配置。这部分可以放心清,前提是 3.2 节已经导出过备份。
还有一个容易被漏掉的地方:HKCR下的文件关联和HKLM\SOFTWARE\Classes下的 ProgID。VS2015 会注册.sln、.csproj、.vcxproj等一堆扩展名关联,卸载器经常留下悬空关联,表现为双击解决方案文件报「找不到关联程序」。清理方式是先确认该 ProgID 指向的可执行文件已经不存在,再删关联,不要无条件删。
4.3 服务与包缓存线:共享的东西一律先查引用
服务清理的风险点和注册表不同——服务是活的,删之前必须确认它没有被别的产品使用。VS2015 带的服务里,诊断收集服务、本地数据库实例、仿真器管理服务都有可能是共享的。
# 服务删除:先查依赖与引用计数,再执行删除 $svcName = 'VSStandardCollectorService150' $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue if ($svc) { # 1) 查依赖:谁依赖这个服务 $dependents = (Get-Service | Where-Object { $_.ServicesDependedOn.Name -contains $svcName }).Name if ($dependents) { Write-Host ("被依赖,暂不删除:{0}" -f ($dependents -join ', ')) -ForegroundColor Yellow return } # 2) 查可执行文件归属,确认路径在待清理目录内 $bin = (Get-CimInstance Win32_Service -Filter "Name='$svcName'").PathName Write-Host ("ImagePath: {0}" -f $bin) # 3) 确认后再删 Stop-Service -Name $svcName -Force -ErrorAction SilentlyContinue & sc.exe delete $svcName }ServicesDependedOn只能查同一台机器上的服务依赖关系,查不到「某个已安装程序通过代码调用这个服务」的情况,所以第二步Win32_Service查PathName很重要:如果服务的可执行文件路径不在你的清理清单里,说明它是独立安装的组件,不该跟着 VS 一起删。sc.exe delete是真正把服务从注册表里摘掉的命令,Stop-Service只是停运行状态,两个都要做。
包缓存这条线单独说,因为它是最容易被「清理干净」这个目标带偏的地方。C:\ProgramData\Package Cache下面存的是各种安装包的原始文件,卸载和修复时安装器会回来这里找。删掉之后,本机所有依赖这些缓存的程序都没法修复和卸载——包括你没打算动的那些。清理包如果带了清空 Package Cache 的脚本,我的建议是直接注释掉,除非你确定这台机器马上要重装系统。
真的要清,用白名单方式,只删清单里明确列出、且已经确认对应产品被卸载的缓存目录:
# 包缓存按子目录白名单清理,仅删除清单命中的目录 $cacheRoot = Join-Path $env:ProgramData 'Package Cache' $allow = Get-Content 'D:\CleanupWork\TU\cache_allow.txt' | Where-Object { $_ -and -not $_.TrimStart().StartsWith('#') } Get-ChildItem $cacheRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object { if ($allow -contains $_.Name) { $mb = [math]::Round((Get-ChildItem $_.FullName -Recurse -Force | Measure-Object Length -Sum).Sum / 1MB, 1) Write-Host ("DEL {0} {1} MB" -f $_.Name, $mb) Remove-Item $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } }cache_allow.txt里放的是缓存子目录名(通常是产品码或包名),不是完整路径,这样脚本不依赖盘符和系统盘位置。-contains做的是精确匹配而非通配,避免*vs*这种写法误伤。这段代码我建议永远先注释掉删除那行跑一遍,看清输出体积再决定。
4.4 把四步串成可回滚的执行序列
四条线跑完,中间任何一步出错都需要知道退回到哪。入口脚本里我会加一个阶段标记,每完成一条线写一行到日志,回滚时按标记倒着来:
| 阶段 | 动作 | 回滚方式 |
|---|---|---|
| S1 | 停进程与服务 | 按日志里的 StartType 恢复启动类型 |
| S2 | 删用户级目录 | 从backup\vs14_local镜像拷回 |
| S3 | 清注册表配置键 | 双击对应.reg文件导入 |
| S4 | 清 Installer 产品库 | 导入01_*.reg整体还原 |
| S5 | 删机器级目录 | 只能重装,无回滚 |
S1 到 S4 都可逆,S5 不可逆。这也是我坚持把机器级目录放到最后的原因——前面的步骤全部验证通过,再动这块。日志文件按阶段追加,格式统一成时间戳 | 阶段 | 结果 | 对象,出问题时用Select-String一筛就能定位。
5. 避坑排查:五条踩过的血泪记录
清理类操作出问题的表现形式很集中,基本都是「删不掉」和「删过头」两类。下面五条是实际遇到过的,每条按现象、原因、解决写,附上定位命令。
5.1 现象:清理后重装报 1603,或者提示已安装另一版本
原因通常不是清理不干净,而是清理顺序错了。目录先删、注册表留着,安装器读产品库发现产品已注册,去校验文件却一个都找不到,直接抛 1603。另一种情况是产品库删了、包缓存也删了,安装器既认为没装过、又找不到源文件,同样报错。
解决思路是先确认产品库状态,再决定补哪一头:
# 查产品库里是否还有目标产品的残留条目 Get-ChildItem 'HKLM:\SOFTWARE\Classes\Installer\Products' -ErrorAction SilentlyContinue | ForEach-Object { $p = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue if ($p.ProductName -match 'Visual Studio') { [pscustomobject]@{ Key=$_.PSChildName; Name=$p.ProductName; Ver=$p.Version } } } | Format-Table -AutoSize有残留就补删产品库,没有残留就说明是包缓存缺失,需要重新下载安装包而不是继续清。这里最忌讳的是「再跑一遍清理脚本」——同一套动作跑两遍不会解决状态不一致,只会把还能恢复的东西也删掉。
5.2 现象:目录明明删了,安装器还说文件已存在
原因是删除动作静默失败了。Remove-Item -Recurse -Force遇到被占用的文件时会跳过该文件继续删其他项,最后返回成功,但目录本身因为非空而保留下来。日志里看不到任何错误,Test-Path却仍然返回 true。
定位方法是删完立刻验证,而不是等重装时才发现:
# 删除后立即验证,输出仍然存在的路径 Get-Content 'D:\CleanupWork\logs\dirs_clean.log' | Select-String '^OK' | ForEach-Object { $p = ($_ -split '\s+')[2] if (Test-Path $p) { Write-Host "仍存在: $p" -ForegroundColor Red } }解决要靠 3.3 节的进程隔离,加上删除后立刻复查。我现在的习惯是每个目录删完马上Test-Path一次,失败就记进FAIL列表,最后统一处理,而不是相信脚本的「执行完毕」。
5.3 现象:清完之后另一个开发环境起不来
这是删了共享组件的典型后果。VS2015 装的运行库、SDK、诊断工具很多是跨版本共用的,清理清单如果把这些路径也列进去了,另一个版本的 IDE 启动时会报缺 DLL 或者加载不了项目系统。
判断方法是看目录的创建时间和所属产品:共享组件的目录通常在Program Files (x86)\Common Files或者独立的 SDK 根目录下,不在 VS 的版本目录里。清单里出现这类路径,一律先注释。已经删了的,靠系统文件检查或者重装对应运行库补回来:
# 检查系统组件存储完整性(只读扫描,不自动修复) DISM /Online /Cleanup-Image /ScanHealth/ScanHealth只扫描不修改,确认有问题再加/RestoreHealth。这条命令跑得慢,十几分钟很正常,但它是判断「系统组件有没有被误伤」最直接的手段。
5.4 现象:注册表删除报「拒绝访问」,即使已经是管理员
原因是键的所有者是系统账户,管理员组只有读权限。VS2015 的部分安装信息键归系统所有,直接Remove-Item会被拒。解决方式是先接管所有权再删,但这一步风险高,建议只在确认键归属后再做:
# 查看键的所有者与 ACL,先诊断再决定是否接管 $key = 'HKLM:\SOFTWARE\Classes\Installer\Products\<压缩产品码>' $acl = Get-Acl $key [pscustomobject]@{ Owner = $acl.Owner Access = ($acl.Access | ForEach-Object { "$($_.IdentityReference)=$($_.RegistryRights)" }) -join '; ' } | Format-List先看清 Owner 是谁,再决定要不要改。多数情况下不需要动所有权——把清理脚本用系统权限跑(而不是普通管理员)就能删。反复接管所有权会把注册表权限结构搞乱,后患比残留本身更大。
5.5 现象:脚本跑到一半停了,日志里没有任何失败记录
最常见的原因是安全软件把脚本行为拦了。清理脚本的典型动作——批量删目录、改注册表、删服务——和恶意软件的行为特征高度重合,被拦下来时进程直接被终止,脚本自己来不及写日志。
排查顺序是:先看脚本退出码,再看安全软件的拦截记录,最后看是不是 UAC 提权失败。退出码用$LASTEXITCODE拿:
& 'D:\CleanupWork\TU\run.bat' Write-Host ("退出码: {0}" -f $LASTEXITCODE)退出码非零且日志停在某一步,基本就是被拦了。解决方式不是关掉防护,而是把工作目录和脚本加入排除项,或者改成逐条手动执行动作脚本——后者慢,但每一步的结果你都能亲眼看到。
6. 收尾验证:用前后差异表确认到底清干净没有
清完不等于清对。我一般会用清理前的快照做一次差异对比,三个维度看结果:服务数量变化、目标目录的存活情况、注册表产品库的条目数。差异表不是为了好看,是为了回答两个问题——该删的删了没有,不该删的动了没有。
# 清理后快照 + 与清理前对比 $before = Get-ChildItem 'D:\CleanupWork\snapshot' -Directory | Sort-Object Name | Select-Object -Last 1 $after = "D:\CleanupWork\snapshot\$(Get-Date -Format 'yyyyMMdd_HHmm')" New-Item -ItemType Directory -Path $after -Force | Out-Null Get-Service | Sort-Object Name | Select-Object Name, DisplayName, Status, StartType | Export-Csv "$after\services_after.csv" -NoTypeInformation -Encoding UTF8 # 服务差异:只在清理前存在、清理后消失的 $s1 = Import-Csv "$($before.FullName)\services_before.csv" | Select-Object -Expand Name $s2 = Import-Csv "$after\services_after.csv" | Select-Object -Expand Name Compare-Object $s1 $s2 | Where-Object SideIndicator -eq '<=' | Select-Object @{n='已删除的服务';e={$_.InputObject}} | Format-Table -AutoSizeCompare-Object的SideIndicator里,<=表示只在第一个集合(清理前)出现,也就是被删掉的;=>表示清理后新增的,正常情况下应该是空的,出现就说明有组件在清理过程中被自动重装了。这一步能抓出很多隐蔽问题——比如某个服务删掉之后被更新程序拉回来,你以为清了,其实没有。
目录维度的验证更直接,把清理前后的dirs_before.csv和当前实际状态对一遍:
| 检查项 | 期望结果 | 不符时的处理 |
|---|---|---|
| 清单内目录全部不存在 | Test-Path全为 False | 回到 5.2 排查占用 |
| 服务数量减少数等于清单条数 | 差值一致 | 少了说明有服务被自动恢复 |
| Installer 产品库无目标产品 | 筛不出匹配项 | 补删产品库条目 |
| 系统组件扫描无损坏 | ScanHealth无错误 | 重装对应运行库 |
| 其他版本 IDE 能正常启动 | 打开项目无报错 | 回滚共享组件相关删除 |
还有一项容易被忽略的验证:文件关联。清完之后随便双击一个解决方案文件,看能不能正常唤起 IDE 或者弹出「选择程序」对话框。前者说明关联还在(可能指向已删除的旧路径,会报错),后者说明关联被清掉了,两种都不算理想状态,需要在系统设置的默认应用里重新指定。
最后说个我自己的习惯。从那以后我每次跑这类清理包,都会强制走一遍「干跑 — 快照 — 实删 — 差异对比」四步,哪怕这台机器看起来毫无风险,哪怕时间很紧。清理类工具的坑不在于它有多猛,而在于它失败的时候往往不出声,等到重装报错才发现,可回滚的窗口早就关了。希望帮到你。
本文还有配套的精品资源,点击获取