Search This Blog

Friday, 1 December 2017

PowerShell Golden Egg - Delete files/folders on Reboot the Microsoft Way (MoveFileEx)

Yeah, just do it on reboot...

So simple, eh? Can't delete the script while it's running. but once the script reboots the PC, there's just that to delete and you're done. So delete it on the reboot yeah? If only it was that simple...

Run\RunOnce

So Microsoft have these "run and runonce" reg keys, one set for each user, one set for the machine itself. But as people have discovered, the time at which these keys execute is between OS starting to load and when the user logon is initiated, but this is before the desktop has loaded, which can complicate things and this has been depreciated in Windows 10 in favour of Asynchronous commands. Also, through my own experiences, I can attest to the complications of having to set a run\runonce command in script which requires some interesting syntax skill and it's worth nothing that the current user run\runonce keys run under the credentials of THAT user. So if they aren't an administrator, the command will be run in a standard user context with the restrictions that apply. All in all, I found this approach buggy, prone to many unusual errors dependent on OS or machine and insufferably complex for something as simple as deleting on reboot.

How do Microsoft do it?

The PendingFileRenameOperations key is a red herring.

Installers, updates and all kinds of packages delete files on reboot and they aren't leveraging keys like this, so what's the correct approach? Well, according to Microsoft documentation, the information for these file changes on reboot is kept in the  'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager' key under the value 'PendingFileRenameOperations' which is a 'REG_MULTI_SZ' or MultiString in PowerShell. Now this seems like a good idea, and in a basic format of source and destination, but you soon find that the strings in the multistring array are null terminated, meaning, they need to have this at end of the string for the values to be read correctly and missing these or messing them up can corrupt the key, causing the computer to constantly think there's pending changes. So trying to keep the null values intact while parsing the key through a PowerShell script seems daunting. Instead...

MoveFileEx!!!


Microsoft have their own Windows API function (or method, my jargon is flexible) for handling this key in the correct manner, so if we declare a new type with the call to that definition, we can use the function to have Windows delete the file on reboot correctly. This poor guy on reddit was so close and I give him many thanks for basically handing me most of the code but the thread is locked and I can't seem to contact the guy. Still, for anyone reading this...

THE ANSWER!!!

Null... ish... (That reddit guy's answer)

PowerShell has a few ideas of what 'null' actually is. Is it an object with a type, but no value; an object without type; the absence of an object? Well, the one that answered the question for me was '[Management.Automation.Language.NullString]::Value()'. This is important because the destination of the MoveFileEx function has to be inputted as the correct type of null for the command to succeed which is what that guy needed.

Add-Type but no Remove-Type

It's also worth noting that I made some use of try catch to check for type loading. This is because the type is loaded in the AppDomain context and can't be unloaded unless a new session is established. The default namespace for these autogenerated types is 'Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes'.

The Code

function Move-OnReboot([parameter(Mandatory=$True)]$Path, $Destination){
Begin{
try{
[Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MoveFileUtils]|Out-Null
}catch{
$memberDefinition = @’
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
‘@
Add-Type -Name MoveFileUtils -MemberDefinition $memberDefinition
}
}
Process{
$Path="$((Resolve-Path $Path).Path)"
if ($Destination){
$Destination = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination)
}else{$Destination = [Management.Automation.Language.NullString]::Value}
$MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004
[Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MoveFileUtils]::MoveFileEx($Path, $Destination, $MOVEFILE_DELAY_UNTIL_REBOOT)
}
End{}
}


Stick this in a script, module, whatever.
Pass it a path.
Pass it a destination to move to another path or omit to delete the original on reboot.
Get a boolean success or fail on return.

Files First - Bonus Material

One joyful quirk of the MoveFileEx operation is that although you can set a folder to delete on reboot, it will not actually perform the command if the folder contains files. This means you must set the files within the folder to delete before you set the folder to delete.

Bonus, bonus...

My script is contained in one folder when deployed, so I made a simple compression of the function code specific for deleting the script folder and containing files only, no sub folders. Placed in a script, will schedule the parent folder and all files for deletion. Put the header at the top of your script and the footer just before the last reboot call.

# Header
$scriptPath=$MyInvocation.MyCommand.Definition
$scriptFolder=(Split-Path -Parent $scriptPath)
function D-OR([parameter(Mandatory=$True)]$Path){
$Path="$((Resolve-Path $Path).Path)"
try{
[Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MoveFileUtils]|Out-Null
}catch{
$memberDefinition = @’
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
‘@
Add-Type -Name MoveFileUtils -MemberDefinition $memberDefinition
}
"Attempting to delete $Path on Reboot"
[Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.MoveFileUtils]::MoveFileEx($Path,[Management.Automation.Language.NullString]::Value,4)
}
# Footer
if(Test-Path $scriptFolder){
 Get-ChildItem $scriptFolder|Select-Object -ExpandProperty FullName|ForEach-Object{D-OR $_}
 D-OR $scriptFolder
}