Public/Invoke-TechCareCleanup.ps1
|
function Invoke-TechCareCleanup { <# .SYNOPSIS Libera espacio en disco de forma segura y medida. .DESCRIPTION Borra temporales de usuario y de sistema, cache de Internet, informes de errores de Windows, la cache de descargas de Windows Update (parando y rearrancando los servicios en el orden correcto), la papelera de reciclaje y los componentes superados del almacen de Windows. No toca documentos, perfiles, descargas, instaladores ni aplicaciones. Lo que se borra se mide antes y despues, de modo que el informe no promete espacio que no se recupero: un fichero en uso no se borra y no se cuenta. .PARAMETER SkipWindowsUpdateCache No toca la cache de Windows Update. Util si hay una actualizacion a medio descargar que no se quiere perder. .EXAMPLE Invoke-TechCareCleanup -WhatIf Lista cada carpeta y cuanto ocupa, sin borrar nada. .EXAMPLE Invoke-TechCareCleanup -Confirm:$false Limpia sin preguntar por cada carpeta. .EXAMPLE Invoke-TechCareCleanup -SkipWindowsUpdateCache -Confirm:$false Limpieza sin parar wuauserv ni bits. .OUTPUTS PSCustomObject .LINK https://github.com/earbona23/TechCare #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([pscustomobject])] param( [switch]$SkipWindowsUpdateCache ) if (-not (Test-TechCarePlatform)) { throw $script:PlatformMessage } $findings = New-Object -TypeName System.Collections.ArrayList $actions = New-Object -TypeName System.Collections.ArrayList $recs = New-Object -TypeName System.Collections.ArrayList $status = 'Green' $mod = 'Cleanup' $deleted = [int64]0 $freeBefore = Get-TechCareFreeSpace $rawTargets = @( $env:TEMP, (Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Temp'), (Join-Path -Path $env:SystemRoot -ChildPath 'Temp'), (Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Microsoft\Windows\INetCache'), (Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Microsoft\Windows\WER') ) # %TEMP% y %LOCALAPPDATA%\Temp son la misma carpeta en una instalacion normal, # pero %TEMP% puede llegar en formato 8.3. Sin normalizar, la carpeta se # recorreria dos veces y el espacio liberado saldria contado por duplicado. $targets = New-Object -TypeName System.Collections.ArrayList foreach ($candidate in $rawTargets) { if ([string]::IsNullOrWhiteSpace($candidate)) { continue } try { $full = [System.IO.Path]::GetFullPath($candidate) } catch { continue } if (-not (Test-Path -LiteralPath $full)) { continue } $already = $false foreach ($seen in $targets) { if ($seen.Equals($full, [System.StringComparison]::OrdinalIgnoreCase)) { $already = $true; break } } if (-not $already) { [void]$targets.Add($full) } } $step = 0 foreach ($target in $targets) { $step++ $sizeBefore = Get-TechCarePathSize -Path $target if ($sizeBefore -le 0) { [void]$findings.Add(('{0}: ya vacia.' -f $target)) continue } if (-not $PSCmdlet.ShouldProcess($target, ('Eliminar contenido ({0})' -f (Format-TechCareSize -Bytes $sizeBefore)))) { [void]$findings.Add(('Vista previa: {0} contiene {1} recuperables.' -f $target, (Format-TechCareSize -Bytes $sizeBefore))) continue } Write-Progress -Activity 'TechCare' -Status ('Limpiando ' + $target) -PercentComplete ([math]::Min(50, 10 + ($step * 8))) $locked = 0 Get-ChildItem -LiteralPath $target -Force -ErrorAction SilentlyContinue | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop } catch { # Un fichero abierto por otro proceso es lo normal en %TEMP%; se # cuenta y se sigue, nunca se fuerza el cierre de un handle ajeno. $locked++ } } # Se vuelve a medir en lugar de dar por liberado el tamano inicial: los # ficheros bloqueados siguen ahi y no deben aparecer en el informe. $sizeAfter = Get-TechCarePathSize -Path $target $gain = $sizeBefore - $sizeAfter if ($gain -lt 0) { $gain = [int64]0 } $deleted += $gain [void]$actions.Add(('Limpiado {0}: {1} eliminados, {2} elementos bloqueados, {3} restantes.' -f $target, (Format-TechCareSize -Bytes $gain), $locked, (Format-TechCareSize -Bytes $sizeAfter))) Write-TechCareLog -ModuleName $mod -Message ('Limpiado {0} ({1}).' -f $target, (Format-TechCareSize -Bytes $gain)) } if (-not $SkipWindowsUpdateCache) { $wuCache = Join-Path -Path $env:SystemRoot -ChildPath 'SoftwareDistribution\Download' # Los parentesis alrededor de Test-TechCareAdmin son obligatorios. Sin # ellos PowerShell pasa "-and" y el resultado de Test-Path como # argumentos posicionales a la funcion, que no los declara, y descarta # su VALOR: el if se decide solo con lo que devuelve Test-TechCareAdmin. # Medido con contadores: la segunda condicion SI se ejecuta, lo que se # pierde es su resultado y el cortocircuito. Aqui es Test-Path, que no # tiene efecto lateral, pero la forma rota ejecutaria sin guarda # cualquier segunda condicion que si lo tuviera. if ((Test-TechCareAdmin) -and (Test-Path -LiteralPath $wuCache)) { $sizeBefore = Get-TechCarePathSize -Path $wuCache if ($PSCmdlet.ShouldProcess($wuCache, ('Vaciar cache de Windows Update ({0})' -f (Format-TechCareSize -Bytes $sizeBefore)))) { Write-Progress -Activity 'TechCare' -Status 'Cache de Windows Update' -PercentComplete 60 # dosvc (Optimizacion de distribucion) tambien mantiene handles en # esa carpeta; pararlo evita dejar media cache sin borrar. $services = @('wuauserv', 'bits', 'dosvc') $stopped = New-Object -TypeName System.Collections.ArrayList foreach ($name in $services) { $svc = Get-Service -Name $name -ErrorAction SilentlyContinue if ($null -eq $svc -or $svc.Status -ne 'Running') { continue } try { Stop-Service -Name $name -Force -ErrorAction Stop [void]$stopped.Add($name) } catch { [void]$findings.Add(('No se pudo parar el servicio {0}: {1}' -f $name, $_.Exception.Message)) } } try { $wuLocked = 0 Get-ChildItem -LiteralPath $wuCache -Force -ErrorAction SilentlyContinue | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop } catch { # Una descarga en curso mantiene el fichero abierto aunque # los servicios esten parados; se cuenta y se sigue. $wuLocked++ } } if ($wuLocked -gt 0) { [void]$findings.Add(('{0} elementos de la cache de Windows Update quedaron bloqueados.' -f $wuLocked)) } $gain = $sizeBefore - (Get-TechCarePathSize -Path $wuCache) if ($gain -lt 0) { $gain = [int64]0 } $deleted += $gain [void]$actions.Add(('Cache de Windows Update vaciada ({0}).' -f (Format-TechCareSize -Bytes $gain))) } catch { $status = 'Yellow' [void]$findings.Add('No se pudo vaciar por completo la cache de Windows Update: ' + $_.Exception.Message) } finally { # El finally es el punto del modulo: dejar servicios de # actualizacion parados es peor que no haber limpiado. foreach ($name in $stopped) { try { Start-Service -Name $name -ErrorAction Stop } catch { $status = 'Red' [void]$findings.Add(("El servicio $name NO rearranco: " + $_.Exception.Message)) [void]$recs.Add("Arrancar a mano el servicio $name antes de cerrar la intervencion.") } } } } } elseif (-not (Test-TechCareAdmin)) { [void]$findings.Add('Cache de Windows Update omitida: requiere administrador.') } } if ($PSCmdlet.ShouldProcess('Papelera de reciclaje', 'Vaciar')) { try { Clear-RecycleBin -Force -ErrorAction Stop [void]$actions.Add('Papelera de reciclaje vaciada.') } catch { # Clear-RecycleBin lanza si ya esta vacia, que no es un problema. [void]$findings.Add('Papelera vacia o no accesible: ' + $_.Exception.Message) } } $dism = Join-Path -Path $env:SystemRoot -ChildPath 'System32\DISM.exe' if ((Test-TechCareAdmin) -and (Test-Path -LiteralPath $dism)) { if ($PSCmdlet.ShouldProcess('Almacen de componentes de Windows', 'DISM /Online /Cleanup-Image /StartComponentCleanup')) { Write-Progress -Activity 'TechCare' -Status 'Limpieza de componentes' -PercentComplete 80 Write-TechCareLog -ModuleName $mod -Message 'DISM StartComponentCleanup (sin /ResetBase: las actualizaciones siguen siendo desinstalables).' $r = Invoke-TechCareProcess -FilePath $dism -ArgumentList @('/Online', '/Cleanup-Image', '/StartComponentCleanup') if ($r.ExitCode -eq 0) { [void]$actions.Add('DISM StartComponentCleanup completado.') } else { $status = 'Yellow' [void]$findings.Add(('StartComponentCleanup devolvio codigo {0}.' -f $r.ExitCode)) } Write-TechCareLog -ModuleName $mod -Level Detail -Message 'Salida de DISM registrada.' -ErrorText $r.Output } } $freeAfter = Get-TechCareFreeSpace $measured = $freeAfter - $freeBefore [void]$findings.Add(('Espacio libre antes: {0} / despues: {1}' -f (Format-TechCareSize -Bytes $freeBefore), (Format-TechCareSize -Bytes $freeAfter))) [void]$findings.Add(('Eliminado por TechCare: {0}' -f (Format-TechCareSize -Bytes $deleted))) if ($measured -gt 0) { [void]$findings.Add(('Variacion de espacio libre del volumen: +{0}' -f (Format-TechCareSize -Bytes $measured))) } elseif ($measured -lt 0) { # El volumen puede perder espacio durante la limpieza por culpa de otro # proceso. Decirlo evita que el informe parezca incoherente. [void]$findings.Add(('El volumen tiene {0} menos libres que al empezar: otro proceso escribio durante la limpieza.' -f (Format-TechCareSize -Bytes ([math]::Abs($measured))))) } Write-Progress -Activity 'TechCare' -Status 'Limpieza completada' -Completed Write-TechCareLog -ModuleName $mod -Level Success -Message ('Limpieza finalizada. Eliminado: {0}' -f (Format-TechCareSize -Bytes $deleted)) return Add-TechCareResult -Module $mod -Status $status -Findings $findings.ToArray() -Actions $actions.ToArray() -Recommendations $recs.ToArray() -SpaceFreedBytes $deleted } |