Weekend Scripter: Use PowerShell and Pinvoke to Remove Stubborn Files

Dr Scripto

October 19th, 2013

Summary: Windows PowerShell MVP, Boe Prox, talks about using Windows PowerShell and Pinvoke to delete locked files.

Microsoft Scripting Guy, Ed Wilson, is here. Today we have a guest blog post by newly crowned Windows PowerShell MVP, Boe Prox. Long-time readers of the Hey, Scripting Guy! Blog are familiar with Boe’s work, but this is his first guest blog post as an MVP. WooHoo!

Boe Prox is a Microsoft MVP in Windows PowerShell and a senior Windows system administrator. He has worked in the IT field since 2003, and he supports a variety of different platforms. He is also an Honorary Scripting Guy and has submitted a number of posts as a guest blogger, which discuss a variety of topics. He is a contributing author in PowerShell Deep Dives with chapters about WSUS and TCP communication. He is a moderator on the Hey, Scripting Guy! forum, and he has been a judge for the Scripting Games since 2010. He recently presented talks on the topic of WSUS and Windows PowerShell at the Mississippi PowerShell User Group.

To read more of his posts, see these Hey, Scripting Guy! Blog posts by Boe Prox.

Boe’s blog: Learn Powershell | Achieve More

Codeplex projects: PoshWSUS, PoshPAIG, PoshChat, and PoshEventUI

Take it away, Boe…

Note The complete script for this blog post is available via the Scripting Guys Script Repository.

We have all been there. A file that we need to delete is locked by some process that we are unable to kill for one reason or another, and we have to reboot the system in hopes that by the time we get signed in to the system, it will not have a lock on the file. Wouldn’t it be great if we can just mark the file for deletion after a reboot and not worry about it being around by the time we sign in? If you answered, “Yes!” to this question, stick around because I will show you how to accomplish this task by using Windows PowerShell and hooking into the Win32 API using Pinvoke.

One of the many beauties of Windows PowerShell is its ability to use inline code (C# for example) that can be compiled by using Add-Type and then called to perform an action. By doing so, you can leverage some pretty powerful low-level functions to do things that you just cannot do natively via Windows PowerShell.

The first thing that needs to be done is determine what we can use to make this work. Looking at Pinvoke.net (a great website for finding out what is available to use with Pinvoke), I find the MoveFileEx function and the C# signature that we can put to work in Windows PowerShell.

Looking at the MoveFileEx function, I see three parameters (lpExistingFileName, lpNewFileName, and dwFlags) that are required for the function to operate correctly. The first two parameters are pretty simple to understand: we need the source file that will be deleted and then we need a destination file. If this sounds like it an issue, trust me, it isn’t. All I need to do is specify $Null in its place, and the file will be marked for deletion instead. Of course, all of this rests in the hands of the dwFlags parameter that is still left to deal with.

Looking at the same page, I can see a link to MoveFileFlags (Enum) where I can get info about the available flags. Judging from the information, I only need to supply 0x4 as the flag to mark the file for deletion after a reboot. Or I can more easily create the Enum and reference the proper value (MOVEFILE_DELAY_UNTIL_REBOOT).

Although this is fine, I would prefer to know what each one means. Fortunately, I can also check out the Windows Dev Center page about this: MoveFileEx function.

From here, I find the flag that I am most curious about, MOVEFILE_DELAY_UNTIL_REBOOT.

MOVEFILE_DELAY_UNTIL_REBOOT 4 (0x4) The system does not move the file until the operating system is restarted. The system moves the file immediately after AUTOCHK is executed, but before creating any paging files. Consequently, this parameter enables the function to delete paging files from previous startups. This value can be used only if the process is in the context of a user who belongs to the administrators group or the LocalSystem account. This value cannot be used with MOVEFILE_COPY_ALLOWED. Windows Server 2003 and Windows XP: For information about special situations where this functionality can fail, and a suggested workaround solution, see Files are not exchanged when Windows Server 2003 restarts if you use the MoveFileEx function to schedule a replacement for some files in the Help and Support Knowledge Base.

The key takeaway here is that only an Administrator can really utilize this flag to mark a file for deletion. In case you were wondering how I know that this flag is used in conjunction with the second parameter of the MoveFileEx, check out the description for the lpNewFileName parameter:

By using this information, I can create a Here-String and supply it to Add-Type to create a compiled type that can be used in our example to mark a file for deletion.

Add-Type @”

using System;

using System.Text;

using System.Runtime.InteropServices;

public class Posh

{

public enum MoveFileFlags

{

MOVEFILE_REPLACE_EXISTING = 0x00000001,

MOVEFILE_COPY_ALLOWED = 0x00000002,

MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004,

MOVEFILE_WRITE_THROUGH = 0x00000008,

MOVEFILE_CREATE_HARDLINK = 0x00000010,

MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020

}

[DllImport(“kernel32.dll”, SetLastError = true, CharSet = CharSet.Unicode)]

static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags);

public static bool MarkFileDelete (string sourcefile)

{

bool brc = false;

brc = MoveFileEx(sourcefile, null, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT);

return brc;

}

}

“@

I’ll caveat this by saying that I am not a C# developer, but I can work my way around the code to make this work the way I want it to. My original intention was to simply reference the MoveFileEx method with my custom type, but supplying $Null into the NewFileName parameter would not work properly. In fact, it assumes that $Null is the actual file name, and it would throw an error when it couldn’t locate it. So with that, I had to dip my toes into the C# waters to make some adjustments to get this to work properly.

We can verify that this actually worked by calling the new type with the static method:

[Posh]::MoveFileEx

Perfect! Now I just need to pick out a file that I want to test this against.

Warning This has the potential to greatly cause havoc to your system if you pick the wrong file or a system file to mark for deletion! Use at your own risk!

Well look at this, a test.txt file just waiting to be deleted:

When you are working with this method and supplying the file name, it is important that you provide the full name of the file. Adding only the file name will have unintended results.

[Posh]::MarkFileDelete(“C:\users\Administrator\Desktop\TEST.txt”)

This will return a Boolean value (True = file marked for deletion; False = something happened and needs to be looked at). To really see what the issue is other than a False value, we will have to force the error out by calling System.WindowsComponent.Win32Exception:

Throw (New-Object System.ComponentModel.Win32Exception)

In this case, the error is nothing more than a confirmation that the command worked. But if a False value was returned, this error would let you know why it was returned.

How can we verify that this will actually happen? The registry will provide the answer to this question. By navigating to HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager and looking for the PendingFileRenameOperations property in the registry, you should see the file name listed and nothing on the next line. (You may have multiple operations for deletions or moves in this property, which will have the original file and the new location listed directly after the original file.)

(Get-ItemProperty -Path ‘HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager’ -Name ‘PendingFileRenameOperations’).PendingFileRenameOperations

\??\C:\users\Administrator\desktop\test.txt is what we are looking for. That space after the file is there for a reason. What this means is that as soon as we reboot the system, this file will be gone the next time we sign in. Now queue the reboot:

Restart-Computer -Force

Now we are back and signed in to the system. Let’s check to see if the file is still around.

Nope, it is long gone. And we can also check the registry to see if we have any pending file operations:

(Get-ItemProperty -Path ‘HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager’ -Name ‘PendingFileRenameOperations’).PendingFileRenameOperations

Well, it is a good sign that there is nothing in the registry. So with that, you have seen how to use the MoveFileEx method with Pinvoke to mark a file for deletion after a reboot. You can also use this on directories, but the directory must have no child items (files or folders) in it for this to work.

Of course, I couldn’t stop at this. I also made a function (Register-FileToDelete) that makes this a much easier process and allows for verbose logging, -WhatIf parameter (because if you are doing something that has potential to make a change, you better have this), and handles input from the pipeline. You can download this function from the Script Center Repository: Register-FileToDelete.

Here is an example of what would happen if I simply wanted to hit all .txt files, and I use the –WhatIf switch to avoid accidently deleting the files:

Get-ChildItem -File -Filter *.txt | Register-FileToDelete -WhatIf

In this instance, only two files would have been deleted after the reboot. So now let’s mark the test.txt file for deletion:

Get-ChildItem -File -Filter test.txt | Register-FileToDelete -Verbose

And there you have it! The next time this system reboots, it will delete the file.

That is all for working with Pinvoke and utilizing the MoveFileEx function to make it easier to remove a file that may be locked by a process after a reboot. Hope you enjoyed it!

~Boe

Thank you, Boe, for an awesome post, and one that will be immediately useful to many readers.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy