Skip to content

FileHelper

Calvindd2f edited this page Jan 14, 2026 · 1 revision

FileHelper

Relevant source files

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

Purpose and Scope

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


Class Structure and Members

The FileHelper class maintains internal state to track pending deletions and provides both synchronous deletion methods and reboot-scheduling fallback mechanisms.

Class Declaration and Fields

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


Public Methods

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


File Deletion Strategy

The DeleteFile method implements a multi-stage approach to handle various file system scenarios, including read-only attributes and file locks.

File Deletion Decision Flow

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
Loading

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


Read-Only Attribute Handling

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


Directory Deletion Strategy

The DeleteDirectory method implements a performance-optimized approach using native Windows commands with .NET fallback.

Directory Deletion Workflow

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
Loading

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


Performance Optimization: cmd.exe vs .NET

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


Reboot Scheduling Mechanism

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.

MoveFileEx API Integration

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&lt;string&gt;"]
    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
Loading

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


File Scheduling Implementation

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


Directory Scheduling Implementation

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)
Loading

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


PendingFileRenameOperations Registry Mechanism

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


Utility Methods

The FileHelper class provides additional methods for file system inspection and status reporting.

Directory Size Calculation

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
Loading

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


File Lock Detection

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
Loading

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


Pending Deletes Accessor

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


Integration with OfficeScrubOrchestrator

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&lt;string&gt;"]
    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
Loading

Integration Points:

  1. Initialization: The orchestrator creates a FileHelper instance during construction: Files = new FileHelper();

  2. Stage 3 Operations: During file system cleanup, the orchestrator calls Files.DeleteDirectory() for Office installation paths and Files.DeleteFile() for individual files like scheduled tasks

  3. Reboot Detection: After cleanup, the orchestrator checks Files.GetPendingDeletes() to determine if reboot is required and includes the count in status reporting

  4. No Reboot Mode: When /NoReboot parameter is used, the orchestrator passes scheduleOnFail: false to prevent automatic scheduling

Sources: OfficeScrubC2R-Native.cs:1349-1383


Usage Patterns

Standard File Deletion

fileHelper.DeleteFile(@"C:\Users\Username\Desktop\Office.lnk", scheduleOnFail: true)

Returns true if deleted immediately, false if scheduled for reboot.


Directory Deletion with Options

fileHelper.DeleteDirectory(@"C:\Program Files\Microsoft Office", recursive: true, scheduleOnFail: true)

Attempts fast cmd.exe deletion, falls back to .NET, and schedules on failure.


Checking Lock Status

if (fileHelper.IsFileLocked(filePath))
{
    // File is in use, schedule for reboot
    fileHelper.ScheduleDeleteOnReboot(filePath);
}

Reporting Pending Operations

var pendingDeletes = fileHelper.GetPendingDeletes();
if (pendingDeletes.Count > 0)
{
    Console.WriteLine($"Reboot required to delete {pendingDeletes.Count} items");
}

Sources: OfficeScrubC2R-Native.cs:620-767


Error Handling Strategy

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


Performance Characteristics

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

Clone this wiki locally