-
Notifications
You must be signed in to change notification settings - Fork 0
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:
- Identifying the failure symptom (error message, incomplete operation, hung process)
- Locating the relevant code entity (helper class, method, P/Invoke call)
- Examining log output and error codes
- 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.
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
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.
- PowerShell throws
UnauthorizedAccessExceptionduring module import - Registry operations fail with "Access Denied" in
RegistryHelper - File deletion fails in
FileHelper.DeleteFile()orFileHelper.DeleteDirectory() - Service operations fail in
ServiceHelper.DeleteService() - Log shows: "Elevation required" or "Not running as administrator"
Check current privilege level:
# Windows PowerShell
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)-
Launch elevated PowerShell session:
Start-Process powershell -Verb RunAs
-
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
-
Check UAC settings if elevation prompts fail:
- Navigate to:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System - Verify
EnableLUAandConsentPromptBehaviorAdminvalues
- Navigate to:
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
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)
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.
- 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]
-
Check if DLL exists and is unblocked:
$dllPath = Join-Path $PSScriptRoot "OfficeScrubNative.dll" Test-Path $dllPath Get-Item $dllPath | Select-Object -ExpandProperty Zone.Identifier
-
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.*" }
-
Test manual load:
Add-Type -Path ".\OfficeScrubNative.dll" [OfficeScrubNative.OfficeScrubOrchestrator]::new($true)
Issue: Zone.Identifier blocking downloaded DLL
Get-ChildItem -Path $PSScriptRoot -Filter *.dll -Recurse | Unblock-File
Get-ChildItem -Path $PSScriptRoot -Filter *.cs -Recurse | Unblock-FileIssue: 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
- Delete
OfficeScrubNative.dll - Ensure
OfficeScrubC2R-Native.csis present - 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.exeis available in PATH (Windows PowerShell) - Or verify
dotnet.exeis available (PowerShell Core) - Check for syntax errors in
OfficeScrubC2R-Native.cs
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
| 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
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.
- File deletion fails with
IOException: "The process cannot access the file because it is being used by another process" - Directory deletion returns
falsefromFileHelper.DeleteDirectory() - Log shows: "Scheduled for deletion on reboot"
- Files tracked in
FileHelper._pendingDeleteslist - Registry key:
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations
-
Check for file locks:
$orchestrator = New-OfficeScrubOrchestrator $helpers = Get-OfficeScrubHelpers -Orchestrator $orchestrator $helpers.Files.IsFileLocked("C:\Path\To\File.dll")
-
Check for processes holding locks:
$helpers.Processes.GetProcessesUsingPath("C:\Program Files\Microsoft Office")
-
View pending deletes:
$helpers.Files.GetPendingDeletes() -
Check registry for scheduled operations:
Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations
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 -ForceOption 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)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
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
| 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)
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.
- Office applications remain running after cleanup
-
TerminateProcesses()returns empty list -
Process.Kill()throwsWin32Exceptionwith "Access is denied" - Processes restart immediately after termination (service-based processes)
-
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: $_" } }
-
Identify processes using Office directories:
$processNames = $helpers.Processes.GetProcessesUsingPath("C:\Program Files\Microsoft Office") $processNames | Format-Table
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
}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
| 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)
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.
-
RegistryHelper.KeyExists()returnsfalsewhen key exists in alternate view -
DeleteKey()returnsfalsewith "Access Denied" - Log shows: "Failed to delete registry key"
- Windows Installer metadata cleanup incomplete
- COM TypeLib registration remains after cleanup
-
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" )
-
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
-
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"
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)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
| 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)
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.
- 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\UpgradeCodesHKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\ProductsHKCR\Installer\ProductsHKCR\Installer\Components
-
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 }
-
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"
-
Test product scope filtering:
$orchestrator = New-OfficeScrubOrchestrator $inScope = $orchestrator.IsInScope("{9AC08E99-230B-47E8-9721-4577B7F124EA}") Write-Host "Is in scope: $inScope"
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 msiserverflowchart 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
| 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)
| 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
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