今天看到一臺windows 7 的計算機,C盤分了50GB,結果installer 目錄有47GB,幸好我對該目錄啓用過壓縮,壓縮後實際佔用32GB的樣子,但也足夠大了,已經致使C盤滿了,我刪了下TEMP目錄,清了c:\users\下一些好久沒用的用戶配置文件,救回2GB出來。但這個Installer目錄爲何會佔用這麼多空間?什麼樣的靠譜方法能夠縮減該尺寸。html
列舉下一些以前嘗試的方法,這些方法安全,可是收效甚微。c++
windows 清理程序,即便使用了隱藏的高級功能,可是清理掉的空間不是不少。cmd.exe /c Cleanmgr /sageset:65535 /sagerun:65535
sageset會彈出窗口讓選擇清理的項目,選擇後會保留在註冊表中,後面sagerun就會使用這個註冊表裏存儲的選項執行靜默的清理。git
OK,通常這種情況下會找些流行的專用軟件來幹這個事,畢竟術業有專攻,第一次使用了WICleanup。程序員
WICleanup列出了冗餘文件,並且個人文件清單上有多個文件大小都是一個尺寸,我說這個軟件難道是能夠算出重複文件的功能,而後把重複文件刪掉?!,而後鑑於桌面說用過,我以爲應該至少問題不大吧,看了下目錄下有個命令行的版本帶-s 能夠靜默清理,我試了一下,發現清掉30GB多的空間,到installer 目錄一看,我就知道壞了,裏面的MSP、MSI文件全乾掉了。github
這個軟件我後面看開發時間也是超級古老了,最近還有人發Blog介紹這個工具,並且評論區還有好多人反饋清理了好多.............沒發現反作用很大麼?。redis
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 更新均有問題,主要問題是彈對話框提示找文件。shell
一篇看起很高深的文章指引,又是解構msi文件,又是C++清註冊表,主要是一個操做,就是刪除HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Installer\Products\***********\Patches
註冊表,而後一股腦刪installer目錄的下的MSP、MSI文件。數據庫
有了上面教訓,我想我得了解下此類軟件的原理,而後肯定可行後才能使用。首先我參考了微軟的員工的解決方法,算是廖勝於無吧,大概意思就是官方僅支持經過卸載軟件的方式來清理installer目錄,這個Blog在評論區討論了不少次,但彷佛沒有什麼好的結論,也沒有提供太多有價值的信息。express
在我訪問相似superuser 上的討論時,我發現了這個軟件patchCleaner,爲啥說可能還靠譜,由於下面:windows
總結了上面的一些信息,我目前有下列問題,須要讓實際的數聽說話:
個人計劃:
本身寫了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