Private/JournalLock.ps1
|
$script:LedgerJournalLocks = [System.Collections.Generic.Dictionary[string, System.IO.FileStream]]::new( $(if ($IsWindows) { [StringComparer]::OrdinalIgnoreCase } else { [StringComparer]::Ordinal })) function Enter-LedgerJournalLock { [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$JournalPath, [ValidateRange(0, 30)] [int]$TimeoutSeconds = 30 ) if ($WhatIfPreference) { return $null } $fullPath = [System.IO.Path]::GetFullPath($PSCmdlet.GetUnresolvedProviderPathFromPSPath($JournalPath)) if (-not [System.IO.Directory]::Exists($fullPath)) { return $null # Let the caller report the missing journal. } $lockPath = Join-Path $fullPath '.write.lock' if ($script:LedgerJournalLocks.ContainsKey($lockPath)) { return $null # The outer operation owns the handle in this runspace. } $timer = [System.Diagnostics.Stopwatch]::StartNew() while ($true) { try { $stream = [System.IO.File]::Open($lockPath, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) $script:LedgerJournalLocks.Add($lockPath, $stream) return [pscustomobject]@{ Path = $lockPath; Stream = $stream } } catch [System.IO.IOException] { # Sharing violation on Windows, EWOULDBLOCK/EAGAIN on Unix. $code = $_.Exception.HResult -band 0xffff if ($code -notin 32, 11, 35) { throw } if ($timer.Elapsed.TotalSeconds -ge $TimeoutSeconds) { throw [System.TimeoutException]::new( "Journal '$fullPath' is busy. Could not acquire its write lock within $TimeoutSeconds seconds. Retry after the other writer finishes; do not delete .write.lock.", $_.Exception) } Start-Sleep -Milliseconds 100 } } } function Exit-LedgerJournalLock { [CmdletBinding()] param ([AllowNull()] [object]$Lock) if ($null -eq $Lock) { return } try { $Lock.Stream.Dispose() } finally { [void]$script:LedgerJournalLocks.Remove($Lock.Path) } # Keep the file: deleting it would allow Unix writers to lock different inodes. } |