Skip to content

Troubleshooting

Calvindd2f edited this page Jan 14, 2026 · 1 revision

Troubleshooting

Relevant source files

The following files were used as context for generating this wiki page:

This page provides technical troubleshooting guidance for common issues encountered when using OfficeScrubC2R. Each issue maps to specific code entities in the native C# layer (OfficeScrubNative.dll) and PowerShell wrapper layer, with actionable resolution steps and diagnostic procedures.

The troubleshooting workflow typically involves:

  1. Identifying the failure symptom (error message, incomplete operation, hung process)
  2. Locating the relevant code entity (helper class, method, P/Invoke call)
  3. Examining log output and error codes
  4. Applying resolution steps (privilege elevation, process termination, reboot scheduling)

For command-line parameters, see Command-Line Parameters. For error code reference, see Constants and Error Codes.

Troubleshooting Strategy

OfficeScrubC2R uses a layered architecture where PowerShell orchestrates operations through native C# helper classes. When issues occur, failures typically originate in one of these layers:

Layer Components Common Failure Points
PowerShell Layer OfficeScrubC2R.psm1 Module loading, DLL import, parameter validation
Native C# Layer OfficeScrubOrchestrator, Helper classes P/Invoke failures, access denied, locked resources
System Layer Windows Registry, File System, WMI Permission errors, locked files, missing privileges

Sources:
OfficeScrubC2R-Native.cs:1-2000, OfficeScrubC2R.psm1:1-100

Common Issues and Solutions

1. Administrator Privilege Failures

OfficeScrubC2R performs privileged operations through multiple native Windows APIs that require administrator rights. Operations that require elevation include registry modifications via advapi32.dll, file deletion via kernel32.dll, and service management via WMI.

Symptoms

  • PowerShell throws UnauthorizedAccessException during module import
  • Registry operations fail with "Access Denied" in RegistryHelper
  • File deletion fails in FileHelper.DeleteFile() or FileHelper.DeleteDirectory()
  • Service operations fail in ServiceHelper.DeleteService()
  • Log shows: "Elevation required" or "Not running as administrator"

Diagnostic Procedure

Check current privilege level:

# Windows PowerShell
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

Resolution Steps

  1. Launch elevated PowerShell session:

    Start-Process powershell -Verb RunAs
  2. Verify elevation before import:

    if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw "Administrator privileges required"
    }
    Import-Module OfficeScrubC2R
  3. Check UAC settings if elevation prompts fail:

    • Navigate to: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
    • Verify EnableLUA and ConsentPromptBehaviorAdmin values

Code Entity Mapping

graph TB
    PowerShell["PowerShell Session"]
    Import["Import-Module OfficeScrubC2R"]
    Orchestrator["OfficeScrubOrchestrator"]
    
    subgraph "Helper Classes Requiring Elevation"
        RegHelper["RegistryHelper<br/>RegOpenKeyEx, RegDeleteKeyEx"]
        FileHelper["FileHelper<br/>MoveFileEx, SetFileAttributes"]
        SvcHelper["ServiceHelper<br/>WMI Win32_Service"]
        ProcHelper["ProcessHelper<br/>Process.Kill()"]
    end
    
    subgraph "Windows APIs (advapi32.dll, kernel32.dll)"
        RegAPI["RegOpenKeyEx<br/>KEY_ALL_ACCESS"]
        FileAPI["MoveFileEx<br/>MOVEFILE_DELAY_UNTIL_REBOOT"]
        SvcAPI["Win32_Service.Delete()"]
    end
    
    PowerShell --> Import
    Import --> Orchestrator
    Orchestrator --> RegHelper
    Orchestrator --> FileHelper
    Orchestrator --> SvcHelper
    Orchestrator --> ProcHelper
    
    RegHelper --> RegAPI
    FileHelper --> FileAPI
    SvcHelper --> SvcAPI
    
    RegAPI -."|Access Denied".-> RegHelper
    FileAPI -."|Access Denied".-> FileHelper
    SvcAPI -."|Access Denied".-> SvcHelper
Loading

Sources:
OfficeScrubC2R-Native.cs:70-91 (RegDeleteKeyEx, RegOpenKeyEx P/Invoke), OfficeScrubC2R-Native.cs:334-616 (RegistryHelper), OfficeScrubC2R-Native.cs:622-768 (FileHelper), OfficeScrubC2R-Native.cs:1245-1309 (ServiceHelper)

2. Native DLL Loading and Compilation Failures

The PowerShell module loads OfficeScrubNative.dll to access high-performance C# implementations of registry, file, and process operations. If the DLL fails to load, the system attempts fallback compilation from OfficeScrubC2R-Native.cs. Both paths must succeed for the module to function.

Symptoms

  • PowerShell error: "Could not load file or assembly 'OfficeScrubNative'"
  • PowerShell error: "Add-Type : Cannot add type"
  • Module import fails with: "The type initializer for 'OfficeScrubNative.OfficeScrubOrchestrator' threw an exception"
  • Missing types: [OfficeScrubNative.OfficeScrubOrchestrator], [OfficeScrubNative.RegistryHelper]

Diagnostic Procedure

  1. Check if DLL exists and is unblocked:

    $dllPath = Join-Path $PSScriptRoot "OfficeScrubNative.dll"
    Test-Path $dllPath
    Get-Item $dllPath | Select-Object -ExpandProperty Zone.Identifier
  2. Verify .NET Framework version:

    # Requires .NET Framework 4.5+
    [Environment]::Version
    Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse | 
        Get-ItemProperty -Name Version -EA 0 | Where-Object { $_.Version -like "4.*" }
  3. Test manual load:

    Add-Type -Path ".\OfficeScrubNative.dll"
    [OfficeScrubNative.OfficeScrubOrchestrator]::new($true)

Resolution Steps

Issue: Zone.Identifier blocking downloaded DLL

Get-ChildItem -Path $PSScriptRoot -Filter *.dll -Recurse | Unblock-File
Get-ChildItem -Path $PSScriptRoot -Filter *.cs -Recurse | Unblock-File

Issue: Missing .NET Framework

  • Install .NET Framework 4.5 or later from Microsoft
  • Or use PowerShell Core 7+ with .NET Core compilation fallback

Issue: Corrupted or incompatible DLL

  1. Delete OfficeScrubNative.dll
  2. Ensure OfficeScrubC2R-Native.cs is present
  3. Re-import module to trigger fallback compilation:
    Remove-Module OfficeScrubC2R -ErrorAction SilentlyContinue
    Import-Module .\OfficeScrubC2R.psd1 -Force

Issue: Compilation failure on source fallback

  • Verify csc.exe is available in PATH (Windows PowerShell)
  • Or verify dotnet.exe is available (PowerShell Core)
  • Check for syntax errors in OfficeScrubC2R-Native.cs

Native DLL Load and Fallback Workflow

flowchart TD
    Import["Import-Module OfficeScrubC2R.psd1"]
    CheckDLL{"OfficeScrubNative.dll<br/>exists?"}
    LoadDLL["Add-Type -Path<br/>OfficeScrubNative.dll"]
    DLLSuccess{"Load<br/>successful?"}
    
    CheckCS{"OfficeScrubC2R-Native.cs<br/>exists?"}
    CompileCS["Add-Type -TypeDefinition<br/>Compile from source"]
    CompileSuccess{"Compile<br/>successful?"}
    
    CreateOrch["New-OfficeScrubOrchestrator<br/>Instantiate"]
    Helpers["Access Helper Classes:<br/>RegistryHelper<br/>FileHelper<br/>ProcessHelper<br/>etc."]
    
    Error["Module Load Failure<br/>Neither DLL nor source available"]
    
    Import --> CheckDLL
    CheckDLL -->|Yes| LoadDLL
    CheckDLL -->|No| CheckCS
    
    LoadDLL --> DLLSuccess
    DLLSuccess -->|Yes| CreateOrch
    DLLSuccess -->|No| CheckCS
    
    CheckCS -->|Yes| CompileCS
    CheckCS -->|No| Error
    
    CompileCS --> CompileSuccess
    CompileSuccess -->|Yes| CreateOrch
    CompileSuccess -->|No| Error
    
    CreateOrch --> Helpers
Loading

Code Entity Mapping: Module Loading Mechanism

Component File Path Purpose
Import-OfficeScrubNative OfficeScrubC2R.psm1:1-50 Loads pre-compiled DLL or falls back to source compilation
New-OfficeScrubOrchestrator OfficeScrubC2R.psm1:50-80 Instantiates OfficeScrubOrchestrator with system bitness
OfficeScrubOrchestrator constructor OfficeScrubC2R-Native.cs:1315-1349 Initializes all helper class instances
RegistryHelper constructor OfficeScrubC2R-Native.cs:338-341 Accepts is64Bit parameter for WOW64 handling
Helper class properties OfficeScrubC2R-Native.cs:1330-1337 Exposes public instances of all helpers

Sources:
OfficeScrubC2R.psm1:1-100, OfficeScrubC2R-Native.cs:1315-1349, OfficeScrubC2R-Native.cs:338-341, README.md:48-66

3. Locked Files and Reboot Scheduling

Office files may be locked by running processes (e.g., WINWORD.EXE, EXCEL.EXE, OfficeClickToRun.exe) or loaded DLLs. The FileHelper class uses kernel32.dll!MoveFileEx with the MOVEFILE_DELAY_UNTIL_REBOOT flag to schedule deletion on the next boot.

Symptoms

  • File deletion fails with IOException: "The process cannot access the file because it is being used by another process"
  • Directory deletion returns false from FileHelper.DeleteDirectory()
  • Log shows: "Scheduled for deletion on reboot"
  • Files tracked in FileHelper._pendingDeletes list
  • Registry key: HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations

Diagnostic Procedure

  1. Check for file locks:

    $orchestrator = New-OfficeScrubOrchestrator
    $helpers = Get-OfficeScrubHelpers -Orchestrator $orchestrator
    $helpers.Files.IsFileLocked("C:\Path\To\File.dll")
  2. Check for processes holding locks:

    $helpers.Processes.GetProcessesUsingPath("C:\Program Files\Microsoft Office")
  3. View pending deletes:

    $helpers.Files.GetPendingDeletes()
  4. Check registry for scheduled operations:

    Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations

Resolution Steps

Option 1: Terminate locking processes

$orchestrator = New-OfficeScrubOrchestrator
$helpers = Get-OfficeScrubHelpers -Orchestrator $orchestrator

# Terminate all Office processes
$terminated = $helpers.Processes.TerminateProcesses([OfficeScrubNative.OfficeConstants]::OFFICE_PROCESSES, 10000)
Write-Host "Terminated PIDs: $($terminated -join ', ')"

# Retry deletion
$success = $helpers.Files.DeleteDirectory("C:\Program Files\Microsoft Office", $true, $false)

Option 2: Schedule deletion and reboot

# Force reboot scheduling
$helpers.Files.ScheduleDeleteOnReboot("C:\Path\To\LockedFile.dll")
$helpers.Files.ScheduleDirectoryDeleteOnReboot("C:\Program Files\Microsoft Office")

# Check pending deletes
$pending = $helpers.Files.GetPendingDeletes()
Write-Host "Scheduled $($pending.Count) items for deletion on reboot"

# Reboot
Restart-Computer -Force

Option 3: Use cmd.exe fallback for directories

The FileHelper.DeleteDirectory() method first attempts cmd.exe /c rd /s /q for performance (15x faster), then falls back to Directory.Delete():

# This happens automatically in DeleteDirectory()
# First attempt: cmd.exe rd /s /q (fast)
# Second attempt: Directory.Delete() (.NET method)
# Third attempt: ScheduleDirectoryDeleteOnReboot() (reboot scheduling)

File Deletion Strategy with Reboot Fallback

flowchart TD
    DeleteReq["FileHelper.DeleteFile(path, scheduleOnFail)"]
    Exists{"File<br/>exists?"}
    RemoveRO["Remove ReadOnly attribute<br/>File.SetAttributes()"]
    Delete["File.Delete()"]
    Success{"Delete<br/>successful?"}
    
    CheckSched{"scheduleOnFail<br/>== true?"}
    Schedule["MoveFileEx(path, null,<br/>MOVEFILE_DELAY_UNTIL_REBOOT)"]
    AddToList["Add to _pendingDeletes"]
    Registry["Update Registry:<br/>PendingFileRenameOperations"]
    
    ReturnTrue["Return true"]
    ReturnFalse["Return false"]
    
    DeleteReq --> Exists
    Exists -->|No| ReturnTrue
    Exists -->|Yes| RemoveRO
    RemoveRO --> Delete
    Delete --> Success
    
    Success -->|Yes| ReturnTrue
    Success -->|No| CheckSched
    
    CheckSched -->|Yes| Schedule
    CheckSched -->|No| ReturnFalse
    
    Schedule --> AddToList
    AddToList --> Registry
    Registry --> ReturnFalse
Loading

Directory Deletion with Multi-Stage Fallback

sequenceDiagram
    participant Caller
    participant FileHelper
    participant CmdExe["cmd.exe /c rd /s /q"]
    participant DotNet["Directory.Delete()"]
    participant MoveFileEx["kernel32!MoveFileEx"]
    participant Registry["HKLM\\...\\Session Manager"]
    
    Caller->>FileHelper: DeleteDirectory(path, recursive, scheduleOnFail)
    FileHelper->>CmdExe: Launch with 30s timeout
    
    alt cmd.exe succeeds (15x faster)
        CmdExe-->>FileHelper: Exit 0
        FileHelper-->>Caller: Return true
    else cmd.exe fails or timeout
        CmdExe-->>FileHelper: Failure
        FileHelper->>DotNet: Directory.Delete(path, recursive)
        
        alt .NET succeeds
            DotNet-->>FileHelper: Success
            FileHelper-->>Caller: Return true
        else .NET fails (locked files)
            DotNet-->>FileHelper: IOException
            
            alt scheduleOnFail == true
                FileHelper->>FileHelper: ScheduleDirectoryDeleteOnReboot(path)
                FileHelper->>FileHelper: Enumerate all files
                
                loop For each file
                    FileHelper->>MoveFileEx: Schedule file deletion
                    MoveFileEx->>Registry: Add to PendingFileRenameOperations
                end
                
                FileHelper->>FileHelper: Enumerate directories (reverse depth)
                
                loop For each directory (deepest first)
                    FileHelper->>MoveFileEx: Schedule directory deletion
                    MoveFileEx->>Registry: Add to PendingFileRenameOperations
                end
                
                FileHelper-->>Caller: Return false (scheduled for reboot)
            else scheduleOnFail == false
                FileHelper-->>Caller: Return false
            end
        end
    end
Loading

Code Entity Mapping: File Operations

Method Location Purpose P/Invoke
FileHelper.DeleteFile() OfficeScrubC2R-Native.cs:626-651 Delete single file with reboot fallback MoveFileEx
FileHelper.DeleteDirectory() OfficeScrubC2R-Native.cs:653-687 Delete directory with cmd.exe optimization MoveFileEx
FileHelper.ScheduleDeleteOnReboot() OfficeScrubC2R-Native.cs:689-697 Schedule file for deletion on next boot kernel32!MoveFileEx
FileHelper.ScheduleDirectoryDeleteOnReboot() OfficeScrubC2R-Native.cs:699-727 Schedule directory tree for deletion kernel32!MoveFileEx
FileHelper.IsFileLocked() OfficeScrubC2R-Native.cs:750-767 Test if file is in use File.Open with FileShare.None
NativeMethods.MoveFileEx OfficeScrubC2R-Native.cs:64-68 P/Invoke declaration Win32 API
MOVEFILE_DELAY_UNTIL_REBOOT OfficeScrubC2R-Native.cs:150 Flag constant: 0x4 Win32 constant

Sources:
OfficeScrubC2R-Native.cs:620-768 (FileHelper class), OfficeScrubC2R-Native.cs:64-68 (MoveFileEx P/Invoke), OfficeScrubC2R-Native.cs:150 (MOVEFILE_DELAY_UNTIL_REBOOT constant)

4. Process Termination Failures

The ProcessHelper class terminates Office processes using parallel task execution. Each process receives a Kill() signal with a configurable timeout. Failures can occur due to insufficient privileges, protected processes, or system process dependencies.

Symptoms

  • Office applications remain running after cleanup
  • TerminateProcesses() returns empty list
  • Process.Kill() throws Win32Exception with "Access is denied"
  • Processes restart immediately after termination (service-based processes)

Diagnostic Procedure

  1. Check for running Office processes:

    $orchestrator = New-OfficeScrubOrchestrator
    $helpers = Get-OfficeScrubHelpers -Orchestrator $orchestrator
    
    # Check specific process
    $helpers.Processes.IsProcessRunning("WINWORD.EXE")
    
    # List all Office processes
    [OfficeScrubNative.OfficeConstants]::OFFICE_PROCESSES | ForEach-Object {
        if ($helpers.Processes.IsProcessRunning($_)) {
            Write-Host "Running: $_"
        }
    }
  2. Identify processes using Office directories:

    $processNames = $helpers.Processes.GetProcessesUsingPath("C:\Program Files\Microsoft Office")
    $processNames | Format-Table

Resolution Steps

Option 1: Terminate with extended timeout

$orchestrator = New-OfficeScrubOrchestrator
$helpers = Get-OfficeScrubHelpers -Orchestrator $orchestrator

# Terminate with 30-second timeout per process
$terminatedPids = $helpers.Processes.TerminateProcesses(
    [OfficeScrubNative.OfficeConstants]::OFFICE_PROCESSES,
    30000  # 30 seconds
)

Write-Host "Terminated $($terminatedPids.Count) processes: $($terminatedPids -join ', ')"

Option 2: Stop Office services before terminating processes

# Stop ClickToRun service first
$helpers.Services.DeleteService("ClickToRunSvc")

# Then terminate processes
$terminatedPids = $helpers.Processes.TerminateProcesses(
    [OfficeScrubNative.OfficeConstants]::OFFICE_PROCESSES,
    10000
)

Option 3: Force termination via Task Manager or taskkill

# Fallback for protected processes
Get-Process | Where-Object { 
    [OfficeScrubNative.OfficeConstants]::OFFICE_PROCESSES -contains "$($_.Name).exe" 
} | ForEach-Object {
    Stop-Process -Id $_.Id -Force
}

Parallel Process Termination Workflow

flowchart TD
    Start["TerminateProcesses(processNames[], timeoutMs)"]
    Enumerate["Enumerate matching processes<br/>Process.GetProcessesByName()"]
    
    subgraph "Parallel Task Execution"
        Task1["Task.Run()<br/>Process 1"]
        Task2["Task.Run()<br/>Process 2"]
        TaskN["Task.Run()<br/>Process N"]
    end
    
    subgraph "Per-Process Logic"
        CheckExit{"HasExited?"}
        Kill["Process.Kill()"]
        WaitExit["WaitForExit(timeoutMs)"]
        AddPID["lock(terminatedPids)<br/>Add PID to list"]
        Dispose["Process.Dispose()"]
    end
    
    WaitAll["Task.WaitAll(tasks[], timeoutMs * 2)"]
    Return["Return terminatedPids"]
    
    Start --> Enumerate
    Enumerate --> Task1
    Enumerate --> Task2
    Enumerate --> TaskN
    
    Task1 --> CheckExit
    Task2 --> CheckExit
    TaskN --> CheckExit
    
    CheckExit -->|No| Kill
    CheckExit -->|Yes| Dispose
    Kill --> WaitExit
    WaitExit -->|Success| AddPID
    WaitExit -->|Timeout| Dispose
    AddPID --> Dispose
    
    Task1 --> WaitAll
    Task2 --> WaitAll
    TaskN --> WaitAll
    
    WaitAll --> Return
Loading

Code Entity Mapping: Process Management

Method Location Purpose Thread Safety
ProcessHelper.TerminateProcesses() OfficeScrubC2R-Native.cs:775-814 Parallel process termination lock(terminatedPids)
ProcessHelper.GetProcessesUsingPath() OfficeScrubC2R-Native.cs:816-842 Find processes by executable path Thread-safe
ProcessHelper.IsProcessRunning() OfficeScrubC2R-Native.cs:844-857 Check if process name is running Thread-safe
OfficeConstants.OFFICE_PROCESSES OfficeScrubC2R-Native.cs:47-55 Array of Office process names Static readonly

Sources:
OfficeScrubC2R-Native.cs:771-858 (ProcessHelper class), OfficeScrubC2R-Native.cs:47-55 (OFFICE_PROCESSES constant)

5. Registry Access and WOW64 Issues

The RegistryHelper class handles both native 64-bit and WOW64 32-bit registry views on 64-bit systems. Issues arise when registry keys exist in only one view, when permissions are insufficient, or when keys are locked by other processes.

Symptoms

  • RegistryHelper.KeyExists() returns false when key exists in alternate view
  • DeleteKey() returns false with "Access Denied"
  • Log shows: "Failed to delete registry key"
  • Windows Installer metadata cleanup incomplete
  • COM TypeLib registration remains after cleanup

Diagnostic Procedure

  1. Check key existence in both views:

    $orchestrator = New-OfficeScrubOrchestrator
    $helpers = Get-OfficeScrubHelpers -Orchestrator $orchestrator
    
    # Checks both native and WOW64 views automatically
    $exists = $helpers.Registry.KeyExists(
        [OfficeScrubNative.RegistryHiveType]::LocalMachine,
        "SOFTWARE\Microsoft\Office\ClickToRun"
    )
  2. Manually check WOW64 view:

    # Native 64-bit view
    Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Office" -ErrorAction SilentlyContinue
    
    # WOW64 32-bit view
    Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Office" -ErrorAction SilentlyContinue
  3. Test key deletion with verbose logging:

    $success = $helpers.Registry.DeleteKey(
        [OfficeScrubNative.RegistryHiveType]::LocalMachine,
        "SOFTWARE\Microsoft\Office\16.0",
        $true  # recursive
    )
    Write-Host "Deletion success: $success"

Resolution Steps

Option 1: Ensure both registry views are targeted

The RegistryHelper automatically handles WOW64 on 64-bit systems:

# This internally checks and deletes from both:
# - HKLM\SOFTWARE\Microsoft\Office\16.0 (native 64-bit)
# - HKLM\SOFTWARE\Wow6432Node\Microsoft\Office\16.0 (WOW64 32-bit)
$helpers.Registry.DeleteKey(
    [OfficeScrubNative.RegistryHiveType]::LocalMachine,
    "SOFTWARE\Microsoft\Office\16.0",
    $true
)

Option 2: Use P/Invoke for low-level access

The RegistryHelper uses P/Invoke to advapi32.dll!RegDeleteKeyEx with appropriate flags:

// Internal implementation (automatic)
// KEY_WOW64_64KEY = 0x0100 (native 64-bit view)
// KEY_WOW64_32KEY = 0x0200 (WOW64 32-bit view)
RegDeleteKeyEx(hKey, subKey, KEY_WOW64_64KEY, 0);
RegDeleteKeyEx(hKey, subKey, KEY_WOW64_32KEY, 0);

Option 3: Manual cleanup of stubborn keys

# Use reg.exe for protected keys
Start-Process reg.exe -ArgumentList "delete", "HKLM\SOFTWARE\Microsoft\Office\16.0", "/f" -Wait -NoNewWindow

# Or use .NET registry APIs with specific views
$key = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
    [Microsoft.Win32.RegistryHive]::LocalMachine,
    [Microsoft.Win32.RegistryView]::Registry64
)
$key.DeleteSubKeyTree("SOFTWARE\Microsoft\Office\16.0", $false)

WOW64 Registry Handling Logic

flowchart TD
    DeleteKey["RegistryHelper.DeleteKey(hive, subKey, recursive)"]
    Is64Bit{"_is64Bit<br/>flag?"}
    
    NativePath["Native path:<br/>SOFTWARE\\X"]
    WOW64Path["WOW64 path:<br/>SOFTWARE\\Wow6432Node\\X"]
    
    GetWow64Key["GetWow64Key(subKey)<br/>Transform path"]
    
    NativeExists{"KeyExists<br/>native path?"}
    WOW64Exists{"KeyExists<br/>WOW64 path?"}
    
    DeleteNative["DeleteKeyInternal()<br/>RegDeleteKeyEx with KEY_WOW64_64KEY"]
    DeleteWOW64["DeleteKeyInternal()<br/>RegDeleteKeyEx with KEY_WOW64_32KEY"]
    
    ReturnResult["Return success if either deletion succeeded"]
    
    DeleteKey --> NativePath
    DeleteKey --> Is64Bit
    
    NativePath --> NativeExists
    NativeExists -->|Yes| DeleteNative
    
    Is64Bit -->|Yes| GetWow64Key
    Is64Bit -->|No| ReturnResult
    
    GetWow64Key --> WOW64Path
    WOW64Path --> WOW64Exists
    WOW64Exists -->|Yes| DeleteWOW64
    
    DeleteNative --> ReturnResult
    DeleteWOW64 --> ReturnResult
Loading

Code Entity Mapping: Registry Operations

Method Location Purpose P/Invoke
RegistryHelper.DeleteKey() OfficeScrubC2R-Native.cs:349-364 Delete registry key in both views RegDeleteKeyEx
RegistryHelper.KeyExists() OfficeScrubC2R-Native.cs:343-347 Check key existence in both views RegOpenKeyEx
RegistryHelper.GetWow64Key() OfficeScrubC2R-Native.cs:583-593 Transform path for WOW64 view N/A
RegistryHelper.EnumerateKeys() OfficeScrubC2R-Native.cs:402-436 List subkeys from both views RegEnumKeyEx
NativeMethods.RegDeleteKeyEx OfficeScrubC2R-Native.cs:82-86 P/Invoke declaration Win32 API
NativeMethods.KEY_WOW64_64KEY OfficeScrubC2R-Native.cs:145 Flag constant: 0x0100 Win32 constant
NativeMethods.KEY_WOW64_32KEY OfficeScrubC2R-Native.cs:146 Flag constant: 0x0200 Win32 constant

Sources:
OfficeScrubC2R-Native.cs:332-616 (RegistryHelper class), OfficeScrubC2R-Native.cs:70-153 (P/Invoke declarations), OfficeScrubC2R-Native.cs:583-593 (GetWow64Key method)

6. Windows Installer Metadata Cleanup Failures

The WindowsInstallerHelper class removes Office entries from Windows Installer metadata using GUID transformations. Issues occur when GUIDs are malformed, when the IsInScope() callback fails, or when registry keys are protected by system ACLs.

Symptoms

  • Office still appears in "Programs and Features" after cleanup
  • Windows Installer cached MSI files remain in C:\Windows\Installer
  • Registry keys remain under:
    • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes
    • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products
    • HKCR\Installer\Products
    • HKCR\Installer\Components

Diagnostic Procedure

  1. Check for remaining Windows Installer metadata:

    # Check UpgradeCodes
    Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes" | 
        Where-Object { $_.PSChildName.Length -eq 32 }
    
    # Check Products
    Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products" |
        Where-Object { $_.PSChildName.Length -eq 32 }
  2. Test GUID transformation:

    # Compressed to Expanded
    $compressed = "00004159430090400100000000F01FEC"
    $expanded = [OfficeScrubNative.GuidHelper]::GetExpandedGuid($compressed)
    Write-Host "Expanded: $expanded"
    
    # Expanded to Compressed
    $expanded = "{9AC08E99-230B-47E8-9721-4577B7F124EA}"
    $compressed = [OfficeScrubNative.GuidHelper]::GetCompressedGuid($expanded)
    Write-Host "Compressed: $compressed"
  3. Test product scope filtering:

    $orchestrator = New-OfficeScrubOrchestrator
    $inScope = $orchestrator.IsInScope("{9AC08E99-230B-47E8-9721-4577B7F124EA}")
    Write-Host "Is in scope: $inScope"

Resolution Steps

Option 1: Run full Windows Installer cleanup

$orchestrator = New-OfficeScrubOrchestrator
$helpers = Get-OfficeScrubHelpers -Orchestrator $orchestrator

# Cleanup UpgradeCodes
$helpers.WindowsInstaller.CleanupUpgradeCodes([Func[string, bool]]{
    param($guid)
    $orchestrator.IsInScope($guid)
})

# Cleanup Products
$helpers.WindowsInstaller.CleanupProducts([Func[string, bool]]{
    param($guid)
    $orchestrator.IsInScope($guid)
})

# Cleanup Components
$helpers.WindowsInstaller.CleanupComponents([Func[string, bool]]{
    param($guid)
    $orchestrator.IsInScope($guid)
})

# Cleanup Published Components
$helpers.WindowsInstaller.CleanupPublishedComponents([Func[string, bool]]{
    param($guid)
    $orchestrator.IsInScope($guid)
})

Option 2: Manual GUID-based cleanup

# Remove specific product by GUID
$productGuid = "{9AC08E99-230B-47E8-9721-4577B7F124EA}"
$compressed = [OfficeScrubNative.GuidHelper]::GetCompressedGuid($productGuid)

# Delete from all Windows Installer locations
$helpers.Registry.DeleteKey(
    [OfficeScrubNative.RegistryHiveType]::LocalMachine,
    "SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\$compressed",
    $true
)
$helpers.Registry.DeleteKey(
    [OfficeScrubNative.RegistryHiveType]::ClassesRoot,
    "Installer\Products\$compressed",
    $true
)

Option 3: Clear entire Windows Installer cache (nuclear option)

# WARNING: This removes ALL Windows Installer metadata, not just Office
# Only use as last resort

# Stop Windows Installer service
Stop-Service msiserver -Force

# Delete cached MSI files
Remove-Item "C:\Windows\Installer\*.msi" -Force -ErrorAction SilentlyContinue

# Restart service
Start-Service msiserver

GUID Transformation for Windows Installer Metadata

flowchart LR
    subgraph "GUID Formats"
        Expanded["Expanded GUID<br/>{9AC08E99-230B-47E8-9721-4577B7F124EA}<br/>38 characters"]
        Compressed["Compressed GUID<br/>99E08CA9B03228E417925774B7F124AE<br/>32 characters"]
        Encoded["Encoded GUID<br/>6ZDbbD_B;F@J$$nH[$V%<br/>20 characters (Base85)"]
    end
    
    subgraph "Registry Storage"
        UpgradeCodes["UpgradeCodes\\<br/>{compressed}"]
        Products["Products\\<br/>{compressed}"]
        Components["Components\\<br/>{compressed}\\{value}"]
        Published["PublishedComponents\\<br/>{compressed}\\{encoded}"]
    end
    
    subgraph "Helper Methods"
        GetExpanded["GuidHelper.GetExpandedGuid()"]
        GetCompressed["GuidHelper.GetCompressedGuid()"]
        GetDecoded["GuidHelper.GetDecodedGuid()"]
    end
    
    Compressed -->|"Transform"| GetExpanded
    GetExpanded --> Expanded
    
    Expanded -->|"Reverse"| GetCompressed
    GetCompressed --> Compressed
    
    Encoded -->|"Decode"| GetDecoded
    GetDecoded --> Expanded
    
    Compressed --> UpgradeCodes
    Compressed --> Products
    Compressed --> Components
    Encoded --> Published
Loading

Code Entity Mapping: Windows Installer Cleanup

Method Location Purpose GUID Format
WindowsInstallerHelper.CleanupUpgradeCodes() OfficeScrubC2R-Native.cs:989-1011 Remove UpgradeCode entries Compressed (32 chars)
WindowsInstallerHelper.CleanupProducts() OfficeScrubC2R-Native.cs:1013-1039 Remove Product entries Compressed (32 chars)
WindowsInstallerHelper.CleanupComponents() OfficeScrubC2R-Native.cs:1041-1067 Remove Component entries Compressed (32 chars)
WindowsInstallerHelper.CleanupPublishedComponents() OfficeScrubC2R-Native.cs:1069-1116 Remove PublishedComponent entries Encoded (20 chars)
GuidHelper.GetExpandedGuid() OfficeScrubC2R-Native.cs:173-214 Convert compressed to standard GUID 32 → 38 chars
GuidHelper.GetCompressedGuid() OfficeScrubC2R-Native.cs:216-253 Convert standard to compressed GUID 38 → 32 chars
GuidHelper.GetDecodedGuid() OfficeScrubC2R-Native.cs:255-320 Decode Base85 to standard GUID 20 → 38 chars

Sources:
OfficeScrubC2R-Native.cs:978-1116 (WindowsInstallerHelper class), OfficeScrubC2R-Native.cs:169-328 (GuidHelper class)


Additional Resources


Summary Table: Troubleshooting Topics and Code Entities

Issue Area Code Entity / Symbol Log Location / Output
Elevation errors Test-IsElevated, ERROR_ELEVATION Log file, PowerShell error
DLL load/compilation Initialize-NativeTypes Log file, module import error
Locked files/reboot Remove-FolderRecursive, Add-PendingFileDelete, ERROR_REBOOT_REQUIRED Log file, scheduled for reboot
Log file location Initialize-Log, $script:LogDir $env:TEMP\OfficeScrubC2R\...
Error codes Set-ErrorCode, Set-ReturnValue, ERROR_* Log file, ScrubRetValFile.txt
Permission issues Remove-FileForced, ERROR_FAIL Log file, PowerShell warning

Sources:
OfficeScrubC2R-Utilities.psm1, README.md, docs/CHANGELOG.md


Disclaimer

This tool performs deep system changes. Always:

  • Backup important data before use
  • Close all Office applications
  • Run in a test environment first
  • Review logs if issues occur

Sources:
README.md:265-269

Clone this wiki locally