重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
今天看到一台windows 7 的计算机,C盘分了50GB,结果installer 目录有47GB,幸亏我对该目录启用过压缩,压缩后实际占用32GB的样子,但也足够大了,已经导致C盘满了,我删了下TEMP目录,清了c:\users\下一些很久没用的用户配置文件,救回2GB出来。但这个Installer目录为什么会占用这么多空间?什么样的靠谱方法可以缩减该尺寸。
创新互联公司专注为客户提供全方位的互联网综合服务,包含不限于网站制作、做网站、湖南网络推广、小程序开发、湖南网络营销、湖南企业策划、湖南品牌公关、搜索引擎seo、人物专访、企业宣传片、企业代运营等,从售前售中售后,我们都将竭诚为您服务,您的肯定,是我们最大的嘉奖;创新互联公司为所有大学生创业者提供湖南建站搭建服务,24小时服务热线:18982081108,官方网址:www.cdcxhl.com
列举下一些之前尝试的方法,这些方法安全,但是收效甚微。
windows 清理程序,即使使用了隐藏的高级功能,但是清理掉的空间不是很多。cmd.exe /c Cleanmgr /sageset:65535 /sagerun:65535
sageset会弹出窗口让选择清理的项目,选择后会保留在注册表中,后面sagerun就会使用这个注册表里存储的选项执行静默的清理。
OK,一般这种状况下会找些流行的专用软件来干这个事,毕竟术业有专攻,第一次使用了WICleanup。
WICleanup列出了冗余文件,而且我的文件清单上有多个文件大小都是一个尺寸,我说这个软件难道是可以算出重复文件的功能,然后把重复文件删掉?!,然后鉴于桌面说用过,我觉得应该至少问题不大吧,看了下目录下有个命令行的版本带-s 可以静默清理,我试了一下,发现清掉30GB多的空间,到installer 目录一看,我就知道坏了,里面的MSP、MSI文件全干掉了。
这个软件我后面看开发时间也是超级古老了,最近还有人发Blog介绍这个工具,而且评论区还有好多人反馈清理了好多.............没发现副作用很大么?。
pjl6523853
爱武侠的程序员2018-04-09 20:37:08#4楼
太感谢博主了!帮我清理了30G!请问博主可以转载嘛
Maxwell_STU
Maxwell_STU2018-03-11 00:45:01#3楼
感谢博主分享,突然就清理出来10G以上的空间,感觉清爽了超级多,压力一下子就小了。
KEVIN_LI_MY
KEVIN_LI_MY2017-11-29 08:59:26#2楼
我清理出了3g,也不少了。
我稍后测试了下用Wicleanup清理过的计算机,控制面板中部分程序的卸载、修复,windows 更新均有问题,主要问题是弹对话框提示找文件。
一篇看起很高深的文章指引,又是解构msi文件,又是C++清注册表,主要是一个操作,就是删除HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Installer\Products\***********\Patches
注册表,然后一股脑删installer目录的下的MSP、MSI文件。
有了上面教训,我想我得了解下此类软件的原理,然后确定可行后才能使用。首先我参考了微软的员工的解决方法,算是廖胜于无吧,大概意思就是官方仅支持通过卸载软件的方式来清理installer目录,这个Blog在评论区讨论了很多次,但似乎没有什么好的结论,也没有提供太多有价值的信息。
在我访问类似superuser 上的讨论时,我发现了这个软件patchCleaner,为啥说可能还靠谱,因为下面:
总结了上面的一些信息,我目前有下列问题,需要让实际的数据说话:
我的计划:
自己写了powershell 脚本,按照patchCleaner的思路,自己过滤出孤立的安装文件,这部分孤立文件我后来只过滤出MSI、MSP后缀的文件(这部分文件占用最大)。其他后缀的Installer目录下的文件,我们不去动它(因为可能被引用,比如ICON文件或者EXE等文件)。MSI,MSP文件当中会有一些除了发布者为微软的安装文件,比如Adobe的文件用get-msisummaryinfo 获取不到信息,我们也过滤掉(在PatchCleaner中也默认过滤掉了adobe的安装文件),过滤后的孤立安装文件大概如下图所示。
下面截图中时我的win10 的installer目录的分析情况。使用之前,用过windows 清理程序的高级功能清过。即使使用清理程序清理过,我们也可以看见孤立的文件还有大概4GB,我手动测试了两个安装程序在清理后的工作情况,一个是AMD的显卡软件的,一个是微软的c++ 2012 redistribution ,我先把文件从installer 目录剪切走,然后执行卸载或者修复功能,都没有报错或者弹框要文件。
当上面数据很明了清晰时,我们是否可以写出自己的工具来..大致臆想了自己工具的功能和执行步骤
列下PatchCleaner存在的不足的地方:
1.没有办法导出列表。
2.需要.net framwork4,不便于携带。
3.似乎没有办法可以对筛选后的孤立的文件再做选择性操作。
由于windows 上有PSMSI 这个powershell 模组,所以最开始省去我大部分代码,把主要精力放在测试上(反复考虑后,还是自己写powershell 调用Installer Com 接口的函数用于获取信息,虽然比较困难,全程要用反射功能来操作Installer COM,而且读取MSP文件额度问题已经解决,读MSP时,数据库的Openmode需要指定其他值,这样可以不依赖外部模组)。
$Installer = New-Object -ComObject WindowsInstaller.Installer
$Type = $Installer.GetType()
function Get-MsiProducts {
$Products = $Type.InvokeMember('Products', "GetProperty", $null, $Installer, $null)
foreach ($Product In $Products) {
$hash = @{}
$hash.ProductCode = $Product
$Attributes = @('Language', 'ProductName', 'PackageCode', 'Transforms', 'AssignmentType', 'PackageName', 'InstalledProductName', 'VersionString', 'RegCompany', 'RegOwner', 'ProductID', 'ProductIcon', 'InstallLocation', 'InstallSource', 'InstallDate', 'Publisher', 'LocalPackage', 'HelpLink', 'HelpTelephone', 'URLInfoAbout', 'URLUpdateInfo')
foreach ($Attribute In $Attributes) {
$hash."$($Attribute)" = $null
}
foreach ($Attribute In $Attributes) {
try {
$hash."$($Attribute)" = $Type.InvokeMember('ProductInfo',"GetProperty", $null, $Installer, @($Product, $Attribute))
} catch [System.Exception] {
}
}
if($hash."LocalPackage"){
if(test-path $hash."LocalPackage"){
$hash.size=$(get-item $hash."LocalPackage").Length
}
}
New-Object -TypeName PSObject -Property $hash
}
}
function Get-MsiPatch {
[cmdletbinding()]
param(
$product
)
$Patches = $Type.InvokeMember('Patches',"GetProperty", $null, $Installer, @($product))
foreach ($Patch In $Patches) {
$hash = @{}
$hash.ProductCode = $Product
$hash.PatchCode=$Patch
$Attributes = @('LocalPackage')
foreach ($Attribute In $Attributes) {
$hash."$($Attribute)" = $null
}
foreach ($Attribute In $Attributes) {
try {
$hash."$($Attribute)" = $Type.InvokeMember('PatchInfo', 'GetProperty', $null, $Installer, @($Patch, $Attribute))
} catch [System.Exception] {
#$error[0]|format-list –force
}
}
if($hash."LocalPackage"){
if(test-path $hash."LocalPackage"){
$hash.size=$(get-item $hash."LocalPackage").Length
}
}
New-Object -TypeName PSObject -Property $hash
}
}
function Get-MSIFileInfo {
[cmdletbinding()]
param
(
[Parameter(Mandatory = $true)]$Path
)
try {
if(test-path $path){
$path=get-item $path
$extension=$path.Extension.ToLower()
$DBOPENMODE=0
$TABLENAME='Property'
if($extension -eq '.msp'){
$DBOPENMODE=32
$TABLENAME="MsiPatchMetadata"
}
$msiProps = @{}
$Database = $Type.InvokeMember("OpenDatabase", "InvokeMethod", $Null, $Installer, @($Path.FullName, $DBOPENMODE))
$Query = "SELECT Property,Value FROM $TABLENAME"
$View = $Database.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $Database, ($Query))
$View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)|Out-Null
$record=$view.gettype().invokemember("Fetch","InvokeMethod",$null,$view,$null)
# Loop thru the table
while($record -ne $null) {
$propName=$null
$propValue=$null
$propName=$record.gettype().invokeMember("StringData","GetProperty",$null,$record,1)
$propValue= $record.gettype().invokeMember("StringData","GetProperty",$null,$record,2)
$msiProps[$propName] =$propValue
$record=$view.gettype().invokemember("Fetch","InvokeMethod",$null,$view,$null)
}
$view.gettype().invokemember("Close","InvokeMethod",$null,$view,$null)|Out-Null
# Compose a unified object to express the MSI and MSP information
# MSP 'DisplayName','ManufacturerName','Description', 'MoreInfoURL','TargetProductName'
# MSI 'ProductName','Manufacturer','ProductVersion','ProductCode','UpgradeCode'
if($extension -eq '.msi'){
New-Object -TypeName PSObject -Property @{
'DisplayName'=$msiProps['ProductName']
'Manufacturer'=$msiProps['Manufacturer']
'Version'=$msiProps['ProductVersion']
'PackageCode'=$msiProps['ProductCode']
'Description'=$msiProps['Description']
'TargetProductName'=$msiProps['TargetProductName']
'MoreInfoURL'=$msiProps['MoreInfoURL']
'Size'=$path.Length
'Path'=$path.FullName
'CreationTime'=$path.CreationTime
}
}elseif($extension -eq ".msp"){
New-Object -TypeName PSObject -Property @{
'DisplayName'=$msiProps['DisplayName']
'Manufacturer'=$msiProps['ManufacturerName']
'Version'=$msiProps['BuildNumber']
'PackageCode'=$msiProps['ProductCode']
'Description'=$msiProps['Description']
'TargetProductName'=$msiProps['TargetProductName']
'MoreInfoURL'=$msiProps['MoreInfoURL']
'Size'=$path.Length
'Path'=$path.FullName
'CreationTime'=$path.CreationTime
}
}
}
} catch {
Write-Error $_.Exception.Message
}
}
function filter_product{
param(
$productName
)
$PRODUCT_FILTER=@("adobe")
$r=$PRODUCT_FILTER|?{$productName -like "*$_*"}
if($r){
return $true
}else{
return $false
}
}
$products=Get-MsiProducts
$patches=$products|%{Get-MsiPatch -product $_.ProductCode}
$productsHash=@{}
$products|?{$_.LocalPackage}|%{$productsHash.add($_.LocalPackage,$true)}
$patchesHash=@{}
$patches|?{$_.LocalPackage}|%{if(!$patchesHash.ContainsKey($_.localPackage)){$patchesHash.add($_.LocalPackage,$true)}}
$InstallFolder="$($env:SystemRoot)\installer"
$files=dir -Recurse -Include "*.msi","*.msp" -path $InstallFolder
$Files2=$files|%{
if($productsHash.ContainsKey($_.FullName)){
$_|Add-Member -MemberType NoteProperty -Name "installerState" -Value "InstalledProduct"
}elseif($patchesHash.ContainsKey($_.FullName)){
$_|Add-Member -MemberType NoteProperty -Name "installerState" -Value "InstalledPatch"
}else{
$_|Add-Member -MemberType NoteProperty -Name "installerState" -Value "Orphaned"
}
$_
}
$groups=$files2|Group-Object -Property "installerState"
$groups|%{
@{$($_.name)=($_.group|Measure-Object -Property Length -Sum).Sum}
}
$OrphanedFiles=$($groups|?{$_.name -eq 'Orphaned'}).Group
if($OrphanedFiles){
$ValidOrphanedFiles=($OrphanedFiles|%{
$item=Get-MSIFileInfo -path $_.FullName;
if((filter_product $item.DisplayName) -or (filter_product $item.Manufacturer)){
# do nothing for this filtered products
}else{
$item
}
})
$selectedOrphanedFiles=$ValidOrphanedFiles|select DisplayName,Manufacturer,Size,Path,CreationTime|Out-GridView -PassThru -Title "select the Orphaned Files to delete"
if($ValidOrphanedFiles){
$ValidOrphanedFiles|Export-Csv -Path $PSScriptRoot\ValidOrphanedFiles.$((get-date).ToString('yyyyMMddhhmmss')).csv -NoClobber -NoTypeInformation -Encoding UTF8
}
if($selectedOrphanedFiles){
$selectedOrphanedFiles|Export-Csv -Path $PSScriptRoot\CleanedOrphanedFiles.$((get-date).ToString('yyyyMMddhhmmss')).csv -NoClobber -NoTypeInformation -Encoding UTF8
# delete code
#$selectedOrphanedFiles|remove-item -Force
}
}
使用上面的powershell 脚本在另外一台计算机运行,发现输出如下图,大部分是office的更新,还有4个关于7zip的,所以我又瞄了一眼添加删除程序里的信息。
7z 在添加删除里显示占用空间1.91GB。有点奇怪,我找到这篇参考 还有这篇blog,让我们找找7zip的注册表设置。
######### 我们需要看看HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\[IdentifyingNumber]\EstimatedSize
######### 通过win32_product 可以获取程序的IdentifyingNumber
Caption : 7-Zip 9.20 (x64 edition)
Description : 7-Zip 9.20 (x64 edition)
HelpLink : http://www.7-zip.org/support.html
HelpTelephone :
IdentifyingNumber : {23170F69-40C1-2702-0920-000001000000}
InstallDate : 20180516
InstallDate2 :
InstallLocation :
InstallSource : C:\Windows\ccmcache\7\
Language : 1033
LocalPackage : C:\Windows\Installer\623473.msi
PackageCache : C:\Windows\Installer\623473.msi
PackageCode : {23170F69-40C1-2702-0920-000002000000}
PackageName : 7z920-x64.msi
然后我们发现7zip的EstimatedSize为2004912,而symantec的EstimatedSize为608916
####### 发现7zip 应该是在注册表中写错数值了,可能是对EstimatedSize单位理解不对,正确单位是1kb的单位。
PS E:\> 594mb/608916
1022.89009321483
PS E:\> 2004912kb/1gb
1.91203308105469
PS E:\> 608916kb/1mb
594.64453125
PS E:\>
原来7Z只是软件BUG导致的尺寸显示问题,那我们接着查补丁相关的问题。
然后我们对孤立的安装文件按照尺寸排序,看看这些文件的详细内容。PSMSI模组的get-msisummaryinfo 工作似乎不是很好,所以我们看不到msp的详细信息(比如我关注的KB编号),没有关系,我们有专门工具Orca可以看MSI、MSP的信息。
以dbeb3e.msp 为例,我们看看orca的显示,切到msipatchMetadata表,这个补丁的KB编号为KB4011169
让我们看看这个KB编号的补丁打了没有,在已安装的更新中搜索这个KB编号,没有找到。
该补丁对应的ProductID 是{643AA346-D215-46E8-89B5-152AD0B7034E},在目标计算机的注册表中搜索这个ProductID 也找不到结果。
那么我们在WSUS看看这个KB4011169补丁的信息。
####### 最上面的这个补丁编号是最新的取代该补丁的补丁编号,这个补丁对我这台正在排错的计算机已经安装。
Kb4018389
KB4018330
Kb4018297
KB4011690
Kb4011636
Kb4011279
Kb4011229
这里我找到一个方法可以方便的批量看MSP的信息,而不用图形的Orca 工具。下面脚本遍历所有MSP文件,然后提取MsiPatchMetadata中的Displayname属性(包含微软补丁的KB编号)
########## 我这里使用了PSMI模组里的get-msitable
$msps=dir -recurse -path c:\windows\instaler\ -include *.msp
$xxx=$msps|%{
$displayname=Get-MSITable -Path $_.fullname -Table MsiPatchMetadata|?{$_.property -eq 'displayname'}|Select-Object -ExpandProperty "value";
[PSCustomObject]@{"displayname"=$displayname;Path=$_.fullname}
}
$xxx|out-gridview
然后在窗口里搜索以上列的取代KB4011169的补丁链KB编号,发现每个历史补丁都在。
这里假设下孤立安装文件产生的主要原因是因为补丁取代导致,那我们验证下这个测试:
把KB4011169的对应的孤立文件dbeb3e.msp 移走,然后因为取代该补丁的最新补丁KB为Kb4018389,且在正在排错的这个计算机上有安装。那我们测试卸载这个Kb4018389补丁,看是否有问题。没有问题,因为这个补丁是补丁替代链上的最后一个补丁
卸载KB4018389后,其替代的补丁KB4018330是否会在已安装的更新列表中呢?(windows 是否会还原上一个版本的补丁?)是的,第一次卸载这个补丁花了4-5小时,重启后发现前一个版本的补丁在已安装的更新中。
如果我们卸载了KB4018389,那么对应Kb4018389现在对应的MSP文件,c:\windows\installer\dd988e.msp 是否会被删掉?是的,该文件在补丁卸载后在installer目录不再存在。
假设卸载了KB4018389,我们又通过运行windows更新又把它更新上了,那么新安装的KB4018389对应的MSP文件名字是否有变化?名字有变化,变为dc993.msp
把KB4018389(kb4011196对应的最新补丁)的前一个版本补丁(KB4018330)对应的安装文件ddf836.msp 删除,那我们测试卸载这个Kb4018389补丁,看是否有问题。同时注意KB4018330或Kb4018297是否会出现在已安装更新里。 KB4018389 可以正常卸载没有问题,KB4018330由于我们把其对应的补丁的MSP文件移走所以在已安装的更新中看不到。但是我看到了KB4018297在已安装的更新当中,有意思的发现。另外这个是否如果检查更新的话,你会看到有个两个更新可用(KB4018389,KB4018330)看来我得加一个测试
我想已经有足够的信息去弄明白为什么Installer目录会变得这么大了,因为windows 保留了多个补丁的历史替代版本,当你卸载一个补丁A时,它还原上一个版本的补丁B,如果这个B还有上一个版本C,当你再卸载B时,它会还原C。
系统应该有信息保留着补丁链的信息,因此如果找到这些信息的存放位置,可以构建一个工具来保留特定数目的补丁链,比如只保留一个历史版本。
按照现有的计算机的情况分析,instaler的空间大部分是被windows的 更新所占用,特别是补丁有多个替代版本时。其中office 补丁最多。
Windows Disk Cleaner 有清理历史补丁的功能,但是不确定它的逻辑,比如我自己的windows 10 机器,使用了磁盘清理功能后,还有较多的历史补丁存在。如果要弄明白windows disk cleaner的机制,可能还要做很多的实验才有结果。
powershell 使用的PSMSI的cmdlet get-msisummaryinfo 感觉在不同操作系统上显示的信息不同,不是太可靠。所以自己最后决定还是写powershell 函数来提取关键信息,最终完成单脚本不再使用PSMSI模组。改良后的脚本最后发现了大量的SilverLight 更新残留包,占了大概12GB,因为之前使用的get-msiSummaryinfo 获取silverlight 相关补丁信息时,获取不到标题。所以会被认为是adobe的包跳过。
脚本已经转成可自解压执行的exe文件 [下载](http://down.51cto.com/data/2447291)
放个github地址吧,方便更新维护。
https://github.com/yoke88/InstallerClean