Fixing System.IO.File Sharing Violations Between Readers and Writers in PowerShell
When coordinating tasks across multiple computers or processes, using a lock file on a shared network drive (SMB) is a common, lightweight synchronization pattern. However, configuring [System.IO.File]::Open to allow a reader process to inspect a locked file held by a writer can easily trigger an unexpected IOException:
Exception calling "Open" with "4" argument(s): "The process cannot access the file because it is being used by another process."
Even if you explicitly specified FileShare.Read on both ends, Windows will block the second process. Let's explore why this happens and how to fix it properly.
The Root Cause: How FileShare Negotiation Works
The issue stems from a subtle misunderstanding of the FileShare parameter. The FileShare flag does not declare what access rights you need; it declares what access rights you are willing to grant to other processes.
When two processes access a file, Windows checks two compatibility rules:
- Is the access I am requesting compatible with the sharing permissions already granted by existing open handles?
- Is the access currently held by existing handles compatible with the sharing permissions I am declaring?
Let's break down what happened in the original code:
- Process 1 (Writer):
FileAccess: WriteFileShare: Read(Process 1 permits other processes to read, but denies write access).
- Process 2 (Reader):
FileAccess: Read(Compatible with Process 1'sFileShare: Read).FileShare: Read(Process 2 permits other processes to read, but denies write access).
Because Process 2 specified only Read for its sharing mode, it is asking Windows to guarantee that no other process currently has or will obtain Write access. However, Process 1 already holds a Write handle. As a result, Windows rejects Process 2 with a sharing violation.
The Solution: Allow Concurrent Writers on the Reader Handle
To resolve this, the reader process must explicitly tolerate existing writers by specifying FileShare.ReadWrite (or 'ReadWrite' in PowerShell).
Additionally, don't forget to call $FileStream.Flush() on the writer side. Network file systems and .NET streams buffer data in memory, meaning another process may read an empty file if the stream has not yet flushed its buffer to disk.
Refactored Working Code
Here is the updated script implementing proper share flags, buffering, and lock file reading:
$path = '\\share\lock\test-lock.lock'
$gotTheLock = $false
$FileStream = $null
try {
try {
# Writer: Creates/opens the file with Write access, allowing others to Read
# Use 'CreateNew' or 'Create' depending on your lock requirements
$FileStream = [System.IO.File]::Open($path, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read)
$gotTheLock = $true
} catch [System.IO.IOException], [System.Management.Automation.MethodInvocationException] {
# Failed to get lock because another process has an exclusive handle
$gotTheLock = $false
}
if (-not $gotTheLock) {
# Allow the lock owner a brief moment to write its metadata
Start-Sleep -Milliseconds 500
try {
# Reader: Request Read access, permitting existing writers (ReadWrite)
$readStream = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
$reader = New-Object System.IO.StreamReader($readStream, [System.Text.Encoding]::UTF8)
$lockInfo = $reader.ReadToEnd()
$reader.Close()
$readStream.Close()
Write-Warning "Did not get the lock. Current owner info: $lockInfo"
} catch {
Write-Warning "Did not get the lock, and could not read lock details: $($_.Exception.Message)"
}
exit 1
}
# Lock acquired: Write ownership payload
$lockMetadata = [PSCustomObject]@{
Timestamp = (Get-Date).ToString('o')
ComputerName = $env:COMPUTERNAME
PID = $PID
} | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($lockMetadata)
$FileStream.SetLength(0) # Truncate any previous leftover data
$FileStream.Write($bytes, 0, $bytes.Length)
$FileStream.Flush() # Critical: Push bytes to disk so concurrent readers can see it
Write-Host "Lock acquired successfully. Press any key to release lock..." -ForegroundColor Green
[void]$host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
} finally {
if ($null -ne $FileStream) {
$FileStream.Dispose()
}
}
Best Practices for SMB-Based Lock Files
- Use
Dispose(): Always clean up streams in afinallyblock usingDispose()orClose()to guarantee locks release even if an unhandled exception or script cancellation occurs. - Account for Network Caching: SMB client caching (such as Directory Leases and SMB Opportunistic Locks) can sometimes delay file attribute updates between different physical machines. Keeping lock interactions simple and flushing streams immediately minimizes caching anomalies.
- Consider Atomic Creation: If you strictly want mutual exclusion during initial creation, use
[System.IO.FileMode]::CreateNew. This guarantees atomic creation and immediately throws anIOExceptionif the file already exists, eliminating race conditions before writing the lock metadata.