Compartilhe que você está se especializando

A pasta C:\Windows\Installer é uma pasta do Windows que passa despercebida, até o dia você descobre o quanto ela está ocupando (e normalmente isso ocorreu porque está faltando espaço livre no disco). Apesar do nome sugerir que ali ficam apenas instaladores descartáveis, o diretório armazena arquivos para atualização, reparo e desinstalação de aplicativos, por isso que apagar esse conteúdo é uma péssima ideia. A própria Microsoft explica que esses arquivos podem ser necessários no futuro e que a exclusão pode inviabilizar e corromper o uso de programas.

E aí está o problema: A pasta cresce (e bastante), mas o Windows não oferece uma ferramenta para limpar seu conteúdo, mesmo que ao longo do tempo começem a ter arquivos de instaladores e patches que já não são mais necessários. Para muitos a solução é formatar o computador, vamos propor algumas ações corretivas.

Opção 1 – O Adobe Acrobat Reader

Infelizmente não é possível fazer esse post sem referenciar diretamente o programa que (dentre os instalados normalmente em diversos computadores) é o que mais faz download de atualizações (e o que mais deixa usa espaço na pasta Installer). Tanto que em seu próprio site ela informa chaves de registros para evitar que a ferramenta faça atualização. A estrutura geral é (altere em cinza, conforme sua realidade, consulte o seu registro e veja quais as variáveis, caso tenha dúvidas):

  • Em HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Adobe\produto\versão\FeatureLockDown\ criar bUpdater (tipo DWORD) com dado 0
  • Em HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Policies\Adobe\produto\versão\FeatureLockDown\ criar bUpdater (tipo DWORD) com dado 0

Caso queira controlar (a frequencia) do update, sem bloqueá-lo totalmente, pode ser configurado:

  • Em HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Adobe\Adobe ARM\Legacy\produto\codigo\ criar Mode (tipo DWORD) com dado 0
  • Em HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Adobe\Adobe ARM\Legacy\produto\codigo\ criar Check (tipo DWORD) com dado 720 (esse número é em decimal, sendo a quantidade em horas. Nesse caso, seriam 30 dias)

Opção 2 – Ferramentas específicas de Limpeza

Uma das ferramentas conhecidas para identificar esses arquivos é o PatchCleaner., que compara os arquivos de instalação existentes com a relação de instaladores e patches que o Windows reconhece, procurando possíveis arquivos órfãos e move esses arquivos.

Opção 3 – Script PowerShell de Limpeza

Seguindo o fluxo proposto pelo PatchCleaner, encontramos um script PowerShell do gakamor com opções interessantes (auditar, mover, deletar). Mas para usar nosso método de Comparando membros de dois arrays no PowerShell (que é muito mais rápido do que fazer “loop” e comparar item a item). O código abaixo precisa ser executado como administrador.

#Requires -RunAsAdministrator
# BASED ON: https://github.com/gakamor/public-scripts/blob/main/Cleanup-WindowsInstallerDirectory.ps1

clear-host

$folderDialog = New-Object -ComObject Shell.Application
$folder = $folderDialog.BrowseForFolder(0, "Choose the folder to save Orphaned Files", 0, 0)
$Destination= $folder.Self.Path

$excludedProducts = @(
    'Adobe',
    'ExampleProduct'
)

# FUNCTION: GET FILE METADATA
# ============================================================
function Get-FileMetaData {
    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            ValueFromPipeline
        )]
        [Object] $File,
        [switch] $Signature
    )
    Process {
        foreach ($F in $File) {
            $MetaDataObject = [ordered] @{}
            if ($F -is [string]) { $FileInformation = Get-ItemProperty -Path $F }
            elseif ($F -is [System.IO.DirectoryInfo]) { continue }
            elseif ($F -is [System.IO.FileInfo]) { $FileInformation = $F }
            else {
                Write-Warning "Get-FileMetaData - Only files are supported. Skipping $F."
                continue
            }
            $ShellApplication = New-Object -ComObject Shell.Application
            $ShellFolder = $ShellApplication.Namespace($FileInformation.Directory.FullName)
            $ShellFile = $ShellFolder.ParseName($FileInformation.Name)
            $MetaDataProperties = [ordered] @{}

            20..22 | ForEach-Object -Process {
                $DataValue = $ShellFolder.GetDetailsOf($null,$_)
                $PropertyValue = (Get-Culture).TextInfo.ToTitleCase($DataValue.Trim()).Replace(' ','')
                if ($PropertyValue -ne '') { $MetaDataProperties["$_"] = $PropertyValue }
            }
            foreach ($Key in $MetaDataProperties.Keys) {
                $Property = $MetaDataProperties[$Key]
                $Value = $ShellFolder.GetDetailsOf($ShellFile,[int] $Key)
                if ( $Property -in 'Attributes','Folder','Type','SpaceFree','TotalSize','SpaceUsed' ) { continue }
                if ( ($null -ne $Value) -and ($Value -ne '') ) { $MetaDataObject["$Property"] = $Value }
            }
            # Retrieve digital signature information when requested.
            if ($Signature) { 
                $DigitalSignature = Get-AuthenticodeSignature -FilePath $FileInformation.FullName
                $MetaDataObject[
                    'SignatureCertificateSubject'
                ] = $DigitalSignature.SignerCertificate.Subject
            }
            [PSCustomObject] $MetaDataObject
        }
    }
}

Write-Output "Reading Windows Installer information..."
# CREATE WINDOWS INSTALLER COM OBJECT
# ============================================================
$Installer = New-Object -ComObject WindowsInstaller.Installer

# ADD HELPER METHODS TO WINDOWS INSTALLER COM OBJECT
# ============================================================
$Installer | Add-Member -Name 'InvokeMethod' -MemberType ScriptMethod -Value {
    $type = $this.GetType()
    $index = $args.Count - 1
    $methodargs = $args[1..$index]
    $type.invokeMember(
        $args[0],
        [System.Reflection.BindingFlags]::InvokeMethod,
        $null,
        $this,
        $methodargs
    )
}

$Installer | Add-Member -Name 'GetProperty' -MemberType ScriptMethod -Value {
    $type = $this.GetType()
    $index = $args.Count - 1
    $methodargs = $args[1..$index]
    $type.invokeMember(
        $args[0],
        [System.Reflection.BindingFlags]::GetProperty,
        $null,
        $this,
        $methodargs
    )
}

# GET INSTALLED MSI PRODUCTS
# ============================================================
$InstallerProducts = $Installer.ProductsEx("","",7)
$InstalledProducts = foreach ($Product in $InstallerProducts) {
    try {
        [PSCustomObject]@{
            ProductCode = $Product.ProductCode()
            LocalPackage = $Product.InstallProperty("LocalPackage")
            VersionString = $Product.InstallProperty("VersionString")
            ProductName = $Product.InstallProperty("ProductName")
        }
    }
    catch {
        # Some products may have missing or inaccessible properties.
        # Ignore those products.
    }
}

# GET INSTALLED MSP PATCHES
# ============================================================
$products = $Installer.GetProperty('Products')
$InstalledPatches = foreach ($productCode in $products) {
    $patches = $Installer.GetProperty('Patches',$productCode)
    if ($patches) {
        foreach ($patchCode in $patches) {
            $location = $Installer.GetProperty('PatchInfo',$patchCode,'LocalPackage')
            if ($location) {
                $productName = $Installer.GetProperty('ProductInfo',$productCode,'ProductName')
                [PSCustomObject]@{
                    ProductCode = $productCode
                    PatchCode = $patchCode
                    LocalPackage = $location
                    ProductName = $productName
                }
            }
        }
    }
}

Write-Output "Building index..."
# CREATE HASHSET FOR INSTALLED MSI PACKAGES
# ============================================================
$InstalledMsiPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

foreach ($product in $InstalledProducts) {
    if ( -not [string]::IsNullOrWhiteSpace($product.LocalPackage) ) {
        try {
            $path = [System.IO.Path]::GetFullPath($product.LocalPackage)
            [void]$InstalledMsiPaths.Add($path)
        }
        catch {
            # Ignore invalid paths.
        }
    }
}

# CREATE HASHSET FOR INSTALLED MSP PATCHES
# ============================================================
$InstalledMspPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($patch in $InstalledPatches) {
    if ( -not [string]::IsNullOrWhiteSpace($patch.LocalPackage) ) {
        try {
            $path = [System.IO.Path]::GetFullPath($patch.LocalPackage)
            [void]$InstalledMspPaths.Add($path)
        }
        catch {
            # Ignore invalid paths.
        }
    }
}

Write-Output "Scanning..."
# GET FILES
# ============================================================
$InstallationFiles = @(Get-ChildItem -Path "$env:windir\Installer" -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "*.msi" -or $_.Name -like "*.msp" })

# COMPARE FILES AGAINST INSTALLED PACKAGES
# ============================================================
$OrphanedFiles = @($InstallationFiles | Where-Object { -not ($InstalledMsiPaths.Contains($_.FullName) -or $InstalledMspPaths.Contains($_.FullName)) })

# PROCESS ORPHANED FILES
# ============================================================
$MovedFiles = New-Object 'System.Collections.Generic.List[PSCustomObject]'

foreach ($file in $OrphanedFiles) {
    $fullname = $file.FullName
    $fileSize = $file.Length
    # Get metadata only for orphaned files
    # --------------------------------------------------------
    try {
        $metaData = $file | Get-FileMetaData -Signature | Select-Object Authors,Title,Subject,SignatureCertificateSubject
    }
    catch {
        Write-Warning "Unable to retrieve metadata for: $fullname"
        $metaData = [PSCustomObject]@{}
    }

    # Move file
    # --------------------------------------------------------
    try {
        Write-Output "Moving: $fullname"
        Move-Item -LiteralPath $fullname -Destination $Destination -Force -ErrorAction Stop
        $MovedFiles.Add(
            [PSCustomObject]@{
                FileName = $fullname
                Size = $fileSize
                Destination = $Destination
            }
        )
    }
    catch {
        Write-Warning "Failed to move '$fullname': $($_.Exception.Message)"
    }
}

# REPORT
# ============================================================
if ($MovedFiles.Count -gt 0) {
    Write-Output ""
    Write-Output "Moved Files:"
    Write-Output "------------"
    $MovedFiles | Format-Table FileName,Size,Destination -AutoSize
}
Write-Host ""
Write-Host "SUMMARY"
Write-Host "============================================================"
Write-Host "Application files scanned    : $($InstallationFiles.Count)"
Write-Host "Files moved                  : $($MovedFiles.Count)"
Write-Host ""
Write-Host "Reboot the computer and check if everything is OK. If so, clean the application installation files from the $destination folder."

Fontes/Referências

NVLAN – Comparando membros de dois arrays no PowerShell

https://adobe.com/devnet-docs/acrobatetk/tools/PrefRef/Windows/Updater-Win.html
https://github.com/gakamor/public-scripts
https://homedev.com.au/free/patchcleaner
https://learn.microsoft.com/pt-br/troubleshoot/sql/database-engine/install/windows/restore-missing-windows-installer-cache-files
https://learn.microsoft.com/pt-br/troubleshoot/windows-client/application-management/missing-windows-installer-cache

Mais Informações

Esperamos ter te ajudado e estaremos sempre a disposição para mais informações.

Se você tem interesse em algum assunto específico, tem alguma dúvida, precisa de ajuda, ou quer sugerir um post, entre em contato conosco pelo e-mail equipe@nvlan.com.br.

NVLAN - Consultoria