0

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:

  1. I can move and rename the file without issues.
  2. 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.

Shaggie
  • 111
  • 1
  • 8
  • I have included code I tried to use to simulate the file lock, but it did not produce the results I had expected. I also unsuccessfully tried to use powershell to reproduce that. I am hoping that someone can provide me with an example or insight on how to reproduce this behaviour. – Shaggie May 11 '22 at 05:34
  • Thank you @HansPassant. I thought I tried that in one of my many attempts, but I will try it again. Perhaps I released the lock when I tried that. Whether it works or not, I will update this post with the results. – Shaggie May 11 '22 at 14:12
  • I still can't simulate that behaviour. I don't know what I am doing wrong. – Shaggie May 26 '22 at 04:04
  • It appears that this is due to transactional NTFS. Apparently Windows used to use transactional NTFS for file operations, but it appears to have switched to a different method somewhat recently, however applications can still use (opt-in to) transactional NTFS on files, which seems to be what has been occurring. I am investigating that now. see: https://stackoverflow.com/questions/60424732/did-the-behaviour-of-deleted-files-open-with-fileshare-delete-change-on-windows#:~:text=If%20you%20want%20the%20classic%20behavior%20that%20leaves%20the%20file – Shaggie May 26 '22 at 04:43

0 Answers0