I am trying to simulate an annoying file lock that occurs sometimes so I can test the affect on a tool I have created. So far I have been unable to simulate the file lock. The symptoms I experience with the file lock is the following:
- I can move and rename the file without issues.
- If I attempt to delete the file, it appears to delete, but when I refresh the folder, it re-appears. After the attempted deletion, I cannot create a new file with the same name in that folder until after I reboot.
Note that the file appears to be locked by a thread in the system process.
All attempts so far to lock a file in a similar way prevent me from being able to move or rename the locked file. I am now on Update #3 (see below). I have used different Windows APIs such as LockFileEx and .NET libraries, but so far none have reproduced the behaviour. I appreciate any suggestions that could help determine a way to simulate this, even if not via the system process.
This is one example of the code I have unsuccessfully tried to attempt to simulate this locking behaviour.
Private FileHandle As IntPtr
Private LockAcquired As LockAcquiredEnum
Private ResetEvent As ManualResetEvent = Nothing
Private Overlapped As New System.Threading.NativeOverlapped
Private Enum LockAcquiredEnum
Failed
Pending
Succeeded
End Enum
#Region "Declarations"
Private Const INVALID_HANDLE_VALUE As Short = -1
Private Const ERROR_SUCCESS As Short = 0
Public Const ERROR_IO_PENDING As Integer = &H3E5
<DllImport("kernel32.dll", CharSet:=CharSet.Unicode, SetLastError:=True, EntryPoint:="CreateFileW")>
Private Shared Function CreateFile(<MarshalAs(UnmanagedType.LPWStr)> ByVal filename As String, <MarshalAs(UnmanagedType.U4)> ByVal access As FileAccess, <MarshalAs(UnmanagedType.U4)> ByVal share As FileShare, ByVal securityAttributes As IntPtr, <MarshalAs(UnmanagedType.U4)> ByVal creationDisposition As FileMode, <MarshalAs(UnmanagedType.U4)> ByVal OptionsAndAttributes As UInteger, ByVal templateFile As IntPtr) As IntPtr
End Function
<DllImport("kernel32.dll", SetLastError:=True)>
Private Shared Function LockFileEx(ByVal hFile As IntPtr, ByVal dwFlags As UInteger, ByVal dwReserved As UInteger, ByVal nNumberOfBytesToLockLow As UInteger, ByVal nNumberOfBytesToLockHigh As UInteger, <[In]> ByRef lpOverlapped As System.Threading.NativeOverlapped) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
<DllImport("kernel32.dll")>
Shared Function UnlockFileEx(ByVal hFile As IntPtr, ByVal dwReserved As UInteger, ByVal nNumberOfBytesToUnlockLow As UInteger, ByVal nNumberOfBytesToUnlockHigh As UInteger, <[In]> ByRef lpOverlapped As System.Threading.NativeOverlapped) As Boolean
End Function
<DllImport("kernel32.dll", SetLastError:=True)>
Private Shared Function CloseHandle(ByVal hHandle As IntPtr) As Boolean
End Function
Private Enum FileLockEnum As UInteger
LOCKFILE_FAIL_IMMEDIATELY = 1
LOCKFILE_EXCLUSIVE_LOCK = 2
End Enum
#End Region
Private Sub cmdLockTestFile_Click(sender As Object, e As EventArgs) Handles cmdLockTestFile.Click
LockFile()
End Sub
Private Sub cmdUnlockTestFile_Click(sender As Object, e As EventArgs) Handles cmdUnlockTestFile.Click
UnlockFile()
End Sub
Private Sub LockFile()
' Get the options
Dim dwFileFlags As FileOptions
Dim dwLockFlags As FileLockEnum
dwFileFlags = FileOptions.Asynchronous
' Open the file
lblStatus.Text = $"Opening the file 'TestFile.exe' as {If((dwFileFlags And FileOptions.Asynchronous) <> 0, "asynchronous", "synchronous")}" & vbLf
FileHandle = CreateFile("C:\Program Files (x86)\App\TestFile.exe", FileAccess.Read, FileShare.ReadWrite, Nothing, FileMode.Open, FileAttributes.Normal Or dwFileFlags, Nothing)
If FileHandle = INVALID_HANDLE_VALUE Then
lblStatus.Text = $"Open failed, error = {Marshal.GetLastWin32Error()}" & vbLf
Return
End If
'Optionally set this
'dwLockFlags = FileLockEnum.LOCKFILE_EXCLUSIVE_LOCK
' Set the starting position in the OVERLAPPED structure
Dim o As New System.Threading.NativeOverlapped
o.OffsetLow = 0 ' we lock on byte zero
' Say what kind of lock we want
If (dwLockFlags And FileLockEnum.LOCKFILE_EXCLUSIVE_LOCK) <> 0 Then
lblStatus.Text = "Requesting exclusive lock" & vbLf
Else
lblStatus.Text = "Requesting shared lock" & vbLf
End If
' Say whether we're going to wait to acquire
If (dwLockFlags And FileLockEnum.LOCKFILE_FAIL_IMMEDIATELY) <> 0 Then
lblStatus.Text = "Requesting immediate failure" & vbLf
ElseIf dwFileFlags And FileOptions.Asynchronous Then
lblStatus.Text = "Requesting notification on lock acquisition" & vbLf
' The event that will be signaled when the lock is acquired
' error checking deleted for expository purposes
ResetEvent = New ManualResetEvent(False)
o.EventHandle = ResetEvent.SafeWaitHandle.DangerousGetHandle
Else
lblStatus.Text = "Call will block until lock is acquired" & vbLf
End If
' Okay, here we go.
lblStatus.Text = "Attempting lock" & vbLf
Dim IsLockAcquired As Boolean = LockFileEx(FileHandle, dwLockFlags, 0, 1, 0, o)
' If the lock failed, remember why.
Dim dwError As UInteger = If(IsLockAcquired, ERROR_SUCCESS, Marshal.GetLastWin32Error())
lblStatus.Text = $"Wait {If(IsLockAcquired, "succeeded", "failed")}, error code {dwError}" & vbLf
If IsLockAcquired Then
lblStatus.Text = "Lock acquired immediately" & vbLf
LockAcquired = LockAcquiredEnum.Succeeded
ElseIf dwError = ERROR_IO_PENDING Then
LockAcquired = LockAcquiredEnum.Pending
lblStatus.Text = "Waiting for lock" & vbLf
Else
LockAcquired = LockAcquiredEnum.Failed
End If
End Sub
Private Sub UnlockFile()
If LockAcquired = LockAcquiredEnum.Pending Then
ResetEvent.WaitOne()
LockAcquired = LockAcquiredEnum.Succeeded
End If
' If we got the lock, then hold the lock until the user releases it.
If LockAcquired = LockAcquiredEnum.Succeeded Then
lblStatus.Text = "Unlocking" & vbLf
UnlockFileEx(FileHandle, 0, 1, 0, Overlapped)
End If
' Clean up
If ResetEvent IsNot Nothing Then
ResetEvent.Close()
ResetEvent.Dispose()
End If
CloseHandle(FileHandle)
lblStatus.Text = "Unlocked TestFile.exe" & vbLf
End Sub
I am unable to reproduce the behaviour I have experienced using this code. It seems to be caused by something running under Windows' system process (pid 4)
Update: Based on Feedback from @HansPassant, I have tried to modify my code to simulate this behaviour, but I still cannot reproduce this. I first tried removing the code that makes use of the LockFileEx API, and only opening the file with the CreateFile API with the addition of the FileShare.Delete attribute to one parameter.
FileHandle = CreateFile("C:\Program Files (x86)\App\TestFile.exe", FileAccess.Read, FileShare.Read Or FileShare.Delete, Nothing, FileMode.Open, FileAttributes.Normal Or dwFileFlags, Nothing)
I then tried to completely rewrite it using a FileStream object like the other article mentions, but that did not work either.
Public Class Form1
Private TestFileStream As FileStream
Private Sub cmdLockTestFile_Click(sender As Object, e As EventArgs) Handles cmdLockTestFile.Click
LockFile()
End Sub
Private Sub cmdUnlockTestFile_Click(sender As Object, e As EventArgs) Handles cmdUnlockTestFile.Click
UnlockFile()
End Sub
Private Sub LockFile()
' Open the file
lblStatus.Text = $"Opening the file 'TestFile.exe' with FileShare.Delete permissions." & vbLf
Try
TestFileStream = New FileStream("C:\Program Files (x86)\App\TestFile.exe", FileMode.Open, FileAccess.Read, FileShare.Read Or FileShare.Delete)
lblStatus.Text &= "File is opened with FileShare.Delete permissions." & vbLf
FileReadTimer.Enabled = True
Catch ex As Exception
lblStatus.Text &= $"Open failed, error = {ex.Message}" & vbLf
Return
End Try
End Sub
Private Sub UnlockFile()
If TestFileStream IsNot Nothing Then
TestFileStream.Close()
End If
FileReadTimer.Enabled = False
lblStatus.Text &= "Closed TestFile.exe"
End Sub
Private Sub FileReadTimer_Tick(sender As Object, e As EventArgs) Handles FileReadTimer.Tick
Dim Value As Integer
Value = TestFileStream.ReadByte()
If Value = -1 Then
TestFileStream.Position = 0
Value = TestFileStream.ReadByte()
End If
End Sub
End Class
I even tried this, but it didn't help.
TestFileStream = New FileStream("C:\Program Files (x86)\App\TestFile.exe", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite Or FileShare.Delete)
The only thing I did not try was modifying the file. It does not make sense to me that whatever process is locking the file would actually be modifying it, and I do not want to damage the file in any way.
So far, no matter what I have tried, while the file is open in code, I am still able to delete the file from the folder. A refresh of that folder does not make it re-appear, and I am able to create a new file with the same name.
Update #2:
I tried following the example here https://stackoverflow.com/a/19875330/4816919 but I still could not reproduce it. (It is the same stackoverflow post mentioned by @HansPassant) I changed the lines that create the FileStream object to where the Timer is enabled to this:
TestFileStream = New FileStream("C:\Program Files (x86)\App\TestFile.exe", FileMode.Open, FileAccess.Read, FileShare.Read Or FileShare.Delete, 4096, FileOptions.SequentialScan)
lblStatus.Text &= "File is opened with FileShare.Delete permissions." & vbLf
TestFileStream.Read(New Byte(99){}, 1, 1)
GC.KeepAlive(TestFileStream)
FileReadTimer.Enabled = True
I am at a loss as to how to do this so far.
Update #3 Based on my research, it appears that this is a behaviour of transactional NTFS. I have therefore modified my project to use the AlphaFS library, which simplifies performing transactional NTFS operations.
This is my latest code.
Public Class Form1
Private sxRuntimeStream As FileStream
Private FileTransaction As Alphaleonis.Win32.Filesystem.KernelTransaction
Private Sub cmdLockSxRuntime_Click(sender As Object, e As EventArgs) Handles cmdLockSxRuntime.Click
LockFile()
End Sub
Private Sub cmdUnlockSxRuntime_Click(sender As Object, e As EventArgs) Handles cmdUnlockSxRuntime.Click
UnlockFile()
End Sub
Private Sub cmdCloseTransaction_Click(sender As Object, e As EventArgs) Handles cmdCloseTransaction.Click
CloseTransaction()
End Sub
Private Sub LockFile()
' Open the file
If FileTransaction IsNot Nothing Then
lblStatus.Text = "The Transaction is still open." & vbLf
ElseIf sxRuntimeStream IsNot Nothing Then
lblStatus.Text = "The file 'sxRuntime.exe' is already open." & vbLf
Else
lblStatus.Text = "Opening the file 'sxRuntime.exe' with FileShare.Delete permissions." & vbLf
Try
FileTransaction = New Alphaleonis.Win32.Filesystem.KernelTransaction()
sxRuntimeStream = Alphaleonis.Win32.Filesystem.File.OpenTransacted(FileTransaction, "C:\Program Files (x86)\ActiveERP\sxRuntime.exe", FileMode.Open, FileAccess.Read, FileShare.ReadWrite Or FileShare.Delete, 4096, Alphaleonis.Win32.Filesystem.PathFormat.FullPath)
lblStatus.Text &= "File is opened with FileShare.Delete permissions." & vbLf
sxRuntimeStream.Read(New Byte(99) {}, 1, 1)
GC.KeepAlive(sxRuntimeStream)
FileReadTimer.Enabled = True
Catch ex As Exception
lblStatus.Text &= $"Open failed, error = {ex.Message}" & vbLf
Return
End Try
End If
End Sub
Private Sub UnlockFile()
FileReadTimer.Enabled = False
If sxRuntimeStream IsNot Nothing Then
sxRuntimeStream.Close()
sxRuntimeStream.Dispose()
sxRuntimeStream = Nothing
lblStatus.Text &= "Closed sxRuntime.exe" & vbLf
End If
End Sub
Private Sub CloseTransaction()
If FileTransaction IsNot Nothing Then
FileTransaction.Commit()
FileTransaction.Dispose()
FileTransaction = Nothing
lblStatus.Text &= "Transaction closed"
End If
End Sub
Private Sub FileReadTimer_Tick(sender As Object, e As EventArgs) Handles FileReadTimer.Tick
Dim Value As Integer
Value = sxRuntimeStream.ReadByte()
If Value = -1 Then
sxRuntimeStream.Position = 0
Value = sxRuntimeStream.ReadByte()
End If
End Sub
End Class
Unfortunately, I am still unable to reproduce this behaviour. I appreciate any suggestions or insights into this matter.
FYI, as it may have been lost above, the behaviour is that when an affected file is attempted to be deleted via Windows Explorer, it appears to work, but a new file in the same location and name cannot be created. Upon refresh, the file re-appears, but most of the file's properties are now lost. Typically only a reboot will finalize the deletion of the file, although I am not concerned with this part.