Skip to content

No log file? #1

Description

@PCAssistSoftware

Output says "Log: C:\Temp\PatchCleanerPS.log" - but it never creates this log

Amended version below with working logging:

# PatchCleanerPS — Interactive & Auto Modes
# Version       : 1.0.0
# Author        : jackharvest
# License       : MIT
# Last Updated  : 2025-07-07
# =============================================================================
# PowerShell successor to PatchCleaner. Scans **C:\Windows\Installer** for
# orphaned .MSI / .MSP files, then deletes or quarantines them. Empty
# sub-folders are pruned afterwards.

param(
    [string[]] $ExcludeVendors = @(),
    [switch]   $Auto        = $false,
    [switch]   $AutoAll     = $false,
    [switch]   $AutoDry     = $false,
    [switch]   $AutoDryAll  = $false
)

# ---------------------------------------------------------------------------
# Update-check: Ensure this exe is the latest release (skip when running .ps1)
# ---------------------------------------------------------------------------
function Check-LatestVersion {
    param([string]$CurrentVersion)
    try {
        $apiUrl = 'https://api.github.com/repos/jackharvest/PatchCleanerPS/releases/latest'
        $resp   = Invoke-RestMethod -Uri $apiUrl -Headers @{ 'User-Agent' = 'PatchCleanerPS' } -ErrorAction Stop
        $latest = $resp.tag_name.TrimStart('v','V')
        if ([version]$CurrentVersion -lt [version]$latest) {
            Write-Warning "PatchCleanerPS v$latest is available (you have v$CurrentVersion)."
			Write-Warning "Download it here: https://github.com/jackharvest/PatchCleanerPS/releases"		
            exit 1
        }
    } catch {
		Write-Verbose "Version check failed: $_"
	}
}

# Only run update-check when packaged as .exe, dynamically get version from exe																		 
if ($MyInvocation.MyCommand.Path -match '\.exe$') {
    $scriptPath     = $MyInvocation.MyCommand.Path
    $currentVersion = (Get-Item $scriptPath).VersionInfo.FileVersion
    Check-LatestVersion -CurrentVersion $currentVersion
}

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function Format-Size([long]$bytes) {
    if ($bytes -lt 1MB)   { "{0:N2} KB" -f ($bytes/1KB) }
    elseif ($bytes -lt 1GB){ "{0:N2} MB" -f ($bytes/1MB) }
    else                   { "{0:N2} GB" -f ($bytes/1GB) }
}

function Build-KeepSet {
    $keep = @{}; function AddK($n){ $keep[$n.ToLower()] = $true }
    $wi   = New-Object -ComObject WindowsInstaller.Installer
    foreach ($prod in $wi.Products()) {
        if ($wi.ProductState($prod) -eq 5) {
            AddK (Split-Path ($wi.ProductInfo($prod,'LocalPackage')) -Leaf)
        }
    }
    $ud = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData'
    if (Test-Path $ud) {
        Get-ChildItem $ud | ForEach-Object {
            $patchKey = Join-Path $_.PsPath 'Patches'
            if (Test-Path $patchKey) {
                Get-ChildItem $patchKey | ForEach-Object {
                    $p = Get-ItemProperty $_.PsPath -EA SilentlyContinue
                    if ($p.LocalPackage) { AddK (Split-Path $p.LocalPackage -Leaf) }
                }
            }
            $prodKey = Join-Path $_.PsPath 'Products'
            if (Test-Path $prodKey) {
                Get-ChildItem $prodKey | ForEach-Object {
                    $ip = Join-Path $_.PsPath 'InstallProperties'
                    if (Test-Path $ip) {
                        $iprop = Get-ItemProperty $ip -EA SilentlyContinue
                        if ($iprop.LocalPackage) { AddK (Split-Path $iprop.LocalPackage -Leaf) }
                    }
                }
            }
        }
    }
    return $keep
}

function Is-Excluded($file) {
    if (-not $ExcludeVendors) { return $false }
    foreach ($v in $ExcludeVendors) {
        if ($file.Name -match $v) { return $true }
        try {
            $sig = Get-AuthenticodeSignature $file.FullName
            if ($sig.SignerCertificate -and $sig.SignerCertificate.Subject -match $v) {
                return $true
            }
        } catch {}
    }
    return $false
}

# ---------------------------------------------------------------------------
# Core scan + action logic
# ---------------------------------------------------------------------------
function Run-Cleanup {
    param([bool]$Interactive)

    # Determine run mode flags
    $autoFlags = @($Auto, $AutoAll, $AutoDry, $AutoDryAll) | Where-Object { $_ }
    if ($autoFlags.Count -gt 1) {
        throw 'Use only ONE of -Auto, -AutoAll, -AutoDry, -AutoDryAll.'
    }
    $MoveMode = $false
    $WhatIf   = $true
    switch ($true) {
        $Auto       { $WhatIf = $false;  if (-not $ExcludeVendors) { $ExcludeVendors = @('Acrobat') } }
        $AutoAll    { $WhatIf = $false }
        $AutoDry    { $WhatIf = $true;   if (-not $ExcludeVendors) { $ExcludeVendors = @('Acrobat') } }
        $AutoDryAll { $WhatIf = $true }
    }

    if ($Interactive) {
        Clear-Host
		# Warn about admin rights only if not elevated
        $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
        if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            Write-Host 'NOTE: Admin rights required for Move/Delete modes.' -ForegroundColor Yellow
        }

        # Banner & mode selection											  
        $banner = @'
 _____      _       _      _____ _                            _____   _____ 
|  __ \    | |     | |    / ____| |                          |  __ \ / ____|
| |__) |_ _| |_ ___| |__ | |    | | ___  __ _ _ __   ___ _ __| |__) | (___  
|  ___/ _` | __/ __| '_ \| |    | |/ _ \/ _` | '_ \ / _ \ '__|  ___/ \___ \ 
| |  | (_| | || (__| | | | |____| |  __/ (_| | | | |  __/ |  | |     ____) |
|_|   \__,_|\__\___|_| |_|\_____|_|\___|\__,_|_| |_|\___|_|  |_|    |_____/ 
'@
        Write-Host $banner -ForegroundColor DarkYellow
        Write-Host ' 1) Dry-Run (no changes)'
        Write-Host ' 2) Move-Run (quarantine)'
        Write-Host ' 3) Delete-Run'
        $sel = Read-Host 'Enter 1, 2, or 3 [default 1]'
        if (-not $sel) { $sel = '1' }
        switch ($sel) {
            '2' {
                $WhatIf = $false
                $MoveMode = $true
                $QuarDir = 'C:\InstallerQuarantine'
                if (-not (Test-Path $QuarDir)) {
                    New-Item $QuarDir -ItemType Directory -Force | Out-Null
                }
            }
            '3' { $WhatIf = $false }
            default { $WhatIf = $true }
        }

		# Elevation check for live runs							   
        if (-not $WhatIf) {
            $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
            if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
                 Write-Host 'ERROR: run PowerShell **as Administrator** for live actions.' -ForegroundColor Red
                exit 1
            }
        }

       # Vendor filter prompt
        if (-not $ExcludeVendors) { $ExcludeVendors = @('Acrobat') }
        Write-Host "Current vendor filter list: $($ExcludeVendors -join ', ')" -ForegroundColor Cyan
        $add = Read-Host 'Add vendors (comma-separated) or press Enter'
        if ($add) {
            $ExcludeVendors += ($add -split ',').Trim()
            $ExcludeVendors = $ExcludeVendors | Select-Object -Unique
        }
    }

    # Paths & logger
    $logDir = 'C:\Temp'
    if (-not (Test-Path $logDir)) {
        New-Item $logDir -ItemType Directory -Force | Out-Null
    }
    $logFile = Join-Path $logDir 'PatchCleanerPS.log'
    function Log($msg) {
      (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + "`t$msg" |
            Out-File -FilePath $logFile -Append -Encoding utf8
    }
    $insDir = 'C:\Windows\Installer'

   
    # >>> ADDED: tracking lists
    
    $orphanedFiles = @()
    $excludedFiles = @()

    # Create restore point for live runs
    if (-not $WhatIf) {
        try {
            Checkpoint-Computer -Description 'PatchCleanerPS' -RestorePointType MODIFY_SETTINGS -ErrorAction Stop | Out-Null
			Write-Host 'System restore point created.' -ForegroundColor Green
            Log 'Restore point created.'
        } catch {
            Write-Host 'ERROR: failed to create restore point, aborting.' -ForegroundColor Red
            exit 1
        }
    }

    # Build keep set & scan files
    $keepSet = Build-KeepSet
    $files   = Get-ChildItem $insDir -Recurse -File -Force | Where-Object { $_.Extension -match '\.(msi|msp)$' }
	$total   = $files.Count					   

    [long]$usedBytes = 0; $usedCnt = 0
    [long]$excBytes  = 0; $excCnt  = 0
    [long]$orpBytes  = 0; $orpCnt  = 0

	$i = 0	  
    foreach ($f in $files) {
		$i++
        if ($Interactive) {
            Write-Progress -Activity 'Scanning installer files' -Status "$i / $total" -PercentComplete ([math]::Floor($i/$total*100))
        }
        $size = $f.Length; $key = $f.Name.ToLower()
        if ($keepSet[$key]) {
            $usedCnt++; $usedBytes += $size
            continue
        }
        if (Is-Excluded $f) {
            $excCnt++; $excBytes += $size
            $excludedFiles += $f.FullName   # >>> ADDED
            continue
        }
        # orphaned
        $orpCnt++; $orpBytes += $size
        $orphanedFiles += $f.FullName      # >>> ADDED
        if (-not $WhatIf) {
            if ($MoveMode) {
                Move-Item $f.FullName -Destination $QuarDir -Force -Confirm:$false
            } else {
                Remove-Item $f.FullName -Force -Confirm:$false
            }
        }
    }
																				if ($Interactive) { Write-Progress -Activity 'Scanning installer files' -Completed }

    # Remove empty directories
    Get-ChildItem $insDir -Recurse -Directory -Force -ErrorAction SilentlyContinue |
        Sort-Object FullName -Descending |
        Where-Object { (Get-ChildItem $_ -Force -ErrorAction SilentlyContinue).Count -eq 0 } |
        ForEach-Object {
            if (-not $WhatIf) {
                try { Remove-Item $_.FullName -Recurse -Force -Confirm:$false } catch {}
            }
        }		

    # LOG OUTPUT (>>> ADDED)

    Log "===== PatchCleanerPS RUN ====="

    Log ""
    Log "---- EXCLUDED FILES ----"
    if ($excludedFiles.Count -eq 0) { Log "None" }
    else { $excludedFiles | ForEach-Object { Log $_ } }

    Log ""
    Log "---- ORPHANED FILES ----"
    if ($orphanedFiles.Count -eq 0) { Log "None" }
    else { $orphanedFiles | ForEach-Object { Log $_ } }

    Log ""
    Log "===== END RUN ====="

    # Summary banner
    $usedStr = Format-Size $usedBytes
    $excStr  = Format-Size $excBytes
    $orpStr  = Format-Size $orpBytes
	$mode    = if ($WhatIf) { 'Completed (WhatIf)' } else { 'Completed' }

    Write-Host '=======================' -ForegroundColor Cyan -BackgroundColor DarkBlue
    Write-Host "=== $mode ===" -ForegroundColor Cyan -BackgroundColor DarkBlue
    Write-Host '=======================' -ForegroundColor Cyan -BackgroundColor DarkBlue
    Write-Host "=== $usedCnt files still used, $usedStr ===" -ForegroundColor Red
    Write-Host "=== $excCnt files excluded, $excStr ===" -ForegroundColor Yellow
    if ($WhatIf) {
        Write-Host "=== $orpCnt files orphaned, $orpStr ===" -ForegroundColor Green
    } elseif ($MoveMode) {
        Write-Host "=== $orpCnt files moved, $orpStr ===" -ForegroundColor Green
    } else {
        Write-Host "=== $orpCnt files deleted, $orpStr ===" -ForegroundColor Green
    }
    Write-Host "Log: $logFile"
}

# ---------------------------------------------------------------------------
# Entry point: headless vs interactive
# ---------------------------------------------------------------------------
if (@($Auto, $AutoAll, $AutoDry, $AutoDryAll) -contains $true) {
    # headless: single run, no prompts
    Run-Cleanup -Interactive:$false
} else {
    # interactive: allow repeating
    do {
        Run-Cleanup -Interactive:$true
        $resp = Read-Host 'Start over? [y/N]'
    } while ($resp.Trim().ToLower() -eq 'y')
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions