Repository navigation
FileHelper
Relevant source files
The following files were used as context for generating this wiki page:
The FileHelper class provides high-performance file system operations for Office Click-to-Run cleanup, including file and directory deletion with automatic fallback to reboot-based deletion when files are locked by running processes. This class handles read-only attributes, implements performance optimizations using native Windows commands, and manages the PendingFileRenameOperations registry mechanism for locked file cleanup.
For registry operations used alongside file deletion, see RegistryHelper. For process termination that enables file deletion, see ProcessHelper. For the orchestration of cleanup stages, see OfficeScrubOrchestrator.
Sources: OfficeScrubC2R-Native.cs:620-767
The FileHelper class maintains internal state to track pending deletions and provides both synchronous deletion methods and reboot-scheduling fallback mechanisms.
| Member | Type | Purpose |
|---|---|---|
_pendingDeletes |
List<string> |
Tracks files and directories scheduled for deletion on next reboot |
The class is instantiated by OfficeScrubOrchestrator and accessed through the Files property.
Sources: OfficeScrubC2R-Native.cs:622-624
| Method | Return Type | Parameters | Description |
|---|---|---|---|
DeleteFile |
bool |
string filePath, bool scheduleOnFail = true |
Deletes a file with optional reboot scheduling on failure |
DeleteDirectory |
bool |
string directoryPath, bool recursive = true, bool scheduleOnFail = true |
Deletes a directory with performance optimization and fallback |
ScheduleDeleteOnReboot |
void |
string filePath |
Schedules a file for deletion on next system reboot |
ScheduleDirectoryDeleteOnReboot |
void |
string directoryPath |
Recursively schedules directory contents for reboot deletion |
GetPendingDeletes |
List<string> |
None | Returns list of files/directories scheduled for reboot deletion |
GetDirectorySize |
long |
string directoryPath |
Calculates total size of directory in bytes |
IsFileLocked |
bool |
string filePath |
Checks if a file is currently locked by another process |
Sources: OfficeScrubC2R-Native.cs:626-766
The DeleteFile method implements a multi-stage approach to handle various file system scenarios, including read-only attributes and file locks.
flowchart TD
Start["DeleteFile(filePath, scheduleOnFail)"] --> Exists{File.Exists?}
Exists -->|No| ReturnTrue["return true<br/>(Already deleted)"]
Exists -->|Yes| GetAttrs["File.GetAttributes()"]
GetAttrs --> CheckRO{ReadOnly<br/>attribute set?}
CheckRO -->|Yes| RemoveRO["File.SetAttributes()<br/>Clear ReadOnly"]
CheckRO -->|No| TryDelete
RemoveRO --> TryDelete["File.Delete(filePath)"]
TryDelete --> Success{Success?}
Success -->|Yes| ReturnTrue
Success -->|No| CheckSchedule{scheduleOnFail<br/>parameter?}
CheckSchedule -->|true| Schedule["ScheduleDeleteOnReboot(filePath)"]
CheckSchedule -->|false| ReturnFalse["return false"]
Schedule --> ReturnFalse
style Start fill:#f9f9f9
style ReturnTrue fill:#d4edda
style ReturnFalse fill:#f8d7da
style Schedule fill:#fff3cd
Analysis: The method first checks file existence to avoid errors. If the file exists, it retrieves attributes and clears the ReadOnly flag if present, as this is a common obstacle to deletion. After attempting File.Delete(), failures are caught and handled based on the scheduleOnFail parameter. When enabled, the system schedules the file for deletion on the next reboot using the Win32 MoveFileEx API.
Sources: OfficeScrubC2R-Native.cs:626-651
The method explicitly handles the ReadOnly attribute before attempting deletion:
var attrs = File.GetAttributes(filePath);
if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
File.SetAttributes(filePath, attrs & ~FileAttributes.ReadOnly);
}
This bitwise operation clears only the ReadOnly flag while preserving other file attributes like Hidden, System, or Archive.
Sources: OfficeScrubC2R-Native.cs:634-638
The DeleteDirectory method implements a performance-optimized approach using native Windows commands with .NET fallback.
flowchart TD
Start["DeleteDirectory(directoryPath, recursive, scheduleOnFail)"] --> Exists{Directory.Exists?}
Exists -->|No| ReturnTrue["return true<br/>(Already deleted)"]
Exists -->|Yes| CmdRd["Process.Start()<br/>cmd.exe /c rd /s /q"]
CmdRd --> Wait["WaitForExit(30000)"]
Wait --> CheckGone{Directory still<br/>exists?}
CheckGone -->|No| ReturnTrue
CheckGone -->|Yes| NetDelete["Directory.Delete()<br/>(directoryPath, recursive)"]
NetDelete --> Success{Success?}
Success -->|Yes| ReturnTrue
Success -->|No| CheckSchedule{scheduleOnFail<br/>parameter?}
CheckSchedule -->|true| ScheduleDir["ScheduleDirectoryDeleteOnReboot()"]
CheckSchedule -->|false| ReturnFalse["return false"]
ScheduleDir --> ReturnFalse
style Start fill:#f9f9f9
style ReturnTrue fill:#d4edda
style ReturnFalse fill:#f8d7da
style CmdRd fill:#e7f3ff
style NetDelete fill:#fff3cd
Analysis: The method employs a two-stage deletion strategy for performance. First, it attempts to use cmd.exe with rd /s /q (remove directory, subdirectories, quiet mode), which is 10-15x faster than managed .NET methods for large directory trees with many files. The process has a 30-second timeout. If the directory still exists after cmd.exe completes, the method falls back to Directory.Delete() with the recursive flag. Final failures trigger reboot-based scheduling when enabled.
Sources: OfficeScrubC2R-Native.cs:653-687
The implementation uses cmd.exe as the primary deletion mechanism:
using (var process = Process.Start(new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c rd /s /q \"" + directoryPath + "\"",
UseShellExecute = false,
CreateNoWindow = true
}))
{
process.WaitForExit(30000);
}
This approach provides significant performance benefits:
- Bulk operations: Native OS command handles all files/subdirectories in a single operation
- Reduced overhead: Avoids per-file managed code marshalling
- Optimized I/O: Kernel-level batch processing reduces context switches
The 30-second timeout prevents indefinite blocking while allowing time for large directory trees (typical Office installations contain thousands of files).
Sources: OfficeScrubC2R-Native.cs:660-670
When immediate deletion fails due to file locks, the FileHelper class uses Windows' MoveFileEx API with the MOVEFILE_DELAY_UNTIL_REBOOT flag to schedule deletion during the next system boot, before most processes start.
graph TB
subgraph "FileHelper Methods"
DeleteFile["DeleteFile()<br/>catches exceptions"]
DeleteDir["DeleteDirectory()<br/>catches exceptions"]
ScheduleFile["ScheduleDeleteOnReboot()<br/>(string filePath)"]
ScheduleDir["ScheduleDirectoryDeleteOnReboot()<br/>(string directoryPath)"]
end
subgraph "NativeMethods P/Invoke"
MoveFileEx["MoveFileEx()<br/>kernel32.dll"]
Flag["MOVEFILE_DELAY_UNTIL_REBOOT<br/>constant = 0x4"]
end
subgraph "Registry Integration"
RegKey["HKLM:\\SYSTEM\\CurrentControlSet\\<br/>Control\\Session Manager"]
RegValue["PendingFileRenameOperations<br/>(REG_MULTI_SZ)"]
end
subgraph "Internal State"
PendingList["_pendingDeletes<br/>List<string>"]
end
DeleteFile -->|"on failure"| ScheduleFile
DeleteDir -->|"on failure"| ScheduleDir
ScheduleFile --> MoveFileEx
ScheduleDir --> MoveFileEx
MoveFileEx --> Flag
MoveFileEx --> RegValue
RegValue --> RegKey
ScheduleFile --> PendingList
ScheduleDir --> PendingList
style MoveFileEx fill:#e7f3ff
style RegValue fill:#fff3cd
Analysis: The MoveFileEx Win32 API with MOVEFILE_DELAY_UNTIL_REBOOT writes entries to the PendingFileRenameOperations registry value. During boot, the Session Manager processes this list before user logon, when files are not locked. The FileHelper maintains a parallel _pendingDeletes list for logging and status reporting. Each file is added as a pair: the source path (prefixed with \??\) and an empty destination, which signals deletion rather than move.
Sources: OfficeScrubC2R-Native.cs:689-697, NativeMethods:64-68,150
The ScheduleDeleteOnReboot method provides single-file scheduling:
public void ScheduleDeleteOnReboot(string filePath)
{
try
{
NativeMethods.MoveFileEx(filePath, null, NativeMethods.MOVEFILE_DELAY_UNTIL_REBOOT);
_pendingDeletes.Add(filePath);
}
catch { }
}
Key implementation details:
-
Second parameter
null: Indicates deletion rather than move operation -
Flag constant:
MOVEFILE_DELAY_UNTIL_REBOOT(0x4) tells kernel to defer operation - Silent failure: Method catches all exceptions to ensure continuation of cleanup
- List tracking: Maintains local record for reporting purposes
Sources: OfficeScrubC2R-Native.cs:689-697
The ScheduleDirectoryDeleteOnReboot method implements depth-first scheduling to ensure proper deletion order:
sequenceDiagram
participant Caller
participant ScheduleDirMethod as "ScheduleDirectory<br/>DeleteOnReboot()"
participant GetFiles as "Directory.GetFiles()"
participant GetDirs as "Directory.GetDirectories()"
participant ScheduleFile as "ScheduleDelete<br/>OnReboot()"
participant MoveFileEx as "MoveFileEx"
participant PendingList as "_pendingDeletes"
Caller->>ScheduleDirMethod: directoryPath
ScheduleDirMethod->>GetFiles: "*", SearchOption.AllDirectories
GetFiles-->>ScheduleDirMethod: file[]
loop For each file
ScheduleDirMethod->>ScheduleFile: file path
ScheduleFile->>MoveFileEx: file, null, DELAY_FLAG
ScheduleFile->>PendingList: Add(file)
end
ScheduleDirMethod->>GetDirs: "*", SearchOption.AllDirectories
GetDirs-->>ScheduleDirMethod: directories[]
ScheduleDirMethod->>ScheduleDirMethod: OrderByDescending(d => d.Length)
Note over ScheduleDirMethod: Sorts deepest first
loop For each directory (deepest first)
ScheduleDirMethod->>MoveFileEx: dir, null, DELAY_FLAG
ScheduleDirMethod->>PendingList: Add(dir)
end
ScheduleDirMethod->>MoveFileEx: directoryPath, null, DELAY_FLAG
ScheduleDirMethod->>PendingList: Add(directoryPath)
Analysis: Directory scheduling requires careful ordering. All files are scheduled first using SearchOption.AllDirectories to enumerate the entire tree. Then directories are enumerated and sorted by path length in descending order, ensuring leaf directories are scheduled before their parents. This depth-first approach prevents deletion failures during boot when the OS processes operations sequentially. Finally, the root directory itself is scheduled.
Sources: OfficeScrubC2R-Native.cs:699-727
The RegistryHelper provides the complementary method to update the registry directly:
public void AddPendingFileRenameOperation(string filePath)
{
const string keyPath = @"SYSTEM\CurrentControlSet\Control\Session Manager";
const string valueName = "PendingFileRenameOperations";
try
{
using (var key = Registry.LocalMachine.OpenSubKey(keyPath, true))
{
if (key != null)
{
var existing = key.GetValue(valueName) as string[];
var list = new List<string>(existing ?? new string[0]);
list.Add("\\??\\" + filePath);
list.Add(string.Empty);
key.SetValue(valueName, list.ToArray(), RegistryValueKind.MultiString);
}
}
}
catch { }
}
Registry Structure:
-
Hive:
HKEY_LOCAL_MACHINE -
Path:
SYSTEM\CurrentControlSet\Control\Session Manager -
Value:
PendingFileRenameOperations(REG_MULTI_SZ) -
Format: Pairs of strings:
[\??\source_path, destination_path] - Deletion: When destination is empty string, file is deleted
Sources: OfficeScrubC2R-Native.cs:595-616
The FileHelper class provides additional methods for file system inspection and status reporting.
graph LR
Start["GetDirectorySize(directoryPath)"] --> Exists{Directory.Exists?}
Exists -->|No| Return0["return 0"]
Exists -->|Yes| GetFiles["Directory.GetFiles()<br/>SearchOption.AllDirectories"]
GetFiles --> Sum["Sum(f => new FileInfo(f).Length)"]
Sum --> ReturnSize["return long (bytes)"]
style Start fill:#f9f9f9
style ReturnSize fill:#d4edda
The GetDirectorySize method calculates the total size in bytes by enumerating all files recursively and summing their lengths. This is used for logging and reporting space reclaimed during cleanup operations.
Sources: OfficeScrubC2R-Native.cs:734-748
graph LR
Start["IsFileLocked(filePath)"] --> Exists{File.Exists?}
Exists -->|No| ReturnFalse["return false"]
Exists -->|Yes| TryOpen["File.Open()<br/>FileMode.Open<br/>FileAccess.Read<br/>FileShare.None"]
TryOpen --> Success{Open<br/>successful?}
Success -->|Yes| Close["using disposes handle"]
Close --> ReturnFalse
Success -->|No| Catch["catch exception"]
Catch --> ReturnTrue["return true<br/>(file is locked)"]
style ReturnTrue fill:#f8d7da
style ReturnFalse fill:#d4edda
The IsFileLocked method attempts to open the file with exclusive access (FileShare.None). If the operation fails, the file is locked by another process. This helps determine whether scheduleOnFail behavior will be triggered.
Sources: OfficeScrubC2R-Native.cs:750-766
The GetPendingDeletes method returns a copy of the internal tracking list:
public List<string> GetPendingDeletes()
{
return new List<string>(_pendingDeletes);
}
This enables the orchestrator to log scheduled deletions and inform users that a reboot is required. The method returns a new list to prevent external modification of internal state.
Sources: OfficeScrubC2R-Native.cs:729-732
The FileHelper is instantiated and managed by the OfficeScrubOrchestrator as part of the helper class collection:
graph TB
subgraph "OfficeScrubOrchestrator"
Orch["OfficeScrubOrchestrator<br/>constructor"]
FilesProp["Files property<br/>(FileHelper)"]
end
subgraph "FileHelper Instance"
FH["FileHelper instance"]
PendingList["_pendingDeletes<br/>List<string>"]
end
subgraph "Orchestrator Usage"
DeleteFiles["Delete Office files"]
DeleteDirs["Delete Office directories"]
CheckPending["Check pending deletes"]
ReportReboot["Report reboot requirement"]
end
subgraph "Stage 3: File System Cleanup"
RemoveProgFiles["Remove Program Files"]
RemoveProgramData["Remove ProgramData"]
RemoveAppData["Remove AppData"]
end
Orch -->|"initializes"| FH
FH --> FilesProp
FilesProp --> PendingList
RemoveProgFiles --> DeleteDirs
RemoveProgramData --> DeleteDirs
RemoveAppData --> DeleteDirs
DeleteFiles -->|"calls"| FilesProp
DeleteDirs -->|"calls"| FilesProp
CheckPending -->|"calls GetPendingDeletes()"| FilesProp
CheckPending --> ReportReboot
style Orch fill:#f9f9f9
style FilesProp fill:#e7f3ff
Integration Points:
-
Initialization: The orchestrator creates a
FileHelperinstance during construction:Files = new FileHelper(); -
Stage 3 Operations: During file system cleanup, the orchestrator calls
Files.DeleteDirectory()for Office installation paths andFiles.DeleteFile()for individual files like scheduled tasks -
Reboot Detection: After cleanup, the orchestrator checks
Files.GetPendingDeletes()to determine if reboot is required and includes the count in status reporting -
No Reboot Mode: When
/NoRebootparameter is used, the orchestrator passesscheduleOnFail: falseto prevent automatic scheduling
Sources: OfficeScrubC2R-Native.cs:1349-1383
fileHelper.DeleteFile(@"C:\Users\Username\Desktop\Office.lnk", scheduleOnFail: true)
Returns true if deleted immediately, false if scheduled for reboot.
fileHelper.DeleteDirectory(@"C:\Program Files\Microsoft Office", recursive: true, scheduleOnFail: true)
Attempts fast cmd.exe deletion, falls back to .NET, and schedules on failure.
if (fileHelper.IsFileLocked(filePath))
{
// File is in use, schedule for reboot
fileHelper.ScheduleDeleteOnReboot(filePath);
}
var pendingDeletes = fileHelper.GetPendingDeletes();
if (pendingDeletes.Count > 0)
{
Console.WriteLine($"Reboot required to delete {pendingDeletes.Count} items");
}
Sources: OfficeScrubC2R-Native.cs:620-767
The FileHelper class implements a consistent error handling pattern:
| Scenario | Behavior |
|---|---|
| File doesn't exist | Return true (already deleted) |
| Deletion succeeds | Return true
|
Deletion fails + scheduleOnFail=true |
Schedule for reboot, return false
|
Deletion fails + scheduleOnFail=false |
Return false immediately |
| Exception during scheduling | Caught silently, operation continues |
All exceptions are caught with empty catch blocks to ensure cleanup continues even if individual operations fail. The orchestrator logs operations at a higher level for diagnostic purposes.
Sources: OfficeScrubC2R-Native.cs:626-687
| Operation | Strategy | Performance Gain |
|---|---|---|
| Single file deletion | .NET File.Delete()
|
Baseline |
| Read-only file | Attribute clearing before delete | Avoids common failure |
| Small directory | .NET Directory.Delete()
|
Baseline |
| Large directory |
cmd.exe rd /s /q first |
10-15x faster than .NET |
| Locked files |
MoveFileEx scheduling |
Deferred to boot (0 runtime cost) |
| Directory size | LINQ Sum() with FileInfo
|
Single pass enumeration |
The primary optimization is the cmd.exe approach for directories, which significantly improves performance in typical Office cleanup scenarios where thousands of files need deletion.
Sources: OfficeScrubC2R-Native.cs:653-687, docs/memory-bank/product.md:26-30